Beat-oriented functionality (#976)

* annotate pure values with their value, allowing single mininotation values to maintain their labels as pure
* Don't use any 'patternified' arguments if they're all 'pure'
* allow pattern weights (roughly, beats-per-cycle) to be inferred where possible, including from mininotation and across many transformations (e.g. `fast` with a 'pure' factor)
* Add `beatCat`, similar to `timeCat` but funkier
* `silence` has a weight of 1, add alternative `nothing` with a weight of 0, and `gap` function with weight argument
* preserve weight across applicative operations (weight comes with the structure)
* add `stack` alternatives that take advantage of pattern weights to align patterns differently - `stackLeft`, `stackRight`, `stackCentre`, `stackExpand`, with `stackBy` with an argument for patterning the alignment.
This commit is contained in:
Alex McLean 2024-03-16 17:24:37 +00:00 committed by GitHub
parent d102e1dbbc
commit 398533877c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1391 additions and 1381 deletions

View File

@ -51,6 +51,11 @@ Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other; return this.gt(other) ? this : other;
}; };
Fraction.prototype.maximum = function (...others) {
others = others.map((x) => new Fraction(x));
return others.reduce((max, other) => other.max(max), this);
};
Fraction.prototype.min = function (other) { Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other; return this.lt(other) ? this : other;
}; };
@ -83,6 +88,10 @@ export const gcd = (...fractions) => {
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1)); return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
}; };
export const lcm = (...fractions) => {
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
};
fraction._original = Fraction; fraction._original = Fraction;
export default fraction; export default fraction;

View File

