diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index c5c19644..6e34c3d0 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -581,6 +581,11 @@ const generic_params = [ 'size', 'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.', ], + [ + 'f', + 'roomsize', + 'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.', + ], // ['f', 'sagogo', ''], // ['f', 'sclap', ''], // ['f', 'sclaves', ''], diff --git a/packages/webaudio/reverb.mjs b/packages/webaudio/reverb.mjs new file mode 100644 index 00000000..10e6dcd1 --- /dev/null +++ b/packages/webaudio/reverb.mjs @@ -0,0 +1,20 @@ +if (typeof AudioContext !== 'undefined') { + AudioContext.prototype.impulseResponse = function (duration) { + const length = this.sampleRate * duration; + const impulse = this.createBuffer(2, length, this.sampleRate); + const IR = impulse.getChannelData(0); + for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration); + return impulse; + }; + + AudioContext.prototype.createReverb = function (duration) { + const convolver = this.createConvolver(); + convolver.setDuration = (d) => { + convolver.buffer = this.impulseResponse(d); + convolver.duration = duration; + return convolver; + }; + convolver.setDuration(duration); + return convolver; + }; +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 1412f613..41c6f748 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -8,6 +8,7 @@ This program is free software: you can redistribute it and/or modify it under th import * as strudel from '@strudel.cycles/core'; import { fromMidi, toMidi } from '@strudel.cycles/core'; import './feedbackdelay.mjs'; +import './reverb.mjs'; import { loadBuffer, reverseBuffer } from './sampler.mjs'; const { Pattern } = strudel; import './vowel.mjs'; @@ -207,6 +208,21 @@ function getDelay(orbit, delaytime, delayfeedback, t) { return delays[orbit]; } +let reverbs = {}; +function getReverb(orbit, duration = 2) { + if (!reverbs[orbit]) { + const ac = getAudioContext(); + const reverb = ac.createReverb(duration); + reverb.connect(getDestination()); + reverbs[orbit] = reverb; + } + if (reverbs[orbit].duration !== duration) { + reverbs[orbit] = reverbs[orbit].setDuration(duration); + reverbs[orbit].duration = duration; + } + return reverbs[orbit]; +} + function effectSend(input, effect, wet) { const send = gainNode(wet); input.connect(send); @@ -260,6 +276,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => { cut, loop, orbit = 1, + room, + size = 2, + roomsize = size, } = hap.value; const { velocity = 1 } = hap.context; gain *= velocity; // legacy fix for velocity @@ -386,12 +405,18 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => { const delyNode = getDelay(orbit, delaytime, delayfeedback, t); delaySend = effectSend(post, delyNode, delay); } + // reverb + let reverbSend; + if (room > 0 && roomsize > 0) { + const reverbNode = getReverb(orbit, roomsize); + reverbSend = effectSend(post, reverbNode, room); + } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); // disconnect all nodes when source node has ended: - chain[0].onended = () => chain.concat([delaySend]).forEach((n) => n?.disconnect()); + chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect()); } catch (e) { console.warn('.out error:', e); } diff --git a/repl/src/test/__snapshots__/tunes.test.mjs.snap b/repl/src/test/__snapshots__/tunes.test.mjs.snap index dba92c73..9092749e 100644 --- a/repl/src/test/__snapshots__/tunes.test.mjs.snap +++ b/repl/src/test/__snapshots__/tunes.test.mjs.snap @@ -1743,6 +1743,19 @@ exports[`renders tunes > tune: caverave 1`] = ` ] `; +exports[`renders tunes > tune: chop 1`] = ` +[ + "0/1 -> 1/4: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0,\\"end\\":0.0078125,\\"pan\\":0,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "1/4 -> 1/2: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0.0078125,\\"end\\":0.015625,\\"pan\\":0,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "1/2 -> 3/4: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0.015625,\\"end\\":0.0234375,\\"pan\\":0,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "3/4 -> 1/1: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0.0234375,\\"end\\":0.03125,\\"pan\\":0,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "3/4 -> 1/1: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0,\\"end\\":0.0078125,\\"pan\\":1,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "1/2 -> 3/4: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0.0078125,\\"end\\":0.015625,\\"pan\\":1,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "1/4 -> 1/2: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0.015625,\\"end\\":0.0234375,\\"pan\\":1,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", + "0/1 -> 1/4: {\\"s\\":\\"p\\",\\"speed\\":0.03125,\\"unit\\":\\"c\\",\\"begin\\":0.0234375,\\"end\\":0.03125,\\"pan\\":1,\\"shape\\":0.4,\\"decay\\":0.1,\\"sustain\\":0.6}", +] +`; + exports[`renders tunes > tune: customTrigger 1`] = ` [ "0/1 -> 1/8: {\\"freq\\":55.33,\\"s\\":\\"sawtooth\\"}", @@ -1771,6 +1784,13 @@ exports[`renders tunes > tune: customTrigger 1`] = ` ] `; +exports[`renders tunes > tune: delay 1`] = ` +[ + "0/1 -> 1/2: {\\"s\\":\\"bd\\",\\"delay\\":0,\\"delaytime\\":0.16,\\"delayfeedback\\":0.8,\\"speed\\":-1}", + "1/2 -> 1/1: {\\"s\\":\\"sd\\",\\"delay\\":0,\\"delaytime\\":0.16,\\"delayfeedback\\":0.8,\\"speed\\":-1}", +] +`; + exports[`renders tunes > tune: echoPiano 1`] = ` [ "0/1 -> 1/2: {\\"note\\":\\"D3\\",\\"clip\\":1,\\"s\\":\\"piano\\",\\"release\\":0.1,\\"pan\\":0.4814814814814815}", @@ -13380,6 +13400,15 @@ exports[`renders tunes > tune: meltingsubmarine 1`] = ` ] `; +exports[`renders tunes > tune: orbit 1`] = ` +[ + "0/1 -> 1/2: {\\"s\\":\\"bd\\",\\"delay\\":0.5,\\"delaytime\\":0.33,\\"delayfeedback\\":0.6,\\"speed\\":-1}", + "1/2 -> 1/1: {\\"s\\":\\"sd\\",\\"delay\\":0.5,\\"delaytime\\":0.33,\\"delayfeedback\\":0.6,\\"speed\\":-1}", + "0/1 -> 1/2: {\\"s\\":\\"hh\\",\\"delay\\":0.8,\\"delaytime\\":0.08,\\"delayfeedback\\":0.7,\\"orbit\\":2,\\"speed\\":-1}", + "1/2 -> 1/1: {\\"s\\":\\"hh\\",\\"delay\\":0.8,\\"delaytime\\":0.08,\\"delayfeedback\\":0.7,\\"orbit\\":2,\\"speed\\":-1}", +] +`; + exports[`renders tunes > tune: outroMusic 1`] = ` [ "0/1 -> 3/1: {\\"n\\":\\"B3\\",\\"s\\":\\"0040_FluidR3_GM_sf2_file\\",\\"attack\\":0.05,\\"decay\\":0.1,\\"sustain\\":0.7,\\"cutoff\\":1111.7252990603447,\\"gain\\":0.3}", diff --git a/repl/src/tunes.mjs b/repl/src/tunes.mjs index 5d7dda2c..639fc042 100644 --- a/repl/src/tunes.mjs +++ b/repl/src/tunes.mjs @@ -897,3 +897,33 @@ stack( x? ~ ~ x ~ x@3\`), roots.struct("x [~ x?0.2] x [~ x?] | x!4 | x@2 ~ ~ ~ x x x").transpose("0 7") ).slow(2).pianoroll().note().piano().out();`; + +export const chop = `samples({ p: 'https://cdn.freesound.org/previews/648/648433_11943129-lq.mp3' }) + +s("p") + .loopAt(32,1) + .chop(128) + .jux(rev) + .shape(.4) + .decay(.1) + .sustain(.6) + .out()`; + +export const delay = `stack( + s("bd ") + .delay("<0 .5>") + .delaytime(".16 | .33") + .delayfeedback(".6 | .8") + ).sometimes(x=>x.speed("-1")).out()`; + +export const orbit = `stack( + s("bd ") + .delay(.5) + .delaytime(.33) + .delayfeedback(.6), + s("hh*2") + .delay(.8) + .delaytime(.08) + .delayfeedback(.7) + .orbit(2) + ).sometimes(x=>x.speed("-1")).out()`;