YNOproject Collective Unconscious Kalimba Performer

Music can be played automatically based on the given score.

// ==UserScript==
// @name         YNOproject Collective Unconscious Kalimba Performer
// @name:zh-CN   YNOproject Collective Unconscious 卡林巴演奏家
// @namespace    https://github.com/Exsper/
// @version      0.1.8
// @description  Music can be played automatically based on the given score.
// @description:zh-CN  可以根据给定乐谱自动演奏乐曲。
// @author       Exsper
// @homepage     https://github.com/Exsper/yno-unconscious-performer#readme
// @supportURL   https://github.com/Exsper/yno-unconscious-performer/issues
// @match        https://ynoproject.net/unconscious/
// @require      https://cdn.staticfile.org/jquery/2.1.3/jquery.min.js
// @license      MIT License
// @grant        none
// @run-at       document-end
// ==/UserScript==

// 自定义设置,可以修改
// 同时音符之间演奏最短延迟,设置过小可能无法及时切换音调导致弹错音符
const Same_Time_Interval = 40;
// 在低于最低音调(C3)多少半音的音符用最低音调弹奏,再低则忽略该音符
const Allow_Exceed_Range_low = 2;
// 在高于最高音调(B5)多少半音的音符用最高音调弹奏,再高则忽略该音符
const Allow_Exceed_Range_high = 2;

// 全局变量,不能修改
let nowGroup = "";
let isLoop = false;
let stopped = true;
let midiData = null;

function getKeyData(code) {
    switch (code) {
        case "1":
        case "C3": return { group: "left", key: "Digit1", keyCode: 49 };
        case "2":
        case "Db3":
        case "C#3": return { group: "left", key: "Digit2", keyCode: 50 };
        case "3":
        case "D3": return { group: "left", key: "Digit3", keyCode: 51 };
        case "4":
        case "Eb3":
        case "D#3": return { group: "left", key: "Digit4", keyCode: 52 };
        case "5":
        case "E3": return { group: "left", key: "Digit5", keyCode: 53 };
        case "6":
        case "F3": return { group: "left", key: "Digit6", keyCode: 54 };
        case "7":
        case "Gb3":
        case "F#3": return { group: "left", key: "Digit7", keyCode: 55 };
        case "8":
        case "G3": return { group: "left", key: "Digit8", keyCode: 56 };
        case "9":
        case "Ab3":
        case "G#3": return { group: "left", key: "Digit9", keyCode: 57 };
        case "10":
        case "A3": return { group: "left", key: "Digit0", keyCode: 48 };
        case "11":
        case "Bb3":
        case "A#3": return { group: "left", key: "BracketLeft", keyCode: 219 };
        case "12":
        case "B3": return { group: "left", key: "BracketRight", keyCode: 221 };

        case "13":
        case "C4": return { group: "down", key: "Digit1", keyCode: 49 };
        case "14":
        case "Db4":
        case "C#4": return { group: "down", key: "Digit2", keyCode: 50 };
        case "15":
        case "D4": return { group: "down", key: "Digit3", keyCode: 51 };
        case "16":
        case "Eb4":
        case "D#4": return { group: "down", key: "Digit4", keyCode: 52 };
        case "17":
        case "E4": return { group: "down", key: "Digit5", keyCode: 53 };
        case "18":
        case "F4": return { group: "down", key: "Digit6", keyCode: 54 };
        case "19":
        case "Gb4":
        case "F#4": return { group: "down", key: "Digit7", keyCode: 55 };
        case "20":
        case "G4": return { group: "down", key: "Digit8", keyCode: 56 };
        case "21":
        case "Ab4":
        case "G#4": return { group: "down", key: "Digit9", keyCode: 57 };
        case "22":
        case "A4": return { group: "down", key: "Digit0", keyCode: 48 };
        case "23":
        case "Bb4":
        case "A#4": return { group: "down", key: "BracketLeft", keyCode: 219 };
        case "24":
        case "B4": return { group: "down", key: "BracketRight", keyCode: 221 };

        case "25":
        case "C5": return { group: "right", key: "Digit1", keyCode: 49 };
        case "26":
        case "Db5":
        case "C#5": return { group: "right", key: "Digit2", keyCode: 50 };
        case "27":
        case "D5": return { group: "right", key: "Digit3", keyCode: 51 };
        case "28":
        case "Eb5":
        case "D#5": return { group: "right", key: "Digit4", keyCode: 52 };
        case "29":
        case "E5": return { group: "right", key: "Digit5", keyCode: 53 };
        case "30":
        case "F5": return { group: "right", key: "Digit6", keyCode: 54 };
        case "31":
        case "Gb5":
        case "F#5": return { group: "right", key: "Digit7", keyCode: 55 };
        case "32":
        case "G5": return { group: "right", key: "Digit8", keyCode: 56 };
        case "33":
        case "Ab5":
        case "G#5": return { group: "right", key: "Digit9", keyCode: 57 };
        case "34":
        case "A5": return { group: "right", key: "Digit0", keyCode: 48 };
        case "35":
        case "Bb5":
        case "A#5": return { group: "right", key: "BracketLeft", keyCode: 219 };
        case "36":
        case "B5": return { group: "right", key: "BracketRight", keyCode: 221 };

        case "0": return null;
        default: return null;
    }
}

