Skip to content

Commit

Permalink
Add compressor to HooverSynth
Browse files Browse the repository at this point in the history
  • Loading branch information
ryukau committed May 19, 2024
1 parent 6446c43 commit 366f682
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 33 deletions.
7 changes: 6 additions & 1 deletion CollidingComb/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,18 @@ onmessage = async (event) => {
}

// Discard silence of delay at start.
let sound = new Array(Math.floor(upRate * pv.renderDuration)).fill(0);
let counter = 0;
let sig = 0;
do {
sig = process(upRate, pv, dsp);
if (++counter >= sound.length) { // Avoid infinite loop on silent signal.
postMessage({sound: sound, status: "Output is completely silent."});
return;
}
} while (sig === 0);

// Process.
let sound = new Array(Math.floor(upRate * pv.renderDuration)).fill(0);
sound[0] = sig;
for (let i = 1; i < sound.length; ++i) sound[i] = process(upRate, pv, dsp);
sound = downSampleIIR(sound, upFold);
Expand Down
26 changes: 22 additions & 4 deletions HooverSynth/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as wave from "../common/wave.js";

import * as menuitems from "./menuitems.js";

const version = 0;
const version = 1;

const localRecipeBook = {
"Default": {
Expand All @@ -20,12 +20,18 @@ const localRecipeBook = {
stereoMerge: () => {},
overSample: () => {},
sampleRateScaler: () => {},
dcHighpassHz: () => {},
toneSlope: () => {},
dcHighpassHz: () => {},

negativeEnvelope: () => {},

noteNumber: () => {},
mainPwmAmount: () => {},

chorusDelayInterpType: () => {},
chorusAM: () => {},

compressorEnable: () => {},
limiterEnable: () => {},
},
};
Expand Down Expand Up @@ -65,11 +71,13 @@ const scales = {
ratio: new parameter.LinearScale(0, 1),
octave: new parameter.IntScale(-1, 3),

chorusDelayInterpType: new parameter.MenuItemScale(menuitems.delayInterpTypeItems),
chorusAM: new parameter.DecibelScale(-30, 0, true),
chorusTimeSeconds:
new parameter.DecibelScale(util.ampToDB(1e-4), util.ampToDB(0.2), true),
chorusDelayCount: new parameter.IntScale(1, 8),

compressorInputGain: new parameter.DecibelScale(-40, 40, false),
limiterThreshold: new parameter.DecibelScale(-20, 20, false),
};

Expand Down Expand Up @@ -100,13 +108,16 @@ const param = {
subOctave: new parameter.Parameter(0, scales.octave, true),
pwmSawOctave: new parameter.Parameter(1, scales.octave, true),

chorusDelayInterpType: new parameter.Parameter(2, scales.chorusDelayInterpType),
chorusMix: new parameter.Parameter(1, scales.ratio, true),
chorusAM: new parameter.Parameter(0, scales.chorusAM, true),
chorusTimeBaseSeconds: new parameter.Parameter(0.01, scales.chorusTimeSeconds, true),
chorusTimeModSeconds: new parameter.Parameter(0.01, scales.chorusTimeSeconds, true),
chorusDelayCount: new parameter.Parameter(1, scales.chorusDelayCount, true),
chorusLfoSpread: new parameter.Parameter(1, scales.ratio, true),

compressorEnable: new parameter.Parameter(1, scales.boolean, true),
compressorInputGain: new parameter.Parameter(1, scales.compressorInputGain, false),
limiterEnable: new parameter.Parameter(0, scales.boolean, true),
limiterThreshold: new parameter.Parameter(1, scales.limiterThreshold, false),
};
Expand Down Expand Up @@ -195,10 +206,15 @@ const ui = {
dcHighpassHz:
new widget.NumberInput(detailRender, "DC Highpass [Hz]", param.dcHighpassHz, render),

compressorEnable: new widget.ToggleButtonLine(
detailLimiter, ["Compressor - Off", "Compressor - On"], param.compressorEnable,
render),
compressorInputGain: new widget.NumberInput(
detailLimiter, "Compressor Input Gain [dB]", param.compressorInputGain, render),
limiterEnable: new widget.ToggleButtonLine(
detailLimiter, ["Off", "On"], param.limiterEnable, render),
detailLimiter, ["Limiter - Off", "Limiter - On"], param.limiterEnable, render),
limiterThreshold: new widget.NumberInput(
detailLimiter, "Threshold [dB]", param.limiterThreshold, render),
detailLimiter, "Limiter Threshold [dB]", param.limiterThreshold, render),

