早在大学的时候做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 Off | 0x8n | 音符(0-127) | 力度(0-127) | 3 |
| Note On | 0x9n | 音符(0-127) | 力度(0-127) | 3 |
| 控制改变 | 0xBn | 控制器号 | 值 | 3 |
| 音色改变 | 0xCn | 音色号 | – | 2 |
| 通道压力 | 0xDn | 压力值 | – | 2 |
| 弯音 | 0xEn | LSB(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,轨道结束
七、文件格式类型
- 格式0:单音轨,所有事件在一个音轨中
- 格式1:多音轨,多个音轨同时播放
- 格式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在我这个案例中用处不大)。我们可以做一些推测,并进行实验,我这里直接给出结论:
- time的单位是tick
- 每个音轨都有时间轴,事件的事件都是相对时间(例如第一个事件的time是100,第二个是200,意思是第一个发生在全局时间100ticks处,第二个发生在300ticks处)根据 ticks/ticks_per_beat*tempo 可以得到事件发生的现实世界微秒数
- note事件的note表示从21开始为第一个的钢琴对应琴键
- velocity表示0-127的音量
- 一个note值相同的note_on和note_off是一次完整的琴键弹奏,这个note_off距离它对应note_on中间所有的时间累加是这个note的持续时间
- 例外的,有的.mid文件并没有那么标准,你可能找不到任何一个note_off,它是使用velocity为0的note_on来表示音符结束的,所有一个完整的音符要考虑这两个条件
- 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 铜钹" },
},
}