function switchToGroup(group) {
    if (nowGroup !== group) {
        nowGroup = group;
        switch (group) {
            case "left": return simulateKeyboardInput("ArrowLeft", 37);
            case "down": return simulateKeyboardInput("ArrowDown", 40);
            case "right": return simulateKeyboardInput("ArrowRight", 39);
            // case "up": return simulateKeyboardInput("ArrowUp",38);
            default: return;
        }
    }
}

function wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function playSong(song, bpm) {
    nowGroup = "";
    let bpmnum = parseFloat(bpm);
    if (bpmnum <= 10 || bpmnum > 1200) bpmnum = 60;
    let interval = 60 / bpmnum * 1000;
    let keys = song.split(" ");
    stopped = false;
    for (let i = 0; i < keys.length; i++) {
        let keyData = getKeyData(keys[i]);
        if (keyData !== null) {
            if (nowGroup === "") switchToGroup(keyData.group);
            simulateKeyboardInput(keyData.key, keyData.keyCode);
        }

        if (stopped) break;

        await wait(interval / 2);

        if (stopped) break;

        let nextKey = "";
        if (i < keys.length - 1) nextKey = getKeyData(keys[i + 1]);
        else nextKey = getKeyData(keys[0]);
        if (nextKey !== null) switchToGroup(nextKey.group);

        await wait(interval / 2);
    }
    if (stopped) {
        $("#rs-play").text("开始演奏");
        return;
    }
    if (isLoop) await playSong(song, bpm);
    else {
        stopped = true;
        $("#rs-play").text("开始演奏");
    }
}

// ---------------------------------------------------------------------------------------
// refer to https://madderscientist.github.io/je_score_operator
function JE2Song(text) {
    let n = 0;
    let pitch = 13;
    let song = [];
    const note = ["1", "#1", "2", "#2", "3", "4", "#4", "5", "#5", "6", "#6", "7"];
    while (n < text.length) {
        while (n < text.length) {
            for (; n < text.length; n++) {              //音高
                if (text[n] === '[' || text[n] === ')') pitch += 12;
                else if (text[n] === ']' || text[n] === '(') pitch -= 12;
                else break;
            }
            if (text[n] === '\n' || text[n] === ' ') {    //空格换行停顿
                n++;
                song.push(0);
                break;
            } else {
                let up = 0;
                if (n < text.length) {                  //升降
                    if (text[n] === '#') up = 1;
                    else if (text[n] === 'b') up = -1;
                }
                if (n + Math.abs(up) < text.length) {
                    let noteid = note.indexOf(text[n + Math.abs(up)]);
                    if (noteid > -1) {
                        song.push(noteid + pitch + up);
                        n = n + Math.abs(up) + 1;
                        break;
                    } else {        //没找到,说明不是音符,不结束,找音符
                        n++;
                    }
                } else {
                    break;
                }
            }
        }
    }
    return song.join(" ");
}

