sound: add YouTube audio compressor.user.js
This commit is contained in:
parent
c4bcf2c029
commit
cae9d7762d
1 changed files with 197 additions and 0 deletions
197
sound/YouTube audio compressor.user.js
Normal file
197
sound/YouTube audio compressor.user.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
// ==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();
|
||||
});
|
||||
}
|
||||
})();
|
Loading…
Reference in a new issue