197 lines
No EOL
6.4 KiB
JavaScript
197 lines
No EOL
6.4 KiB
JavaScript
// ==UserScript==
|
|
// @name YouTube audio compressor
|
|
// @namespace https://eaguru.guru/
|
|
// @version 0.4.2
|
|
// @description Adds an audio compressor option to YouTube videos. Now with over-engineering!
|
|
// @author Vivelin, 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==
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
let active = false;
|
|
let audioContext = null;
|
|
let source = null;
|
|
let compressor = null;
|
|
let gainIn = null;
|
|
let filterIn = null;
|
|
let filterOut = null;
|
|
let gainOut = 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, the zeros of an IIR are not 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,
|
|
// with their numerators and denominators swapped.
|
|
//
|
|
// 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];
|
|
|
|
let fancy = true;
|
|
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 = audioContext.createIIRFilter(num, den);
|
|
return filter;
|
|
}
|
|
|
|
function createCompressorMenuItem() {
|
|
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 = 'Compressor';
|
|
menuItem.appendChild(label);
|
|
|
|
const content = document.createElement('div');
|
|
content.className = 'ytp-menuitem-content';
|
|
const toggleCheckbox = document.createElement('div');
|
|
toggleCheckbox.className = 'ytp-menuitem-toggle-checkbox';
|
|
content.appendChild(toggleCheckbox);
|
|
menuItem.appendChild(content);
|
|
console.log(menuItem);
|
|
return menuItem;
|
|
}
|
|
|
|
function setupCompressor(player, ytpPanelMenu) {
|
|
if (!audioContext) {
|
|
audioContext = new AudioContext();
|
|
}
|
|
const iir = iirs[audioContext.sampleRate];
|
|
if (typeof iir === "undefined") {
|
|
fancy = false;
|
|
}
|
|
|
|
if (!source) {
|
|
source = audioContext.createMediaElementSource(player);
|
|
}
|
|
if (!gainIn) {
|
|
gainIn = audioContext.createGain();
|
|
gainIn.gain.setValueAtTime(0.7, audioContext.currentTime);
|
|
}
|
|
if (!filterIn && fancy) {
|
|
filterIn = createFilter(iir, false, 2.0);
|
|
}
|
|
if (!compressor) {
|
|
compressor = audioContext.createDynamicsCompressor();
|
|
compressor.threshold.setValueAtTime(-50, audioContext.currentTime);
|
|
compressor.knee.setValueAtTime(40, audioContext.currentTime);
|
|
compressor.ratio.setValueAtTime(12, audioContext.currentTime);
|
|
compressor.attack.setValueAtTime(0.01, audioContext.currentTime);
|
|
compressor.release.setValueAtTime(0.33, audioContext.currentTime);
|
|
}
|
|
if (!filterOut && fancy) {
|
|
filterOut = createFilter(iir, true, 0.5);
|
|
}
|
|
if (!gainOut) {
|
|
gainOut = audioContext.createGain();
|
|
gainOut.gain.setValueAtTime(0.7, audioContext.currentTime);
|
|
}
|
|
|
|
if (active) {
|
|
source.connect(gainIn);
|
|
} else {
|
|
source.connect(audioContext.destination);
|
|
}
|
|
|
|
if (fancy) {
|
|
gainIn.connect(filterIn);
|
|
filterIn.connect(compressor);
|
|
compressor.connect(filterOut);
|
|
filterOut.connect(gainOut);
|
|
} else {
|
|
gainIn.connect(compressor);
|
|
compressor.connect(gainOut);
|
|
}
|
|
|
|
if (!compressorMenuItem) {
|
|
compressorMenuItem = createCompressorMenuItem();
|
|
ytpPanelMenu.appendChild(compressorMenuItem);
|
|
compressorMenuItem.onclick = function () {
|
|
const isActive = compressorMenuItem.getAttribute('aria-checked');
|
|
if (isActive === 'false') {
|
|
compressorMenuItem.setAttribute('aria-checked', 'true');
|
|
source.disconnect(audioContext.destination);
|
|
source.connect(gainIn);
|
|
gainOut.connect(audioContext.destination);
|
|
active = true;
|
|
} else {
|
|
compressorMenuItem.setAttribute('aria-checked', 'false');
|
|
source.disconnect(gainIn);
|
|
gainOut.disconnect(audioContext.destination);
|
|
source.connect(audioContext.destination);
|
|
active = false;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
function startSetup() {
|
|
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 option added');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function trySetup() {
|
|
console.debug('Polling for player...');
|
|
const intervalId = window.setInterval(function () {
|
|
if (startSetup()) {
|
|
window.clearInterval(intervalId);
|
|
}
|
|
}, 100);
|
|
|
|
const cancelId = window.setTimeout(function () {
|
|
window.clearInterval(intervalId);
|
|
}, 10000);
|
|
}
|
|
|
|
if (!startSetup()) {
|
|
trySetup();
|
|
|
|
window.addEventListener('yt-navigate-finish', function () {
|
|
trySetup();
|
|
});
|
|
}
|
|
})(); |