diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e4745368..6e52023e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -386,6 +386,15 @@ export class Pattern { return this.fmap(func).squeezeJoin(); } + polyJoin = function () { + const pp = this; + return pp.fmap((p) => p.s_extend(pp.tactus.div(p.tactus))).outerJoin(); + }; + + polyBind(func) { + return this.fmap(func).polyJoin(); + } + ////////////////////////////////////////////////////////////////////// // Utility methods mainly for internal use @@ -754,6 +763,10 @@ export class Pattern { const otherPat = reify(other); return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).restartJoin(); } + _opPoly(other, func) { + const otherPat = reify(other); + return this.fmap((b) => otherPat.fmap((a) => func(a)(b))).polyJoin(); + } ////////////////////////////////////////////////////////////////////// // End-user methods. @@ -1062,7 +1075,7 @@ function _composeOp(a, b, func) { func: [(a, b) => b(a)], }; - const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart']; + const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly']; // generate methods to do what and how for (const [what, [op, preprocess]] of Object.entries(composers)) { diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index f43fae34..c0637383 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -138,7 +138,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x))); const timeToRandsPrime = (seed, n) => { const result = []; // eslint-disable-next-line - for (let i = 0; i < n; ++n) { + for (let i = 0; i < n; ++i) { result.push(intSeedToRand(seed)); seed = xorwise(seed); } @@ -159,6 +159,50 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n); */ export const run = (n) => saw.range(0, n).floor().segment(n); +export const randrun = (n) => { + return signal((t) => { + // Without adding 0.5, the first cycle is always 0,1,2,3,... + const rands = timeToRands(t.floor().add(0.5), n); + const nums = rands + .map((n, i) => [n, i]) + .sort((a, b) => a[0] > b[0] - a[0] < b[0]) + .map((x) => x[1]); + const i = t.cyclePos().mul(n).floor() % n; + return nums[i]; + })._segment(n); +}; + +const _rearrangeWith = (ipat, n, pat) => { + const pats = [...Array(n).keys()].map((i) => pat.zoom(Fraction(i).div(n), Fraction(i + 1).div(n))); + return ipat.fmap((i) => pats[i].repeatCycles(n)._fast(n)).innerJoin(); +}; + +/** + * @name shuffle + * Slices a pattern into the given number of parts, then plays those parts in random order. + * Each part will be played exactly once per cycle. + * @example + * note("c d e f").sound("piano").shuffle(4) + * @example + * note("c d e f".shuffle(4), "g").sound("piano") + */ +export const shuffle = register('shuffle', (n, pat) => { + return _rearrangeWith(randrun(n), n, pat); +}); + +/** + * @name scramble + * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, + * but parts might be played more than once, or not at all, per cycle. + * @example + * note("c d e f").sound("piano").scramble(4) + * @example + * note("c d e f".scramble(4), "g").sound("piano") + */ +export const scramble = register('scramble', (n, pat) => { + return _rearrangeWith(_irand(n)._segment(n), n, pat); +}); + /** * A continuous pattern of random numbers, between 0 and 1. * diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 11b27c7a..3d48e842 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -6637,6 +6637,56 @@ exports[`runs examples > example "scope" example index 0 1`] = ` ] `; +exports[`runs examples > example "scramble +Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, +but parts might be played more than once, or not at all, per cycle." example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:e s:piano ]", + "[ 1/4 → 1/2 | note:d s:piano ]", + "[ 1/2 → 3/4 | note:c s:piano ]", + "[ 3/4 → 1/1 | note:d s:piano ]", + "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:c s:piano ]", + "[ 3/2 → 7/4 | note:d s:piano ]", + "[ 7/4 → 2/1 | note:e s:piano ]", + "[ 2/1 → 9/4 | note:f s:piano ]", + "[ 9/4 → 5/2 | note:f s:piano ]", + "[ 5/2 → 11/4 | note:c s:piano ]", + "[ 11/4 → 3/1 | note:c s:piano ]", + "[ 3/1 → 13/4 | note:d s:piano ]", + "[ 13/4 → 7/2 | note:d s:piano ]", + "[ 7/2 → 15/4 | note:f s:piano ]", + "[ 15/4 → 4/1 | note:e s:piano ]", +] +`; + +exports[`runs examples > example "scramble +Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, +but parts might be played more than once, or not at all, per cycle." example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | note:e s:piano ]", + "[ 1/8 → 1/4 | note:d s:piano ]", + "[ 1/4 → 3/8 | note:c s:piano ]", + "[ 3/8 → 1/2 | note:d s:piano ]", + "[ 1/2 → 1/1 | note:g s:piano ]", + "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:c s:piano ]", + "[ 5/4 → 11/8 | note:d s:piano ]", + "[ 11/8 → 3/2 | note:e s:piano ]", + "[ 3/2 → 2/1 | note:g s:piano ]", + "[ 2/1 → 17/8 | note:f s:piano ]", + "[ 17/8 → 9/4 | note:f s:piano ]", + "[ 9/4 → 19/8 | note:c s:piano ]", + "[ 19/8 → 5/2 | note:c s:piano ]", + "[ 5/2 → 3/1 | note:g s:piano ]", + "[ 3/1 → 25/8 | note:d s:piano ]", + "[ 25/8 → 13/4 | note:d s:piano ]", + "[ 13/4 → 27/8 | note:f s:piano ]", + "[ 27/8 → 7/2 | note:e s:piano ]", + "[ 7/2 → 4/1 | note:g s:piano ]", +] +`; + exports[`runs examples > example "segment" example index 0 1`] = ` [ "[ 0/1 → 1/24 | note:40.25 ]", @@ -6853,6 +6903,56 @@ exports[`runs examples > example "shape" example index 0 1`] = ` ] `; +exports[`runs examples > example "shuffle +Slices a pattern into the given number of parts, then plays those parts in random order. +Each part will be played exactly once per cycle." example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c s:piano ]", + "[ 1/4 → 1/2 | note:d s:piano ]", + "[ 1/2 → 3/4 | note:e s:piano ]", + "[ 3/4 → 1/1 | note:f s:piano ]", + "[ 1/1 → 5/4 | note:c s:piano ]", + "[ 5/4 → 3/2 | note:d s:piano ]", + "[ 3/2 → 7/4 | note:e s:piano ]", + "[ 7/4 → 2/1 | note:f s:piano ]", + "[ 2/1 → 9/4 | note:c s:piano ]", + "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 5/2 → 11/4 | note:e s:piano ]", + "[ 11/4 → 3/1 | note:f s:piano ]", + "[ 3/1 → 13/4 | note:c s:piano ]", + "[ 13/4 → 7/2 | note:d s:piano ]", + "[ 7/2 → 15/4 | note:e s:piano ]", + "[ 15/4 → 4/1 | note:f s:piano ]", +] +`; + +exports[`runs examples > example "shuffle +Slices a pattern into the given number of parts, then plays those parts in random order. +Each part will be played exactly once per cycle." example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | note:c s:piano ]", + "[ 1/8 → 1/4 | note:d s:piano ]", + "[ 1/4 → 3/8 | note:e s:piano ]", + "[ 3/8 → 1/2 | note:f s:piano ]", + "[ 1/2 → 1/1 | note:g s:piano ]", + "[ 1/1 → 9/8 | note:c s:piano ]", + "[ 9/8 → 5/4 | note:d s:piano ]", + "[ 5/4 → 11/8 | note:e s:piano ]", + "[ 11/8 → 3/2 | note:f s:piano ]", + "[ 3/2 → 2/1 | note:g s:piano ]", + "[ 2/1 → 17/8 | note:c s:piano ]", + "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 9/4 → 19/8 | note:e s:piano ]", + "[ 19/8 → 5/2 | note:f s:piano ]", + "[ 5/2 → 3/1 | note:g s:piano ]", + "[ 3/1 → 25/8 | note:c s:piano ]", + "[ 25/8 → 13/4 | note:d s:piano ]", + "[ 13/4 → 27/8 | note:e s:piano ]", + "[ 27/8 → 7/2 | note:f s:piano ]", + "[ 7/2 → 4/1 | note:g s:piano ]", +] +`; + exports[`runs examples > example "silence" example index 0 1`] = `[]`; exports[`runs examples > example "sine" example index 0 1`] = `