对 .mid 工作的一些整理

早在大学的时候做mc粒子音乐就对.mid文件做过一些处理,但是当时做的有些许草率,甚至还有些.mid文件处理会报错,因为有些格式的没有考虑到。最近做了钢琴练习的应用,趁着还熟悉,记录一下,以防以后会用到。

MIDI文件格式介绍

MIDI 文件格式详解

MIDI 文件(.mid/.midi)是一种二进制文件,采用分层结构存储音乐数据,不是简单的对象存储。它包含多个”块”(chunks),每个块有特定的格式。

一、文件整体结构

MIDI文件 = 头块(Header Chunk) + 一个或多个音轨块(Track Chunks)

二、头块(Header Chunk) – 14字节

struct HeaderChunk {
    char chunkID[4];      // "MThd"(4字节ASCII码)
    uint32_t length;      // 固定为6(4字节,大端序)
    uint16_t format;      // 格式:0,1,2(2字节)
    uint16_t tracks;      // 音轨数(2字节)
    uint16_t division;    // 时间划分(2字节)
}; // 总共14字节

时间划分(division)说明

  • 最高位为0:表示每四分音符的ticks数
  • 例:0x00C0 = 192 ticks/四分音符
  • 最高位为1:表示SMPTE时间码
  • 例:0xE250 = 每秒25帧,每帧80 ticks

三、音轨块(Track Chunk)

struct TrackChunk {
    char chunkID[4];      // "MTrk"(4字节)
    uint32_t length;      // 音轨数据长度(4字节)
    uint8_t data[];       // 事件数据(可变长度)
};

四、事件结构 – 可变长度

事件格式:Δt + 事件类型 + 参数

1. 时间差(Δt) – 可变长度编码(VLQ)

  • 每个字节最高位是标志位(1表示还有后续字节)
  • 例:值300的编码过程:
  300 = 0x12C
  分解为7位组:[0000010][1001100]
  编码为:10000101 00101100 = 0x85 0x2C

2. MIDI通道事件(占3-4字节)

格式:状态字节(0x80-0xEF) + 数据字节1 + [数据字节2]
事件类型状态字节参数1参数2总字节
Note Off0x8n音符(0-127)力度(0-127)3
Note On0x9n音符(0-127)力度(0-127)3
控制改变0xBn控制器号3
音色改变0xCn音色号2
通道压力0xDn压力值2
弯音0xEnLSB(7位)MSB(7位)3

:n = 通道号(0-15)

3. 元事件(Meta Events) – 可变长度

格式:0xFF + 类型 + 长度(VLQ) + 数据

常见元事件:

  • 轨道结束(0x2F):FF 2F 00(3字节)
  • 设置速度(0x51):FF 51 03 tt tt tt(6字节)
  • tt tt tt = 微秒/四分音符(24位)
  • 拍号(0x58):FF 58 04 nn dd cc bb(8字节)
  • nn=分子, dd=分母(2^dd), cc=ticks/节拍, bb=32分音符/四分音符
  • 调号(0x59):FF 59 02 sf mi(5字节)
  • sf=升降号, mi=大小调(0=大调,1=小调)

4. 系统专有事件(SysEx) – 可变长度

格式:0xF0 + 长度(VLQ) + 厂商数据 + 0xF7

五、运行状态(Running Status)

为节省空间,如果连续事件类型相同,可省略状态字节:

Δt1 状态 数据1 数据2
Δt2 -- 数据1 数据2    ← 省略状态字节

六、完整示例分析

// 头块
4D 54 68 64  // "MThd"
00 00 00 06  // 长度=6
00 01        // 格式1
00 02        // 2个音轨
00 60        // 96 ticks/四分音符

// 音轨1(控制信息)
4D 54 72 6B  // "MTrk"
00 00 00 13  // 长度=19字节

00 FF 03 04 54 65 6D 70  // 设置音轨名"Temp"
00 FF 51 03 07 A1 20     // 速度=500000μs/beat=120BPM
00 FF 58 04 04 02 18 08  // 拍号4/4,24ticks/beat
83 00 FF 2F 00           // Δt=384,轨道结束

// 音轨2(音符数据)
4D 54 72 6B  // "MTrk"
00 00 00 0F  // 长度=15字节

00 90 3C 64  // Δt=0,音符开启(C4,力度100)
60 90 3C 00  // Δt=96,音符关闭(力度=0即Note Off)
00 FF 2F 00  // Δt=0,轨道结束

七、文件格式类型

  1. 格式0:单音轨,所有事件在一个音轨中
  2. 格式1:多音轨,多个音轨同时播放
  3. 格式2:多音轨,独立播放(较少用)

八、特点总结

  • 二进制编码:紧凑高效
  • 分层结构:头块 + 多个音轨块
  • 可变长度:Δt和部分数据使用VLQ编码
  • 事件驱动:基于Δt的时间轴
  • 通道分离:16个逻辑通道
  • 非音频数据:只存储演奏信息,文件很小

.mid是一种二进制的文件,具体的一下信息可以看上面ai的总结,不过一般而言我们不用重复造轮子,接下来我就放一下我用到的处理方法,主要是python和javascript

python处理

py有很多能对.mid文件进行处理的库,我这里使用的mido库。mido库支持创建编写和读取.mid文件,这里只涉及到读取

#安装mido
pip install mido