// midi.js
// 封装二进制事件
class midiEvent {
    // 若tisks == -1, 在addEvent时会自动使用last_tick; 若<-1, 则last_tick - this.ticks
    static note(at, duration, note, intensity) {
        return [{
            ticks: at,
            code: 0x9,
            value: [note, intensity]
        }, {
            ticks: at >= 0 ? at + duration : -duration,
            code: 0x9,
            value: [note, 0]
        }];
    }
    static instrument(at, instrument) {
        return {
            ticks: at,
            code: 0xc,
            value: [instrument]
        };
    }
    static control(at, id, Value) {
        return {
            ticks: at,
            code: 0xb,
            value: [id, Value]
        };
    }
    static tempo(at, bpm) {
        bpm = Math.round(60000000 / bpm);
        return {
            ticks: at,
            code: 0xff,
            type: 0x51,
            value: mtrk.number_hex(bpm, 3)
        };
    }
    static time_signature(at, numerator, denominator) {
        return {
            ticks: at,
            code: 0xff,
            type: 0x58,
            value: [numerator, Math.floor(Math.log2(denominator)), 0x18, 0x8]
        };
    }
}
// 一个音轨
class mtrk {
    /**
     * 将tick数转换为midi的时间格式
     * @param {number} ticknum int
     * @returns midi tick array
     * @example mtrk.tick_hex(555555) // [0x08, 0x7A, 0x23]
     */
    static tick_hex(ticknum) {
        ticknum = ticknum.toString(2);
        let i = ticknum.length, j = Math.ceil(i / 7) * 7;
        for (; i < j; i++) ticknum = '0' + ticknum;
        let t = Array();
        for (i = 0; i + 7 < j; i = i + 7) t.push('1' + ticknum.substring(i, i + 7));
        t.push('0' + ticknum.substr(-7, 7));
        for (i = 0; i < t.length; i++) t[i] = parseInt(t[i], 2);
        return t;
    }
    /**
     * 将字符串转换为ascii数组
     * @param {string} name string
     * @param {number} x array's length (default:self-adaption)
     * @returns array
     * @example mtrk.string_hex("example",3) // [101,120,97]
     */
    static string_hex(str, x = -1) {
        let Buffer = Array(x > 0 ? x : str.length).fill(0);
        let len = Math.min(Buffer.length, str.length);
        for (let i = 0; i < len; i++) Buffer[i] = str[i].charCodeAt();
        return Buffer;
    }
    /**
     * 将一个正整数按16进制拆分成各个位放在数组中, 最地位在数组最高位
     * @param {number} num int
     * @param {number} x array's length (default:self-adaption)
     * @returns array
     * @example mtrk.number_hex(257,5) // [0,0,0,1,1]
     */
    static number_hex(num, x = -1) {
        if (x > 0) {
            let Buffer = Array(x).fill(0);
            for (--x; x >= 0 && num != 0; x--) {
                Buffer[x] = num & 0xff;
                num = num >> 8;
            }
            return Buffer;
        } else {
            let len = 0;
            let num2 = num;
            while (num2 != 0) {
                num2 = num2 >> 8;
                len++;
            }
            let Buffer = Array(len);
            for (--len; len >= 0; len--) {
                Buffer[len] = num & 0xff;
                num = num >> 8;
            }
            return Buffer;
        }
    }
    constructor(name = "untitled", event_list = Array()) {
        this.name = name;
        this.events = event_list;
        this.last_tick = 0; // 最后一个事件的时间
    }
    /**
     * 向mtrk添加事件
     * @param {object} event {ticks,code,*[type],value}
     * @returns event (or event list, or event list nesting)
     * @example m.addEvent({ticks:0,code:0x9,value:[40,100]}); m.addEvent(midiEvent.tempo(0,120));
     */
    addEvent(event) {
        const addevent = (e) => {
            if (e.ticks < 0) {
                if (e.ticks == -1)
                    e.ticks = this.last_tick;
                else
                    e.ticks = this.last_tick - e.ticks;
            }
            this.events.push(e);
            if (e.ticks > this.last_tick)
                this.last_tick = e.ticks;
        }
        const parseEvents = (el) => {
            if (Array.isArray(el)) {
                for (let i = 0; i < el.length; i++)
                    parseEvents(el[i]);
            } else addevent(el);
        }
        parseEvents(event);
        return event;
    }
    /**
     * 对齐事件
     * @param {number} tick 一个四分音符的tick数
     * @param {number} accuracy int, 精度, 越大允许的最短时长越小
     */
    align(tick, accuracy = 4) {
        accuracy = tick / parseInt(accuracy);
        for (let i = 0; i < this.events.length; i++) {
            this.events[i].ticks = Math.round(this.events[i].ticks / accuracy) * accuracy;
        }
    }
    /**
     * 事件按时间排序,同时间的音符事件则按力度排序
     */
    sort() {
        this.events.sort((a, b) => {
            if (a.ticks == b.ticks) {
                if (a.code == b.code && a.code == 9) return a.value[1] - b.value[1];
                return b.code - a.code;
            } return a.ticks - b.ticks;
        });
    }
    /**
     * 将mtrk转换为track_id音轨上的midi数据
     * @param {number} track_id int, [0, 15]
     * @returns Array
     */
    export(track_id) {
        this.sort();
        // 音轨名
        let data = mtrk.string_hex(this.name);
        data = [0, 255, 3, data.length, ...data];
        // 事件解析
        let current = 0;
        for (let i = 0; i < this.events.length; i++) {
            let temp = this.events[i];
            let d = null;
            if (temp.code >= 0xf0) {
                if (temp.code == 0xf0) d = [0xf0, temp.value.length];
                else d = [0xff, temp.type, temp.value.length];
            } else d = (temp.code << 4) + track_id;
            data = data.concat(mtrk.tick_hex(temp.ticks - current), d, temp.value);
            current = temp.ticks;
        }
        return [77, 84, 114, 107,
            ...mtrk.number_hex(data.length + 4, 4),
            ...data,
            0, 255, 47, 0];
    }

