Negative steps change euclid into euclidLegato and vice versa

Also fix that euclidLegato did not support negative pulses for inversion
This commit is contained in:
Tijmen Zwaan
2026-08-13 04:00:41 +02:00
parent 8f81463b9c
commit d7384dfc3d
3 changed files with 135 additions and 6 deletions
+29 -6
View File
@@ -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);
});
+56
View File
@@ -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', () => {
+50
View File
@@ -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]'));
});