fix issue #1368 euclidLegatoRot

This commit is contained in:
Bernhard Wagner 2025-06-12 19:32:41 +02:00
parent db33707e52
commit d7b83e200c
8 changed files with 205 additions and 61 deletions

View file

@ -10,9 +10,8 @@ https://rohandrape.net/?t=hmt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction from './fraction.mjs';
const left = function (n, x) {
const [ons, offs] = n;
@ -42,29 +41,26 @@ const _bjork = function (n, x) {
export const bjork = function (ons, steps) {
const inverted = ons < 0;
ons = Math.abs(ons);
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]);
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
if (inverted) {
return p.map((x) => (x === 0 ? 1 : 0));
}
return p;
const absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]);
const result = _bjork([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern;
};
/**
* Changes the structure of the pattern to form an euclidean rhythm.
* Euclidian rhythms are rhythms obtained using the greatest common
* Changes the structure of the pattern to form an Euclidean rhythm.
* Euclidean rhythms are rhythms obtained using the greatest common
* divisor of two numbers. They were described in 2004 by Godfried
* Toussaint, a canadian computer scientist. Euclidian rhythms are
* Toussaint, a Canadian computer scientist. Euclidean rhythms are
* really useful for computer/algorithmic music because they can
* describe a large number of rhythms with a couple of numbers.
*
* @memberof Pattern
* @name euclid
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @returns Pattern
* @example
@ -76,7 +72,7 @@ export const bjork = function (ons, steps) {
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
* @memberof Pattern
* @name euclidRot
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @returns Pattern
@ -86,13 +82,13 @@ export const bjork = function (ons, steps) {
*/
/**
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
* @example // A thirteenth-century Persian rhythm called Khafif-e-ramal.
* note("c3").euclid(2,5)
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
* note("c3").euclid(3,4)
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
* note("c3").euclidRot(3,5,2)
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
* @example // A Ruchenitza rhythm used in a Bulgarian folk dance.
* note("c3").euclid(3,7)
* @example // The Cuban tresillo pattern.
* note("c3").euclid(3,8)
@ -151,8 +147,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param rotation offset in steps
* @param pat
* @example
* note("c3").euclidLegato(3,8)
*/
@ -161,13 +159,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
}
const bin_pat = _euclidRot(pulses, steps, rotation);
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));
return pat.struct(timeCat(...gapless)).late(rotation / steps);
};
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
@ -180,7 +178,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
* the resulting sequence
* @name euclidLegatoRot
* @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @example

View file

@ -0,0 +1,89 @@
import { bjork } from '../euclid.mjs';
import { describe, expect, it } from 'vitest';
import { fastcat } from '../pattern.mjs';
describe('bjork', () => {
it('should apply bjorklund to ons and steps', () => {
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
});
});
describe('euclid', () => {
it('Can create euclid', () => {
expect(
fastcat('a')
.euclid(3, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']);
expect(
fastcat('a')
.euclid(5, 8)
.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', () => {
it('Can create euclidRot', () => {
expect(
fastcat('a')
.euclidRot(3, 8, 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', '5/8 → 3/4: a']);
expect(
fastcat('a')
.euclidRot(5, 8, 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', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']);
});
});
describe('euclidLegato', () => {
it('Can create euclidLegato', () => {
expect(
fastcat('a')
.euclidLegato(3, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']);
expect(
fastcat('a')
.euclidLegato(5, 8)
.firstCycle()
.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', () => {
it('Can create euclidLegatoRot', () => {
expect(
fastcat('a')
.euclidLegatoRot(3, 8, 2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']);
expect(
fastcat('a')
.euclidLegatoRot(5, 8, 2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']);
});
});

View file

@ -6,21 +6,25 @@ This program is free software: you can redistribute it and/or modify it under th
import { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
flatten,
fractionalArgs,
freqToMidi,
getFrequency,
getPlayableNoteValue,
parseNumeral,
parseFractional,
isNote,
midiToFreq,
noteToMidi,
numeralArgs,
fractionalArgs,
parseFractional,
parseNumeral,
rotate,
splitAt,
tokenizeNote,
zipWith,
} from '../util.mjs';
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
@ -233,3 +237,46 @@ describe('fractionalArgs', () => {
expect(add('q', 2)).toBe(2.25);
});
});
describe('rotate', () => {
it('should rotate array to the left', () => {
expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]);
expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]);
expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]);
expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]);
expect(rotate([0], 3)).toStrictEqual([0]);
expect(rotate([], 3)).toStrictEqual([]);
});
});
describe('flatten', () => {
it('should flatten array by one level', () => {
expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]);
expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]);
expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]);
expect(flatten([0])).toStrictEqual([0]);
expect(flatten([])).toStrictEqual([]);
});
});
describe('splitAt', () => {
it('should split array into two', () => {
expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([
[0, 1],
[2, 3],
]);
expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]);
expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]);
expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]);
});
});
describe('zipWith', () => {
it('should use the function to combine the two arrays element-wise', () => {
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]);
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]);
expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]);
expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]);
expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]);
});
});

View file

@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse());
// Removes 'None' values from given list
export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
// flattens by one level
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
@ -237,6 +238,7 @@ export const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
// Uses the function f to combine the arrays xs, ys element-wise
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const pairs = function (xs) {