// ==UserScript== // @name Media Volume Booster // @name:zh-TW 媒體音量增強器 // @name:zh-CN 媒体音量增强器 // @name:en Media Volume Booster // @version 2025.10.12-Beta // @author Canaan HS // @description 調整媒體音量與濾波器,增強倍數最高 20 倍,設置可記住並自動應用。部分網站可能無效、無聲音或無法播放,可選擇禁用。 // @description:zh-TW 調整媒體音量與濾波器,增強倍數最高 20 倍,設置可記住並自動應用。部分網站可能無效、無聲音或無法播放,可選擇禁用。 // @description:zh-CN 调整媒体音量与滤波器,增强倍数最高 20 倍,设置可记住并自动应用。部分网站可能无效、无声音或无法播放,可选择禁用。 // @description:en Adjust media volume and filters with enhancement factor up to 20x. Settings are saved and auto-applied. May not work on some sites (causing no sound or playback issues). Can be disabled if needed. // @noframes // @match *://*/* // @icon https://cdn-icons-png.flaticon.com/512/16108/16108408.png // @license MPL-2.0 // @namespace https://greasyfork.org/users/989635 // @supportURL https://github.com/Canaan-HS/MonkeyScript/issues // @resource Icon https://cdn-icons-png.flaticon.com/512/11243/11243783.png // @require https://update.greasyfork.icu/scripts/487608/1676101/SyntaxLite_min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_getResourceURL // @grant GM_registerMenuCommand // @grant GM_addValueChangeListener // @run-at document-body // @downloadURL none // ==/UserScript== (function () { const Default = { Gain: 1, LowFilterGain: 1.2, LowFilterFreq: 200, MidFilterQ: 1, MidFilterGain: 1.6, MidFilterFreq: 2e3, HighFilterGain: 1.8, HighFilterFreq: 1e4, CompressorRatio: 3, CompressorKnee: 4, CompressorThreshold: -8, CompressorAttack: .03, CompressorRelease: .2 }; const Share = { Menu: null, Parame: null, SetControl: null, ProcessLock: false, EnhancedNodes: [], ProcessedElements: new WeakSet() }; const word = { Traditional: {}, Simplified: { "🛠️ 調整菜單": "🛠️ 调整菜单", "✂️ 斷開增幅": "✂️ 断开增幅", "🔗 恢復增幅": "🔗 恢复增幅", "❌ 禁用網域": "❌ 禁用网域", "✅ 啟用網域": "✅ 启用网域", "增強錯誤": "增强错误", "音量增強器": "音量增强器", "增強倍數 ": "增强倍数 ", " 倍": " 倍", "增益": "增益", "頻率": "频率", "Q值": "Q值", "低頻設定": "低频设置", "中頻設定": "中频设置", "高頻設定": "高频设置", "動態壓縮": "动态压缩", "壓縮率": "压缩率", "過渡反應": "过渡反应", "閾值": "阈值", "起音速度": "起音速度", "釋放速度": "释放速度", "關閉": "关闭", "保存": "保存", "不支援的媒體跳過": "不支持的媒体跳过", "不支援音頻增強節點": "不支持音频增强节点", "添加增強節點成功": "添加增强节点成功", "添加增強節點失敗": "添加增强节点失败", "當前沒有被增幅的媒體": "当前没有被增幅的媒体", "快捷組合 : (Alt + B)": "快捷组合 : (Alt + B)" }, English: { "🛠️ 調整菜單": "🛠️ Settings Menu", "✂️ 斷開增幅": "✂️ Disconnect Amplification", "🔗 恢復增幅": "🔗 Restore Amplification", "❌ 禁用網域": "❌ Disable Domain", "✅ 啟用網域": "✅ Enable Domain", "增強錯誤": "Enhancement Error", "音量增強器": "Volume Booster", "增強倍數 ": "Enhancement Multiplier ", " 倍": "x", "增益": "Gain", "頻率": "Frequency", "Q值": "Q Factor", "低頻設定": "Low Frequency Settings", "中頻設定": "Mid Frequency Settings", "高頻設定": "High Frequency Settings", "動態壓縮": "Dynamic Compressor", "壓縮率": "Compression Ratio", "過渡反應": "Knee", "閾值": "Threshold", "起音速度": "Attack", "釋放速度": "Release", "關閉": "Close", "保存": "Save", "不支援的媒體跳過": "Unsupported Media Skipped", "不支援音頻增強節點": "Audio Enhancement Node Not Supported", "添加增強節點成功": "Enhancement Node Added Successfully", "添加增強節點失敗": "Failed to Add Enhancement Node", "當前沒有被增幅的媒體": "No media is currently being amplified", "快捷組合 : (Alt + B)": "Shortcut: (Alt + B)" } }; const { Transl } = (() => { const matcher = Lib.translMatcher(word); return { Transl: str => matcher[str] ?? str }; })(); const bannedDomains = (() => { let banned = new Set(Lib.getV("Banned", [])); let excludeStatus = banned.has(Lib.$domain); return { isEnabled: callback => callback(!excludeStatus), addBanned: async () => { excludeStatus ? banned.delete(Lib.$domain) : banned.add(Lib.$domain); Lib.setV("Banned", [...banned]); location.reload(); } }; })(); const updateParame = () => { let Config = Lib.getV(Lib.$domain, {}); if (typeof Config === "number") { Config = { Gain: Config }; } Share.Parame = Object.assign({}, Default, Config); }; const Booster = (() => { let updated = false; let initialized = false; let mediaAudioContent = null; const audioContext = window.AudioContext || window.webkitAudioContext; function booster(mediaObj) { try { if (!audioContext) throw new Error(Transl("不支援音頻增強節點")); if (!mediaAudioContent) mediaAudioContent = new audioContext(); if (mediaAudioContent.state === "suspended") mediaAudioContent.resume(); const successNode = []; for (const media of mediaObj) { Share.ProcessedElements.add(media); if (media.mediaKeys || media.encrypted || window.MediaSource && media.srcObject instanceof MediaSource) { Lib.log(media, { group: Transl("不支援的媒體跳過"), collapsed: false }); continue; } try { if (!media.crossOrigin && media.src && !media.src.startsWith("blob:")) { const src = media.src; media.crossOrigin = "anonymous"; media.src = ""; media.src = src; } const SourceNode = mediaAudioContent.createMediaElementSource(media); const GainNode = mediaAudioContent.createGain(); const LowFilterNode = mediaAudioContent.createBiquadFilter(); const MidFilterNode = mediaAudioContent.createBiquadFilter(); const HighFilterNode = mediaAudioContent.createBiquadFilter(); const CompressorNode = mediaAudioContent.createDynamicsCompressor(); GainNode.gain.value = Share.Parame.Gain; LowFilterNode.type = "lowshelf"; LowFilterNode.gain.value = Share.Parame.LowFilterGain; LowFilterNode.frequency.value = Share.Parame.LowFilterFreq; MidFilterNode.type = "peaking"; MidFilterNode.Q.value = Share.Parame.MidFilterQ; MidFilterNode.gain.value = Share.Parame.MidFilterGain; MidFilterNode.frequency.value = Share.Parame.MidFilterFreq; HighFilterNode.type = "highshelf"; HighFilterNode.gain.value = Share.Parame.HighFilterGain; HighFilterNode.frequency.value = Share.Parame.HighFilterFreq; CompressorNode.ratio.value = Share.Parame.CompressorRatio; CompressorNode.knee.value = Share.Parame.CompressorKnee; CompressorNode.threshold.value = Share.Parame.CompressorThreshold; CompressorNode.attack.value = Share.Parame.CompressorAttack; CompressorNode.release.value = Share.Parame.CompressorRelease; SourceNode.connect(GainNode).connect(LowFilterNode).connect(MidFilterNode).connect(HighFilterNode).connect(CompressorNode).connect(mediaAudioContent.destination); Share.EnhancedNodes.push({ Connected: true, Destination: mediaAudioContent.destination, SourceNode: SourceNode, GainNode: GainNode, LowFilterNode: LowFilterNode, MidFilterNode: MidFilterNode, HighFilterNode: HighFilterNode, CompressorNode: CompressorNode, Gain: GainNode.gain, LowFilterGain: LowFilterNode.gain, LowFilterFreq: LowFilterNode.frequency, MidFilterQ: MidFilterNode.Q, MidFilterGain: MidFilterNode.gain, MidFilterFreq: MidFilterNode.frequency, HighFilterGain: HighFilterNode.gain, HighFilterFreq: HighFilterNode.frequency, CompressorRatio: CompressorNode.ratio, CompressorKnee: CompressorNode.knee, CompressorThreshold: CompressorNode.threshold, CompressorAttack: CompressorNode.attack, CompressorRelease: CompressorNode.release }); successNode.push(media); } catch (error) { Lib.log({ media: media, error: error }, { group: Transl("添加增強節點失敗"), collapsed: false }).error; } } if (successNode.length > 0) { Share.ProcessLock = false; Lib.log(successNode, { group: Transl("添加增強節點成功"), collapsed: false }); if (!initialized) { initialized = true; let disconnected = false; const regChange = () => { Lib.regMenu({ [Transl(disconnected ? "🔗 恢復增幅" : "✂️ 斷開增幅")]: () => { if (Share.EnhancedNodes.length === 0) { alert(Transl("當前沒有被增幅的媒體")); return; } Share.EnhancedNodes.forEach(items => { const { Connected, SourceNode, GainNode, LowFilterNode, MidFilterNode, HighFilterNode, CompressorNode, Destination } = items; if (disconnected && !Connected) { SourceNode.connect(GainNode).connect(LowFilterNode).connect(MidFilterNode).connect(HighFilterNode).connect(CompressorNode).connect(Destination); items.Connected = true; } else if (!disconnected && Connected) { SourceNode.disconnect(); GainNode.disconnect(); LowFilterNode.disconnect(); MidFilterNode.disconnect(); HighFilterNode.disconnect(); CompressorNode.disconnect(); SourceNode.connect(Destination); items.Connected = false; } }); disconnected = !disconnected; regChange(); }, [Transl("🛠️ 調整菜單")]: { desc: Transl("快捷組合 : (Alt + B)"), func: () => { Share.Menu(); } } }, { index: 2 }); }; regChange(); Lib.onEvent(document, "keydown", event => { if (event.altKey && event.key.toUpperCase() == "B") Share.Menu(); }, { passive: true, capture: true, mark: "Media-Booster-Hotkey" }); Lib.storageListen([Lib.$domain], call => { if (call.far && call.key === Lib.$domain) { Object.entries(call.nv).forEach(([type, value]) => { Share.SetControl(type, value); }); } }); } } } catch (error) { Lib.log(error, { group: Transl("增強錯誤"), collapsed: false }).error; } } function trigger(media) { try { if (!updated) { updated = true; updateParame(); } booster(media); } catch (error) { Lib.log(error, { group: "Trigger Error : ", collapsed: false }).error; } } return { trigger: trigger }; })(); const CreateMenu = () => { const icon = GM_getResourceURL("Icon"); return () => { const shadowID = "Booster_Menu"; if (Lib.$q(`#${shadowID}`)) return; const shadow = Lib.createElement(Lib.body, "div", { id: shadowID }); const shadowRoot = shadow.attachShadow({ mode: "open" }); const style = ` `; const generateOtherTemplate = (label, groups) => `
${groups.map(group => `
${Transl(group.label)} ${Share.Parame[group.id]}
`).join("")}
`; shadowRoot.$safeiHtml(` ${style} <${shadowID} id="Booster-Modal-Menu">

