From ee3e1217b40f1f2b884e925602308df2a4ea9be9 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:24:19 +0200 Subject: [PATCH 1/4] add euclidish --- packages/core/euclid.mjs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 25e12b07c..38ae6f673 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ 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 . */ -import { timeCat, register, silence } from './pattern.mjs'; +import { timeCat, register, silence, fastGap } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; @@ -196,3 +196,40 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { return _euclidLegato(pulses, steps, rotation, pat); }); + +/** + * A 'euclid' variant with an additional parameter that morphs the resulting + * rhythm from 0 (no morphing) to 1 (completely 'even'). For example + * `sound("bd").euclidish(3,8,0)` would be the same as + * `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the + * same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a + * groove somewhere between. + * Inspired by the work of Malcom Braff. + * @name euclidish + * @synonyms eish + * @memberof Pattern + * @param {number} pulses the number of onsets + * @param {number} steps the number of steps to fill + * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) + * @example + * sound("hh").euclidish(7,12,tri.slow(8)) + * .pan(tri.slow(8)) + * @example + * sound("bd").euclidish(7,12,slider(0,0.1,1)) + */ +export const [euclidish, eish] = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { + const b = bjork(pulses, steps); + let trues = 0; + const offs = []; + for (const [pos, step] of b.entries()) { + if (step) { + offs.push([trues++, pos]); + } + } + const tweened = offs.map(([n, pos]) => + Fraction(pos) + .div(steps) + .add(Fraction(n).div(pulses).sub(Fraction(pos).div(steps)).mul(perc)), + ); + return pat.struct(stack(...tweened.map((pos) => pure(true)._fastGap(steps)._late(pos)))).setSteps(steps); +}); From 784ee576da9be9114ae1fad2ac9317484f898c06 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:33:18 +0200 Subject: [PATCH 2/4] fixes --- packages/core/euclid.mjs | 4 +-- test/__snapshots__/examples.test.mjs.snap | 33 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 38ae6f673..3dfb152c2 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -214,10 +214,8 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @example * sound("hh").euclidish(7,12,tri.slow(8)) * .pan(tri.slow(8)) - * @example - * sound("bd").euclidish(7,12,slider(0,0.1,1)) */ -export const [euclidish, eish] = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { +export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { const b = bjork(pulses, steps); let trues = 0; const offs = []; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..2f8404d05 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3271,6 +3271,39 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = ` ] `; +exports[`runs examples > example "euclidish" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | s:hh pan:0 ]", + "[ 1/6 → 1/4 | s:hh pan:0.041666666666666664 ]", + "[ 1/4 → 1/3 | s:hh pan:0.0625 ]", + "[ 5/12 → 1/2 | s:hh pan:0.10416666666666667 ]", + "[ 7/12 → 2/3 | s:hh pan:0.14583333333333334 ]", + "[ 2/3 → 3/4 | s:hh pan:0.16666666666666666 ]", + "[ 5/6 → 11/12 | s:hh pan:0.20833333333333334 ]", + "[ 1/1 → 13/12 | s:hh pan:0.25 ]", + "[ 65/56 → 209/168 | s:hh pan:0.29017857142857145 ]", + "[ 141/112 → 451/336 | s:hh pan:0.31473214285714285 ]", + "[ 159/112 → 505/336 | s:hh pan:0.3549107142857143 ]", + "[ 177/112 → 559/336 | s:hh pan:0.3950892857142857 ]", + "[ 47/28 → 37/21 | s:hh pan:0.41964285714285715 ]", + "[ 103/56 → 323/168 | s:hh pan:0.45982142857142855 ]", + "[ 2/1 → 25/12 | s:hh pan:0.5 ]", + "[ 181/84 → 47/21 | s:hh pan:0.5386904761904762 ]", + "[ 127/56 → 395/168 | s:hh pan:0.5669642857142857 ]", + "[ 407/168 → 421/168 | s:hh pan:0.6056547619047619 ]", + "[ 433/168 → 149/56 | s:hh pan:0.6443452380952381 ]", + "[ 113/42 → 233/84 | s:hh pan:0.6726190476190477 ]", + "[ 239/84 → 41/14 | s:hh pan:0.7113095238095238 ]", + "[ 3/1 → 37/12 | s:hh pan:0.75 ]", + "[ 529/168 → 181/56 | s:hh pan:0.7872023809523809 ]", + "[ 367/112 → 1129/336 | s:hh pan:0.8191964285714286 ]", + "[ 1151/336 → 393/112 | s:hh pan:0.8563988095238095 ]", + "[ 1201/336 → 1229/336 | s:hh pan:0.8936011904761905 ]", + "[ 311/84 → 53/14 | s:hh pan:0.9255952380952381 ]", + "[ 647/168 → 661/168 | s:hh pan:0.9627976190476191 ]", +] +`; + exports[`runs examples > example "every" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", From 03cbe4b6d4f567e763328b595ed5ef617d2a6e90 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:48:09 +0200 Subject: [PATCH 3/4] delint --- packages/core/euclid.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 3dfb152c2..bf5a82b00 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ 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 . */ -import { timeCat, register, silence, fastGap } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; From b7176155589f6533346d0557172bd1ef72e859f3 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 17 Aug 2025 22:13:26 +0100 Subject: [PATCH 4/4] optimise euclidish, creating new `morph` function in the process --- packages/core/euclid.mjs | 22 ++--- packages/core/pattern.mjs | 67 ++++++++++++++++ packages/core/timespan.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 98 ++++++++++++++++------- 4 files changed, 143 insertions(+), 46 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index bf5a82b00..44ab07f11 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ 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 . */ -import { timeCat, register, silence, stack, pure } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; @@ -212,22 +212,10 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @param {number} steps the number of steps to fill * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) * @example - * sound("hh").euclidish(7,12,tri.slow(8)) - * .pan(tri.slow(8)) + * sound("hh").euclidish(7,12,sine.slow(8)) + * .pan(sine.slow(8)) */ export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { - const b = bjork(pulses, steps); - let trues = 0; - const offs = []; - for (const [pos, step] of b.entries()) { - if (step) { - offs.push([trues++, pos]); - } - } - const tweened = offs.map(([n, pos]) => - Fraction(pos) - .div(steps) - .add(Fraction(n).div(pulses).sub(Fraction(pos).div(steps)).mul(perc)), - ); - return pat.struct(stack(...tweened.map((pos) => pure(true)._fastGap(steps)._late(pos)))).setSteps(steps); + const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc); + return pat.struct(morphed).setSteps(steps); }); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4168645b8..ada13f65b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,7 @@ import { numeralArgs, parseNumeral, pairs, + zipWith, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -3400,3 +3401,69 @@ export const { beat } = register( ['beat'], __beat((x) => x.innerJoin()), ); + +export const _morph = (from, to, by) => { + by = Fraction(by); + const dur = Fraction(1).div(from.length); + const positions = (list) => { + const result = []; + for (const [pos, value] of list.entries()) { + if (value) { + result.push([Fraction(pos).div(list.length), value]); + } + } + return result; + }; + const arcs = zipWith( + ([posa, valuea], [posb, valueb]) => { + const b = by.mul(posb - posa).add(posa); + const e = b.add(dur); + return new TimeSpan(b, e); + }, + positions(from), + positions(to), + ); + function query(state) { + const cycle = state.span.begin.sam(); + const cycleArc = state.span.cycleArc(); + const result = []; + for (const whole of arcs) { + const part = whole.intersection(cycleArc); + if (part !== undefined) { + result.push( + new Hap( + whole.withTime((x) => x.add(cycle)), + part.withTime((x) => x.add(cycle)), + true, + ), + ); + } + } + return result; + } + return new Pattern(query).splitQueries(); +}; + +/** + * Takes two binary rhythms represented as lists of 1s and 0s, and a number + * between 0 and 1 that morphs between them. The two lists should contain the same + * number of true values. + * @example + * sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm + * [1,1,0,1,0,1,0], // wonky rhythm + * 0.25 // creates a slightly wonky rhythm + * ) + * ) + * @example + * sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm + * "1:1:0:1:0:1:0", // wonky rhythm + * sine.slow(8) // slowly morph between the rhythms + * ) + * ) + */ +export const morph = (frompat, topat, bypat) => { + frompat = reify(frompat); + topat = reify(topat); + bypat = reify(bypat); + return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); +}; diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index 0dbc74fc8..446156bbf 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -72,7 +72,7 @@ export class TimeSpan { } intersection(other) { - // Intersection of two timespans, returns None if they don't intersect. + // Intersection of two timespans, returns undefined if they don't intersect. const intersect_begin = this.begin.max(other.begin); const intersect_end = this.end.min(other.end); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 83dd8c029..079e6ff3b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3273,34 +3273,34 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = ` exports[`runs examples > example "euclidish" example index 0 1`] = ` [ - "[ 0/1 → 1/12 | s:hh pan:0 ]", - "[ 1/6 → 1/4 | s:hh pan:0.041666666666666664 ]", - "[ 1/4 → 1/3 | s:hh pan:0.0625 ]", - "[ 5/12 → 1/2 | s:hh pan:0.10416666666666667 ]", - "[ 7/12 → 2/3 | s:hh pan:0.14583333333333334 ]", - "[ 2/3 → 3/4 | s:hh pan:0.16666666666666666 ]", - "[ 5/6 → 11/12 | s:hh pan:0.20833333333333334 ]", - "[ 1/1 → 13/12 | s:hh pan:0.25 ]", - "[ 65/56 → 209/168 | s:hh pan:0.29017857142857145 ]", - "[ 141/112 → 451/336 | s:hh pan:0.31473214285714285 ]", - "[ 159/112 → 505/336 | s:hh pan:0.3549107142857143 ]", - "[ 177/112 → 559/336 | s:hh pan:0.3950892857142857 ]", - "[ 47/28 → 37/21 | s:hh pan:0.41964285714285715 ]", - "[ 103/56 → 323/168 | s:hh pan:0.45982142857142855 ]", - "[ 2/1 → 25/12 | s:hh pan:0.5 ]", - "[ 181/84 → 47/21 | s:hh pan:0.5386904761904762 ]", - "[ 127/56 → 395/168 | s:hh pan:0.5669642857142857 ]", - "[ 407/168 → 421/168 | s:hh pan:0.6056547619047619 ]", - "[ 433/168 → 149/56 | s:hh pan:0.6443452380952381 ]", - "[ 113/42 → 233/84 | s:hh pan:0.6726190476190477 ]", - "[ 239/84 → 41/14 | s:hh pan:0.7113095238095238 ]", - "[ 3/1 → 37/12 | s:hh pan:0.75 ]", - "[ 529/168 → 181/56 | s:hh pan:0.7872023809523809 ]", - "[ 367/112 → 1129/336 | s:hh pan:0.8191964285714286 ]", - "[ 1151/336 → 393/112 | s:hh pan:0.8563988095238095 ]", - "[ 1201/336 → 1229/336 | s:hh pan:0.8936011904761905 ]", - "[ 311/84 → 53/14 | s:hh pan:0.9255952380952381 ]", - "[ 647/168 → 661/168 | s:hh pan:0.9627976190476191 ]", + "[ 0/1 → 1/12 | s:hh pan:0.5 ]", + "[ 13/84 → 5/21 | s:hh pan:0.5606253170575308 ]", + "[ 15/56 → 59/168 | s:hh pan:0.604413082836085 ]", + "[ 71/168 → 85/168 | s:hh pan:0.6629314122869361 ]", + "[ 97/168 → 37/56 | s:hh pan:0.7190455010067492 ]", + "[ 29/42 → 65/84 | s:hh pan:0.7580531369037533 ]", + "[ 71/84 → 13/14 | s:hh pan:0.8080762739548087 ]", + "[ 1/1 → 13/12 | s:hh pan:0.8535533905932737 ]", + "[ 451099417/393511398 → 322594689/262340932 | s:hh pan:0.891768001805729 ]", + "[ 335923379/262340932 → 536677685/393511398 | s:hh pan:0.9222657853371297 ]", + "[ 1122946175/787022796 → 99044284/65585233 | s:hh pan:0.9501869591788796 ]", + "[ 1238122213/787022796 → 651853723/393511398 | s:hh pan:0.9721673436944069 ]", + "[ 335923379/196755699 → 469759583/262340932 | s:hh pan:0.9868472639237561 ]", + "[ 729434777/393511398 → 1524454787/787022796 | s:hh pan:0.9967009321321423 ]", + "[ 2/1 → 25/12 | s:hh pan:1 ]", + "[ 15/7 → 187/84 | s:hh pan:0.9968561049466214 ]", + "[ 16/7 → 199/84 | s:hh pan:0.9874639560909118 ]", + "[ 17/7 → 211/84 | s:hh pan:0.9719416651541839 ]", + "[ 18/7 → 223/84 | s:hh pan:0.9504844339512095 ]", + "[ 19/7 → 235/84 | s:hh pan:0.9233620996141421 ]", + "[ 20/7 → 247/84 | s:hh pan:0.890915741234015 ]", + "[ 3/1 → 37/12 | s:hh pan:0.8535533905932737 ]", + "[ 1238122213/393511398 → 847276553/262340932 | s:hh pan:0.8106731928589048 ]", + "[ 860605243/262340932 → 1323700481/393511398 | s:hh pan:0.7677528833339 ]", + "[ 2696991767/787022796 → 230214750/65585233 | s:hh pan:0.7175585019834292 ]", + "[ 2812167805/787022796 → 1438876519/393511398 | s:hh pan:0.6644931595798675 ]", + "[ 729434777/196755699 → 994441447/262340932 | s:hh pan:0.6139286689554151 ]", + "[ 1516457573/393511398 → 3098500379/787022796 | s:hh pan:0.557342689325327 ]", ] `; @@ -5997,6 +5997,48 @@ exports[`runs examples > example "miditouch" example index 0 1`] = ` ] `; +exports[`runs examples > example "morph" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 25/112 → 39/112 | s:hh ]", + "[ 27/56 → 17/28 | s:hh ]", + "[ 83/112 → 97/112 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 137/112 → 151/112 | s:hh ]", + "[ 83/56 → 45/28 | s:hh ]", + "[ 195/112 → 209/112 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 249/112 → 263/112 | s:hh ]", + "[ 139/56 → 73/28 | s:hh ]", + "[ 307/112 → 321/112 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 361/112 → 375/112 | s:hh ]", + "[ 195/56 → 101/28 | s:hh ]", + "[ 419/112 → 433/112 | s:hh ]", +] +`; + +exports[`runs examples > example "morph" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 11/56 → 9/28 | s:hh ]", + "[ 13/28 → 33/56 | s:hh ]", + "[ 41/56 → 6/7 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 303934523/262340932 → 673454279/524681864 | s:hh ]", + "[ 188758485/131170466 → 820619173/524681864 | s:hh ]", + "[ 451099417/262340932 → 967784067/524681864 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 15/7 → 127/56 | s:hh ]", + "[ 17/7 → 143/56 | s:hh ]", + "[ 19/7 → 159/56 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 828616387/262340932 → 1722818007/524681864 | s:hh ]", + "[ 451099417/131170466 → 1869982901/524681864 | s:hh ]", + "[ 975781281/262340932 → 2017147795/524681864 | s:hh ]", +] +`; + exports[`runs examples > example "mousex" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]",