    /**
     * 将音轨转为可JSON对象
     * @param {number} track_id 音轨所属轨道id (从0开始)
     * @returns json object
     */
    JSON(track_id) {
        this.sort();
        let Notes = [],
            controls = [],
            Instruments = [],
            Tempos = [],
            TimeSignatures = [];
        for (let i = 0; i < this.events.length; i++) {
            let temp = this.events[i];
            switch (temp.code) {
                case 0x9:
                    if (temp.value[1] > 0) {    // 力度不为0表示按下
                        let overat = temp.ticks;
                        for (let j = i + 1; j < this.events.length; j++) {
                            let over = this.events[j];
                            if (over.code == 0x9 && over.value[0] == temp.value[0]) {
                                overat = over.ticks;
                                if (overat > temp.ticks) {
                                    Notes.push({
                                        ticks: temp.ticks,
                                        durationTicks: overat - temp.ticks,
                                        midi: temp.value[0],
                                        intensity: temp.value[1]
                                    });
                                    break;
                                }
                            }
                        }
                    }
                    break;
                case 0xb:
                    controls.push({
                        ticks: temp.ticks,
                        controller: temp.value[0],
                        value: temp.value[1]
                    })
                    break;
                case 0xc:
                    Instruments.push({
                        ticks: temp.ticks,
                        number: temp.value[0]
                    });
                    break;
                case 0xff:
                    switch (temp.type) {
                        case 0x51:  // 速度
                            Tempos.push({
                                ticks: temp.ticks,
                                bpm: Math.round(60000000 / ((temp.value[0] << 16) + (temp.value[1] << 8) + temp.value[2]))
                            });
                            break;
                        case 0x58:  // 节拍
                            TimeSignatures.push({
                                ticks: temp.ticks,
                                timeSignature: [temp.value[0], 2 << temp.value[1]]
                            });
                            break;
                    }
                    break;
            }
        }
        return {
            channel: track_id,
            name: this.name,
            tempos: Tempos,
            controlChanges: controls,
            instruments: Instruments,
            notes: Notes,
            timeSignatures: TimeSignatures
        }
    }
    toJSON(track_id) {
        return this.JSON(track_id);
    }
}
// midi文件,组织多音轨
class midi {
    constructor(bpm = 120, time_signature = [4, 4], tick = 480, Mtrk = [], Name = 'untitled') {
        this.bpm = bpm;
        this.Mtrk = Mtrk;   // Array<mtrk>
        this.tick = tick;   // 一个四分音符的tick数
        this.time_signature = time_signature;
        this.name = Name;
    }
    /**
     * 添加音轨,如果无参则创建并返回
     * @param {mtrk} newtrack
     * @returns mtrk
     * @example track = m.addTrack(); m2.addTrack(new mtrk("test"))
     */
    addTrack(newtrack = null) {
        if (newtrack == null)
            newtrack = new mtrk(String(this.Mtrk.length));
        this.Mtrk.push(newtrack);
        return newtrack;
    }
    /**
     * 对齐所有音轨 修改自身
     * @param {number} accuracy 对齐精度
     */
    align(accuracy = 4) {
        for (let i = 0; i < this.Mtrk.length; i++)
            this.Mtrk[i].align(this.tick, accuracy);
    }
    /**
     * 解析midi文件,返回新的midi对象
     * @param {Uint8Array} midi_file midi数据
     * @returns new midi object
     */
    static import(midi_file) {
        // 判断是否为midi文件
        if (midi_file.length < 14) return null;
        if (midi_file[0] != 77 || midi_file[1] != 84 || midi_file[2] != 104 || midi_file[3] != 100) return null;
        let newmidi = new midi(120, [4, 4], 480, Array.from({ length: 16 }, (_, i) => new mtrk(String(i))), '');
        // 读取文件头
        newmidi.tick = midi_file[13] + (midi_file[12] << 8);
        let mtrkNum = midi_file[11] + (midi_file[10] << 8);
        let midtype = midi_file[9];
        // 读mtrk音轨
        for (let n = 0, i = 14; n < mtrkNum; n++) {
            // 判断是否为MTrk音轨
            if (midi_file[i++] != 77 || midi_file[i++] != 84 || midi_file[i++] != 114 || midi_file[i++] != 107) { n--; i -= 3; continue; }
            let timeline = 0;       // 时间线
            let lastType = 0xC0;	// 上一个midi事件类型
            let lastChaneel = n - 1;  // 上一个midi事件通道
            let mtrklen = (midi_file[i++] << 24) + (midi_file[i++] << 16) + (midi_file[i++] << 8) + midi_file[i++] + i;
            // 读取事件
            for (; i < mtrklen; i++) {
                // 时间间隔(tick)
                let flag = 0;
                while (midi_file[i] > 127)
                    flag = (flag << 7) + midi_file[i++] - 128;
                timeline += (flag << 7) + midi_file[i++];
                // 事件类型
                let type = midi_file[i] & 0xf0;
                let channel = midi_file[i++] - type;
                let ichannel = midtype ? n : channel;
                do {
                    flag = false;
                    switch (type) { //结束后指向事件的最后一个字节
                        case 0x90:	// 按下音符
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0x9,
                                value: [midi_file[i++], midi_file[i]]
                            });
                            break;
                        case 0x80:	// 松开音符
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0x9,
                                value: [midi_file[i++], 0]
                            });
                            break;
                        case 0xF0:	// 系统码和其他格式
                            if (channel == 0xF) {
                                switch (midi_file[i++]) {
                                    case 0x2f:
                                        break;
                                    case 0x03:
                                        // 给当前mtrk块同序号的音轨改名
                                        // newmidi.Mtrk[n].name = '';
                                        // for (let q = 1; q <= midi_file[i]; q++)
                                        //     newmidi.Mtrk[n].name += String.fromCharCode(midi_file[i + q]);
                                        break;
                                    case 0x58:
                                        if (timeline == 0) {
                                            newmidi.time_signature = [midi_file[i + 1], 1 << midi_file[i + 2]];
                                            break;
                                        }
                                    case 0x51:
                                        if (timeline == 0) {
                                            newmidi.bpm = Math.round(60000000 / ((midi_file[i + 1] << 16) + (midi_file[i + 2] << 8) + midi_file[i + 3]));
                                            break;
                                        }
                                    default:
                                        newmidi.Mtrk[0].addEvent({
                                            ticks: timeline,
                                            code: 0xff,
                                            type: midi_file[i - 1],
                                            value: Array.from(midi_file.slice(i + 1, i + 1 + midi_file[i]))
                                        });
                                        break;
                                }
                            } else {	// 系统码
                                newmidi.Mtrk[0].addEvent({
                                    ticks: timeline,
                                    code: 0xf0,
                                    value: Array.from(midi_file.slice(i + 1, i + 1 + midi_file[i]))
                                });
                            }
                            i += midi_file[i];
                            break;
                        case 0xB0:	// 控制器
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0xb,
                                value: [midi_file[i++], midi_file[i]]
                            });
                            break;
                        case 0xC0:	// 改变乐器
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0xc,
                                value: [midi_file[i]]
                            });
                            break;
                        case 0xD0:	// 触后通道
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0xd,
                                value: [midi_file[i]]
                            });
                            break;
                        case 0xE0:	// 滑音
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0xe,
                                value: [midi_file[i++], midi_file[i]]
                            });
                            break;
                        case 0xA0:	// 触后音符
                            newmidi.Mtrk[ichannel].addEvent({
                                ticks: timeline,
                                code: 0xa,
                                value: [midi_file[i++], midi_file[i]]
                            });
                            break;
                        default:
                            type = lastType;
                            channel = lastChaneel
                            flag = true;
                            i--;
                            break;
                    }
                } while (flag);
                lastType = type;
                lastChaneel = channel;
            }
        }
        newmidi.name = newmidi.Mtrk[0].name;
        // 找到第一个有音符的音轨
        mtrkNum = 0;
        for (let i = 1; i < newmidi.Mtrk.length; i++) {
            let temp = newmidi.Mtrk[i].events;
            for (let j = 0; j < temp.length; j++) {
                if (temp[j].code == 0x9) {
                    mtrkNum = i;
                    temp = null;
                    break;
                }
            }
            if (!temp) break;
        }
        // 把没有音符的音轨事件移到第一个有音符的音轨
        for (let i = 0; i < newmidi.Mtrk.length; i++) {
            let temp = newmidi.Mtrk[i].events;
            for (let j = 0; j < temp.length; j++) {
                if (temp[j].code == 0x9) {
                    temp = null;
                    break;
                }
            }
            if (temp) {
                newmidi.Mtrk[mtrkNum].events = newmidi.Mtrk[mtrkNum].events.concat(temp);
                newmidi.Mtrk[i] = null;
            }
        }
        // 删去空的音轨
        for (let i = 0; i < newmidi.Mtrk.length; i++)
            if (!newmidi.Mtrk[i] || newmidi.Mtrk[i].events.length == 0) newmidi.Mtrk.splice(i--, 1);
        return newmidi;
    }
    /**
     * 转换为midi数据
     * @param {*} type midi file type [0 or 1(default)]
     * @returns Uint8Array
     */
    export(type = 1) {
        if (type == 0) {    // midi0创建 由于事件不记录音轨,需要归并排序输出
            let Mtrks = Array(this.Mtrk.length + 1);
            for (let i = 0; i < this.Mtrk.length; i++) {
                this.Mtrk[i].sort();
                Mtrks[i] = this.Mtrk[i].events;
            }
            Mtrks[this.Mtrk.length] = new mtrk("head", [
                midiEvent.tempo(0, this.bpm),
                midiEvent.time_signature(0, this.time_signature[0], this.time_signature[1])
            ]);
            let current = 0;
            let index = Array(Mtrks.length).fill(0);
            let data = [];
            while (true) {
                // 找到ticks最小项
                let min = -1;
                let minticks = 0;
                for (let i = 0; i < index.length; i++) {
                    if (index[i] < Mtrks[i].length) {
                        if (min == -1 || Mtrks[i][index[i]].ticks < minticks) {
                            min = i;
                            minticks = Mtrks[i][index[i]].ticks;
                        }
                    }
                }
                if (min == -1) break;
                // 转为midi数据
                let d = null;
                let temp = Mtrks[min][index[min]];
                if (temp.code >= 0xf0) {
                    if (temp.code == 0xf0) d = [0xf0, temp.value.length];
                    else d = [0xff, temp.type, temp.value.length];
                } else d = (temp.code << 4) + min;
                data = data.concat(mtrk.tick_hex(temp.ticks - current), d, temp.value);
                // 善后
                current = minticks;
                index[min]++;
            }
            data = [0, 255, 3, 5, 109, 105, 100, 105, 48, ...data, 0, 255, 47, 0];  // 加了音轨名和结尾
            return new Uint8Array([
                77, 84, 104, 100, 0, 0, 0, 6, 0, 0, 0, 1, ...mtrk.number_hex(this.tick, 2),
                77, 84, 114, 107,
                ...mtrk.number_hex(data.length, 4),
                ...data
            ]);
        } else {    // 除了初始速度、初始节拍,其余ff事件全放0音轨。头音轨不在Mtrk中,export时生成
            // MThd创建
            let data = [77, 84, 104, 100, 0, 0, 0, 6, 0, 1, ...mtrk.number_hex(1 + this.Mtrk.length, 2), ...mtrk.number_hex(this.tick, 2)];
            // 加入全局音轨
            let headMtrk = new mtrk("head", [
                midiEvent.tempo(0, this.bpm),
                midiEvent.time_signature(0, this.time_signature[0], this.time_signature[1])
            ])
            data = data.concat(headMtrk.export(0));
            // 加入其余音轨
            for (let i = 0; i < this.Mtrk.length; i++)
                data = data.concat(this.Mtrk[i].export(i));
            return new Uint8Array(data);
        }
    }

    /**
     * 将midi转换为json对象。原理:每个音轨转换为json对象并对事件进行合并
     * @returns json object
     */
    JSON() {
        let j = {
            header: {
                name: this.name,
                tick: this.tick,
                tempos: [{
                    ticks: 0,
                    bpm: this.bpm
                }],
                timeSignatures: [{
                    ticks: 0,
                    timeSignature: this.time_signature
                }]
            },
            tracks: []
        }
        for (let i = 0; i < this.Mtrk.length; i++) {
            let t = this.Mtrk[i].JSON(i);
            j.header.tempos = j.header.tempos.concat(t.tempos);
            j.header.timeSignatures = j.header.timeSignatures.concat(t.timeSignatures);
            j.tracks.push({
                channel: t.channel,
                name: t.name,
                controlChanges: t.controlChanges,
                instruments: t.instruments,
                notes: t.notes
            });
        }
        return j;
    }
    toJSON() {
        return this.JSON();
    }
}

