Youtube HTML5 Karaoke

HTML5 Karaoke Vocal Control, support center channel cut on regular MV, left/right channel vocal/instrumental mixed MVs. Support: Youtube and Bilibili

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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