From cc445e89a36cd96400f9641fbba56cbb15556bff Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Apr 2022 09:21:40 +0100 Subject: [PATCH 01/18] cat/append/fastcat/slowcat as pattern methods --- packages/core/pattern.mjs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 8f1f7d61..c2f2a17c 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -598,10 +598,6 @@ export class Pattern { return slowcatPrime(...pats); } - append(other) { - return fastcat(...[this, other]); - } - rev() { const pat = this; const query = function (state) { @@ -644,14 +640,30 @@ export class Pattern { return this.juxBy(1, func); } - // is there a different name for those in tidal? stack(...pats) { return stack(this, ...pats); } + sequence(...pats) { return sequence(this, ...pats); } + cat(...pats) { + return cat(this, ...pats); + } + + append(...pats) { + return cat(this, ...pats); + } + + fastcat(...pats) { + return fastcat(this, ...pats); + } + + slowcat(...pats) { + return slowcat(this, ...pats); + } + superimpose(...funcs) { return this.stack(...funcs.map((func) => func(this))); } From 9df20fcbc5f9d2ef01ac90010427c8baca3f461a Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Apr 2022 09:38:05 +0100 Subject: [PATCH 02/18] support `cat`ting subpatterns, fixes #87 --- packages/core/pattern.mjs | 5 ++++- packages/core/test/pattern.test.mjs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 3662b405..4c8df71d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -837,7 +837,10 @@ export function stack(...pats) { export function slowcat(...pats) { // Concatenation: combines a list of patterns, switching between them // successively, one per cycle. - pats = pats.map(reify); + + // Array test here is to avoid infinite recursions.. + pats = pats.map(pat => Array.isArray(pat) ? sequence(...pat) : reify(pat)); + const query = function (state) { const span = state.span; const pat_n = mod(span.begin.sam(), pats.length); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1c60766a..17385c78 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -413,6 +413,12 @@ describe('Pattern', function () { ['c'], ); }); + it ('Can cat subpatterns', () => { + sameFirst( + slowcat('a', ['b','c']).fast(4), + sequence('a', ['b', 'c']).fast(2) + ) + }) }); describe('rev()', function () { it('Can reverse things', function () { From a4cacc079e8585889508b09a87f9c3c1f09f1164 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Apr 2022 09:47:12 +0100 Subject: [PATCH 03/18] Support subpattern sequences in `stack` like `slowcat` in ref #87 --- packages/core/pattern.mjs | 8 +++++--- packages/core/test/pattern.test.mjs | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4c8df71d..f8a6e411 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -826,11 +826,13 @@ export function reify(thing) { } return pure(thing); } + // Basic functions for combining patterns export function stack(...pats) { - const reified = pats.map((pat) => reify(pat)); - const query = (state) => flatten(reified.map((pat) => pat.query(state))); + // Array test here is to avoid infinite recursions.. + pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); + const query = (state) => flatten(pats.map((pat) => pat.query(state))); return new Pattern(query); } @@ -839,7 +841,7 @@ export function slowcat(...pats) { // successively, one per cycle. // Array test here is to avoid infinite recursions.. - pats = pats.map(pat => Array.isArray(pat) ? sequence(...pat) : reify(pat)); + pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); const query = function (state) { const span = state.span; diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 17385c78..46308912 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -247,6 +247,12 @@ describe('Pattern', function () { ['a', 'b', 'c'], ); }); + it('Can stack subpatterns', function () { + sameFirst( + stack('a', ['b','c']), + stack('a', sequence('b', 'c')), + ); + }); }); describe('_fast()', function () { it('Makes things faster', function () { From 4c43d3d85051b2b3dbc44c4d265223fc2622bb41 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Apr 2022 11:01:12 +0100 Subject: [PATCH 04/18] Make sequence a `fastcat` alias and change `cat` to be a `slowcat` ref #87 --- packages/core/pattern.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f8a6e411..6e8c36e1 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -879,7 +879,7 @@ export function fastcat(...pats) { } export function cat(...pats) { - return fastcat(...pats); + return slowcat(...pats); } export function timeCat(...timepats) { @@ -895,6 +895,10 @@ export function timeCat(...timepats) { return stack(...pats); } +export function sequence(...pats) { + return fastcat(...pats); +} + function _sequenceCount(x) { if (Array.isArray(x)) { if (x.length == 0) { @@ -908,10 +912,6 @@ function _sequenceCount(x) { return [reify(x), 1]; } -export function sequence(...xs) { - return _sequenceCount(xs)[0]; -} - export function polymeterSteps(steps, ...args) { const seqs = args.map((a) => _sequenceCount(a)); if (seqs.length == 0) { From ff418c9f0c43d8c3664c8253cfaa04e607f270eb Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Apr 2022 11:10:05 +0100 Subject: [PATCH 05/18] seq -> sequence shorthand ref #87 --- packages/core/pattern.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6e8c36e1..ce49c537 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -647,6 +647,11 @@ export class Pattern { return sequence(this, ...pats); } + // shorthand for sequence + seq(...pats) { + return sequence(this, ...pats); + } + cat(...pats) { return cat(this, ...pats); } @@ -899,6 +904,11 @@ export function sequence(...pats) { return fastcat(...pats); } +// shorthand for sequence +export function seq(...pats) { + return fastcat(...pats); +} + function _sequenceCount(x) { if (Array.isArray(x)) { if (x.length == 0) { From 615f71b099f886f413ce38c514a32703970a3bff Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Apr 2022 11:11:45 +0100 Subject: [PATCH 06/18] Fix evaluate test with new cat behaviour ref #87 --- packages/eval/test/evaluate.test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eval/test/evaluate.test.mjs b/packages/eval/test/evaluate.test.mjs index 99c0c311..9fbedb20 100644 --- a/packages/eval/test/evaluate.test.mjs +++ b/packages/eval/test/evaluate.test.mjs @@ -3,7 +3,7 @@ import { evaluate, extend } from '../evaluate.mjs'; import { mini } from '@strudel.cycles/mini'; import * as strudel from '@strudel.cycles/core'; -const { cat } = strudel; +const { fastcat } = strudel; extend({ mini }, strudel); @@ -12,11 +12,11 @@ describe('evaluate', () => { it('Should evaluate strudel functions', async () => { assert.deepStrictEqual(await ev("pure('c3')"), ['c3']); assert.deepStrictEqual(await ev('cat(c3)'), ['c3']); - assert.deepStrictEqual(await ev('cat(c3, d3)'), ['c3', 'd3']); + assert.deepStrictEqual(await ev('fastcat(c3, d3)'), ['c3', 'd3']); assert.deepStrictEqual(await ev('slowcat(c3, d3)'), ['c3']); }); it('Should be extendable', async () => { - extend({ myFunction: (...x) => cat(...x) }); + extend({ myFunction: (...x) => fastcat(...x) }); assert.deepStrictEqual(await ev('myFunction(c3, d3)'), ['c3', 'd3']); }); it('Should evaluate simple double quoted mini notation', async () => { From dd2552eda8ef2e6ff7a410cac62e349d341a815a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 20:27:16 +0200 Subject: [PATCH 07/18] add seq to factories for highlighting --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index ce49c537..b5879d1d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -803,7 +803,7 @@ Pattern.prototype.patternified = [ 'velocity', ]; // methods that create patterns, which are added to patternified Pattern methods -Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr }; +Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, sequence, seq, polymeter, pm, polyrhythm, pr }; // the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts) // Elemental patterns From f1c0d8c8f838792e4a961369d0dad41dbf12f5b0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 20:27:40 +0200 Subject: [PATCH 08/18] refactor cat -> seq --- repl/src/tunes.mjs | 112 +++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/repl/src/tunes.mjs b/repl/src/tunes.mjs index 23693556..c75784be 100644 --- a/repl/src/tunes.mjs +++ b/repl/src/tunes.mjs @@ -5,16 +5,16 @@ export const timeCatMini = `stack( )`; export const timeCat = `stack( - timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]), - cat(c2, g2), - sequence( - timeCat([5, eb4], [3, cat(f4, eb4, d4)]), - cat(eb4, c4).slow(2) + timeCat([3, c3], [1, stack(eb3, g3, seq(c4, d4).slow(2))]), + seq(c2, g2), + seq( + timeCat([5, eb4], [3, seq(f4, eb4, d4)]), + seq(eb4, c4).slow(2) ).slow(4) )`; export const shapeShifted = `stack( - sequence( + seq( e5, [b4, c5], d5, [c5, b4], a4, [a4, c5], e5, [d5, c5], b4, [r, c5], d5, e5, @@ -24,7 +24,7 @@ export const shapeShifted = `stack( b4, [b4, c5], d5, e5, c5, a4, a4, r, ).rev(), - sequence( + seq( e2, e3, e2, e3, e2, e3, e2, e3, a2, a3, a2, a3, a2, a3, a2, a3, gs2, gs3, gs2, gs3, e2, e3, e2, e3, @@ -36,16 +36,16 @@ export const shapeShifted = `stack( ).rev() ).slow(16)`; -export const tetrisWithFunctions = `stack(sequence( - 'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'), - 'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'), - 'b4', sequence(silence, 'c5'), 'd5', 'e5', - 'c5', 'a4', 'a4', silence, - sequence(silence, 'd5'), sequence(silence, 'f5'), 'a5', sequence('g5', 'f5'), - 'e5', sequence(silence, 'c5'), 'e5', sequence('d5', 'c5'), - 'b4', sequence('b4', 'c5'), 'd5', 'e5', - 'c5', 'a4', 'a4', silence), - sequence( +/* export const tetrisWithFunctions = `stack(seq( + 'e5', seq('b4', 'c5'), 'd5', seq('c5', 'b4'), + 'a4', seq('a4', 'c5'), 'e5', seq('d5', 'c5'), + 'b4', seq(r, 'c5'), 'd5', 'e5', + 'c5', 'a4', 'a4', r, + seq(r, 'd5'), seq(r, 'f5'), 'a5', seq('g5', 'f5'), + 'e5', seq(r, 'c5'), 'e5', seq('d5', 'c5'), + 'b4', seq('b4', 'c5'), 'd5', 'e5', + 'c5', 'a4', 'a4', r), + seq( 'e2', 'e3', 'e2', 'e3', 'e2', 'e3', 'e2', 'e3', 'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'g#2', 'g#3', 'g#2', 'g#3', 'e2', 'e3', 'e2', 'e3', @@ -55,10 +55,10 @@ export const tetrisWithFunctions = `stack(sequence( 'b1', 'b2', 'b1', 'b2', 'e2', 'e3', 'e2', 'e3', 'a1', 'a2', 'a1', 'a2', 'a1', 'a2', 'a1', 'a2', ) -).slow(16)`; +).slow(16)`; */ export const tetris = `stack( - cat( + seq( "e5 [b4 c5] d5 [c5 b4]", "a4 [a4 c5] e5 [d5 c5]", "b4 [~ c5] d5 e5", @@ -68,7 +68,7 @@ export const tetris = `stack( "b4 [b4 c5] d5 e5", "c5 a4 a4 ~" ), - cat( + seq( "e2 e3 e2 e3 e2 e3 e2 e3", "a2 a3 a2 a3 a2 a3 a2 a3", "g#2 g#3 g#2 g#3 e2 e3 e2 e3", @@ -98,14 +98,14 @@ export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]] [[a1 a2]*4]\`.slow(16) `; -export const whirlyStrudel = `sequence(e4, [b2, b3], c4) +export const whirlyStrudel = `seq(e4, [b2, b3], c4) .every(4, fast(2)) .every(3, slow(1.5)) -.fast(slowcat(1.25, 1, 1.5)) -.every(2, _ => sequence(e4, r, e3, d4, r))`; +.fast(cat(1.25, 1, 1.5)) +.every(2, _ => seq(e4, r, e3, d4, r))`; export const swimming = `stack( - cat( + seq( "~", "~", "~", @@ -124,7 +124,7 @@ export const swimming = `stack( "A5 [F5@2 C5] [D5@2 F5] F5", "[C5@2 F5] [Bb5 A5 G5] F5@2" ), - cat( + seq( "[F4,Bb4,D5] [[D4,G4,Bb4]@2 [Bb3,D4,F4]] [[G3,C4,E4]@2 [[Ab3,F4] [A3,Gb4]]] [Bb3,E4,G4]", "[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, Bb3, Db3] [F3, Bb3, Db3]]", "[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]", @@ -143,7 +143,7 @@ export const swimming = `stack( "[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]", "[~ [F3, Bb3, D4] [F3, Bb3, D4]] [~ [F3, Bb3, C4] [F3, Bb3, C4]] [~ [F3, A3, C4] [F3, A3, C4]] [~ [F3, A3, C4] [F3, A3, C4]]" ), - cat( + seq( "[G3 G3 C3 E3]", "[F2 D2 G2 C2]", "[F2 D2 G2 C2]", @@ -167,38 +167,38 @@ export const swimming = `stack( export const giantSteps = `stack( // melody - cat( + seq( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", "Bb4 [B4 A4] D5 [D#5 C#5]", "F#5 [G5 F5] Bb5 [F#5 F#5]", ), // chords - cat( + seq( "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]", "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]", "Eb^7 [Am7 D7] G^7 [C#m7 F#7]", "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]" ).voicings(['E3', 'G4']), // bass - cat( + seq( "[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]", "[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", "[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]" ) -).slow(20);`; +).slow(20)`; export const giantStepsReggae = `stack( // melody - cat( + seq( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", "Bb4 [B4 A4] D5 [D#5 C#5]", "F#5 [G5 F5] Bb5 [F#5 [F#5 ~@3]]", ), // chords - cat( + seq( "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]", "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]", "Eb^7 [Am7 D7] G^7 [C#m7 F#7]", @@ -207,7 +207,7 @@ export const giantStepsReggae = `stack( .struct("~ [x ~]".fast(4*8)) .voicings(['E3', 'G4']), // bass - cat( + seq( "[B2 D2] [G2 D2] [Eb2 Bb2] [A2 D2]", "[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", @@ -220,13 +220,13 @@ export const transposedChordsHacked = `stack( "c2 eb2 g2", "Cm7".voicings(['g2','c4']).slow(2) ).transpose( - slowcat(1, 2, 3, 2).slow(2) + cat(1, 2, 3, 2).slow(2) ).transpose(5)`; export const scaleTranspose = `stack(f2, f3, c4, ab4) -.scale(sequence('F minor', 'F harmonic minor').slow(4)) -.scaleTranspose(sequence(0, -1, -2, -3).slow(4)) -.transpose(sequence(0, 1).slow(16))`; +.scale(seq('F minor', 'F harmonic minor').slow(4)) +.scaleTranspose(seq(0, -1, -2, -3).slow(4)) +.transpose(seq(0, 1).slow(16))`; export const struct = `stack( "c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]", @@ -239,9 +239,9 @@ export const magicSofa = `stack( .every(2, fast(2)) .voicings(), " " -).slow(1).transpose(slowcat(0, 2, 3, 4))`; +).slow(1).transpose(cat(0, 2, 3, 4))`; // below doesn't work anymore due to constructor cleanup -// ).slow(1).transpose.slowcat(0, 2, 3, 4)`; +// ).slow(1).transpose.cat(0, 2, 3, 4)`; export const confusedPhone = `"[g2 ~@1.3] [c3 ~@1.3]" .superimpose( @@ -251,8 +251,8 @@ export const confusedPhone = `"[g2 ~@1.3] [c3 ~@1.3]" transpose(12).late(0.3), transpose(24).late(0.4) ) -.scale(slowcat('C dorian', 'C mixolydian')) -.scaleTranspose(slowcat(0,1,2,1)) +.scale(cat('C dorian', 'C mixolydian')) +.scaleTranspose(cat(0,1,2,1)) .slow(2)`; export const zeldasRescue = `stack( @@ -340,7 +340,7 @@ stack( export const callcenterhero = `const bpm = 90; const lead = polysynth().set({...osc('sine4'),...adsr(.004)}).chain(vol(0.15),out()) const bass = fmsynth({...osc('sawtooth6'),...adsr(0.05,.6,0.8,0.1)}).chain(vol(0.6), out()); -const s = scale(slowcat('F3 minor', 'Ab3 major', 'Bb3 dorian', 'C4 phrygian dominant').slow(4)); +const s = scale(cat('F3 minor', 'Ab3 major', 'Bb3 dorian', 'C4 phrygian dominant').slow(4)); stack( "0 2".struct(" [x ~]").apply(s).scaleTranspose(stack(0,2)).tone(lead), "<6 7 9 7>".struct("[~ [x ~]*2]*2").apply(s).scaleTranspose("[0,2] [2,4]".fast(2).every(4,rev)).tone(lead), @@ -379,7 +379,7 @@ stack( `; export const xylophoneCalling = `const t = x => x.scaleTranspose("<0 2 4 3>/4").transpose(-2) -const s = x => x.scale(slowcat('C3 minor pentatonic','G3 minor pentatonic').slow(4)) +const s = x => x.scale(cat('C3 minor pentatonic','G3 minor pentatonic').slow(4)) const delay = new FeedbackDelay(1/8, .6).chain(vol(0.1), out()); const chorus = new Chorus(1,2.5,0.5).start(); stack( @@ -412,7 +412,8 @@ stack( "~ ".tone(noise().chain(vol(0.8),out())), // hihat "c3*4".transpose("[-24 0]*2").tone(metal(adsr(0,.02)).chain(vol(0.5).connect(delay),out())) -).slow(1)`; +).slow(1) +// strudel disable-highlighting`; export const sowhatelse = `// mixer const mix = (key) => vol({ @@ -448,7 +449,8 @@ stack( "~ c3".tone(instr('snare')), "<[c1@5 c1] >".tone(instr('kick')), "[2,4]/4".scale('D dorian').apply(t).tone(instr('pad')).mask("/8") -).fast(6/8)`; +).fast(6/8) +// strudel disable-highlighting`; export const barryHarris = `backgroundImage( 'https://media.npr.org/assets/img/2017/02/03/barryharris_600dpi_wide-7eb49998aa1af377d62bb098041624c0a0d1a454.jpg', @@ -481,7 +483,7 @@ const rhodes = await sampler({ }, 'https://loophole-letters.vercel.app/') const bass = synth(osc('sawtooth8')).chain(vol(.5),out()) -const scales = sequence('C major', 'C mixolydian', 'F lydian', ['F minor', slowcat('Db major','Db mixolydian')]).slow(4) +const scales = cat('C major', 'C mixolydian', 'F lydian', ['F minor', cat('Db major','Db mixolydian')]) stack( " " @@ -500,12 +502,12 @@ stack( .tone(bass), ).fast(3/2)`; -export const wavyKalimba = `const delay = new FeedbackDelay(1/3, .5).chain(vol(.2), out()); +export const wavyKalimba = `const delay = new FeedbackDelay(1/3, .5).chain(vol(.2), out()) let kalimba = await sampler({ C5: 'https://freesound.org/data/previews/536/536549_11935698-lq.mp3' }) kalimba = kalimba.chain(vol(0.6).connect(delay),out()); -const scales = sequence('C major', 'C mixolydian', 'F lydian', ['F minor', 'Db major']).slow(4); +const scales = cat('C major', 'C mixolydian', 'F lydian', ['F minor', 'Db major']) stack( "[0 2 4 6 9 2 0 -2]*3" @@ -573,7 +575,7 @@ stack( chords.voicings().struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)), chords.rootNotes(2).struct("x(4,8,-2)"), chords.rootNotes(4) - .scale(slowcat('C minor','F dorian','G dorian','F# mixolydian')) + .scale(cat('C minor','F dorian','G dorian','F# mixolydian')) .struct("x(3,8,-2)".fast(2)) .scaleTranspose("0 4 0 6".early(".125 .5")).layer(scaleTranspose("0,<2 [4,6] [5,7]>/4")) ).slow(2) @@ -581,7 +583,7 @@ stack( .tone((await piano()).chain(out()))`; export const festivalOfFingers2 = `const chords = ""; -const scales = slowcat('C minor','F dorian','G dorian','F# mixolydian') +const scales = cat('C minor','F dorian','G dorian','F# mixolydian') stack( chords.voicings().struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)), chords.rootNotes(2).struct("x(4,8)"), @@ -638,7 +640,7 @@ stack( ).cpm(78).slow(4).pianoroll() `; -export const goodTimes = `const scale = slowcat('C3 dorian','Bb2 major').slow(4); +export const goodTimes = `const scale = cat('C3 dorian','Bb2 major').slow(4); stack( "2*4".add(12).scale(scale) .off(1/8,x=>x.scaleTranspose("2")).fast(2) @@ -708,7 +710,7 @@ export const speakerman = `backgroundImage('https://external-content.duckduckgo. { className:'darken', style:'background-size:cover'}) stack( "[g3,bb3,d4] [f3,a3,c4] [c3,e3,g3]@2".slow(2).late(.1), - slowcat( + cat( 'Baker man', 'is baking bread', 'Baker man', @@ -738,7 +740,7 @@ bell = bell.chain(vol(0.6).connect(delay),out()); .add(rand.range(0,12)) .velocity(rand.range(.5,1)) .legato(rand.range(.4,3)) - .scale(slowcat('D minor pentatonic')).tone(bell) + .scale(cat('D minor pentatonic')).tone(bell) .stack("".euclidLegato(6,8,1).tone(bass.toDestination())) .slow(6) .pianoroll({minMidi:20,maxMidi:120,background:'transparent'})`; @@ -768,7 +770,7 @@ export const hyperpop = `const lfo = cosine.slow(15); const lfo2 = sine.slow(16); const filter1 = x=>x.filter('lowpass', lfo2.range(300,3000)); const filter2 = x=>x.filter('highpass', lfo.range(1000,6000)).filter('lowpass',4000) -const scales = slowcat('D3 major', 'G3 major').slow(8) +const scales = cat('D3 major', 'G3 major').slow(8) const drums = await players({ bd: '344/344757_1676145-lq.mp3', @@ -819,7 +821,7 @@ export const festivalOfFingers3 = `"[-7*3],0,2,6,[8 7]" .legato(sine.range(.5,2).slow(8)) .velocity(sine.range(.4,.8).slow(5)) .echo(4,1/12,.5)) -.scale(slowcat('D dorian','G mixolydian','C dorian','F mixolydian')) +.scale(cat('D dorian','G mixolydian','C dorian','F mixolydian')) .legato(1) .slow(2) .tone((await piano()).toDestination()) From 67bef933bedf5d0fe6d335bad982c305fcb99654 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 20:38:51 +0200 Subject: [PATCH 09/18] migrate tutorial cat -> seq --- repl/src/tutorial/tutorial.mdx | 52 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/repl/src/tutorial/tutorial.mdx b/repl/src/tutorial/tutorial.mdx index 117695c9..38ff8655 100644 --- a/repl/src/tutorial/tutorial.mdx +++ b/repl/src/tutorial/tutorial.mdx @@ -15,7 +15,8 @@ The best place to actually make music with Strudel is the [Strudel REPL](https:/ To get a taste of what Strudel can do, check out this track: - + An important difference to the mini notation: For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name. The above is the same as: - + Using strings, you can also use "#". @@ -220,38 +221,35 @@ Most of the time, you won't need that function as input values of pattern creati ### cat(...values) -The given items are con**cat**enated spread evenly over one cycle: +The given items are con**cat**enated, where each one takes one cycle: - + -The function **fastcat** does the same as **cat**. +- Square brackets will create a subsequence +- The function **slowcat** does the same as **cat**. -### sequence(...values) +### seq(...values) -Like **cat**, but allows nesting with arrays: +Like **cat**, but the items are crammed into one cycle: - + + +- Synonyms: **fastcat**, **sequence** ### stack(...values) The given items are played at the same time at the same length: - + -### slowcat(...values) - -Like cat, but each item has the length of one cycle: - - - - +- Square Brackets will create a subsequence ### Nesting functions You can nest functions inside one another: + You can also use the shorthand **pr** instead of **polyrhythm**. @@ -295,7 +293,7 @@ The following functions modify a pattern. Like "/" in mini notation, **slow** will slow down a pattern over the given number of cycles: - + The same in mini notation: @@ -305,19 +303,19 @@ The same in mini notation: Like "\*" in mini notation, **fast** will play a pattern times the given number in one cycle: - + ### early(cycles) With early, you can nudge a pattern to start earlier in time: - + ### late(cycles) Like early, but in the other direction: - + @@ -325,19 +323,19 @@ Like early, but in the other direction: Will reverse the pattern: - + ### every(n, func) Will apply the given function every n cycles: - + Note that late is called directly. This is a shortcut for: - x.late(0.5)))`} /> + x.late(0.5)))`} /> @@ -612,7 +610,7 @@ Turns numbers into notes in the scale (zero indexed). Also sets scale for other Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3. From e747c0b0602d3056bda21b4f799ba50893a9ce82 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 21:08:58 +0200 Subject: [PATCH 10/18] remove featurelist as it's now kept up to date in #31 --- featurelist.md | 131 ------------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 featurelist.md diff --git a/featurelist.md b/featurelist.md deleted file mode 100644 index 5135e511..00000000 --- a/featurelist.md +++ /dev/null @@ -1,131 +0,0 @@ -# Tidal Features in Strudel - -## Basics - -- [ ] drawLine -- [ ] setcps -- [ ] naming patterns? block based evaluation? -- [ ] once -- [x] silence -- [x] hush -- [ ] panic - -## Concatenation - -https://tidalcycles.org/docs/patternlib/tour/concatenation - -- [x] cat: is synonym to fastcat in strudel, while [in tidal, cat is slowcat](https://tidalcycles.org/docs/patternlib/tour/concatenation#cat) -- [x] fastcat -- [x] timeCat: why is this camel case? -- [ ] randcat -- [x] append: but is like fastAppend in tidal -- [ ] fastAppend -- [ ] slowAppend -- [ ] wedge -- [ ] brak -- [ ] flatpat - -## Accumulation - -- [ ] overlay => like stack? "The overlay function is similar to cat" => wrong? -- [ ] `<>` operator (=== overlay) -- [x] stack -- [x] superimpose -- [x] layer -- [ ] steps ? -- [x] iter -- [x] iter' = iterBack - -## Alteration - -- [ ] range, rangex -- [ ] quantise -- [ ] ply -- [x] stutter = echo -- [ ] stripe, slowstripe -- [ ] palindrome = every(2, rev) -- [ ] trunc -- [ ] linger -- [x] chunk, chunk' -- [ ] shuffle -- [ ] scramble -- [ ] rot -- [ ] step / step' -- [ ] lindenmeyer -- [ ] spread / spreadf / fastspread -- [ ] spreadChoose / spreadr - -## conditions - -- [x] every -- [ ] every' -- [ ] whenmod -- [ ] sometimes, sometimesBy, someCycles, someCyclesBy -- [ ] choose, chooseby, wchoose, wchooseby -- [x] struct -- [x] mask -- [ ] sew -- [ ] stitch -- [ ] select, selectF -- [ ] pickF -- [ ] squeeze -- [x] euclid, euclidLegato -- [ ] euclidInv, euclidFull -- [ ] ifp - -## Time - -- [x] fast -- [x] fastGap -- [x] slow -- [ ] hurry -- [ ] compress: is this compressSpan? -- [ ] zoom -- [ ] within -- [x] off -- [ ] rotL / rotR -- [x] rev -- [x] jux -- [ ] juxBy -- [ ] swingBy / swing -- [ ] ghost -- [ ] inside / outside - -## Harmony & Melody - -- [x] scale -- [ ] scaleList -- [ ] getScale -- [ ] toScale -- [ ] chordList -- [ ] arpeggiate -- [ ] arp - -## Transitions - -- [ ] anticipate / anticipateIn -- [ ] clutch / clutchIn -- [ ] histpan -- [ ] interpolate / interpolateIn -- [ ] jump / jumpIn / jumpIn' / jumpMod -- [ ] wait / waitT -- [ ] wash / washIn -- [ ] xfade / xfadeIn - -## Sampling - -- [ ] chop -- [ ] striate / striateBy -- [ ] loopAt -- [x] segment -- [ ] discretise - -## Randomness - -- [ ] rand / irand -- [ ] perlin / perlinWith / perlin2 / perlin2With - -## Composition - -- [ ] ur -- [ ] seqP / seqPLoop From 9e2e5ce581840b4d6c69aadfc19990174f036ba9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 23:28:43 +0200 Subject: [PATCH 11/18] log + drawLine --- packages/core/drawLine.mjs | 24 ++++++++++++++++++++++++ packages/core/fraction.mjs | 6 ++++++ packages/core/hap.mjs | 2 +- packages/core/pattern.mjs | 27 ++++++++++++++++++++++++++- packages/core/test/fraction.test.mjs | 9 +++++++++ repl/src/useRepl.mjs | 5 ++++- 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 packages/core/drawLine.mjs create mode 100644 packages/core/test/fraction.test.mjs diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs new file mode 100644 index 00000000..73fac46b --- /dev/null +++ b/packages/core/drawLine.mjs @@ -0,0 +1,24 @@ +import { gcd } from './fraction.mjs'; + +function drawLine(pat) { + let s = ''; + let c = 0; + while (s.length < 60) { + const haps = pat.queryArc(c, c + 1); + const durations = haps.map((hap) => hap.duration); + const totalSlots = gcd(...durations).inverse(); + haps.forEach((hap) => { + const duration = hap.whole.end.sub(hap.whole.begin); + const slots = totalSlots.mul(duration); + s += Array(slots.valueOf()) + .fill() + .map((_, i) => (!i ? hap.value : '-')) + .join(''); + }); + s += '|'; + ++c; + } + return s; +} + +export default drawLine; diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index 4fe2b12a..3b43f3a6 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -72,6 +72,12 @@ const fraction = (n) => { return Fraction(n); }; +export const gcd = (...fractions) => { + return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1)); +}; + +fraction._original = Fraction; + export default fraction; // "If you concern performance, cache Fraction.js objects and pass arrays/objects.“ diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index 44ff7ba0..e1ad05f1 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -24,7 +24,7 @@ export class Hap { } get duration() { - return this.whole.end.sub(this.whole.begin).valueOf(); + return this.whole.end.sub(this.whole.begin); } wholeOrPart() { diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index b5879d1d..51943b8a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -4,6 +4,7 @@ import Hap from './hap.mjs'; import State from './state.mjs'; import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs'; +import drawLine from './drawLine.mjs'; export class Pattern { // the following functions will get patternFactories as nested functions: @@ -563,6 +564,17 @@ export class Pattern { return this._withContext((context) => ({ ...context, color })); } + log() { + return this._withEvent((e) => { + return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) }); + }); + } + + drawLine() { + console.log(drawLine(this)); + return this; + } + _segment(rate) { return this.struct(pure(true)._fast(rate)); } @@ -803,7 +815,20 @@ Pattern.prototype.patternified = [ 'velocity', ]; // methods that create patterns, which are added to patternified Pattern methods -Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, sequence, seq, polymeter, pm, polyrhythm, pr }; +Pattern.prototype.factories = { + pure, + stack, + slowcat, + fastcat, + cat, + timeCat, + sequence, + seq, + polymeter, + pm, + polyrhythm, + pr, +}; // the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts) // Elemental patterns diff --git a/packages/core/test/fraction.test.mjs b/packages/core/test/fraction.test.mjs new file mode 100644 index 00000000..90d3ac76 --- /dev/null +++ b/packages/core/test/fraction.test.mjs @@ -0,0 +1,9 @@ +import Fraction, { gcd } from '../fraction.mjs'; +import { strict as assert } from 'assert'; + +describe('gcd', () => { + it('should work', () => { + const F = Fraction._original; + assert.equal(gcd(F(1 / 6), F(1 / 4)).toFraction(), '1/12'); + }); +}); diff --git a/repl/src/useRepl.mjs b/repl/src/useRepl.mjs index c20a9cba..d48aa141 100644 --- a/repl/src/useRepl.mjs +++ b/repl/src/useRepl.mjs @@ -29,7 +29,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }) { return onDraw; } }, [activeCode, onDraw]); - + // cycle hook to control scheduling const cycle = useCycle({ onDraw, @@ -37,6 +37,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }) { (time, event, currentTime) => { try { onEvent?.(event); + if (event.context.logs?.length) { + event.context.logs.forEach(pushLog); + } const { onTrigger, velocity } = event.context; if (!onTrigger) { if (defaultSynth) { From 766cccf3354ecd0ad00f9b081b14771795c69182 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 23:36:26 +0200 Subject: [PATCH 12/18] refactor event.duration references --- packages/core/examples/canvas.html | 2 +- packages/midi/midi.mjs | 2 +- packages/tone/tone.mjs | 10 +++++----- packages/webaudio/webaudio.mjs | 4 ++-- repl/src/CodeMirror.jsx | 4 ++-- repl/src/static.mjs | 2 +- repl/src/useRepl.mjs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core/examples/canvas.html b/packages/core/examples/canvas.html index 685cc56e..736c9a22 100644 --- a/packages/core/examples/canvas.html +++ b/packages/core/examples/canvas.html @@ -27,7 +27,7 @@ const events = pattern.firstCycle(); // query first cycle events.forEach((event) => { ctx.fillStyle = event.value; - ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration * canvas.width, canvas.height); + ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration.valueOf() * canvas.width, canvas.height); }); } diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index c9b93777..52d3ecbd 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -69,7 +69,7 @@ Pattern.prototype.midi = function (output, channel = 1) { // await enableWebMidi() device.playNote(note, channel, { time, - duration: event.duration * 1000 - 5, + duration: event.duration.valueOf() * 1000 - 5, velocity, }); }; diff --git a/packages/tone/tone.mjs b/packages/tone/tone.mjs index 2426dac3..cf3d750a 100644 --- a/packages/tone/tone.mjs +++ b/packages/tone/tone.mjs @@ -54,14 +54,14 @@ Pattern.prototype.tone = function (instrument) { note = getPlayableNoteValue(event); instrument.triggerAttack(note, time); } else if (instrument instanceof NoiseSynth) { - instrument.triggerAttackRelease(event.duration, time); // noise has no value + instrument.triggerAttackRelease(event.duration.valueOf(), time); // noise has no value } else if (instrument instanceof Piano) { note = getPlayableNoteValue(event); instrument.keyDown({ note, time, velocity }); - instrument.keyUp({ note, time: time + event.duration, velocity }); + instrument.keyUp({ note, time: time + event.duration.valueOf(), velocity }); } else if (instrument instanceof Sampler) { note = getPlayableNoteValue(event); - instrument.triggerAttackRelease(note, event.duration, time, velocity); + instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); } else if (instrument instanceof Players) { if (!instrument.has(event.value)) { throw new Error(`name "${event.value}" not defined for players`); @@ -69,10 +69,10 @@ Pattern.prototype.tone = function (instrument) { const player = instrument.player(event.value); // velocity ? player.start(time); - player.stop(time + event.duration); + player.stop(time + event.duration.valueOf()); } else { note = getPlayableNoteValue(event); - instrument.triggerAttackRelease(note, event.duration, time, velocity); + instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); } }; return event.setContext({ ...event.context, instrument, onTrigger }); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index de07a56e..f66f94bd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -39,7 +39,7 @@ Pattern.prototype._wave = function (type) { const f = getFrequency(e); osc.frequency.value = f; // expects frequency.. const begin = t ?? e.whole.begin.valueOf() + lookahead; - const end = begin + e.duration; + const end = begin + e.valueOf(); osc.start(begin); osc.stop(end); // release? return osc; @@ -49,7 +49,7 @@ Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) { return this.withAudioNode((t, e, node) => { const velocity = e.context?.velocity || 1; const begin = t ?? e.whole.begin.valueOf() + lookahead; - const end = begin + e.duration + lookahead; + const end = begin + e.duration.valueOf() + lookahead; const envelope = adsr(a, d, s, r, velocity, begin, end); node?.connect(envelope); return envelope; diff --git a/repl/src/CodeMirror.jsx b/repl/src/CodeMirror.jsx index b58ad75a..a659e3ee 100644 --- a/repl/src/CodeMirror.jsx +++ b/repl/src/CodeMirror.jsx @@ -44,8 +44,8 @@ export const markEvent = (editor) => (time, event) => { //Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler... setTimeout(() => { marks.forEach((mark) => mark.clear()); - // }, '+' + event.duration * 0.5); - }, event.duration /* * 0.9 */ * 1000); + // }, '+' + event.duration.valueOf() * 0.5); + }, event.duration.valueOf() /* * 0.9 */ * 1000); }; let parenMark; diff --git a/repl/src/static.mjs b/repl/src/static.mjs index 0fc70537..5ae319ae 100644 --- a/repl/src/static.mjs +++ b/repl/src/static.mjs @@ -45,7 +45,7 @@ async function playStatic(code) { if (!onTrigger) { if (defaultSynth) { const note = getPlayableNoteValue(event); - defaultSynth.triggerAttackRelease(note, event.duration, time, velocity); + defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); } else { throw new Error('no defaultSynth passed to useRepl.'); } diff --git a/repl/src/useRepl.mjs b/repl/src/useRepl.mjs index d48aa141..63ea65bb 100644 --- a/repl/src/useRepl.mjs +++ b/repl/src/useRepl.mjs @@ -44,7 +44,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }) { if (!onTrigger) { if (defaultSynth) { const note = getPlayableNoteValue(event); - defaultSynth.triggerAttackRelease(note, event.duration, time, velocity); + defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); } else { throw new Error('no defaultSynth passed to useRepl.'); } From 2bbd306b94093693d96acecfa27350d9e89df571 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 23 Apr 2022 23:47:52 +0200 Subject: [PATCH 13/18] drawLine test --- packages/core/drawLine.mjs | 8 +++++--- packages/core/test/drawLine.test.mjs | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 packages/core/test/drawLine.test.mjs diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 73fac46b..05c857de 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -1,12 +1,15 @@ import { gcd } from './fraction.mjs'; -function drawLine(pat) { +// TODO: make it work for stacked patterns + support silence + +function drawLine(pat, chars = 60) { let s = ''; let c = 0; - while (s.length < 60) { + while (s.length < chars) { const haps = pat.queryArc(c, c + 1); const durations = haps.map((hap) => hap.duration); const totalSlots = gcd(...durations).inverse(); + s += '|'; haps.forEach((hap) => { const duration = hap.whole.end.sub(hap.whole.begin); const slots = totalSlots.mul(duration); @@ -15,7 +18,6 @@ function drawLine(pat) { .map((_, i) => (!i ? hap.value : '-')) .join(''); }); - s += '|'; ++c; } return s; diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs new file mode 100644 index 00000000..d697c793 --- /dev/null +++ b/packages/core/test/drawLine.test.mjs @@ -0,0 +1,11 @@ +import { fastcat } from '../pattern.mjs'; +import { strict as assert } from 'assert'; +import drawLine from '../drawLine.mjs'; + +describe('drawLine', () => { + it('should work', () => { + assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12'); + assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123'); + assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23'); + }); +}); From dd03ad6c140f2ee45bc845054262f7ca85f290f0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 24 Apr 2022 00:34:04 +0200 Subject: [PATCH 14/18] support stack in drawLine --- packages/core/drawLine.mjs | 52 +++++++++++++++++++--------- packages/core/test/drawLine.test.mjs | 24 ++++++++++++- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 05c857de..cf380fd2 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -1,26 +1,46 @@ import { gcd } from './fraction.mjs'; -// TODO: make it work for stacked patterns + support silence - function drawLine(pat, chars = 60) { - let s = ''; let c = 0; - while (s.length < chars) { + let lines = ['']; + const slots = []; + while (lines[0].length < chars) { const haps = pat.queryArc(c, c + 1); - const durations = haps.map((hap) => hap.duration); + const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration); const totalSlots = gcd(...durations).inverse(); - s += '|'; - haps.forEach((hap) => { - const duration = hap.whole.end.sub(hap.whole.begin); - const slots = totalSlots.mul(duration); - s += Array(slots.valueOf()) - .fill() - .map((_, i) => (!i ? hap.value : '-')) - .join(''); - }); - ++c; + slots.push(totalSlots); + const minDuration = durations.reduce((a, b) => a.min(b), durations[0]); + lines = lines.map((line) => line + '|'); + + for (let i = 0; i < totalSlots; i++) { + const step = c * totalSlots + i; + const [begin, end] = [minDuration.mul(step), minDuration.mul(step + 1)]; + const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end)); + const missingLines = matches.length - lines.length; + if (missingLines > 0) { + console.log(c, 'missingLines', missingLines); + const emptyCycles = + '|' + + new Array(c) + .fill() + .map((_, l) => Array(slots[l]).fill('.').join('')) + .join('|') + + Array(i).fill('.').join(''); + lines = lines.concat(Array(missingLines).fill(emptyCycles)); + } + lines = lines.map((line, i) => { + const hap = matches[i]; + if (hap) { + const isOnset = hap.whole.begin.eq(begin); + const char = isOnset ? '' + hap.value : '-'; + return line + char; + } + return line + '.'; + }); + } + c++; } - return s; + return lines.join('\n'); } export default drawLine; diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index d697c793..f7b4fca3 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,4 +1,4 @@ -import { fastcat } from '../pattern.mjs'; +import { fastcat, stack } from '../pattern.mjs'; import { strict as assert } from 'assert'; import drawLine from '../drawLine.mjs'; @@ -7,5 +7,27 @@ describe('drawLine', () => { assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12'); assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123'); assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23'); + assert.equal( + drawLine(fastcat(0, stack(1, 2)), 10), + `|01|01|01|01 +|.2|.2|.2|.2`, + ); + assert.equal( + drawLine(fastcat(0, 1, stack(2, 3)), 10), + `|012|012|012 +|..3|..3|..3`, + ); + assert.equal( + drawLine(fastcat(0, stack(1, 2, 3)), 10), + `|01|01|01|01 +|.2|.2|.2|.2 +|.3|.3|.3|.3`, + ); + assert.equal( + drawLine(fastcat(0, 1, stack(2, 3, 4)), 10), + `|012|012|012 +|..3|..3|..3 +|..4|..4|..4`, + ); }); }); From a0d6fc47e02d4a0073dfb40f3d95855d575024f2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 24 Apr 2022 00:56:16 +0200 Subject: [PATCH 15/18] improve drawLine --- packages/core/drawLine.mjs | 35 ++++++++++++---------------- packages/core/test/drawLine.test.mjs | 14 +++++++++-- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index cf380fd2..2fcdb07d 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -1,32 +1,25 @@ -import { gcd } from './fraction.mjs'; +import Fraction, { gcd } from './fraction.mjs'; function drawLine(pat, chars = 60) { - let c = 0; + let cycle = 0; + let pos = Fraction(0); let lines = ['']; + let emptyLine = ''; // this will be the "reference" empty line, which will be copied into extra lines const slots = []; while (lines[0].length < chars) { - const haps = pat.queryArc(c, c + 1); + const haps = pat.queryArc(cycle, cycle + 1); const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration); - const totalSlots = gcd(...durations).inverse(); - slots.push(totalSlots); - const minDuration = durations.reduce((a, b) => a.min(b), durations[0]); - lines = lines.map((line) => line + '|'); - + const totalSlots = gcd(...durations).inverse(); // number of character slots for the current cycle + slots.push(totalSlots); // remember slots for possible empty lines needed in a later cycle + const minDuration = durations.reduce((a, b) => a.min(b), durations[0]); // min duration = step length + lines = lines.map((line) => line + '|'); // add pipe character before each cycle + emptyLine += '|'; for (let i = 0; i < totalSlots; i++) { - const step = c * totalSlots + i; - const [begin, end] = [minDuration.mul(step), minDuration.mul(step + 1)]; + const [begin, end] = [pos, pos.add(minDuration)]; const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end)); const missingLines = matches.length - lines.length; if (missingLines > 0) { - console.log(c, 'missingLines', missingLines); - const emptyCycles = - '|' + - new Array(c) - .fill() - .map((_, l) => Array(slots[l]).fill('.').join('')) - .join('|') + - Array(i).fill('.').join(''); - lines = lines.concat(Array(missingLines).fill(emptyCycles)); + lines = lines.concat(Array(missingLines).fill(emptyLine)); } lines = lines.map((line, i) => { const hap = matches[i]; @@ -37,8 +30,10 @@ function drawLine(pat, chars = 60) { } return line + '.'; }); + emptyLine += '.'; + pos = pos.add(minDuration); } - c++; + cycle++; } return lines.join('\n'); } diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index f7b4fca3..49575755 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,12 +1,19 @@ -import { fastcat, stack } from '../pattern.mjs'; +import { fastcat, stack, slowcat } from '../pattern.mjs'; import { strict as assert } from 'assert'; import drawLine from '../drawLine.mjs'; describe('drawLine', () => { - it('should work', () => { + it('supports equal lengths', () => { + assert.equal(drawLine(fastcat(0), 4), '|0|0'); + assert.equal(drawLine(fastcat(0, 1), 4), '|01|01'); + assert.equal(drawLine(fastcat(0, 1, 2), 6), '|012|012'); + }); + it('supports unequal lengths', () => { assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12'); assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123'); assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23'); + }); + it('supports multiple lines', () => { assert.equal( drawLine(fastcat(0, stack(1, 2)), 10), `|01|01|01|01 @@ -30,4 +37,7 @@ describe('drawLine', () => { |..4|..4|..4`, ); }); + it('supports unequal cycle lengths', () => { + assert.equal(drawLine(slowcat(0, [1, 2]), 10), `|0|12|0|12`); + }); }); From 58a936cf33fe4e1c6249fc610fee08e4b2abb9de Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 24 Apr 2022 00:59:38 +0200 Subject: [PATCH 16/18] test silence --- packages/core/test/drawLine.test.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index 49575755..4300b119 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,4 +1,4 @@ -import { fastcat, stack, slowcat } from '../pattern.mjs'; +import { fastcat, stack, slowcat, silence } from '../pattern.mjs'; import { strict as assert } from 'assert'; import drawLine from '../drawLine.mjs'; @@ -13,6 +13,9 @@ describe('drawLine', () => { assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123'); assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23'); }); + it('supports unequal silence', () => { + assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12'); + }); it('supports multiple lines', () => { assert.equal( drawLine(fastcat(0, stack(1, 2)), 10), From 283e5fde693df5ddf93f70bcb01223160bccbe19 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 24 Apr 2022 01:40:26 +0200 Subject: [PATCH 17/18] fix drawLine polyrhythm --- packages/core/drawLine.mjs | 10 ++++------ packages/core/test/drawLine.test.mjs | 6 +++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 2fcdb07d..2d88367f 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -5,17 +5,15 @@ function drawLine(pat, chars = 60) { let pos = Fraction(0); let lines = ['']; let emptyLine = ''; // this will be the "reference" empty line, which will be copied into extra lines - const slots = []; while (lines[0].length < chars) { const haps = pat.queryArc(cycle, cycle + 1); const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration); - const totalSlots = gcd(...durations).inverse(); // number of character slots for the current cycle - slots.push(totalSlots); // remember slots for possible empty lines needed in a later cycle - const minDuration = durations.reduce((a, b) => a.min(b), durations[0]); // min duration = step length + const charFraction = gcd(...durations); + const totalSlots = charFraction.inverse(); // number of character slots for the current cycle lines = lines.map((line) => line + '|'); // add pipe character before each cycle emptyLine += '|'; for (let i = 0; i < totalSlots; i++) { - const [begin, end] = [pos, pos.add(minDuration)]; + const [begin, end] = [pos, pos.add(charFraction)]; const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end)); const missingLines = matches.length - lines.length; if (missingLines > 0) { @@ -31,7 +29,7 @@ function drawLine(pat, chars = 60) { return line + '.'; }); emptyLine += '.'; - pos = pos.add(minDuration); + pos = pos.add(charFraction); } cycle++; } diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index 4300b119..587c45fb 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,4 +1,4 @@ -import { fastcat, stack, slowcat, silence } from '../pattern.mjs'; +import { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs'; import { strict as assert } from 'assert'; import drawLine from '../drawLine.mjs'; @@ -16,6 +16,10 @@ describe('drawLine', () => { it('supports unequal silence', () => { assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12'); }); + it('supports polyrhythms', () => { + '0*2 1*3'; + assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-'); + }); it('supports multiple lines', () => { assert.equal( drawLine(fastcat(0, stack(1, 2)), 10), From 6a6464b0f7a1c11437b541cd597275c9697a11bf Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 24 Apr 2022 17:51:31 +0100 Subject: [PATCH 18/18] Remove append, fixes #89 --- packages/core/pattern.mjs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index ce49c537..1ca14d6a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -656,10 +656,6 @@ export class Pattern { return cat(this, ...pats); } - append(...pats) { - return cat(this, ...pats); - } - fastcat(...pats) { return fastcat(this, ...pats); } @@ -968,7 +964,6 @@ export function pr(args) { } export const add = curry((a, pat) => pat.add(a)); -export const append = curry((a, pat) => pat.append(a)); export const chunk = curry((a, pat) => pat.chunk(a)); export const chunkBack = curry((a, pat) => pat.chunkBack(a)); export const div = curry((a, pat) => pat.div(a));