negativeEnvelope: new widget.ToggleButtonLine(
detailEnvelope, ["Positive", "Negative"], param.negativeEnvelope, render),
Expand Down Expand Up @@ -230,6 +246,8 @@ const ui = {
pwmSawOctave: new widget.NumberInput(
detailOscillator, "PWM Saw Pitch [oct]", param.pwmSawOctave, render),

chorusDelayInterpType: new widget.ComboBoxLine(
detailChorus, "Delay Interpolation", param.chorusDelayInterpType, render),
chorusMix: new widget.NumberInput(detailChorus, "Mix", param.chorusMix, render),
chorusAM: new widget.NumberInput(detailChorus, "AM", param.chorusAM, render),
chorusTimeBaseSeconds: new widget.NumberInput(
Expand Down
5 changes: 5 additions & 0 deletions HooverSynth/menuitems.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ import {oversampleLinearPhaseItems} from "../common/dsp/multirate.js";

export const oversampleItems = oversampleLinearPhaseItems;
export const sampleRateScalerItems = ["1", "2", "4", "8", "16"];
export const delayInterpTypeItems = [
"None - Noisy Modulation",
"Linear",
"Cubic - Slightly brighter",
];
30 changes: 25 additions & 5 deletions HooverSynth/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import * as delay from "../common/dsp/delay.js";
import {DrumCompressor} from "../common/dsp/drumcompressor.js";
import {Limiter} from "../common/dsp/limiter.js";
import {downSampleLinearPhase} from "../common/dsp/multirate.js";
import {SlopeFilter} from "../common/dsp/slopefilter.js";
Expand Down Expand Up @@ -66,10 +67,10 @@ class TriangleLFO {
}

class Chorus {
constructor(sampleRate, baseTimeSeconds, modTimeSeconds) {
constructor(sampleRate, baseTimeSeconds, modTimeSeconds, delayType = delay.IntDelay) {
this.baseTime = Math.ceil(sampleRate * baseTimeSeconds);
this.modTime = Math.ceil(sampleRate * modTimeSeconds);
this.delay = new delay.IntDelay(this.baseTime + this.modTime);
this.delay = new delayType(this.baseTime + this.modTime);
}

process(input, mod) {
Expand Down Expand Up @@ -142,6 +143,9 @@ function process(upRate, pv, dsp) {

if (pv.toneSlope < 1) sig = dsp.slopeFilter.process(sig);
if (pv.dcHighpassHz > 0) sig = dsp.dcHighpass.hp(sig);
if (pv.compressorEnable === 1) {
sig = dsp.compressor.process(pv.compressorInputGain * sig);
}
if (pv.limiterEnable === 1) sig = dsp.limiter.process(sig);
return sig;
}
Expand Down Expand Up @@ -179,19 +183,35 @@ onmessage = async (event) => {
dsp.pwmLfo = new Array(pv.chorusDelayCount);
dsp.pwmLfoFreqRatio = new Array(pv.chorusDelayCount);
dsp.chorus = new Array(pv.chorusDelayCount);
const delayType = pv.chorusDelayInterpType == 0 ? delay.IntDelay
: pv.chorusDelayInterpType == 1 ? delay.Delay
: delay.CubicDelay;
for (let idx = 0; idx < dsp.chorus.length; ++idx) {
dsp.pwmLfo[idx] = new TriangleLFO();
dsp.pwmLfoFreqRatio[idx] = Math.exp(exp2Scaler * lerp(0, idx, pv.chorusLfoSpread));
dsp.chorus[idx]
= new Chorus(upRate, pv.chorusTimeBaseSeconds, pv.chorusTimeModSeconds);
= new Chorus(upRate, pv.chorusTimeBaseSeconds, pv.chorusTimeModSeconds, delayType);
}

dsp.compressor = new DrumCompressor(upRate, "hoover");

dsp.limiter = new Limiter(
Math.floor(upRate * 0.002), Math.floor(upRate * 0.002), 0, pv.limiterThreshold);
Math.floor(upRate * 0.012), Math.floor(upRate * 0.002), 0, pv.limiterThreshold);

// Discard silence of delay at start.
let counter = 0;
let sig = 0;
do {
sig = process(upRate, pv, dsp);
if (++counter >= sound.length) { // Avoid infinite loop on silent signal.
postMessage({sound: sound, status: "Output is completely silent."});
return;
}
} while (sig === 0);

// Process.
for (let i = 0; i < sound.length; ++i) sound[i] = process(upRate, pv, dsp);
sound = downSampleLinearPhase(sound, upFold);
sound = downSampleLinearPhase(sound, upFold); // This line is adding latency.

// Post effect.
let gainEnv = 1;
Expand Down
48 changes: 47 additions & 1 deletion common/dsp/delay.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Takamitsu Endo ([email protected])
// SPDX-License-Identifier: Apache-2.0

import {clamp} from "../util.js";
import {clamp, lagrange3Interp} from "../util.js";

export class IntDelay {
#wptr;
Expand Down Expand Up @@ -77,6 +77,52 @@ export class Delay {
}
}

export class CubicDelay {
#wptr;
#buf;

constructor(maxDelayTimeInSamples) {
this.#wptr = 0;
this.#buf = new Array(Math.max(Math.ceil(maxDelayTimeInSamples) + 4, 4));
this.reset();
}

reset() { this.#buf.fill(0); }

setTime(timeInSample) {
const clamped = clamp(timeInSample - 1, 0, this.#buf.length - 4);
this.timeInt = Math.floor(clamped);
this.rFraction = clamped - this.timeInt;
}

// Always call `setTime` before `process`.
process(input) {
// Write to buffer.
if (++this.#wptr >= this.#buf.length) this.#wptr = 0;
this.#buf[this.#wptr] = input;

let rptr0 = this.#wptr - this.timeInt;
let rptr1 = rptr0 - 1;
let rptr2 = rptr0 - 2;
let rptr3 = rptr0 - 3;
if (rptr0 < 0) rptr0 += this.#buf.length;
if (rptr1 < 0) rptr1 += this.#buf.length;
if (rptr2 < 0) rptr2 += this.#buf.length;
if (rptr3 < 0) rptr3 += this.#buf.length;

// Read from buffer.
return lagrange3Interp(
this.#buf[rptr0], this.#buf[rptr1], this.#buf[rptr2], this.#buf[rptr3],
this.rFraction);
}

// Convenient method for audio-rate modulation.
processMod(input, timeInSample) {
this.setTime(timeInSample);
return this.process(input);
}
}

export class MultiTapDelay {
#wptr;
#buf;
Expand Down
60 changes: 38 additions & 22 deletions common/dsp/drumcompressor.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,28 +289,44 @@ class BandSplitter {
}

export class DrumCompressor {
constructor(sampleRate) {
this.compressor = [
new ExpCompressor(2 / 4, sampleRate * 1.0, sampleRate * 1.0, "innerTanh"),
new ExpCompressor(2 / 4, sampleRate * 0.1, sampleRate * 0.1, "innerTanh"),
new ExpCompressor(1 / 4, sampleRate * 0.2, sampleRate * 0.2, "outerTanh"),
];

this.saturator = [
// (x) => softclipInnerAlgebraicAbs(x, 0.05),
// (x) => softclipInnerAlgebraicAbs(x, 2.0),
// (x) => softclipOuterAlgebraicAbs(x, 0.5),

(x) => x, // bypass
(x) => x, // bypass
(x) => x, // bypass
];

this.inputGain = [2, 1, 1];
this.outputGain = [1, 1, 1];

this.splitter = new BandSplitter([200 / sampleRate, 3200 / sampleRate], [2, 1]);
this.corrector = new BandSplitter([200 / sampleRate, 3200 / sampleRate], [2, 1]);
constructor(sampleRate, recipeName) {
if (recipeName == "hoover") {
this.compressor = [
new ExpCompressor(2 / 4, sampleRate * 0.1, sampleRate * 0.1, "outerTanh"),
new ExpCompressor(2 / 4, sampleRate * 0.4, sampleRate * 0.4, "innerTanh"),
new ExpCompressor(4 / 4, sampleRate * 1.0, sampleRate * 1.0, "innerTanh"),
];

this.saturator = [
(x) => softclipOuterAlgebraicAbs(x, 0.5),
(x) => softclipInnerAlgebraicAbs(x, 0.5),
(x) => softclipInnerAlgebraicAbs(x, 1.0),
];

this.inputGain = [1.2, 1, 2];
this.outputGain = [1, 1, 1.5];

this.splitter = new BandSplitter([3200 / sampleRate, 200 / sampleRate], [1, 2]);
this.corrector = new BandSplitter([3200 / sampleRate, 200 / sampleRate], [1, 2]);
} else {
this.compressor = [
new ExpCompressor(2 / 4, sampleRate * 1.0, sampleRate * 1.0, "innerTanh"),
new ExpCompressor(2 / 4, sampleRate * 0.1, sampleRate * 0.1, "innerTanh"),
new ExpCompressor(1 / 4, sampleRate * 0.2, sampleRate * 0.2, "outerTanh"),
];

this.saturator = [
(x) => x, // bypass
(x) => x, // bypass
(x) => x, // bypass
];

this.inputGain = [2, 1, 1];
this.outputGain = [1, 1, 1];

this.splitter = new BandSplitter([1000 / sampleRate, 200 / sampleRate], [1, 2]);
this.corrector = new BandSplitter([1000 / sampleRate, 200 / sampleRate], [1, 2]);
}
}

process(input) {
Expand Down

0 comments on commit 366f682

Please sign in to comment.