${Transl("音量增強器")}

${Transl("增強倍數 ")} ${Share.Parame.Gain}${Transl(" 倍")}
${generateOtherTemplate("低頻設定", [{ label: "增益", id: "LowFilterGain", min: "-12", max: "12", step: "0.1" }, { label: "頻率", id: "LowFilterFreq", min: "20", max: "1000", step: "20" }])} ${generateOtherTemplate("中頻設定", [{ label: "增益", id: "MidFilterGain", min: "-12", max: "12", step: "0.1" }, { label: "頻率", id: "MidFilterFreq", min: "200", max: "8000", step: "100" }, { label: "Q值", id: "MidFilterQ", min: "0.5", max: "5", step: "0.1" }])} ${generateOtherTemplate("高頻設定", [{ label: "增益", id: "HighFilterGain", min: "-12", max: "12", step: "0.1" }, { label: "頻率", id: "HighFilterFreq", min: "2000", max: "22000", step: "500" }])} ${generateOtherTemplate("動態壓縮", [{ label: "壓縮率", id: "CompressorRatio", min: "1", max: "30", step: "0.1" }, { label: "過渡反應", id: "CompressorKnee", min: "0", max: "40", step: "1" }, { label: "閾值", id: "CompressorThreshold", min: "-60", max: "0", step: "1" }, { label: "起音速度", id: "CompressorAttack", min: "0.001", max: "0.5", step: "0.001" }, { label: "釋放速度", id: "CompressorRelease", min: "0.01", max: "2", step: "0.01" }])}
`); const shadowGate = shadow.shadowRoot; const modal = shadowGate.querySelector(shadowID); const content = shadowGate.querySelector(".Booster-Modal-Content"); function deleteMenu() { modal.classList.add("close"); content.classList.add("close"); setTimeout(() => { shadow.remove(); }, 800); } const displayMap = { ...Object.fromEntries([...shadowGate.querySelectorAll(".Booster-Label")].map(el => [el.id, el])) }; function updateControl(id, value) { displayMap[`${id}-Label`].textContent = value; shadowGate.querySelector(`#${id}`).value = value; Share.SetControl(id, value); } content.addEventListener("input", event => { const target = event.target; if (target.type !== "range") return; const id = target.id; const value = parseFloat(target.value); updateControl(id, value); }); content.addEventListener("click", event => { const target = event.target; if (!target.classList.contains("Booster-Label") || target.isEditing) return; target.isEditing = true; const originalValue = target.textContent.trim(); const controlId = target.id.replace("-Label", ""); const slider = shadowGate.querySelector(`#${controlId}`); target.textContent = ""; const input = Lib.createElement(target, "input", { class: "Booster-Label-Input", value: originalValue, on: { blur: { listen: () => { let newValue = parseFloat(input.value); const min = parseFloat(slider.min); const max = parseFloat(slider.max); if (isNaN(newValue)) { newValue = parseFloat(originalValue); } else if (newValue < min) { newValue = min; } else if (newValue > max) { newValue = max; } target.isEditing = false; updateControl(controlId, newValue); target.textContent = newValue; }, add: { once: true } }, keydown: e => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { e.target.value = originalValue; e.target.blur(); } } } }); requestAnimationFrame(() => input.focus()); }); modal.addEventListener("click", click => { const target = click.target; click.stopPropagation(); if (target.classList.contains("Booster-Accordion")) { target.classList.toggle("active"); const panel = target.nextElementSibling; if (panel.style.maxHeight) { panel.style.maxHeight = null; panel.classList.remove("active"); } else { panel.style.maxHeight = panel.scrollHeight + "px"; panel.classList.add("active"); } } else if (target.id === "Booster-Sound-Save") { Lib.setV(Lib.domain, Share.Parame); deleteMenu(); } else if (target.id === "Booster-Menu-Close" || target.id === "Booster-Modal-Menu") { deleteMenu(); } }); }; }; function Main() { bannedDomains.isEnabled(status => { const regMenu = async name => { Lib.regMenu({ [name]: () => bannedDomains.addBanned() }); }; if (status) { Share.Menu = CreateMenu(); Share.SetControl = (type, value) => { Share.Parame[type] = value; Share.EnhancedNodes.forEach(items => { items[type].value = value; }); }; const findMedia = Lib.$debounce(func => { const media = []; const tree = document.createTreeWalker(Lib.body, NodeFilter.SHOW_ELEMENT, { acceptNode: node => { const tag = node.tagName; if (tag === "VIDEO" || tag === "AUDIO") { if (!Share.ProcessedElements.has(node)) return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_SKIP; } }); while (tree.nextNode()) { media.push(tree.currentNode); } media.length > 0 && func(media); }, 50); Lib.$observer(Lib.body, () => { if (Share.ProcessLock) return; findMedia(media => { Share.ProcessLock = true; Booster.trigger(media); }); }, { mark: "Media-Booster", attributes: false, throttle: 200 }, ({ ob }) => { regMenu(Transl("❌ 禁用網域")); }); } else regMenu(Transl("✅ 啟用網域")); }); } Main(); })();