From 2fd2bdba605ecb3bcb3d9a0b7e2d134e757b0389 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 27 Mar 2024 12:06:05 +0000 Subject: [PATCH] Feature: tactus marking (#1021) * rename `beat` option to `stackBy` to `repeat` * fix parse error reporting * rename `weight` to `tactus` (it might in the end be pulse, step, or tap) * tactus marking with ^ * and add some tests --- packages/core/pattern.mjs | 121 +++++++++++----------- packages/core/test/controls.test.mjs | 12 +-- packages/core/test/pattern.test.mjs | 24 ++--- packages/mini/krill-parser.js | 42 +++++--- packages/mini/krill.pegjs | 10 +- packages/mini/mini.mjs | 99 ++++++++++++------ packages/mini/test/mini.test.mjs | 10 ++ test/__snapshots__/examples.test.mjs.snap | 21 ++++ 8 files changed, 205 insertions(+), 134 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index b1aa852f..42e59c85 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -29,22 +29,22 @@ export class Pattern { * @param {function} query - The function that maps a `State` to an array of `Hap`. * @noAutocomplete */ - constructor(query, weight = undefined) { + constructor(query, tactus = undefined) { this.query = query; this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern - this.__weight = weight; // in terms of number of beats per cycle + this.__tactus = tactus; // in terms of number of beats per cycle } - get weight() { - return this.__weight ?? Fraction(1); + get tactus() { + return this.__tactus ?? Fraction(1); } - set weight(weight) { - this.__weight = Fraction(weight); + set tactus(tactus) { + this.__tactus = Fraction(tactus); } - setWeight(weight) { - this.weight = weight; + setTactus(tactus) { + this.tactus = tactus; return this; } @@ -62,7 +62,7 @@ export class Pattern { */ withValue(func) { const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func))); - result.weight = this.weight; + result.tactus = this.tactus; return result; } @@ -130,7 +130,7 @@ export class Pattern { return span_a.intersection_e(span_b); }; const result = pat_func.appWhole(whole_func, pat_val); - result.weight = lcm(pat_val.weight, pat_func.weight); + result.tactus = lcm(pat_val.tactus, pat_func.tactus); return result; } @@ -165,7 +165,7 @@ export class Pattern { return haps; }; const result = new Pattern(query); - result.weight = this.weight; + result.tactus = this.tactus; return result; } @@ -198,7 +198,7 @@ export class Pattern { return haps; }; const result = new Pattern(query); - result.weight = pat_val.weight; + result.tactus = pat_val.tactus; return result; } @@ -452,7 +452,7 @@ export class Pattern { */ withHaps(func) { const result = new Pattern((state) => func(this.query(state), state)); - result.weight = this.weight; + result.tactus = this.tactus; return result; } @@ -1160,13 +1160,13 @@ Pattern.prototype.factories = { // Elemental patterns /** - * Does absolutely nothing, with a given metrical 'weight' + * Does absolutely nothing, but with a given metrical 'tactus' * @name gap - * @param {number} weight + * @param {number} tactus * @example * gap(3) // "~@3" */ -export const gap = (weight) => new Pattern(() => [], Fraction(weight)); +export const gap = (tactus) => new Pattern(() => [], Fraction(tactus)); /** * Does absolutely nothing.. @@ -1176,7 +1176,7 @@ export const gap = (weight) => new Pattern(() => [], Fraction(weight)); */ export const silence = gap(1); -/* Like silence, but with a 'weight' (relative duration) of 0 */ +/* Like silence, but with a 'tactus' (relative duration) of 0 */ export const nothing = gap(0); /** A discrete value that repeats once per cycle. @@ -1235,7 +1235,7 @@ export function stack(...pats) { pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); const query = (state) => flatten(pats.map((pat) => pat.query(state))); const result = new Pattern(query); - result.weight = lcm(...pats.map((pat) => pat.weight)); + result.tactus = lcm(...pats.map((pat) => pat.tactus)); return result; } @@ -1247,33 +1247,33 @@ function _stackWith(func, pats) { if (pats.length === 1) { return pats[0]; } - const [left, ...right] = pats.map((pat) => pat.weight); - const weight = left.maximum(...right); - return stack(...func(weight, pats)); + const [left, ...right] = pats.map((pat) => pat.tactus); + const tactus = left.maximum(...right); + return stack(...func(tactus, pats)); } export function stackLeft(...pats) { return _stackWith( - (weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(pat, gap(weight.sub(pat.weight))))), + (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timeCat(pat, gap(tactus.sub(pat.tactus))))), pats, ); } export function stackRight(...pats) { return _stackWith( - (weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(gap(weight.sub(pat.weight)), pat))), + (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timeCat(gap(tactus.sub(pat.tactus)), pat))), pats, ); } export function stackCentre(...pats) { return _stackWith( - (weight, pats) => + (tactus, pats) => pats.map((pat) => { - if (pat.weight.eq(weight)) { + if (pat.tactus.eq(tactus)) { return pat; } - const g = gap(weight.sub(pat.weight).div(2)); + const g = gap(tactus.sub(pat.tactus).div(2)); return timeCat(g, pat, g); }), pats, @@ -1281,20 +1281,20 @@ export function stackCentre(...pats) { } export function stackBy(by, ...pats) { - const [left, ...right] = pats.map((pat) => pat.weight); - const weight = left.maximum(...right); + const [left, ...right] = pats.map((pat) => pat.tactus); + const tactus = left.maximum(...right); const lookup = { centre: stackCentre, left: stackLeft, right: stackRight, expand: stack, - beat: (...args) => polymeterSteps(weight, ...args), + repeat: (...args) => polymeterSteps(tactus, ...args), }; return by .inhabit(lookup) .fmap((func) => func(...pats)) .innerJoin() - .setWeight(weight); + .setTactus(tactus); } /** Concatenation: combines a list of patterns, switching between them successively, one per cycle: @@ -1361,8 +1361,7 @@ export function cat(...pats) { /** Sequences patterns like `seq`, but each pattern has a length, relative to the whole. * This length can either be provided as a [length, pattern] pair, or inferred from - * mininotation as the number of toplevel steps. The latter only works if the mininotation - * hasn't first been modified by another function. + * the pattern's 'tactus', generally inferred by the mininotation. * @return {Pattern} * @example * timeCat([3,"e3"],[1, "g3"]).note() @@ -1372,13 +1371,11 @@ export function cat(...pats) { * // the same as "bd sd cp hh hh".sound() */ export function timeCat(...timepats) { - // Weights may either be provided explicitly in [weight, pattern] pairs, or - // where possible, inferred from the pattern. - const findWeight = (x) => (Array.isArray(x) ? x : [x.weight, x]); - timepats = timepats.map(findWeight); + const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]); + timepats = timepats.map(findtactus); if (timepats.length == 1) { const result = reify(timepats[0][1]); - result.weight = timepats[0][0]; + result.tactus = timepats[0][0]; return result; } @@ -1391,7 +1388,7 @@ export function timeCat(...timepats) { begin = end; } const result = stack(...pats); - result.weight = total; + result.tactus = total; return result; } @@ -1416,7 +1413,7 @@ export function fastcat(...pats) { let result = slowcat(...pats); if (pats.length > 1) { result = result._fast(pats.length); - result.weight = pats.length; + result.tactus = pats.length; } return result; } @@ -1437,10 +1434,10 @@ export function beatCat(...groups) { for (let cycle = 0; cycle < cycles; ++cycle) { result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length]))); } - result = result.filter((x) => x.weight > 0); - const weight = result.reduce((a, b) => a.add(b.weight), Fraction(0)); + result = result.filter((x) => x.tactus > 0); + const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0)); result = timeCat(...result); - result.weight = weight; + result.tactus = tactus; return result; } @@ -1474,13 +1471,13 @@ function _sequenceCount(x) { } /** - * Speeds a pattern up or down, to fit to the given metrical 'weight'. + * Speeds a pattern up or down, to fit to the given metrical 'tactus'. * @example - * s("bd sd cp").reweight(4) + * s("bd sd cp").toTactus(4) * // The same as s("{bd sd cp}%4") */ -export const reweight = register('reweight', function (targetWeight, pat) { - return pat.fast(Fraction(targetWeight).div(pat.weight)); +export const toTactus = register('toTactus', function (targetTactus, pat) { + return pat.fast(Fraction(targetTactus).div(pat.tactus)); }); export function _polymeterListSteps(steps, ...args) { @@ -1525,7 +1522,7 @@ export function polymeterSteps(steps, ...args) { return _polymeterListSteps(steps, ...args); } - return polymeter(...args).reweight(steps); + return polymeter(...args).toTactus(steps); } /** @@ -1545,11 +1542,11 @@ export function polymeter(...args) { if (args.length == 0) { return silence; } - const weight = args[0].weight; + const tactus = args[0].tactus; const [head, ...tail] = args; - const result = stack(head, ...tail.map((pat) => pat._slow(pat.weight.div(weight)))); - result.weight = weight; + const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus)))); + result.tactus = tactus; return result; } @@ -1592,7 +1589,7 @@ export const func = curry((a, b) => reify(b).func(a)); * @noAutocomplete * */ -export function register(name, func, patternify = true, preserveWeight = false) { +export function register(name, func, patternify = true, preserveTactus = false) { if (Array.isArray(name)) { const result = {}; for (const name_item of name) { @@ -1632,8 +1629,8 @@ export function register(name, func, patternify = true, preserveWeight = false) result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin(); } } - if (preserveWeight) { - result.weight = pat.weight; + if (preserveTactus) { + result.tactus = pat.tactus; } return result; }; @@ -1641,8 +1638,8 @@ export function register(name, func, patternify = true, preserveWeight = false) pfunc = function (...args) { args = args.map(reify); const result = func(...args); - if (preserveWeight) { - result.weight = args[args.length - 1].weight; + if (preserveTactus) { + result.tactus = args[args.length - 1].tactus; } return result; }; @@ -1880,7 +1877,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun */ export const ply = register('ply', function (factor, pat) { const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); - result.weight = pat.weight.mul(factor); + result.tactus = pat.tactus.mul(factor); return result; }); @@ -1902,7 +1899,7 @@ export const { fast, density } = register(['fast', 'density'], function (factor, factor = Fraction(factor); const fastQuery = pat.withQueryTime((t) => t.mul(factor)); const result = fastQuery.withHapTime((t) => t.div(factor)); - result.weight = factor.mul(pat.weight); + result.tactus = factor.mul(pat.tactus); return result; }); @@ -2106,7 +2103,7 @@ export const linger = register( * note(saw.range(40,52).segment(24)) */ export const segment = register('segment', function (rate, pat) { - return pat.struct(pure(true)._fast(rate)).setWeight(rate); + return pat.struct(pure(true)._fast(rate)).setTactus(rate); }); /** @@ -2264,7 +2261,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }))); - return stack(left, right).setWeight(lcm(left.weight, right.weight)); + return stack(left, right).setTactus(lcm(left.tactus, right.tactus)); }); /** @@ -2510,7 +2507,7 @@ export const chop = register('chop', function (n, pat) { const func = function (o) { return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); }; - return pat.squeezeBind(func).setWeight(pat.weight.mul(n)); + return pat.squeezeBind(func).setTactus(pat.tactus.mul(n)); }); /** @@ -2574,7 +2571,7 @@ export const slice = register( }), ), ) - .setWeight(ipat.weight); + .setTactus(ipat.tactus); }, false, // turns off auto-patternification ); @@ -2605,7 +2602,7 @@ export const splice = register( ...v, })), ); - }).setWeight(ipat.weight); + }).setTactus(ipat.tactus); }, false, // turns off auto-patternification ); diff --git a/packages/core/test/controls.test.mjs b/packages/core/test/controls.test.mjs index 387e506f..3b926685 100644 --- a/packages/core/test/controls.test.mjs +++ b/packages/core/test/controls.test.mjs @@ -30,13 +30,13 @@ describe('controls', () => { expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]); expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]); }); - it('preserves weight of the left pattern', () => { - expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).weight).toEqual(Fraction(3)); + it('preserves tactus of the left pattern', () => { + expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3)); }); - it('preserves weight of the right pattern for .out', () => { - expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(4)); + it('preserves tactus of the right pattern for .out', () => { + expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4)); }); - it('combines weight of the pattern for .mix as lcm', () => { - expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(12)); + it('combines tactus of the pattern for .mix as lcm', () => { + expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12)); }); }); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index e112a95f..f825c4df 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1119,21 +1119,21 @@ describe('Pattern', () => { ); }); }); - describe('weight', () => { + describe('tactus', () => { it('Is correctly preserved/calculated through transformations', () => { - expect(sequence(0, 1, 2, 3).linger(4).weight).toStrictEqual(Fraction(4)); - expect(sequence(0, 1, 2, 3).iter(4).weight).toStrictEqual(Fraction(4)); - expect(sequence(0, 1, 2, 3).fast(4).weight).toStrictEqual(Fraction(16)); - expect(sequence(0, 1, 2, 3).hurry(4).weight).toStrictEqual(Fraction(16)); - expect(sequence(0, 1, 2, 3).rev().weight).toStrictEqual(Fraction(4)); - expect(sequence(1).segment(10).weight).toStrictEqual(Fraction(10)); - expect(sequence(1, 0, 1).invert().weight).toStrictEqual(Fraction(3)); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).weight).toStrictEqual(Fraction(8)); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).weight).toStrictEqual(Fraction(8)); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual( + expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4)); + expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4)); + expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(16)); + expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16)); + expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4)); + expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10)); + expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3)); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8)); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8)); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual( Fraction(4), ); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual( + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual( Fraction(4), ); }); diff --git a/packages/mini/krill-parser.js b/packages/mini/krill-parser.js index b482d96c..7762242d 100644 --- a/packages/mini/krill-parser.js +++ b/packages/mini/krill-parser.js @@ -308,11 +308,11 @@ function peg$parse(input, options) { } return result; }; - var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); }; + var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); }; var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; }; var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; }; - var peg$f21 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; + var peg$f21 = function(head, tail) {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; var peg$f23 = function(sc) { return sc; }; var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}}; @@ -1477,24 +1477,36 @@ function peg$parse(input, options) { } function peg$parsesequence() { - var s0, s1, s2; + var s0, s1, s2, s3; s0 = peg$currPos; - s1 = []; - s2 = peg$parseslice_with_ops(); - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseslice_with_ops(); - } + if (input.charCodeAt(peg$currPos) === 94) { + s1 = peg$c9; + peg$currPos++; } else { s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e17); } } - if (s1 !== peg$FAILED) { + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = []; + s3 = peg$parseslice_with_ops(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseslice_with_ops(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f17(s1); + s0 = peg$f17(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; } - s0 = s1; return s0; } @@ -2476,10 +2488,10 @@ function peg$parse(input, options) { this.location_ = location(); } - var PatternStub = function(source, alignment, seed) + var PatternStub = function(source, alignment, seed, tactus) { this.type_ = "pattern"; - this.arguments_ = { alignment: alignment }; + this.arguments_ = { alignment: alignment, tactus: tactus }; if (seed !== undefined) { this.arguments_.seed = seed; } diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index 18764a34..35d7bdc4 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th this.location_ = location(); } - var PatternStub = function(source, alignment, seed) + var PatternStub = function(source, alignment, seed, tactus) { this.type_ = "pattern"; - this.arguments_ = { alignment: alignment }; + this.arguments_ = { alignment: alignment, tactus: tactus }; if (seed !== undefined) { this.arguments_.seed = seed; } @@ -165,8 +165,8 @@ slice_with_ops = s:slice ops:slice_op* } // a sequence is a combination of one or more successive slices (as an array) -sequence = s:(slice_with_ops)+ - { return new PatternStub(s, 'fastcat'); } +sequence = tactus:'^'? s:(slice_with_ops)+ + { return new PatternStub(s, 'fastcat', undefined, !!tactus); } // a stack is a series of vertically aligned sequence, separated by a comma stack_tail = tail:(comma @sequence)+ @@ -184,7 +184,7 @@ dot_tail = tail:(dot @sequence)+ // if the stack contains only one element, we don't create a stack but return the // underlying element stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)? - { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } + {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } polymeter_stack = head:sequence tail:stack_tail? { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); } diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index c032cf12..8c893273 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import * as krill from './krill-parser.js'; import * as strudel from '@strudel/core'; +import Fraction, { lcm } from '@strudel/core/fraction.mjs'; const randOffset = 0.0003; @@ -88,45 +89,75 @@ export function patternifyAST(ast, code, onEnter, offset = 0) { resolveReplications(ast); const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter)); const alignment = ast.arguments_.alignment; - if (alignment === 'stack') { - return strudel.stack(...children); - } - if (alignment === 'polymeter_slowcat') { - const aligned = children.map((child) => child._slow(child.weight)); - return strudel.stack(...aligned); - } - if (alignment === 'polymeter') { - // polymeter - const stepsPerCycle = ast.arguments_.stepsPerCycle - ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) - : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].weight : 1)); + const with_tactus = children.filter((child) => child.__tactus_source); + let pat; + switch (alignment) { + case 'stack': { + pat = strudel.stack(...children); + if (with_tactus.length) { + pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus))); + } + break; + } + case 'polymeter_slowcat': { + pat = strudel.stack(...children.map((child) => child._slow(child.__weight))); + if (with_tactus.length) { + pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus))); + } + break; + } + case 'polymeter': { + // polymeter + const stepsPerCycle = ast.arguments_.stepsPerCycle + ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) + : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1)); - const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.weight)))); - return strudel.stack(...aligned); + const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight)))); + pat = strudel.stack(...aligned); + break; + } + case 'rand': { + pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); + if (with_tactus.length) { + pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus))); + } + break; + } + case 'feet': { + pat = strudel.fastcat(...children); + break; + } + default: { + const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); + if (weightedChildren) { + const weightSum = ast.source_.reduce( + (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), + strudel.Fraction(0), + ); + pat = strudel.timeCat( + ...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]), + ); + pat.__weight = weightSum; // for polymeter + pat.tactus = weightSum; + if (with_tactus.length) { + pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus)))); + } + } else { + pat = strudel.sequence(...children); + pat.tactus = children.length; + } + if (ast.arguments_.tactus) { + pat.__tactus_source = true; + } + } } - if (alignment === 'rand') { - return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); + if (with_tactus.length) { + pat.__tactus_source = true; } - if (alignment === 'feet') { - return strudel.fastcat(...children); - } - const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); - if (weightedChildren) { - const weightSum = ast.source_.reduce( - (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), - strudel.Fraction(0), - ); - const pat = strudel.timeCat( - ...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]), - ); - pat.weight = weightSum; - return pat; - } - const pat = strudel.sequence(...children); - pat.weight = children.length; return pat; } case 'element': { + 1; return enter(ast.source_); } case 'atom': { @@ -166,7 +197,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => { }; // takes quoted mini string, returns ast -export const mini2ast = (code, start, userCode) => { +export const mini2ast = (code, start = 0, userCode = code) => { try { return krill.parse(code); } catch (error) { diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 647512a8..92aa6c12 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs'; import '@strudel/core/euclid.mjs'; +import { Fraction } from '@strudel/core/index.mjs'; import { describe, expect, it } from 'vitest'; describe('mini', () => { @@ -207,6 +208,15 @@ describe('mini', () => { it('_ and @ are almost interchangeable', () => { expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3')); }); + it('supports ^ tactus marking', () => { + expect(mini('a [^b c]').tactus).toEqual(Fraction(4)); + expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2)); + expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2)); + expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8)); + expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4)); + expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12)); + expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24)); + }); }); describe('getLeafLocation', () => { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 115a9ef1..a1b2f4a0 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7357,6 +7357,27 @@ exports[`runs examples > example "timeCat" example index 1 1`] = ` ] `; +exports[`runs examples > example "toTactus" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 1/4 → 1/2 | s:sd ]", + "[ 1/2 → 3/4 | s:cp ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 1/1 → 5/4 | s:sd ]", + "[ 5/4 → 3/2 | s:cp ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 7/4 → 2/1 | s:sd ]", + "[ 2/1 → 9/4 | s:cp ]", + "[ 9/4 → 5/2 | s:bd ]", + "[ 5/2 → 11/4 | s:sd ]", + "[ 11/4 → 3/1 | s:cp ]", + "[ 3/1 → 13/4 | s:bd ]", + "[ 13/4 → 7/2 | s:sd ]", + "[ 7/2 → 15/4 | s:cp ]", + "[ 15/4 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]",