From 4c058760667c6b2c744cc2beffd4fbaf9bf4a90f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 17 Jun 2022 23:23:51 +0200 Subject: [PATCH] rewrite webaudio + migrate tunes + empty setters --- packages/core/controls.mjs | 9 +- packages/webaudio/webaudio.mjs | 206 ++++++++++++--------------------- repl/.gitignore | 2 + repl/src/tunes.mjs | 68 +++++++---- 4 files changed, 125 insertions(+), 160 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7b8cfc8c..c6f96263 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -750,19 +750,22 @@ const generic_params = [ const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x })); -const _setter = (func) => +const _setter = (func, name) => function (...pats) { + if (!pats.length) { + return this.fmap((value) => ({ [name]: value })); + } return this.set(func(...pats)); }; generic_params.forEach(([type, name, description]) => { controls[name] = (...pats) => _name(name, ...pats); - Pattern.prototype[name] = _setter(controls[name]); + Pattern.prototype[name] = _setter(controls[name], name); }); // create custom param controls.createParam = (name) => { - Pattern.prototype[name] = _setter(controls[name]); + Pattern.prototype[name] = _setter(controls[name], name); return (...pats) => _name(name, ...pats); }; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index a8b301fb..8cd3b70b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -6,21 +6,20 @@ This program is free software: you can redistribute it and/or modify it under th // import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core'; +import { fromMidi } from '@strudel.cycles/core'; import { Tone } from '@strudel.cycles/tone'; -const { Pattern, getFrequency, patternify2 } = strudel; +const { Pattern } = strudel; -let audioContext; -export const getAudioContext = () => { - if (!audioContext) { - audioContext = new AudioContext(); - } - return audioContext; +const getFilter = (ac, type, frequency, Q) => { + const filter = ac.createBiquadFilter(); + filter.type = type; + filter.frequency.value = frequency; + filter.Q.value = Q; + return filter; }; -const lookahead = 0.2; - -const adsr = (attack, decay, sustain, release, velocity, begin, end) => { - const gainNode = getAudioContext().createGain(); +const getADSR = (ac, attack, decay, sustain, release, velocity, begin, end) => { + const gainNode = ac.createGain(); gainNode.gain.setValueAtTime(0, begin); gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start @@ -30,129 +29,70 @@ const adsr = (attack, decay, sustain, release, velocity, begin, end) => { return gainNode; }; -Pattern.prototype.withAudioNode = function (createAudioNode) { - return this._withHap((hap) => { - return hap.setContext({ - ...hap.context, - createAudioNode: (t, e) => createAudioNode(t, e, hap.context.createAudioNode?.(t, hap)), - }); - }); -}; - -Pattern.prototype._wave = function (type) { - return this.withAudioNode((t, e) => { - const osc = getAudioContext().createOscillator(); - osc.type = type; - const f = getFrequency(e); - osc.frequency.value = f; // expects frequency.. - const begin = t ?? e.whole.begin.valueOf() + lookahead; - const end = begin + e.duration.valueOf(); - osc.start(begin); - osc.stop(end); // release? - return osc; - }); -}; -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.valueOf() + lookahead; - const envelope = adsr(a, d, s, r, velocity, begin, end); - node?.connect(envelope); - return envelope; - }); -}; -Pattern.prototype._filter = function (type = 'lowpass', frequency = 1000) { - return this.withAudioNode((t, e, node) => { - const filter = getAudioContext().createBiquadFilter(); - filter.type = type; - filter.frequency.value = frequency; - node?.connect(filter); - return filter; - }); -}; - -Pattern.prototype.filter = function (type, frequency) { - return patternify2(Pattern.prototype._filter)(reify(type), reify(frequency), this); -}; - Pattern.prototype.out = function () { - const master = getAudioContext().createGain(); - master.gain.value = 0.1; - master.connect(getAudioContext().destination); - return this.withAudioNode((t, e, node) => { - if (!node) { - console.warn('out: no source! call .osc() first'); + return this.onTrigger((t, hap, ct) => { + const ac = Tone.getContext().rawContext; + // calculate correct time (tone.js workaround) + t = ac.currentTime + t - ct; + // destructure value + let { + freq, + s, + n, + gain = 1, + cutoff, + resonance = 1, + hcutoff, + hresonance = 1, + bandf, + bandq = 1, + pan, + attack = 0.001, + decay = 0, + sustain = 1, + release = 0.001, + } = hap.value; + if (!n && !freq) { + console.warn('unplayable value:', hap.value); + return; } - node?.connect(master); - })._withHap((hap) => { - const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e); - return hap.setContext({ ...hap.context, onTrigger }); + // get frequency + if (!freq && typeof n === 'number') { + freq = fromMidi(n); // + 48); + } + if (!freq && typeof n === 'string') { + freq = fromMidi(toMidi(n)); + } + // the chain will hold all audio nodes that connect to each other + const chain = []; + // make oscillator + const o = ac.createOscillator(); + o.type = s || 'triangle'; + o.frequency.value = Number(freq); + o.start(t); + o.stop(t + hap.duration + release); + chain.push(o); + // envelope + const adsr = getADSR(ac, attack, decay, sustain, release, 1, t, t + hap.duration); + chain.push(adsr); + // filters + cutoff !== undefined && chain.push(getFilter(ac, 'lowpass', cutoff, resonance)); + hcutoff !== undefined && chain.push(getFilter(ac, 'highpass', hcutoff, hresonance)); + bandf !== undefined && chain.push(getFilter(ac, 'bandpass', bandf, bandq)); + // TODO vowel + // TODO delay / delaytime / delayfeedback + // panning + if (pan !== undefined) { + const panner = ac.createStereoPanner(); + panner.pan.value = 2 * pan - 1; + chain.push(panner); + } + // master out + const master = ac.createGain(); + master.gain.value = 0.1 * gain; + chain.push(master); + chain.push(ac.destination); + // connect chain elements together + chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); }); }; - -Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true }); - -/* - -// TODO: throw all this away and use that: - -stack( - freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)") - .layer(x=>x.mul("1.006,.995"))), // detune - freq("440(5,8)".legato(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8)) -).s("/2") - .cutoff(perlin.range(100,4000).slow(4))//.resonance(saw.range(0,15).slow(7)) - .jux(rev) - .onTrigger((t,hap,ct) => { - const ac = Tone.getContext().rawContext; - // calculate correct time (tone.js workaround) - t = ac.currentTime + t - ct; - // destructure value - const { freq, s, gain = 1, - cutoff, resonance = 1, - hcutoff, hresonance = 1, - bandf, bandq = 1, - pan - } = hap.value; - // TODO get frequency from n or note - // oscillator - const o = ac.createOscillator(); - o.type = s || 'triangle'; - o.frequency.value = Number(freq); - o.start(t); - o.stop(t + hap.duration); - // chaining logic - let last = o; - const addToChain = (node) => { - last.connect(node); - last = node; - } - // filters - const getFilter = (type, frequency, Q) => { - const filter = ac.createBiquadFilter(); - filter.type = type; - filter.frequency.value = frequency; - filter.Q.value = Q; - return filter; - } - cutoff !== undefined && addToChain(getFilter('lowpass', cutoff, resonance)); - hcutoff !== undefined && addToChain(getFilter('highpass', hcutoff, hresonance)); - bandf !== undefined && addToChain(getFilter('bandpass', bandf, bandq)); - // TODO: vowel - // TODO: delay / delaytime / delayfeedback - // panning - if(pan !== undefined) { - const panner = ac.createStereoPanner(); - panner.pan.value = 2*pan-1; - addToChain(panner); - } - // gain out - const master = ac.createGain(); - master.gain.value = 0.1 * gain; - master.connect(ac.destination); - last.connect(master); -}) -.stack(s("bd(3,8),hh*4,~ sd").webdirt()) - -*/ diff --git a/repl/.gitignore b/repl/.gitignore index a547bf36..7e93270e 100644 --- a/repl/.gitignore +++ b/repl/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +oldtunes.mjs \ No newline at end of file diff --git a/repl/src/tunes.mjs b/repl/src/tunes.mjs index 9ab29658..900344d9 100644 --- a/repl/src/tunes.mjs +++ b/repl/src/tunes.mjs @@ -728,7 +728,7 @@ bell = bell.chain(vol(0.6).connect(delay),out()); .slow(6) .pianoroll({minMidi:20,maxMidi:120,background:'transparent'})`; -export const waa = `"a4 [a3 c3] a3 c3" +/* export const waa = `n("a4 [a3 c3] a3 c3") .sub("<7 12>/2") .off(1/8, add("12")) .off(1/4, add("7")) @@ -736,23 +736,37 @@ export const waa = `"a4 [a3 c3] a3 c3" .slow(2) .wave("sawtooth square") .filter('lowpass', "<2000 1000 500>") -.out()`; +.out()`; */ -export const waar = `"a4 [a3 c3] a3 c3".color('#F9D649') -.sub("<7 12 5 12>".slow(2)) -.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928")) -.off(1/8,x=>x.add(12).color('#215CB6')) -.slow(2) -.legato(sine.range(0.3, 2).slow(28)) -.wave("sawtooth square".fast(2)) -.filter('lowpass', cosine.range(500,4000).slow(16)) -.out() -.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})`; +export const waa = `n( + "a4 [a3 c3] a3 c3" + .sub("<7 12>/2") + .off(1/8, add("12")) + .off(1/4, add("7")) +) + .legato(.5) + .slow(2) + .s("sawtooth square") + .cutoff("<2000 1000 500>") + .out() +`; + +export const waa2 = `n( + "a4 [a3 c3] a3 c3" + .sub("<7 12 5 12>".slow(2)) + .off(1/4,x=>x.add(7)) + .off(1/8,x=>x.add(12)) +) + .slow(2) + .legato(sine.range(0.3, 2).slow(28)) + .s("sawtooth square".fast(2)) + .cutoff(cosine.range(500,4000).slow(16)) + .out()`; 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 filter1 = x=>x.cutoff(lfo2.range(300,3000)); +const filter2 = x=>x.hcutoff(lfo.range(1000,6000)).cutoff(4000) const scales = cat('D3 major', 'G3 major').slow(8) const drums = await players({ @@ -765,24 +779,30 @@ const drums = await players({ stack( "-7 0 -7 7".struct("x(5,8,2)").fast(2).sub(7) - .scale(scales).wave("sawtooth,square").velocity(.3).adsr(0.01,0.1,.5,0) + .scale(scales) + .n() + .s("sawtooth,square") + .gain(.3).attack(0.01).decay(0.1).sustain(.5) .apply(filter1), "~@3 [<2 3>,<4 5>]" - .echo(8,1/16,.7) + .echo(4,1/16,.7) .scale(scales) - .wave('square').velocity(.7).adsr(0.01,0.1,0).apply(filter1), - "6 5 4".add(14) + .n() + .s('square').gain(.7) + .attack(0.01).decay(0.1).sustain(0) + .apply(filter1), + "6 4 2".add(14) .superimpose(sub("5")) .fast(1).euclidLegato(3,8) .mask("<1 0@7>") .fast(2) - .echo(32, 1/8, .9) + .echo(32, 1/8, .8) .scale(scales) - .wave("sawtooth") - .velocity(.2) - .adsr(.01,.5,0) + .n() + .s("sawtooth") + .gain(sine.range(.1,.4).slow(8)) + .attack(.001).decay(.2).sustain(0) .apply(filter2) - //.echo(4,1/16,.5) ).out().stack( stack( "bd <~@7 [~ bd]>".fast(2), @@ -790,7 +810,7 @@ stack( "[~ hh3]*2" ).tone(drums.chain(vol(.18),out())).fast(2) ).slow(2) - + //.pianoroll({minMidi:20, maxMidi:160}) // strudel disable-highlighting`;