// ---------------------------------------------------------------------------------------

// 把序号转换为音符,越界为0
function indexToKey(index) {
    index = index - 60;
    if (index <= -12) return 0;
    if (index >= 25) return 0;
    return index + 12;
}

function getTrackPlayableNoteCount(track) {
    let playableCount = 0;
    for (let i = 0; i < track.notes.length; i++) {
        if (indexToKey(track.notes[i].midi) !== 0) playableCount += 1;
    }
    return playableCount;
}

function getTrackPlayableNoteLength(track) {
    let playableLength = 0;
    for (let i = 0; i < track.notes.length; i++) {
        if (indexToKey(track.notes[i].midi) !== 0) playableLength += track.notes[i].durationTicks;
    }
    return playableLength;
}

// 检查轨道内音符可弹奏数,有大于20%的音符无法弹奏则舍弃掉该轨道
function checkTrackPlayable(track) {
    let noteCount = track.notes.length;
    if (noteCount <= 0) return false;
    let playableCount = getTrackPlayableNoteCount(track);
    if ((playableCount / noteCount) < 0.8) return false;
    return true;
}

// 检查是否在相同音阶
function checkKeyInSameScale(key1, key2) {
    let scale1 = parseInt((key1 - 1) / 12);
    let scale2 = parseInt((key2 - 1) / 12);
    return scale1 === scale2;
}

