// ==UserScript==
// @name Video Volume Booster
// @version 0.0.27
// @author HentaiSaru
// @description 增強影片音量上限 , 最高增幅至10倍 , 未測試是否所有網域皆可使用 *://*/* , 目前只match特定網域
// @match *://*.twitch.tv/*
// @match *://*.youtube.com/*
// @match *://*.bilibili.com/*
// @exclude *://video.eyny.com/*
// @icon https://cdn-icons-png.flaticon.com/512/8298/8298181.png
// @license MIT
// @namespace https://gf.qytechs.cn/users/989635
// @run-at document-body
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
var Booster, Increase,
ListenerRecord = new Map(),
domain = location.hostname,
buffer = document.createDocumentFragment(),
enabledDomains = store("get", "啟用網域", []);
FindVideo();
MenuHotkey();
setTimeout(function() {MonitorAjax()}, 1000);
/* ==================== 菜單註冊 ==================== */
Menu({
"🔊 [開關] 自動增幅": ()=> Useboost(enabledDomains, domain),
"🛠️ 設置增幅": ()=> IncrementalSetting(),
"📜 菜單熱鍵": ()=> alert("可使用熱鍵方式呼叫設置菜單!!\n\n快捷組合 : (Alt + B)"),
})
/* ==================== API ==================== */
/* 添加監聽 */
async function addlistener(element, type, listener, add={}) {
if (!ListenerRecord.has(element) || !ListenerRecord.get(element).has(type)) {
element.addEventListener(type, listener, add);
if (!ListenerRecord.has(element)) {
ListenerRecord.set(element, new Map());
}
ListenerRecord.get(element).set(type, listener);
}
}
/* 查找元素 */
function $(element, all=false) {
if (!all) {
const analyze = element.includes(" ") ? " " : element[0];
return analyze == " " ? document.querySelector(element)
: analyze == "#" ? document.getElementById(element.slice(1))
: analyze == "." ? document.getElementsByClassName(element.slice(1))[0]
: document.getElementsByTagName(element)[0];
} else {return document.querySelectorAll(element)}
}
/* 等待元素 */
async function WaitElem(selector, timeout, callback) {
let timer, element;
const observer = new MutationObserver(() => {
element = $(selector);
if (element) {
observer.disconnect();
clearTimeout(timer);
callback(element);
}
});
observer.observe(document.body, { childList: true, subtree: true });
timer = setTimeout(() => {
observer.disconnect();
}, timeout);
}
/* 監聽 Ajex 變化 */
async function MonitorAjax() {
let Video;
const observer = new MutationObserver(() => {
Video = $("video");
Video && !Video.hasAttribute("data-audio-context") ? FindVideo() : null;
});
observer.observe(document.head, { childList: true, subtree: true });
}
/* 註冊菜單 API */
async function Menu(item) {
for (const [name, call] of Object.entries(item)) {
GM_registerMenuCommand(name, ()=> {call()});
}
}
/* 註冊快捷鍵(開啟菜單) API */
async function MenuHotkey() {
addlistener(document, "keydown", event => {
if (event.altKey && event.key === "b") {
IncrementalSetting()
}
}, { passive: true, capture: true });
}
/* 數據保存讀取 API */
function store(operate, key, orig=null){
return {
__verify: val => val !== undefined ? val : null,
set: function(val, put) {return GM_setValue(val, put)},
get: function(val, call) {return this.__verify(GM_getValue(val, call))},
setjs: function(val, put) {return GM_setValue(val, JSON.stringify(put, null, 4))},
getjs: function(val, call) {return JSON.parse(this.__verify(GM_getValue(val, call)))},
}[operate](key, orig);
}
/* ==================== 注入邏輯 ==================== */
/* 查找 Video 元素 */
async function FindVideo() {
WaitElem("video", 10000, video => {
try {
Increase = enabledDomains.includes(domain) ? store("get", domain) || 1.0 : 1.0;
Booster = booster(video, Increase);
} catch {}
});
}
/* 音量增量邏輯 */
function booster(video, increase) {
const AudioContext = new (window.AudioContext || window.webkitAudioContext);
const SourceNode = AudioContext.createMediaElementSource(video); // 音頻來源
const GainNode = AudioContext.createGain(); // 增益節點
const LowFilterNode = AudioContext.createBiquadFilter(); // 低音慮波器
const HighFilterNode = AudioContext.createBiquadFilter(); // 高音濾波器
const CompressorNode = AudioContext.createDynamicsCompressor(); // 動態壓縮節點
// 將預設音量調整至 100% (有可能被其他腳本調整)
video.volume = 1;
// 設置增量
GainNode.gain.value = increase * increase;
// 設置動態壓縮器的參數(通用性測試!!)
CompressorNode.ratio.value = 6; // 壓縮率
CompressorNode.knee.value = 0.5; // 壓縮過渡反應時間(越小越快)
CompressorNode.threshold.value = -14; // 壓縮閾值
CompressorNode.attack.value = 0.020; // 開始壓縮的速度
CompressorNode.release.value = 0.40; // 釋放壓縮的速度
// 低音慮波增強
LowFilterNode.frequency.value = 250;
LowFilterNode.type = "lowshelf";
LowFilterNode.gain.value = 2.2;
// 高音慮波增強
HighFilterNode.frequency.value = 10000;
HighFilterNode.type = "highshelf";
HighFilterNode.gain.value = 1.8;
// 進行節點連結
SourceNode.connect(GainNode);
GainNode.connect(LowFilterNode);
LowFilterNode.connect(HighFilterNode);
GainNode.connect(CompressorNode);
CompressorNode.connect(AudioContext.destination);
// 節點創建標記
video.setAttribute("data-audio-context", true);
return {
// 設置音量
setVolume: function(increase) {
GainNode.gain.value = increase * increase;
Increase = increase;
}
}
}
/* 使用自動增幅 */
async function Useboost(enabledDomains, domain) {
if (enabledDomains.includes(domain)) {
enabledDomains = enabledDomains.filter(function(value) { // 從已啟用列表中移除當前網域
return value !== domain;
});
alert("❌ 禁用自動增幅");
} else {
enabledDomains.push(domain); // 添加當前網域到已啟用列表
alert("✅ 啟用自動增幅");
}
store("set", "啟用網域", enabledDomains);
location.reload();
}
GM_addStyle(`
.modal-background {
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
z-index: 9999;
overflow: auto;
position: fixed;
align-items: center;
justify-content: center;
}
.modal-button {
top: 0;
margin: 3% 2%;
color: #d877ff;
font-size: 16px;
font-weight: bold;
border-radius: 3px;
background-color: #ffebfa;
border: 1px solid rgb(124, 183, 252);
}
.modal-button:hover,
.modal-button:focus {
color: #fc0e85;
cursor: pointer;
text-decoration: none;
}
.modal-content {
width: 400px;
padding: 5px;
overflow: auto;
background-color: #cff4ff;
border-radius: 10px;
text-align: center;
border: 2px ridge #82c4e2;
border-collapse: collapse;
margin: 2% auto 8px auto;
}
.multiplier {
font-size:25px;
color:rgb(253, 1, 85);
margin: 10px;
font-weight:bold;
}
.slider {width: 350px;}
input {cursor: pointer;}
`);
/* 設定菜單 */
async function IncrementalSetting() {
const modal = document.createElement("div");
modal.innerHTML = `
<div class="modal-content">
<h2 style="color: #3754f8;">音量增量</h2>
<div style="margin:1rem auto 1rem auto;">
<div class="multiplier">
<span><img src="https://cdn-icons-png.flaticon.com/512/8298/8298181.png" width="5%">增量倍數 </span><span id="CurrentValue">${Increase}</span><span> 倍</span>
</div>
<input type="range" id="sound-amplification" class="slider" min="0" max="10.0" value="${Increase}" step="0.1"><br>
</div>
<div style="text-align: right;">
<button class="modal-button" id="sound-save">保存設置</button>
<button class="modal-button" id="sound-close">退出選單</button>
</div>
</div>
`
modal.classList.add("modal-background");
document.body.appendChild(buffer.appendChild(modal));
const CurrentValue = $("#CurrentValue");
const slider = $("#sound-amplification");
// 監聽設定拉條
addlistener(slider, "input", event => {
const Current = event.target.value;
CurrentValue.textContent = Current;
Booster.setVolume(Current);
}, { passive: true, capture: true });
// 監聽保存關閉
addlistener($(".modal-background"), "click", click => {
click.stopPropagation();
const target = click.target;
if (target.id === "sound-save") {
if (enabledDomains.includes(domain)) {
store("set", domain, parseFloat(slider.value));
$(".modal-background").remove();
} else {alert("需啟用自動增幅才可保存")}
} else if (target.className === "modal-background" || target.id === "sound-close") {
$(".modal-background").remove();
}
}, { capture: true });
}
})();