automated patternify, pseudo getters, string hacks

This commit is contained in:
Felix Roos 2022-02-14 19:56:37 +01:00
parent f2acc12174
commit 240ebede6f
7 changed files with 184 additions and 69 deletions

View File

@ -92,8 +92,14 @@ function App() {
// set active pattern on ctrl+enter // set active pattern on ctrl+enter
useLayoutEffect(() => { useLayoutEffect(() => {
const handleKeyPress = (e: any) => { const handleKeyPress = (e: any) => {
if (e.ctrlKey && e.code === 'Enter') { if (e.ctrlKey || e.altKey) {
activatePattern(); switch (e.code) {
case 'Enter':
activatePattern();
break;
case 'Period':
cycle.stop();
}
} }
}; };
document.addEventListener('keypress', handleKeyPress); document.addEventListener('keypress', handleKeyPress);
@ -188,7 +194,7 @@ function App() {
} }
}} }}
/> />
<span className="p-4 absolute bottom-0 right-0 text-xs whitespace-pre text-right"> <span className="p-4 absolute top-0 right-0 text-xs whitespace-pre text-right">
{!cycle.started {!cycle.started
? `press ctrl+enter to play\n` ? `press ctrl+enter to play\n`
: !isHot && activePattern !== pattern : !isHot && activePattern !== pattern

View File

@ -28,6 +28,7 @@ Pattern.prototype.midi = function (output: string, channel = 1) {
return this.fmap((value: any) => ({ return this.fmap((value: any) => ({
...value, ...value,
onTrigger: (time: number, event: any) => { onTrigger: (time: number, event: any) => {
value = value.value || value;
if (!isNote(value)) { if (!isNote(value)) {
throw new Error('not a note: ' + value); throw new Error('not a note: ' + value);
} }

View File

@ -5,31 +5,50 @@ import './tone';
import './midi'; import './midi';
import './voicings'; import './voicings';
import './tonal'; import './tonal';
import * as tonalStuff from './tonal';
import './groove'; import './groove';
import * as toneStuff from './tone'; import * as toneStuff from './tone';
import shapeshifter from './shapeshifter'; import shapeshifter from './shapeshifter';
// even if some functions are not used, we need them to be available in eval // even if some functions are not used, we need them to be available in eval
const { const {
Fraction,
TimeSpan,
Hap,
Pattern,
pure, pure,
stack, stack,
slowcat, slowcat,
fastcat, fastcat,
cat, cat,
timeCat,
sequence, sequence,
polymeter, polymeter,
pm, pm,
polyrhythm, polyrhythm,
pr, pr,
/* reify, */ silence, // reify,
timeCat, silence,
Fraction, fast,
Pattern, slow,
TimeSpan, early,
Hap, late,
rev,
add,
sub,
mul,
div,
union,
every,
when,
off,
jux,
append,
} = strudel; } = strudel;
const { autofilter, filter, gain } = toneStuff; const { autofilter, filter, gain } = toneStuff;
const { transpose } = tonalStuff;
function reify(thing: any) { function reify(thing: any) {
if (thing?.constructor?.name === 'Pattern') { if (thing?.constructor?.name === 'Pattern') {
return thing; return thing;
@ -135,12 +154,20 @@ export const mini = (...strings: string[]) => {
// shorthand for mini // shorthand for mini
const m = mini; const m = mini;
// shorthand for stack, automatically minifying strings
const s = (...strings) => { const hackStrings = () => {
const patternified = strings.map((s) => minify(s)); const miniGetter = {
return stack(...patternified); 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) // includes haskell style (raw krill parsing)
export const h = (string: string) => { export const h = (string: string) => {
const ast = krill.parse(string); const ast = krill.parse(string);
@ -151,18 +178,18 @@ export const h = (string: string) => {
export const parse: any = (code: string) => { export const parse: any = (code: string) => {
let _pattern; let _pattern;
let mode; let mode;
try { /* try {
_pattern = h(code); _pattern = h(code);
mode = 'pegjs'; mode = 'pegjs';
} catch (err) { } catch (err) { */
// code is not haskell like // code is not haskell like
mode = 'javascript'; mode = 'javascript';
code = shapeshifter(code); code = shapeshifter(code);
_pattern = eval(code); _pattern = minify(eval(code));
if (_pattern?.constructor?.name !== 'Pattern') { if (_pattern?.constructor?.name !== 'Pattern') {
const message = `got "${typeof _pattern}" instead of pattern`; const message = `got "${typeof _pattern}" instead of pattern`;
throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.')); throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.'));
}
} }
/* } */
return { mode, pattern: _pattern }; return { mode, pattern: _pattern };
}; };

View File

@ -1,5 +1,5 @@
import { Note, Interval, Scale } from '@tonaljs/tonal'; 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; const Pattern = _Pattern as any;
@ -74,9 +74,11 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
return { value: Note.transpose(value, interval), scale }; 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) { Pattern.prototype._scaleTranspose = function (offset: number | string) {
return this._mapNotes(({ value, scale }: NoteEvent) => { 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 }; 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) { Pattern.prototype._scale = function (scale: string) {
return this._mapNotes((value) => ({ ...value, scale })); return this._mapNotes((value) => ({ ...value, scale }));
}; };
Pattern.prototype.scale = function (scale: string) { Pattern.prototype.patternified = Pattern.prototype.patternified.concat(['transpose', 'scaleTranspose', 'scale']);
return this._patternify(Pattern.prototype._scale)(scale); // Object.assign(Pattern.prototype.modifiers, { transpose })
};

View File

@ -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) { Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
return this.fmap((value: any) => { return this.fmap((value: any) => {
if (!value?.getInstrument) { if (!value?.getInstrument) {
@ -85,16 +81,11 @@ export const gain =
Pattern.prototype._gain = function (g: number) { Pattern.prototype._gain = function (g: number) {
return this.chain(gain(g)); 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') { Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') {
return this.chain(filter(freq, q, type)); 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) { Pattern.prototype.autofilter = function (g: number) {
return this.chain(autofilter(g)); return this.chain(autofilter(g));
}; };
Pattern.prototype.patternified = Pattern.prototype.patternified.concat(['synth', 'gain', 'filter']);

View File

@ -1,7 +1,7 @@
export const timeCatMini = `s( export const timeCatMini = `stack(
'c3@3 [eb3, g3, [c4 d4]/2]', 'c3@3 [eb3, g3, [c4 d4]/2]'.mini,
'c2 g2', 'c2 g2'.mini,
m('[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2').slow(8) '[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8)
)`; )`;
export const timeCat = `stack( export const timeCat = `stack(
@ -285,6 +285,13 @@ export const transposedChords = `stack(
slowcat(1, 2, 3, 2).slow(2) slowcat(1, 2, 3, 2).slow(2)
).transpose(5)`; ).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) export const scaleTranspose = `stack(f2, f3, c4, ab4)
.scale(sequence('F minor', 'F harmonic minor').slow(4)) .scale(sequence('F minor', 'F harmonic minor').slow(4))
.scaleTranspose(sequence(0, -1, -2, -3).slow(4)) .scaleTranspose(sequence(0, -1, -2, -3).slow(4))
@ -307,4 +314,53 @@ export const groove = `stack(
.gain(0.25) .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),
'[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; export default swimming;

View File

@ -7,7 +7,7 @@ const flatten = arr => [].concat(...arr)
const id = a => a const id = a => a
function curry(func) { export function curry(func) {
return function curried(...args) { return function curried(...args) {
if (args.length >= func.length) { if (args.length >= func.length) {
return func.apply(this, args) return func.apply(this, args)
@ -204,8 +204,26 @@ class Hap {
} }
class Pattern { class Pattern {
// the following functions will get patternFactories as nested functions:
constructor(query) { 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() { _splitQueries() {
@ -461,38 +479,21 @@ class Pattern {
return fastQuery.withEventTime(t => t.div(factor)) return fastQuery.withEventTime(t => t.div(factor))
} }
fast(...factor) {
return this._patternify(Pattern.prototype._fast)(...factor)
}
_slow(factor) { _slow(factor) {
return this._fast(1/factor) return this._fast(1/factor)
} }
slow(...factor) {
return this._patternify(Pattern.prototype._slow)(...factor)
}
_early(offset) { _early(offset) {
// Equivalent of Tidal's <~ operator // Equivalent of Tidal's <~ operator
offset = Fraction(offset) offset = Fraction(offset)
return this.withQueryTime(t => t.add(offset)).withEventTime(t => t.sub(offset)) return this.withQueryTime(t => t.add(offset)).withEventTime(t => t.sub(offset))
} }
early(...factor) {
return this._patternify(Pattern.prototype._early)(...factor)
}
_late(offset) { _late(offset) {
// Equivalent of Tidal's ~> operator // Equivalent of Tidal's ~> operator
return this._early(0-offset) return this._early(0-offset)
} }
late(...factor) {
return this._patternify(Pattern.prototype._late)(...factor)
}
when(binary_pat, func) { when(binary_pat, func) {
//binary_pat = sequence(binary_pat) //binary_pat = sequence(binary_pat)
const true_pat = binary_pat._filterValues(id) const true_pat = binary_pat._filterValues(id)
@ -549,8 +550,40 @@ class Pattern {
return stack([left,func(right)]) 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(_ => []) const silence = new Pattern(_ => [])
function pure(value) { function pure(value) {
@ -583,8 +616,12 @@ function slowcat(...pats) {
// successively, one per cycle. // successively, one per cycle.
pats = pats.map(reify) pats = pats.map(reify)
const query = function(span) { 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] 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. // 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 // 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. // be the second (rather than fourth) cycle from the first pattern.