通过对.mid文件的理解,我们知道有meta信息,还有包含事件的音轨信息组成,我们只是想要可以自定义处理把音乐播放出来,我们只需要关注几个数据

  • ticks_per_beat 表示每拍多少个tick
  • set_tempo 设置速率,没设置默认是500000,单位是微秒,对应120BPM
  • note_on 开始播放一个音符
  • note_off 结束播放一个音符

我们可以一步一步来,首先,我们利用mido库把.mid文件转换成json,观察一下

import mido
def mid2json(mid_file_path, json_file_path=None):
    # 打开 MIDI 文件
    mid = mido.MidiFile(mid_file_path)
    midi_data = {
        "ticks_per_beat": mid.ticks_per_beat,  # 添加 ticks_per_beat 属性
        "tracks": []
    }

    # 遍历 MIDI 文件中的轨道
    for track_idx, track in enumerate(mid.tracks):
        track_data = []
        for msg in track:
            # 将消息转换为字典格式,包含所有属性
            msg_dict = {
                'type': msg.type,
                'time': msg.time
            }
            # 添加消息的所有其他属性
            for key in msg.__dict__:
                if key not in ['type', 'time']:
                    msg_dict[key] = getattr(msg, key)
            track_data.append(msg_dict)
        midi_data["tracks"].append({
            'track_index': track_idx,
            'track_name': track.name if hasattr(track, 'name') else f'Track {track_idx}',
            'messages': track_data
        })
    # 将数据写入 JSON 文件
    if json_file_path is None:
        return midi_data
    with open(json_file_path, 'w') as json_file:
        json.dump(midi_data, json_file, indent=4)

通过这个代码处理我们可以得到类似这样的json文件

