sound: update YouTube audio compressor

This commit is contained in:
Connor Olding 2024-02-15 17:20:15 -08:00
parent 4be6251641
commit 869c3e9433
3 changed files with 355 additions and 92 deletions

View file

@ -0,0 +1,25 @@
assuming you already have a browser addon such as Greasemonkey, Tampermonkey, or Violentmonkey installed, simply
## [click here to install](https://gist.github.com/notwa/9b8466b0c2ca48d756afcd02a5e43739/raw/YouTube%2520audio%2520compressor.user.js)
![what it looks like when installed](https://user-images.githubusercontent.com/1459466/281552372-282b2d47-ebe9-4a64-996c-60d8fc3905f4.png)
this userscript is a heavily customized fork of [Vivelin's original userscript](https://gist.github.com/Vivelin/2321d17bf26016ceaed87d6d1a281881)
and integrates [a limiter written by Wareya](https://github.com/wareya/LimiterTest) that i've ported to JavaScript.
## usage
click on the gear symbol near the bottom right of YouTube's video player to access these features:
### Level Boost
this adds +10 decibels of gain to the audio, as well as a brickwall limiter to reduce unwanted distortion (clipping).
this works in conjunction with YouTube's volume slider, so if +10 dB is a little too much, simply turn the volume down.
### Compressor
in terms of volume, this squeezes the quiet and loud parts closer together to allow for a more uniform listening experience.
this compressor has been tuned to be less responsive to bass and treble frequencies, which helps to reduce "pumping" artifacts.
note that the level boost feeds into the compressor, so the amount of compression somewhat increases with both features enabled.

File diff suppressed because one or more lines are too long

175
sound/limiter.js Normal file
View file

@ -0,0 +1,175 @@
// 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);