实现了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文件的解析和播放_Java


解析完毕,实现midi文件的播放,调用了Windows SDK midiOutShortMsg播放。

首先要在Java中定义3个native方法,采用64bit编译,句柄定义为long:

public static native int midiOutOpen(long[] phmo,  int uDeviceID);
public static native int midiOutShortMsg(long hmo, int dwData);
public static native int midiOutClose(long hmo);
  • 1.
  • 2.
  • 3.

定义后通过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.

播放日志如下:

Midi文件的解析和播放_Midi解析_02

播放过程中会显示日志:[线程id]Play 事件类型 - ticks长度(转换后的毫秒), 事件 at 通道号,事件参数。

调用解析播放程序:parse("C:\\Users\\cc\\OneDrive\\src\\resource\\Pachelbel Johann — Canon in D.mid"); 即可听到midi音乐播放。