Video Volume Booster

增強影片音量上限 , 最高增幅至10倍 , 未測試是否所有網域皆可使用 *://*/* , 目前只match特定網域

目前为 2023-09-07 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Video Volume Booster
  3. // @version 0.0.27
  4. // @author HentaiSaru
  5. // @description 增強影片音量上限 , 最高增幅至10倍 , 未測試是否所有網域皆可使用 *://*/* , 目前只match特定網域
  6.  
  7. // @match *://*.twitch.tv/*
  8. // @match *://*.youtube.com/*
  9. // @match *://*.bilibili.com/*
  10. // @exclude *://video.eyny.com/*
  11. // @icon https://cdn-icons-png.flaticon.com/512/8298/8298181.png
  12.  
  13. // @license MIT
  14. // @namespace https://gf.qytechs.cn/users/989635
  15.  
  16. // @run-at document-body
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_addStyle
  20. // @grant GM_registerMenuCommand
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. var Booster, Increase,
  25. ListenerRecord = new Map(),
  26. domain = location.hostname,
  27. buffer = document.createDocumentFragment(),
  28. enabledDomains = store("get", "啟用網域", []);
  29. FindVideo();
  30. MenuHotkey();
  31. setTimeout(function() {MonitorAjax()}, 1000);
  32.  
  33. /* ==================== 菜單註冊 ==================== */
  34.  
  35. Menu({
  36. "🔊 [開關] 自動增幅": ()=> Useboost(enabledDomains, domain),
  37. "🛠️ 設置增幅": ()=> IncrementalSetting(),
  38. "📜 菜單熱鍵": ()=> alert("可使用熱鍵方式呼叫設置菜單!!\n\n快捷組合 : (Alt + B)"),
  39. })
  40.  
  41. /* ==================== API ==================== */
  42.  
  43. /* 添加監聽 */
  44. async function addlistener(element, type, listener, add={}) {
  45. if (!ListenerRecord.has(element) || !ListenerRecord.get(element).has(type)) {
  46. element.addEventListener(type, listener, add);
  47. if (!ListenerRecord.has(element)) {
  48. ListenerRecord.set(element, new Map());
  49. }
  50. ListenerRecord.get(element).set(type, listener);
  51. }
  52. }
  53.  
  54. /* 查找元素 */
  55. function $(element, all=false) {
  56. if (!all) {
  57. const analyze = element.includes(" ") ? " " : element[0];
  58. return analyze == " " ? document.querySelector(element)
  59. : analyze == "#" ? document.getElementById(element.slice(1))
  60. : analyze == "." ? document.getElementsByClassName(element.slice(1))[0]
  61. : document.getElementsByTagName(element)[0];
  62. } else {return document.querySelectorAll(element)}
  63. }
  64.  
  65. /* 等待元素 */
  66. async function WaitElem(selector, timeout, callback) {
  67. let timer, element;
  68. const observer = new MutationObserver(() => {
  69. element = $(selector);
  70. if (element) {
  71. observer.disconnect();
  72. clearTimeout(timer);
  73. callback(element);
  74. }
  75. });
  76. observer.observe(document.body, { childList: true, subtree: true });
  77. timer = setTimeout(() => {
  78. observer.disconnect();
  79. }, timeout);
  80. }
  81.  
  82. /* 監聽 Ajex 變化 */
  83. async function MonitorAjax() {
  84. let Video;
  85. const observer = new MutationObserver(() => {
  86. Video = $("video");
  87. Video && !Video.hasAttribute("data-audio-context") ? FindVideo() : null;
  88. });
  89. observer.observe(document.head, { childList: true, subtree: true });
  90. }
  91.  
  92. /* 註冊菜單 API */
  93. async function Menu(item) {
  94. for (const [name, call] of Object.entries(item)) {
  95. GM_registerMenuCommand(name, ()=> {call()});
  96. }
  97. }
  98.  
  99. /* 註冊快捷鍵(開啟菜單) API */
  100. async function MenuHotkey() {
  101. addlistener(document, "keydown", event => {
  102. if (event.altKey && event.key === "b") {
  103. IncrementalSetting()
  104. }
  105. }, { passive: true, capture: true });
  106. }
  107.  
  108. /* 數據保存讀取 API */
  109. function store(operate, key, orig=null){
  110. return {
  111. __verify: val => val !== undefined ? val : null,
  112. set: function(val, put) {return GM_setValue(val, put)},
  113. get: function(val, call) {return this.__verify(GM_getValue(val, call))},
  114. setjs: function(val, put) {return GM_setValue(val, JSON.stringify(put, null, 4))},
  115. getjs: function(val, call) {return JSON.parse(this.__verify(GM_getValue(val, call)))},
  116. }[operate](key, orig);
  117. }
  118.  
  119. /* ==================== 注入邏輯 ==================== */
  120.  
  121. /* 查找 Video 元素 */
  122. async function FindVideo() {
  123. WaitElem("video", 10000, video => {
  124. try {
  125. Increase = enabledDomains.includes(domain) ? store("get", domain) || 1.0 : 1.0;
  126. Booster = booster(video, Increase);
  127. } catch {}
  128. });
  129. }
  130.  
  131. /* 音量增量邏輯 */
  132. function booster(video, increase) {
  133. const AudioContext = new (window.AudioContext || window.webkitAudioContext);
  134. const SourceNode = AudioContext.createMediaElementSource(video); // 音頻來源
  135. const GainNode = AudioContext.createGain(); // 增益節點
  136. const LowFilterNode = AudioContext.createBiquadFilter(); // 低音慮波器
  137. const HighFilterNode = AudioContext.createBiquadFilter(); // 高音濾波器
  138. const CompressorNode = AudioContext.createDynamicsCompressor(); // 動態壓縮節點
  139.  
  140. // 將預設音量調整至 100% (有可能被其他腳本調整)
  141. video.volume = 1;
  142. // 設置增量
  143. GainNode.gain.value = increase * increase;
  144.  
  145. // 設置動態壓縮器的參數(通用性測試!!)
  146. CompressorNode.ratio.value = 6; // 壓縮率
  147. CompressorNode.knee.value = 0.5; // 壓縮過渡反應時間(越小越快)
  148. CompressorNode.threshold.value = -14; // 壓縮閾值
  149. CompressorNode.attack.value = 0.020; // 開始壓縮的速度
  150. CompressorNode.release.value = 0.40; // 釋放壓縮的速度
  151.  
  152. // 低音慮波增強
  153. LowFilterNode.frequency.value = 250;
  154. LowFilterNode.type = "lowshelf";
  155. LowFilterNode.gain.value = 2.2;
  156.  
  157. // 高音慮波增強
  158. HighFilterNode.frequency.value = 10000;
  159. HighFilterNode.type = "highshelf";
  160. HighFilterNode.gain.value = 1.8;
  161.  
  162. // 進行節點連結
  163. SourceNode.connect(GainNode);
  164. GainNode.connect(LowFilterNode);
  165. LowFilterNode.connect(HighFilterNode);
  166. GainNode.connect(CompressorNode);
  167. CompressorNode.connect(AudioContext.destination);
  168. // 節點創建標記
  169. video.setAttribute("data-audio-context", true);
  170. return {
  171. // 設置音量
  172. setVolume: function(increase) {
  173. GainNode.gain.value = increase * increase;
  174. Increase = increase;
  175. }
  176. }
  177. }
  178.  
  179. /* 使用自動增幅 */
  180. async function Useboost(enabledDomains, domain) {
  181. if (enabledDomains.includes(domain)) {
  182. enabledDomains = enabledDomains.filter(function(value) { // 從已啟用列表中移除當前網域
  183. return value !== domain;
  184. });
  185. alert("❌ 禁用自動增幅");
  186. } else {
  187. enabledDomains.push(domain); // 添加當前網域到已啟用列表
  188. alert("✅ 啟用自動增幅");
  189. }
  190. store("set", "啟用網域", enabledDomains);
  191. location.reload();
  192. }
  193.  
  194. GM_addStyle(`
  195. .modal-background {
  196. top: 0;
  197. left: 0;
  198. width: 100%;
  199. height: 100%;
  200. display: flex;
  201. z-index: 9999;
  202. overflow: auto;
  203. position: fixed;
  204. align-items: center;
  205. justify-content: center;
  206. }
  207. .modal-button {
  208. top: 0;
  209. margin: 3% 2%;
  210. color: #d877ff;
  211. font-size: 16px;
  212. font-weight: bold;
  213. border-radius: 3px;
  214. background-color: #ffebfa;
  215. border: 1px solid rgb(124, 183, 252);
  216. }
  217. .modal-button:hover,
  218. .modal-button:focus {
  219. color: #fc0e85;
  220. cursor: pointer;
  221. text-decoration: none;
  222. }
  223. .modal-content {
  224. width: 400px;
  225. padding: 5px;
  226. overflow: auto;
  227. background-color: #cff4ff;
  228. border-radius: 10px;
  229. text-align: center;
  230. border: 2px ridge #82c4e2;
  231. border-collapse: collapse;
  232. margin: 2% auto 8px auto;
  233. }
  234. .multiplier {
  235. font-size:25px;
  236. color:rgb(253, 1, 85);
  237. margin: 10px;
  238. font-weight:bold;
  239. }
  240. .slider {width: 350px;}
  241. input {cursor: pointer;}
  242. `);
  243.  
  244. /* 設定菜單 */
  245. async function IncrementalSetting() {
  246. const modal = document.createElement("div");
  247. modal.innerHTML = `
  248. <div class="modal-content">
  249. <h2 style="color: #3754f8;">音量增量</h2>
  250. <div style="margin:1rem auto 1rem auto;">
  251. <div class="multiplier">
  252. <span><img src="https://cdn-icons-png.flaticon.com/512/8298/8298181.png" width="5%">增量倍數 </span><span id="CurrentValue">${Increase}</span><span> 倍</span>
  253. </div>
  254. <input type="range" id="sound-amplification" class="slider" min="0" max="10.0" value="${Increase}" step="0.1"><br>
  255. </div>
  256. <div style="text-align: right;">
  257. <button class="modal-button" id="sound-save">保存設置</button>
  258. <button class="modal-button" id="sound-close">退出選單</button>
  259. </div>
  260. </div>
  261. `
  262. modal.classList.add("modal-background");
  263. document.body.appendChild(buffer.appendChild(modal));
  264. const CurrentValue = $("#CurrentValue");
  265. const slider = $("#sound-amplification");
  266.  
  267. // 監聽設定拉條
  268. addlistener(slider, "input", event => {
  269. const Current = event.target.value;
  270. CurrentValue.textContent = Current;
  271. Booster.setVolume(Current);
  272. }, { passive: true, capture: true });
  273.  
  274. // 監聽保存關閉
  275. addlistener($(".modal-background"), "click", click => {
  276. click.stopPropagation();
  277. const target = click.target;
  278. if (target.id === "sound-save") {
  279. if (enabledDomains.includes(domain)) {
  280. store("set", domain, parseFloat(slider.value));
  281. $(".modal-background").remove();
  282. } else {alert("需啟用自動增幅才可保存")}
  283. } else if (target.className === "modal-background" || target.id === "sound-close") {
  284. $(".modal-background").remove();
  285. }
  286. }, { capture: true });
  287. }
  288. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址