Tidy parser, implement polymeters (#336)

* rename parser stuff to be more explicit and fit with tidal concepts. qualify all strudel function calls
* Add {,}%n polymeter support, with a few tests
This commit is contained in:
Alex McLean 2022-12-31 21:42:49 +00:00 committed by GitHub
parent c0a7173ca4
commit 8bb460701f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 549 additions and 439 deletions

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,10 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
// Some terminology: // Some terminology:
// a sequence = a serie of elements placed between quotes // mini(notation) = a series of elements placed between quotes
// a stack = a serie of vertically aligned slices sharing the same overall length // a stack = a series of vertically aligned slices sharing the same overall length
// a slice = a serie of horizontally aligned elements // a sequence = a series of horizontally aligned elements
// a choose = a serie of elements, one of which is chosen at random // a choose = a series of elements, one of which is chosen at random
{ {
@ -93,18 +93,26 @@ step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step = ws chars:step_char+ ws { return chars.join("") } step = ws chars:step_char+ ws { return chars.join("") }
// define a sub cycle e.g. [1 2, 3 [4]] // define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s} sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement // define a polymeter e.g. {1 2, 3 4 5}
timeline = ws "<" ws sc:single_cycle ws ">" ws polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
{ sc.arguments_.alignment = "t"; return sc;} { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
polymeter_steps = "%"a:number
{ return a }
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
// change the alignment to slowcat
slow_sequence = ws "<" ws s:sequence ws ">" ws
{ s.arguments_.alignment = 'slowcat'; return s; }
// a slice is either a single step or a sub cycle // a slice is either a single step or a sub cycle
slice = step / sub_cycle / timeline slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3) // slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators // at this point, we assume we can represent them as regular sequence operators
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_replicate / slice_degrade
slice_weight = "@" a:number slice_weight = "@" a:number
{ return { weight: a} } { return { weight: a} }
@ -121,9 +129,6 @@ slice_slow = "/"a:number
slice_fast = "*"a:number slice_fast = "*"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'fast' } } } } { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'fast' } } } }
slice_fixed_step = "%"a:number
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
slice_degrade = "?"a:number? slice_degrade = "?"a:number?
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } } { return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
@ -131,35 +136,42 @@ slice_degrade = "?"a:number?
slice_with_modifier = s:slice o:slice_modifier? slice_with_modifier = s:slice o:slice_modifier?
{ return new ElementStub(s, o);} { return new ElementStub(s, o);}
// a single cycle is a combination of one or more successive slices (as an array). If we // a sequence is a combination of one or more successive slices (as an array)
// have only one element, we skip the array and return the element itself sequence = s:(slice_with_modifier)+
single_cycle = s:(slice_with_modifier)+ { return new PatternStub(s, 'fastcat'); }
{ return new PatternStub(s,"h"); }
// a stack is a serie of vertically aligned single cycles, separated by a comma // a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @single_cycle)+ stack_tail = tail:(comma @sequence)+
{ return { alignment: 'v', list: tail }; } { return { alignment: 'stack', list: tail }; }
// a choose is a serie of pipe-separated single cycles, one of which is chosen // a choose is a series of pipe-separated sequence, one of which is
// at random each time through the pattern // chosen at random, each cycle
choose_tail = tail:(pipe @single_cycle)+ choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'r', list: tail }; } { return { alignment: 'rand', list: tail }; }
// if the stack contains only one element, we don't create a stack but return the // if the stack contains only one element, we don't create a stack but return the
// underlying element // underlying element
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)? stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } } { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
// a sequence is a quoted stack polymeter_stack = head:sequence tail:stack_tail?
sequence = ws quote sc:stack_or_choose quote { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
// Mini-notation innards ends
// ---------->8---------->8---------->8---------->8---------->8----------
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
{ return sc; } { return sc; }
// ------------------ operators --------------------------- // ------------------ operators ---------------------------
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
struct = "struct" ws s:sequence_or_operator struct = "struct" ws s:mini_or_operator
{ return { name: "struct", args: { sequence:s }}} { return { name: "struct", args: { mini:s }}}
target = "target" ws quote s:step quote target = "target" ws quote s:step quote
{ return { name: "target", args : { name:s}}} { return { name: "target", args : { name:s}}}
@ -189,27 +201,27 @@ comment = '//' p:([^\n]*)
group_operator = cat group_operator = cat
// cat is another form of timeline // cat is another form of timeline
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]" cat = "cat" ws "[" ws s:mini_or_operator ss:(comma v:mini_or_operator { return v})* ws "]"
{ ss.unshift(s); return new PatternStub(ss,"t"); } { ss.unshift(s); return new PatternStub(ss, 'slowcat'); }
// ------------------ high level sequence --------------------------- // ------------------ high level mini ---------------------------
sequence_or_group = mini_or_group =
group_operator / group_operator /
sequence mini
sequence_or_operator = mini_or_operator =
sg:sequence_or_group ws (comment)* sg:mini_or_group ws (comment)*
{return sg} {return sg}
/ o:operator ws "$" ws soc:sequence_or_operator / o:operator ws "$" ws soc:mini_or_operator
{ return new OperatorStub(o.name,o.args,soc)} { return new OperatorStub(o.name,o.args,soc)}
sequ_or_operator_or_comment = sequ_or_operator_or_comment =
sc: sequence_or_operator sc: mini_or_operator
{ return sc } { return sc }
/ comment / comment
sequence_definition = s:sequ_or_operator_or_comment mini_definition = s:sequ_or_operator_or_comment
// ---------------------- statements ---------------------------- // ---------------------- statements ----------------------------
@ -227,4 +239,4 @@ hush = "hush"
// ---------------------- statements ---------------------------- // ---------------------- statements ----------------------------
statement = sequence_definition / command statement = mini_definition / command

View File

@ -7,8 +7,6 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js'; import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
const { pure, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
/* var _seedState = 0; /* var _seedState = 0;
const randOffset = 0.0002; const randOffset = 0.0002;
@ -28,7 +26,7 @@ const applyOptions = (parent) => (pat, i) => {
if (!legalTypes.includes(type)) { if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`); throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
} }
return reify(pat)[type](amount); return strudel.reify(pat)[type](amount);
} }
case 'bjorklund': case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation); return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
@ -48,12 +46,12 @@ const applyOptions = (parent) => (pat, i) => {
// this is how it was: // this is how it was:
/* /*
return reify(pat)._degradeByWith( return strudel.reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1), strudel.rand.early(randOffset * _nextSeed()).segment(1),
operator.arguments_.amount ?? 0.5, operator.arguments_.amount ?? 0.5,
); );
*/ */
return reify(pat)._degradeBy(operator.arguments_.amount ?? 0.5); return strudel.reify(pat)._degradeBy(operator.arguments_.amount ?? 0.5);
// TODO: case 'fixed-step': "%" // TODO: case 'fixed-step': "%"
} }
@ -87,7 +85,7 @@ function resolveReplications(ast) {
source_: { source_: {
type_: 'pattern', type_: 'pattern',
arguments_: { arguments_: {
alignment: 'h', alignment: 'fastcat',
}, },
source_: [ source_: [
{ {
@ -113,31 +111,45 @@ export function patternifyAST(ast, code) {
resolveReplications(ast); 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));
const alignment = ast.arguments_.alignment; const alignment = ast.arguments_.alignment;
if (alignment === 'v') { if (alignment === 'stack') {
return stack(...children); return strudel.stack(...children);
} }
if (alignment === 'r') { if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = strudel.Fraction(
ast.arguments_.stepsPerCycle
? ast.arguments_.stepsPerCycle
: strudel.Fraction(children.length > 0 ? children[0].__weight : 1),
);
const aligned = children.map((child) => child.fast(stepsPerCycle.div(child.__weight || strudel.Fraction(1))));
return strudel.stack(...aligned);
}
if (alignment === 'rand') {
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422 // https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children); // return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
return strudel.chooseCycles(...children); return strudel.chooseCycles(...children);
} }
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 't') { if (!weightedChildren && alignment === 'slowcat') {
return slowcat(...children); return strudel.slowcat(...children);
} }
if (weightedChildren) { if (weightedChildren) {
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
if (alignment === 't') { const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0); if (alignment === 'slowcat') {
return pat._slow(weightSum); // timecat + slow return pat._slow(weightSum); // timecat + slow
} }
pat.__weight = weightSum;
return pat; return pat;
} }
return sequence(...children); const pat = strudel.sequence(...children);
pat.__weight = strudel.Fraction(children.length);
return pat;
} }
case 'element': { case 'element': {
if (ast.source_ === '~') { if (ast.source_ === '~') {
return silence; return strudel.silence;
} }
if (typeof ast.source_ !== 'object') { if (typeof ast.source_ !== 'object') {
if (!ast.location_) { if (!ast.location_) {
@ -153,10 +165,12 @@ export function patternifyAST(ast, code) {
const [offsetStart = 0, offsetEnd = 0] = actual const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length) ? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: []; : [];
return pure(value).withLocation( return strudel
[start.line, start.column + offsetStart, start.offset + offsetStart], .pure(value)
[start.line, end.column - offsetEnd, end.offset - offsetEnd], .withLocation(
); [start.line, start.column + offsetStart, start.offset + offsetStart],
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
);
} }
return patternifyAST(ast.source_, code); return patternifyAST(ast.source_, code);
} }
@ -183,10 +197,10 @@ export function patternifyAST(ast, code) {
}); */ }); */
/* case 'struct': /* case 'struct':
// TODO: // TODO:
return silence; */ return strudel.silence; */
default: default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`); console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return silence; return strudel.silence;
} }
} }
@ -197,7 +211,7 @@ export const mini = (...strings) => {
const ast = krill.parse(code); const ast = krill.parse(code);
return patternifyAST(ast, code); return patternifyAST(ast, code);
}); });
return sequence(...pats); return strudel.sequence(...pats);
}; };
// includes haskell style (raw krill parsing) // includes haskell style (raw krill parsing)
@ -211,5 +225,5 @@ export function minify(thing) {
if (typeof thing === 'string') { if (typeof thing === 'string') {
return mini(thing); return mini(thing);
} }
return reify(thing); return strudel.reify(thing);
} }

View File

@ -36,6 +36,16 @@ describe('mini', () => {
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']); expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']); expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
}); });
it('supports curly brackets', () => {
expect(minS('{a b, c d e}*3')).toEqual(minS('[a b a b a b, c d e c d e]'));
expect(minS('{a b, c [d e] f}*3')).toEqual(minS('[a b a b a b, c [d e] f c [d e] f]'));
expect(minS('{a b c, d e}*2')).toEqual(minS('[a b c a b c, d e d e d e]'));
});
it('supports curly brackets with explicit step-per-cycle', () => {
expect(minS('{a b, c d e}%3')).toEqual(minS('[a b a, c d e]'));
expect(minS('{a b, c d e}%5')).toEqual(minS('[a b a b a, c d e c d]'));
expect(minS('{a b, c d e}%6')).toEqual(minS('[a b a b a b, c d e c d e]'));
});
it('supports commas', () => { it('supports commas', () => {
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']); expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']); expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);