diff --git a/repl/src/App.tsx b/repl/src/App.tsx index 49afb436..5cecfae9 100644 --- a/repl/src/App.tsx +++ b/repl/src/App.tsx @@ -4,13 +4,12 @@ import cx from './cx'; import * as Tone from 'tone'; import useCycle from './useCycle'; import type { Pattern } from './types'; -import defaultTune from './tunes'; -import * as parser from './parse'; +import * as tunes from './tunes'; +import { evaluate } from './evaluate'; import CodeMirror from './CodeMirror'; import hot from '../public/hot'; import { isNote } from 'tone'; import { useWebMidi } from './midi'; -const { parse } = parser; const [_, codeParam] = window.location.href.split('#'); const decoded = atob(codeParam || ''); @@ -23,7 +22,7 @@ const getHotCode = async () => { }); }; -const defaultSynth = new Tone.PolySynth().toDestination(); +const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination); defaultSynth.set({ oscillator: { type: 'triangle' }, envelope: { @@ -31,9 +30,17 @@ defaultSynth.set({ }, }); +function getRandomTune() { + const allTunes = Object.values(tunes); + const randomItem = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]; + return randomItem(allTunes); +} + +const randomTune = getRandomTune(); + function App() { const [mode, setMode] = useState('javascript'); - const [code, setCode] = useState(decoded || defaultTune); + const [code, setCode] = useState(decoded || randomTune); const [log, setLog] = useState(''); const logBox = useRef(); const [error, setError] = useState(); @@ -125,7 +132,7 @@ function App() { } // normal mode try { - const parsed = parse(_code); + const parsed = evaluate(_code); // need arrow function here! otherwise if user returns a function, react will think it's a state reducer // only first time, then need ctrl+enter setPattern(() => parsed.pattern); @@ -164,17 +171,31 @@ function App() { logo

Strudel REPL

- {window.location.href.includes('http://localhost:8080') && ( +
- )} + {window.location.href.includes('http://localhost:8080') && ( + + )} +
diff --git a/repl/src/evaluate.ts b/repl/src/evaluate.ts new file mode 100644 index 00000000..abdb63a0 --- /dev/null +++ b/repl/src/evaluate.ts @@ -0,0 +1,38 @@ +import * as strudel from '../../strudel.mjs'; +import './tone'; +import './midi'; +import './voicings'; +import './tonal'; +import './groove'; +import shapeshifter from './shapeshifter'; +import { minify } from './parse'; + +// this will add all methods from definedMethod to strudel + connect all the partial application stuff +const bootstrapped: any = { ...strudel, ...strudel.Pattern.prototype.bootstrap() }; +// console.log('bootstrapped',bootstrapped.transpose(2).transpose); + +function hackLiteral(literal, names, func) { + names.forEach((name) => { + Object.defineProperty(literal.prototype, name, { + get: function () { + return func(String(this)); + }, + }); + }); +} + +// with this, you can do 'c2 [eb2 g2]'.mini.fast(2) or 'c2 [eb2 g2]'.m.fast(2), +hackLiteral(String, ['mini', 'm'], bootstrapped.mini); // comment out this line if you panic +hackLiteral(String, ['pure', 'p'], bootstrapped.pure); // comment out this line if you panic + +Object.assign(globalThis, bootstrapped); // this will add contents of bootstrapped to scope (used by eval) + +export const evaluate: any = (code: string) => { + const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code + const pattern = minify(eval(shapeshifted)); // eval and minify (if user entered a string) + if (pattern?.constructor?.name !== 'Pattern') { + const message = `got "${typeof pattern}" instead of pattern`; + throw new Error(message + (typeof pattern === 'function' ? ', did you forget to call a function?' : '.')); + } + return { mode: 'javascript', pattern: pattern }; +}; diff --git a/repl/src/groove.ts b/repl/src/groove.ts index eb83d17a..fac9e5da 100644 --- a/repl/src/groove.ts +++ b/repl/src/groove.ts @@ -6,3 +6,5 @@ const Pattern = _Pattern as any; Pattern.prototype.groove = function (groove) { return groove.fmap(() => (v) => v).appLeft(this); }; + +Pattern.prototype.define('groove', (groove, pat) => pat.groove(groove), { composable: true }); diff --git a/repl/src/parse.ts b/repl/src/parse.ts index c4e5ba41..e4ee60ff 100644 --- a/repl/src/parse.ts +++ b/repl/src/parse.ts @@ -1,67 +1,10 @@ import * as krill from '../krill-parser'; import * as strudel from '../../strudel.mjs'; import { Scale, Note, Interval } from '@tonaljs/tonal'; -import './tone'; -import './midi'; -import './voicings'; -import './tonal'; -import * as tonalStuff from './tonal'; -import './groove'; -import * as toneStuff from './tone'; -import shapeshifter from './shapeshifter'; -// even if some functions are not used, we need them to be available in eval -const { - Fraction, - TimeSpan, - Hap, - Pattern, - pure, - stack, - slowcat, - fastcat, - cat, - timeCat, - sequence, - polymeter, - pm, - polyrhythm, - pr, - // reify, - silence, - fast, - slow, - early, - late, - rev, - add, - sub, - mul, - div, - union, - every, - when, - off, - jux, - append, -} = strudel; -const { autofilter, filter, gain } = toneStuff; +const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence } = strudel; -const { transpose } = tonalStuff; -function reify(thing: any) { - if (thing?.constructor?.name === 'Pattern') { - return thing; - } - return pure(thing); -} - -function minify(thing: any) { - if (typeof thing === 'string') { - return mini(thing); - } - return reify(thing); -} const applyOptions = (parent: any) => (pat: any, i: number) => { const ast = parent.source_[i]; @@ -94,12 +37,20 @@ export function patternifyAST(ast: any): any { switch (ast.type_) { case 'pattern': const children = ast.source_.map(patternifyAST).map(applyOptions(ast)); - if (ast.arguments_.alignment === 'v') { + const alignment = ast.arguments_.alignment; + if (alignment === 'v') { return stack(...children); } const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); + if (!weightedChildren && alignment === 't') { + return slowcat(...children); + } if (weightedChildren) { - return timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); + const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); + if (alignment === 't') { + return pat._slow(children.length); // timecat + slow + } + return pat; } return sequence(...children); case 'element': @@ -142,32 +93,13 @@ export function patternifyAST(ast: any): any { // mini notation only (wraps in "") export const mini = (...strings: string[]) => { - const pattern = sequence( - ...strings.map((str) => { - const ast = krill.parse(`"${str}"`); - // console.log('ast', ast); - return patternifyAST(ast); - }) - ); - return pattern; + const pats = strings.map((str) => { + const ast = krill.parse(`"${str}"`); + return patternifyAST(ast); + }); + return sequence(...pats); }; -// shorthand for mini -const m = mini; - -const hackStrings = () => { - const miniGetter = { - get: function () { - return mini(String(this)); - }, - }; - // with this, you can do 'c2 [eb2 g2]'.mini.fast(2) or 'c2 [eb2 g2]'.m.fast(2), - Object.defineProperty(String.prototype, 'mini', miniGetter); - Object.defineProperty(String.prototype, 'm', miniGetter); -}; - -hackStrings(); // comment out this line if you panic - // includes haskell style (raw krill parsing) export const h = (string: string) => { const ast = krill.parse(string); @@ -175,21 +107,22 @@ export const h = (string: string) => { return patternifyAST(ast); }; -export const parse: any = (code: string) => { - let _pattern; - let mode; - /* try { - _pattern = h(code); - mode = 'pegjs'; - } catch (err) { */ - // code is not haskell like - mode = 'javascript'; - code = shapeshifter(code); - _pattern = minify(eval(code)); - if (_pattern?.constructor?.name !== 'Pattern') { - const message = `got "${typeof _pattern}" instead of pattern`; - throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.')); +// shorthand for mini +Pattern.prototype.define('mini', mini, { composable: true }); +Pattern.prototype.define('m', mini, { composable: true }); +Pattern.prototype.define('h', h, { composable: true }); + +// TODO: move this to strudel? +export function reify(thing: any) { + if (thing?.constructor?.name === 'Pattern') { + return thing; } - /* } */ - return { mode, pattern: _pattern }; -}; + return pure(thing); +} + +export function minify(thing: any) { + if (typeof thing === 'string') { + return mini(thing); + } + return reify(thing); +} \ No newline at end of file diff --git a/repl/src/tonal.ts b/repl/src/tonal.ts index 47fe8421..89cdba10 100644 --- a/repl/src/tonal.ts +++ b/repl/src/tonal.ts @@ -1,5 +1,5 @@ import { Note, Interval, Scale } from '@tonaljs/tonal'; -import { Pattern as _Pattern, curry, makeComposable } from '../../strudel.mjs'; +import { Pattern as _Pattern } from '../../strudel.mjs'; const Pattern = _Pattern as any; @@ -76,10 +76,6 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { }; // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) -export const transpose = curry( - (a, pat) => pat.transpose(a), - (partial) => makeComposable(partial) -); // TODO: add Pattern.define(name, function, options) that handles all the meta programming stuff // TODO: find out how to patternify this function when it's standalone // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or @@ -97,5 +93,6 @@ Pattern.prototype._scale = function (scale: string) { return this._mapNotes((value) => ({ ...value, scale })); }; -Pattern.prototype.patternified = Pattern.prototype.patternified.concat(['transpose', 'scaleTranspose', 'scale']); -Object.assign(Pattern.prototype.composable, { transpose }); +Pattern.prototype.define('transpose', (a, pat) => pat.transpose(a), { composable: true, patternified: true }); +Pattern.prototype.define('scale', (a, pat) => pat.scale(a), { composable: true, patternified: true }); +Pattern.prototype.define('scaleTranspose', (a, pat) => pat.scaleTranspose(a), { composable: true, patternified: true }); diff --git a/repl/src/tone.ts b/repl/src/tone.ts index 64f44cf5..4a90af45 100644 --- a/repl/src/tone.ts +++ b/repl/src/tone.ts @@ -89,3 +89,7 @@ Pattern.prototype.autofilter = function (g: number) { }; Pattern.prototype.patternified = Pattern.prototype.patternified.concat(['synth', 'gain', 'filter']); + +Pattern.prototype.define('synth', (type, pat) => pat.synth(type), { composable: true, patternified: true }); +Pattern.prototype.define('gain', (gain, pat) => pat.synth(gain), { composable: true, patternified: true }); +Pattern.prototype.define('filter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true }); diff --git a/repl/src/tunes.ts b/repl/src/tunes.ts index be653cd5..b7906efb 100644 --- a/repl/src/tunes.ts +++ b/repl/src/tunes.ts @@ -36,8 +36,6 @@ export const shapeShifted = `stack( ).rev() ).slow(16)`; -export const tetrisMidi = `${shapeShifted}.midi('IAC-Treiber Bus 1')`; - export const tetrisWithFunctions = `stack(sequence( 'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'), 'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'), @@ -57,10 +55,9 @@ 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( - sequence( mini( 'e5 [b4 c5] d5 [c5 b4]', 'a4 [a4 c5] e5 [d5 c5]', @@ -70,9 +67,7 @@ export const tetris = `stack( 'e5 [~ c5] e5 [d5 c5]', 'b4 [b4 c5] d5 e5', 'c5 a4 a4 ~' - ) - ), - sequence( + ), mini( 'e2 e3 e2 e3 e2 e3 e2 e3', 'a2 a3 a2 a3 a2 a3 a2 a3', @@ -82,14 +77,10 @@ export const tetris = `stack( 'c2 c3 c2 c3 c2 c3 c2 c3', 'b1 b2 b1 b2 e2 e3 e2 e3', 'a1 a2 a1 a2 a1 a2 a1 a2' - ) ) -).slow(16).synth({ - oscillator: {type: 'sawtooth'} -})`; +).slow(16)`; export const tetrisRev = `stack( - sequence( mini( 'e5 [b4 c5] d5 [c5 b4]', 'a4 [a4 c5] e5 [d5 c5]', @@ -99,9 +90,7 @@ export const tetrisRev = `stack( 'e5 [~ c5] e5 [d5 c5]', 'b4 [b4 c5] d5 e5', 'c5 a4 a4 ~' - ).rev() - ), - sequence( + ).rev(), mini( 'e2 e3 e2 e3 e2 e3 e2 e3', 'a2 a3 a2 a3 a2 a3 a2 a3', @@ -112,8 +101,7 @@ export const tetrisRev = `stack( 'b1 b2 b1 b2 e2 e3 e2 e3', 'a1 a2 a1 a2 a1 a2 a1 a2' ).rev() - ) -).slow(16).synth('sawtooth').filter(1000).gain(0.6)`; +).slow(16)`; /* .synth({ @@ -123,8 +111,10 @@ export const tetrisRev = `stack( */ -export const tetrisMini1 = `m\`[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] [[~ d5] [~ f5] a5 [g5 f5]] [e5 [~ c5] e5 [d5 c5]] [b4 [b4 c5] d5 e5] [c5 a4 a4 ~]],[[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] [a2 a3 a2 a3 a2 a3 b1 c2] [d2 d3 d2 d3 d2 d3 d2 d3] [c2 c3 c2 c3 c2 c3 c2 c3] [b1 b2 b1 b2 e2 e3 e2 e3] [a1 a2 a1 a2 a1 a2 a1 a2]]')._slow(16)\``; -export const tetrisMini = `m\`[[e5 [b4 c5] d5 [c5 b4]] +/* export const tetrisMini1 = + "'[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] [[~ d5] [~ f5] a5 [g5 f5]] [e5 [~ c5] e5 [d5 c5]] [b4 [b4 c5] d5 e5] [c5 a4 a4 ~]],[[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] [a2 a3 a2 a3 a2 a3 b1 c2] [d2 d3 d2 d3 d2 d3 d2 d3] [c2 c3 c2 c3 c2 c3 c2 c3] [b1 b2 b1 b2 e2 e3 e2 e3] [a1 a2 a1 a2 a1 a2 a1 a2]]'.mini.slow(16)"; + */ +export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] @@ -139,10 +129,10 @@ export const tetrisMini = `m\`[[e5 [b4 c5] d5 [c5 b4]] [[d2 d3]*4] [[c2 c3]*4] [[b1 b2]*2 [e2 e3]*2] -[[a1 a2]*4]\`._slow(16); +[[a1 a2]*4]\`.mini.slow(16) `; -export const tetrisHaskellH = `h(\`slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] +/* export const tetrisHaskellH = `h(\`slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] @@ -158,8 +148,9 @@ export const tetrisHaskellH = `h(\`slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] [[c2 c3]*4] [[b1 b2]*2 [e2 e3]*2] [[a1 a2]*4]"\`) -`; -export const tetrisHaskell = `slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] +`; */ +// following syntax is not supported anymore +/* export const tetrisHaskell = `slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] @@ -175,23 +166,23 @@ export const tetrisHaskell = `slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] [[c2 c3]*4] [[b1 b2]*2 [e2 e3]*2] [[a1 a2]*4]" -`; +`; */ /* export const tetrisHaskell = `h(\`slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] [[~ d5] [~ f5] a5 [g5 f5]] [e5 [~ c5] e5 [d5 c5]] [b4 [b4 c5] d5 e5] [c5 a4 a4 ~]], [[e2 e3]*4] [[a2 a3]*4] [[g#2 g#3]*2 [e2 e3]*2] [a2 a3 a2 a3 a2 a3 b1 c2] [[d2 d3]*4] [[c2 c3]*4] [[b1 b2]*2 [e2 e3]*2] [[a1 a2]*4]"\`)`; */ export const spanish = `slowcat( - stack('c4','eb4','g4'), - stack('bb3','d4','f4'), - stack('ab3','c4','eb4'), - stack('g3','b3','d4') - )`; + stack(c4,eb4,g4), + stack(bb3,d4,f4), + stack(ab3,c4,eb4), + stack(g3,b3,d4) +)`; export const whirlyStrudel = `mini("[e4 [b2 b3] c4]") - .every(4, x => x.fast(2)) - .every(3, x => x.slow(1.5)) - .fast(slowcat(1.25,1,1.5)) - .every(2, _ => mini("e4 ~ e3 d4 ~"))`; +.every(4, fast(2)) +.every(3, slow(1.5)) +.fast(slowcat(1.25, 1, 1.5)) +.every(2, _ => mini("e4 ~ e3 d4 ~"))`; export const swimming = `stack( mini( @@ -278,12 +269,39 @@ export const giantSteps = `stack( ) ).slow(20);`; -export const transposedChords = `stack( +export const giantStepsReggae = `stack( + // melody + mini( + '[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 + mini( + '[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]' + ) + .groove('~ [x ~]'.m.fast(4*8)) + .voicings(['E3', 'G4']), + // bass + mini( + '[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]', + '[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]' + ) + .groove('x ~'.m.fast(4*8)) +).slow(25)`; + +/* export const transposedChords = `stack( m('c2 eb2 g2'), m('Cm7').voicings(['g2','c4']).slow(2) ).transpose( slowcat(1, 2, 3, 2).slow(2) -).transpose(5)`; +).transpose(5)`; */ export const transposedChordsHacked = `stack( 'c2 eb2 g2'.mini, @@ -295,55 +313,38 @@ export const transposedChordsHacked = `stack( 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)) -.synth('sawtooth') -.filter(800) -.gain(0.5)`; +.transpose(sequence(0, 1).slow(16))`; -export const groove = `stack( - m('c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]') - .synth('sawtooth') - .filter(500) - .gain(.6), +/* export const groove = `stack( + m('c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'), m('[C^7 A7] [Dm7 G7]') .groove(m('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2')) .voicings(['G3','A4']) - .synth('square') - .filter(1000) - .adsr(.1,.1,.2) - .gain(0.25) -).slow(4.5)`; +).slow(4.5)`; */ -export const grooveHacked = `stack( - 'c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'.mini - .synth('sawtooth') - .filter(500) - .gain(.6), +export const groove = `stack( + 'c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'.mini, '[C^7 A7] [Dm7 G7]'.mini.groove('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2'.mini) .voicings(['G3','A4']) - .synth('square') - .filter(1000) - .adsr(.1,.1,.2) - .gain(0.25) -).slow(4.5)`; +).slow(4)`; -export const magicSofa = `stack( +/* export const magicSofa = `stack( m('[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4') .every(2, fast(2)) .voicings(), m('[c2 f2 g2]/3 [d2 g2 a2 e2]/4') ).slow(1) - .transpose.slowcat(0, 2, 3, 4).midi()`; + .transpose.slowcat(0, 2, 3, 4)`; */ -export const magicSofaHacked = `stack( +export const magicSofa = `stack( '[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4'.mini .every(2, fast(2)) .voicings(), '[c2 f2 g2]/3 [d2 g2 a2 e2]/4'.mini ).slow(1) - .transpose.slowcat(0, 2, 3, 4).midi()`; + .transpose.slowcat(0, 2, 3, 4)`; -export const confusedPhone = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) +/* export const confusedPhone = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) .superimpose( x => transpose(-12,x).late(0), x => transpose(7,x).late(0.2), @@ -353,7 +354,7 @@ export const confusedPhone = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) ) .scale(sequence('C dorian', 'C mixolydian').slow(4)) .scaleTranspose(slowcat(0,1,2,1).slow(2)) -.synth('triangle').gain(0.2).filter(1500)`; +.synth('triangle').gain(0.5).filter(1500)`; */ export const confusedPhoneDynamic = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) .superimpose( @@ -361,18 +362,16 @@ export const confusedPhoneDynamic = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) ) .scale(sequence('C dorian', 'C mixolydian').slow(4)) .scaleTranspose(slowcat(0,1,2,1).slow(2)) -.synth('triangle').gain(0.2).filter(1500)`; +.synth('triangle').gain(0.5).filter(1500)`; -export const confusedPhonePartial = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) +export const confusedPhone = `'[g2 ~@1.3] [c3 ~@1.3]'.mini .superimpose( transpose(-12).late(0), - transpose(7).late(0.2), - transpose(10).late(0.4), - transpose(12).late(0.6), - transpose(24).late(0.8) + transpose(7).late(0.1), + transpose(10).late(0.2), + transpose(12).late(0.3), + transpose(24).late(0.4) ) -.scale(sequence('C dorian', 'C mixolydian').slow(4)) -.scaleTranspose(slowcat(0,1,2,1).slow(2)) -.synth('triangle').gain(0.2).filter(1500)`; - -export default swimming; +.scale(slowcat('C dorian', 'C mixolydian')) +.scaleTranspose(slowcat(0,1,2,1)) +.slow(2)`; diff --git a/repl/src/voicings.ts b/repl/src/voicings.ts index 4f7504bc..ef24cf9c 100644 --- a/repl/src/voicings.ts +++ b/repl/src/voicings.ts @@ -1,6 +1,6 @@ -import { Pattern as _Pattern, stack, TimeSpan, Hap, reify } from '../../strudel.mjs'; -import voicings from 'chord-voicings'; -const { dictionaryVoicing, minTopNoteDiff, lefthand } = voicings; +import { Pattern as _Pattern, stack, Hap, reify } from '../../strudel.mjs'; +import _voicings from 'chord-voicings'; +const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings; const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) => dictionaryVoicing({ @@ -25,10 +25,29 @@ Pattern.prototype.fmapNested = function (func) { ); }; -Pattern.prototype.voicings = function (range = ['F3', 'A4']) { +Pattern.prototype.voicings = function (range) { let lastVoicing; + if (!range?.length) { + // allows to pass empty array, if too lazy to specify range + range = ['F3', 'A4']; + } return this.fmapNested((event) => { lastVoicing = getVoicing(event.value, lastVoicing, range); return stack(...lastVoicing); }); }; + +Pattern.prototype.chordBass = function () { // range = ['G1', 'C3'] + return this._mapNotes((value) => { + console.log('value',value); + const [_, root] = value.value.match(/^([a-gC-G])[b#]?.*$/); + const bassNote = root + '2'; + return { ...value, value: bassNote }; + }); +}; + +Pattern.prototype.define('voicings', (range, pat) => pat.voicings(range), { composable: true }); +Pattern.prototype.define('chordBass', (pat) => { + console.log('call chordBass ...', pat); + return pat.chordBass() +}, { composable: true }); diff --git a/strudel.mjs b/strudel.mjs index a6aed80d..ad87815b 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -9,7 +9,7 @@ const flatten = arr => [].concat(...arr) const id = a => a export function curry(func, overload) { - return function curried(...args) { + const fn = function curried(...args) { if (args.length >= func.length) { return func.apply(this, args) } @@ -23,6 +23,10 @@ export function curry(func, overload) { return partial; } } + if (overload) { // overload function without args... needed for chordBass.transpose(2) + overload(fn, []); + } + return fn; } // Returns the start of the cycle. @@ -567,6 +571,10 @@ class Pattern { superimpose(...funcs) { return this.stack(...funcs.map((func) => func(this))); } + + edit(...funcs) { + return stack(...funcs.map(func => func(this))); + } } // methods of Pattern that get callable factories @@ -575,20 +583,6 @@ Pattern.prototype.patternified = ['fast', 'slow', 'early', 'late']; Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, sequence, 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) -// let's hack built in strings -const hackStrings = () => { - const pureGetter = { - get: function () { - return pure(String(this)); - }, - }; - // with this, you can do 'c2'.pure.fast(2) or 'c2'.p.fast(2) - Object.defineProperty(String.prototype, 'pure', pureGetter); - Object.defineProperty(String.prototype, 'p', pureGetter); -}; - -hackStrings(); // comment out this line if you panic - const silence = new Pattern(_ => []) function pure(value) { @@ -604,7 +598,7 @@ function steady(value) { } function reify(thing) { - if (thing.constructor.name == "Pattern") { + if (thing?.constructor?.name == "Pattern") { return thing } return pure(thing) @@ -744,8 +738,13 @@ const when = curry((binary, f, pat) => pat.when(binary, f)) const off = curry((t, f, pat) => pat.off(t,f)) const jux = curry((f, pat) => pat.jux(f)) const append = curry((a, pat) => pat.append(a)) +const superimpose = curry((array, pat) => pat.superimpose(...array)) -Pattern.prototype.composable = { fast, slow, early, late } +// problem: curried functions with spread arguments must have pat at the beginning +// with this, we cannot keep the pattern open at the end.. solution for now: use array to keep using pat as last arg + +// these are the core composable functions. they are extended with Pattern.prototype.define below +Pattern.prototype.composable = { fast, slow, early, late, superimpose } // adds Pattern.prototype.composable to given function as child functions // then you can do transpose(2).late(0.2) instead of x => x.transpose(2).late(0.2) @@ -754,16 +753,42 @@ export function makeComposable(func) { // compose with dot func[functionName] = (...args) => { // console.log(`called ${functionName}(${args.join(',')})`); - return compose(func, composable(...args)); + const composition = compose(func, composable(...args)); + // the composition itself must be composable too :) + // then you can do endless chaining transpose(2).late(0.2).fast(2) ... + return makeComposable(composition); }; }); + return func; +} + +// this will add func as name to list of composable / patternified functions. +// those lists will be used in bootstrap to curry and compose everything, to support various call patterns +Pattern.prototype.define = (name, func, options = {}) => { + if (options.composable) { + Pattern.prototype.composable[name] = func; + } + if(options.patternified) { + Pattern.prototype.patternified = Pattern.prototype.patternified.concat([name]); + } +} + +// call this after all Patter.prototype.define calls have been executed! (right before evaluate) +Pattern.prototype.bootstrap = () => { + // makeComposable(Pattern.prototype); + const bootstrapped = Object.fromEntries(Object.entries(Pattern.prototype.composable).map(([functionName, composable]) => { + if(Pattern.prototype[functionName]) { + // without this, 'C^7'.m.chordBass.transpose(2) will throw "C^7".m.chordBass.transpose is not a function + Pattern.prototype[functionName] = makeComposable(Pattern.prototype[functionName]); // is this needed? + } + return [functionName, curry(composable, makeComposable)]; + })); + return bootstrapped; } -// TODO: automatically make curried functions curried functions composable -// see tonal.ts#transpose for an example export {Fraction, TimeSpan, Hap, Pattern, pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr, reify, silence, fast, slow, early, late, rev, - add, sub, mul, div, union, every, when, off, jux, append + add, sub, mul, div, union, every, when, off, jux, append, superimpose }