您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
HTML5 Karaoke Vocal Control, support center channel cut on regular MV, left/right channel vocal/instrumental mixed MVs. Support: Youtube and Bilibili
// ==UserScript== // @name Youtube HTML5 Karaoke // @namespace https://github.com/heyqule/youtubekaraoke // @version 1.4.0 // @description HTML5 Karaoke Vocal Control, support center channel cut on regular MV, left/right channel vocal/instrumental mixed MVs. Support: Youtube and Bilibili // @description:zh HTML5 卡拉OK人声控制,支持常规MV中置声道切换,左右声道人声/器乐混合MV。支持:Youtube 和 Bilibili // @description:ja HTML5 カラオケ ボーカル コントロール、通常の MV でのセンター チャンネル カット、左/右チャンネルのボーカル/インストゥルメンタル ミックス MV をサポート。サポート: Youtube, Bilibili // @author heyqule // @license GPLv3 // @match https://www.youtube.com/* // @match https://www.bilibili.com/* // @require https://code.jquery.com/jquery-4.0.0-beta.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/build/md5.min.js // @grant unsafeWindow // @grant GM.xmlHttpRequest // @grant window.onurlchange // @run-at document-end // ==/UserScript== (function($, md5) { 'use strict'; if (window.trustedTypes && window.trustedTypes.createPolicy) { window.trustedTypes.createPolicy('default', { createHTML: (string) => string, createScript: (string) => string }); } const languages={ "zh":{ "title": "🎤 控制", "off": "🎤: 关", "on": "🎤: 开", "vocal_l1": "人声衰减", "vocal_l2": "(左 - 中1 - 中2 - 右)", "high_pass": "高通", "low_pass": "低通", "mic_gain": "🎤 增益", "mic_gain_desc": "从浏览器连接的麦克风有明显的延迟。 建议通过音频接口器去控制。", }, //Ja by google translate "ja":{ "title": "🎤 コントロール", "off": "🎤: オフ", "on": "🎤: オン", "vocal_l1": "ボーカルの減衰", "vocal_l2": "(左 - 中1 - 中2 - 右)", "high_pass": "ハイパス", "low_pass": "ローパス", "mic_gain": "🎤 ゲイン", "mic_gain_desc": "ブラウザから接続したマイクの遅延が顕著です。 オーディオインターフェース経由でコントロールすることをお勧めします。", }, "en":{ "title": "🎤 Controls", "off": "🎤: OFF", "on": "🎤: ON", "vocal_l1": "Vocal Attenuation", "vocal_l2": "(left - center1 - center2 - right)", "high_pass": "High Pass", "low_pass": "Low Pass", "mic_gain": "🎤 Gain", "mic_gain_desc": "Mic connected from browser has noticeable delay. Recommend to connect mic through an audio interface.", }, } let lang = 'en'; //Youtube Handler let mediaElement = 'video.html5-main-video'; let targetContainer = 'div.ytp-right-controls'; let UiAttachTo = 'div#primary div#player'; let youtubeDarkThemeUiAttachTo = 'div#primary div#alerts'; let buttonTag = '<button />'; let buttonClass = 'ytp-karaoke-button ytp-button'; let buttonStyle = 'position: relative; top:-1.5rem; padding-left:1rem; font-size:2rem; cursor: pointer;'; let urlChangePattern = 'watch'; let getSongId = function() { let queryString = window.location.search; let urlParams = new URLSearchParams(queryString); return urlParams.get('v'); } let isYoutubeDarkTheme = document.documentElement.hasAttribute('dark'); let darkThemeTextColor = ' color:#fff;'; let youtubeLang = document.documentElement.getAttribute('lang'); if (youtubeLang) { lang = (youtubeLang.indexOf("-") != -1 ? youtubeLang.split("-")[0] : 'en').toLocaleLowerCase(); } if (/bilibili\.com/.test(window.location.href)) { mediaElement = '#bilibili-player video'; targetContainer = 'div.bpx-player-control-bottom-right'; UiAttachTo = '#playerWrap'; buttonTag = '<div />'; buttonClass = 'bpx-player-ctrl-btn'; buttonStyle = 'position: relative; margin-right:1rem; font-size:1.5rem; cursor: pointer;'; urlChangePattern = 'video'; getSongId = function() { let token = window.location.pathname; return md5(token); } if (/bilibili\.com\/bangumi\/play/.test(window.location.href)) { targetContainer = 'div.bpx-player-control-bottom-right'; UiAttachTo = '#bilibili-player-wrap'; urlChangePattern = 'bangumi/play'; } } let KaraokeUI = function ($) { let _translate = function(label) { return languages[lang][label] ?? languages["en"][label] ?? '{404 locale:'+label+'}'; } let karaokeButton = $(buttonTag,{ title: _translate('off'), id: 'karaoke-button', class: buttonClass, text: '🎤', style: buttonStyle, 'aria-haspopup': 'true', onClick: 'KaraokePluginSwitch();' }); //Control Panel let controlPanel, channelAdjustControl, highPassAdjustControl, lowPassAdjustControl, gainAdjustControl; let highPassAdjustDisplay, lowPassAdjustDisplay return { menuUI : function() { $(targetContainer).prepend(karaokeButton); }, controlPanelUI : function(channelAdjustedValue, highPassAdjustedValue, lowPassAdjustedValue, gainAdjustedValue) { let columnStyle = 'width:33%; display:inline-block;'; let titleStyle = ''; if (isYoutubeDarkTheme) { columnStyle += darkThemeTextColor; titleStyle = darkThemeTextColor; } controlPanel = $('<div>',{ id:"karaoke_controlpanel", }); controlPanel.append($('<h3>',{ text: _translate('title'), style: titleStyle })); channelAdjustControl = $('<input>',{ type: 'range', id: 'channelshift', min: 0, max: 3, value: channelAdjustedValue, step: 1, onchange: 'KaraokePluginChannelAdjust(this)' }); highPassAdjustControl = $('<input>',{ type: 'range', id: 'highpass', min: 50, max: 400, value: highPassAdjustedValue, step: 10, onchange: 'KaraokePluginHighPassAdjust(this)' }); lowPassAdjustControl = $('<input>',{ type: 'range', id: 'lowpass', min: 2000, max: 8000, value: lowPassAdjustedValue, step: 200, onchange: 'KaraokePluginLowPassAdjust(this)' }) gainAdjustControl = $('<input>',{ type: 'range', id: 'micgain', min: 0, max: 2, value: gainAdjustedValue, step: 0.1, onchange: 'KaraokePluginMicGainAdjust(this)' }) controlPanel.append( $('<div>',{style: columnStyle}). append('<label style="width:100px;">'+_translate('vocal_l1')+':</label><br />'). append('<label>'+_translate('vocal_l2')+'</label><br />'). append(channelAdjustControl). append('<br />'). append('<label style="width:100px;">'+_translate('high_pass')+': <span id="KaraokeHighPassValue">'+highPassAdjustedValue+'</span> Hz</label><br />'). append(highPassAdjustControl). append('<br />'). append('<label style="width:100px;">'+_translate('low_pass')+': <span id="KaraokeLowPassValue">'+lowPassAdjustedValue+'</span> Hz</label><br />'). append(lowPassAdjustControl) ); let secondColumn = $('<div>',{style: columnStyle}); secondColumn.append('<label style="width:100px;">'+_translate('mic_gain')+': <span id="KaraokeGainValue">'+gainAdjustedValue+'</span></label><br />'). append(gainAdjustControl). append('<p>'+_translate('mic_gain_desc')+'</p>'); controlPanel.append(secondColumn); if (isYoutubeDarkTheme) { controlPanel.insertBefore(youtubeDarkThemeUiAttachTo); } else { controlPanel.insertAfter(UiAttachTo); } highPassAdjustDisplay = $('#KaraokeHighPassValue'); lowPassAdjustDisplay = $('#KaraokeLowPassValue'); return controlPanel }, setKaraokeButtonOn: function() { karaokeButton.attr('title', _translate('on')); }, setKaraokeButtonOff: function() { karaokeButton.attr('title',_translate('off')); }, getChannelAdjustControl: function() { return channelAdjustControl }, getHighPassAdjustControl: function() { return highPassAdjustControl }, getLowPassAdjustControl: function() { return lowPassAdjustControl }, getHighPassAdjustDisplay: function() { return highPassAdjustDisplay; }, getLowPassAdjustDisplay: function() { return lowPassAdjustDisplay; } } }(jQuery) let KaraokePlugin = function ($, KaraokeUI) { const MAX_CACHE_SIZE = 5000; const MAX_RETRIES = 20; const TIME_INTERVAL = 1500; //webaudio elements let audioContext, audioSource,micAudioContext, micSource; let karaokeFilterOn = false; let channelAdjustedValue = 1, gainAdjustedValue = 1; let highPassAdjustedValue = 200, lowPassAdjustedValue = 6000 let trackSearchDialog = null; let _createBiquadFilter = function(type,freq,qValue) { let filter = audioContext.createBiquadFilter(); filter.type = type; filter.frequency.value = freq; filter.Q.value = qValue; return filter; } /** * Cut common vocal frequencies @ center * Algo origin: https://github.com/stanton119/YouTube-Karaoke */ let _cutCenterV1 = function() { //cutoff frequencies let f1 = highPassAdjustedValue; let f2 = lowPassAdjustedValue; console.log('setting center cut v1 @'+f1+' - '+f2); //splitter and gains let splitter, gainL, gainR; //biquadFilters let filterLP1, filterHP1, filterLP2, filterHP2; let filterLP3, filterHP3, filterLP4, filterHP4; //phase inversion filter splitter = audioContext.createChannelSplitter(2); gainL = audioContext.createGain(); gainR = audioContext.createGain(); gainL.gain.value = 1; gainR.gain.value = -1; splitter.connect(gainL, 0); splitter.connect(gainR, 1); gainL.connect(audioContext.destination); gainR.connect(audioContext.destination); //biquad filters filterLP1 = _createBiquadFilter("lowpass",f2,1); filterLP2 = _createBiquadFilter("lowpass",f1,1); filterLP3 = _createBiquadFilter("lowpass",f2,1); filterLP4 = _createBiquadFilter("lowpass",f1,1); filterHP1 = _createBiquadFilter("highpass",f1,1); filterHP2 = _createBiquadFilter("highpass",f2,1); filterHP3 = _createBiquadFilter("highpass",f1,1); filterHP4 = _createBiquadFilter("highpass",f2,1); //connect filters audioSource.connect(filterLP1); audioSource.connect(filterLP2); audioSource.connect(filterHP2); filterLP1.connect(filterLP3); filterLP3.connect(filterHP1); filterHP1.connect(filterHP3); filterHP3.connect(splitter); filterLP2.connect(filterLP4); filterLP4.connect(audioContext.destination); filterHP2.connect(filterHP4); filterHP4.connect(audioContext.destination); } /** * Cut common vocal frequencies @ center with preserve stereo field * Algo origin: https://github.com/stanton119/YouTube-Karaoke */ let _cutCenterV2 = function() { //cutoff frequencies let f1 = highPassAdjustedValue; let f2 = lowPassAdjustedValue; console.log('setting center cut with stereo field @'+f1+' - '+f2); // stereo conversion let merger = audioContext.createChannelMerger(2); merger.connect(audioContext.destination); // L_Out = (Mid+side)/2 let gainNodeMS1_05 = audioContext.createGain(); gainNodeMS1_05.gain.value = 0.5; gainNodeMS1_05.connect(merger,0,0); // R_Out = (Mid-side)/2 let gainNodeMS2_05 = audioContext.createGain(); gainNodeMS2_05.gain.value = 0.5; gainNodeMS2_05.connect(merger,0,1); let gainNodeS_1 = audioContext.createGain(); gainNodeS_1.gain.value = -1; gainNodeS_1.connect(gainNodeMS2_05); // create band stop filter using two cascaded biquads // inputs -> FilterLP1 & FilterLP2 // outputs -> splitter & destinations // Bandstop filter = LP + HP let FilterLP1 = _createBiquadFilter('lowpass', f1, 1); let FilterLP2 = _createBiquadFilter('lowpass', f1, 1); FilterLP1.connect(FilterLP2); let FilterHP1 = _createBiquadFilter('highpass', f2, 1); let FilterHP2 = _createBiquadFilter('highpass', f2, 1); FilterHP1.connect(FilterHP2); // connect filters to left and right outputs FilterLP2.connect(gainNodeMS1_05); FilterHP2.connect(gainNodeMS1_05); FilterLP2.connect(gainNodeMS2_05); FilterHP2.connect(gainNodeMS2_05); // band pass with gain, adds mids into the side channel let gainNodeBP = audioContext.createGain(); gainNodeBP.gain.value = 1; let FilterBP1 = _createBiquadFilter('lowpass', f2, 1); let FilterBP2 = _createBiquadFilter('lowpass', f2, 1); FilterBP2.connect(FilterBP1); let FilterBP3 = _createBiquadFilter('highpass', f1, 1); FilterBP3.connect(FilterBP2); let FilterBP4 = _createBiquadFilter('highpass', f1, 1); FilterBP4.connect(FilterBP3); FilterBP1.connect(gainNodeBP); gainNodeBP.connect(gainNodeS_1); gainNodeBP.connect(gainNodeMS1_05); // mid-side conversion // split into L/R let splitter = audioContext.createChannelSplitter(2); // mid = L+R splitter.connect(FilterLP1,0); // // L->filter splitter.connect(FilterHP1,0); splitter.connect(FilterLP1,1); // R->filter splitter.connect(FilterHP1,1); // side = L-R, 2 outputs, 2 destinations let gainNodeR_1 = audioContext.createGain(); gainNodeR_1.gain.value = -1; splitter.connect(gainNodeR_1,1); gainNodeR_1.connect(gainNodeS_1); splitter.connect(gainNodeS_1,0); gainNodeR_1.connect(gainNodeMS1_05); splitter.connect(gainNodeMS1_05,0); gainNodeR_1.connect(FilterBP4); splitter.connect(FilterBP4,0); audioSource.connect(splitter); } /** * Expand left channel to both channel, drop right channel */ let _cutRight = function() { console.log('setting right cut'); let splitter, merger; splitter = audioContext.createChannelSplitter(2); merger = audioContext.createChannelMerger(1); splitter.connect(merger, 0); audioSource.connect(splitter); merger.connect(audioContext.destination); } /** * Expand right channel to both channel, drop left channel */ let _cutLeft = function() { console.log('setting left cut'); let splitter,merger; splitter = audioContext.createChannelSplitter(2); merger = audioContext.createChannelMerger(1); splitter.connect(merger, 1); audioSource.connect(splitter); merger.connect(audioContext.destination); } /** * Handle Microphone gain. This only applicable to mic that connected to browser. * @param amount * @private */ let _micGain = function(amount) { let gainElement = $('#KaraokeGainValue') gainElement.html(amount); console.log(gainElement.html()); micSource.disconnect(); let micGain = micAudioContext.createGain(); micSource.connect( micGain ); micGain.connect( micAudioContext.destination ); micGain.gain.value = amount; micSource.connect( micAudioContext.destination ); } /** * 0 = left cut, 1 = center cut v2, 2 = center cut v1, 2 = right cut **/ let _adjustChannel = function() { console.log('channelAdjust:'+channelAdjustedValue); _disconnectProcessors(); switch(channelAdjustedValue) { case 0: _cutLeft(); break; case 1: _cutCenterV2(); break; case 2: _cutCenterV1(); break; case 3: _cutRight(); break; } _saveSetting(); } let _disconnectProcessors = function() { console.log('disconnect audio processors'); audioSource.disconnect(); } let _getSongId = function() { return getSongId(); } let _loadSetting = function() { let songId = _getSongId(); if(typeof songId === undefined || songId === null) { return; } let localSetting = localStorage.getItem(songId); let savedItem = null; if(localSetting !== null) { savedItem = JSON.parse(localSetting); } console.log("Loading "+songId, savedItem); if(savedItem !== null) { touchLocalStorage(songId, savedItem); } } let touchLocalStorage = function(songId, savedItem) { channelAdjustedValue = savedItem.cv; lowPassAdjustedValue = savedItem.lpv; highPassAdjustedValue = savedItem.hpv; savedItem.date = Date.now(); localStorage.setItem(songId, JSON.stringify(savedItem)); } let _readjustControls = function() { KaraokeUI.getChannelAdjustControl().val(channelAdjustedValue); KaraokeUI.getHighPassAdjustControl().val(highPassAdjustedValue); KaraokeUI.getLowPassAdjustControl().val(lowPassAdjustedValue); KaraokeUI.getHighPassAdjustDisplay().html(highPassAdjustedValue.toString()) KaraokeUI.getLowPassAdjustDisplay().html(lowPassAdjustedValue.toString()) } let _saveSetting = function() { let songId = _getSongId(); if(songId === null) { return; } let data = { cv: channelAdjustedValue, lpv: lowPassAdjustedValue, hpv: highPassAdjustedValue, date: Date.now() } console.log('Saving Setting: '+songId, data) localStorage.setItem(songId, JSON.stringify(data)); _trimCache(); } let _trimCache = function() { if(localStorage.length > MAX_CACHE_SIZE) { let sortableArray = []; for (let i = 0; i < localStorage.length; i++) { let jsonItem = localStorage.getItem(localStorage.key(i)); let item = JSON.parse(jsonItem); if(typeof item.cv !== undefined) { sortableArray[localStorage.key(i)] = { key: localStorage.key(i), data: JSON.parse(localStorage.getItem(localStorage.key(i))) }; } } sortableArray.sort((a, b) => (a.data.date > b.data.date) ? 1 : -1); for (let i = 0; i < MAX_CACHE_SIZE/5; i++) { localStorage.removeItem(sortableArray[i].key); } } } let _connectAudio = function(element) { //setup audio routing try { window.AudioContext = window.AudioContext || window.webkitAudioContext; audioContext = new AudioContext(); audioSource = audioContext.createMediaElementSource(element); audioSource.connect(audioContext.destination); } catch (e) { console.error('Media element not found.'); console.error(e.message); } } let _getVideoElement = function(mediaElement) { let element = $(mediaElement) if (typeof $(mediaElement)[0] !== 'undefined') { element = $(mediaElement)[0] } return element; } return { setupAudioSource : function () { if(typeof _getVideoElement(mediaElement).tagName === 'undefined') { console.log('audio connecting via interval'); var retries = 0; var intervalId = setInterval(function() { console.log('audio connect retry: '+retries); if(retries > 10) { clearInterval(intervalId); return this; } console.log(_getVideoElement(mediaElement)); if(_getVideoElement(mediaElement).tagName === 'VIDEO') { console.log('audio connected'); _connectAudio(_getVideoElement(mediaElement)); clearInterval(intervalId); return this; } retries++; }, TIME_INTERVAL); } else { console.log('audio connected immediately'); _connectAudio(_getVideoElement(mediaElement)); } return this; }, setupMic: function() { navigator.mediaDevices.getUserMedia({ audio: true }) .then(function(stream) { /* use the stream */ window.AudioContext = window.AudioContext || window.webkitAudioContext; micAudioContext = new AudioContext(); console.log('Mic Latency:'+micAudioContext.baseLatency); // Create an AudioNode from the stream. micSource = micAudioContext.createMediaStreamSource( stream ); // Connect it to the destination to hear yourself (or any other node for processing!) micSource.connect( micAudioContext.destination ); }) .catch(function(err) { /* handle the error */ }); return this; }, setupMenu: function() { if($(targetContainer).length === 0) { console.log('menu connecting via interval'); var retries = 0; var intervalId = setInterval(function() { console.log('menu retry: '+retries); if(retries > 10) { clearInterval(intervalId); return this; } if($(targetContainer).length > 0) { console.log('audio connected'); KaraokeUI.menuUI(); clearInterval(intervalId); return this; } retries++; }, TIME_INTERVAL); } else { console.log('menu connected immediately'); KaraokeUI.menuUI(); } }, filterOn: function() { console.log("Removing vocals"); _adjustChannel(); return this; }, filterOff: function() { console.log("Adding in vocals"); _disconnectProcessors(); audioSource.connect(audioContext.destination); return this; }, switch: function() { if(karaokeFilterOn) { karaokeFilterOn = false; this.filterOff(); KaraokeUI.setKaraokeButtonOff(); this.removeControlPanel(); } else { karaokeFilterOn = true; this.filterOn(); KaraokeUI.setKaraokeButtonOn(); this.showControlPanel(); } return this; }, showControlPanel: function() { console.log('showpanel'); this.controlPanel = KaraokeUI.controlPanelUI(channelAdjustedValue, highPassAdjustedValue, lowPassAdjustedValue, gainAdjustedValue); _loadSetting(); return this; }, removeControlPanel: function() { console.log('hidepanel'); this.controlPanel.remove(); return this; }, isFilterOn: function() { return karaokeFilterOn; }, micGainAdjust: function(element) { gainAdjustedValue = $(element).val(); _micGain(gainAdjustedValue); return this; }, channelAdjust: function(element) { channelAdjustedValue = parseInt($(element).val()); _adjustChannel(); return this; }, highPassAdjust: function(element) { highPassAdjustedValue = parseInt($(element).val()); KaraokeUI.getHighPassAdjustDisplay().html(highPassAdjustedValue.toString()); _adjustChannel() return this; }, lowPassAdjust: function(element) { lowPassAdjustedValue = parseInt($(element).val()); KaraokeUI.getLowPassAdjustDisplay().html(lowPassAdjustedValue.toString()); _adjustChannel() return this; }, loadSetting: function() { _loadSetting(); } }; }(jQuery, KaraokeUI); if (typeof audioContext === 'undefined') { console.log(mediaElement); console.log(targetContainer); console.log(UiAttachTo); console.log("Loading setting"); KaraokePlugin.loadSetting(); console.log("setting up mic"); KaraokePlugin.setupMic(); console.log("setting up audio source"); KaraokePlugin.setupAudioSource(mediaElement); console.log("setting up menu"); KaraokePlugin.setupMenu(targetContainer); unsafeWindow.KaraokePluginSwitch = function() { KaraokePlugin.switch(); } unsafeWindow.KaraokePluginMicGainAdjust = function(element) { KaraokePlugin.micGainAdjust(element); } unsafeWindow.KaraokePluginChannelAdjust = function(element) { KaraokePlugin.channelAdjust(element); } unsafeWindow.KaraokePluginHighPassAdjust = function(element) { KaraokePlugin.highPassAdjust(element); } unsafeWindow.KaraokePluginLowPassAdjust = function(element) { KaraokePlugin.lowPassAdjust(element); } } window.addEventListener("popstate", (event) => { console.log('Event: popstate, reload setting'); KaraokePlugin.loadSetting(); if(KaraokePlugin.isFilterOn()) { KaraokePlugin.switch(); KaraokePlugin.switch(); } }); if (window.onurlchange === null) { console.log('Url Change Event. Setup'); window.addEventListener('urlchange', (info) => { console.log('Url Changed, reload setting.'); if (window.location.href.includes(urlChangePattern)) { KaraokePlugin.loadSetting(); if(KaraokePlugin.isFilterOn()) { KaraokePlugin.switch(); KaraokePlugin.switch(); } } }); } })(jQuery, md5);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址