Java 早期僅支援 Applet 播放音頻,JDK 1.3+ 引入 javax.sound 套件,提供 Clip(採樣音頻 WAV/AIFF/AU)與 Sequencer(MIDI)兩大引擎。但官方 API 存在資源釋放不徹底、記憶體洩漏、重複播放崩潰等問題,需二次封裝才能生產可用。
URI(網絡/本地統一)、SoundData(位元組快取+元數據)SoundPlay 統一管理 Clip/Sequencer 生命週期load()、play()、autoPlay()、stop()、reset()public class URI {
public static final int HTTP = 1, FILE = 2;
private final String uri;
private final int type;
public URI(String uri, int type) { this.uri = uri; this.type = type; }
public URI(String uri) { this(uri, HTTP); }
public byte[] getData() {
if (type == HTTP) return StreamHelper.getHttpStream(uri); // 需自行實作 HTTP 下載
return StreamHelper.getFileStream(uri); // 本地文件讀取
}
}
public class SoundData {
public static final int MIDI = 1, WAV = 2;
private final byte[] data;
private final int type;
private final boolean loop;
public SoundData(URI uri, int type, boolean loop) {
this(uri.getData(), type, loop);
}
public SoundData(byte[] data, int type, boolean loop) {
this.data = data.clone(); // 防禦性複製
this.type = type;
this.loop = loop;
}
public SoundData(String filePath, int type, boolean loop) {
this(StreamHelper.getFileStream(filePath), type, loop);
}
// getters...
}
public class SoundPlay implements MetaEventListener, Runnable {
private Clip clip; // WAV 播放器
private Sequencer sequencer; // MIDI 播放器
private int soundType; // 當前類型
private boolean loop;
private Thread autoThread; // 自動播放線程
public boolean load(SoundData data) {
reset(); // 釋放舊資源
if (data == null || data.getData() == null) return false;
return init(data.getData(), data.getType(), data.getLoop());
}
private boolean init(byte[] data, int type, boolean loop) {
this.loop = loop;
ByteArrayInputStream bis = new ByteArrayInputStream(data);
try {
if (type == SoundData.MIDI) {
sequencer = MidiSystem.getSequencer();
sequencer.open();
sequencer.addMetaEventListener(this); // 監聽循環結束事件
Sequence seq = MidiSystem.getSequence(bis);
sequencer.setSequence(seq);
soundType = SoundData.MIDI;
} else { // WAV
AudioFileFormat fmt = AudioSystem.getAudioFileFormat(bis);
DataLine.Info info = new DataLine.Info(Clip.class, fmt.getFormat());
clip = (Clip) AudioSystem.getLine(info);
clip.open(fmt.getFormat(), data, 0, data.length);
soundType = SoundData.WAV;
}
return true;
} catch (Exception e) { reset(); return false; }
finally { closeQuietly(bis); }
}
public boolean play() {
if (soundType == SoundData.MIDI && sequencer != null) {
sequencer.start(); return true;
}
if (soundType == SoundData.WAV && clip != null) {
if (loop) clip.loop(Clip.LOOP_CONTINUOUSLY);
else { clip.setFramePosition(0); clip.start(); }
return true;
}
return false;
}
// 自動播放:播放結束後自動釋放資源
public boolean autoPlay(SoundData data) {
if (!load(data)) return false;
autoThread = new Thread(this);
autoThread.start();
return true;
}
@Override public void run() {
play();
while (isPlaying()) {
sleep(100);
}
reset(); // 自動清理
}
private boolean isPlaying() {
return (clip != null && clip.isRunning()) || (sequencer != null && sequencer.isRunning());
}
public void stop() {
if (clip != null) clip.stop();
if (sequencer != null) sequencer.stop();
}
public void reset() {
stop();
if (clip != null) { clip.close(); clip = null; }
if (sequencer != null) { sequencer.close(); sequencer = null; }
autoThread = null;
}
// MIDI 循環處理:MetaEventListener 回調
@Override public void meta(MetaMessage meta) {
if (loop && soundType == SoundData.MIDI && meta.getType() == 47) { // 47 = End of Track
sequencer.setMicrosecondPosition(0);
sequencer.start();
}
}
}
| 問題 | 解決方案 |
|---|---|
| Clip/Sequencer 不釋放導致記憶體洩漏 | reset() 中顯式 close(),並置 null 協助 GC |
| 重複播放崩潰 | load() 開頭調用 reset(),確保單實例單播放器 |
| MIDI 循環播放 | 實作 MetaEventListener,捕獲 Type 47 事件重置位置 |
| WAV 循環播放 | clip.loop(Clip.LOOP_CONTINUOUSLY) |
| 資源來源多樣(網絡/本地/字節陣列) | SoundData 多構造器 + URI 統一抽象 |
SoundPlay player = new SoundPlay();
// 1. 播放本地 WAV,循環
player.autoPlay(new SoundData("bgm.wav", SoundData.WAV, true));
// 2. 播放網絡 MIDI,單次
player.play(new SoundData(new URI("https://example.com/music.mid"), SoundData.MIDI, false));
// 3. 手動控制
player.load(new SoundData(bytes, SoundData.WAV, false));
player.play();
// ... 遊戲邏輯 ...
player.stop();
player.reset();
public static byte[] toByteArray(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) baos.write(buffer, 0, len);
return baos.toByteArray();
}