diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e23114ca..8841b4bc 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -86,6 +86,16 @@ const generic_params = [ * */ ['gain'], + /** + * Gain applied after all effects have been processed. + * + * @name postgain + * @example + * s("bd sd,hh*4") + * .compressor("-20:20:10:.002:.02").postgain(1.5) + * + */ + ['postgain'], /** * Like {@link gain}, but linear. * @@ -1060,6 +1070,21 @@ const generic_params = [ * */ ['shape'], + /** + * Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")` + * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) + * + * @name compressor + * @example + * s("bd sd,hh*4") + * .compressor("-20:20:10:.002:.02") + * + */ + [['compressor', 'compressorRatio', 'compressorKnee', 'compressorAttack', 'compressorRelease']], + ['compressorKnee'], + ['compressorRatio'], + ['compressorAttack'], + ['compressorRelease'], /** * Changes the speed of sample playback, i.e. a cheap way of changing pitch. * diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 576ec3f1..d87ea94d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -78,6 +78,17 @@ export const getParamADSR = (param, attack, decay, sustain, release, min, max, b param.linearRampToValueAtTime(min, end + Math.max(release, 0.1)); }; +export function getCompressor(ac, threshold, ratio, knee, attack, release) { + const options = { + threshold: threshold ?? -3, + ratio: ratio ?? 10, + knee: knee ?? 10, + attack: attack ?? 0.005, + release: release ?? 0.05, + }; + return new DynamicsCompressorNode(ac, options); +} + export function createFilter( context, type, diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index f853d3fb..0f638ca8 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -1,8 +1,6 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.generateReverb = reverbGen.generateReverb; - AudioContext.prototype.adjustLength = function (duration, buffer) { const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); @@ -19,14 +17,18 @@ if (typeof AudioContext !== 'undefined') { AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) { const convolver = this.createConvolver(); - convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, buf) => { - if (buf) { - convolver.buffer = this.adjustLength(d, buf); + convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => { + convolver.duration = d; + convolver.fade = fade; + convolver.lp = lp; + convolver.dim = dim; + convolver.ir = ir; + if (ir) { + convolver.buffer = this.adjustLength(d, ir); } else { - this.generateReverb( + reverbGen.generateReverb( { audioContext: this, - sampleRate: 44100, numChannels: 2, decayTime: d, fadeInTime: fade, @@ -37,20 +39,8 @@ if (typeof AudioContext !== 'undefined') { convolver.buffer = buffer; }, ); - convolver.duration = d; - convolver.fade = fade; - convolver.lp = lp; - convolver.dim = dim; } }; - convolver.setIR = (d, fade, lp, dim, buf) => { - if (buf) { - convolver.buffer = this.adjustLength(d, buf); - } else { - convolver.generate(d, fade, lp, dim, buf); - } - return convolver; - }; convolver.generate(duration, fade, lp, dim, ir); return convolver; }; diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs index 4fd76570..d2533cae 100644 --- a/packages/superdough/reverbGen.mjs +++ b/packages/superdough/reverbGen.mjs @@ -23,7 +23,7 @@ var reverbGen = {}; immediately within the current execution context, or later. */ reverbGen.generateReverb = function (params, callback) { var audioContext = params.audioContext || new AudioContext(); - var sampleRate = params.sampleRate || 44100; + var sampleRate = audioContext.sampleRate; var numChannels = params.numChannels || 2; // params.decayTime is the -60dB fade time. We let it go 50% longer to get to -90dB. var totalTime = params.decayTime * 1.5; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 76b6a542..a7e9c3a3 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -64,6 +64,7 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol export const loadBuffer = (url, ac, s, n = 0) => { const label = s ? `sound "${s}:${n}"` : 'sample'; + url = url.replace('#', '%23'); if (!loadCache[url]) { logger(`[sampler] load ${label}..`, 'load-sample', { url }); const timestamp = Date.now(); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index d2507672..1d615548 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { clamp } from './util.mjs'; import workletsUrl from './worklets.mjs?url'; -import { createFilter, gainNode } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -124,26 +124,20 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { reverb.connect(getDestination()); reverbs[orbit] = reverb; } - if ( hasChanged(duration, reverbs[orbit].duration) || hasChanged(fade, reverbs[orbit].fade) || hasChanged(lp, reverbs[orbit].lp) || - hasChanged(dim, reverbs[orbit].dim) + hasChanged(dim, reverbs[orbit].dim) || + reverbs[orbit].ir !== ir ) { // only regenerate when something has changed // avoids endless regeneration on things like // stack(s("a"), s("b").rsize(8)).room(.5) // this only works when args may stay undefined until here // setting default values breaks this - reverbs[orbit].generate(duration, fade, lp, dim); + reverbs[orbit].generate(duration, fade, lp, dim, ir); } - - if (reverbs[orbit].ir !== ir) { - reverbs[orbit] = reverbs[orbit].setIR(duration, fade, lp, dim, ir); - reverbs[orbit].ir = ir; - } - return reverbs[orbit]; } @@ -204,6 +198,7 @@ export const superdough = async (value, deadline, hapDuration) => { bank, source, gain = 0.8, + postgain = 1, // filters ftype = '12db', fanchor = 0.5, @@ -251,6 +246,11 @@ export const superdough = async (value, deadline, hapDuration) => { velocity = 1, analyze, // analyser wet fft = 8, // fftSize 0 - 10 + compressor: compressorThreshold, + compressorRatio, + compressorKnee, + compressorAttack, + compressorRelease, } = value; gain *= velocity; // legacy fix for velocity let toDisconnect = []; // audio nodes that will be disconnected when the source has ended @@ -366,6 +366,11 @@ export const superdough = async (value, deadline, hapDuration) => { crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); + compressorThreshold !== undefined && + chain.push( + getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease), + ); + // panning if (pan !== undefined) { const panner = ac.createStereoPanner(); @@ -374,7 +379,7 @@ export const superdough = async (value, deadline, hapDuration) => { } // last gain - const post = gainNode(1); + const post = gainNode(postgain); chain.push(post); post.connect(getDestination()); @@ -385,20 +390,20 @@ export const superdough = async (value, deadline, hapDuration) => { delaySend = effectSend(post, delyNode, delay); } // reverb - let buffer; - let url; - if (ir !== undefined) { - let sample = getSound(ir); - if (Array.isArray(sample)) { - url = sample.data.samples[i % sample.data.samples.length]; - } else if (typeof sample === 'object') { - url = Object.values(sample.data.samples)[i & Object.values(sample.data.samples).length]; - } - buffer = await loadBuffer(url, ac, ir, 0); - } let reverbSend; - if (room > 0 && roomsize > 0) { - const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, buffer); + if (room > 0) { + let roomIR; + if (ir !== undefined) { + let url; + let sample = getSound(ir); + if (Array.isArray(sample)) { + url = sample.data.samples[i % sample.data.samples.length]; + } else if (typeof sample === 'object') { + url = Object.values(sample.data.samples)[i & Object.values(sample.data.samples).length]; + } + roomIR = await loadBuffer(url, ac, ir, 0); + } + const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); reverbSend = effectSend(post, reverbNode, room); } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 18370903..f351a5a0 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1185,6 +1185,35 @@ exports[`runs examples > example "compress" example index 0 1`] = ` ] `; +exports[`runs examples > example "compressor" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 0/1 → 1/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 1/4 → 1/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 1/2 → 3/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 1/2 → 1/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 3/4 → 1/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 1/1 → 5/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 1/1 → 3/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 5/4 → 3/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 3/2 → 7/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 3/2 → 2/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 7/4 → 2/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 2/1 → 9/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 2/1 → 5/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 9/4 → 5/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 5/2 → 11/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 5/2 → 3/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 11/4 → 3/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 3/1 → 13/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 3/1 → 7/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 13/4 → 7/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 7/2 → 15/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 7/2 → 4/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", + "[ 15/4 → 4/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]", +] +`; + exports[`runs examples > example "cosine" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:Eb4 ]", @@ -3293,6 +3322,35 @@ exports[`runs examples > example "polymeterSteps" example index 0 1`] = ` ] `; +exports[`runs examples > example "postgain" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 0/1 → 1/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 1/4 → 1/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 1/2 → 3/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 1/2 → 1/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 3/4 → 1/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 1/1 → 5/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 1/1 → 3/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 5/4 → 3/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 3/2 → 7/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 3/2 → 2/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 7/4 → 2/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 2/1 → 9/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 2/1 → 5/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 9/4 → 5/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 5/2 → 11/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 5/2 → 3/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 11/4 → 3/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 3/1 → 13/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 3/1 → 7/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 13/4 → 7/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 7/2 → 15/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 7/2 → 4/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", + "[ 15/4 → 4/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", +] +`; + exports[`runs examples > example "press" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:hh ]", diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 35052788..fb506aad 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -144,6 +144,14 @@ There is one filter envelope for each filter type and thus one set of envelope f +## compressor + + + +## postgain + + + # Panning ## jux