{
    "ticks_per_beat": 960,
    "tracks": [
        {
            "track_index": 0,
            "track_name": "\u00a1\u00b6\u00c8\u00fd\u00cc\u00e5\u00a1\u00b7\u00c6\u00ac\u00cd\u00b7\u00c7\u00fa-\u00ca\u00b1\u00bc\u00e4\u00b5\u00c4\u00be\u00a1\u00cd\u00b7",
            "messages": [
                {
                    "type": "track_name",
                    "time": 0,
                    "name": "\u00a1\u00b6\u00c8\u00fd\u00cc\u00e5\u00a1\u00b7\u00c6\u00ac\u00cd\u00b7\u00c7\u00fa-\u00ca\u00b1\u00bc\u00e4\u00b5\u00c4\u00be\u00a1\u00cd\u00b7"
                },
                {
                    "type": "set_tempo",
                    "time": 0,
                    "tempo": 500000
                },
                {
                    "type": "time_signature",
                    "time": 0,
                    "numerator": 4,
                    "denominator": 4,
                    "clocks_per_click": 24,
                    "notated_32nd_notes_per_beat": 8
                },
                {
                    "type": "end_of_track",
                    "time": 0
                }
            ]
        },
        {
            "track_index": 1,
            "track_name": "\u00d6\u00f7\u00d0\u00fd\u00c2\u00c9",
            "messages": [
                {
                    "type": "track_name",
                    "time": 0,
                    "name": "\u00d6\u00f7\u00d0\u00fd\u00c2\u00c9"
                },
                {
                    "type": "control_change",
                    "time": 0,
                    "control": 64,
                    "value": 127,
                    "channel": 0
                },
                {
                    "type": "note_on",
                    "time": 29580,
                    "note": 69,
                    "velocity": 53,
                    "channel": 0
                },
                {
                    "type": "note_off",
                    "time": 840,
                    "note": 69,
                    "velocity": 64,
                    "channel": 0
                },
                {
                    "type": "note_on",
                    "time": 2550,
                    "note": 67,
                    "velocity": 48,
                    "channel": 0
                },
                {
                    "type": "note_off",
                    "time": 810,
                    "note": 67,
                    "velocity": 64,
                    "channel": 0
                },...
        }
    ]
}

我们大致可以看到,一个文件可能有多个track,有的track里可能没有note事件,绝大多数事件都有time属性,并且time并不是递增的,每个note事件都有note,velocity,(channel在我这个案例中用处不大)。我们可以做一些推测,并进行实验,我这里直接给出结论:

  1. time的单位是tick
  2. 每个音轨都有时间轴,事件的事件都是相对时间(例如第一个事件的time是100,第二个是200,意思是第一个发生在全局时间100ticks处,第二个发生在300ticks处)根据 ticks/ticks_per_beat*tempo 可以得到事件发生的现实世界微秒数
  3. note事件的note表示从21开始为第一个的钢琴对应琴键
  4. velocity表示0-127的音量
  5. 一个note值相同的note_on和note_off是一次完整的琴键弹奏,这个note_off距离它对应note_on中间所有的时间累加是这个note的持续时间
  6. 例外的,有的.mid文件并没有那么标准,你可能找不到任何一个note_off,它是使用velocity为0的note_on来表示音符结束的,所有一个完整的音符要考虑这两个条件
  7. set_tempo可能在任意音轨都有,它对速率的影响是全局的,如果没有,起始默认是5000000

于是,我们可以得到类似的代码

def j2track(json_file, output_file=None):
    """
    读取JSON文件,提取音符信息,并将其转换为统一速率的音符列表
    """
    with open(json_file, 'r') as f:
        data = json.load(f)

    ticks_per_beat = data['ticks_per_beat']
    tracks = [t['messages'] for t in data['tracks']]

    # 定义统一速率(如120 BPM,对应500000 μs per beat)
    standard_tempo = 500000  # 120 BPM
    processed_notes = []

    # 初始化每个音轨的当前事件指针和时间
    current_event_indices = [0] * len(tracks)
    current_times = [0.0] * len(tracks)
    active_notes = [{} for _ in range(len(tracks))]  # 每个音轨的活跃音符

    while True:
        # 找出下一个最近的事件
        next_event_time = None
        next_event_index = -1
        next_track_index = -1

        for i in range(len(tracks)):
            if current_event_indices[i] < len(tracks[i]):
                event = tracks[i][current_event_indices[i]]
                event_time = current_times[i] + \
                    event['time'] / ticks_per_beat * standard_tempo
                if next_event_time is None or event_time < next_event_time:
                    next_event_time = event_time
                    next_event_index = current_event_indices[i]
                    next_track_index = i

        # 如果没有更多事件,退出循环
        if next_event_index == -1:
            break

        # 处理下一个事件
        event = tracks[next_track_index][next_event_index]
        current_times[next_track_index] = next_event_time
        if event['type'] == 'set_tempo':
            standard_tempo = event['tempo']
        elif event['type'] == 'note_on' and event['velocity'] > 0:
            note = event['note']
            active_notes[next_track_index][note] = (
                next_event_time, event['velocity'])
        elif event['type'] == 'note_off' or (event['type'] == 'note_on' and event['velocity'] == 0):
            note = event['note']
            if note in active_notes[next_track_index]:
                start_time, velocity = active_notes[next_track_index].pop(note)
                duration = next_event_time - start_time
                # 计算基于统一速率的开始时间和持续时间
                tempo_ratio = standard_tempo / standard_tempo
                standard_start_time = round(start_time * tempo_ratio)
                standard_duration = round(duration * tempo_ratio)
                processed_notes.append({
                    'note': note,
                    'start_time': standard_start_time,
                    'duration': standard_duration,
                    'velocity': velocity,
                    'track': next_track_index  # 记录音轨信息
                })

        # 移动到下一个事件
        current_event_indices[next_track_index] += 1

    processed_notes.sort(key=lambda x: (x['start_time'], x['track']))
    if output_file is not None:
        with open(output_file, 'w') as f:
            json.dump(processed_notes, f)
    return processed_notes

对我们第一步得到的json进行处理后,就能得到单一时间流向,统一单位为微妙,没有变速,只有音符的信息,类似这样

[
  { "note": 50, "start_time": 0, "duration": 250000, "track": 2 },
  { "note": 66, "start_time": 39062, "duration": 1499998, "track": 1 },
  { "note": 69, "start_time": 117766, "duration": 1499998, "track": 1 },
  { "note": 74, "start_time": 186053, "duration": 1499998, "track": 1 },
  { "note": 57, "start_time": 277778, "duration": 250000, "track": 2 },
  { "note": 62, "start_time": 555555, "duration": 250000, "track": 2 },
  { "note": 66, "start_time": 833332, "duration": 250000, "track": 2 },
  ...
]

javascript处理

具体逻辑上面python处理解释了,这里就不赘述了,我们直接开始处理。js处理使用的是Midi库,下面附上Midi.js代码:

Midi.js

/*
    Project Name : midi-parser-js
    Project Url  : https://github.com/colxi/midi-parser-js/
    Author       : colxi
    Author URL   : http://www.colxi.info/
    Description  : MidiParser library reads .MID binary files, Base64 encoded MIDI Data,
    or UInt8 Arrays, and outputs as a readable and structured JS object.
*/

(function(){
    'use strict';

    /**
     * CROSSBROWSER & NODEjs POLYFILL for ATOB() -
     * By: https://github.com/MaxArt2501 (modified)
     * @param  {string} string [description]
     * @return {[type]}        [description]
     */
    const _atob = function(string) {
        // base64 character set, plus padding character (=)
        let b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
        // Regular expression to check formal correctness of base64 encoded strings
        let b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
        // remove data type signatures at the begining of the string
        // eg :  "data:audio/mid;base64,"
        string = string.replace( /^.*?base64,/ , '');
        // atob can work with strings with whitespaces, even inside the encoded part,
        // but only \t, \n, \f, \r and ' ', which can be stripped.
        string = String(string).replace(/[\t\n\f\r ]+/g, '');
        if (!b64re.test(string))
            throw new TypeError('Failed to execute _atob() : The string to be decoded is not correctly encoded.');

        // Adding the padding if missing, for semplicity
        string += '=='.slice(2 - (string.length & 3));
        let bitmap, result = '';
        let r1, r2, i = 0;
        for (; i < string.length;) {
            bitmap = b64.indexOf(string.charAt(i++)) << 18 | b64.indexOf(string.charAt(i++)) << 12
                    | (r1 = b64.indexOf(string.charAt(i++))) << 6 | (r2 = b64.indexOf(string.charAt(i++)));

            result += r1 === 64 ? String.fromCharCode(bitmap >> 16 & 255)
                    : r2 === 64 ? String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255)
                    : String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255, bitmap & 255);
        }
        return result;
    };


    /**
     * [MidiParser description]
     * @type {Object}
     */
    const MidiParser  = {
        // debug (bool), when enabled will log in console unimplemented events
        // warnings and internal handled errors.
        debug: false,

        /**
         * [parse description]
         * @param  {[type]} input     [description]
         * @param  {[type]} _callback [description]
         * @return {[type]}           [description]
         */
        parse: function(input, _callback){
            if(input instanceof Uint8Array) return MidiParser.Uint8(input);
            else if(typeof input === 'string') return MidiParser.Base64(input);
            else if(input instanceof HTMLElement && input.type === 'file') return MidiParser.addListener(input , _callback);
            else throw new Error('MidiParser.parse() : Invalid input provided');
        },

        /**
         * addListener() should be called in order attach a listener to the INPUT HTML element
         * that will provide the binary data automating the conversion, and returning
         * the structured data to the provided callback function.
         */
        addListener: function(_fileElement, _callback){
            if(!File || !FileReader) throw new Error('The File|FileReader APIs are not supported in this browser. Use instead MidiParser.Base64() or MidiParser.Uint8()');

            // validate provided element
            if( _fileElement === undefined ||
                !(_fileElement instanceof HTMLElement) ||
                _fileElement.tagName !== 'INPUT' ||
                _fileElement.type.toLowerCase() !== 'file' 
            ){
                console.warn('MidiParser.addListener() : Provided element is not a valid FILE INPUT element');
                return false;
            }
            _callback = _callback || function(){};

            _fileElement.addEventListener('change', function(InputEvt){             // set the 'file selected' event handler
                if (!InputEvt.target.files.length) return false;                    // return false if no elements where selected
                console.log('MidiParser.addListener() : File detected in INPUT ELEMENT processing data..');
                let reader = new FileReader();                                      // prepare the file Reader
                reader.readAsArrayBuffer(InputEvt.target.files[0]);                 // read the binary data
                reader.onload =  function(e){
                    _callback( MidiParser.Uint8(new Uint8Array(e.target.result)));  // encode data with Uint8Array and call the parser
                };
            });
        },

        /**
         * Base64() : convert baset4 string into uint8 array buffer, before performing the
         * parsing subroutine.
         */
        Base64 : function(b64String){
            b64String = String(b64String);

            let raw = _atob(b64String);
            let rawLength = raw.length;
            let t_array = new Uint8Array(new ArrayBuffer(rawLength));

            for(let i=0; i<rawLength; i++) t_array[i] = raw.charCodeAt(i);
            return  MidiParser.Uint8(t_array) ;
        },

        /**
         * parse() : function reads the binary data, interpreting and spliting each chuck
         * and parsing it to a structured Object. When job is finised returns the object
         * or 'false' if any error was generated.
         */
        Uint8: function(FileAsUint8Array){
            let file = {
                data: null,
                pointer: 0,
                movePointer: function(_bytes){                                      // move the pointer negative and positive direction
                    this.pointer += _bytes;
                    return this.pointer;
                },
                readInt: function(_bytes){                                          // get integer from next _bytes group (big-endian)
                    _bytes = Math.min(_bytes, this.data.byteLength-this.pointer);
                    if (_bytes < 1) return -1;                                                                      // EOF
                    let value = 0;
                    if(_bytes > 1){
                        for(let i=1; i<= (_bytes-1); i++){
                            value += this.data.getUint8(this.pointer) * Math.pow(256, (_bytes - i));
                            this.pointer++;
                        }
                    }
                    value += this.data.getUint8(this.pointer);
                    this.pointer++;
                    return value;
                },
                readStr: function(_bytes){                                          // read as ASCII chars, the followoing _bytes
                    let text = '';
                    for(let char=1; char <= _bytes; char++) text +=  String.fromCharCode(this.readInt(1));
                    return text;
                },
                readIntVLV: function(){                                             // read a variable length value
                    let value = 0;
                    if ( this.pointer >= this.data.byteLength ){
                        return -1;                                                  // EOF
                    }else if(this.data.getUint8(this.pointer) < 128){               // ...value in a single byte
                        value = this.readInt(1);
                    }else{                                                          // ...value in multiple bytes
                        let FirstBytes = [];
                        while(this.data.getUint8(this.pointer) >= 128){
                            FirstBytes.push(this.readInt(1) - 128);
                        }
                        let lastByte  = this.readInt(1);
                        for(let dt = 1; dt <= FirstBytes.length; dt++){
                            value += FirstBytes[FirstBytes.length - dt] * Math.pow(128, dt);
                        }
                        value += lastByte;
                    }
                    return value;
                }
            };

            file.data = new DataView(FileAsUint8Array.buffer, FileAsUint8Array.byteOffset, FileAsUint8Array.byteLength);                                            // 8 bits bytes file data array
            //  ** read FILE HEADER
            if(file.readInt(4) !== 0x4D546864){
                console.warn('Header validation failed (not MIDI standard or file corrupt.)');
                return false;                                                       // Header validation failed (not MIDI standard or file corrupt.)
            }
            let headerSize          = file.readInt(4);                              // header size (unused var), getted just for read pointer movement
            let MIDI                = {};                                           // create new midi object
            MIDI.formatType         = file.readInt(2);                              // get MIDI Format Type
            MIDI.tracks             = file.readInt(2);                              // get ammount of track chunks
            MIDI.track              = [];                                           // create array key for track data storing
            let timeDivisionByte1   = file.readInt(1);                              // get Time Division first byte
            let timeDivisionByte2   = file.readInt(1);                              // get Time Division second byte
            if(timeDivisionByte1 >= 128){                                           // discover Time Division mode (fps or tpf)
                MIDI.timeDivision    = [];
                MIDI.timeDivision[0] = timeDivisionByte1 - 128;                     // frames per second MODE  (1st byte)
                MIDI.timeDivision[1] = timeDivisionByte2;                           // ticks in each frame     (2nd byte)
            }else MIDI.timeDivision  = (timeDivisionByte1 * 256) + timeDivisionByte2;// else... ticks per beat MODE  (2 bytes value)

            //  ** read TRACK CHUNK
            for(let t=1; t <= MIDI.tracks; t++){
                MIDI.track[t-1]     = {event: []};                                  // create new Track entry in Array
                let headerValidation = file.readInt(4);
                if ( headerValidation === -1 ) break;                               // EOF
                if(headerValidation !== 0x4D54726B) return false;                   // Track chunk header validation failed.
                file.readInt(4);                                                    // move pointer. get chunk size (bytes length)
                let e               = 0;                                            // init event counter
                let endOfTrack      = false;                                        // FLAG for track reading secuence breaking
                // ** read EVENT CHUNK
                let statusByte;
                let laststatusByte;
                while(!endOfTrack){
                    e++;                                                            // increase by 1 event counter
                    MIDI.track[t-1].event[e-1] = {};                                // create new event object, in events array
                    MIDI.track[t-1].event[e-1].deltaTime  = file.readIntVLV();      // get DELTA TIME OF MIDI event (Variable Length Value)
                    statusByte = file.readInt(1);                                   // read EVENT TYPE (STATUS BYTE)
                    if(statusByte === -1) break;                                    // EOF
                    else if(statusByte >= 128) laststatusByte = statusByte;         // NEW STATUS BYTE DETECTED
                    else{                                                           // 'RUNNING STATUS' situation detected
                        statusByte = laststatusByte;                                // apply last loop, Status Byte
                        file.movePointer(-1);                                       // move back the pointer (cause readed byte is not status byte)
                    }


                    //
                    // ** IS META EVENT
                    //
                    if(statusByte === 0xFF){                                        // Meta Event type
                        MIDI.track[t-1].event[e-1].type = 0xFF;                     // assign metaEvent code to array
                        MIDI.track[t-1].event[e-1].metaType =  file.readInt(1);     // assign metaEvent subtype
                        let metaEventLength = file.readIntVLV();                    // get the metaEvent length
                        switch(MIDI.track[t-1].event[e-1].metaType){
                            case 0x2F:                                              // end of track, has no data byte
                            case -1:                                                // EOF
                                endOfTrack = true;                                  // change FLAG to force track reading loop breaking
                                break;
                            case 0x01:                                              // Text Event
                            case 0x02:                                              // Copyright Notice
                            case 0x03:
                            case 0x04:                                              // Instrument Name
                            case 0x05:                                              // Lyrics)
                            case 0x07:                                              // Cue point                                         // Sequence/Track Name (documentation: http://www.ta7.de/txt/musik/musi0006.htm)
                            case 0x06:                                              // Marker
                                MIDI.track[t-1].event[e-1].data = file.readStr(metaEventLength);
                                break;
                            case 0x21:                                              // MIDI PORT
                            case 0x59:                                              // Key Signature
                            case 0x51:                                              // Set Tempo
                                MIDI.track[t-1].event[e-1].data = file.readInt(metaEventLength);
                                break;
                            case 0x54:                                              // SMPTE Offset
                                MIDI.track[t-1].event[e-1].data    = [];
                                MIDI.track[t-1].event[e-1].data[0] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[1] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[2] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[3] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[4] = file.readInt(1);
                                break;
                            case 0x58:                                              // Time Signature
                                MIDI.track[t-1].event[e-1].data    = [];
                                MIDI.track[t-1].event[e-1].data[0] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[1] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[2] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[3] = file.readInt(1);
                                break;
                            default :
                                // if user provided a custom interpreter, call it
                                // and assign to event the returned data
                                if( this.customInterpreter !== null){
                                    MIDI.track[t-1].event[e-1].data = this.customInterpreter( MIDI.track[t-1].event[e-1].metaType, file, metaEventLength);
                                }
                                // if no customInterpretr is provided, or returned
                                // false (=apply default), perform default action
                                if(this.customInterpreter === null || MIDI.track[t-1].event[e-1].data === false){
                                    file.readInt(metaEventLength);
                                    MIDI.track[t-1].event[e-1].data = file.readInt(metaEventLength);
                                    if (this.debug) console.info('Unimplemented 0xFF meta event! data block readed as Integer');
                                }
                        }
                    }

                    //
                    // IS REGULAR EVENT
                    //
                    else{                                                           // MIDI Control Events OR System Exclusive Events
                        statusByte = statusByte.toString(16).split('');             // split the status byte HEX representation, to obtain 4 bits values
                        if(!statusByte[1]) statusByte.unshift('0');                 // force 2 digits
                        MIDI.track[t-1].event[e-1].type = parseInt(statusByte[0], 16);// first byte is EVENT TYPE ID
                        MIDI.track[t-1].event[e-1].channel = parseInt(statusByte[1], 16);// second byte is channel
                        switch(MIDI.track[t-1].event[e-1].type){
                            case 0xF:{                                              // System Exclusive Events

                                // if user provided a custom interpreter, call it
                                // and assign to event the returned data
                                if( this.customInterpreter !== null){
                                    MIDI.track[t-1].event[e-1].data = this.customInterpreter( MIDI.track[t-1].event[e-1].type, file , false);
                                }

                                // if no customInterpretr is provided, or returned
                                // false (=apply default), perform default action
                                if(this.customInterpreter === null || MIDI.track[t-1].event[e-1].data === false){
                                    let event_length = file.readIntVLV();
                                    MIDI.track[t-1].event[e-1].data = file.readInt(event_length);
                                    if (this.debug) console.info('Unimplemented 0xF exclusive events! data block readed as Integer');
                                }
                                break;
                            }
                            case 0xA:                                               // Note Aftertouch
                            case 0xB:                                               // Controller
                            case 0xE:                                               // Pitch Bend Event
                            case 0x8:                                               // Note off
                            case 0x9:                                               // Note On
                                MIDI.track[t-1].event[e-1].data = [];
                                MIDI.track[t-1].event[e-1].data[0] = file.readInt(1);
                                MIDI.track[t-1].event[e-1].data[1] = file.readInt(1);
                                break;
                            case 0xC:                                               // Program Change
                            case 0xD:                                               // Channel Aftertouch
                                MIDI.track[t-1].event[e-1].data = file.readInt(1);
                                break;
                            case -1:                                                // EOF
                                endOfTrack = true;                                  // change FLAG to force track reading loop breaking
                                break;
                            default:
                                // if user provided a custom interpreter, call it
                                // and assign to event the returned data
                                if( this.customInterpreter !== null){
                                    MIDI.track[t-1].event[e-1].data = this.customInterpreter( MIDI.track[t-1].event[e-1].metaType, file , false);
                                }

                                // if no customInterpretr is provided, or returned
                                // false (=apply default), perform default action
                                if(this.customInterpreter === null || MIDI.track[t-1].event[e-1].data === false){
                                    console.log('Unknown EVENT detected... reading cancelled!');
                                    return false;
                                }
                        }
                    }
                }
            }
            return MIDI;
        },

        /**
         * custom function to handle unimplemented, or custom midi messages.
         * If message is a meta-event, the value of metaEventLength will be >0.
         * Function must return the value to store, and pointer of dataView needs
         * to be manually increased
         * If you want default action to be performed, return false
         */
        customInterpreter : null // function( e_type , arrayByffer, metaEventLength){ return e_data_int }
    };


    // if running in NODE export module
    if(typeof module !== 'undefined') module.exports = MidiParser;
    else{
        // if running in Browser, set a global variable.
        let _global = typeof window === 'object' && window.self === window && window ||
                    typeof self === 'object' && self.self === self && self ||
                    typeof global === 'object' && global.global === global && global;

        _global.MidiParser = MidiParser;
    }


    
})();