function ReadMIDIInfo() {
    if (!midiData) return null;

    let midiJson = midiData.JSON();
    let midiInfo = [];
    for (let i = 0; i < midiJson.tracks.length; i++) {
        let playableNoteCount = getTrackPlayableNoteCount(midiJson.tracks[i]);
        let playableNoteLength = getTrackPlayableNoteLength(midiJson.tracks[i]);
        let isPlayable = checkTrackPlayable(midiJson.tracks[i])
        midiInfo.push({ index: i, playableNoteCount, playableNoteLength, isPlayable });
    }
    midiInfo = midiInfo.filter((a) => a.isPlayable === true);
    midiInfo.sort((a, b) => b.playableNoteLength - a.playableNoteLength);
    return midiInfo;
}

function MIDI2Song(trackIndexs, keyConflictMethod = "all") {
    function approximateIndexToKey(index) {
        index = index - 59;
        if (index <= -12 - Allow_Exceed_Range_low) return 0;
        if (index >= 25 + Allow_Exceed_Range_high) return 0;
        if (index <= -12) return 1;
        if (index >= 25) return 36;
        return index + 12;
    }

    if (!midiData) return null;

    let midiJson = midiData.JSON();

    let mix = [];

    for (let i = 0; i < midiJson.tracks.length; i++) {
        if (trackIndexs.includes(i)) mix = mix.concat(midiJson.tracks[i].notes);
    }
    if (mix.length <= 0) return null;

    mix.sort((a, b) => (a.ticks === b.ticks ? a.midi - b.midi : a.ticks - b.ticks));

    let intervalList = [];
    let lastKey = 0;
    let keyList = [];
    let totalInterval = 0;
    let sameTimeOffsetSum = 0;

    let bpm = midiJson.header.tempos.pop().bpm;
    let realTimePerTick = 60000 / bpm / midiJson.header.tick;

    for (let i = 0; i < mix.length; i++) {
        // 音符演奏长度一致,故不用考虑durationTicks
        let interval = mix[i].ticks * realTimePerTick + sameTimeOffsetSum - totalInterval;
        let key = approximateIndexToKey(mix[i].midi);
        if (key === 0) continue;
        if ((lastKey !== 0) && (interval < Same_Time_Interval)) {
            // 如果同时间、同音符,则舍弃
            if (key === lastKey) {
                continue;
            }
            else if (checkKeyInSameScale(key, lastKey)) {
                // 相同音阶,不用考虑短时切换音阶造成的错误弹奏
            }
            else {
                if (keyConflictMethod === "all") {
                    // 全部弹奏
                    interval = Same_Time_Interval;
                    sameTimeOffsetSum += Same_Time_Interval - interval;
                }
                else {
                    if (keyConflictMethod === "high") {
                        // 取高音
                        if (key <= lastKey) continue;
                    }
                    else if (keyConflictMethod === "low") {
                        // 取低音
                        if (key >= lastKey) continue;
                    }
                    else if (keyConflictMethod === "middle") {
                        // 取靠近F4的音
                        if (Math.abs(key - 18) >= Math.abs(lastKey - 18)) continue;
                    }
                    // 删除上一个key
                    let _lastInterval = intervalList.pop();
                    let _lastKey = keyList.pop();
                    if (keyList.length <= 0) lastKey = 0;
                    else lastKey = keyList[keyList.length - 1];
                    totalInterval -= _lastInterval;
                    // 重复该次循环,重新判断
                    i -= 1;
                    continue;
                }
            }
        }
        intervalList.push(interval);
        keyList.push(key);
        lastKey = key;
        totalInterval += interval;
    }

    let lastnote = mix.pop();
    let endWaitTime = lastnote.durationTicks;
    if (bpm && bpm > 0) {
        let itv = 60 * 1000 / bpm * 4;
        let toEnd = itv - lastnote.ticks % itv;
        if (toEnd > endWaitTime) endWaitTime = toEnd;
    }
    endWaitTime *= realTimePerTick;

    return { intervalList, keyList, endWaitTime };
}

