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