From 240ebede6f405abac9146e17cc4c0661c95cbbcb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 14 Feb 2022 19:56:37 +0100 Subject: [PATCH] automated patternify, pseudo getters, string hacks --- repl/src/App.tsx | 12 ++++++-- repl/src/midi.ts | 1 + repl/src/parse.ts | 67 +++++++++++++++++++++++++++++------------ repl/src/tonal.ts | 19 +++++------- repl/src/tone.ts | 13 ++------ repl/src/tunes.ts | 64 ++++++++++++++++++++++++++++++++++++--- strudel.mjs | 77 +++++++++++++++++++++++++++++++++++------------ 7 files changed, 184 insertions(+), 69 deletions(-) diff --git a/repl/src/App.tsx b/repl/src/App.tsx index 0e3f278f..49afb436 100644 --- a/repl/src/App.tsx +++ b/repl/src/App.tsx @@ -92,8 +92,14 @@ function App() { // set active pattern on ctrl+enter useLayoutEffect(() => { const handleKeyPress = (e: any) => { - if (e.ctrlKey && e.code === 'Enter') { - activatePattern(); + if (e.ctrlKey || e.altKey) { + switch (e.code) { + case 'Enter': + activatePattern(); + break; + case 'Period': + cycle.stop(); + } } }; document.addEventListener('keypress', handleKeyPress); @@ -188,7 +194,7 @@ function App() { } }} /> - + {!cycle.started ? `press ctrl+enter to play\n` : !isHot && activePattern !== pattern diff --git a/repl/src/midi.ts b/repl/src/midi.ts index 66a0af51..a7e601c9 100644 --- a/repl/src/midi.ts +++ b/repl/src/midi.ts @@ -28,6 +28,7 @@ Pattern.prototype.midi = function (output: string, channel = 1) { return this.fmap((value: any) => ({ ...value, onTrigger: (time: number, event: any) => { + value = value.value || value; if (!isNote(value)) { throw new Error('not a note: ' + value); } diff --git a/repl/src/parse.ts b/repl/src/parse.ts index baa5a47e..c4e5ba41 100644 --- a/repl/src/parse.ts +++ b/repl/src/parse.ts @@ -5,31 +5,50 @@ 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, - timeCat, - Fraction, - Pattern, - TimeSpan, - Hap, + // reify, + silence, + fast, + slow, + early, + late, + rev, + add, + sub, + mul, + div, + union, + every, + when, + off, + jux, + append, } = strudel; const { autofilter, filter, gain } = toneStuff; +const { transpose } = tonalStuff; + function reify(thing: any) { if (thing?.constructor?.name === 'Pattern') { return thing; @@ -135,12 +154,20 @@ export const mini = (...strings: string[]) => { // shorthand for mini const m = mini; -// shorthand for stack, automatically minifying strings -const s = (...strings) => { - const patternified = strings.map((s) => minify(s)); - return stack(...patternified); + +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); @@ -151,18 +178,18 @@ export const h = (string: string) => { export const parse: any = (code: string) => { let _pattern; let mode; - try { + /* try { _pattern = h(code); mode = 'pegjs'; - } catch (err) { - // code is not haskell like - mode = 'javascript'; - code = shapeshifter(code); - _pattern = 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?' : '.')); - } + } 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?' : '.')); } + /* } */ return { mode, pattern: _pattern }; }; diff --git a/repl/src/tonal.ts b/repl/src/tonal.ts index 6f3bfe35..cf6bb683 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 } from '../../strudel.mjs'; +import { Pattern as _Pattern, curry } from '../../strudel.mjs'; const Pattern = _Pattern as any; @@ -74,9 +74,11 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { return { value: Note.transpose(value, interval), scale }; }); }; -Pattern.prototype.transpose = function (intervalOrSemitones: string | number) { - return this._patternify(Pattern.prototype._transpose)(intervalOrSemitones); -}; + +// TODO: find out how to patternify this function when it's standalone +// e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or +// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or +export const transpose = curry((a, pat) => pat.transpose(a)) Pattern.prototype._scaleTranspose = function (offset: number | string) { return this._mapNotes(({ value, scale }: NoteEvent) => { @@ -86,14 +88,9 @@ Pattern.prototype._scaleTranspose = function (offset: number | string) { return { value: scaleTranspose(scale, Number(offset), value), scale }; }); }; -Pattern.prototype.scaleTranspose = function (offset: number | string) { - return this._patternify(Pattern.prototype._scaleTranspose)(offset); -}; - Pattern.prototype._scale = function (scale: string) { return this._mapNotes((value) => ({ ...value, scale })); }; -Pattern.prototype.scale = function (scale: string) { - return this._patternify(Pattern.prototype._scale)(scale); -}; +Pattern.prototype.patternified = Pattern.prototype.patternified.concat(['transpose', 'scaleTranspose', 'scale']); +// Object.assign(Pattern.prototype.modifiers, { transpose }) diff --git a/repl/src/tone.ts b/repl/src/tone.ts index 8e14d4ef..64f44cf5 100644 --- a/repl/src/tone.ts +++ b/repl/src/tone.ts @@ -32,10 +32,6 @@ Pattern.prototype._synth = function (type: any = 'triangle') { }); }; -Pattern.prototype.synth = function (type: any = 'triangle') { - return this._patternify(Pattern.prototype._synth)(type); -}; - Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) { return this.fmap((value: any) => { if (!value?.getInstrument) { @@ -85,16 +81,11 @@ export const gain = Pattern.prototype._gain = function (g: number) { return this.chain(gain(g)); }; -Pattern.prototype.gain = function (g: number) { - return this._patternify(Pattern.prototype._gain)(g); -}; Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') { return this.chain(filter(freq, q, type)); }; -Pattern.prototype.filter = function (freq: number) { - return this._patternify(Pattern.prototype._filter)(freq); -}; - Pattern.prototype.autofilter = function (g: number) { return this.chain(autofilter(g)); }; + +Pattern.prototype.patternified = Pattern.prototype.patternified.concat(['synth', 'gain', 'filter']); diff --git a/repl/src/tunes.ts b/repl/src/tunes.ts index 525fcb58..d44d1f91 100644 --- a/repl/src/tunes.ts +++ b/repl/src/tunes.ts @@ -1,7 +1,7 @@ -export const timeCatMini = `s( - 'c3@3 [eb3, g3, [c4 d4]/2]', - 'c2 g2', - m('[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2').slow(8) +export const timeCatMini = `stack( + 'c3@3 [eb3, g3, [c4 d4]/2]'.mini, + 'c2 g2'.mini, + '[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8) )`; export const timeCat = `stack( @@ -285,6 +285,13 @@ export const transposedChords = `stack( slowcat(1, 2, 3, 2).slow(2) ).transpose(5)`; +export const transposedChordsHacked = `stack( + 'c2 eb2 g2'.mini, + 'Cm7'.pure.voicings(['g2','c4']).slow(2) +).transpose( + slowcat(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)) @@ -307,4 +314,53 @@ export const groove = `stack( .gain(0.25) ).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), + '[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)`; + +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()`; + +export const magicSofaHacked = `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()`; + +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), + x => transpose(10,x).late(0.4), + x => transpose(12,x).late(0.6), + x => transpose(24,x).late(0.8) +) +.scale(sequence('C dorian', 'C mixolydian').slow(4)) +.scaleTranspose(slowcat(0,1,2,1).slow(2)) +.synth('triangle').gain(0.2).filter(1500)`; + +export const confusedPhoneDynamic = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2)) +.superimpose( + ...[-12,7,10,12,24].slice(0,5).map((t,i,{length}) => x => transpose(t,x).late(i/length)) +) +.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; diff --git a/strudel.mjs b/strudel.mjs index 8fdf0895..aef9b8af 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -7,7 +7,7 @@ const flatten = arr => [].concat(...arr) const id = a => a -function curry(func) { +export function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args) @@ -204,8 +204,26 @@ class Hap { } class Pattern { + // the following functions will get patternFactories as nested functions: constructor(query) { - this.query = query + this.query = query; + // the following code will assign `patternFactories` as child functions to all methods of Pattern that don't start with '_' + const proto = Object.getPrototypeOf(this); + // proto.patternified is defined below Pattern class. You can add more patternified functions from outside. + proto.patternified.forEach((prop) => { + // patternify function + this[prop] = (...args) => this._patternify(Pattern.prototype['_' + prop])(...args); + // with the following, you can do, e.g. `stack(c3).fast.slowcat(1, 2, 4, 8)` instead of `stack(c3).fast(slowcat(1, 2, 4, 8))` + Object.assign( + this[prop], + Object.fromEntries( + Object.entries(Pattern.prototype.factories).map(([type, func]) => [ + type, + (...args) => this[prop](func(...args)), + ]) + ) + ); + }); } _splitQueries() { @@ -461,38 +479,21 @@ class Pattern { return fastQuery.withEventTime(t => t.div(factor)) } - fast(...factor) { - return this._patternify(Pattern.prototype._fast)(...factor) - } - _slow(factor) { return this._fast(1/factor) } - slow(...factor) { - return this._patternify(Pattern.prototype._slow)(...factor) - } - _early(offset) { // Equivalent of Tidal's <~ operator offset = Fraction(offset) return this.withQueryTime(t => t.add(offset)).withEventTime(t => t.sub(offset)) } - early(...factor) { - return this._patternify(Pattern.prototype._early)(...factor) - } - _late(offset) { // Equivalent of Tidal's ~> operator return this._early(0-offset) } - - late(...factor) { - return this._patternify(Pattern.prototype._late)(...factor) - } - when(binary_pat, func) { //binary_pat = sequence(binary_pat) const true_pat = binary_pat._filterValues(id) @@ -549,8 +550,40 @@ class Pattern { return stack([left,func(right)]) } + + // is there a different name for those in tidal? + stack(...pats) { + return stack(this, ...pats) + } + sequence(...pats) { + return sequence(this, ...pats) + } + + superimpose(...funcs) { + return this.stack(...funcs.map((func) => func(this))); + } } +// methods of Pattern that get callable factories +Pattern.prototype.patternified = ['fast', 'slow', 'early', 'late']; +// 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}; +// 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) { @@ -583,8 +616,12 @@ function slowcat(...pats) { // successively, one per cycle. pats = pats.map(reify) const query = function(span) { - const pat_n = Math.floor(span.begin) % pats.length + const pat_n = Math.floor(span.begin) % pats.length; const pat = pats[pat_n] + if (!pat) { + // pat_n can be negative, if the span is in the past.. + return []; + } // A bit of maths to make sure that cycles from constituent patterns aren't skipped. // For example if three patterns are slowcat-ed, the fourth cycle of the result should // be the second (rather than fourth) cycle from the first pattern.