From cae9d7762dc4f42cfe4f3079466b121865d6a5ca Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Tue, 7 Jun 2022 07:40:49 +0200 Subject: [PATCH] sound: add YouTube audio compressor.user.js --- sound/YouTube audio compressor.user.js | 197 +++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 sound/YouTube audio compressor.user.js diff --git a/sound/YouTube audio compressor.user.js b/sound/YouTube audio compressor.user.js new file mode 100644 index 0000000..1000d41 --- /dev/null +++ b/sound/YouTube audio compressor.user.js @@ -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(); + }); + } +})(); \ No newline at end of file