我们上传一个.mid文件后,使用MidiParser.parse(data)处理文件数据后,我们可以得到这样的的js对象

{
  "formatType": 1,
  "timeDivision": 120,
  "tracks": 4,
  "track": [
    {
      "event": [
        { "deltaTime": 0, "type": 255, "metaType": 88, "data": [4, 2, 24, 8] },
        { "deltaTime": 0, "type": 255, "metaType": 81, "data": 444444 },
        { "deltaTime": 24000, "type": 255, "metaType": 81, "data": 444444 },
        { "deltaTime": 0, "type": 255, "metaType": 88, "data": [4, 2, 24, 8] },
        { "deltaTime": 0, "type": 255, "metaType": 81, "data": 444444 },
        { "deltaTime": 24000, "type": 255, "metaType": 81, "data": 444444 },
        { "deltaTime": 1, "type": 255, "metaType": 47 }
      ]
    },
    {
      "event": [
        { "deltaTime": 0, "type": 255, "metaType": 1, "data": "start" },
        { "deltaTime": 0, "type": 12, "channel": 1, "data": 0 },
        { "deltaTime": 0, "type": 11, "channel": 1, "data": [0, 0] },
        { "deltaTime": 0, "type": 11, "channel": 1, "data": [7, 90] },
        { "deltaTime": 0, "type": 9, "channel": 1, "data": [79, 120] },
        { "deltaTime": 120, "type": 8, "channel": 1, "data": [79, 120] },
        ...
      ]
    }
  ]
}

