backyard/sound/limiter.js

176 lines
5.3 KiB
JavaScript

// port of https://github.com/wareya/LimiterTest/blob/main/Main.gd
class Limiter extends AudioWorkletProcessor {
constructor(options) {
super();
this.srate = sampleRate;
this.pre_gain = 1.0;
this.post_gain = 1.0;
this.limit = 0.8912509381337456; // -1 decibels
if (false) {
this.attack = 0.0010;
this.sustain = 0.0400;
this.release = 0.0400;
} else { // ultrafast
this.attack = 0.0006;
this.sustain = 0.0060;
this.release = 0.0300;
}
this.release_memory = 0.0;
this.box_blur = 0;
// this.amp = 1.0; // not stateful
this.ref_amp = 1.0;
this.memory_cursor = 0;
this.sustained_amp = null;
this.sample_memory = null;
this.amp_memory_cursor = 0;
this.amp_memory = null;
this.amp_min_buckets = null;
this.amp_bucket_size = 0;
this.peaks = null;
this.delays = [];
const toSamples = (t) => Math.max(Math.round(this.srate * t), 1);
let memory_count = toSamples(this.attack);
this.sample_memory = new Float32Array(memory_count);
this.sustained_amp = new Float32Array(memory_count);
let amp_count = toSamples(this.attack + this.sustain);
this.amp_bucket_size = Math.sqrt(amp_count) | 0;
this.amp_memory = new Float32Array(amp_count);
this.amp_memory.fill(1.0);
let amp_bucket_count = amp_count / this.amp_bucket_size | 0;
this.amp_min_buckets = new Float32Array(amp_bucket_count);
this.amp_min_buckets.fill(1.0);
this.peaks = new Float32Array(128); // guess the buffer size
this.delays = [this.sample_memory];
}
process(inputs, outputs, parameters) {
const memory_count = this.sample_memory.length; // this shows up a lot
const input = inputs[0];
const output = outputs[0];
if (input.length === 0) {
return false;
}
while (this.delays.length < input.length) {
this.delays.push(new Float32Array(memory_count));
}
const blockSize = input[0].length;
if (blockSize > 0 && this.peaks.length !== blockSize) {
this.peaks = new Float32Array(blockSize);
}
this.peaks.fill(0.0);
for (let c = 0; c < input.length; c++) {
const source = input[c];
for (let i = 0; i < blockSize; i++) {
this.peaks[i] = Math.max(this.peaks[i], Math.abs(source[i]));
}
}
const delay_cursor = this.memory_cursor;
for (let i = 0; i < blockSize; i++) {
const sample = this.peaks[i] * this.pre_gain;
// do the release curve
this.release_memory = Math.max(0.0, this.release_memory - 1.0 / this.srate);
let amp = 1.0;
if (this.release > 0.0) {
let t = 1.0 - this.release_memory / this.release;
amp = (1.0 - t) * this.ref_amp + t;
}
// reset sustain if we exceed the limit
let ref_val = Math.abs(amp * sample);
if (ref_val >= this.limit) {
let amount = this.limit / ref_val;
amp *= amount;
ref_val *= amount;
this.ref_amp = amp;
this.release_memory = this.release;
}
this.amp_memory[this.amp_memory_cursor] = amp;
// update affected bucket
let bucket = this.amp_memory_cursor / this.amp_bucket_size | 0;
this.amp_min_buckets[bucket] = 1.0;
let end = Math.min((bucket + 1) * this.amp_bucket_size, this.amp_memory.length);
for (let j = bucket * this.amp_bucket_size; j < end; j++) {
this.amp_min_buckets[bucket] = Math.min(this.amp_min_buckets[bucket], this.amp_memory[j]);
}
// now get the current sustain value
let min_amp = 1.0;
for (let j = 0; j < this.amp_min_buckets.length; j++) {
let past_amp = this.amp_min_buckets[j];
min_amp = Math.min(min_amp, past_amp);
}
this.amp_memory_cursor += 1;
this.amp_memory_cursor %= this.amp_memory.length;
// update the sustain buffer
this.box_blur -= this.sustained_amp[this.memory_cursor] * 32767 | 0;
this.sustained_amp[this.memory_cursor] = min_amp;
this.box_blur += this.sustained_amp[this.memory_cursor] * 32767 | 0;
// update the sample memory buffer
// NOTE(notwa): we instead do this later for each channel.
//let ret_sample = this.sustain + this.attack > 0.0 ? this.sample_memory[this.memory_cursor] : sample;
//this.sample_memory[this.memory_cursor] = sample;
this.memory_cursor += 1;
this.memory_cursor %= memory_count;
this.peaks[i] = this.box_blur / 32767 / memory_count;
}
const final_gain = this.pre_gain * this.post_gain;
for (let c = 0; c < input.length; c++) {
const source = input[c];
const target = output[c];
if (this.sustain + this.attack <= 0.0) {
for (let i = 0; i < blockSize; i++) {
// return the limited sample
target[i] = source[i] * this.peaks[i] * final_gain;
}
} else {
const delay = this.delays[c];
let local_cursor = delay_cursor;
for (let i = 0; i < blockSize; i++) {
const ret_sample = delay[local_cursor];
delay[local_cursor] = source[i];
local_cursor += 1;
local_cursor %= memory_count;
// return the limited sample
target[i] = ret_sample * this.peaks[i] * final_gain;
}
}
}
return false; // docs say to return false for processors with no reverb tails
}
}
registerProcessor("limiter", Limiter);