diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 49f96384b..eba3f3303 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -59,6 +59,11 @@ export const bjorklund = function (ons, steps) { * really useful for computer/algorithmic music because they can * describe a large number of rhythms with a couple of numbers. * + * A negative number of pulses inverts the pattern + * so pulses happen in the gaps of the normal euclidean rhythm. + * + * A negative number of steps changes it into `euclidLegato`. + * * @memberof Pattern * @name euclid * @tags temporal @@ -137,8 +142,38 @@ const _euclidRot = function (pulses, steps, rotation) { return b; }; +const _euclidLegato = function (pulses, steps, rotation, pat) { + var bin_pat = _euclidRot(pulses, steps, 0); + // Because the `gapless` algorithm logic assumes the first entry in bin_pat is a 1, + // we rotate the pattern so the first onset is at the start. + const firstOnset = bin_pat.indexOf(1); + bin_pat = rotate(bin_pat, firstOnset); + steps = Math.abs(steps); + const gapless = bin_pat + .join('') + .split('1') + .slice(1) + .map((s) => [s.length + 1, true]); + return pat.struct(timeCat(...gapless).late(Fraction(rotation + firstOnset).div(steps))); +}; + +const _euclid = function (pulses, steps, rotation, pat) { + if (steps === 0 || pulses === 0) { + return silence; + } + const cycles = Math.ceil(Math.abs(pulses) / Math.abs(steps)); + const totalSteps = cycles * steps; + if (totalSteps < 0) { + return _euclidLegato(pulses, -totalSteps, rotation, pat.fast(cycles)).slow(cycles); + } + return pat + .fast(cycles) + .struct(_euclidRot(pulses, totalSteps, rotation)) + .slow(cycles); +}; + export const euclid = register('euclid', function (pulses, steps, pat) { - return pat.struct(_euclidRot(pulses, steps, 0)); + return _euclid(pulses, steps, 0, pat); }); export const bjork = register('bjork', function (euc, pat) { @@ -146,16 +181,18 @@ export const bjork = register('bjork', function (euc, pat) { euc = [euc]; } const [pulses, steps = pulses, rot = 0] = euc; - return pat.struct(_euclidRot(pulses, steps, rot)); + return _euclid(pulses, steps, rot, pat); }); export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { - return pat.struct(_euclidRot(pulses, steps, rotation)); + return _euclid(pulses, steps, rotation, pat); }); /** * Similar to `euclid`, but each pulse is held until the next pulse, * so there will be no gaps. + * + * A negative number of steps changes it into `euclid`. * @name euclidLegato * @memberof Pattern * @tags temporal @@ -167,27 +204,16 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun * note("c3").euclidLegato(3,8) */ -const _euclidLegato = function (pulses, steps, rotation, pat) { - if (pulses < 1) { - return silence; - } - const bin_pat = _euclidRot(pulses, steps, 0); - const gapless = bin_pat - .join('') - .split('1') - .slice(1) - .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps)); -}; - export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { - return _euclidLegato(pulses, steps, 0, pat); + return _euclid(pulses, -steps, 0, pat); }); /** * Similar to `euclid`, but each pulse is held until the next pulse, * so there will be no gaps, and has an additional parameter for 'rotating' - * the resulting sequence + * the resulting sequence. + * + * A negative number of steps changes it into `euclidRot`. * @name euclidLegatoRot * @memberof Pattern * @tags temporal @@ -198,7 +224,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * note("c3").euclidLegatoRot(3,5,2) */ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { - return _euclidLegato(pulses, steps, rotation, pat); + return _euclid(pulses, -steps, rotation, pat); }); /** diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js index 6169abd71..a5fc23857 100644 --- a/packages/core/test/euclid.test.js +++ b/packages/core/test/euclid.test.js @@ -29,6 +29,44 @@ describe('euclid', () => { .map((a) => a.showWhole(true)), ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); }); + it('Can create euclidLegato using negative steps', () => { + expect( + fastcat('a') + .euclid(3, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclidLegato(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + expect( + fastcat('a') + .euclid(5, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclidLegato(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + }); + it('stretches over multiple cycles if pulses is greater than steps', () => { + expect( + fastcat('a') + .euclid(5, 4) + .fast(2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); + }); }); describe('euclidRot', () => { @@ -67,6 +105,45 @@ describe('euclidLegato', () => { .map((a) => a.showWhole(true)), ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); }); + it('Can create euclid using negative steps', () => { + expect( + fastcat('a') + .euclidLegato(3, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclid(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + expect( + fastcat('a') + .euclidLegato(5, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclid(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + }); + it('stretches over multiple cycles if pulses is greater than steps', () => { + expect( + fastcat('a') + .euclidLegato(5, 4) + .fast(2) + .firstCycle() + .filter((hap) => hap.hasOnset()) + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); + }); }); describe('euclidLegatoRot', () => { diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index c38997308..c84819e87 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -84,6 +84,22 @@ describe('mini', () => { it('supports patterning euclidean rhythms', () => { expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)')); }); + it('supports inverted euclidean rhythms', () => { + expect(minS('a(-3, 8)')).toEqual(['a: 1/8 - 1/4', 'a: 1/4 - 3/8', 'a: 1/2 - 5/8', 'a: 5/8 - 3/4', 'a: 7/8 - 1']); + }); + it('supports euclidean legato rhythms', () => { + expect(minS('a(3, -8)')).toEqual(['a: 0 - 3/8', 'a: 3/8 - 3/4', 'a: 3/4 - 1']); + }); + it('supports inverted euclidean legato rhythms', () => { + expect(minS('a(-3, -8)')).toEqual([ + 'a: -1/8 - 1/8', + 'a: 1/8 - 1/4', + 'a: 1/4 - 1/2', + 'a: 1/2 - 5/8', + 'a: 5/8 - 7/8', + 'a: 7/8 - 9/8', + ]); + }); it("reproduces Toussaint's example euclidean algorithms", () => { const checkEuclid = function (spec, target) { expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target)); @@ -118,6 +134,40 @@ describe('mini', () => { checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~'); checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~'); }); + it("reproduces Toussaint's example euclidean legato algorithms", () => { + const checkEuclid = function (spec, target) { + expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target)); + }; + checkEuclid([1, -2], 'x _'); + checkEuclid([1, -3], 'x _ _'); + checkEuclid([1, -4], 'x _ _ _'); + checkEuclid([4, -12], 'x _ _ x _ _ x _ _ x _ _'); + checkEuclid([2, -5], 'x _ x _ _'); + // checkEuclid([3, 4], "x _ x x"); // Toussaint is wrong.. + checkEuclid([3, -4], 'x x x _'); // correction + checkEuclid([3, -5], 'x _ x _ x'); + checkEuclid([3, -7], 'x _ x _ x _ _'); + checkEuclid([3, -8], 'x _ _ x _ _ x _'); + checkEuclid([4, -7], 'x _ x _ x _ x'); + checkEuclid([4, -9], 'x _ x _ x _ x _ _'); + checkEuclid([4, -11], 'x _ _ x _ _ x _ _ x _'); + // checkEuclid([5, 6], "x _ x x x x"); // Toussaint is wrong.. + checkEuclid([5, -6], 'x x x x x _'); // correction + checkEuclid([5, -7], 'x _ x x _ x x'); + checkEuclid([5, -8], 'x _ x x _ x x _'); + checkEuclid([5, -9], 'x _ x _ x _ x _ x'); + checkEuclid([5, -11], 'x _ x _ x _ x _ x _ _'); + checkEuclid([5, -12], 'x _ _ x _ x _ _ x _ x _'); + // checkEuclid([5, 16], "x _ _ x _ _ x _ _ x _ _ x _ _ _ _"); // Toussaint is wrong.. + checkEuclid([5, -16], 'x _ _ x _ _ x _ _ x _ _ x _ _ _'); // correction + // checkEuclid([7, 8], "x _ x x x x x x"); // Toussaint is wrong.. + checkEuclid([7, -8], 'x x x x x x x _'); // Correction + checkEuclid([7, -12], 'x _ x x _ x _ x x _ x _'); + checkEuclid([7, -16], 'x _ _ x _ x _ x _ _ x _ x _ x _'); + checkEuclid([9, -16], 'x _ x x _ x _ x _ x x _ x _ x _'); + checkEuclid([11, -24], 'x _ _ x _ x _ x _ x _ x _ _ x _ x _ x _ x _ x _'); + checkEuclid([13, -24], 'x _ x x _ x _ x _ x _ x _ x x _ x _ x _ x _ x _'); + }); it('supports the - alias for ~', () => { expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]')); }); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ccb91377f..a0a81ada8 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4264,13 +4264,16 @@ exports[`runs examples > example "euclidLegatoRot" example index 0 1`] = ` "[ -1/5 ⇜ (0/1 → 1/5) | note:c3 ]", "[ 1/5 → 2/5 | note:c3 ]", "[ 2/5 → 4/5 | note:c3 ]", - "[ 4/5 → 6/5 | note:c3 ]", + "[ (4/5 → 1/1) ⇝ 6/5 | note:c3 ]", + "[ 4/5 ⇜ (1/1 → 6/5) | note:c3 ]", "[ 6/5 → 7/5 | note:c3 ]", "[ 7/5 → 9/5 | note:c3 ]", - "[ 9/5 → 11/5 | note:c3 ]", + "[ (9/5 → 2/1) ⇝ 11/5 | note:c3 ]", + "[ 9/5 ⇜ (2/1 → 11/5) | note:c3 ]", "[ 11/5 → 12/5 | note:c3 ]", "[ 12/5 → 14/5 | note:c3 ]", - "[ 14/5 → 16/5 | note:c3 ]", + "[ (14/5 → 3/1) ⇝ 16/5 | note:c3 ]", + "[ 14/5 ⇜ (3/1 → 16/5) | note:c3 ]", "[ 16/5 → 17/5 | note:c3 ]", "[ 17/5 → 19/5 | note:c3 ]", "[ (19/5 → 4/1) ⇝ 21/5 | note:c3 ]", diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 1fb178801..d33592bb4 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -7322,12 +7322,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` [ "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.598990320228 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.836992966942 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 0/1 → 3/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.921303892508 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.842607785016 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 3/2 → 9/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 9/4 → 3/1 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.700696257874 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.650794394314 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", @@ -7339,12 +7339,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.875811336562 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 6/1 → 15/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.75977108255 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.774316444062 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.836244757287 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 15/2 → 33/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 33/4 → 9/1 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.59140187595 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", @@ -7356,12 +7356,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.734787144698 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 12/1 → 27/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.979763559997 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.666239210404 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.95169514697 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 27/2 → 57/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 57/4 → 15/1 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.918253320269 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.571157105267 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", @@ -7373,12 +7373,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.945647025481 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 18/1 → 39/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.608644555323 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.506259439513 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.671621939167 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 39/2 → 81/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 81/4 → 21/1 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.704345900565 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.887838857248 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]",