// ==UserScript==
// @name 音量控制 -- Video Volume Booster with Advanced Audio Enhancer
// @namespace https://greasyfork.org/users/1171320
// @version 1.0
// @description 按喜好调整网站音量,按域名存储(即不同网站能分别设置)。音量增大降低(0.1x-5.0x) + EQ降噪 + 动态压缩器、自动检测音质以限制最高音量,支持开关项勾选控制。高音质网站默认关闭。
// @author yzcjd
// @author2 ChatGPT4 辅助
// @match *://*/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const GOOD_SITES = [
'netflix.com', 'imdb.com', 'disneyplus.com', 'hulu.com', 'primevideo.com',
'apple.com', 'hbomax.com', 'paramountplus.com', 'peacocktv.com', 'qobuz.com',
'tidal.com', 'music.apple.com', 'music.amazon.com', 'deezer.com', 'soundcloud.com'
];
const domain = location.hostname;
const isGoodSite = GOOD_SITES.some(site => domain.includes(site));
// 读取域名相关设置
const getStoredSetting = (key) => {
return JSON.parse(localStorage.getItem(`${key}_${domain}`)) ?? false;
};
// 存储设置
const setStoredSetting = (key, value) => {
localStorage.setItem(`${key}_${domain}`, JSON.stringify(value));
};
const settings = {
eqEnabled: !isGoodSite && getStoredSetting('eqEnabled'),
compressorEnabled: !isGoodSite && getStoredSetting('compressorEnabled'),
autoLimitEnabled: !isGoodSite && getStoredSetting('autoLimitEnabled')
};
let audioContext, analyser, audioDataArray;
let gainNodes = new Map(), eqNodes = new Map(), compressorNodes = new Map();
let maxAllowedGain = 5.0;
const savedVolume = parseFloat(localStorage.getItem(`volume_multiplier_${domain}`)) || 1.0;
const style = document.createElement('style');
style.textContent = `
#volume-control-button {
position: fixed;
top: 60px;
right: 5px;
z-index: 99999;
background: #f5f5f5;
color: #000;
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #ccc;
cursor: pointer;
font-size: 12px;
transform: scale(0.75);
}
#volume-control-panel {
position: fixed;
top: 100px;
right: 5px;
z-index: 99999;
background: #fff;
border: 1px solid #ccc;
border-radius: 6px;
padding: 10px;
display: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
font-size: 12px;
}
#volume-slider {
width: 120px;
}
#volume-message {
font-size: 12px;
color: #555;
margin-top: 6px;
}
#volume-options label {
display: block;
margin: 4px 0;
}
`;
document.head.appendChild(style);
const button = document.createElement('button');
button.id = 'volume-control-button';
button.textContent = 'volume';
document.body.appendChild(button);
const panel = document.createElement('div');
panel.id = 'volume-control-panel';
panel.innerHTML = `
<label for="volume-slider">音量倍率:</label>
<input type="range" id="volume-slider" min="0.1" max="5" step="0.1">
<input type="number" id="volume-input" min="0.1" max="5" step="0.1" style="width:50px;">
<div id="volume-options">
<label><input type="checkbox" id="eq-toggle"> EQ降噪</label>
<label><input type="checkbox" id="compressor-toggle"> 动态压缩器</label>
<label><input type="checkbox" id="limit-toggle"> 限制最高音量</label>
</div>
<div id="volume-message"></div>
`;
document.body.appendChild(panel);
const slider = document.getElementById('volume-slider');
const input = document.getElementById('volume-input');
const message = document.getElementById('volume-message');
const eqToggle = document.getElementById('eq-toggle');
const compressorToggle = document.getElementById('compressor-toggle');
const limitToggle = document.getElementById('limit-toggle');
// 初始化设置
slider.value = input.value = savedVolume;
eqToggle.checked = settings.eqEnabled;
compressorToggle.checked = settings.compressorEnabled;
limitToggle.checked = settings.autoLimitEnabled;
function getAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
audioDataArray = new Uint8Array(analyser.frequencyBinCount);
}
return audioContext;
}
function applyGain(volume) {
const videos = document.querySelectorAll('video');
videos.forEach(video => {
if (!video.src) return;
if (!gainNodes.has(video)) {
const ctx = getAudioContext();
const source = ctx.createMediaElementSource(video);
const gainNode = ctx.createGain();
let node = source;
if (settings.eqEnabled) {
const eq = ctx.createBiquadFilter();
eq.type = 'highshelf';
eq.frequency.value = 4000;
eq.gain.value = -15;
node.connect(eq);
node = eq;
eqNodes.set(video, eq);
}
if (settings.compressorEnabled) {
const comp = ctx.createDynamicsCompressor();
comp.threshold.value = -50;
comp.knee.value = 40;
comp.ratio.value = 12;
comp.attack.value = 0.003;
comp.release.value = 0.25;
node.connect(comp);
node = comp;
compressorNodes.set(video, comp);
}
node.connect(gainNode);
gainNode.connect(ctx.destination);
gainNodes.set(video, gainNode);
}
const gainNode = gainNodes.get(video);
gainNode.gain.value = Math.min(volume, maxAllowedGain);
});
}
function analyzeAudio() {
if (!settings.autoLimitEnabled) {
maxAllowedGain = 5.0;
return;
}
analyser.getByteTimeDomainData(audioDataArray);
let sumSquares = 0, max = 0, min = 255;
for (let i = 0; i < audioDataArray.length; i++) {
const val = audioDataArray[i];
sumSquares += (val - 128) ** 2;
if (val > max) max = val;
if (val < min) min = val;
}
const rms = Math.sqrt(sumSquares / audioDataArray.length) / 128;
const dynamicRange = (max - min) / 255;
if (rms > 0.5 && dynamicRange < 0.2) {
maxAllowedGain = 2.5;
message.textContent = '检测到音源音质较差,仅允许放大至 2.5x';
} else if (rms > 0.3 && dynamicRange < 0.3) {
maxAllowedGain = 3.5;
message.textContent = '动态范围较低,建议不超过 3.5x';
} else {
maxAllowedGain = 5.0;
message.textContent = '音质良好,允许放大至 5.0x';
}
slider.max = input.max = maxAllowedGain;
if (parseFloat(slider.value) > maxAllowedGain) {
slider.value = input.value = maxAllowedGain;
applyGain(maxAllowedGain);
}
}
setInterval(analyzeAudio, 2000);
function setVolume(val) {
const v = Math.max(0.1, Math.min(maxAllowedGain, parseFloat(val) || 1.0));
slider.value = input.value = v;
localStorage.setItem(`volume_multiplier_${domain}`, v);
applyGain(v);
}
slider.addEventListener('input', () => setVolume(slider.value));
input.addEventListener('input', () => setVolume(input.value));
// 切换开关状态
eqToggle.addEventListener('change', () => {
settings.eqEnabled = eqToggle.checked;
setStoredSetting('eqEnabled', eqToggle.checked);
clearAudioNodes();
setVolume(slider.value);
});
compressorToggle.addEventListener('change', () => {
settings.compressorEnabled = compressorToggle.checked;
setStoredSetting('compressorEnabled', compressorToggle.checked);
clearAudioNodes();
setVolume(slider.value);
});
limitToggle.addEventListener('change', () => {
settings.autoLimitEnabled = limitToggle.checked;
setStoredSetting('autoLimitEnabled', limitToggle.checked);
analyzeAudio();
});
button.addEventListener('click', () => {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
function clearAudioNodes() {
gainNodes.clear();
compressorNodes.clear();
eqNodes.clear();
}
setVolume(savedVolume);
})();