@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import TimeSpan from './timespan.mjs'; import TimeSpan from './timespan.mjs';
import Fraction from './fraction.mjs'; import Fraction, { lcm } from './fraction.mjs';
import Hap from './hap.mjs'; import Hap from './hap.mjs';
import State from './state.mjs'; import State from './state.mjs';
import { unionWithObj } from './value.mjs'; import { unionWithObj } from './value.mjs';
@ -29,9 +29,23 @@ export class Pattern {
* @param {function} query - The function that maps a `State` to an array of `Hap`. * @param {function} query - The function that maps a `State` to an array of `Hap`.
* @noAutocomplete * @noAutocomplete
*/ */
constructor(query) { constructor(query, weight = undefined) {
this.query = query; 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._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
}
get weight() {
return this.__weight ?? Fraction(1);
}
set weight(weight) {
this.__weight = Fraction(weight);
}
setWeight(weight) {
this.weight = weight;
return this;
} }
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@ -47,7 +61,9 @@ export class Pattern {
* "0 1 2".withValue(v => v + 10).log() * "0 1 2".withValue(v => v + 10).log()
*/ */
withValue(func) { withValue(func) {
return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func))); const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
result.weight = this.weight;
return result;
} }
/** /**
@ -104,6 +120,8 @@ export class Pattern {
* @returns Pattern * @returns Pattern
*/ */
appBoth(pat_val) { appBoth(pat_val) {
const pat_func = this;
// Tidal's <*> // Tidal's <*>
const whole_func = function (span_a, span_b) { const whole_func = function (span_a, span_b) {
if (span_a == undefined || span_b == undefined) { if (span_a == undefined || span_b == undefined) {
@ -111,7 +129,9 @@ export class Pattern {
} }
return span_a.intersection_e(span_b); return span_a.intersection_e(span_b);
}; };
return this.appWhole(whole_func, pat_val); const result = pat_func.appWhole(whole_func, pat_val);
result.weight = lcm(pat_val.weight, pat_func.weight);
return result;
} }
/** /**
@ -144,7 +164,9 @@ export class Pattern {
} }
return haps; return haps;
}; };
return new Pattern(query); const result = new Pattern(query);
result.weight = this.weight;
return result;
} }
/** /**
@ -175,7 +197,9 @@ export class Pattern {
} }
return haps; return haps;
}; };
return new Pattern(query); const result = new Pattern(query);
result.weight = pat_val.weight;
return result;
} }
bindWhole(choose_whole, func) { bindWhole(choose_whole, func) {
@ -457,7 +481,11 @@ export class Pattern {
* @noAutocomplete * @noAutocomplete
*/ */
withContext(func) { withContext(func) {
return this.withHap((hap) => hap.setContext(func(hap.context))); const result = this.withHap((hap) => hap.setContext(func(hap.context)));
if (this.__pure !== undefined) {
result.__pure = this.__pure;
}
return result;
} }
/** /**
@ -1123,13 +1151,25 @@ Pattern.prototype.factories = {
// Elemental patterns // Elemental patterns
/**
* Does absolutely nothing, with a given metrical 'weight'
* @name gap
* @param {number} weight
* @example
* gap(3) // "~@3"
*/
export const gap = (weight) => new Pattern(() => [], Fraction(weight));
/** /**
* Does absolutely nothing.. * Does absolutely nothing..
* @name silence * @name silence
* @example * @example
* silence // "~" * silence // "~"
*/ */
export const silence = new Pattern(() => []); export const silence = gap(1);
/* Like silence, but with a 'weight' (relative duration) of 0 */
export const nothing = gap(0);
/** A discrete value that repeats once per cycle. /** A discrete value that repeats once per cycle.
* *
@ -1142,7 +1182,9 @@ export function pure(value) {
function query(state) { function query(state) {
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value)); return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
} }
return new Pattern(query); const result = new Pattern(query, 1);
result.__pure = value;
return result;
} }
export function isPattern(thing) { export function isPattern(thing) {
@ -1184,7 +1226,67 @@ export function stack(...pats) {
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
const query = (state) => flatten(pats.map((pat) => pat.query(state))); const query = (state) => flatten(pats.map((pat) => pat.query(state)));
return new Pattern(query); const result = new Pattern(query);
result.weight = lcm(...pats.map((pat) => pat.weight));
return result;
}
function _stackWith(func, pats) {
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
if (pats.length === 0) {
return silence;
}
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));
}
export function stackLeft(...pats) {
return _stackWith(
(weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(pat, gap(weight.sub(pat.weight))))),
pats,
);
}
export function stackRight(...pats) {
return _stackWith(
(weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(gap(weight.sub(pat.weight)), pat))),
pats,
);
}
export function stackCentre(...pats) {
return _stackWith(
(weight, pats) =>
pats.map((pat) => {
if (pat.weight.eq(weight)) {
return pat;
}
const g = gap(weight.sub(pat.weight).div(2));
return timeCat(g, pat, g);
}),
pats,
);
}
export function stackBy(by, ...pats) {
const [left, ...right] = pats.map((pat) => pat.weight);
const weight = left.maximum(...right);
const lookup = {
centre: stackCentre,
left: stackLeft,
right: stackRight,
expand: stack,
beat: (...args) => polymeterSteps(weight, ...args),
};
return by
.inhabit(lookup)
.fmap((func) => func(...pats))
.innerJoin()
.setWeight(weight);
} }
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle: /** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
@ -1198,7 +1300,11 @@ export function stack(...pats) {
*/ */
export function slowcat(...pats) { export function slowcat(...pats) {
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
if (pats.length == 1) {
return pats[0];
}
const query = function (state) { const query = function (state) {
const span = state.span; const span = state.span;
@ -1245,13 +1351,29 @@ export function cat(...pats) {
return slowcat(...pats); return slowcat(...pats);
} }
/** Like `seq`, but each step has a length, relative to the whole. /** 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.
* @return {Pattern} * @return {Pattern}
* @example * @example
* timeCat([3,"e3"],[1, "g3"]).note() * timeCat([3,"e3"],[1, "g3"]).note()
* // "e3@3 g3".note() * // the same as "e3@3 g3".note()
* @example
* timeCat("bd sd cp","hh hh").sound()
* // the same as "bd sd cp hh hh".sound()
*/ */
export function timeCat(...timepats) { 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);
if (timepats.length == 1) {
const result = reify(timepats[0][1]);
result.weight = timepats[0][0];
return result;
}
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
let begin = Fraction(0); let begin = Fraction(0);
const pats = []; const pats = [];
@ -1260,7 +1382,9 @@ export function timeCat(...timepats) {
pats.push(reify(pat)._compress(begin.div(total), end.div(total))); pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
begin = end; begin = end;
} }
return stack(...pats); const result = stack(...pats);
result.weight = total;
return result;
} }
/** /**
@ -1281,7 +1405,35 @@ export function arrange(...sections) {
} }
export function fastcat(...pats) { export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length); let result = slowcat(...pats);
if (pats.length > 1) {
result = result._fast(pats.length);
result.weight = pats.length;
}
return result;
}
/**
* Concatenates patterns beatwise, similar to `timeCat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
*
* @return {Pattern}
* @example
* beatCat(["bd cp", "mt"], "bd").sound()
*/
export function beatCat(...groups) {
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
let result = [];
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 = timeCat(...result);
result.weight = weight;
return result;
} }
/** See `fastcat` */ /** See `fastcat` */
@ -1312,18 +1464,18 @@ function _sequenceCount(x) {
} }
return [reify(x), 1]; return [reify(x), 1];
} }
/** /**
* Aligns one or more given sequences to the given number of steps per cycle. * Speeds a pattern up or down, to fit to the given metrical 'weight'.
*
* @name polymeterSteps
* @param {number} steps how many items are placed in one cycle
* @param {any[]} sequences one or more arrays of Patterns / values
* @example * @example
* polymeterSteps(4, ["c", "d", "e"]) * s("bd sd cp").reweight(4)
* .note().stack(s("bd")) * // The same as s("{bd sd cp}%4")
* // note("{c d e}%4").stack(s("bd"))
*/ */
export function polymeterSteps(steps, ...args) { export const reweight = register('reweight', function (targetWeight, pat) {
return pat.fast(Fraction(targetWeight).div(pat.weight));
});
export function _polymeterListSteps(steps, ...args) {
const seqs = args.map((a) => _sequenceCount(a)); const seqs = args.map((a) => _sequenceCount(a));
if (seqs.length == 0) { if (seqs.length == 0) {
return silence; return silence;
@ -1346,15 +1498,51 @@ export function polymeterSteps(steps, ...args) {
} }
/** /**
* Combines the given lists of patterns with the same pulse. This will create so called polymeters when different sized sequences are used. * Aligns one or more given patterns to the given number of steps per cycle.
* This relies on patterns having coherent number of steps per cycle,
*
* @name polymeterSteps
* @param {number} steps how many items are placed in one cycle
* @param {any[]} patterns one or more patterns
* @example
* // the same as "{c d, e f g}%4"
* polymeterSteps(4, "c d", "e f g")
*/
export function polymeterSteps(steps, ...args) {
if (args.length == 0) {
return silence;
}
if (Array.isArray(args[0])) {
// Support old behaviour
return _polymeterListSteps(steps, ...args);
}
return polymeter(...args).reweight(steps);
}
/**
* Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used.
* @synonyms pm * @synonyms pm
* @example * @example
* polymeter(["c", "eb", "g"], ["c2", "g2"]).note() * // The same as "{c eb g, c2 g2}"
* // "{c eb g, c2 g2}".note() * polymeter("c eb g", "c2 g2")
* *
*/ */
export function polymeter(...args) { export function polymeter(...args) {
return polymeterSteps(0, ...args); if (Array.isArray(args[0])) {
// Support old behaviour
return _polymeterListSteps(0, ...args);
}
if (args.length == 0) {
return silence;
}
const weight = args[0].weight;
const [head, ...tail] = args;
const result = stack(head, ...tail.map((pat) => pat._slow(pat.weight.div(weight))));
result.weight = weight;
return result;
} }
export const mask = curry((a, b) => reify(b).mask(a)); export const mask = curry((a, b) => reify(b).mask(a));
@ -1396,7 +1584,7 @@ export const func = curry((a, b) => reify(b).func(a));
* @noAutocomplete * @noAutocomplete
* *
*/ */
export function register(name, func, patternify = true) { export function register(name, func, patternify = true, preserveWeight = false) {
if (Array.isArray(name)) { if (Array.isArray(name)) {
const result = {}; const result = {};
for (const name_item of name) { for (const name_item of name) {
@ -1411,26 +1599,39 @@ export function register(name, func, patternify = true) {
pfunc = function (...args) { pfunc = function (...args) {
args = args.map(reify); args = args.map(reify);
const pat = args[args.length - 1]; const pat = args[args.length - 1];
let result;
if (arity === 1) { if (arity === 1) {
return func(pat); result = func(pat);
} else {
const firstArgs = args.slice(0, -1);
if (firstArgs.every((arg) => arg.__pure != undefined)) {
const pureArgs = firstArgs.map((arg) => arg.__pure);
result = func(...pureArgs, pat);
} else {
const [left, ...right] = firstArgs;
let mapFn = (...args) => {
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
}
} }
const [left, ...right] = args.slice(0, -1); if (preserveWeight) {
let mapFn = (...args) => { result.weight = pat.weight;
// make sure to call func with the correct argument count }
// args.length is expected to be <= arity-1 return result;
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
}; };
} else { } else {
pfunc = function (...args) { pfunc = function (...args) {
args = args.map(reify); args = args.map(reify);
return func(...args); const result = func(...args);
if (preserveWeight) {
result.weight = args[args.length - 1].weight;
}
return result;
}; };
} }
@ -1665,7 +1866,9 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
* s("bd ~ sd cp").ply("<1 2 3>") * s("bd ~ sd cp").ply("<1 2 3>")
*/ */
export const ply = register('ply', function (factor, pat) { export const ply = register('ply', function (factor, pat) {
return pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
result.weight = pat.weight.mul(factor);
return result;
}); });
/** /**
@ -1685,7 +1888,9 @@ export const { fast, density } = register(['fast', 'density'], function (factor,
} }
factor = Fraction(factor); factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor)); const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor)); const result = fastQuery.withHapTime((t) => t.div(factor));
result.weight = factor.mul(pat.weight);
return result;
}); });
/** /**
@ -1810,10 +2015,15 @@ export const cpm = register('cpm', function (cpm, pat) {
* @example * @example
* "bd ~".stack("hh ~".early(.1)).s() * "bd ~".stack("hh ~".early(.1)).s()
*/ */
export const early = register('early', function (offset, pat) { export const early = register(
offset = Fraction(offset); 'early',
return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset)); function (offset, pat) {
}); offset = Fraction(offset);
return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
},
true,
true,
);
/** /**
* Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator * Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
@ -1825,10 +2035,15 @@ export const early = register('early', function (offset, pat) {
* @example * @example
* "bd ~".stack("hh ~".late(.1)).s() * "bd ~".stack("hh ~".late(.1)).s()
*/ */
export const late = register('late', function (offset, pat) { export const late = register(
offset = Fraction(offset); 'late',
return pat._early(Fraction(0).sub(offset)); function (offset, pat) {
}); offset = Fraction(offset);
return pat._early(Fraction(0).sub(offset));
},
true,
true,
);
/** /**
* Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern: * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
@ -1857,14 +2072,19 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
* @example * @example
* s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>") * s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>")
*/ */
export const linger = register('linger', function (t, pat) { export const linger = register(
if (t == 0) { 'linger',
return silence; function (t, pat) {
} else if (t < 0) { if (t == 0) {
return pat._zoom(t.add(1), 1)._slow(t); return silence;
} } else if (t < 0) {
return pat._zoom(0, t)._slow(t); return pat._zoom(t.add(1), 1)._slow(t);
}); }
return pat._zoom(0, t)._slow(t);
},
true,
true,
);
/** /**
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
@ -1873,7 +2093,7 @@ export const linger = register('linger', function (t, pat) {
* note(saw.range(40,52).segment(24)) * note(saw.range(40,52).segment(24))
*/ */
export const segment = register('segment', function (rate, pat) { export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate)); return pat.struct(pure(true)._fast(rate)).setWeight(rate);
}); });
/** /**
@ -1883,10 +2103,15 @@ export const segment = register('segment', function (rate, pat) {
* @example * @example
* s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert)) * s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert))
*/ */
export const { invert, inv } = register(['invert', 'inv'], function (pat) { export const { invert, inv } = register(
// Swap true/false in a binary pattern ['invert', 'inv'],
return pat.fmap((x) => !x); function (pat) {
}); // Swap true/false in a binary pattern
return pat.fmap((x) => !x);
},
true,
true,
);
/** /**
* Applies the given function whenever the given pattern is in a true state. * Applies the given function whenever the given pattern is in a true state.
@ -1935,24 +2160,29 @@ export const brak = register('brak', function (pat) {
* @example * @example
* note("c d e g").rev() * note("c d e g").rev()
*/ */
export const rev = register('rev', function (pat) { export const rev = register(
const query = function (state) { 'rev',
const span = state.span; function (pat) {
const cycle = span.begin.sam(); const query = function (state) {
const next_cycle = span.begin.nextSam(); const span = state.span;
const reflect = function (to_reflect) { const cycle = span.begin.sam();
const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time))); const next_cycle = span.begin.nextSam();
// [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work const reflect = function (to_reflect) {
const tmp = reflected.begin; const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time)));
reflected.begin = reflected.end; // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work
reflected.end = tmp; const tmp = reflected.begin;
return reflected; reflected.begin = reflected.end;
reflected.end = tmp;
return reflected;
};
const haps = pat.query(state.setSpan(reflect(span)));
return haps.map((hap) => hap.withSpan(reflect));
}; };
const haps = pat.query(state.setSpan(reflect(span))); return new Pattern(query).splitQueries();
return haps.map((hap) => hap.withSpan(reflect)); },
}; false,
return new Pattern(query).splitQueries(); true,
}); );
/** Like press, but allows you to specify the amount by which each /** Like press, but allows you to specify the amount by which each
* event is shifted. pressBy(0.5) is the same as press, while * event is shifted. pressBy(0.5) is the same as press, while
@ -1994,9 +2224,14 @@ Pattern.prototype.hush = function () {
* @example * @example
* note("c d e g").palindrome() * note("c d e g").palindrome()
*/ */
export const palindrome = register('palindrome', function (pat) { export const palindrome = register(
return pat.lastOf(2, rev); 'palindrome',
}); function (pat) {
return pat.lastOf(2, rev);
},
true,
true,
);
/** /**
* Jux with adjustable stereo width. 0 = mono, 1 = full stereo. * Jux with adjustable stereo width. 0 = mono, 1 = full stereo.
@ -2014,9 +2249,9 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
return dflt; return dflt;
}; };
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
const right = 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, func(right)); return stack(left, right).setWeight(lcm(left.weight, right.weight));
}); });
/** /**
@ -2097,9 +2332,14 @@ const _iter = function (times, pat, back = false) {
); );
}; };
export const iter = register('iter', function (times, pat) { export const iter = register(
return _iter(times, pat, false); 'iter',
}); function (times, pat) {
return _iter(times, pat, false);
},
true,
true,
);
/** /**
* Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
@ -2110,9 +2350,14 @@ export const iter = register('iter', function (times, pat) {
* @example * @example
* note("0 1 2 3".scale('A minor')).iterBack(4) * note("0 1 2 3".scale('A minor')).iterBack(4)
*/ */
export const { iterBack, iterback } = register(['iterBack', 'iterback'], function (times, pat) { export const { iterBack, iterback } = register(
return _iter(times, pat, true); ['iterBack', 'iterback'],
}); function (times, pat) {
return _iter(times, pat, true);
},
true,
true,
);
/** /**
* Repeats each cycle the given number of times. * Repeats each cycle the given number of times.
@ -2122,11 +2367,14 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
* @example * @example
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon") * note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
*/ */
const _repeatCycles = function (n, pat) { export const { repeatCycles } = register(
return slowcat(...Array(n).fill(pat)); 'repeatCycles',
}; function (n, pat) {
return slowcat(...Array(n).fill(pat));
const { repeatCycles } = register('repeatCycles', _repeatCycles); },
true,
true,
);
/** /**
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
@ -2150,7 +2398,7 @@ const _chunk = function (n, func, pat, back = false, fast = false) {
return pat.when(binary_pat, func); return pat.when(binary_pat, func);
}; };
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) { export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, false); return _chunk(n, func, pat, false, false);
}); });
@ -2185,10 +2433,15 @@ export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], fun
}); });
// TODO - redefine elsewhere in terms of mask // TODO - redefine elsewhere in terms of mask
export const bypass = register('bypass', function (on, pat) { export const bypass = register(
on = Boolean(parseInt(on)); 'bypass',
return on ? silence : pat; function (on, pat) {
}); on = Boolean(parseInt(on));
return on ? silence : pat;
},
true,
true,
);
/** /**
* Loops the pattern inside at `offset` for `cycles`. * Loops the pattern inside at `offset` for `cycles`.
@ -2245,7 +2498,7 @@ export const chop = register('chop', function (n, pat) {
const func = function (o) { const func = function (o) {
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
}; };
return pat.squeezeBind(func); return pat.squeezeBind(func).setWeight(pat.weight.mul(n));
}); });
/** /**
@ -2297,17 +2550,19 @@ const _loopAt = function (factor, pat, cps = 0.5) {
export const slice = register( export const slice = register(
'slice', 'slice',
function (npat, ipat, opat) { function (npat, ipat, opat) {
return npat.innerBind((n) => return npat
ipat.outerBind((i) => .innerBind((n) =>
opat.outerBind((o) => { ipat.outerBind((i) =>
// If it's not an object, assume it's a string and make it a 's' control parameter opat.outerBind((o) => {
o = o instanceof Object ? o : { s: o }; // If it's not an object, assume it's a string and make it a 's' control parameter
const begin = Array.isArray(n) ? n[i] : i / n; o = o instanceof Object ? o : { s: o };
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n; const begin = Array.isArray(n) ? n[i] : i / n;
return pure({ begin, end, _slices: n, ...o }); const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
}), return pure({ begin, end, _slices: n, ...o });
), }),
); ),
)
.setWeight(ipat.weight);
}, },
false, // turns off auto-patternification false, // turns off auto-patternification
); );
@ -2338,7 +2593,7 @@ export const splice = register(
...v, ...v,
})), })),
); );
}); }).setWeight(ipat.weight);
}, },
false, // turns off auto-patternification false, // turns off auto-patternification
); );

View File

@ -4,9 +4,10 @@ Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { s } from '../controls.mjs'; import { s, pan } from '../controls.mjs';
import { mini } from '../../mini/mini.mjs'; import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import Fraction from '../fraction.mjs';
describe('controls', () => { describe('controls', () => {
it('should support controls', () => { it('should support controls', () => {
@ -29,4 +30,13 @@ describe('controls', () => {
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]); 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 }]); 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 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('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));
});
}); });

View File

@ -604,7 +604,7 @@ describe('Pattern', () => {
}); });
}); });
describe('polymeter()', () => { describe('polymeter()', () => {
it('Can layer up cycles, stepwise', () => { it('Can layer up cycles, stepwise, with lists', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual( expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(), fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
); );
@ -613,6 +613,9 @@ describe('Pattern', () => {
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(), stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
); );
}); });
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
}); });
describe('firstOf()', () => { describe('firstOf()', () => {
@ -1116,4 +1119,23 @@ describe('Pattern', () => {
); );
}); });
}); });
describe('weight', () => {
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(
Fraction(4),
);
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual(
Fraction(4),
);
});
});
}); });

View File

@ -323,3 +323,23 @@ export function objectMap(obj, fn) {
} }
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)])); return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
} }
// Floating point versions, see Fraction for rational versions
// // greatest common divisor
// export const gcd = function (x, y, ...z) {
// if (!y && z.length > 0) {
// return gcd(x, ...z);
// }
// if (!y) {
// return x;
// }
// return gcd(y, x % y, ...z);
// };
// // lowest common multiple
// export const lcm = function (x, y, ...z) {
// if (z.length == 0) {
// return (x * y) / gcd(x, y);
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };

View File

@ -92,16 +92,16 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
return strudel.stack(...children); return strudel.stack(...children);
} }
if (alignment === 'polymeter_slowcat') { if (alignment === 'polymeter_slowcat') {
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1))); const aligned = children.map((child) => child._slow(child.weight));
return strudel.stack(...aligned); return strudel.stack(...aligned);
} }
if (alignment === 'polymeter') { if (alignment === 'polymeter') {
// polymeter // polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1)); : 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 || 1)))); const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.weight))));
return strudel.stack(...aligned); return strudel.stack(...aligned);
} }
if (alignment === 'rand') { if (alignment === 'rand') {
@ -112,13 +112,18 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
} }
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) { if (weightedChildren) {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0); const weightSum = ast.source_.reduce(
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
pat.__weight = weightSum; 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; return pat;
} }
const pat = strudel.sequence(...children); const pat = strudel.sequence(...children);
pat.__weight = children.length; pat.weight = children.length;
return pat; return pat;
} }
case 'element': { case 'element': {

View File

@ -567,10 +567,8 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = `
exports[`runs examples > example "accelerate" example index 0 1`] = ` exports[`runs examples > example "accelerate" example index 0 1`] = `
[ [
"[ (0/1 → 1/1) ⇝ 2/1 | s:sax accelerate:0 ]", "[ 0/1 → 2/1 | s:sax accelerate:0 ]",
"[ 0/1 ⇜ (1/1 → 2/1) | s:sax accelerate:0 ]", "[ 2/1 → 4/1 | s:sax accelerate:1 ]",
"[ (2/1 → 3/1) ⇝ 4/1 | s:sax accelerate:1 ]",
"[ 2/1 ⇜ (3/1 → 4/1) | s:sax accelerate:1 ]",
] ]
`; `;
@ -956,6 +954,31 @@ exports[`runs examples > example "bank" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "beatCat" example index 0 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:cp ]",
"[ 2/5 → 3/5 | s:bd ]",
"[ 3/5 → 4/5 | s:mt ]",
"[ 4/5 → 1/1 | s:bd ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:cp ]",
"[ 7/5 → 8/5 | s:bd ]",
"[ 8/5 → 9/5 | s:mt ]",
"[ 9/5 → 2/1 | s:bd ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:cp ]",
"[ 12/5 → 13/5 | s:bd ]",
"[ 13/5 → 14/5 | s:mt ]",
"[ 14/5 → 3/1 | s:bd ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:cp ]",
"[ 17/5 → 18/5 | s:bd ]",
"[ 18/5 → 19/5 | s:mt ]",
"[ 19/5 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "begin" example index 0 1`] = ` exports[`runs examples > example "begin" example index 0 1`] = `
[ [
"[ 0/1 → 1/2 | s:rave begin:0 ]", "[ 0/1 → 1/2 | s:rave begin:0 ]",
@ -1652,8 +1675,7 @@ exports[`runs examples > example "cpm" example index 0 1`] = `
"[ 0/1 → 2/3 | s:bd ]", "[ 0/1 → 2/3 | s:bd ]",
"[ 1/3 → 2/3 | s:hh ]", "[ 1/3 → 2/3 | s:hh ]",
"[ 2/3 → 1/1 | s:hh ]", "[ 2/3 → 1/1 | s:hh ]",
"[ (2/3 → 1/1) ⇝ 4/3 | s:sd ]", "[ 2/3 → 4/3 | s:sd ]",
"[ 2/3 ⇜ (1/1 → 4/3) | s:sd ]",
"[ 1/1 → 4/3 | s:hh ]", "[ 1/1 → 4/3 | s:hh ]",
"[ 4/3 → 5/3 | s:hh ]", "[ 4/3 → 5/3 | s:hh ]",
"[ 4/3 → 2/1 | s:bd ]", "[ 4/3 → 2/1 | s:bd ]",
@ -1662,8 +1684,7 @@ exports[`runs examples > example "cpm" example index 0 1`] = `
"[ 2/1 → 8/3 | s:sd ]", "[ 2/1 → 8/3 | s:sd ]",
"[ 7/3 → 8/3 | s:hh ]", "[ 7/3 → 8/3 | s:hh ]",
"[ 8/3 → 3/1 | s:hh ]", "[ 8/3 → 3/1 | s:hh ]",
"[ (8/3 → 3/1) ⇝ 10/3 | s:bd ]", "[ 8/3 → 10/3 | s:bd ]",
"[ 8/3 ⇜ (3/1 → 10/3) | s:bd ]",
"[ 3/1 → 10/3 | s:hh ]", "[ 3/1 → 10/3 | s:hh ]",
"[ 10/3 → 11/3 | s:hh ]", "[ 10/3 → 11/3 | s:hh ]",
"[ 10/3 → 4/1 | s:sd ]", "[ 10/3 → 4/1 | s:sd ]",
@ -2116,14 +2137,11 @@ exports[`runs examples > example "early" example index 0 1`] = `
[ [
"[ -1/10 ⇜ (0/1 → 2/5) | s:hh ]", "[ -1/10 ⇜ (0/1 → 2/5) | s:hh ]",
"[ 0/1 → 1/2 | s:bd ]", "[ 0/1 → 1/2 | s:bd ]",
"[ (9/10 → 1/1) ⇝ 7/5 | s:hh ]", "[ 9/10 → 7/5 | s:hh ]",
"[ 9/10 ⇜ (1/1 → 7/5) | s:hh ]",
"[ 1/1 → 3/2 | s:bd ]", "[ 1/1 → 3/2 | s:bd ]",
"[ (19/10 → 2/1) ⇝ 12/5 | s:hh ]", "[ 19/10 → 12/5 | s:hh ]",
"[ 19/10 ⇜ (2/1 → 12/5) | s:hh ]",
"[ 2/1 → 5/2 | s:bd ]", "[ 2/1 → 5/2 | s:bd ]",
"[ (29/10 → 3/1) ⇝ 17/5 | s:hh ]", "[ 29/10 → 17/5 | s:hh ]",
"[ 29/10 ⇜ (3/1 → 17/5) | s:hh ]",
"[ 3/1 → 7/2 | s:bd ]", "[ 3/1 → 7/2 | s:bd ]",
"[ (39/10 → 4/1) ⇝ 22/5 | s:hh ]", "[ (39/10 → 4/1) ⇝ 22/5 | s:hh ]",
] ]
@ -2443,10 +2461,8 @@ exports[`runs examples > example "firstOf" example index 0 1`] = `
exports[`runs examples > example "fit" example index 0 1`] = ` exports[`runs examples > example "fit" example index 0 1`] = `
[ [
"[ (0/1 → 1/1) ⇝ 2/1 | s:rhodes speed:0.5 unit:c ]", "[ 0/1 → 2/1 | s:rhodes speed:0.5 unit:c ]",
"[ 0/1 ⇜ (1/1 → 2/1) | s:rhodes speed:0.5 unit:c ]", "[ 2/1 → 4/1 | s:rhodes speed:0.5 unit:c ]",
"[ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.5 unit:c ]",
"[ 2/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.5 unit:c ]",
] ]
`; `;
@ -2827,6 +2843,8 @@ exports[`runs examples > example "gain" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
exports[`runs examples > example "hpattack" example index 0 1`] = ` exports[`runs examples > example "hpattack" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]", "[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
@ -3094,11 +3112,9 @@ exports[`runs examples > example "hpsustain" example index 0 1`] = `
exports[`runs examples > example "hurry" example index 0 1`] = ` exports[`runs examples > example "hurry" example index 0 1`] = `
[ [
"[ 0/1 → 3/4 | s:bd speed:1 ]", "[ 0/1 → 3/4 | s:bd speed:1 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:2 speed:1 ]", "[ 3/4 → 3/2 | s:sd n:2 speed:1 ]",
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd n:2 speed:1 ]",
"[ 3/2 → 15/8 | s:bd speed:2 ]", "[ 3/2 → 15/8 | s:bd speed:2 ]",
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd n:2 speed:2 ]", "[ 15/8 → 9/4 | s:sd n:2 speed:2 ]",
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd n:2 speed:2 ]",
"[ 9/4 → 21/8 | s:bd speed:2 ]", "[ 9/4 → 21/8 | s:bd speed:2 ]",
"[ 21/8 → 3/1 | s:sd n:2 speed:2 ]", "[ 21/8 → 3/1 | s:sd n:2 speed:2 ]",
"[ 3/1 → 51/16 | s:bd speed:4 ]", "[ 3/1 → 51/16 | s:bd speed:4 ]",
@ -3705,10 +3721,8 @@ exports[`runs examples > example "loop" example index 0 1`] = `
exports[`runs examples > example "loopAt" example index 0 1`] = ` exports[`runs examples > example "loopAt" example index 0 1`] = `
[ [
"[ (0/1 → 1/1) ⇝ 2/1 | s:rhodes speed:0.25 unit:c ]", "[ 0/1 → 2/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (1/1 → 2/1) | s:rhodes speed:0.25 unit:c ]", "[ 2/1 → 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 2/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.25 unit:c ]",
] ]
`; `;
@ -4178,10 +4192,8 @@ exports[`runs examples > example "never" example index 0 1`] = `
exports[`runs examples > example "noise" example index 0 1`] = ` exports[`runs examples > example "noise" example index 0 1`] = `
[ [
"[ (0/1 → 1/1) ⇝ 2/1 | s:white ]", "[ 0/1 → 2/1 | s:white ]",
"[ 0/1 ⇜ (1/1 → 2/1) | s:white ]", "[ 2/1 → 4/1 | s:pink ]",
"[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]",
"[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]",
] ]
`; `;
@ -4903,55 +4915,67 @@ exports[`runs examples > example "ply" example index 0 1`] = `
exports[`runs examples > example "polymeter" example index 0 1`] = ` exports[`runs examples > example "polymeter" example index 0 1`] = `
[ [
"[ 0/1 → 1/3 | note:c ]", "[ 0/1 → 1/3 | c ]",
"[ 0/1 → 1/3 | note:c2 ]", "[ 0/1 → 1/3 | c2 ]",
"[ 1/3 → 2/3 | note:eb ]", "[ 1/3 → 2/3 | eb ]",
"[ 1/3 → 2/3 | note:g2 ]", "[ 1/3 → 2/3 | g2 ]",
"[ 2/3 → 1/1 | note:g ]", "[ 2/3 → 1/1 | g ]",
"[ 2/3 → 1/1 | note:c2 ]", "[ 2/3 → 1/1 | c2 ]",
"[ 1/1 → 4/3 | note:c ]", "[ 1/1 → 4/3 | c ]",
"[ 1/1 → 4/3 | note:g2 ]", "[ 1/1 → 4/3 | g2 ]",
"[ 4/3 → 5/3 | note:eb ]", "[ 4/3 → 5/3 | eb ]",
"[ 4/3 → 5/3 | note:c2 ]", "[ 4/3 → 5/3 | c2 ]",
"[ 5/3 → 2/1 | note:g ]", "[ 5/3 → 2/1 | g ]",
"[ 5/3 → 2/1 | note:g2 ]", "[ 5/3 → 2/1 | g2 ]",
"[ 2/1 → 7/3 | note:c ]", "[ 2/1 → 7/3 | c ]",
"[ 2/1 → 7/3 | note:c2 ]", "[ 2/1 → 7/3 | c2 ]",
"[ 7/3 → 8/3 | note:eb ]", "[ 7/3 → 8/3 | eb ]",
"[ 7/3 → 8/3 | note:g2 ]", "[ 7/3 → 8/3 | g2 ]",
"[ 8/3 → 3/1 | note:g ]", "[ 8/3 → 3/1 | g ]",
"[ 8/3 → 3/1 | note:c2 ]", "[ 8/3 → 3/1 | c2 ]",
"[ 3/1 → 10/3 | note:c ]", "[ 3/1 → 10/3 | c ]",
"[ 3/1 → 10/3 | note:g2 ]", "[ 3/1 → 10/3 | g2 ]",
"[ 10/3 → 11/3 | note:eb ]", "[ 10/3 → 11/3 | eb ]",
"[ 10/3 → 11/3 | note:c2 ]", "[ 10/3 → 11/3 | c2 ]",
"[ 11/3 → 4/1 | note:g ]", "[ 11/3 → 4/1 | g ]",
"[ 11/3 → 4/1 | note:g2 ]", "[ 11/3 → 4/1 | g2 ]",
] ]
`; `;
exports[`runs examples > example "polymeterSteps" example index 0 1`] = ` exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:c ]", "[ 0/1 → 1/4 | c ]",
"[ 0/1 → 1/1 | s:bd ]", "[ 0/1 → 1/4 | e ]",
"[ 1/4 → 1/2 | note:d ]", "[ 1/4 → 1/2 | d ]",
"[ 1/2 → 3/4 | note:e ]", "[ 1/4 → 1/2 | f ]",
"[ 3/4 → 1/1 | note:c ]", "[ 1/2 → 3/4 | c ]",
"[ 1/1 → 5/4 | note:d ]", "[ 1/2 → 3/4 | g ]",
"[ 1/1 → 2/1 | s:bd ]", "[ 3/4 → 1/1 | d ]",
"[ 5/4 → 3/2 | note:e ]", "[ 3/4 → 1/1 | e ]",
"[ 3/2 → 7/4 | note:c ]", "[ 1/1 → 5/4 | c ]",
"[ 7/4 → 2/1 | note:d ]", "[ 1/1 → 5/4 | f ]",
"[ 2/1 → 9/4 | note:e ]", "[ 5/4 → 3/2 | d ]",
"[ 2/1 → 3/1 | s:bd ]", "[ 5/4 → 3/2 | g ]",
"[ 9/4 → 5/2 | note:c ]", "[ 3/2 → 7/4 | c ]",
"[ 5/2 → 11/4 | note:d ]", "[ 3/2 → 7/4 | e ]",
"[ 11/4 → 3/1 | note:e ]", "[ 7/4 → 2/1 | d ]",
"[ 3/1 → 13/4 | note:c ]", "[ 7/4 → 2/1 | f ]",
"[ 3/1 → 4/1 | s:bd ]", "[ 2/1 → 9/4 | c ]",
"[ 13/4 → 7/2 | note:d ]", "[ 2/1 → 9/4 | g ]",
"[ 7/2 → 15/4 | note:e ]", "[ 9/4 → 5/2 | d ]",
"[ 15/4 → 4/1 | note:c ]", "[ 9/4 → 5/2 | e ]",
"[ 5/2 → 11/4 | c ]",
"[ 5/2 → 11/4 | f ]",
"[ 11/4 → 3/1 | d ]",
"[ 11/4 → 3/1 | g ]",
"[ 3/1 → 13/4 | c ]",
"[ 3/1 → 13/4 | e ]",
"[ 13/4 → 7/2 | d ]",
"[ 13/4 → 7/2 | f ]",
"[ 7/2 → 15/4 | c ]",
"[ 7/2 → 15/4 | g ]",
"[ 15/4 → 4/1 | d ]",
"[ 15/4 → 4/1 | e ]",
] ]
`; `;
@ -5501,6 +5525,27 @@ exports[`runs examples > example "rev" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "reweight" 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 "ribbon" example index 0 1`] = ` exports[`runs examples > example "ribbon" example index 0 1`] = `
[ [
"[ 0/1 → 1/2 | note:d ]", "[ 0/1 → 1/2 | note:d ]",
@ -6428,8 +6473,7 @@ exports[`runs examples > example "slice" example index 0 1`] = `
"[ 3/4 → 27/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", "[ 3/4 → 27/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 27/32 → 15/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]", "[ 27/32 → 15/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 15/16 → 63/64 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", "[ 15/16 → 63/64 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ (63/64 → 1/1) ⇝ 33/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", "[ 63/64 → 33/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 63/64 ⇜ (1/1 → 33/32) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 33/32 → 9/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", "[ 33/32 → 9/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 9/8 → 75/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", "[ 9/8 → 75/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 75/64 → 39/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", "[ 75/64 → 39/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
@ -6442,8 +6486,7 @@ exports[`runs examples > example "slice" example index 0 1`] = `
"[ 57/32 → 15/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", "[ 57/32 → 15/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 15/8 → 123/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", "[ 15/8 → 123/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 123/64 → 63/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", "[ 123/64 → 63/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (63/32 → 2/1) ⇝ 33/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", "[ 63/32 → 33/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
"[ 63/32 ⇜ (2/1 → 33/16) | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
"[ 33/16 → 69/32 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]", "[ 33/16 → 69/32 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
"[ 69/32 → 9/4 | begin:0.875 end:1 _slices:8 s:breaks165 ]", "[ 69/32 → 9/4 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 9/4 → 75/32 | begin:0.875 end:1 _slices:8 s:breaks165 ]", "[ 9/4 → 75/32 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
@ -7089,6 +7132,31 @@ exports[`runs examples > example "timeCat" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "timeCat" example index 1 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:sd ]",
"[ 2/5 → 3/5 | s:cp ]",
"[ 3/5 → 4/5 | s:hh ]",
"[ 4/5 → 1/1 | s:hh ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:sd ]",
"[ 7/5 → 8/5 | s:cp ]",
"[ 8/5 → 9/5 | s:hh ]",
"[ 9/5 → 2/1 | s:hh ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:sd ]",
"[ 12/5 → 13/5 | s:cp ]",
"[ 13/5 → 14/5 | s:hh ]",
"[ 14/5 → 3/1 | s:hh ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:sd ]",
"[ 17/5 → 18/5 | s:cp ]",
"[ 18/5 → 19/5 | s:hh ]",
"[ 19/5 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "transpose" example index 0 1`] = ` exports[`runs examples > example "transpose" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:C2 ]", "[ 0/1 → 1/4 | note:C2 ]",

File diff suppressed because it is too large Load Diff