diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index daacc946..7cfa6a6d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1328,7 +1328,8 @@ export function slowcat(...pats) { const offset = span.begin.floor().sub(span.begin.div(pats.length).floor()); return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset)))); }; - return new Pattern(query).splitQueries(); + const tactus = lcm(...pats.map((x) => x.tactus)); + return new Pattern(query).splitQueries().setTactus(tactus); } /** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles. @@ -1359,42 +1360,6 @@ export function cat(...pats) { return slowcat(...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 - * the pattern's 'tactus', generally inferred by the mininotation. - * @return {Pattern} - * @example - * timecat([3,"e3"],[1, "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) { - 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.tactus = timepats[0][0]; - return result; - } - - const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); - let begin = Fraction(0); - const pats = []; - for (const [time, pat] of timepats) { - const end = begin.add(time); - pats.push(reify(pat)._compress(begin.div(total), end.div(total))); - begin = end; - } - const result = stack(...pats); - result.tactus = total; - return result; -} - -/** Deprecated alias for `timecat` */ -export const timeCat = timecat; - /** * Allows to arrange multiple patterns together over multiple cycles. * Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use. @@ -1426,30 +1391,6 @@ export function sequence(...pats) { return fastcat(...pats); } -/** - * Concatenates patterns stepwise, according to their 'tactus'. - * 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 - * stepcat(["bd cp", "mt"], "bd").sound() - */ -export function stepcat(...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.tactus > 0); - const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0)); - result = timecat(...result); - result.tactus = tactus; - return result; -} - /** Like **cat**, but the items are crammed into one cycle. * @synonyms fastcat, sequence * @example @@ -1474,86 +1415,6 @@ function _sequenceCount(x) { return [reify(x), 1]; } -/** - * Speeds a pattern up or down, to fit to the given metrical 'tactus'. - * @example - * s("bd sd cp").toTactus(4) - * // The same as s("{bd sd cp}%4") - */ -export const toTactus = register('toTactus', function (targetTactus, pat) { - return pat.fast(Fraction(targetTactus).div(pat.tactus)); -}); - -export function _polymeterListSteps(steps, ...args) { - const seqs = args.map((a) => _sequenceCount(a)); - if (seqs.length == 0) { - return silence; - } - if (steps == 0) { - steps = seqs[0][1]; - } - const pats = []; - for (const seq of seqs) { - if (seq[1] == 0) { - continue; - } - if (steps == seq[1]) { - pats.push(seq[0]); - } else { - pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1])))); - } - } - return stack(...pats); -} - -/** - * 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).toTactus(steps); -} - -/** - * Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used. - * @synonyms pm - * @example - * // The same as "{c eb g, c2 g2}" - * polymeter("c eb g", "c2 g2") - * - */ -export function polymeter(...args) { - if (Array.isArray(args[0])) { - // Support old behaviour - return _polymeterListSteps(0, ...args); - } - - if (args.length == 0) { - return silence; - } - const tactus = args[0].tactus; - const [head, ...tail] = args; - - const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus)))); - result.tactus = tactus; - return result; -} - export const mask = curry((a, b) => reify(b).mask(a)); export const struct = curry((a, b) => reify(b).struct(a)); export const superimpose = curry((a, b) => reify(b).superimpose(...a)); @@ -2069,11 +1930,16 @@ export const late = register( export const zoom = register('zoom', function (s, e, pat) { e = Fraction(e); s = Fraction(s); + if (s.gte(e)) { + return nothing; + } const d = e.sub(s); + const tactus = pat.tactus.mul(d); return pat .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) - .splitQueries(); + .splitQueries() + .setTactus(tactus); }); export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a, pat) { @@ -2487,6 +2353,250 @@ Pattern.prototype.tag = function (tag) { return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); }; +////////////////////////////////////////////////////////////////////// +// Tactus-related functions, i.e. ones that do stepwise +// transformations + +/** + * *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given metrical 'tactus'. + * @example + * s("bd sd cp").toTactus(4) + * // The same as s("{bd sd cp}%4") + */ +export const toTactus = register('toTactus', function (targetTactus, pat) { + if (pat.tactus.eq(0)) { + // avoid divide by zero.. + return nothing; + } + return pat.fast(Fraction(targetTactus).div(pat.tactus)); +}); + +export function _polymeterListSteps(steps, ...args) { + const seqs = args.map((a) => _sequenceCount(a)); + if (seqs.length == 0) { + return silence; + } + if (steps == 0) { + steps = seqs[0][1]; + } + const pats = []; + for (const seq of seqs) { + if (seq[1] == 0) { + continue; + } + if (steps == seq[1]) { + pats.push(seq[0]); + } else { + pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1])))); + } + } + return stack(...pats); +} + +/** + * 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).toTactus(steps); +} + +/** + * *EXPERIMENTAL* - Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used. + * @synonyms pm + * @example + * // The same as "{c eb g, c2 g2}" + * polymeter("c eb g", "c2 g2") + * + */ +export function polymeter(...args) { + if (Array.isArray(args[0])) { + // Support old behaviour + return _polymeterListSteps(0, ...args); + } + + if (args.length == 0) { + return silence; + } + const tactus = args[0].tactus; + const [head, ...tail] = args; + + const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus)))); + result.tactus = tactus; + return result; +} + +/** 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 + * the pattern's 'tactus', generally inferred by the mininotation. + * @return {Pattern} + * @example + * timecat([3,"e3"],[1, "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) { + 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.tactus = timepats[0][0]; + return result; + } + + const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); + let begin = Fraction(0); + const pats = []; + for (const [time, pat] of timepats) { + if (Fraction(time).eq(0)) { + continue; + } + const end = begin.add(time); + pats.push(reify(pat)._compress(begin.div(total), end.div(total))); + begin = end; + } + const result = stack(...pats); + result.tactus = total; + return result; +} + +/** Deprecated alias for `timecat` */ +export const timeCat = timecat; + +/** + * *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'. + * 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 + * stepcat(["bd cp", "mt"], "bd").sound() + */ +export function stepcat(...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.tactus > 0); + const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0)); + result = timecat(...result); + result.tactus = tactus; + return result; +} + +/** + * *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'. + */ +export const stepwax = register('stepwax', function (i, pat) { + if (pat.tactus.lte(0)) { + return nothing; + } + i = Fraction(i); + if (i.eq(0)) { + return nothing; + } + const flip = i < 0; + if (flip) { + i = i.abs(); + } + const frac = i.div(pat.tactus); + if (frac.lte(0)) { + return nothing; + } + if (frac.gte(1)) { + return pat; + } + if (flip) { + return pat.zoom(Fraction(1).sub(frac), 1); + } + return pat.zoom(0, frac); +}); + +/** + * *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'. + */ +export const stepwane = register('stepwane', function (i, pat) { + i = Fraction(i); + if (i.lt(0)) { + return pat.stepwax(Fraction(0).sub(pat.tactus.add(i))); + } + return pat.stepwax(pat.tactus.sub(i)); +}); + +/** + * *EXPERIMENTAL* + */ +Pattern.prototype.taperlist = function (amount, times) { + const pat = this; + times = times - 1; + + if (times === 0) { + return pat; + } + + const list = []; + const reverse = amount > 0; + amount = Fraction(Math.abs(amount)); + const start = pat.tactus.sub(amount.mul(Fraction(times))).max(Fraction(0)); + + for (let i = 0; i < times; ++i) { + list.push(pat.zoom(0, start.add(amount.mul(Fraction(i))).div(pat.tactus))); + } + list.push(pat); + if (reverse) { + list.reverse(); + } + return list; +}; +export const taperlist = (amount, times, pat) => pat.taperlist(amount, times); + +/** + * *EXPERIMENTAL* + */ +export const steptaper = register('steptaper', function (amount, times, pat) { + const list = pat.taperlist(amount, times); + const result = timecat(...list); + result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0)); + return result; +}); + +/** + * *EXPERIMENTAL* + */ +Pattern.prototype.steptour = function (...many) { + return stepcat( + ...[].concat( + ...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]), + this, + ...many, + ), + ); +}; + +export const steptour = function (pat, ...many) { + return pat.steptour(...many); +}; + ////////////////////////////////////////////////////////////////////// // Control-related functions, i.e. ones that manipulate patterns of // objects diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index f825c4df..1ef90f73 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1138,4 +1138,28 @@ describe('Pattern', () => { ); }); }); + describe('steptaper', () => { + it('can taper', () => { + expect(sameFirst(sequence(0, 1, 2, 3, 4).steptaper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0))); + }); + it('can taper backwards', () => { + expect( + sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)), + ); + }); + }); + describe('wax and wane, left', () => { + it('can wax from the left', () => { + expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1))); + }); + it('can wane to the left', () => { + expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2))); + }); + it('can wax from the right', () => { + expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4))); + }); + it('can wane to the right', () => { + expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4))); + }); + }); });