Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save notwa/9b8466b0c2ca48d756afcd02a5e43739 to your computer and use it in GitHub Desktop.
Save notwa/9b8466b0c2ca48d756afcd02a5e43739 to your computer and use it in GitHub Desktop.
YouTube audio compressor

assuming you already have a browser addon such as Greasemonkey, Tampermonkey, or Violentmonkey installed, simply

what it looks like when installed

this userscript is a heavily customized fork of Vivelin's original userscript and integrates a limiter written by Wareya 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.

// 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);
// ==UserScript==
// @name YouTube audio compressor
// @namespace https://eaguru.guru/
// @version 0.5.2
// @description Adds an audio compressor option to YouTube videos. Now with over-engineering! Based on code by Vivelin and Wareya.
// @author notwa
// @match https://*.youtube.com/*
// @updateURL https://gist.github.com/notwa/9b8466b0c2ca48d756afcd02a5e43739/raw/YouTube%2520audio%2520compressor.user.js
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
"use strict";
// Boolean:
let boosterActive = false;
let compressorActive = false;
let fancyCompression = true;
// AudioContext:
let context = null;
// AudioNode:
let source = null;
let gainIn = null;
let filterIn = null;
let compressor = null;
let filterOut = null;
let gainOut = null;
let limiter = null;
// DOM:
let boosterMenuItem = null;
let compressorMenuItem = null;
// these filters very roughly approximate the inverse of ISO 226:2003 at 70 phons.
// NOTE: these MUST have all zeros (and poles) within the unit circle,
// else the output signal will grow indefinitely (i.e. blow up).
// normally, only the poles of an IIR are subject to this constraint.
// however, as a quick 'n' dirty method to reverse the effect
// of the filters after compression, i am using their inverses,
// wherein the numerators and denominators are swapped.
// TODO: reduce ringing.
//
// zeroth-degree terms are fixed at 1.0.
// 1.0 + num_1 * z^-1 + num_2 * z^-2
// H(z) = gain * -----------------------------------
// 1.0 + den_1 * z^-1 + den_2 * z^-2
// gain num_1 num_2 den_1 den_2
const iir44100 = [+0.4707103, -1.1384015, +0.1384129, -1.5989047, +0.6369472];
const iir48000 = [+0.4560796, -1.1713186, +0.1713303, -1.6291792, +0.6617519];
const iir88200 = [+0.3747475, -1.4029588, +0.4029728, -1.7895837, +0.8000784];
const iir96000 = [+0.3665513, -1.4342713, +0.4342856, -1.8057964, +0.8147329];
const iirs = {
44100: iir44100,
48000: iir48000,
88200: iir88200,
96000: iir96000
}
function createFilter(coeffs, flip, moreGain = 1.0) {
const gain = flip ? moreGain / coeffs[0] : moreGain * coeffs[0];
const b0 = gain;
const b1 = gain * coeffs[flip ? 3 : 1];
const b2 = gain * coeffs[flip ? 4 : 2];
const a0 = 1.0;
const a1 = coeffs[flip ? 1 : 3];
const a2 = coeffs[flip ? 2 : 4];
const num = [b0, b1, b2];
const den = [a0, a1, a2];
const filter = context.createIIRFilter(num, den);
return filter;
}
function createMenuItem(title, defaultState) {
const menuItem = document.createElement("div");
menuItem.className = "ytp-menuitem";
menuItem.setAttribute("role", "menuitemcheckbox");
menuItem.setAttribute("aria-checked", "false");
menuItem.tabIndex = 0;
const icon = document.createElement("div");
icon.className = "ytp-menuitem-icon";
menuItem.appendChild(icon);
const label = document.createElement("div");
label.className = "ytp-menuitem-label";
label.textContent = title;
menuItem.appendChild(label);
const toggleCheckbox = document.createElement("div");
toggleCheckbox.className = "ytp-menuitem-toggle-checkbox";
const content = document.createElement("div");
content.className = "ytp-menuitem-content";
content.appendChild(toggleCheckbox);
menuItem.appendChild(content);
if (defaultState && defaultState !== "false") {
menuItem.setAttribute("aria-checked", "true");
} else {
menuItem.setAttribute("aria-checked", "false");
}
console.log(menuItem);
return menuItem;
}
function ariaToggle(element) {
const oldState = element.getAttribute("aria-checked");
const newState = oldState === "false" ? true : false;
element.setAttribute("aria-checked", newState.toString());
return newState;
}
function reconnect() {
if (source) source.disconnect();
if (gainIn) gainIn.disconnect();
if (filterIn) filterIn.disconnect();
if (compressor) compressor.disconnect();
if (filterOut) filterOut.disconnect();
if (gainOut) gainOut.disconnect();
if (limiter) limiter.disconnect();
if (!source) return false;
let sequence = [source];
if (boosterActive || compressorActive) sequence.push(gainIn);
if (compressorActive) {
if (fancyCompression) sequence.push(filterIn);
sequence.push(compressor);
if (fancyCompression) sequence.push(filterOut);
}
if (boosterActive || compressorActive) sequence.push(gainOut);
if (boosterActive) sequence.push(limiter);
// hook it all up:
sequence = sequence.filter((item) => item); // remove falsy (null) elements
for (let i = 1; i < sequence.length; i++) {
sequence[i - 1].connect(sequence[i]);
}
sequence.at(-1).connect(context.destination);
let compDrive = 1.0;
let limitDrive = 1.0;
if (boosterActive && limiter) {
compDrive *= 3.1622776601683795; // +10 dB
if (compressorActive) {
compDrive *= 0.7943282347242815; // -2 dB
limitDrive *= 1.4125375446227544; // +3 dB
}
} else if (compressorActive) {
compDrive *= 0.7079457843841379; // -3 dB
limitDrive *= 0.7079457843841379; // -3 dB
}
gainIn.gain.setValueAtTime(compDrive, context.currentTime);
gainOut.gain.setValueAtTime(limitDrive, context.currentTime);
return true;
}
function setupCompressor(player, ytpPanelMenu) {
if (!context) {
context = new AudioContext();
// load and register the limiter code:
const uri = "data:text/javascript;base64,Ly8gcG9ydCBvZiBodHRwczovL2dpdGh1Yi5jb20vd2FyZXlhL0xpbWl0ZXJUZXN0L2Jsb2IvbWFpbi9NYWluLmdkCgpjbGFzcyBMaW1pdGVyIGV4dGVuZHMgQXVkaW9Xb3JrbGV0UHJvY2Vzc29yIHsKICBjb25zdHJ1Y3RvcihvcHRpb25zKSB7CiAgICBzdXBlcigpOwoKICAgIHRoaXMuc3JhdGUgPSBzYW1wbGVSYXRlOwoKICAgIHRoaXMucHJlX2dhaW4gPSAxLjA7CiAgICB0aGlzLnBvc3RfZ2FpbiA9IDEuMDsKICAgIHRoaXMubGltaXQgPSAwLjg5MTI1MDkzODEzMzc0NTY7IC8vIC0xIGRlY2liZWxzCgogICAgaWYgKGZhbHNlKSB7CiAgICAgIHRoaXMuYXR0YWNrICA9IDAuMDAxMDsKICAgICAgdGhpcy5zdXN0YWluID0gMC4wNDAwOwogICAgICB0aGlzLnJlbGVhc2UgPSAwLjA0MDA7CiAgICB9IGVsc2UgeyAvLyB1bHRyYWZhc3QKICAgICAgdGhpcy5hdHRhY2sgID0gMC4wMDA2OwogICAgICB0aGlzLnN1c3RhaW4gPSAwLjAwNjA7CiAgICAgIHRoaXMucmVsZWFzZSA9IDAuMDMwMDsKICAgIH0KCiAgICB0aGlzLnJlbGVhc2VfbWVtb3J5ID0gMC4wOwoKICAgIHRoaXMuYm94X2JsdXIgPSAwOwogICAgLy8gdGhpcy5hbXAgPSAxLjA7IC8vIG5vdCBzdGF0ZWZ1bAogICAgdGhpcy5yZWZfYW1wID0gMS4wOwoKICAgIHRoaXMubWVtb3J5X2N1cnNvciA9IDA7CiAgICB0aGlzLnN1c3RhaW5lZF9hbXAgPSBudWxsOwogICAgdGhpcy5zYW1wbGVfbWVtb3J5ID0gbnVsbDsKCiAgICB0aGlzLmFtcF9tZW1vcnlfY3Vyc29yID0gMDsKICAgIHRoaXMuYW1wX21lbW9yeSA9IG51bGw7CiAgICB0aGlzLmFtcF9taW5fYnVja2V0cyA9IG51bGw7CiAgICB0aGlzLmFtcF9idWNrZXRfc2l6ZSA9IDA7CgogICAgdGhpcy5wZWFrcyA9IG51bGw7CiAgICB0aGlzLmRlbGF5cyA9IFtdOwoKICAgIGNvbnN0IHRvU2FtcGxlcyA9ICh0KSA9PiBNYXRoLm1heChNYXRoLnJvdW5kKHRoaXMuc3JhdGUgKiB0KSwgMSk7CgogICAgbGV0IG1lbW9yeV9jb3VudCA9IHRvU2FtcGxlcyh0aGlzLmF0dGFjayk7CiAgICB0aGlzLnNhbXBsZV9tZW1vcnkgPSBuZXcgRmxvYXQzMkFycmF5KG1lbW9yeV9jb3VudCk7CiAgICB0aGlzLnN1c3RhaW5lZF9hbXAgPSBuZXcgRmxvYXQzMkFycmF5KG1lbW9yeV9jb3VudCk7CgogICAgbGV0IGFtcF9jb3VudCA9IHRvU2FtcGxlcyh0aGlzLmF0dGFjayArIHRoaXMuc3VzdGFpbik7CiAgICB0aGlzLmFtcF9idWNrZXRfc2l6ZSA9IE1hdGguc3FydChhbXBfY291bnQpIHwgMDsKICAgIHRoaXMuYW1wX21lbW9yeSA9IG5ldyBGbG9hdDMyQXJyYXkoYW1wX2NvdW50KTsKICAgIHRoaXMuYW1wX21lbW9yeS5maWxsKDEuMCk7CgogICAgbGV0IGFtcF9idWNrZXRfY291bnQgPSBhbXBfY291bnQgLyB0aGlzLmFtcF9idWNrZXRfc2l6ZSB8IDA7CiAgICB0aGlzLmFtcF9taW5fYnVja2V0cyA9IG5ldyBGbG9hdDMyQXJyYXkoYW1wX2J1Y2tldF9jb3VudCk7CiAgICB0aGlzLmFtcF9taW5fYnVja2V0cy5maWxsKDEuMCk7CgogICAgdGhpcy5wZWFrcyA9IG5ldyBGbG9hdDMyQXJyYXkoMTI4KTsgLy8gZ3Vlc3MgdGhlIGJ1ZmZlciBzaXplCiAgICB0aGlzLmRlbGF5cyA9IFt0aGlzLnNhbXBsZV9tZW1vcnldOwogIH0KCiAgcHJvY2VzcyhpbnB1dHMsIG91dHB1dHMsIHBhcmFtZXRlcnMpIHsKICAgIGNvbnN0IG1lbW9yeV9jb3VudCA9IHRoaXMuc2FtcGxlX21lbW9yeS5sZW5ndGg7IC8vIHRoaXMgc2hvd3MgdXAgYSBsb3QKCiAgICBjb25zdCBpbnB1dCA9IGlucHV0c1swXTsKICAgIGNvbnN0IG91dHB1dCA9IG91dHB1dHNbMF07CgogICAgaWYgKGlucHV0Lmxlbmd0aCA9PT0gMCkgewogICAgICByZXR1cm4gZmFsc2U7CiAgICB9CgogICAgd2hpbGUgKHRoaXMuZGVsYXlzLmxlbmd0aCA8IGlucHV0Lmxlbmd0aCkgewogICAgICB0aGlzLmRlbGF5cy5wdXNoKG5ldyBGbG9hdDMyQXJyYXkobWVtb3J5X2NvdW50KSk7CiAgICB9CgogICAgY29uc3QgYmxvY2tTaXplID0gaW5wdXRbMF0ubGVuZ3RoOwogICAgaWYgKGJsb2NrU2l6ZSA+IDAgJiYgdGhpcy5wZWFrcy5sZW5ndGggIT09IGJsb2NrU2l6ZSkgewogICAgICB0aGlzLnBlYWtzID0gbmV3IEZsb2F0MzJBcnJheShibG9ja1NpemUpOwogICAgfQogICAgdGhpcy5wZWFrcy5maWxsKDAuMCk7CgogICAgZm9yIChsZXQgYyA9IDA7IGMgPCBpbnB1dC5sZW5ndGg7IGMrKykgewogICAgICBjb25zdCBzb3VyY2UgPSBpbnB1dFtjXTsKICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBibG9ja1NpemU7IGkrKykgewogICAgICAgIHRoaXMucGVha3NbaV0gPSBNYXRoLm1heCh0aGlzLnBlYWtzW2ldLCBNYXRoLmFicyhzb3VyY2VbaV0pKTsKICAgICAgfQogICAgfQoKICAgIGNvbnN0IGRlbGF5X2N1cnNvciA9IHRoaXMubWVtb3J5X2N1cnNvcjsKCiAgICBmb3IgKGxldCBpID0gMDsgaSA8IGJsb2NrU2l6ZTsgaSsrKSB7CiAgICAgIGNvbnN0IHNhbXBsZSA9IHRoaXMucGVha3NbaV0gKiB0aGlzLnByZV9nYWluOwoKICAgICAgLy8gZG8gdGhlIHJlbGVhc2UgY3VydmUKICAgICAgdGhpcy5yZWxlYXNlX21lbW9yeSA9IE1hdGgubWF4KDAuMCwgdGhpcy5yZWxlYXNlX21lbW9yeSAtIDEuMCAvIHRoaXMuc3JhdGUpOwogICAgICBsZXQgYW1wID0gMS4wOwogICAgICBpZiAodGhpcy5yZWxlYXNlID4gMC4wKSB7CiAgICAgICAgbGV0IHQgPSAxLjAgLSB0aGlzLnJlbGVhc2VfbWVtb3J5IC8gdGhpcy5yZWxlYXNlOwogICAgICAgIGFtcCA9ICgxLjAgLSB0KSAqIHRoaXMucmVmX2FtcCArIHQ7CiAgICAgIH0KCiAgICAgIC8vIHJlc2V0IHN1c3RhaW4gaWYgd2UgZXhjZWVkIHRoZSBsaW1pdAogICAgICBsZXQgcmVmX3ZhbCA9IE1hdGguYWJzKGFtcCAqIHNhbXBsZSk7CiAgICAgIGlmIChyZWZfdmFsID49IHRoaXMubGltaXQpIHsKICAgICAgICBsZXQgYW1vdW50ID0gdGhpcy5saW1pdCAvIHJlZl92YWw7CiAgICAgICAgYW1wICo9IGFtb3VudDsKICAgICAgICByZWZfdmFsICo9IGFtb3VudDsKICAgICAgICB0aGlzLnJlZl9hbXAgPSBhbXA7CiAgICAgICAgdGhpcy5yZWxlYXNlX21lbW9yeSA9IHRoaXMucmVsZWFzZTsKICAgICAgfQoKICAgICAgdGhpcy5hbXBfbWVtb3J5W3RoaXMuYW1wX21lbW9yeV9jdXJzb3JdID0gYW1wOwoKICAgICAgLy8gdXBkYXRlIGFmZmVjdGVkIGJ1Y2tldAogICAgICBsZXQgYnVja2V0ID0gdGhpcy5hbXBfbWVtb3J5X2N1cnNvciAvIHRoaXMuYW1wX2J1Y2tldF9zaXplIHwgMDsKICAgICAgdGhpcy5hbXBfbWluX2J1Y2tldHNbYnVja2V0XSA9IDEuMDsKICAgICAgbGV0IGVuZCA9IE1hdGgubWluKChidWNrZXQgKyAxKSAqIHRoaXMuYW1wX2J1Y2tldF9zaXplLCB0aGlzLmFtcF9tZW1vcnkubGVuZ3RoKTsKICAgICAgZm9yIChsZXQgaiA9IGJ1Y2tldCAqIHRoaXMuYW1wX2J1Y2tldF9zaXplOyBqIDwgZW5kOyBqKyspIHsKICAgICAgICB0aGlzLmFtcF9taW5fYnVja2V0c1tidWNrZXRdID0gTWF0aC5taW4odGhpcy5hbXBfbWluX2J1Y2tldHNbYnVja2V0XSwgdGhpcy5hbXBfbWVtb3J5W2pdKTsKICAgICAgfQoKICAgICAgLy8gbm93IGdldCB0aGUgY3VycmVudCBzdXN0YWluIHZhbHVlCiAgICAgIGxldCBtaW5fYW1wID0gMS4wOwogICAgICBmb3IgKGxldCBqID0gMDsgaiA8IHRoaXMuYW1wX21pbl9idWNrZXRzLmxlbmd0aDsgaisrKSB7CiAgICAgICAgbGV0IHBhc3RfYW1wID0gdGhpcy5hbXBfbWluX2J1Y2tldHNbal07CiAgICAgICAgbWluX2FtcCA9IE1hdGgubWluKG1pbl9hbXAsIHBhc3RfYW1wKTsKICAgICAgfQoKICAgICAgdGhpcy5hbXBfbWVtb3J5X2N1cnNvciArPSAxOwogICAgICB0aGlzLmFtcF9tZW1vcnlfY3Vyc29yICU9IHRoaXMuYW1wX21lbW9yeS5sZW5ndGg7CgogICAgICAvLyB1cGRhdGUgdGhlIHN1c3RhaW4gYnVmZmVyCiAgICAgIHRoaXMuYm94X2JsdXIgLT0gdGhpcy5zdXN0YWluZWRfYW1wW3RoaXMubWVtb3J5X2N1cnNvcl0gKiAzMjc2NyB8IDA7CiAgICAgIHRoaXMuc3VzdGFpbmVkX2FtcFt0aGlzLm1lbW9yeV9jdXJzb3JdID0gbWluX2FtcDsKICAgICAgdGhpcy5ib3hfYmx1ciArPSB0aGlzLnN1c3RhaW5lZF9hbXBbdGhpcy5tZW1vcnlfY3Vyc29yXSAqIDMyNzY3IHwgMDsKCiAgICAgIC8vIHVwZGF0ZSB0aGUgc2FtcGxlIG1lbW9yeSBidWZmZXIKICAgICAgLy8gTk9URShub3R3YSk6IHdlIGluc3RlYWQgZG8gdGhpcyBsYXRlciBmb3IgZWFjaCBjaGFubmVsLgogICAgICAvL2xldCByZXRfc2FtcGxlID0gdGhpcy5zdXN0YWluICsgdGhpcy5hdHRhY2sgPiAwLjAgPyB0aGlzLnNhbXBsZV9tZW1vcnlbdGhpcy5tZW1vcnlfY3Vyc29yXSA6IHNhbXBsZTsKICAgICAgLy90aGlzLnNhbXBsZV9tZW1vcnlbdGhpcy5tZW1vcnlfY3Vyc29yXSA9IHNhbXBsZTsKCiAgICAgIHRoaXMubWVtb3J5X2N1cnNvciArPSAxOwogICAgICB0aGlzLm1lbW9yeV9jdXJzb3IgJT0gbWVtb3J5X2NvdW50OwoKICAgICAgdGhpcy5wZWFrc1tpXSA9IHRoaXMuYm94X2JsdXIgLyAzMjc2NyAvIG1lbW9yeV9jb3VudDsKICAgIH0KCiAgICBjb25zdCBmaW5hbF9nYWluID0gdGhpcy5wcmVfZ2FpbiAqIHRoaXMucG9zdF9nYWluOwogICAgZm9yIChsZXQgYyA9IDA7IGMgPCBpbnB1dC5sZW5ndGg7IGMrKykgewogICAgICBjb25zdCBzb3VyY2UgPSBpbnB1dFtjXTsKICAgICAgY29uc3QgdGFyZ2V0ID0gb3V0cHV0W2NdOwogICAgICBpZiAodGhpcy5zdXN0YWluICsgdGhpcy5hdHRhY2sgPD0gMC4wKSB7CiAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBibG9ja1NpemU7IGkrKykgewogICAgICAgICAgLy8gcmV0dXJuIHRoZSBsaW1pdGVkIHNhbXBsZQogICAgICAgICAgdGFyZ2V0W2ldID0gc291cmNlW2ldICogdGhpcy5wZWFrc1tpXSAqIGZpbmFsX2dhaW47CiAgICAgICAgfQogICAgICB9IGVsc2UgewogICAgICAgIGNvbnN0IGRlbGF5ID0gdGhpcy5kZWxheXNbY107CiAgICAgICAgbGV0IGxvY2FsX2N1cnNvciA9IGRlbGF5X2N1cnNvcjsKICAgICAgICBmb3IgKGxldCBpID0gMDsgaSA8IGJsb2NrU2l6ZTsgaSsrKSB7CiAgICAgICAgICBjb25zdCByZXRfc2FtcGxlID0gZGVsYXlbbG9jYWxfY3Vyc29yXTsKICAgICAgICAgIGRlbGF5W2xvY2FsX2N1cnNvcl0gPSBzb3VyY2VbaV07CgogICAgICAgICAgbG9jYWxfY3Vyc29yICs9IDE7CiAgICAgICAgICBsb2NhbF9jdXJzb3IgJT0gbWVtb3J5X2NvdW50OwoKICAgICAgICAgIC8vIHJldHVybiB0aGUgbGltaXRlZCBzYW1wbGUKICAgICAgICAgIHRhcmdldFtpXSA9IHJldF9zYW1wbGUgKiB0aGlzLnBlYWtzW2ldICogZmluYWxfZ2FpbjsKICAgICAgICB9CiAgICAgIH0KICAgIH0KCiAgICByZXR1cm4gZmFsc2U7IC8vIGRvY3Mgc2F5IHRvIHJldHVybiBmYWxzZSBmb3IgcHJvY2Vzc29ycyB3aXRoIG5vIHJldmVyYiB0YWlscwogIH0KfQoKcmVnaXN0ZXJQcm9jZXNzb3IoImxpbWl0ZXIiLCBMaW1pdGVyKTsK";
context.audioWorklet.addModule(uri).then(() => {
limiter = new AudioWorkletNode(context, "limiter");
console.info("Limiter successfully initialized.");
reconnect();
});
}
const iir = iirs[context.sampleRate];
if (typeof iir === "undefined") {
fancyCompression = false;
}
const now = context.currentTime;
if (!source) {
source = context.createMediaElementSource(player);
}
if (!gainIn) {
gainIn = context.createGain();
gainIn.gain.setValueAtTime(1.0, now);
}
if (!filterIn && fancyCompression) {
filterIn = createFilter(iir, false, 2.0);
}
if (!compressor) {
compressor = context.createDynamicsCompressor();
compressor.threshold.setValueAtTime(-50, now);
compressor.knee.setValueAtTime(40, now);
compressor.ratio.setValueAtTime(12, now);
compressor.attack.setValueAtTime(0.01, now);
compressor.release.setValueAtTime(0.33, now);
}
if (!filterOut && fancyCompression) {
filterOut = createFilter(iir, true, 0.5);
}
if (!gainOut) {
gainOut = context.createGain();
gainOut.gain.setValueAtTime(1.0, now);
}
if (!boosterMenuItem) {
boosterMenuItem = createMenuItem("Level Boost", boosterActive);
boosterMenuItem.onclick = () => {
boosterActive = ariaToggle(boosterMenuItem);
reconnect();
};
ytpPanelMenu.appendChild(boosterMenuItem);
}
if (!compressorMenuItem) {
compressorMenuItem = createMenuItem("Compressor", compressorActive);
compressorMenuItem.onclick = () => {
compressorActive = ariaToggle(compressorMenuItem);
reconnect();
};
ytpPanelMenu.appendChild(compressorMenuItem);
}
reconnect();
}
function attemptSetup() {
const player = document.querySelector("#ytd-player video");
const ytpPanelMenu = document.querySelector("#ytd-player .ytp-settings-menu .ytp-panel-menu");
if (player && ytpPanelMenu) {
setupCompressor(player, ytpPanelMenu);
console.info("Compressor options added.");
return true;
}
return false;
}
function trySetupLater() {
console.debug("Polling for player...");
const intervalId = window.setInterval(() => {
if (attemptSetup()) {
window.clearInterval(intervalId);
}
}, 100);
const cancelId = window.setTimeout(() => {
window.clearInterval(intervalId);
}, 10000);
}
if (!attemptSetup()) {
trySetupLater();
window.addEventListener("yt-navigate-finish", trySetupLater);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment