epianoOptimization

epianoMIDI機能を最適化します。ベロシティ調整、遅延の減少、エンベロープの編集、コード検出機能、その他軽量化など

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         epianoOptimization
// @namespace    https://epiano.jp/
// @version      0.1
// @description epianoMIDI機能を最適化します。ベロシティ調整、遅延の減少、エンベロープの編集、コード検出機能、その他軽量化など
// @match        https://epiano.jp/sp/*
// @grant        none
// @run-at document-start
// @noframes
// @description epiano
// ==/UserScript==

window.MIDIACCESS_BACK = navigator.requestMIDIAccess;
navigator.requestMIDIAccess = null;
window.ICONSIZE = 20;

function releaseBuffer(key, uid) {
    const id = uid + key;

    if (id in playings) {
        for (i in playings[id]) {
            const ar = playings[id][i];
            const time = window.context.currentTime;
            const source = ar[0]
            const gain = ar[1];

            gain.setValueAtTime(gain.value, time);
            gain.linearRampToValueAtTime(0, time + window.PIANORELEASE);
            source.stop(time + window.PIANORELEASE);
        }

        delete playings[id];
    }
}

function playBuffer(key, vol, uid) {
    if (isNaN(vol)) {
        vol = DEFAULT_VELOCITY;
    }
    if (key in buffers) {
        const source = window.context.createBufferSource();
        source.buffer = buffers[key];

        const gainNode = window.context.createGain();
        gainNode.connect(window.context.destination);
        gainNode.gain.value = 0;
        gainNode.gain.linearRampToValueAtTime(vol * window.PIANOVOLUME, window.context.currentTime + window.PIANOATTACK);
        gainNode.gain.linearRampToValueAtTime(vol * window.PIANOVOLUME * window.PIANOSUSTAIN, window.context.currentTime + window.PIANOATTACK + window.PIANODECAY);

        source.connect(gainNode);
        source.start(0);

        if (!playings[uid + key]) {
            playings[uid + key] = [];
        }
        playings[uid + key].push([source, gainNode.gain]);
    }
}

function ease(t, totalTime, min, max) {
    max -= min;
    t = t / totalTime;
    if (t < 0.43) {
        return max * (t / 2.3) + min;
    }

    return max * t * t + min;
}


function press(id, vol) {
    //held
    const newVol = ease(vol, 1.5, 0.0, 2.0) * window.SELFVOLUME;
    gHeldNotes[id] = true;
    gSustainedNotes[id] = true;

    playPiano(id, loginInfo.myID, newVol);

    if (!$("#disconnect").is(":checked")) {
        socket.emit('p', id, newVol);
    }

    window.HOLD_KEY_ID.add(window.MIDI_KEY_NAMES.indexOf(id));
    if (window.CHORDVIEWMODE) {
        setTimeout(displayChordView, 0);
    }
}

function release(id) {
    if (gHeldNotes[id]) {
        delete gHeldNotes[id];
        if (gAutoSustain || gSustain) {
            gSustainedNotes[id] = true;
        } else {
            gSustainedNotes[id] = false;
            releasePiano(id, loginInfo.myID);

            if (!$("#disconnect").is(":checked")) {
                socket.emit('r', id);
            }
            window.HOLD_KEY_ID.delete(window.MIDI_KEY_NAMES.indexOf(id));
        }
    }
}

function playPiano(id, uid, vol) {
    if (ignoreList[uid]) {
        return;
    }

    var newVol = vol;
    // 音量制限
    if (newVol > 2.0 || newVol < -1) {
        newVol = 2.0;
    }
    playBuffer(id, newVol, uid);

    // ユーザーID点滅
    const pikaColor = uid == loginInfo.myID ? "#00FF00" : "#FF0000";
    if (!window.DISCONNECT.is(":checked")) {
        $("[uid=" + uid + "]").css("backgroundColor", pikaColor)
            .stop(true)
            .show()
            .animate({ top: "3px" }, 100)
            .animate({ top: "0px" }, 50)
            .queue(function () {
                $(this).css("backgroundColor", "").dequeue();
            });
    }

    // 鍵盤点滅
    const pikaID = window.MIDI_KEY_NAMES.indexOf(id) + 21;
    const originalColor = $('#' + pikaID).attr("class").split(" ")[0] == "whiteKey" ? "white" : "#333";
    $('#' + pikaID).css("backgroundColor", pikaColor)
        .delay(100)
        .queue(function () {
            $(this).css("backgroundColor", originalColor).dequeue();
        });
}


// ============= チャット関連 ==================
function displayChordView() {
    let notes = [];
    for (let id of window.HOLD_KEY_ID) {
        notes.push(id - 3);
    }
    notes.sort((a, b) => a - b);
    for (let i = 0; i < notes.length; ++i) {
        notes[i] = modC(notes[i], 12);
    }
    let chord = Chord.noteToChord(notes);

    if (chord) {
        $("#chordViewText").text(chord.getChordText());
    }
    else
    {
        $("#chordViewText").text("");
    }
}

function displayChord(chord) {
    let chordObj = new Chord(chord);

    let firstToneIndex = 60;
    let notes = chordObj.getNotes();
    let toneIndex = 0;

    if (chordObj.isOnChord()) {
        let bassNote = notes.pop();
        notes.unshift(bassNote);
    }

    for (let note of notes) {
        let pikaID = firstToneIndex + note.getNoteIndex();
        if (note.getNoteIndex() < toneIndex) {
            pikaID += 12;
        }
        else {
            toneIndex = note.getNoteIndex();
        }
        const pikaColor = "#C030C0";
        const originalColor = $('#' + pikaID).attr("class").split(" ")[0] == "whiteKey" ? "white" : "#333";
        $('#' + pikaID).css("backgroundColor", pikaColor)
            .delay(1000)
            .queue(function () {
                $(this).css("backgroundColor", originalColor).dequeue();
            });
    }
}

function html_parse(html) {
    var text = html;
    let excludeText = text.replace(/https:\/\/i.imgur.com\/([^\.]+)\.(png|jpg|gif)/g, 'imgur');
    excludeText = excludeText.replace(/https:\/\/(?:www|\m).youtube.com\/watch\?v=([\d\w_\-]+)(?:&[;\?a-z\.\&=]+|)/g, 'youtube');
    excludeText = excludeText.replace(/https:\/\/youtu.be\/([\d\w_\-]+)/g, 'youtube');

    text = text.replace(/https:\/\/i.imgur.com\/([^\.]+)\.(png|jpg|gif)/g, '[imgur:$1:$2]');
    text = text.replace(/https:\/\/(?:www|\m).youtube.com\/watch\?v=([\d\w_\-]+)(?:&[;\?a-z\.\&=]+|)/g, '[youtube:$1]');
    text = text.replace(/https:\/\/youtu.be\/([\d\w_\-]+)/g, '[youtube:$1]');

    let chordsDiv = [];
    if (excludeText.indexOf("imgur") == -1 && excludeText.indexOf("youtube")) {
        // 和音を検出する
        text = text.replace(/\[.+?\]/, function ($1) {
            var chord = $1.replace("[", "").replace("]", "");
            var chordDiv = '<div chord="' + chord + '" class="chord" style="padding: 5px; margin-bottom: 5px; border: 1px dotted #333333; border-radius: 5px; background-color: #009999; color: #ffffff; display: inline-block;">' + chord + '</div>';

            chordsDiv.push(chordDiv);
            return "";
        });
    }

    text = text.replace(/((http|https|ftp):\/\/[\w?=&.\/-;#~%-]+(?![\w\s?&.\/;#~%"=-]*>))/g, function ($1) {
        var url = $1;
        var text = url.replace(/https:\/\/|www\./g, "");

        return '<a target=_blank href="' + url + '"><div class=url_wrap><i class="fa fa-external-link" aria-hidden="true"></i>' + text + '</div></a>'
    });

    text = text.replace(/\[youtube:[^\]]+]/g, function ($1) {
        var val = $1.match(/\[(.*)\]/)[1].split(/:/);
        var vid = val[1];
        var thumb = "http://img.youtube.com/vi/" + vid + "/default.jpg";
        var url = "https://www.youtube.com/watch?v=" + vid;
        return '<div class=youtube_wrap><i class="fa fa-play youtubePlayButton" aria-hidden="true"></i>' +
            '<a class=youtube href="' + url + '" vid="' + vid + '"><img style="height:40px" src=' + thumb + '></a></div>';
    });


    text = text.replace(/\[imgur:[^\]]+]/g, function ($1) {
        var val = $1.match(/\[(.*)\]/)[1].split(/:/);
        var imid = val[1];
        var kaku = val[2];
        var thumb = "https://i.imgur.com/" + imid + "s." + kaku;
        var url = "https://i.imgur.com/" + imid + "." + kaku;
        return '<a class=imgur data-lightbox="imgur" title="<a href=# class=share_pic>コラボする</a>" href=' + url + '><img style="height:40px" src=' + thumb + '></a>';
    });

    return [text, chordsDiv];
}

