实现了Midi(musical Instrument Digital Interface)(SMF(Standard MIDI File))文件解析和播放。
程序使用JDK1.8 64bit + C 64bit开发和编译。 JDK位数和C编译位数要保持一致。
Midi解析程序如下:
public static void parse(String file) throws IOException, InterruptedException {
MThd mthd = new MThd();
byte[] _data = Files.readAllBytes(Paths.get(file));
BytesArrayInputBuffer bis = new BytesArrayInputBuffer(_data, ByteOrder.BIG_ENDIAN); // 按大端度
mthd.type = bis.ReadAsciiString(4); // 读入标志MTHD
mthd.length = bis.ReadInt32(); // 读入文件头长度
mthd.format = bis.ReadUInt16(); // 读入格式0/1/2
mthd.channelNumber = bis.ReadUInt16(); // 读入轨道数量
mthd.timeunit = bis.ReadUInt16(); // 读入tick数量
while (bis.isEnd() == false) {
MTrk mtrk = new MTrk(); // 读取音频轨道
mtrk.type = bis.ReadAsciiString(4); // 读取标志MTRK
mtrk.length = bis.ReadInt32(); // 读取轨道数据长度
byte[] data = bis.ReadBytes(mtrk.length); // 读取轨道事件
BytesArrayInputBuffer trunk = new BytesArrayInputBuffer(data);
int lastevent = -1; // 记录上一次的midi事件
while (trunk.isEnd() == false) {
int deltatimes = trunk.ReadVarInt(); // 读取事件的ticks数量
int event = trunk.ReadUInt8(); // 读入事件
if (event == 0xF0) { // 处理 sysex 事件
SysexEvents sysex = new SysexEvents(deltatimes);
int len = trunk.ReadVarInt();
byte[] body = trunk.ReadBytes(len);
sysex.addSysEvent(new SysexEvent1(body));
while (body[body.length - 1] != 0xF7) {
int deltatime = trunk.ReadVarInt();
int f7 = trunk.ReadUInt8();
len = trunk.ReadVarInt();
body = trunk.ReadBytes(len);
sysex.addSysEvent(new SysexEvent2(deltatime, body));
}
mtrk.addEvent(sysex);
}
else if (event >= 0xF1 && event <= 0xFE) {
throw new java.lang.UnsupportedOperationException(event + "");
}
else if (event == 0xFF) { // 处理 meta-event 事件
int type = trunk.ReadUInt8();
int len = trunk.ReadVarInt();
byte[] body = trunk.ReadBytes(len);
MetaEvent e = new MetaEvent(deltatimes, type, body);
mtrk.addEvent(e );
// if (type == 0x2F)
// break;
}
else if (event>=0 && event<=0x7F) { // 处理忽略midi事件标志的midi事件
int type = lastevent >> 4; // 事件类型-高4位
int channel = lastevent & 0x0F; // 读取通道-低4位
int param1 = -1, param2 = -1;
switch (type) {
case 0x8: // 松开音符
param1 = event; // 音符
param2 = trunk.ReadUInt8(); // 速率
break;
case 0x9: // 按下音符
param1 = event; // 音符
param2 = trunk.ReadUInt8(); // 速率
break;
case 0xA: // 触后音符
param1 = event; // 音符
param2 = trunk.ReadUInt8(); // 速率
break;
case 0xB: // 控制器
param1 = event; // 控制器号码
param2 = trunk.ReadUInt8(); // 控制器参数
break;
case 0xC: // 改变乐器
param1 = event; // 乐器号码
break;
case 0xD: // 触后通道
param1 = event; //
break;
case 0xE: // 滑音
param1 = event; // 高音高位
param2 = trunk.ReadUInt8(); // 高音低位
break ;
}
MidiEvent evt = new MidiEvent(deltatimes, type, channel, param1, param2);
mtrk.addEvent(evt);
}
else if (event>=0x80 && event<=0xEF) { // 处理MIDI event
int type = (event >> 4); // 事件类型-高4位
int channel = event & 0x0f; // 读取通道-低4位
int param1 = -1, param2 = -1;
switch (type) {
case 0x8: // 松开音符
param1 = trunk.ReadUInt8(); // 音符
param2 = trunk.ReadUInt8(); // 速率(音量)
break;
case 0x9: // 按下音符
param1 = trunk.ReadUInt8(); // 音符
param2 = trunk.ReadUInt8(); // 速率
break;
case 0xA: // 触后音符
param1 = trunk.ReadUInt8(); // 音符
param2 = trunk.ReadUInt8(); // 速率
break;
case 0xB: // 控制器
param1 = trunk.ReadUInt8(); // 控制器号码
param2 = trunk.ReadUInt8(); // 控制器参数
break;
case 0xC: // 改变乐器
param1 = trunk.ReadUInt8(); // 乐器号码
break;
case 0xD: // 触后通道
param1 = trunk.ReadUInt8(); //
break;
case 0xE: // 滑音
param1 = trunk.ReadUInt8(); // 高音高位
param2 = trunk.ReadUInt8(); // 高音低位
break ;
}
MidiEvent evt = new MidiEvent(deltatimes, type, channel, param1, param2);
mtrk.addEvent(evt);
lastevent = event;
}
}
mthd.addMTrk(mtrk);
}
....
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
解析后显示如下:
解析完毕,实现midi文件的播放,调用了Windows SDK midiOutShortMsg播放。
首先要在Java中定义3个native方法,采用64bit编译,句柄定义为long:
定义后通过javap命令生成c header文件
/*
* Class: audio_midi_MIDI
* Method: midiOutOpen
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_audio_midi_MIDI_midiOutOpen
(JNIEnv *, jobject, jlongArray, jint);
/*
* Class: audio_midi_MIDI
* Method: midiOutShortMsg
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_audio_midi_MIDI_midiOutShortMsg
(JNIEnv *, jobject, jlong, jint);
/*
* Class: audio_midi_MIDI
* Method: midiOutClose
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_audio_midi_MIDI_midiOutClose
(JNIEnv *, jobject, jlong);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
编写头文件实现,如下:
#include "audio_midi_MIDI.h"
#include <windows.h>
#include <mmeapi.h>
JNIEXPORT jint JNICALL Java_audio_midi_MIDI_midiOutOpen(JNIEnv *env, jobject obj, jlongArray phmo, jint uDeviceID) {
HMIDIOUT out;
MMRESULT result = midiOutOpen(&out, uDeviceID, 0, 0, CALLBACK_NULL);
(*env)->SetLongArrayRegion(env, phmo, 0, 1, &out); // 回填句柄
return result;
}
JNIEXPORT jint JNICALL Java_audio_midi_MIDI_midiOutShortMsg(JNIEnv *env, jobject obj, jlong hmo, jint dwMsg) {
return midiOutShortMsg((HMIDIOUT)hmo, dwMsg);;
}
JNIEXPORT jint JNICALL Java_audio_midi_MIDI_midiOutClose(JNIEnv *env, jobject obj, jlong hmo) {
return midiOutClose((HMIDIOUT)hmo);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
编译时采用x64位进行编译。(如果采用了32bit编译,需要将java定义的native方法中句柄类型从long改成int,并使用32bit JDK运行),链接时要加入#pragma comment(lib, "winmm.lib")库,编译后生成midi.dll文件,
编写java测试程序,调用如果能正常运行,可从音箱听到钢琴声音。
public static void main(String[] args) throws IOException, InterruptedException {
System.load("C:\\Users\\cc\\OneDrive\\src\\MIDI.dll");
long[] hmo = new long[1]; // 接收句柄
int result = midiOutOpen(hmo, 0); // 打开midi设备0, 返回值含义见windows sdk[MMRESULT]
long phmo = hmo[0]; // 返回的句柄
result = midiOutShortMsg(phmo, 0x00403c90); // 输出测试声音,会从音箱听到钢琴声
Thread.sleep(2000);
result = midiOutClose(phmo); //关闭midi设备
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
完成的播放程序如下,对于1模式,采用多线程实现各声轨同时播放,Player继承Thread对象
long[] hmo = new long[1];
int result = midiOutOpen(hmo, 0); //打开midi设备
long phmo = hmo[0];
if (mthd.getFormat() == 1) { // 多声轨并行播放
CountDownLatch latch = new CountDownLatch(mthd.getMtrks().size());
for (int i=mthd.getMtrks().size()-1; i>=0 ; i--) {
MTrk mtrk = mthd.getMtrks().get(i);
Vector<MTrk> v = new Vector<>();
v.add(mtrk);
Player p = new Player(latch, phmo, mthd, v); // 通过多线程实现声轨并行播放
p.start();
}
latch.await();
}
result = midiOutClose(phmo); // 播放完毕,关闭midi设备
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
播放代码如下:
class Player extends Thread {
CountDownLatch latch;
long phmo; // 存储midi设备句柄
MThd mthd; // midi文件头
List<MTrk> mtrks; // 要播放的声轨
volatile static int microsecond = 500000; // 每拍微秒数, 线程共享变量
Player(CountDownLatch latch, long phmo, MThd mthd, List<MTrk> mtrks) {
this.latch = latch;
this.phmo = phmo;
this.mthd = mthd;
this.mtrks = mtrks;
}
@Override
public void run() {
try {
for (MTrk mtrk : mtrks) // 对声轨循环
for (Event event : mtrk.getEvents()) { // 对事件循环
// 每个事件的ticks * 每个节拍的微秒 / 四分音符(每个四分音符1拍)的ticks
int time = event.getDeltaTimes() * (microsecond / mthd.getTimeunit()) / 1000; // 将ticks转换为java的毫秒数
if (time != 0) Thread.sleep(time) ;
if (event instanceof MidiEvent) {
MidiEvent mEvt = (MidiEvent) event;
int d = mEvt.getDWORD(); // 参数转换为int
int result = MIDI.midiOutShortMsg(phmo, d); // 播放
if (result != 0)
throw new java.lang.RuntimeException();
System.out.println("["+this.getId() + "]Play Midi - " + mEvt.getDeltaTimes() + "(" + time + ")" + ": " + mEvt.getTypeString() + " at 通道" + mEvt.getChannel() + ", "+ mEvt.getParamString() );
}
else if (event instanceof MetaEvent) {
MetaEvent evt = (MetaEvent) event;
if (evt.getType() == 0x51) {
microsecond = (int) evt.getData(); // 设置每个 MIDI 四分音符的微秒数
}
System.out.println("["+this.getId() + "]Play Meta - " + evt.getDeltaTimes() + "(" + time + ")" + "," + evt.getTypeString() + ":" + evt.getData() );
}
else if (event instanceof SysexEvents) { // 未实现
SysexEvents evt = (SysexEvents) event;
List<SysexEvent> evts = evt.getSysexEvent();
}
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
latch.countDown();
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
播放日志如下:
播放过程中会显示日志:[线程id]Play 事件类型 - ticks长度(转换后的毫秒), 事件 at 通道号,事件参数。
调用解析播放程序:parse("C:\\Users\\cc\\OneDrive\\src\\resource\\Pachelbel Johann — Canon in D.mid"); 即可听到midi音乐播放。