async function playMIDI(trackIndexs, keyConflictMethod = "all") {
    nowGroup = "";
    let keyInfo = MIDI2Song(trackIndexs, keyConflictMethod);
    if (!keyInfo) {
        alert("不支持演奏该MIDI音乐");
        stopped = true;
        $("#rs-play").text("开始演奏");
        return;
    }
    let intervalList = keyInfo.intervalList;
    let keyList = keyInfo.keyList;
    let endWaitTime = keyInfo.endWaitTime;
    stopped = false;
    for (let i = 0; ;) {
        if (stopped) break;

        await wait(intervalList[i] / 2);

        if (stopped) break;

        let keyData = getKeyData(keyList[i].toString());
        if (keyData !== null) {
            if (nowGroup === "") switchToGroup(keyData.group);
            simulateKeyboardInput(keyData.key, keyData.keyCode);
        }

        if (i < intervalList.length - 1) await wait(intervalList[i + 1] / 2);
        else await wait(intervalList[0] / 2);

        if (stopped) break;

        let nextKey = "";
        if (i < keyList.length - 1) nextKey = getKeyData(keyList[i + 1].toString());
        else nextKey = getKeyData(keyList[0].toString());
        if (nextKey !== null) switchToGroup(nextKey.group);

        i++;
        if (i >= keyList.length) {
            if (isLoop) {
                i = 0;
                nowGroup = "";
                await wait(endWaitTime);
                continue;
            }
            else {
                stopped = true;
                break;
            }
        }
    }
    $("#rs-play").text("开始演奏");
    return;
}