function insertTalk(obj) {
    var _text = obj.comment;
    var uid = obj.uid;

    var twid = obj.twid;
    var icon = obj.icon;

    var time = obj.time;

    if (nowRoom.match(/anon/)) {
        twid = "";
        icon = "";
    }

    var values = html_parse(_text);
    let text = values[0];
    let chords = values[1];

    var turl = "https://twitter.com/" + twid;

    var new_line;

    if (icon) {
        new_line = $("<div time=" + time + " class='ll anon_line lines hide " + uid + "'>" +
            ("<table><tr valign=top><td align=center>" +
                "<a href='" + turl + "' target=_blank><img class=icon border=0 width=" + window.ICONSIZE + " height=" + window.ICONSIZE + " src=" + icon + "></a><br>") +
            "<div style='color:#" + uid + ";font-size:7pt;' val=" + uid + " class='chart_uid'><b>" + uid + "</b></div>" +
            "</td><td>" +
            '<div class="huki_wrap"><div class="huki">' + text + "</div></div>" +
            "<font color=#999 style='font-size:5pt'>&nbsp;&nbsp;" + time2date(time) + " </font>" +
            "</td></table>" + "</div>");

    } else {
        new_line = $("<div time=" + time + " class='ll lines hide " + uid + "'>" +
            "<b style='word-wrap: normal;color:#888' val=" + uid + " class='chart_uid'>" + uid + ":</b>" + text +
            "<font color=#999 style='font-size:5pt'>&nbsp;" + time2date(time) + " </font>" +
            "</div>");
    }


    if (new String(text).match(new RegExp("&gt;" + loginInfo.myID))) {
        new_line.addClass("get_anka");
    }

    if (uid == loginInfo.myID) {
        new_line.find(".chart_uid").addClass("self");
    }

    for (let chord of chords) {
        var chordDiv = $(chord);
        chordDiv.on('mouseover', function () {
            $(this).css("backgroundColor", '#008888');
        })
        chordDiv.on('mouseleave', function () {
            $(this).css("backgroundColor", '#009999');
        })
        chordDiv.on('click', function () {
            displayChord(chordDiv.attr("chord"));
        });

        if (icon) {
            chordDiv.attr("width", "100%");
            new_line.find("div .huki").append(chordDiv);
        }
        else {
            chordDiv.attr("width", "5%");
            new_line.find("b").after(chordDiv);
        }

    }


    if (obj.is_log) {
        $("#chat").prepend(new_line);
    } else {
        $("#chat").append(new_line);
    }



    $(new_line).slideDown("fast", function () { $(this).removeClass("hide") });


    if ($("#chat").position().top < 0) {
        $("#chat div:first").remove();
    }

    if ($("#chat").hasClass("log_done")) {
        var now = $("#chat").scrollTop();
        var max = $("#chat").get(0).scrollHeight - $("#chat").outerHeight();;
        var per = parseInt(now / max * 100);
        if ($(".ll").length > 20) {
            if (per > 90) {
                setTimeout(function () {
                    setScrollMax();
                }, 500);
            }
        } else {
            setScrollMax();
        }
    }
}

function updateList(list) {
    window.ONLINE_USERS = list;

    for (let user of window.ONLINE_USERS) {
        if (!(user in window.USER_PIANO_VOLUME)) {
            window.USER_PIANO_VOLUME[user] = 1.0;
        }
    }
    var htmls = new Array();
    $(list).each(function (i) {
        var uid = list[i];
        var isIgnore = ignoreList[uid] ? "class='ignore' style='text-decoration:line-through;background:#999'" : "";
        htmls.push("<span class=user" + isIgnore + " uid=" + uid + ">" + uid + "</span>");
    });
    $("#users").html(htmls.join(""));
    $("#onlineNumber").html(htmls.length);
}



// ============== MIDI関連 =====================

// inputのmidiメッセージの処理関数
function inputMidiMessageHandler(evt) {
    let note_number = evt.data[1] + window.PIANOTRANSPOSE - 21;
    switch (evt.data[0] & 0xf0) {
        // NOTE_ON
        case 0x90:
            if (evt.data[2] > 0) {
                press(window.MIDI_KEY_NAMES[note_number], evt.data[2] / 127.0);
            }
            else {
                release(window.MIDI_KEY_NAMES[note_number]);
            }
            return;
        // NOTE_OFF
        case 0x80:
            release(window.MIDI_KEY_NAMES[note_number]);
            return;

        case 0xB0:
            if (evt.data[1] == 64) {
                if (evt.data[2] > 0) {
                    $(document).trigger("pressSustain");
                } else {
                    $(document).trigger("releaseSustain");
                }
            }
            return;
    }
}

// MIDI入力と出力を列挙して追加する。
function addMIDIInputsOutputs(midi) {
    window.MIDI_INPUTS = [];
    window.MIDI_OUTPUTS = [];

    if (midi.inputs.size > 0) {
        // 入力MIDIデバイスの記録
        var it = midi.inputs.values();
        for (var input = it.next(); !input.done; input = it.next()) {
            window.MIDI_INPUTS.push(input.value);
        }
    }
    if (midi.outputs.size > 0) {
        // 出力MIDIデバイスの記録
        var ot = midi.outputs.values();
        for (var output = ot.next(); !output.done; output = ot.next()) {
            window.MIDI_OUTPUTS.push(output.value);
        }
    }
}

function updateInputsOutputs() {
    for (let input of window.MIDI_INPUTS) {
        // デフォルト設定
        if (input.name == window.selectInputMidiName) {
            console.log("MIDI INPUT " + window.selectInputMidiName + "を使用します。");
            input.onmidimessage = inputMidiMessageHandler;
        }
        else {
            input.onmidimessage = null;
        }
    }

    for (let output of window.MIDI_OUTPUTS) {
        if (output.name == window.selectOutputMidiName) {
            console.log("MIDI OUTPUT " + window.selectOutputMidiName + "を使用します。");
            window.MIDI_OUTPUT = output;
        }
        else {
            output.onmidimessage = null;
        }
    }
}

// MIDI接続成功時
function successCallback(midi) {
    window.MIDIDevice = midi;

    midi.addEventListener("statechange", function (evt) {
        if (evt instanceof MIDIConnectionEvent) {
            addMIDIInputsOutputs(midi);
            updateInputsOutputs();
        }
    });

    addMIDIInputsOutputs(midi);
    updateInputsOutputs();
}

// MIDI接続失敗時
function faildCallback(msg) {
    console.log("[Error]:" + msg);
}