根据midi文件的格式我们可以得知,timeDivision就是ticks_per_beat,type为255的是元事件,我们看metaType为81的是set_tempo,type为9是note_on,type为9是note_off,data对应的就是音符和音量,于是我们可以得到类似的代码

const mid2tracks = (data) => {
  console.log(data)
  let ticks_per_beat = data.timeDivision;
  let tracks = data.track.map((i) => i.event);
  let standard_tempo = 500000;
  let processed_notes = [];

  let current_event_indices = new Array(tracks.length).fill(0);
  let current_times = new Array(tracks.length).fill(0.0);
  let active_notes = Array.from({ length: tracks.length }, () => ({}));
  let active_tracks = [];

  while (true) {
    let next_event_time = null;
    let next_event_index = -1;
    let next_track_index = -1;

    for (let i = 0; i < tracks.length; i++) {
      if (current_event_indices[i] < tracks[i].length) {
        let event = tracks[i][current_event_indices[i]];
        let event_time =
          current_times[i] +
          (event.deltaTime / ticks_per_beat) * standard_tempo;

        if (next_event_time === null || event_time < next_event_time) {
          next_event_time = event_time;
          next_event_index = current_event_indices[i];
          next_track_index = i;
        }
      }
    }
    if (next_event_index === -1) break;
    let event = tracks[next_track_index][next_event_index];
    current_times[next_track_index] = next_event_time;
    if (event.type == 255 && event.metaType === 81) {
      standard_tempo = event.data;
    } else if (event.type === 9 && event.data[1] > 0) {
      let note = event.data[0];
      active_notes[next_track_index][note] = {
        startTime: next_event_time,
        velocity: event.data[1],
      };
    } else if (event.type === 8 || (event.type === 9 && event.data[1] === 0)) {
      let note = event.data[0];
      if (active_notes[next_track_index][note]) {
        let { startTime, velocity } = active_notes[next_track_index][note];
        delete active_notes[next_track_index][note];

        let duration = next_event_time - startTime;
        let standardStartTime = Math.round(startTime);
        let standardDuration = Math.round(duration);
        if (active_tracks.indexOf(next_track_index) === -1) {
          active_tracks.push(next_track_index);
        }
        processed_notes.push({
          note,
          start_time: standardStartTime,
          duration: standardDuration,
          velocity,
          track: next_track_index,
        });
      }
    }
    current_event_indices[next_track_index]++;
  }

  processed_notes.sort((a, b) => {
    if (a.start_time !== b.start_time) return a.start_time - b.start_time;
    return a.track - b.track;
  });
  active_tracks.sort();
  return [active_tracks, processed_notes];
};

