最近折腾Java的MIDI功能,发现网上的教程大多只讲到怎么用Sequencer,更深入的比较难找,而且大都没的注释,于是自己踩坑无数,来这里发点稍微深入使用Java的MIDI功能的示例(嘛其实也没多么深入,毕竟我玩Java也只是刚入门的水平而已)
运行环境及测试如下,Mac虚拟机,Oracle JDK 8u201。
bogon:temp donmor$ screenfetch
readlink: illegal option -- f
usage: readlink [-n] [file ...]
awk: can't open file /proc/fb
source line number 1
/usr/local/bin/screenfetch: line 1341: [: =: unary operator expected
-/+:. donmor@bogon
:++++. OS: 64bit Mac OS X 10.11.6 15G22010
/+++/. Kernel: x86_64 Darwin 15.6.0
.:-::- .+/:-``.::- Uptime: 39m
.:/++++++/::::/++++++/:` Packages: 9
.:///////////////////////:` Shell: bash 3.2.57
////////////////////////` Resolution: 1918x888
-+++++++++++++++++++++++` DE: Aqua
/++++++++++++++++++++++/ WM: Quartz Compositor
/sssssssssssssssssssssss. WM Theme: Blue
:ssssssssssssssssssssssss- Font: Monaco
osssssssssssssssssssssssso/` CPU: Intel Core i5-4210U @ 1.70GHz
`syyyyyyyyyyyyyyyyyyyyyyyy+` GPU:
`ossssssssssssssssssssss/ RAM: 2402MiB / 4096MiB
:ooooooooooooooooooo+.
`:+oo+/:-..-:/+o+/-
bogon:temp donmor$ java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)
bogon:temp donmor$ java MIDITest 01.mid
下面贴代码:
bogon:temp donmor$ cat MIDITest.java
import java.io.File;
import javax.sound.midi.*;
public class MIDITest {
public static void main(String[] args) {
try {
File midif = new File(args[0]);//打开MIDI文件,这里是直接从参数的第一位读取
Sequence seq = MidiSystem.getSequence(midif);//加载文件到序列(Sequence)中
Sequencer midip= MidiSystem.getSequencer();//创建一个音序器(Sequencer),即播放器的核心
midip.open();//启动音序器,
midip.setSequence(seq);//把序列插入到音序器中
if(!midip.isRunning())
midip.start();//开始播放
long time = midip.getMicrosecondLength() / 1000;//获取MIDI文件长度
Thread.sleep(time);//让程序等待,直到播放结束
if(midip.isRunning())
midip.stop();
if(midip.isOpen())
midip.close();//此四句关闭音序器,程序主体结束
} catch(Exception e) {
e.printStackTrace();//处理异常
}
}
}
如果只是听个响,到这里就可以不用看了。然而说好要深入一点的……
那么上第二版,加入了MidiDevice
bogon:temp donmor$ cat MIDITest.java
import java.io.File;
import java.util.ArrayList;
import javax.sound.midi.*;
public class MIDITest {
private static MidiDevice midid;
public static void main(String[] args) {
try {
MidiDevice.Info[] vdevs = MidiSystem.getMidiDeviceInfo();//获取所有MIDI设备信息
ArrayList<MidiDevice.Info> xdevs = new ArrayList<MidiDevice.Info>();//准备筛除一些不能用来播放的设备,用这个ArrayList存放合格的
for (MidiDevice.Info dev : vdevs) {
String s = dev.getName();
try {
MidiDevice vc = MidiSystem.getMidiDevice(dev);//启动每个设备试验
vc.getReceiver();//直接访问Receiver,如果没有就走catch
vc.close();//关闭设备
} catch (MidiUnavailableException e) {
s = "$NORECEIVER";//利用名称筛除没有Receiver的设备
}
if (s != "Real Time Sequencer" && s != "$NORECEIVER")//筛除没有Receiver的设备和Real Time Sequencer(这个是音序器的Receiver,用来做MIDI录制的,当然不能要它)
xdevs.add(dev);//过检的插进ArrayList里
}
MidiDevice.Info[] arrw = new MidiDevice.Info[xdevs.size()];
MidiDevice.Info[] devs = xdevs.toArray(arrw);//这两句用来把ArrayList转为数组,注意不能直接toArray
if (args[0].equals("-devlist")) {//读取命令行参数第一位,如果是-devlist的话
System.out.println("ID Name Description Vendor Version");
int id = 0;
for (MidiDevice.Info dev : devs) {
System.out.println(String.valueOf(id) + " " + dev.getName() + " " + dev.getDescription() + " " + dev.getVendor() + " " + dev.getVersion());
id += 1;
}
System.exit(0);//结束
}
String arg1 = "", arg2 = "";//准备读第二、三个参数
try {
arg1 = args[1];
arg2 = args[2];//读第二、三参数,若参数不全则抛异常跳过
} catch (Exception e) {
}
File midif = new File(args[0]);//打开MIDI文件,这里是直接从参数的第一位读取
Sequence seq = MidiSystem.getSequence(midif);//加载文件到序列(Sequence)中
Sequencer midip= MidiSystem.getSequencer(!arg1.equals("-dev"));//创建一个音序器(Sequencer),即播放器的核心,这里如果传了-dev参数,就不自动连接默认设备
midip.open();//启动音序器,
if (arg1.equals("-dev")) {//检查参数
midid = MidiSystem.getMidiDevice(devs[Integer.parseInt(arg2)]);//获取MIDI设备
midid.open();//启动MIDI设备
midip.getTransmitter().setReceiver(midid.getReceiver());//关键的一步,把之前的音序器的Transmitter和MidiDevice的Receiver挂接起来
}
midip.setSequence(seq);//把序列插入到音序器中
if (!midip.isRunning())
midip.start();//开始播放
Runtime.getRuntime().addShutdownHook(new Thread() {//加入退出检测线程,Ctrl-C时停止并释放资源
public void run() {
try {
System.out.println("Quit");
if (midip.isRunning())
midip.stop();
if (midip.isOpen())
midip.close();
if (midid != null && midid.isOpen())
midid.close();//关闭音序器和设备,结束程序
} catch (Exception e) {
e.printStackTrace();
}
}
});
long time = midip.getMicrosecondLength() / 1000;//获取MIDI文件长度
Thread.sleep(time);//让程序等待,直到播放结束
if (midip.isRunning())
midip.stop();
if (midip.isOpen())
midip.close();
if (midid != null && midid.isOpen())
midid.close();//此六句关闭音序器和设备,程序主体结束
} catch(Exception e) {
e.printStackTrace();//处理异常
}
}
}
bogon:temp donmor$ java MIDITest -devlist
ID Name Description Vendor Version
0 Gervill Software MIDI Synthesizer OpenJDK 1.0
1 FluidSynth virtual port (Qsynth1) FluidSynth virtual port (Qsynth1) Unknown vendor Unknown version
bogon:temp donmor$ java MIDITest 01.mid -dev 1
^CQuit
解释一下,此版相比上一版加入MidiDevice的处理,用-devlist参数可以输出全部可以用来输出的设备,再用命令行参数-dev加id选择设备,然后和之前的音序器挂接;此外加入退出处理的代码,防止使用外部设备时Ctrl-C中止程序后设备一直播放最后那一个音,需要按panic的情况(这里我就用了Qsynth)
然而还没有结束!上最终版,支持直接读取sf2音色库文件:
bogon:temp donmor$ cat MIDITest.java
import java.io.File;
import java.util.ArrayList;
import javax.sound.midi.*;
public class MIDITest {
private static MidiDevice midid;
public static void main(String[] args) {
try {
MidiDevice.Info[] vdevs = MidiSystem.getMidiDeviceInfo();//获取所有MIDI设备信息
ArrayList<MidiDevice.Info> xdevs = new ArrayList<MidiDevice.Info>();//准备筛除一些不能用来播放的设备,用这个ArrayList存放合格的
for (MidiDevice.Info dev : vdevs) {
String s = dev.getName();
try {
MidiDevice vc = MidiSystem.getMidiDevice(dev);//启动每个设备试验
vc.getReceiver();//直接访问Receiver,如果没有就走catch
vc.close();//关闭设备
} catch (MidiUnavailableException e) {
s = "$NORECEIVER";//利用名称筛除没有Receiver的设备
}
if (s != "Real Time Sequencer" && s != "$NORECEIVER")//筛除没有Receiver的设备和Real Time Sequencer(这个是音序器的Receiver,用来做MIDI录制的,当然不能要它)
xdevs.add(dev);//过检的插进ArrayList里
}
MidiDevice.Info[] arrw = new MidiDevice.Info[xdevs.size()];
MidiDevice.Info[] devs = xdevs.toArray(arrw);//这两句用来把ArrayList转为数组,注意不能直接toArray
if (args[0].equals("-devlist")) {//读取命令行参数第一位,如果是-devlist的话
System.out.println("ID Name Description Vendor Version");
int i = 0;
for (MidiDevice.Info dev : devs) {
System.out.println(String.valueOf(i) + " " + dev.getName() + " " + dev.getDescription() + " " + dev.getVendor() + " " + dev.getVersion());
i++;
}
System.exit(0);//结束
} else if (args[0].equals("-help")) {
System.out.println("Usage: MIDITest [FILE] [OPTION] ...");
System.out.println(" MIDITest [OPTION]");
System.out.println("A test application that plays MIDI files.");
System.out.println("");
System.out.println("Options:");
System.out.println(" -dev ID Use specific device to play the MIDI file. Use ");
System.out.println(" -devlist option to enquire about device IDs.");
System.out.println(" -sf2 Load sf2 files into the Gervill Software MIDI ");
System.out.println(" Synthesizer provided by Java by default. ");
System.out.println(" Please note that following sf2 files overwrites");
System.out.println(" previous ones.");
System.out.println(" -help Display this help and exit.");
System.out.println(" -devlist List available MIDI devices and exit.");
System.exit(0);//显示帮助并退出
}
String arg1 = "";
String[] arg2 = new String[1];
arg2[0] = "";//准备读更多参数
ArrayList<String> arg2s = new ArrayList<String>();
try {
arg1 = args[1];
int i = 0;
boolean granted = false;
for (String arg : args) {
if (i > 1)
arg2s.add(arg);
i++;
}
String[] arrw2 = new String[arg2s.size()];
arg2 = arg2s.toArray(arrw2);//读参数,若参数不全则抛异常跳过
} catch (Exception e) {
}
File midif = new File(args[0]);//打开MIDI文件,这里是直接从参数的第一位读取
Sequence seq = MidiSystem.getSequence(midif);//加载文件到序列(Sequence)中
Sequencer midip= MidiSystem.getSequencer(!(arg1.equals("-dev") || arg1.equals("-sf2")));//创建一个音序器(Sequencer),即播放器的核心,这里如果传了-dev或-sf2参数,就不自动连接默认设备
midip.open();//启动音序器
if (arg1.equals("-dev")) {//检查参数
midid = MidiSystem.getMidiDevice(devs[Integer.parseInt(arg2[0])]);//获取MIDI设备
midid.open();//启动MIDI设备
midip.getTransmitter().setReceiver(midid.getReceiver());//关键的一步,把之前的音序器的Transmitter和MidiDevice的Receiver挂接起来
} else if (arg1.equals("-sf2")) {
midid = MidiSystem.getMidiDevice(devs[0]);//获取MIDI设备
midid.open();//启动MIDI设备
Synthesizer midis = (Synthesizer) midid;//获取合成器
for (String sf2 : arg2) {//对每个从参数传来的sf2文件:
try {
Soundbank sbx = MidiSystem.getSoundbank(new File(sf2));
midis.loadAllInstruments(sbx);//顺序加入合成器中
} catch (Exception e) {
}
}
midip.getTransmitter().setReceiver(midid.getReceiver());//关键的一步,把之前的音序器的Transmitter和MidiDevice的Receiver挂接起来
}
midip.setSequence(seq);//把序列插入到音序器中
if (!midip.isRunning()){
System.out.println("Now Playing...");
midip.start();//开始播放
}
Runtime.getRuntime().addShutdownHook(new Thread() {//加入退出检测线程,Ctrl-C时停止并释放资源
public void run() {
try {
System.out.println("[Quit]");
if (midip.isRunning())
midip.stop();
if (midip.isOpen())
midip.close();
if (midid != null && midid.isOpen())
midid.close();//关闭音序器和设备,结束程序
} catch (Exception e) {
e.printStackTrace();
}
}
});
long time = midip.getMicrosecondLength() / 1000;//获取MIDI文件长度
Thread.sleep(time);//让程序等待,直到播放结束
if (midip.isRunning())
midip.stop();
if (midip.isOpen())
midip.close();
if (midid != null && midid.isOpen())
midid.close();//此六句关闭音序器和设备,程序主体结束
} catch(Exception e) {
e.printStackTrace();//处理异常
}
}
}
bogon:temp donmor$ java MIDITest 01.mid -sf2 ~/Documents/SGM-180_v1.5.sf2
Now Playing...
^C[Quit]
这一版改了命令参数解析代码,使用-sf2参数加sf2文件名可以导入多个sf2音色库,载入Gervill设备后cast出合成器Synthesizer,然后逐个加入sf2文件(后加载的会覆盖先加载的重复部分),最后挂接;另外加了一点提示信息和帮助(-help)
以上~代码略丑,权当抛砖引玉了
另外有把MIDI部分单独包装成类、用Swing做了UI的版本在这里:
https://github.com/donmor/Java-MIDI-Player