Amplify volume of Twitch streams on a per-channel basis
目前為
// ==UserScript==
// @name Mouse Amplifier
// @description Amplify volume of Twitch streams on a per-channel basis
// @author Xspeed
// @namespace xspeed.net
// @license MIT
// @version 2
// @icon https://cdn.discordapp.com/icons/399202924695388160/a_131293bc9991ade1ec366efb27b05adf.webp
// @match *://*.twitch.tv/*
// @grant GM.getValue
// @grant GM.setValue
// @run-at document-idle
// ==/UserScript==
let predefinedDefaults = {
ironmouse: 2.25
};
let lastName;
let targetVid;
let gainNode;
let inputElem;
let audioCtx = new AudioContext();
function log(txt) {
console.log('[' + GM.info.script.name + '] ' + txt);
}
async function setup() {
let path = document.location.pathname.substring(1).split('/');
let mobile = document.location.hostname.split('.')[0] == 'm';
if (path.length == 1 || path[0] == 'videos' || path[1] == 'clip') {
let chnlElem;
let chnlName;
if (mobile) {
let child = document.querySelector('a.tw-link>figure.tw-avatar>img.tw-image');
if (child) chnlElem = child.parentElement.parentElement;
if (chnlElem) {
let arr = chnlElem.href.split('/');
chnlName = arr[arr.length - 2];
}
}
else {
chnlElem = document.querySelector('div>a>h1.tw-title');
if (chnlElem) chnlName = chnlElem.innerText;
}
if (!chnlName || chnlName.length == 0) {
// Not a channel page, offline or not loaded yet
return;
}
let gainData = JSON.parse(await GM.getValue('gainData', '{}')) ?? {};
let gain = gainData[chnlName];
if (gain == null || isNaN(gain)) {
gain = predefinedDefaults[chnlName] ?? 1;
gainData[chnlName] = gain;
}
if (chnlName != lastName) {
log('Detected channel ' + chnlName + ", initial gain " + gain);
lastName = chnlName;
}
if (gainNode == null) {
let source = audioCtx.createMediaElementSource(targetVid);
gainNode = audioCtx.createGain();
source.connect(gainNode);
gainNode.connect(audioCtx.destination);
}
if (inputElem == null || !document.body.contains(inputElem)) {
let container = document.createElement('div');
inputElem = document.createElement('input');
inputElem.type = 'range';
inputElem.min = 1;
inputElem.max = 40;
inputElem.step = 1;
inputElem.value = Math.sqrt(gain) * 10;
inputElem.style.verticalAlign = 'middle';
let labelElem = document.createElement('label');
labelElem.innerText = 'Gain ' + Number(gain).toFixed(2) + 'x';
labelElem.style.display = 'inline-block';
labelElem.style.marginLeft = '5px';
labelElem.style.verticalAlign = 'middle';
inputElem.addEventListener('input', setGain);
inputElem.addEventListener('change', function() {
setGain();
gainData[chnlName] = gain;
GM.setValue('gainData', JSON.stringify(gainData));
log('Gain for channel ' + chnlName + ' set to ' + gain);
});
container.appendChild(inputElem);
container.appendChild(labelElem);
let parent = chnlElem.parentElement.parentElement.parentElement.parentElement;
parent.appendChild(container);
}
function setGain() {
gain = inputElem.value * inputElem.value / 100;
gainNode.gain.value = inputElem.value * inputElem.value / 100;
inputElem.parentElement.childNodes[1].innerText = 'Gain ' + Number(gain).toFixed(2) + 'x';
}
setGain();
}
}
function presetup() {
if (gainNode == null || inputElem == null || !document.body.contains(inputElem)) {
setup();
}
}
function detect() {
let vidElem = document.querySelector('video');
if (vidElem != targetVid) {
if (targetVid) {
targetVid.removeEventListener('canplay', presetup);
}
targetVid = vidElem;
gainNode = null;
if (targetVid) {
log('New video element found on page');
targetVid.addEventListener('canplay', presetup);
}
}
presetup();
}
let observer = new MutationObserver(detect);
observer.observe(document, { subtree: true, childList: true });
detect();