function init() {
    let $openButton = $('<button>', { text: "+", id: "rs-open", style: "float:left;top:30%;position:absolute;left:0%;", title: "显示窗口" }).appendTo($("body"));
    $openButton.click(() => {
        $("#rs-div").show();
        $("#rs-open").hide();
    });
    let $mainDiv = $("<div>", { id: "rs-div", class: "container", style: "top:40%;left:0%;transform: translate(0, -50%);width:200px;position:absolute;text-align:center;z-index:999;height:auto;max-height:70vh;min-height:160px;overflow:hidden;border-top: 24px double #000000 !important;padding-top: 0px !important;" });
    $mainDiv.hide();
    let $statLabel = $("<span>", { id: "rs-stat", text: "请在使用卡林巴效果并打开钢琴窗后点击“开始演奏”", style: "display: block; padding: 6px;" }).appendTo($mainDiv);
    let $textTypeSelect = $("<select>", { id: "rs-select-type" }).appendTo($mainDiv);
    $textTypeSelect.append($('<option></option>').attr('value', '0').text('标准音符'));
    $textTypeSelect.append($('<option></option>').attr('value', 'je').text('JE谱'));
    $textTypeSelect.append($('<option></option>').attr('value', 'midi').text('MIDI'));
    $textTypeSelect.val("0");
    let $file = $("<input>", { type: "file", id: "rs-file", style: "font-size: 10px; align-self: center; display: inline-block;" }).appendTo($mainDiv);
    $file.on("change", () => {
        let rawdata;
        let reader = new FileReader();
        try {
            reader.readAsArrayBuffer($file[0].files[0]);
            reader.onload = function () {
                rawdata = new Uint8Array(this.result);
                midiData = midi.import(rawdata);

                let $mainTable = $("#rs-table");
                $mainTable.empty();
                let midiInfo = ReadMIDIInfo();
                if (!midiInfo || midiInfo.length <= 0) {
                    alert("不支持演奏该MIDI音乐");
                    return;
                }
                let $ltr = $("<tr>", { style: "width:100%;" });
                let $ltd = $("<td>", { style: "width:30%" }).appendTo($ltr);
                $("<span>", { text: "演奏音轨" }).appendTo($ltd);
                $ltd = $("<td>", { style: "width:35%" }).appendTo($ltr);
                $("<span>", { text: "可演奏音符数" }).appendTo($ltd);
                $ltd = $("<td>", { style: "width:35%" }).appendTo($ltr);
                $("<span>", { text: "可演奏长度" }).appendTo($ltd);
                $ltr.appendTo($mainTable);
                midiInfo.map((trackInfo, index) => {
                    $ltr = $("<tr>");
                    $ltd = $("<td>").appendTo($ltr);
                    let $trackCheckbox = $("<input>", { type: "checkbox", class: "rs-track", track: trackInfo.index }).appendTo($ltd);
                    if (index === 0) $trackCheckbox.prop("checked", true);
                    $ltd = $("<td>").appendTo($ltr);
                    $("<span>", { text: trackInfo.playableNoteCount }).appendTo($ltd);
                    $ltd = $("<td>").appendTo($ltr);
                    $("<span>", { text: trackInfo.playableNoteLength }).appendTo($ltd);
                    $ltr.appendTo($mainTable);
                });
            };
        }
        catch (err) {
            midiData = null;
        }
    });
    $file.hide();
    let $songText = $("<textarea>", { id: "rs-song", style: "min-height: 80px;" }).appendTo($mainDiv);
    $("<span>", { id: "rs-bpm-label", text: "BPM: " }).appendTo($mainDiv);
    let $numBox = $("<input>", { type: "text", id: "rs-bpm", val: "120", style: "width:30px;align-self:center;" }).appendTo($mainDiv);
    $textTypeSelect.on("change", () => {
        if ($textTypeSelect.val() === "midi") {
            $("#rs-file").show();
            $("#rs-table").show();
            $("#rs-select-conflict-label").show();
            $("#rs-select-conflict").show();
            $("#rs-song").hide();
            $("#rs-bpm-label").hide();
            $("#rs-bpm").hide();
        }
        else {
            $("#rs-file").hide();
            $("#rs-table").hide();
            $("#rs-select-conflict-label").hide();
            $("#rs-select-conflict").hide();
            $("#rs-song").show();
            $("#rs-bpm-label").show();
            $("#rs-bpm").show();
        }
    });
    let $checkButton = $('<button>', { type: "button", text: "开始演奏", id: "rs-play", style: "width:fit-content;align-self:center;" }).appendTo($mainDiv);
    $checkButton.click(async () => {
        if (stopped) {
            stopped = false;
            $checkButton.text("停止演奏");
            if ($("#rs-select-type").val() === "midi") {
                let tracks = [];
                let $checkedTrack = $(".rs-track:checked");
                let keyConflictMethod = $("#rs-select-conflict").val();
                $checkedTrack.each((i, e) => {
                    tracks.push(parseInt(e.attributes["track"].value));
                })
                await playMIDI(tracks, keyConflictMethod);
            }
            else {
                let song = $songText.val();
                if ($("#rs-select-type").val() === "je") song = JE2Song(song);
                await playSong(song, $numBox.val());
            }
        }
        else {
            stopped = true;
            $checkButton.text("开始演奏");
        }
    });
    let $titleDiv = $("<div>", { id: "rs-title", style: "width: 100%; display: flex;" }).prependTo($mainDiv);
    let $rightDiv = $("<div>", { id: "rs-title-right", style: "display: flex; justify-content: right;" }).prependTo($titleDiv);
    let $leftDiv = $("<div>", { id: "rs-title-left", style: "width: 100%; display: flex; justify-content: left;" }).prependTo($titleDiv);
    let $loopCheckBox = $("<input>", { type: "checkbox", id: "rs-loop" }).appendTo($leftDiv);
    $("<label>").attr({ for: "rs-loop" }).text("循环演奏").appendTo($leftDiv);
    $loopCheckBox.change(() => {
        if ($loopCheckBox.prop("checked")) {
            isLoop = true;
        } else {
            isLoop = false;
        }
    });
    let $closeButton = $('<button>', { text: "-", id: "rs-close", title: "隐藏窗口" }).appendTo($rightDiv);
    $closeButton.click(() => {
        $("#rs-div").hide();
        $("#rs-open").show();
    });
    let $mainTable = $("<table>", { id: "rs-table", style: "table-layout:fixed; width:100%; word-wrap: break-word;" }).appendTo($mainDiv);
    $mainTable.hide();
    let $keyConflictMethodLabel = $("<span>", { id: "rs-select-conflict-label", text: "音符冲突处理:" }).appendTo($mainDiv);
    $keyConflictMethodLabel.hide();
    let $keyConflictMethodSelect = $("<select>", { id: "rs-select-conflict" }).appendTo($mainDiv);
    $keyConflictMethodSelect.append($('<option></option>').attr('value', 'all').text('全部弹奏'));
    $keyConflictMethodSelect.append($('<option></option>').attr('value', 'high').text('取高音'));
    $keyConflictMethodSelect.append($('<option></option>').attr('value', 'low').text('取低音'));
    $keyConflictMethodSelect.append($('<option></option>').attr('value', 'middle').text('取接近F4的音'));
    $keyConflictMethodSelect.val("all");
    $keyConflictMethodSelect.hide();

    $mainDiv.appendTo($("body"));
}



function check() {
    let $loaded = $("#loadingOverlay.loaded");
    if ($loaded.length > 0) {
        init();
    }
    else setTimeout(function () { check() }, 2000);
}

$(document).ready(() => {
    check();
});