Mouse Amplifier

Amplify volume of Twitch streams on a per-channel basis

目前為 2022-03-19 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();