Fix Bjorklund (#343)

* port Rohan Drape's Bjorklund implementation, add Toussaint's tests
* fix euclidLegato, simplifying a bit now that bjork results should always begin with an 'on'
* migrate euclid numbers in tunes
- 3,4 +1
- 5,8 -1
- 6,8 +3

Co-authored-by: Felix Roos <flix91@gmail.com>
This commit is contained in:
Alex McLean 2023-01-06 11:31:32 +00:00 committed by GitHub
parent 4fa668927f
commit 68c9008019
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 310 additions and 795 deletions

View File

@ -1,14 +1,59 @@
/*
euclid.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs>
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
Copyright (C) 2023 Rohan Drape and strudel contributors
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
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 } from './pattern.mjs';
import bjork from 'bjork';
import { rotate } from './util.mjs';
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten } from './util.mjs';
import Fraction from './fraction.mjs';
const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
const left = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_xs, __xs] = splitAt(offs, xs);
return [
[offs, ons - offs],
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
];
};
const right = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_ys, __ys] = splitAt(ons, ys);
const result = [
[ons, offs - ons],
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
];
return result;
};
const _bjork = function (n, x) {
const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
};
const bjork = function (ons, steps) {
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]);
return flatten(result[1][0]).concat(flatten(result[1][1]));
};
/**
* Changes the structure of the pattern to form an euclidean rhythm.
* Euclidian rhythms are rhythms obtained using the greatest common
@ -86,7 +131,7 @@ import Fraction from './fraction.mjs';
*/
const _euclidRot = function (pulses, steps, rotation) {
const b = bjork(steps, pulses);
const b = bjork(pulses, steps);
if (rotation) {
return rotate(b, -rotation);
}
@ -94,11 +139,11 @@ const _euclidRot = function (pulses, steps, rotation) {
};
export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(steps, pulses, 0));
return pat.struct(_euclidRot(pulses, steps, 0));
});
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(steps, pulses, rotation));
return pat.struct(_euclidRot(pulses, steps, rotation));
});
/**
@ -111,14 +156,16 @@ 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, rotation);
const firstOne = bin_pat.indexOf(1);
const gapless = rotate(bin_pat, firstOne)
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
return pat.struct(timeCat(...gapless));
};
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {

View File

@ -25,7 +25,6 @@
},
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"

View File

@ -79,6 +79,40 @@ describe('mini', () => {
it('supports patterning euclidean rhythms', () => {
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
});
it("reproduces Toussaint's example euclidean 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 ? operator', () => {
expect(
mini('a?')

File diff suppressed because it is too large Load Diff

View File

@ -167,9 +167,9 @@ export const sampleDrums = `samples({
}, 'https://loophole-letters.vercel.app/samples/tidal/')
stack(
"<bd!3 bd(3,4,2)>".color('#F5A623'),
"<bd!3 bd(3,4,3)>".color('#F5A623'),
"hh*4".color('#673AB7'),
"~ <sn!3 sn(3,4,1)>".color('#4CAF50')
"~ <sn!3 sn(3,4,2)>".color('#4CAF50')
).s()
.pianoroll({fold:1})
`;
@ -328,7 +328,7 @@ stack(
"<0 1 2 3>(3,8,2)"
.scale(scale)
.off(1/4, scaleTranspose("2,4")),
"<0 4>(5,8)".scale(scale).transpose(-12)
"<0 4>(5,8,-1)".scale(scale).transpose(-12)
)
.velocity(".6 .7".fast(4))
.legato("2")
@ -349,7 +349,7 @@ stack(
.add("<0 1 2 1>").hush(),
n("<0 1 2 3>(3,8,2)")
.off(1/4, add("2,4")),
n("<0 4>(5,8)").sub(7)
n("<0 4>(5,8,-1)").sub(7)
)
.scale(scale)
.gain(".6 .7".fast(4))
@ -363,7 +363,7 @@ stack(
export const echoPiano = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
"<0 2 [4 6](3,4,1) 3*2>"
"<0 2 [4 6](3,4,2) 3*2>"
.scale('D minor')
.color('salmon')
.off(1/4, x=>x.scaleTranspose(2).color('green'))
@ -425,7 +425,7 @@ stack(
.scale(cat('D minor pentatonic')).note()
.s('bell').gain(.6).delay(.2).delaytime(1/3).delayfeedback(.8),
// bass
"<D2 A2 G2 F2>".euclidLegatoRot(6,8,1).note().s('bass').clip(1).gain(.8)
"<D2 A2 G2 F2>".euclidLegatoRot(6,8,4).note().s('bass').clip(1).gain(.8)
)
.slow(6)
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'})
@ -464,7 +464,7 @@ samples({
}, 'https://freesound.org/data/previews/')
stack(
"-7 0 -7 7".struct("x(5,8,2)").fast(2).sub(7)
"-7 0 -7 7".struct("x(5,8,1)").fast(2).sub(7)
.scale(scales)
.n()
.s("sawtooth,square")
@ -520,7 +520,7 @@ export const meltingsubmarine = `// licensed with CC BY-NC-SA 4.0 https://creati
// by Felix Roos
await samples('github:tidalcycles/Dirt-Samples/master/')
stack(
s("bd:5,[~ <sd:1!3 sd:1(3,4,2)>],hh27(3,4)") // drums
s("bd:5,[~ <sd:1!3 sd:1(3,4,3)>],hh27(3,4,1)") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
//.hush()
,"<a1 b1*2 a1(3,8) e2>" // bassline
@ -542,7 +542,7 @@ stack(
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
//.hush()
,"a4 c5 <e6 a6>".struct("x(5,8)")
,"a4 c5 <e6 a6>".struct("x(5,8,-1)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
@ -673,7 +673,7 @@ stack(
.orbit(2).delay(.2).delaytime(".33 | .6 | .166 | .25")
.room(1).gain(.5).mask("<0 1>/8"),
// bell
note(rand.range(0,12).struct("x(5,8)").scale('g2 minor pentatonic')).s('bell').begin(.05)
note(rand.range(0,12).struct("x(5,8,-1)").scale('g2 minor pentatonic')).s('bell').begin(.05)
.delay(.2).degradeBy(.4).gain(.4)
.mask("<1 0>/8")
).slow(5)`;
@ -737,9 +737,9 @@ stack(
.rarely(x=>x.speed(".5").delay(.5))
.end(perlin.range(0.02,.05).slow(8))
.bank('RolandTR909').room(.5)
.gain("0.4,0.4(5,8)"),
.gain("0.4,0.4(5,8,-1)"),
note("<0 2 5 3>".scale('G1 minor')).struct("x(5,8)")
note("<0 2 5 3>".scale('G1 minor')).struct("x(5,8,-1)")
.s('sawtooth').decay(.1).sustain(0),
note("<G4 A4 Bb4 A4>,Bb3,D3").struct("~ x*2").s('square').clip(1)
@ -783,7 +783,7 @@ stack(
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
,
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8)>")`;
).reset("<x@7 x(5,8,-1)>")`;
export const juxUndTollerei = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
@ -824,7 +824,7 @@ instr CoolSynth
out(asig, asig)
endin\`
"<0 2 [4 6](3,4,1) 3*2>"
"<0 2 [4 6](3,4,2) 3*2>"
.off(1/4, add(2))
.off(1/2, add(6))
.scale('D minor')