// ==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) => `