Patternify euclid, fast, slow and polymeter step parameters in mininotation (#341)
* patternify the mininotation bjorklund, and fast (*) slow (/) and pattern step (%) parameters * simplify replicate (!), should be closer to tidal now * highlight atoms rather than elements * tests
This commit is contained in:
parent
1a6b649fd6
commit
db7ef01dbb
7 changed files with 740 additions and 113 deletions
|
|
@ -269,17 +269,17 @@ function peg$parse(input, options) {
|
|||
var peg$e47 = peg$literalExpectation("hush", false);
|
||||
|
||||
var peg$f0 = function() { return parseFloat(text()); };
|
||||
var peg$f1 = function(chars) { return chars.join("") };
|
||||
var peg$f1 = function(chars) { return new AtomStub(chars.join("")) };
|
||||
var peg$f2 = function(s) { return s };
|
||||
var peg$f3 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
|
||||
var peg$f4 = function(a) { return a };
|
||||
var peg$f5 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
|
||||
var peg$f6 = function(a) { return { weight: a} };
|
||||
var peg$f7 = function(a) { return { replicate: a } };
|
||||
var peg$f8 = function(p, s, r) { return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } };
|
||||
var peg$f7 = function(a) { return { replicate: a } };
|
||||
var peg$f8 = function(p, s, r) { return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r } } } };
|
||||
var peg$f9 = function(a) { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'slow' } } } };
|
||||
var peg$f10 = function(a) { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'fast' } } } };
|
||||
var peg$f11 = function(a) { return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } };
|
||||
var peg$f11 = function(a) { return { operator : { type_: "degradeBy", arguments_ :{ amount:a } } } };
|
||||
var peg$f12 = function(s, o) { return new ElementStub(s, o);};
|
||||
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
|
|
@ -289,7 +289,7 @@ function peg$parse(input, options) {
|
|||
var peg$f18 = function(sc) { return sc; };
|
||||
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}};
|
||||
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
|
|
@ -992,7 +992,7 @@ function peg$parse(input, options) {
|
|||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f4(s2);
|
||||
|
|
@ -1161,13 +1161,13 @@ function peg$parse(input, options) {
|
|||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
s3 = peg$parseslice_with_modifier();
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
s5 = peg$parsecomma();
|
||||
if (s5 !== peg$FAILED) {
|
||||
s6 = peg$parsews();
|
||||
s7 = peg$parsenumber();
|
||||
s7 = peg$parseslice_with_modifier();
|
||||
if (s7 !== peg$FAILED) {
|
||||
s8 = peg$parsews();
|
||||
s9 = peg$parsecomma();
|
||||
|
|
@ -1175,7 +1175,7 @@ function peg$parse(input, options) {
|
|||
s9 = null;
|
||||
}
|
||||
s10 = peg$parsews();
|
||||
s11 = peg$parsenumber();
|
||||
s11 = peg$parseslice_with_modifier();
|
||||
if (s11 === peg$FAILED) {
|
||||
s11 = null;
|
||||
}
|
||||
|
|
@ -1226,7 +1226,7 @@ function peg$parse(input, options) {
|
|||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f9(s2);
|
||||
|
|
@ -1254,7 +1254,7 @@ function peg$parse(input, options) {
|
|||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f10(s2);
|
||||
|
|
@ -2148,6 +2148,13 @@ function peg$parse(input, options) {
|
|||
}
|
||||
|
||||
|
||||
var AtomStub = function(source)
|
||||
{
|
||||
this.type_ = "atom";
|
||||
this.source_ = source;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
|
||||
|
||||
{
|
||||
var AtomStub = function(source)
|
||||
{
|
||||
this.type_ = "atom";
|
||||
this.source_ = source;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
|
|
@ -90,7 +97,7 @@ quote = '"' / "'"
|
|||
|
||||
// single step definition (e.g bd)
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||
step = ws chars:step_char+ ws { return chars.join("") }
|
||||
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||
|
||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
|
||||
|
|
@ -99,7 +106,7 @@ sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
|
|||
polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
|
||||
{ s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
|
||||
|
||||
polymeter_steps = "%"a:number
|
||||
polymeter_steps = "%"a:slice
|
||||
{ return a }
|
||||
|
||||
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
|
||||
|
|
@ -118,19 +125,19 @@ slice_weight = "@" a:number
|
|||
{ return { weight: a} }
|
||||
|
||||
slice_replicate = "!"a:number
|
||||
{ return { replicate: a } }
|
||||
{ return { replicate: a } }
|
||||
|
||||
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
|
||||
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
|
||||
slice_bjorklund = "(" ws p:slice_with_modifier ws comma ws s:slice_with_modifier ws comma? ws r:slice_with_modifier? ws ")"
|
||||
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r } } } }
|
||||
|
||||
slice_slow = "/"a:number
|
||||
slice_slow = "/"a:slice
|
||||
{ return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'slow' } } } }
|
||||
|
||||
slice_fast = "*"a:number
|
||||
slice_fast = "*"a:slice
|
||||
{ return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'fast' } } } }
|
||||
|
||||
slice_degrade = "?"a:number?
|
||||
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
|
||||
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:a } } } }
|
||||
|
||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||
slice_with_modifier = s:slice o:slice_modifier?
|
||||
|
|
@ -177,7 +184,7 @@ target = "target" ws quote s:step quote
|
|||
{ return { name: "target", args : { name:s}}}
|
||||
|
||||
bjorklund = "euclid" ws p:int ws s:int ws r:int?
|
||||
{ return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}}
|
||||
{ return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}
|
||||
|
||||
slow = "slow" ws a:number
|
||||
{ return { name: "stretch", args :{ amount: a}}}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function _nextSeed() {
|
|||
return _seedState++;
|
||||
} */
|
||||
|
||||
const applyOptions = (parent) => (pat, i) => {
|
||||
const applyOptions = (parent, code) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const operator = options?.operator;
|
||||
|
|
@ -26,10 +26,21 @@ const applyOptions = (parent) => (pat, i) => {
|
|||
if (!legalTypes.includes(type)) {
|
||||
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||
}
|
||||
return strudel.reify(pat)[type](amount);
|
||||
return strudel.reify(pat)[type](patternifyAST(amount, code));
|
||||
}
|
||||
case 'bjorklund':
|
||||
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
|
||||
if (operator.arguments_.rotation) {
|
||||
return pat.euclidRot(
|
||||
patternifyAST(operator.arguments_.pulse, code),
|
||||
patternifyAST(operator.arguments_.step, code),
|
||||
patternifyAST(operator.arguments_.rotation, code),
|
||||
);
|
||||
} else {
|
||||
return pat.euclid(
|
||||
patternifyAST(operator.arguments_.pulse, code),
|
||||
patternifyAST(operator.arguments_.step, code),
|
||||
);
|
||||
}
|
||||
case 'degradeBy':
|
||||
// TODO: find out what is right here
|
||||
// example:
|
||||
|
|
@ -51,9 +62,7 @@ const applyOptions = (parent) => (pat, i) => {
|
|||
operator.arguments_.amount ?? 0.5,
|
||||
);
|
||||
*/
|
||||
return strudel.reify(pat)._degradeBy(operator.arguments_.amount ?? 0.5);
|
||||
|
||||
// TODO: case 'fixed-step': "%"
|
||||
return strudel.reify(pat).degradeBy(operator.arguments_.amount === null ? 0.5 : operator.arguments_.amount);
|
||||
}
|
||||
console.warn(`operator "${operator.type_}" not implemented`);
|
||||
}
|
||||
|
|
@ -72,57 +81,34 @@ const applyOptions = (parent) => (pat, i) => {
|
|||
};
|
||||
|
||||
function resolveReplications(ast) {
|
||||
// the general idea here: x!3 = [x*3]@3
|
||||
// could this be made easier?!
|
||||
ast.source_ = ast.source_.map((child) => {
|
||||
const { replicate, ...options } = child.options_ || {};
|
||||
if (!replicate) {
|
||||
return child;
|
||||
}
|
||||
return {
|
||||
...child,
|
||||
options_: { ...options, weight: replicate },
|
||||
source_: {
|
||||
type_: 'pattern',
|
||||
arguments_: {
|
||||
alignment: 'fastcat',
|
||||
},
|
||||
source_: [
|
||||
{
|
||||
type_: 'element',
|
||||
source_: child.source_,
|
||||
location_: child.location_,
|
||||
options_: {
|
||||
operator: {
|
||||
type_: 'stretch',
|
||||
arguments_: { amount: replicate, type: 'fast' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
ast.source_ = strudel.flatten(
|
||||
ast.source_.map((child) => {
|
||||
const { replicate, ...options } = child.options_ || {};
|
||||
if (!replicate) {
|
||||
return [child];
|
||||
}
|
||||
delete child.options_.replicate;
|
||||
return Array(replicate).fill(child);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function patternifyAST(ast, code) {
|
||||
switch (ast.type_) {
|
||||
case 'pattern': {
|
||||
resolveReplications(ast);
|
||||
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast));
|
||||
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
|
||||
const alignment = ast.arguments_.alignment;
|
||||
if (alignment === 'stack') {
|
||||
return strudel.stack(...children);
|
||||
}
|
||||
if (alignment === 'polymeter') {
|
||||
// polymeter
|
||||
const stepsPerCycle = strudel.Fraction(
|
||||
ast.arguments_.stepsPerCycle
|
||||
? ast.arguments_.stepsPerCycle
|
||||
: strudel.Fraction(children.length > 0 ? children[0].__weight : 1),
|
||||
);
|
||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
|
||||
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||
|
||||
const aligned = children.map((child) => child.fast(stepsPerCycle.div(child.__weight || strudel.Fraction(1))));
|
||||
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
||||
return strudel.stack(...aligned);
|
||||
}
|
||||
if (alignment === 'rand') {
|
||||
|
|
@ -144,38 +130,38 @@ export function patternifyAST(ast, code) {
|
|||
return pat;
|
||||
}
|
||||
const pat = strudel.sequence(...children);
|
||||
pat.__weight = strudel.Fraction(children.length);
|
||||
pat.__weight = children.length;
|
||||
return pat;
|
||||
}
|
||||
case 'element': {
|
||||
return patternifyAST(ast.source_, code);
|
||||
}
|
||||
case 'atom': {
|
||||
if (ast.source_ === '~') {
|
||||
return strudel.silence;
|
||||
}
|
||||
if (typeof ast.source_ !== 'object') {
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const { start, end } = ast.location_;
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
// the following line expects the shapeshifter append .withMiniLocation
|
||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||
// make sure whitespaces are not part of the highlight:
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return strudel
|
||||
.pure(value)
|
||||
.withLocation(
|
||||
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||
);
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
return patternifyAST(ast.source_, code);
|
||||
const { start, end } = ast.location_;
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
// the following line expects the shapeshifter append .withMiniLocation
|
||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||
// make sure whitespaces are not part of the highlight:
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return strudel
|
||||
.pure(value)
|
||||
.withLocation(
|
||||
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||
);
|
||||
}
|
||||
case 'stretch':
|
||||
return patternifyAST(ast.source_, code).slow(ast.arguments_.amount);
|
||||
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
|
||||
/* case 'scale':
|
||||
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
||||
const intervals = Scale.get(scale).intervals;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,21 @@ describe('mini', () => {
|
|||
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
|
||||
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
||||
});
|
||||
it('supports fast', () => {
|
||||
expect(minS('a*3 b')).toEqual(minS('[a a a] b'));
|
||||
});
|
||||
it('supports patterned fast', () => {
|
||||
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
|
||||
});
|
||||
it('supports slow', () => {
|
||||
expect(minS('[a a a]/3 b')).toEqual(minS('a b'));
|
||||
});
|
||||
it('supports patterned slow', () => {
|
||||
expect(minS('[a a a a a a a a]/[2 4]')).toEqual(minS('[a a] a'));
|
||||
});
|
||||
it('supports patterned fast', () => {
|
||||
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
|
||||
});
|
||||
it('supports slowcat', () => {
|
||||
expect(minV('<a b>')).toEqual(['a']);
|
||||
});
|
||||
|
|
@ -56,10 +71,14 @@ describe('mini', () => {
|
|||
});
|
||||
it('supports replication', () => {
|
||||
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
|
||||
});
|
||||
it('supports euclidean rhythms', () => {
|
||||
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||
});
|
||||
it('supports patterning euclidean rhythms', () => {
|
||||
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
|
||||
});
|
||||
it('supports the ? operator', () => {
|
||||
expect(
|
||||
mini('a?')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue