sound: update YouTube audio compressor
This commit is contained in:
parent
4be6251641
commit
869c3e9433
3 changed files with 355 additions and 92 deletions
25
sound/YouTube audio compressor.md
Normal file
25
sound/YouTube audio compressor.md
Normal 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
175
sound/limiter.js
Normal 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);
|
Loading…
Reference in a new issue