// =========== 和音関連 ============
String.prototype.findFirstOf = function (chars, start) {
    var idx = -1;
    [].some.call(this.slice(start || 0), function (c, i) {
        if (chars.indexOf(c) >= 0)
            return idx = i, true;
    });
    return idx >= 0 ? idx + (start || 0) : -1;
}
function array_equal(a, b) {
    if (!Array.isArray(a)) return false;
    if (!Array.isArray(b)) return false;
    if (a.length != b.length) return false;
    for (var i = 0, n = a.length; i < n; ++i) {
        if (a[i] !== b[i]) return false;
    }
    return true;
}
window.C_MAJOR_SCALE = ["C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
window.C_SHARP_MAJOR_SCALE = ["C#", "D", "D#", "E", "E#", "F#", "F##", "G#", "A", "A#", "B", "B#"];
window.D_FLAT_MAJOR_SCALE = ["Db", "Ebb", "Eb", "Fb", "F", "Gb", "G", "Ab", "Bbb", "Bb", "Cb", "C"];
window.D_MAJOR_SCALE = ["D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B", "C", "C#"];
window.E_FLAT_MAJOR_SCALE = ["Eb", "Fb", "F", "Gb", "G", "Ab", "A", "Bb", "Cb", "C", "Db", "D"];
window.E_MAJOR_SCALE = ["E", "F", "F#", "G", "G#", "A", "A#", "B", "C", "C#", "D", "D#"];
window.F_MAJOR_SCALE = ["F", "Gb", "G", "Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E"];
window.F_SHARP_MAJOR_SCALE = ["F#", "G", "G#", "A", "A#", "B", "B#", "C#", "D", "D#", "E", "E#"];
window.G_FLAT_MAJOR_SCALE = ["Gb", "Abb", "Ab", "Bbb", "Bb", "Cb", "C", "Db", "Ebb", "Eb", "Fb", "F"];
window.G_MAJOR_SCALE = ["G", "Ab", "A", "Bb", "B", "C", "C#", "D", "Eb", "E", "F", "F#"];
window.A_FLAT_MAJOR_SCALE = ["Ab", "Bbb", "Bb", "Cb", "C", "Db", "D", "Eb", "Fb", "F", "Gb", "G"];
window.A_MAJOR_SCALE = ["A", "Bb", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"];
window.B_FLAT_MAJOR_SCALE = ["Bb", "Cb", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A"];
window.B_MAJOR_SCALE = ["B", "C", "C#", "D", "D#", "E", "E#", "F#", "G", "G#", "A", "A#"];
window.C_FLAT_MAJOR_SCALE = ["Cb", "Dbb", "Ebb", "Eb", "Fb", "F", "Gb", "Abb", "Ab", "Bbb", "Bb"];

window.A_MINOR_SCALE = ["A", "Bb", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"];
window.E_MINOR_SCALE = ["E", "F", "F#", "G", "G#", "A", "A#", "B", "C", "C#", "D", "D#"];
window.B_MINOR_SCALE = ["B", "C", "C#", "D", "D#", "E", "E#", "F#", "G", "G#", "A", "A#"];
window.F_SHARP_MINOR_SCALE = ["F#", "G", "G#", "A", "A#", "B", "B#", "C#", "D", "D#", "E", "E#"];
window.C_SHARP_MINOR_SCALE = ["C#", "D", "D#", "E", "E#", "F#", "F##", "G#", "A", "A#", "B", "B#"];
window.G_SHARP_MINOR_SCALE = ["G#", "A", "A#", "B", "B#", "C#", "C##", "D#", "E", "E#", "F#", "F##"];
window.D_SHARP_MINOR_SCALE = ["D#", "E", "E#", "F#", "F##", "G#", "G##", "A#", "B", "B#", "C#", "C##"];
window.A_SHARP_MINOR_SCALE = ["A#", "B", "B#", "C#", "C##", "D#", "D##", "E#", "F#", "F##", "G#", "G##"];
window.D_MINOR_SCALE = ["D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B", "C", "C#"];
window.G_MINOR_SCALE = ["G", "Ab", "A", "Bb", "B", "C", "C#", "D", "Eb", "E", "F", "F#"];
window.C_MINOR_SCALE = ["C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
window.F_MINOR_SCALE = ["F", "Gb", "G", "Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E"];
window.B_FLAT_MINOR_SCALE = ["Bb", "Cb", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A"];
window.E_FLAT_MINOR_SCALE = ["Eb", "Fb", "F", "Gb", "G", "Ab", "A", "Bb", "Cb", "C", "Db", "D"];
window.A_FLAT_MINOR_SCALE = ["Ab", "Bbb", "Bb", "Cb", "C", "Db", "D", "Eb", "Fb", "F", "Gb", "G"];

window.TONE_MAP = { "C": 0, "Db": 1, "C#": 1, "D": 2, "Eb": 3, "D#": 3, "E": 4, "F": 5, "Gb": 6, "F#": 6, "G": 7, "Ab": 8, "G#": 8, "A": 9, "Bb": 10, "A#": 10, "B": 11 };
window.FLAT_SCALE_MAP = { 0: "C", 1: "Db", 2: "D", 3: "Eb", 4: "E", 5: "F", 6: "Gb", 7: "G", 8: "Ab", 9: "A", 10: "Bb", 11: "B" };
window.CHORD_MAP = {
    "5": [0, 7],
    "": [0, 4, 7],
    "m": [0, 3, 7],
    "-5": [0, 4, 6],
    "dim": [0, 3, 6],
    "aug": [0, 4, 8],
    "sus2": [0, 2, 7],
    "sus4": [0, 5, 7],
    "6": [0, 4, 7, 9],
    "7": [0, 4, 7, 10],
    "M7": [0, 4, 7, 11],
    "m6": [0, 3, 7, 9],
    "m7": [0, 3, 7, 10],
    "mM7": [0, 3, 7, 11],
    "7-5": [0, 4, 6, 10],
    "M7-5": [0, 4, 6, 11],
    "m7-5": [0, 3, 6, 10],
    "mM7-5": [0, 3, 6, 11],
    "aug7": [0, 4, 8, 10],
    "augM7": [0, 4, 8, 11],
    "aug(b9)": [0, 4, 8, 1],
    "7sus4": [0, 5, 7, 10],
    "dim7": [0, 3, 6, 9],
    "add9": [0, 4, 7, 2],
    "add11": [0, 4, 7, 5],
    "madd9": [0, 3, 7, 2],
    "69": [0, 4, 7, 9, 2],
    "7(9)": [0, 4, 7, 10, 2],
    "7(13)": [0, 4, 7, 10, 9],
    "7(b9)": [0, 4, 7, 10, 1],
    "7(#9)": [0, 4, 7, 10, 3],
    "7(#11)": [0, 4, 7, 10, 6],
    "7(b13)": [0, 4, 7, 10, 8],
    "7-5(9)": [0, 4, 6, 10, 2],
    "7-5(#9)": [0, 4, 6, 10, 3],
    "7-5(b13)": [0, 4, 6, 10, 8],
    "M7(9)": [0, 4, 7, 11, 2],
    "M7(13)": [0, 4, 7, 11, 9],
    "M7(#9)": [0, 4, 7, 11, 3],
    "M7(#11)": [0, 4, 7, 11, 6],
    "M7(b9)": [0, 4, 7, 11, 1],
    "m69": [0, 3, 7, 9, 2],
    "m7(9)": [0, 3, 7, 10, 2],
    "m7(11)": [0, 3, 7, 10, 5],
    "m7(13)": [0, 3, 7, 10, 9],
    "m7(b9)": [0, 3, 7, 10, 1],
    "m7-5(9)": [0, 3, 6, 10, 2],
    "m7-5(11)": [0, 3, 6, 10, 5],
    "mM7(9)": [0, 3, 7, 11, 2],
    "mM7(13)": [0, 3, 7, 11, 9],
    "aug7(9)": [0, 4, 8, 10, 2],
    "augM7(#9)": [0, 4, 8, 11, 3],
    "augM7(9)": [0, 4, 8, 11, 2],
    "7(9, 11)": [0, 4, 7, 10, 2, 5],
    "7(9, 13)": [0, 4, 7, 10, 2, 9],
    "7(9, b13)": [0, 4, 7, 10, 2, 8],
    "7(9, #11)": [0, 4, 7, 10, 2, 6],
    "7(b9, 13)": [0, 4, 7, 10, 1, 9],
    "7(b9, b13)": [0, 4, 7, 10, 1, 8],
    "7(b9, #9)": [0, 4, 7, 10, 1, 3],
    "7(b9, #11)": [0, 4, 7, 10, 1, 6],
    "7(#9, 13)": [0, 4, 7, 10, 3, 9],
    "7(#9, b13)": [0, 4, 7, 10, 3, 8],
    "7(#9, #11)": [0, 4, 7, 10, 3, 6],
    "7(#11, 13)": [0, 4, 7, 10, 6, 9],
    "m7(9, 11)": [0, 3, 7, 10, 2, 5],
    "m7(9, 13)": [0, 3, 7, 10, 2, 9],
    "M7(9, 11)": [0, 4, 7, 11, 2, 5],
    "M7(9, 13)": [0, 4, 7, 11, 2, 9],
    "M7(9, #11)": [0, 4, 7, 11, 2, 6],
    "mM7(9, 13)": [0, 3, 7, 11, 2, 9],
    "7(9, 11, 13)": [0, 4, 7, 10, 2, 5, 9],
    "7(9, #11, 13)": [0, 4, 7, 10, 2, 6, 9],
    "7(b9, #11, 13)": [0, 4, 7, 10, 1, 6, 9],
    "m7(9, 11, 13)": [0, 3, 7, 10, 2, 5, 9],
    "M7(9, 11, 13)": [0, 4, 7, 11, 2, 5, 9],
    "M7(9, #11, 13)": [0, 4, 7, 11, 2, 6, 9],
};

function modC(a, n) {
    return ((a % n) + n) % n;
}

class Note {
    constructor(noteText) {
        this.rawNoteText = noteText;
        this.disable = false;
        this.scale = window.C_MAJOR_SCALE;

        let parseValues = this.parseNote(noteText);
        this.normedText = parseValues[0];
        this.accidentals = parseValues[1];
    }

    parseNote(noteText) {
        let noteTextLength = noteText.length;
        if (noteTextLength == 1) {
            return noteText;
        }

        let normedNoteText = "";
        let accidentals = "";
        let noteIndex = window.TONE_MAP[noteText.substr(0, 1)];

        for (let i = 1; i < noteTextLength; ++i) {
            if (noteText[i] == '#') {
                noteIndex += 1;
                accidentals += '#';
            }
            else if (noteText[i] == 'b') {
                noteIndex -= 1;
                accidentals += 'b';
            }
        }

        noteIndex = modC(noteIndex, 12);
        normedNoteText = window.FLAT_SCALE_MAP[noteIndex];

        return [normedNoteText, accidentals];
    }

    // 移調する
    transposed(steps) {
        let noteIndex = this.getNoteIndex();
        let transposedNoteIndex = modC(noteIndex + steps, 12);
        return new Note(this.scale[transposedNoteIndex]);
    }

    getScaledNoteText() {
        let cShiftIndex = 0;
        for (let i = 0; i < this.scale.length; ++i) {
            if (new Note(this.scale[i]).getNormedNoteText() == "C") {
                cShiftIndex = i;
            }
        }

        let cShiftNoteIndex = modC(this.getNoteIndex() + cShiftIndex, 12);

        return this.scale[cShiftNoteIndex];
    }
    setDisable(disable) {
        this.disable = disable;
    }

    isDisable() {
        return this.disable;
    }

    getNoteIndex() {
        return window.TONE_MAP[this.getNormedNoteText()];
    }

    getAccidentals() {
        return this.accidentals;
    }

    getNormedNoteText() {
        return this.normedText;
    }

    getRawNoteText() {
        return this.rawNoteText;
    }

    static createTensionNote(tension) {
        let tensionNote;
        if (tension == "2" || tension == "9") {
            tensionNote = new Note("D");
        }
        else if (tension == "11" || tension == "4") {
            tensionNote = new Note("F");
        }
        else if (tension == "13" || tension == "6") {
            tensionNote = new Note("A");
        }
        else if (tension == "#9") {
            tensionNote = new Note("Eb");
        }
        else if (tension == "b9") {
            tensionNote = new Note("Db");
        }
        else if (tension == "#11") {
            tensionNote = new Note("F#");
        }
        else if (tension == "b13") {
            tensionNote = new Note("Ab");
        }

        return tensionNote;
    }

    static createIndexNote(index) {
        return new Note(window.FLAT_SCALE_MAP[index]);
    }
}

class QualityParser {
    constructor() { }

    static parse(qualityText) {
        let tensionInParentheses = this.findTensionInParentheses(qualityText);
        let tensions = this.findTension(qualityText);

        let tensionValues = this.calcTensions(tensions, tensionInParentheses);

        return [
            tensionValues[0],
            this.calcAlteredTention(tensions, tensionInParentheses),
            this.calcAddTensions(qualityText),
            this.calcQualities(qualityText),
            this.existsSeventh(qualityText) || tensionValues[1]
        ];
    }

    static findTensionInParentheses(qualityText) {
        let tensions = [];
        let match = qualityText.match(/\(.*?\)/);
        if (!match) {
            return tensions;
        }

        let prefixText = match[0];

        {
            let pattern = /[#b]9|#11|b13|b5/g;
            let matchTentions = prefixText.match(pattern);

            if (matchTentions) {
                prefixText = prefixText.replace(pattern, "");
                tensions = tensions.concat(matchTentions);
            }
        }
        {
            let pattern = /9|11|13/g;
            let matchTensions = prefixText.match(pattern);

            if (matchTensions) {
                prefixText = prefixText.replace(pattern, "");
                tensions = tensions.concat(matchTensions);
            }
        }

        return tensions;
    }
    static findTension(qualityText) {
        let tensions = [];
        let match = qualityText.match(/\(.*?\)/);
        let prefixText = qualityText;

        if (match) {
            prefixText = qualityText.replace(/\(.*?\)/, "");
        }

        let pattern = /9|11|13|[\-\+]5|2|4|6/g;
        let matchTensions = prefixText.match(pattern);

        if (matchTensions) {
            prefixText = prefixText.replace(pattern, "");
            tensions = tensions.concat(matchTensions);
        }

        return tensions;
    }

    static calcTensions(tentionsExcludeParent, tensionInParentheses) {
        let tensions = [];
        let seventh = false;

        for (let tension of tensionInParentheses) {
            if (tension.findFirstOf("#b") == -1) {
                tensions.push(tension);
            }
        }

        for (let tension of tentionsExcludeParent) {
            if (tension == "4" || tension == "6" || tension == "2") {
                tensions.push(tension);
            }
            else if (tension == "13") {
                tensions.push("13");
                tensions.push("11");
                tensions.push("9");
                seventh = true;
            }
            else if (tension == "11") {
                tensions.push("11");
                tensions.push("9");
                seventh = true;
            }
            else if (tension == "9") {
                tensions.push("9");

                if (!tentionsExcludeParent.includes("6")) {
                    seventh = true;
                }
            }
        }

        return [tensions, seventh];
    }
    static calcAlteredTention(tentionsExcludeParent, tensionInParentheses) {
        let alteredTensions = [];
        for (let tension of tentionsExcludeParent) {
            if (tension.findFirstOf("#b-+") != -1) {
                alteredTensions.push(tension);
            }
        }
        for (let tension of tensionInParentheses) {
            if (tension.findFirstOf("#b-+") != -1) {
                alteredTensions.push(tension);
            }
        }

        return alteredTensions;
    }
    static calcAddTensions(qualityText) {
        let tensions = [];
        let pattern = /add9|add11|add2|add4/g;
        let matchTensions = qualityText.match(pattern);

        if (matchTensions) {
            for (let i = 0; i < matchTensions.length; ++i) {
                matchTensions[i] = matchTensions[i].replace("add", "");
            }
            tensions = tensions.concat(matchTensions);
        }

        return tensions;
    }
    static calcQualities(qualityText) {
        let qualities = [];
        let prefixText = qualityText;

        {
            let pattern = /aug|dim|sus/g;
            let matchQualities = prefixText.match(pattern);

            if (matchQualities) {
                prefixText = prefixText.replace(pattern, "");
                qualities = qualities.concat(matchQualities);
            }
        }

        {
            let pattern = /m|M/g;
            let matchQualities = prefixText.match(pattern);

            if (matchQualities) {
                prefixText = prefixText.replace(pattern, "");
                qualities = qualities.concat(matchQualities);
            }
        }

        return qualities;
    }
    static existsSeventh(qualityText) {
        return qualityText.indexOf("7") != -1;
    }
}

class Quality {
    constructor(qualityText) {
        this.rawQualityText = qualityText;
        let parseValues = QualityParser.parse(qualityText);

        this.tensions = parseValues[0];
        this.alteredTensions = parseValues[1];
        this.addTentions = parseValues[2];
        this.qualities = parseValues[3];
        this.seventh = parseValues[4];
    }

    getQualityValue() {
        let qualityValue = [0, 0, 0, 0];

        for (let quality of this.qualities) {
            if (quality == "M") {
                qualityValue[3] += 1;
            }
            else if (quality == "m") {
                qualityValue[1] -= 1;
            }
            else if (quality == "dim") {
                qualityValue[1] -= 1;
                qualityValue[2] -= 1;
                qualityValue[3] -= 1;
            }
            else if (quality == "aug") {
                qualityValue[2] += 1;
            }
        }

        return qualityValue;
    }

    getRawQualityText() {
        return this.rawQualityText;
    }
    getQualityText() {
        let qualityText = "";

        for (let quality of this.qualities) {
            qualityText += quality;
        }

        if (this.existsSeventh()) {
            qualityText += "7";
        }

        return qualityText;
    }
    getTensions() {
        return this.tensions;
    }
    getAlteredTensions() {
        return this.alteredTensions;
    }
    getAddTensions() {
        return this.addTentions;
    }
    existsSeventh() {
        return this.seventh;
    }
    getQualities() {
        return this.qualities;
    }
}

class ChordParser {
    constructor() { }

    static parse(chordText) {
        let accidentals = this.getAccidentals(chordText);
        let root = this.getRoot(chordText, accidentals.length);
        let quality = this.getQuality(chordText, accidentals.length);
        let bass = this.getBass(chordText, accidentals.length);

        return [accidentals, root, bass, quality];
    }

    static getAccidentals(chordText) {
        let accidentals = "";
        let chordLength = chordText.length;
        if (chordLength == 1) {
            return accidentals;
        }

        for (let i = 1; i < chordLength; ++i) {
            if (chordText[i] != '#' && chordText[i] != 'b') {
                break;
            }

            accidentals += chordText[i];
        }

        return accidentals;
    }
    static getRoot(chordText, accidentalsLength) {
        return chordText.substr(0, accidentalsLength + 1);
    }
    static getBass(chordText, accidentalsLength) {
        let bass;
        let bassPos = chordText.indexOf("/");

        if (bassPos == -1) {
            bass = this.getRoot(chordText, accidentalsLength);
        }
        else {
            bass = chordText.substr(bassPos + 1);
        }

        return bass;
    }
    static getQuality(chordText, accidentalsLength) {
        let bassPos = chordText.indexOf("/");
        if (bassPos == -1) {
            return chordText.substr(accidentalsLength + 1);
        }
        else {
            return chordText.substr(accidentalsLength + 1, bassPos - (accidentalsLength + 1));
        }
    }

    static matchChord(notes, omitfive) {
        let relativeNotes = [];
        for (let note of notes) {
            relativeNotes.push(modC(note - notes[0], 12));
        }

        relativeNotes.sort((a, b) => a - b);

        for (let key in window.CHORD_MAP) {
            let qualityNotes = window.CHORD_MAP[key];
            qualityNotes.sort((a, b) => a - b);

            if (qualityNotes.length == relativeNotes.length) {
                if (array_equal(qualityNotes, relativeNotes)) {
                    return [true, key];
                }
            }

            if (omitfive)
            {
                let omitFiveNotes = qualityNotes.filter((note) => {
                    return (note != 7);
                });
                relativeNotes = relativeNotes.filter((note) => {
                    return (note != 7);
                });

                if (array_equal(omitFiveNotes, relativeNotes))
                {
                    return [true, key];
                }

                if (omitFiveNotes.length > 5)
                {
                    let omitFiveThreeNotes = omitFiveNotes.filter((note) => {
                        return (note != 4);
                    });
                    relativeNotes = relativeNotes.filter((note) => {
                        return (note != 4);
                    });

                    if (array_equal(omitFiveThreeNotes, relativeNotes))
                    {
                        return [true, key];
                    }
                }
            }
        }

        return [false, ""];
    }

    static noteToChord(notes) {
        notes = [...new Set(notes)];

        let omitFive = notes.length > 2;
        let matchValues = this.matchChord(notes, omitFive);
        if (matchValues[0]) {
            return new Chord(Note.createIndexNote(notes[0]).getNormedNoteText() + matchValues[1]);
        }


        let shiftedNotes = [];
        for (let i = 1; i < notes.length; ++i) {
            shiftedNotes.push(notes[i]);
        }

        // ベースを含まない転回系
        for (let i = 0; i < shiftedNotes.length; ++i) {
            let matchValues = this.matchChord(shiftedNotes, false);
            if (matchValues[0]) {
                return new Chord(
                    Note.createIndexNote(shiftedNotes[0]).getNormedNoteText() +
                    matchValues[1] + "/" +
                    Note.createIndexNote(notes[0]).getNormedNoteText());
            }

            // 移動
            let note = shiftedNotes.shift();
            shiftedNotes.push(note);
        }

        // ベースを含む転回系
        shiftedNotes.unshift(notes[0]);
        for (let i = 0; i < shiftedNotes.length; ++i) {
            let matchValues = this.matchChord(shiftedNotes, false);
            if (matchValues[0]) {
                return new Chord(
                    Note.createIndexNote(shiftedNotes[0]).getNormedNoteText() +
                    matchValues[1] + "/" +
                    Note.createIndexNote(notes[0]).getNormedNoteText());
            }

            // 移動
            let note = shiftedNotes.shift();
            shiftedNotes.push(note);
        }

        return null;
    }
}
class Chord {
    constructor(chordText) {
        let parseValues = ChordParser.parse(chordText);
        this.accidentals = parseValues[0];
        this.root = new Note(parseValues[1]);
        this.bass = new Note(parseValues[2]);
        this.quality = new Quality(parseValues[3]);
        this.rawChordText = chordText;
    }

    getQuality() {
        return this.quality;
    }
    getChordText() {
        return this.rawChordText;
    }
    getAccidentals() {
        return this.accidentals;
    }
    getRootText() {
        return this.root.getRawNoteText();
    }
    getBassText() {
        return this.bass.getRawNoteText();
    }
    getRoot() {
        return this.root;
    }
    getBass() {
        return this.bass;
    }
    getNotes() {
        let notes = [];
        let relativeNotes = [new Note("C"), new Note("E"), new Note("G"), new Note("Bb")];

        // クオリティによるノートの変化
        let qualityValue = this.quality.getQualityValue();
        for (let i = 0; i < qualityValue.length; ++i) {
            relativeNotes[i] = relativeNotes[i].transposed(qualityValue[i]);
        }

        // 直接変更されるクオリティーによる変化
        let qualities = this.quality.getQualities();
        for (let qua of qualities) {
            if (qua == "sus") {
                relativeNotes[2].setDisable(true);
            }
        }

        // テンションの追加
        let tensions = this.quality.getTensions();
        for (let tension of tensions) {
            let tensionNote = Note.createTensionNote(tension);
            if (tensionNote) {
                relativeNotes.push(tensionNote);
            }
        }

        let addTensions = this.quality.getAddTensions();
        for (let tension of addTensions) {
            let tensionNote = Note.createTensionNote(tension);
            if (tensionNote) {
                relativeNotes.push(tensionNote);
            }
        }

        // オルタードテンションの追加
        let alteredTensions = this.quality.getAlteredTensions();
        for (let tension of alteredTensions) {
            let tensionNote = Note.createTensionNote(tension);
            if (tensionNote) {
                relativeNotes.push(tensionNote);
            }
            else {
                if (tension == "-5") {
                    relativeNotes[2] = relativeNotes[2].transposed(-1);
                }
                else if (tension == "+5") {
                    relativeNotes[2] = relativeNotes[2].transposed(1);
                }
            }
        }

        // 7thの確認
        if (!this.quality.existsSeventh()) {
            relativeNotes[3].setDisable(true);
        }

        // 相対的なノートからルートノートの絶対的なノートに変換する
        for (let note of relativeNotes) {
            if (note.isDisable()) {
                continue;
            }

            let transposedNote = note.transposed(this.getRoot().getNoteIndex());
            // 重複確認
            if (!notes.includes(transposedNote)) {
                notes.push(transposedNote);
            }
        }

        // ベースノートの追加
        if (this.getRoot().getNoteIndex() != this.getBass().getNoteIndex()) {
            notes.push(this.getBass());
        }

        return notes;
    }

    getNoteIndexes() {
        let notes = this.getNotes();
        let noteindexes = [];

        for (let note of notes) {
            noteindexes.push(note.getNoteIndex());
        }

        return noteindexes;
    }
    getNotesText() {
        let notes = this.getNotes();
        let notesText = [];

        for (let note of notes) {
            notesText.push(note.getRawNoteText());
        }

        return notesText;
    }

    isOnChord() {
        return this.getRoot().getNoteIndex() != this.getBass().getNoteIndex();
    }

    transposed(steps) {
        let transposedRoot = this.root.transposed(steps);
        if (this.isOnChord()) {
            return new Chord(
                transposedRoot.getRawNoteText() +
                this.quality.getRawQualityText() + "/" +
                this.bass.transposed(steps).getRawNoteText());
        }
        else {
            return new Chord(
                transposedRoot.getRawNoteText() +
                this.quality.getRawQualityText()
            );
        }
    }
    // 再構成 (未実装)
    reconfigured() {

    }

    static noteToChord(notes) {
        return ChordParser.noteToChord(notes);
    }
}

// ============== メトロノーム ==================
// https://github.com/grantjames/metronome/blob/master/metronome.js
class Metronome
{
    constructor(tempo = 120)
    {
        this.audioContext = null;
        this.notesInQueue = [];
        this.currentQuarterNote = 0;
        this.tempo = tempo;
        this.lookahead = 25;
        this.scheduleAheadTime = 0.1;
        this.nextNoteTime = 0.0;
        this.isRunning = false;
        this.intervalID = null;
        this.metre = 4;
    }

    nextNote()
    {
        var secondsPerBeat = 60.0 / this.tempo;
        this.nextNoteTime += secondsPerBeat;

        this.currentQuarterNote++;
        if (this.currentQuarterNote == this.metre) {
            this.currentQuarterNote = 0;
        }
    }

    scheduleNote(beatNumber, time)
    {
        this.notesInQueue.push({ note: beatNumber, time: time });

        const osc = this.audioContext.createOscillator();
        const envelope = this.audioContext.createGain();

        osc.frequency.value = (beatNumber % this.metre == 0) ? 1000 : 800;
        envelope.gain.value = 1;
        envelope.gain.exponentialRampToValueAtTime(1, time + 0.001);
        envelope.gain.exponentialRampToValueAtTime(0.001, time + 0.02);

        osc.connect(envelope);
        envelope.connect(this.audioContext.destination);

        osc.start(time);
        osc.stop(time + 0.03);
    }

    scheduler()
    {
        while (this.nextNoteTime < this.audioContext.currentTime + this.scheduleAheadTime ) {
            this.scheduleNote(this.currentQuarterNote, this.nextNoteTime);
            this.nextNote();
        }
    }

    start()
    {
        if (this.isRunning) return;

        if (this.audioContext == null)
        {
            this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
        }

        this.isRunning = true;

        this.currentQuarterNote = 0;
        this.nextNoteTime = this.audioContext.currentTime + 0.05;

        this.intervalID = setInterval(() => this.scheduler(), this.lookahead);
    }

    stop()
    {
        this.isRunning = false;

        clearInterval(this.intervalID);
    }

    startStop()
    {
        if (this.isRunning) {
            this.stop();
        }
        else {
            this.start();
        }
    }
}


// キーボード関連
function captureKeyboard() {
    if(IS_SP){
        return;
    }

    $(document).on("keydown", handleKeyDown );
    $(document).on("keyup", handleKeyUp);
    $(window).on("keypress", handleKeyPress );
};

function releaseKeyboard() {
    if(IS_SP){
        return;
    }

    $(document).off("keydown", handleKeyDown );
    $(document).off("keyup", handleKeyUp);
    $(window).off("keypress", handleKeyPress );
};


var NoteA = function(note, octave) {
    this.note = note;
    this.octave = octave || 0;
};
var n = function(a, b) { return {note: new NoteA(a, b), held: false}; };
var key_binding = {
    65: n("gs"),
    90: n("a"),
    83: n("as"),
    88: n("b"),
    67: n("c", 1),
    70: n("cs", 1),
    86: n("d", 1),
    71: n("ds", 1),
    66: n("e", 1),
    78: n("f", 1),
    74: n("fs", 1),
    77: n("g", 1),
    75: n("gs", 1),
    188: n("a", 1),
    76: n("as", 1),
    190: n("b", 1),
    191: n("c", 2),
    222: n("ds", 3),
    49: n("gs", 1),
    81: n("a", 1),
    50: n("as", 1),
    87: n("b", 1),
    69: n("c", 2),
    52: n("cs", 2),
    82: n("d", 2),
    53: n("ds", 2),
    84: n("e", 2),
    89: n("f", 2),
    55: n("fs", 2),
    85: n("g", 2),
    56: n("gs", 2),
    73: n("a", 2),
    57: n("as", 2),
    79: n("b", 2),
    80: n("c", 3),
    189: n("cs", 3),
    219: n("e", 3),
    192: n("d",3),
    226: n("d",2),

    186: n("cs",2),
    221: n("ds",2),
};
var capsLockKey = false;
function handleKeyDown(evt) {
    var code = parseInt(evt.keyCode);
    if(key_binding[code] !== undefined){
        var binding = key_binding[code];
        if(!binding.held) {

            binding.held = true;
            var note = binding.note;
            var octave = 1 + note.octave;
            if(evt.shiftKey) ++octave;
            else if(capsLockKey) --octave;

            var key = (note.note + octave);

            press(key)

        }
        evt.preventDefault();
        evt.stopPropagation();
        return false;

    } else if(code == 240) { // Caps Lock
        capsLockKey = !capsLockKey;
        evt.preventDefault();
    } else if(code== 32){
        $(document).trigger("pressSustain");
        evt.preventDefault();
    }
};

function handleKeyUp(evt) {
    var code = parseInt(evt.keyCode);

    if(key_binding[code] !== undefined) {
        var binding = key_binding[code];
        if(binding.held) {
            binding.held = false;

            var note = binding.note;
            var octave = 1 + note.octave;
            if(evt.shiftKey) ++octave;
            else if(capsLockKey || evt.ctrlKey) --octave;
            note = note.note + octave;

            if($("#pedal").is(":checked")){
                return;
            }
            release(note,loginInfo.myID);
        }
    } else if(code== 32){
        $(document).trigger("releaseSustain");
        evt.preventDefault();
    }

    evt.preventDefault();
    evt.stopPropagation();
    return false;
};

function handleKeyPress(evt) {
    evt.preventDefault();
    evt.stopPropagation();
    return false;
};


function toBoolean (data) {
    return data.toLowerCase() === 'true';
  }

window.addEventListener('load', function () {
    // =============== 鍵盤メッセージ系 ==================
    if (window.HOLD_KEY_ID == undefined) {
        window.HOLD_KEY_ID = new Set();
    }
    window.DISCONNECT = $("#disconnect");

    window.SELFVOLUME = getCookie("selfVolume") ? parseFloat(getCookie("selfVolume")) : 2.0;
    window.PIANORELEASE = getCookie("piano_release") ? parseFloat(getCookie("piano_release")) : 0.1;
    window.PIANOATTACK = getCookie("piano_attack") ? parseFloat(getCookie("piano_attack")) : 0.0;
    window.PIANOSUSTAIN = getCookie("piano_sustain") ? parseFloat(getCookie("piano_sustain")) : 0.7;
    window.PIANODECAY = getCookie("piano_decay") ? parseFloat(getCookie("piano_decay")) : 0.0;
    window.PIANOTRANSPOSE = getCookie("pianoTranspose") ? parseInt(getCookie("pianoTranspose")) : 0;
    window.ICONSIZE = getCookie("iconsize") ? parseInt(getCookie("iconsize")) : 20;
    window.PIANOVOLUME = getCookie("ck_piano_volume") ? parseFloat(getCookie("ck_piano_volume")) : 1.0;
    if (getCookie("chordViewMode") === "") {
        window.CHORDVIEWMODE = false;
    }
    else {
        window.CHORDVIEWMODE = toBoolean(getCookie("chordViewMode"));
    }

    window.MIDIDevice = null;
    window.MIDI_INPUTS = [];
    window.MIDI_OUTPUTS = [];
    window.ONLINE_USERS = [];
    window.USER_PIANO_VOLUME = {};
    window.MIDI_OUTPUT = null;
    window.selectInputMidiName = getCookie("midiInput") ? getCookie("midiInput") : "未設定";
    window.selectOutputMidiName = getCookie("midiOutput") ? getCookie("midiOutput") : "未設定";

    window.metronome = new Metronome();
    window.metronome.tempo = 120;

    addJS_Node(updateList);
    addJS_Node(html_parse);
    addJS_Node(insertTalk);
    addJS_Node(playPiano);
    addJS_Node(playBuffer);
    addJS_Node(releaseBuffer);
    addJS_Node(press);
    addJS_Node(release);
    addJS_Node(releaseKeyboard);
    addJS_Node(captureKeyboard);
    addJS_Node(handleKeyDown);
    addJS_Node(handleKeyPress);
    addJS_Node(handleKeyUp);


    // キーボード関連
    $("#text").off("focus").off("blur");
    $(document).off("keydown");
    $(document).off("keyup");
    $(window).off("keypress");

    captureKeyboard();
    $("#text")
    .on("focus",function(){
        releaseKeyboard();
    })
    .on("blur",function(){
        captureKeyboard();
    });


    // いいね関連
    $("#iineButton").unbind("click");
    $("#iineButton").bind("click", function (e) {
        e.preventDefault();
        socket.emit('iine', loginInfo.myID);

        return false;
    });

    $(document).off("pressSustain");
    $(document).off("releaseSustain");
    $(document).on("pressSustain", function () {
        if (gAutoSustain) {
            return;
        }
        gSustain = true;
    });
    $(document).on("releaseSustain", function () {
        gSustain = false;
        if (gAutoSustain) {
            return;
        }
        for (var id in gSustainedNotes) {
            if (gSustainedNotes.hasOwnProperty(id) && gSustainedNotes[id] && !gHeldNotes[id]) {
                gSustainedNotes[id] = false;
                if (!$("#disconnect").is(":checked")) {
                    socket.emit('r', id);
                }
                releaseBuffer(id, loginInfo.myID);
                window.HOLD_KEY_ID.delete(window.MIDI_KEY_NAMES.indexOf(id));
            }
        }
        gSustainedNotes = {};
    });

    socket.off('p');
    socket.off('r');

    //弾く
    socket.on('p', function (id, uid, vol) {
        if (window.DISCONNECT.is(":checked")) {
            return;
        }

        if (uid in window.USER_PIANO_VOLUME) {
            playPiano(id, uid, vol * window.USER_PIANO_VOLUME[uid]);
        }
        else {
            playPiano(id, uid, vol);
        }

        if (window.MIDI_OUTPUT) {
            var note_number = window.MIDI_KEY_NAMES.indexOf(id);
            if (note_number == -1) return;
            note_number = note_number + 21;

            window.MIDI_OUTPUT.send([0x90, note_number, vol / 1.5 * 127], window.performance.now());

            setTimeout(function () {
                window.MIDI_OUTPUT.send([0x80, note_number, 0], window.performance.now());
            }, 1000);
        }
    });

    //離す
    socket.on('r', function (id, uid) {
        if (window.DISCONNECT.is(":checked")) {
            return;
        }

        releaseBuffer(id, uid);
        if (window.MIDI_OUTPUT) {
            var note_number = window.MIDI_KEY_NAMES.indexOf(id);
            if (note_number == -1) return;
            note_number = note_number + 21;

            window.MIDI_OUTPUT.send([0x80, note_number, 0], window.performance.now());
        }
    });

    $('.setting').off('click');
    $(".setting").click(function (e) {
        e.preventDefault();

        $.ajax({
            url: "setting.html?" + new Date().getTime(),
        }).done(function (res) {
            releaseKeyboard();
            var fg = $("<div class=fg style='z-index:10000;background:rgba(0,0,0,.5);position:fixed;top:0;left:0;width:100%;height:100%;text-align:center'></div>");
            var win = $("<div style='text-align:center;width:100%'><div style='background:#111;color:white'>チャット設定</div><div class=win_inner style='display:inline-block;margin-top:30px;background:white;width:90%;padding:2px'>" +
                "<div style='margin-bottom:20px;'>" + res + "</div></div></div>");
            fg.append(win);

            $(win).on("click", ".closeSetting", function () {
                fgClose();
                captureKeyboard();
            });

            $(fg).click(function () {
                $(win).find(".closeSetting").trigger("click");
            })

            win.click(function (e) {
                e.stopPropagation()
                //return false;
            })
            $(".modal").append(fg);

            $(fg).hide().fadeIn("fast", function () {
                on_setting_open();
            });



            let settings = win.find("div .settings");
            // オプションにリリースを追加
            settings.append("<div class=\"hdr\">ピアノの設定</div>")
                .append("<div class=\"con\"><table><tbody><tr>" +
                    "<td><div style=\"display:inline-block\">リリース<br><input type=\"range\" min=\"0.0\" max=\"1.0\" step=\"0.05\" class=\"range\" id=\"piano_release\"></div></td>" +
                    "<td><div style=\"display:inline-block\">アタック<br><input type=\"range\" min=\"0.0\" max=\"0.5\" step=\"0.01\" class=\"range\" id=\"piano_attack\"></div></td>" +
                    "<td><div style=\"display:inline-block\">ディケイ<br><input type=\"range\" min=\"0.0\" max=\"1.0\" step=\"0.05\" class=\"range\" id=\"piano_decay\"></div></td>" +
                    "<td><div style=\"display:inline-block\">サステイン<br><input type=\"range\" min=\"0.0\" max=\"1.0\" step=\"0.05\" class=\"range\" id=\"piano_sustain\"></div></td>" +
                    "<td><div style=\"display:inline-block\">自分の音量<br><input type=\"range\" min=\"0.0\" max=\"3.0\" step=\"0.01\" class=\"range\" id=\"selfVolume\"></div></td><td><div style=\"display:inline-block\">移調<br><select id=\"pianoTranspose\"></select></div></td></tr></tbody></table></div>")
            // 移調機能
            for (let i = 6; i >= 0; --i) {
                $("#pianoTranspose").append($('<option>').html(i).val(i));
            }
            for (let i = 1; i <= 5; ++i) {
                $("#pianoTranspose").append($('<option>').html(-i).val(-i));
            }
            $("#pianoTranspose").val(String(window.PIANOTRANSPOSE));
            $("#pianoTranspose").on('change', function () {
                window.PIANOTRANSPOSE = parseInt($(this).val());
                setCookie("pianoTranspose", window.PIANOTRANSPOSE);
            });

            // リリース設定
            $('#piano_release').val(window.PIANORELEASE);
            $('#piano_release').on('change', function () {
                window.PIANORELEASE = parseFloat($(this).val());
                setCookie("piano_release", window.PIANORELEASE);
            });
            // アタック設定
            $('#piano_attack').val(window.PIANOATTACK);
            $('#piano_attack').on('change', function () {
                window.PIANOATTACK = parseFloat($(this).val());
                setCookie("piano_attack", window.PIANOATTACK);
            });
            // ディケイ設定
            $('#piano_decay').val(window.PIANODECAY);
            $('#piano_decay').on('change', function () {
                window.PIANODECAY = parseFloat($(this).val());
                setCookie("piano_decay", window.PIANODECAY);
            });
            // サステイン
            $('#piano_sustain').val(window.PIANOSUSTAIN);
            $('#piano_sustain').on('change', function () {
                window.PIANOSUSTAIN = parseFloat($(this).val());
                setCookie("piano_sustain", window.PIANOSUSTAIN);
            });

            // 自分の音量
            $('#selfVolume').val(window.SELFVOLUME);
            $('#selfVolume').on('change', function () {
                window.SELFVOLUME = parseFloat($(this).val());
                setCookie("selfVolume", window.SELFVOLUME);
            });

            // オプションにMIDI INOUTの選択ボックスを追加する
            settings.append("<div class=\"hdr\">MIDI INOUT</div>")
                .append("<div class=\"con\"><table><tbody><tr><td><div style=\"display:inline-block\">MIDI IN <br><select id=\"midi_input\"></select></div></td> <td><div style=\"display:inline-block\">MIDI OUT <br><select id=\"midi_output\"></select></div></td></tr></tbody></table></div></div>");

            // 入力MIDIデバイスの記録
            $("#midi_input").append($('<option>').html("未設定").val("未設定"));
            for (let input of window.MIDI_INPUTS) {
                $("#midi_input").append($('<option>').html(input.name).val(input.name));
            }
            $("#midi_input").val(window.selectInputMidiName);

            $("#midi_output").append($('<option>').html("未設定").val("未設定"));
            // 出力MIDIデバイスの記録
            for (let output of window.MIDI_OUTPUTS) {
                $("#midi_output").append($('<option>').html(output.name).val(output.name));
            }
            $("#midi_output").val(window.selectOutputMidiName);

            $('#midi_input').change(function () {
                window.selectInputMidiName = $(this).val();
                updateInputsOutputs();
                setCookie("midiInput", window.selectInputMidiName);
            })

            $('#midi_output').change(function () {
                window.selectOutputMidiName = $(this).val();
                updateInputsOutputs();
                setCookie("midiOutput", window.selectOutputMidiName);
            })


            // ユーザーごとの音量設定
            settings.append("<div class=\"hdr\">ユーザー設定</div>")
                .append("<div class=\"con\"><table><tbody><tr>" +
                    "<td><div style=\"display:inline-block\">ユーザー <br><select id=\"selectedUser\"></select></div></td>" +
                    "<td><div style=\"display:inline-block\">音量 <br><input type=\"range\" min=\"0.0\" max=\"2.0\" step=\"0.05\" class=\"range\" id=\"userVolume\"></div></td>" +
                    '<td><span class="switch chordViewMode"><input type="checkbox" id="chordViewMode"><span class="slider round"></span></span></td>' +
                    "<td>コードビューを表示する(コード検出機能は重たいので演奏時には無効にしてください)</td>" +
                    "</tr></tbody></table></div>");

            $('#chordViewMode').prop("checked", window.CHORDVIEWMODE);
            $('span .switch,.chordViewMode').on('click', function(){
                let chordViewMode = $(this).find("#chordViewMode");
                chordViewMode.prop("checked", !chordViewMode.is(":checked"));

                window.CHORDVIEWMODE = chordViewMode.is(':checked');
                setCookie("chordViewMode", window.CHORDVIEWMODE);

                if (window.CHORDVIEWMODE) {
                    $('#chordView').show();
                }
                else {
                    $('#chordView').hide();
                }
            });

            for (let user of window.ONLINE_USERS) {
                $("#selectedUser").append($('<option>').html(user).val(user));
            }
            $('#selectedUser').change(function () {
                let user = $('#selectedUser').val();
                if (user in window.USER_PIANO_VOLUME) {
                    $('#userVolume').val(String(window.USER_PIANO_VOLUME[user]));
                }
                else {
                    window.USER_PIANO_VOLUME[user] = 1.0;
                }
            });
            $('#userVolume').on('change', function () {
                let volume = parseFloat($(this).val());
                let user = $('#selectedUser').val();
                window.USER_PIANO_VOLUME[user] = volume;
            });


            // チャットの設定
            settings.append('<div class="hdr">チャット設定</div>')
                .append('<div class="con"><table><tbody><tr><td><div style="display:inline-block">アイコンサイズ<br><input type="number" id="iconsize" min="12" max="48"></div></td></tr></tbody></table></div>')
            $('#iconsize').val(window.ICONSIZE);
            $('#iconsize').on('change', function () {
                let size = parseInt($(this).val());
                window.ICONSIZE = size;
                setCookie("iconsize", window.ICONSIZE);
            });


            // ピアノ音量の設定
            $('input[setting="piano_volume"]').on('change', function () {
                let pianoVolume = $(this).val();
                window.PIANOVOLUME = parseFloat(pianoVolume);
            });


            // メトロノームの設定
            settings.append('<div class="hdr">メトロノーム設定</div>')
                .append('<div class="con"><table><tbody><tr>' +
                '<td><span class="switch playMetronome"><input id="playMetronome" type="checkbox"><span class="slider round"></span></span></td>' +
                '<td>メトロノームを再生する</td>' +
                '<td><div style="display:inline-block">テンポ<br><input type="number" id="tempo" min="0" max="300"></div></td>' +
                '<td><div style="display:inline-block">拍子<br><input type="number" id="metre" min="1" max="20"></div></td>' +
                '</tr></tbody></table></div>');

            $("#playMetronome").prop("checked", window.metronome.isRunning);
            $('span .switch,.playMetronome').on('click', function(){
                let playMetronome = $(this).find("#playMetronome");
                playMetronome.prop("checked", !playMetronome.is(":checked"));

                if (playMetronome.is(":checked"))
                {
                    window.metronome.start();
                }
                else
                {
                    window.metronome.stop();
                }
            });

            $('#tempo').val(window.metronome.tempo);
            $('#tempo').on('change', function(){
                let tempo = parseInt($(this).val());
                if (tempo < 0 ){ tempo = 0; }
                if (tempo > 300) {tempo = 300;}
                if (isNaN(tempo)) { tempo = 120; }

                window.metronome.tempo = tempo;
            });

            $('#metre').val(window.metronome.metre);
            $('#metre').on('change', function(){
                let metre = parseInt($(this).val());
                if (metre < 1) { metre = 1;}
                if (metre > 20) { metre = 20;}
                if (isNaN(metre)) { metre = 4;}

                window.metronome.metre = metre;
            });
        })
    });

    // =============== MIDIの入れ替え ====================
    navigator.requestMIDIAccess = window.MIDIACCESS_BACK;

    window.MIDI_KEY_NAMES = ["a-1", "as-1", "b-1"];
    var bare_notes = "c cs d ds e f fs g gs a as b".split(" ");
    for (var oct = 0; oct < 7; oct++) {
        for (var i in bare_notes) {
            window.MIDI_KEY_NAMES.push(bare_notes[i] + oct);
        }
    }
    window.MIDI_KEY_NAMES.push("c7");

    if (navigator.requestMIDIAccess) {
        $("#connectJava").hide();
        navigator.requestMIDIAccess().then(successCallback, faildCallback);
    }


    // ============== 鍵盤の入れ替え ===============
    // 鍵盤を削除
    $('#canvas').remove();

    let whiteKey = '<div style="width: 1.92308%; z-index: 0; background-color: white; height: 100%; border: 1px solid gray; position: absolute; left: 0.0%" class="whiteKey pianoKey">';
    let blackKey = '<div style="width: 1.28205%; z-index: 1; height: 60%; background-color: #333; position: absolute; left: 0.0%" class="blackKey pianoKey">';
    let left = 0.0;
    let keyID = 21;

    // 新しい鍵盤を追加
    $('body > div.all > div.header').after('<div id="keyboard" style="width: 95%; height: 15%; margin: auto; position: relative; min-height: 180px;"></div>')
    $('#keyboard')
        .append($(whiteKey).attr("id", String(keyID)))
        .append($(blackKey).css("left", String(left + 1.28205) + "%").attr("id", String(keyID += 1)))
        .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)));

    for (let i = 0; i < 7; ++i) {
        $('#keyboard')
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)))
            .append($(blackKey).css("left", String(left + 1.28205) + "%").attr("id", String(keyID += 1)))
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)))
            .append($(blackKey).css("left", String(left + 1.28205) + "%").attr("id", String(keyID += 1)))
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)))
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)))
            .append($(blackKey).css("left", String(left + 1.28205) + "%").attr("id", String(keyID += 1)))
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)))
            .append($(blackKey).css("left", String(left + 1.28205) + "%").attr("id", String(keyID += 1)))
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)))
            .append($(blackKey).css("left", String(left + 1.28205) + "%").attr("id", String(keyID += 1)))
            .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)));
    }
    $('#keyboard')
        .append($(whiteKey).css("left", String(left += 1.92308) + "%").attr("id", String(keyID += 1)));

    // 鍵盤クリック時
    $('div .pianoKey').on('mousedown', function () {
        let id = parseInt($(this).attr("id"));
        press(window.MIDI_KEY_NAMES[id - 21], 1.0);
        setTimeout(function() {release(window.MIDI_KEY_NAMES[id - 21])}, 100);
    });
    $('#keyboard').before('<div id="chordView" style="z-index: 100; display: block; right: 0; left: 0; margin:auto; position: absolute; width: 10%; color: #5d627b; background: white; border-top: solid 6px #BAC; border-bottom: solid 6px rgba(0, 0, 0, 0.25); box-shadow: 5px 3px 5px rgba(0, 0, 0, 0.22); border-radius: 9px;"><p id="chordViewText" style="text-align: center; font-weight:bold;"></p></div>');

    // チャットを下まで伸ばします
    $("#chat").css("max-height", "50%");

    if (window.CHORDVIEWMODE) {
        $('#chordView').show();
    }
    else {
        $('#chordView').hide();
    }
})

addJS_Node(ease);
addJS_Node(displayChord);
addJS_Node(Note);
addJS_Node(Chord);
addJS_Node(ChordParser);
addJS_Node(Quality);
addJS_Node(QualityParser);
addJS_Node(modC);
addJS_Node(array_equal);
addJS_Node(displayChordView);
addJS_Node(toBoolean);

function addJS_Node(text, s_URL, funcToRun, runOnLoad) {
    var D = document;
    var scriptNode = D.createElement('script');
    if (runOnLoad) {
        scriptNode.addEventListener("load", runOnLoad, false);
    }
    scriptNode.type = "text/javascript";
    if (text) scriptNode.textContent = text;
    if (s_URL) scriptNode.src = s_URL;
    if (funcToRun) scriptNode.textContent = '(' + funcToRun.toString() + ')()';

    var targ = D.getElementsByTagName('head')[0] || D.body || D.documentElement;
    targ.appendChild(scriptNode);
}