mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 05:38:34 +00:00
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
This commit is contained in:
parent
2e8ae38d33
commit
2fd2bdba60
@ -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
|
||||
);
|
||||
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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'); }
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 ]",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user