diff --git a/repl/src/tonal.ts b/repl/src/tonal.ts index 89cdba10..f9820ff1 100644 --- a/repl/src/tonal.ts +++ b/repl/src/tonal.ts @@ -62,7 +62,8 @@ function scaleTranspose(scale: string, offset: number, note: string) { Pattern.prototype._mapNotes = function (func: (note: NoteEvent) => NoteEvent) { return this.fmap((event: string | NoteEvent) => { const noteEvent = toNoteEvent(event); - return func(noteEvent); + // TODO: generalize? this is practical for any event that is expected to be an object with + return { ...noteEvent, ...func(noteEvent) }; }); }; diff --git a/repl/src/tone.ts b/repl/src/tone.ts index 4a90af45..5cd28adf 100644 --- a/repl/src/tone.ts +++ b/repl/src/tone.ts @@ -1,8 +1,98 @@ import { Pattern as _Pattern } from '../../strudel.mjs'; -import { AutoFilter, Destination, Filter, Gain, isNote, Synth } from 'tone'; +import { AutoFilter, Destination, Filter, Gain, isNote, Synth, PolySynth } from 'tone'; const Pattern = _Pattern as any; +// with this function, you can play the pattern with any tone synth +Pattern.prototype.tone = function (instrument) { + // instrument.toDestination(); + return this.fmap((value: any) => { + value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value; + const onTrigger = (time, event) => { + if (instrument.constructor.name === 'PluckSynth') { + instrument.triggerAttack(value.value, time); + } else if (instrument.constructor.name === 'NoiseSynth') { + instrument.triggerAttackRelease(event.duration, time); // noise has no value + } else { + instrument.triggerAttackRelease(value.value, event.duration, time); + } + }; + return { ...value, instrument, onTrigger }; + }); +}; + +Pattern.prototype.define('tone', (type, pat) => pat.tone(type), { composable: true, patternified: false }); + +// helpers + +export const vol = (v) => new Gain(v); +export const lowpass = (v) => new Filter(v, 'lowpass'); +export const highpass = (v) => new Filter(v, 'highpass'); +export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } }); +export const osc = (type) => ({ oscillator: { type } }); +export const out = Destination; + +/* + +You are entering experimental zone + +*/ + +// the following code is an attempt to minimize tonejs code.. it is still an experiment + +const chainable = function (instr) { + const _chain = instr.chain.bind(instr); + let chained: any = []; + instr.chain = (...args) => { + chained = chained.concat(args); + instr.disconnect(); // disconnect from destination / previous chain + return _chain(...chained, Destination); + }; + // shortcuts: chaining multiple won't work forn now.. like filter(1000).gain(0.5). use chain + native Tone calls instead + instr.filter = (freq = 1000, type: BiquadFilterType = 'lowpass') => + instr.chain( + new Filter(freq, type) // .Q.setValueAtTime(q, time); + ); + instr.gain = (gain: number = 0.9) => instr.chain(new Gain(gain)); + return instr; +}; + +// helpers +export const poly = (type) => { + const s: any = new PolySynth(Synth, { oscillator: { type } }).toDestination(); + return chainable(s); +}; + +Pattern.prototype._poly = function (type: any = 'triangle') { + const instrumentConfig: any = { + oscillator: { type }, + envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 }, + }; + if (!this.instrument) { + // create only once to keep the js heap happy + // this.instrument = new PolySynth(Synth, instrumentConfig).toDestination(); + this.instrument = poly(type); + } + return this.fmap((value: any) => { + value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value; + const onTrigger = (time, event) => { + this.instrument.set(instrumentConfig); + this.instrument.triggerAttackRelease(value.value, event.duration, time); + }; + return { ...value, instrumentConfig, onTrigger }; + }); +}; + +Pattern.prototype.define('poly', (type, pat) => pat.poly(type), { composable: true, patternified: true }); + +/* + +You are entering danger zone + +*/ + +// everything below is nice in theory, but not healthy for the JS heap, as nodes get recreated on every call + const getTrigger = (getChain: any, value: any) => (time: number, event: any) => { const chain = getChain(); // make sure this returns a node that is connected toDestination // time if (!isNote(value)) { @@ -88,8 +178,6 @@ Pattern.prototype.autofilter = function (g: number) { return this.chain(autofilter(g)); }; -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 });