epianoOptimization

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

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