//使用上传文件等组件获取一个文件
getFileData(file.raw).then((data) => {
  //使用Midi库转换为js对象
  const midArray = MidiParser.parse(data);
  const track = mid2tracks(midArray);
  ...
}

标准midi的音色及分类
{
  打击乐类: {
    0: { id: 0, label: "Percussion 打击乐" },
  },
  钢琴类: {
    1: { id: 1, label: "Acoustic Grand Piano 大钢琴(声学钢琴)" },
    2: { id: 2, label: "Bright Acoustic Piano 明亮的钢琴" },
    3: { id: 3, label: "Electric Grand Piano 电钢琴" },
    4: { id: 4, label: "Honky-tonk Piano 酒吧钢琴" },
    5: { id: 5, label: "Rhodes Piano 柔和的电钢琴" },
    6: { id: 6, label: "Chorused Piano 加合唱效果的电钢琴" },
    7: { id: 7, label: "Harpsichord 羽管键琴(拨弦古钢琴)" },
    8: { id: 8, label: "Clavichord 科拉维科特琴(击弦古钢琴)" },
  },
  色彩打击乐器类: {
    9: { id: 9, label: "Celesta 钢片琴" },
    10: { id: 10, label: "Glockenspiel 钟琴" },
    11: { id: 11, label: "Music box 八音盒" },
    12: { id: 12, label: "Vibraphone 颤音琴" },
    13: { id: 13, label: "Marimba 马林巴琴" },
    14: { id: 14, label: "Xylophone 木琴" },
    15: { id: 15, label: "Tubular Bells 管钟" },
    16: { id: 16, label: "Dulcimer 大扬琴" },
  },
  风琴类: {
    17: { id: 17, label: "Hammond Organ 击杆风琴" },
    18: { id: 18, label: "Percussive Organ 打击式风琴" },
    19: { id: 19, label: "Rock Organ 摇滚风琴" },
    20: { id: 20, label: "Church Organ 教堂风琴" },
    21: { id: 21, label: "Reed Organ 簧管风琴" },
    22: { id: 22, label: "Accordian 手风琴" },
    23: { id: 23, label: "Harmonica 口琴" },
    24: { id: 24, label: "Tango Accordian 探戈手风琴" },
  },
  吉他类: {
    25: { id: 25, label: "Acoustic Guitar (nylon) 尼龙弦吉他" },
    26: { id: 26, label: "Acoustic Guitar (steel) 钢弦吉他" },
    27: { id: 27, label: "Electric Guitar (jazz) 爵士电吉他" },
    28: { id: 28, label: "Electric Guitar (clean) 清音电吉他" },
    29: { id: 29, label: "Electric Guitar (muted) 闷音电吉他" },
    30: { id: 30, label: "Overdriven Guitar 加驱动效果的电吉他" },
    31: { id: 31, label: "Distortion Guitar 加失真效果的电吉他" },
    32: { id: 32, label: "Guitar Harmonics 吉他和音" },
  },
  贝司类: {
    33: { id: 33, label: "Acoustic Bass 大贝司(声学贝司)" },
    34: { id: 34, label: "Electric Bass(finger) 电贝司(指弹)" },
    35: { id: 35, label: "Electric Bass (pick) 电贝司(拨片)" },
    36: { id: 36, label: "Fretless Bass 无品贝司" },
    37: { id: 37, label: "Slap Bass 1 掌击贝司 1" },
    38: { id: 38, label: "Slap Bass 2 掌击贝司 2" },
    39: { id: 39, label: "Synth Bass 1 电子合成贝司 1" },
    40: { id: 40, label: "Synth Bass 2 电子合成贝司 2" },
  },
  弦乐类: {
    41: { id: 41, label: "Violin 小提琴" },
    42: { id: 42, label: "Viola 中提琴" },
    43: { id: 43, label: "Cello 大提琴" },
    44: { id: 44, label: "Contrabass 低音大提琴" },
    45: { id: 45, label: "Tremolo Strings 弦乐群颤音" },
    46: { id: 46, label: "Pizzicato Strings 弦乐群拨弦" },
    47: { id: 47, label: "Orchestral Harp 竖琴" },
    48: { id: 48, label: "Timpani 定音鼓" },
  },
  "合奏 / 合唱类": {
    49: { id: 49, label: "String Ensemble 1 弦乐合奏 1" },
    50: { id: 50, label: "String Ensemble 2 弦乐合奏 2" },
    51: { id: 51, label: "Synth Strings 1 合成弦乐合奏 1" },
    52: { id: 52, label: "Synth Strings 2 合成弦乐合奏 2" },
    53: { id: 53, label: "Choir Aahs 人声合唱“啊”" },
    54: { id: 54, label: "Voice Oohs 人声“嘟”" },
    55: { id: 55, label: "Synth Voice 合成人声" },
    56: { id: 56, label: "Orchestra Hit 管弦乐敲击齐奏" },
  },
  铜管类: {
    57: { id: 57, label: "Trumpet 小号" },
    58: { id: 58, label: "Trombone 长号" },
    59: { id: 59, label: "Tuba 大号" },
    60: { id: 60, label: "Muted Trumpet 加弱音器小号" },
    61: { id: 61, label: "French Horn 法国号(圆号)" },
    62: { id: 62, label: "Brass Section 铜管组(铜管乐器合奏音色)" },
    63: { id: 63, label: "Synth Brass 1 合成铜管音色 1" },
    64: { id: 64, label: "Synth Brass 2 合成铜管音色 2" },
  },
  簧管类: {
    65: { id: 65, label: "Soprano Sax 高音萨克斯风" },
    66: { id: 66, label: "Alto Sax 中音萨克斯风" },
    67: { id: 67, label: "Tenor Sax 次中音萨克斯风" },
    68: { id: 68, label: "Baritone Sax 低音萨克斯风" },
    69: { id: 69, label: "Oboe 双簧管" },
    70: { id: 70, label: "English Horn 英国管" },
    71: { id: 71, label: "Bassoon 巴松(大管)" },
    72: { id: 72, label: "Clarinet 单簧管(黑管)" },
  },
  笛类: {
    73: { id: 73, label: "Piccolo 短笛" },
    74: { id: 74, label: "Flute 长笛" },
    75: { id: 75, label: "Recorder 竖笛" },
    76: { id: 76, label: "Pan Flute 排箫" },
    77: { id: 77, label: "Bottle Blow 芦笛" },
    78: { id: 78, label: "Shakuhachi 日本尺八" },
    79: { id: 79, label: "Whistle 口哨声" },
    80: { id: 80, label: "Ocarina 奥卡雷那" },
  },
  合成主音类: {
    81: { id: 81, label: "Lead 1 (square) 合成主音 1 (方波)" },
    82: { id: 82, label: "Lead 2 (sawtooth) 合成主音 2 (锯齿波)" },
    83: { id: 83, label: "Lead 3 (caliope lead) 合成主音 3" },
    84: { id: 84, label: "Lead 4 (chiff lead) 合成主音 4" },
    85: { id: 85, label: "Lead 5 (charang) 合成主音 5" },
    86: { id: 86, label: "Lead 6 (voice) 合成主音 6 (人声)" },
    87: { id: 87, label: "Lead 7 (fifths) 合成主音 7 (平行五度)" },
    88: { id: 88, label: "Lead 8 (bass+lead) 合成主音 8 (贝司加主音)" },
  },
  合成音色类: {
    89: { id: 89, label: "Pad 1 (new age) 合成音色 1 (新世纪)" },
    90: { id: 90, label: "Pad 2 (warm) 合成音色 2  (温暖)" },
    91: { id: 91, label: "Pad 3 (polysynth) 合成音色 3" },
    92: { id: 92, label: "Pad 4 (choir) 合成音色 4  (合唱)" },
    93: { id: 93, label: "Pad 5 (bowed) 合成音色 5" },
    94: { id: 94, label: "Pad 6 (metallic) 合成音色 6  (金属声)" },
    95: { id: 95, label: "Pad 7 (halo) 合成音色 7  (光环)" },
    96: { id: 96, label: "Pad 8 (sweep) 合成音色 8" },
  },
  合成效果类: {
    97: { id: 97, label: "FX 1 (rain) 合成效果 1 雨声" },
    98: { id: 98, label: "FX 2 (soundtrack) 合成效果 2 音轨" },
    99: { id: 99, label: "FX 3 (crystal) 合成效果 3 水晶" },
    100: { id: 100, label: "FX 4 (atmosphere) 合成效果 4 大气" },
    101: { id: 101, label: "FX 5 (brightness) 合成效果 5 明亮" },
    102: { id: 102, label: "FX 6 (goblins) 合成效果 6 鬼怪" },
    103: { id: 103, label: "FX 7 (echoes) 合成效果 7 回声" },
    104: { id: 104, label: "FX 8 (sci-fi) 合成效果 8 科幻" },
  },
  民间乐器类: {
    105: { id: 105, label: "Sitar 西塔尔(印度)" },
    106: { id: 106, label: "Banjo 班卓琴(美洲)" },
    107: { id: 107, label: "Shamisen 三昧线(日本)" },
    108: { id: 108, label: "Koto 十三弦筝(日本)" },
    109: { id: 109, label: "Kalimba 卡林巴" },
    110: { id: 110, label: "Bagpipe 风笛" },
    111: { id: 111, label: "Fiddle 民族提琴" },
    112: { id: 112, label: "Shanai 山奈" },
  },
  打击乐器类: {
    113: { id: 113, label: "Tinkle Bell 叮当铃" },
    114: { id: 114, label: "Agogo Agogo" },
    115: { id: 115, label: "Steel Drums 钢鼓" },
    116: { id: 116, label: "Woodblock 木鱼" },
    117: { id: 117, label: "Taiko Drum 太鼓" },
    118: { id: 118, label: "Melodic Tom 通通鼓" },
    119: { id: 119, label: "Synth Drum 合成鼓" },
    120: { id: 120, label: "Reverse Cymbal 铜钹" },
  },
}

本文作者: 小世炎
本文链接: https://www.xiaoshiyan.top/archives/537
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议 转载请注明出处!
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