From fdb76867a7e66549190835c2da62c190e8107be1 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Sat, 18 Feb 2023 00:00:18 +0000 Subject: [PATCH 1/2] weave and weaveWith (#465) Added weave and weaveWith from tidal --- packages/core/euclid.mjs | 8 +---- packages/core/pattern.mjs | 31 ++++++++++++++++- packages/core/test/pattern.test.mjs | 5 +++ packages/core/util.mjs | 6 ++++ test/__snapshots__/examples.test.mjs.snap | 42 +++++++++++++++++++++++ 5 files changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index d8560c8e..29539cf9 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Pattern, timeCat, register, silence } from './pattern.mjs'; -import { rotate, flatten } from './util.mjs'; +import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction from './fraction.mjs'; -const splitAt = function (index, value) { - return [value.slice(0, index), value.slice(index)]; -}; - -const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); - const left = function (n, x) { const [ons, offs] = n; const [xs, ys] = x; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 1184e1a1..1274e760 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -977,7 +977,7 @@ export class Pattern { } ////////////////////////////////////////////////////////////////////// -// functions relating to chords/patterns of lists +// functions relating to chords/patterns of lists/lists of patterns // returns Array where each list of haps satisfies eq function groupHapsBy(eq, haps) { @@ -1026,6 +1026,35 @@ addToPrototype('arp', function (pat) { return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length])); }); +/** + * Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are + * distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration. + * @name weave + * @memberof Pattern + * @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd")) + * @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd")) + */ + +addToPrototype('weave', function (t, ...pats) { + return this.weaveWith(t, ...pats.map((x) => set.out(x))); +}); + +/** + * Like 'weave', but accepts functions rather than patterns, which are applied to the pattern. + * @name weaveWith + * @memberof Pattern + */ + +addToPrototype('weaveWith', function (t, ...funcs) { + const pat = this; + const l = funcs.length; + t = Fraction(t); + if (l == 0) { + return silence; + } + return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t); +}); + ////////////////////////////////////////////////////////////////////// // compose matrix functions diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 2142242d..3d71f529 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -981,4 +981,9 @@ describe('Pattern', () => { // sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2)); }); }); + describe('weave', () => { + it('Can distribute patterns along a pattern', () => { + sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1))); + }); + }); }); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 6ba8f397..18586541 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -206,3 +206,9 @@ export function parseFractional(numOrString) { } export const fractionalArgs = (fn) => mapArgs(fn, parseFractional); + +export const splitAt = function (index, value) { + return [value.slice(0, index), value.slice(index)]; +}; + +export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e061d105..e53cf9fa 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4231,6 +4231,48 @@ exports[`runs examples > example "vowel" example index 0 1`] = ` ] `; +exports[`runs examples > example "weave" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | pan:0.015625 s:bd ]", + "[ 3/8 → 1/2 | pan:0.109375 s:bd ]", + "[ 3/4 → 7/8 | pan:0.203125 s:bd ]", + "[ 1/1 → 9/8 | pan:0.265625 s:bd ]", + "[ 11/8 → 3/2 | pan:0.359375 s:bd ]", + "[ 7/4 → 15/8 | pan:0.453125 s:bd ]", + "[ 2/1 → 17/8 | pan:0.515625 s:bd ]", + "[ 19/8 → 5/2 | pan:0.609375 s:bd ]", + "[ 11/4 → 23/8 | pan:0.703125 s:bd ]", + "[ 3/1 → 25/8 | pan:0.765625 s:bd ]", + "[ 27/8 → 7/2 | pan:0.859375 s:bd ]", + "[ 15/4 → 31/8 | pan:0.953125 s:bd ]", + "[ 1/2 → 1/1 | pan:0.6875 s:sd ]", + "[ 3/2 → 2/1 | pan:0.9375 s:sd ]", + "[ 5/2 → 3/1 | pan:0.1875 s:sd ]", + "[ 7/2 → 4/1 | pan:0.4375 s:sd ]", +] +`; + +exports[`runs examples > example "weave" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | n:0 s:bd ]", + "[ 3/8 → 1/2 | n:0 s:bd ]", + "[ 3/4 → 7/8 | n:0 s:bd ]", + "[ 1/1 → 9/8 | n:1 s:bd ]", + "[ 11/8 → 3/2 | n:1 s:bd ]", + "[ 7/4 → 15/8 | n:1 s:bd ]", + "[ 2/1 → 17/8 | n:2 s:bd ]", + "[ 19/8 → 5/2 | n:2 s:bd ]", + "[ 11/4 → 23/8 | n:2 s:bd ]", + "[ 3/1 → 25/8 | n:3 s:bd ]", + "[ 27/8 → 7/2 | n:3 s:bd ]", + "[ 15/4 → 31/8 | n:3 s:bd ]", + "[ 1/2 → 1/1 | n:4 s:sd ]", + "[ 3/2 → 2/1 | n:5 s:sd ]", + "[ 5/2 → 3/1 | n:6 s:sd ]", + "[ 7/2 → 4/1 | n:7 s:sd ]", +] +`; + exports[`runs examples > example "webdirt" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd n:0 ]", From 7c367eb1e8f253fa9a7a1918ca5edb9bd88ea07e Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 22 Feb 2023 11:51:31 +0000 Subject: [PATCH 2/2] slice and splice (#466) Implements `slice` and `splice` from tidal, intended for beat slicing --- packages/core/pattern.mjs | 112 +++++++++++++++++----- packages/core/test/pattern.test.mjs | 27 ++++++ test/__snapshots__/examples.test.mjs.snap | 63 ++++++++++++ 3 files changed, 178 insertions(+), 24 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 1274e760..6268ffb5 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1595,11 +1595,11 @@ export const trigzero = methodToFunction('trigzero'); * @noAutocomplete * */ -export function register(name, func) { +export function register(name, func, patternify = true) { if (Array.isArray(name)) { const result = {}; for (const name_item of name) { - result[name_item] = register(name_item, func); + result[name_item] = register(name_item, func, patternify); } return result; } @@ -1607,29 +1607,40 @@ export function register(name, func) { registerMethod(name); - const pfunc = function (...args) { - args = args.map(reify); - const pat = args[args.length - 1]; - if (arity === 1) { - return func(pat); - } - const [left, ...right] = args.slice(0, -1); - let mapFn = (...args) => { - // make sure to call func with the correct argument count - // args.length is expected to be <= arity-1 - // so we set undefined args explicitly undefined - Array(arity - 1) - .fill() - .map((_, i) => args[i] ?? undefined); - return func(...args, pat); + var pfunc; + + if (patternify) { + pfunc = function (...args) { + args = args.map(reify); + const pat = args[args.length - 1]; + if (arity === 1) { + return func(pat); + } + const [left, ...right] = args.slice(0, -1); + let mapFn = (...args) => { + // make sure to call func with the correct argument count + // args.length is expected to be <= arity-1 + // so we set undefined args explicitly undefined + Array(arity - 1) + .fill() + .map((_, i) => args[i] ?? undefined); + return func(...args, pat); + }; + mapFn = curryPattern(mapFn, arity - 1); + + const app = function (acc, p, i) { + return acc.appLeft(p); + }; + const start = left.fmap(mapFn); + + return right.reduce(app, start).innerJoin(); }; - mapFn = curryPattern(mapFn, arity - 1); - - const app = (acc, p) => acc.appLeft(p); - const start = left.fmap(mapFn); - - return right.reduce(app, start).innerJoin(); - }; + } else { + pfunc = function (...args) { + args = args.map(reify); + return func(...args); + }; + } Pattern.prototype[name] = function (...args) { args = args.map(reify); @@ -2419,6 +2430,59 @@ const _loopAt = function (factor, pat, cps = 1) { .slow(factor); }; +/** + * Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers. + * @name slice + * @memberof Pattern + * @returns Pattern + * @example + * await samples('github:tidalcycles/Dirt-Samples/master') + * s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5) + */ +const slice = register( + 'slice', + function (npat, ipat, opat) { + return npat.innerBind((n) => + ipat.outerBind((i) => + opat.outerBind((o) => { + // If it's not an object, assume it's a string and make it a 's' control parameter + o = o instanceof Object ? o : { s: o }; + // Remember we must stay pure and avoid editing the object directly + const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n }; + return pure({ ...toAdd, ...o }); + }), + ), + ); + }, + false, // turns off auto-patternification +); + +/** + * Works the same as slice, but changes the playback speed of each slice to match the duration of its step. + * @name splice + * @memberof Pattern + * @returns Pattern + * @example + * await samples('github:tidalcycles/Dirt-Samples/master') + * s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65) + */ +const splice = register( + 'splice', + function (npat, ipat, opat) { + const sliced = slice(npat, ipat, opat); + return sliced.withHap(function (hap) { + return hap.withValue((v) => ({ + ...{ + speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1), + unit: 'c', + }, + ...v, + })); + }); + }, + false, // turns off auto-patternification +); + const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) { return _loopAt(factor, pat, 1); }); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 3d71f529..3e95efca 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -47,6 +47,7 @@ import { run, hitch, set, + begin, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -986,4 +987,30 @@ describe('Pattern', () => { sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1))); }); }); + describe('slice', () => { + it('Can slice a sample', () => { + sameFirst( + s('break').slice(4, sequence(0, 1, 2, 3)), + sequence( + { begin: 0, end: 0.25, s: 'break', _slices: 4 }, + { begin: 0.25, end: 0.5, s: 'break', _slices: 4 }, + { begin: 0.5, end: 0.75, s: 'break', _slices: 4 }, + { begin: 0.75, end: 1, s: 'break', _slices: 4 }, + ), + ); + }); + }); + describe('splice', () => { + it('Can splice a sample', () => { + sameFirst( + s('break').splice(4, sequence(0, 1, 2, 3)), + sequence( + { begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 }, + { begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 }, + { begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 }, + { begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 }, + ), + ); + }); + }); }); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e53cf9fa..95ae2b41 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3690,6 +3690,39 @@ exports[`runs examples > example "sine" example index 0 1`] = ` ] `; +exports[`runs examples > example "slice" example index 0 1`] = ` +[ + "[ (15/16 → 1/1) ⇝ 9/8 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 3/4 → 15/16 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 21/32 → 3/4 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", + "[ 9/16 → 21/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 3/8 → 9/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", + "[ 3/16 → 3/8 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]", + "[ 0/1 → 3/16 | begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 21/16 → 3/2 | begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 9/8 → 21/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 15/16 ⇜ (1/1 → 9/8) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 3/2 → 27/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 27/16 → 15/8 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 15/8 → 63/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ (63/32 → 2/1) ⇝ 33/16 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 63/32 ⇜ (2/1 → 33/16) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 33/16 → 9/4 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 9/4 → 75/32 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", + "[ 75/32 → 39/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 39/16 → 21/8 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", + "[ 21/8 → 45/16 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]", + "[ 45/16 → 3/1 | begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 3/1 → 51/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 51/16 → 27/8 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 27/8 → 57/16 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 57/16 → 15/4 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 15/4 → 123/32 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", + "[ 123/32 → 63/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ (63/16 → 4/1) ⇝ 33/8 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", +] +`; + exports[`runs examples > example "slow" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:bd ]", @@ -3815,6 +3848,36 @@ exports[`runs examples > example "speed" example index 1 1`] = ` ] `; +exports[`runs examples > example "splice" example index 0 1`] = ` +[ + "[ 0/1 → 5/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 5/26 → 5/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 5/13 → 20/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 20/39 → 25/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 25/39 → 10/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 10/13 → 25/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ (25/26 → 1/1) ⇝ 35/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 25/26 ⇜ (1/1 → 35/26) | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 35/26 → 20/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 20/13 → 45/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 45/26 → 25/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ (25/13 → 2/1) ⇝ 80/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 25/13 ⇜ (2/1 → 80/39) | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 80/39 → 85/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 85/39 → 30/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 30/13 → 5/2 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 5/2 → 75/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ (75/26 → 3/1) ⇝ 40/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 75/26 ⇜ (3/1 → 40/13) | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 40/13 → 85/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 85/26 → 45/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 45/13 → 140/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 140/39 → 145/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 145/39 → 50/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ (50/13 → 4/1) ⇝ 105/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", +] +`; + exports[`runs examples > example "square" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:C3 ]",