diff --git a/.eslintignore b/.eslintignore
index 13e635f3f..58d3643dc 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -18,4 +18,5 @@ vite.config.js
**/*.json
**/dev-dist
**/dist
-/src-tauri/target/**/*
\ No newline at end of file
+/src-tauri/target/**/*
+reverbGen.mjs
\ No newline at end of file
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 39c2df2f8..88c2c0725 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -979,20 +979,64 @@ const generic_params = [
*
*/
[['room', 'size']],
+ /**
+ * Reverb lowpass starting frequency (in hertz).
+ * When this property is changed, the reverb will be recaculated, so only change this sparsely..
+ *
+ * @name roomlp
+ * @synonyms rlp
+ * @param {number} frequency between 0 and 20000hz
+ * @example
+ * s("bd sd").room(0.5).rlp(10000)
+ * @example
+ * s("bd sd").room(0.5).rlp(5000)
+ */
+ ['roomlp', 'rlp'],
+ /**
+ * Reverb lowpass frequency at -60dB (in hertz).
+ * When this property is changed, the reverb will be recaculated, so only change this sparsely..
+ *
+ * @name roomdim
+ * @synonyms rdim
+ * @param {number} frequency between 0 and 20000hz
+ * @example
+ * s("bd sd").room(0.5).rlp(10000).rdim(8000)
+ * @example
+ * s("bd sd").room(0.5).rlp(5000).rdim(400)
+ *
+ */
+ ['roomdim', 'rdim'],
+ /**
+ * Reverb fade time (in seconds).
+ * When this property is changed, the reverb will be recaculated, so only change this sparsely..
+ *
+ * @name roomfade
+ * @synonyms rfade
+ * @param {number} seconds for the reverb to fade
+ * @example
+ * s("bd sd").room(0.5).rlp(10000).rfade(0.5)
+ * @example
+ * s("bd sd").room(0.5).rlp(5000).rfade(4)
+ *
+ */
+ ['roomfade', 'rfade'],
/**
* Sets the room size of the reverb, see {@link room}.
+ * When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
* @param {number | Pattern} size between 0 and 10
- * @synonyms size, sz
+ * @synonyms rsize, sz, size
+ * @example
+ * s("bd sd").room(.8).rsize(1)
* @example
- * s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
+ * s("bd sd").room(.8).rsize(4)
*
*/
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
- ['size', 'sz', 'roomsize'],
+ ['roomsize', 'size', 'sz', 'rsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs
index e6d31f6aa..de6c903e0 100644
--- a/packages/superdough/reverb.mjs
+++ b/packages/superdough/reverb.mjs
@@ -1,23 +1,30 @@
-if (typeof AudioContext !== 'undefined') {
- AudioContext.prototype.impulseResponse = function (duration, channels = 1) {
- const length = this.sampleRate * duration;
- const impulse = this.createBuffer(channels, 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;
- };
+import reverbGen from './reverbGen.mjs';
- AudioContext.prototype.createReverb = function (duration) {
+if (typeof AudioContext !== 'undefined') {
+ AudioContext.prototype.generateReverb = reverbGen.generateReverb;
+ AudioContext.prototype.createReverb = function (duration, fade, lp, dim) {
const convolver = this.createConvolver();
- convolver.setDuration = (d) => {
- convolver.buffer = this.impulseResponse(d);
- convolver.duration = duration;
- return convolver;
+ convolver.generate = (d, fade, lp, dim) => {
+ this.generateReverb(
+ {
+ audioContext: this,
+ sampleRate: 44100,
+ numChannels: 2,
+ decayTime: d,
+ fadeInTime: fade,
+ lpFreqStart: lp,
+ lpFreqEnd: dim,
+ },
+ (buffer) => {
+ convolver.buffer = buffer;
+ },
+ );
+ convolver.duration = d;
+ convolver.fade = fade;
+ convolver.lp = lp;
+ convolver.dim = dim;
};
- convolver.setDuration(duration);
+ convolver.generate(duration, fade, lp, dim);
return convolver;
};
}
-
-// TODO: make the reverb more exciting
-// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html
diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs
new file mode 100644
index 000000000..494299372
--- /dev/null
+++ b/packages/superdough/reverbGen.mjs
@@ -0,0 +1,129 @@
+// Copyright 2014 Alan deLespinasse
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var reverbGen = {};
+
+/** Generates a reverb impulse response.
+
+ @param {!Object} params TODO: Document the properties.
+ @param {!function(!AudioBuffer)} callback Function to call when
+ the impulse response has been generated. The impulse response
+ is passed to this function as its parameter. May be called
+ 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 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;
+ var decaySampleFrames = Math.round(params.decayTime * sampleRate);
+ var numSampleFrames = Math.round(totalTime * sampleRate);
+ var fadeInSampleFrames = Math.round((params.fadeInTime || 0) * sampleRate);
+ // 60dB is a factor of 1 million in power, or 1000 in amplitude.
+ var decayBase = Math.pow(1 / 1000, 1 / decaySampleFrames);
+ var reverbIR = audioContext.createBuffer(numChannels, numSampleFrames, sampleRate);
+ for (var i = 0; i < numChannels; i++) {
+ var chan = reverbIR.getChannelData(i);
+ for (var j = 0; j < numSampleFrames; j++) {
+ chan[j] = randomSample() * Math.pow(decayBase, j);
+ }
+ for (var j = 0; j < fadeInSampleFrames; j++) {
+ chan[j] *= j / fadeInSampleFrames;
+ }
+ }
+
+ applyGradualLowpass(reverbIR, params.lpFreqStart || 0, params.lpFreqEnd || 0, params.decayTime, callback);
+};
+
+/** Creates a canvas element showing a graph of the given data.
+
+ @param {!Float32Array} data An array of numbers, or a Float32Array.
+ @param {number} width Width in pixels of the canvas.
+ @param {number} height Height in pixels of the canvas.
+ @param {number} min Minimum value of data for the graph (lower edge).
+ @param {number} max Maximum value of data in the graph (upper edge).
+ @return {!CanvasElement} The generated canvas element. */
+reverbGen.generateGraph = function (data, width, height, min, max) {
+ var canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ var gc = canvas.getContext('2d');
+ gc.fillStyle = '#000';
+ gc.fillRect(0, 0, canvas.width, canvas.height);
+ gc.fillStyle = '#fff';
+ var xscale = width / data.length;
+ var yscale = height / (max - min);
+ for (var i = 0; i < data.length; i++) {
+ gc.fillRect(i * xscale, height - (data[i] - min) * yscale, 1, 1);
+ }
+ return canvas;
+};
+
+/** Applies a constantly changing lowpass filter to the given sound.
+
+ @private
+ @param {!AudioBuffer} input
+ @param {number} lpFreqStart
+ @param {number} lpFreqEnd
+ @param {number} lpFreqEndAt
+ @param {!function(!AudioBuffer)} callback May be called
+ immediately within the current execution context, or later.*/
+var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) {
+ if (lpFreqStart == 0) {
+ callback(input);
+ return;
+ }
+ var channelData = getAllChannelData(input);
+ var context = new OfflineAudioContext(input.numberOfChannels, channelData[0].length, input.sampleRate);
+ var player = context.createBufferSource();
+ player.buffer = input;
+ var filter = context.createBiquadFilter();
+
+ lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2);
+ lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2);
+
+ filter.type = 'lowpass';
+ filter.Q.value = 0.0001;
+ filter.frequency.setValueAtTime(lpFreqStart, 0);
+ filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt);
+
+ player.connect(filter);
+ filter.connect(context.destination);
+ player.start();
+ context.oncomplete = function (event) {
+ callback(event.renderedBuffer);
+ };
+ context.startRendering();
+
+ window.filterNode = filter;
+};
+
+/** @private
+ @param {!AudioBuffer} buffer
+ @return {!Array.} An array containing the Float32Array of each channel's samples. */
+var getAllChannelData = function (buffer) {
+ var channels = [];
+ for (var i = 0; i < buffer.numberOfChannels; i++) {
+ channels[i] = buffer.getChannelData(i);
+ }
+ return channels;
+};
+
+/** @private
+ @return {number} A random number from -1 to 1. */
+var randomSample = function () {
+ return Math.random() * 2 - 1;
+};
+
+export default reverbGen;
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index 289e8d97a..ea8afc58c 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -107,17 +107,25 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
}
let reverbs = {};
-function getReverb(orbit, duration = 2) {
+
+function getReverb(orbit, duration = 2, fade, lp, dim) {
+ // If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) {
const ac = getAudioContext();
- const reverb = ac.createReverb(duration);
+ const reverb = ac.createReverb(duration, fade, lp, dim);
reverb.connect(getDestination());
reverbs[orbit] = reverb;
}
- if (reverbs[orbit].duration !== duration) {
- reverbs[orbit] = reverbs[orbit].setDuration(duration);
- reverbs[orbit].duration = duration;
+
+ if (
+ reverbs[orbit].duration !== duration ||
+ reverbs[orbit].fade !== fade ||
+ reverbs[orbit].lp !== lp ||
+ reverbs[orbit].dim !== dim
+ ) {
+ reverbs[orbit].generate(duration, fade, lp, dim);
}
+
return reverbs[orbit];
}
@@ -215,7 +223,10 @@ export const superdough = async (value, deadline, hapDuration) => {
delaytime = 0.25,
orbit = 1,
room,
- size = 2,
+ roomfade = 0.1,
+ roomlp = 15000,
+ roomdim = 1000,
+ roomsize = 2,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
@@ -353,8 +364,8 @@ export const superdough = async (value, deadline, hapDuration) => {
}
// reverb
let reverbSend;
- if (room > 0 && size > 0) {
- const reverbNode = getReverb(orbit, size);
+ if (room > 0 && roomsize > 0) {
+ const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim);
reverbSend = effectSend(post, reverbNode, room);
}
diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap
index 4a4c966de..f2bc70395 100644
--- a/test/__snapshots__/examples.test.mjs.snap
+++ b/test/__snapshots__/examples.test.mjs.snap
@@ -3663,16 +3663,107 @@ exports[`runs examples > example "room" example index 1 1`] = `
]
`;
+exports[`runs examples > example "roomdim" example index 0 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
+]
+`;
+
+exports[`runs examples > example "roomdim" example index 1 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
+]
+`;
+
+exports[`runs examples > example "roomfade" example index 0 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
+]
+`;
+
+exports[`runs examples > example "roomfade" example index 1 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
+]
+`;
+
+exports[`runs examples > example "roomlp" example index 0 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 ]",
+]
+`;
+
+exports[`runs examples > example "roomlp" example index 1 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 ]",
+]
+`;
+
exports[`runs examples > example "roomsize" example index 0 1`] = `
[
- "[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
- "[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
- "[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
- "[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
- "[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
- "[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
- "[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
- "[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
+ "[ 0/1 → 1/2 | s:bd room:0.8 roomsize:1 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.8 roomsize:1 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.8 roomsize:1 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.8 roomsize:1 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.8 roomsize:1 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.8 roomsize:1 ]",
+]
+`;
+
+exports[`runs examples > example "roomsize" example index 1 1`] = `
+[
+ "[ 0/1 → 1/2 | s:bd room:0.8 roomsize:4 ]",
+ "[ 1/2 → 1/1 | s:sd room:0.8 roomsize:4 ]",
+ "[ 1/1 → 3/2 | s:bd room:0.8 roomsize:4 ]",
+ "[ 3/2 → 2/1 | s:sd room:0.8 roomsize:4 ]",
+ "[ 2/1 → 5/2 | s:bd room:0.8 roomsize:4 ]",
+ "[ 5/2 → 3/1 | s:sd room:0.8 roomsize:4 ]",
+ "[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
+ "[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
]
`;
diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx
index f77ab4c47..2ee6c44a2 100644
--- a/website/src/pages/learn/effects.mdx
+++ b/website/src/pages/learn/effects.mdx
@@ -183,24 +183,40 @@ global effects use the same chain for all events of the same orbit:
-## delay
+## Delay
+
+### delay
-## delaytime
+### delaytime
-## delayfeedback
+### delayfeedback
-## room
+## Reverb
+
+### room
-## roomsize
+### roomsize
+### roomfade
+
+
+
+### roomlp
+
+
+
+### roomdim
+
+
+
Next, we'll look at strudel's support for [Csound](/learn/csound).