diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 49f96384b..c477ddfc7 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 @@ -138,6 +143,9 @@ const _euclidRot = function (pulses, steps, rotation) { }; export const euclid = register('euclid', function (pulses, steps, pat) { + if (steps < 0) { + return _euclidLegato(pulses, -steps, 0, pat); + } return pat.struct(_euclidRot(pulses, steps, 0)); }); @@ -150,12 +158,17 @@ export const bjork = register('bjork', function (euc, pat) { }); export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { + if (steps < 0) { + return _euclidLegato(pulses, -steps, rotation, pat); + } return pat.struct(_euclidRot(pulses, steps, rotation)); }); /** * 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 @@ -168,26 +181,33 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun */ const _euclidLegato = function (pulses, steps, rotation, pat) { - if (pulses < 1) { - return silence; - } - const bin_pat = _euclidRot(pulses, steps, 0); + 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).div(steps)); + return pat.struct(timeCat(...gapless)).late(Fraction(rotation + firstOnset).div(steps)); }; export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { + if (steps < 0) { + return pat.struct(_euclidRot(pulses, -steps, 0)); + } return _euclidLegato(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,6 +218,9 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * note("c3").euclidLegatoRot(3,5,2) */ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { + if (steps < 0) { + return pat.struct(_euclidRot(pulses, -steps, rotation)); + } return _euclidLegato(pulses, steps, rotation, pat); }); diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js index 6169abd71..bf02a6109 100644 --- a/packages/core/test/euclid.test.js +++ b/packages/core/test/euclid.test.js @@ -29,6 +29,34 @@ 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)), + ); + }); }); describe('euclidRot', () => { @@ -67,6 +95,34 @@ 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)), + ); + }); }); 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]')); });