Java 多媒體能力有限,但輕量級背景音樂 (MIDI) 仍有需求。網路常見教程示範「每次播放創建 Sequencer、播放完 close()」,看似正確,實則隱藏嚴重記憶體洩漏:Sequencer 內部持有原生資源,close() 並不保證立即釋放,頻繁創建導致堆積/原生記憶體雙重暴漲,最終拋 OutOfMemoryError。
| 類別 | 角色 |
|---|---|
MidiSystem | MIDI 系統入口,獲取 Sequencer、載入 Sequence |
Sequencer | 播放引擎,控制播放/暫停/循環、註冊事件監聽 |
Sequence | MIDI 樂譜資料結構,含多軌、速度、分辨率、MIDI 事件 |
// ❌ 錯誤模式:迴圈中反覆創建/關閉
while (true) {
Sequencer seq = MidiSystem.getSequencer();
seq.open();
seq.setSequence(MidiSystem.getSequence(new File("song.mid")));
seq.start();
Thread.sleep(10000);
seq.close(); // 以為釋放了,實際未必
}
package com.example.audio;
import javax.sound.midi.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.*;
public class MidiPlayer implements Runnable {
private static final Logger LOG = Logger.getLogger(MidiPlayer.class.getName());
private final Sequencer sequencer;
private final Map sequenceCache = new ConcurrentHashMap<>();
private final List playlist;
private volatile boolean running = true;
public MidiPlayer(List midiFiles) throws MidiUnavailableException, IOException, InvalidMidiDataException {
this.playlist = new ArrayList<>(midiFiles);
// 1. 單例 Sequencer,全生命週期只開啟一次
this.sequencer = MidiSystem.getSequencer(false); // false = 不自動連接預設合成器
this.sequencer.open();
// 2. 啟動時預載所有 Sequence 到快取
for (String file : midiFiles) {
try {
Sequence seq = MidiSystem.getSequence(new File(file));
sequenceCache.put(file, seq);
} catch (Exception e) {
LOG.log(Level.WARNING, "載入失敗: " + file, e);
}
}
}
public void play(String fileName) {
Sequence seq = sequenceCache.get(fileName);
if (seq == null) {
LOG.warning("序列不存在: " + fileName);
return;
}
try {
sequencer.setSequence(seq); // 切換樂譜
sequencer.start();
} catch (InvalidMidiDataException e) {
LOG.log(Level.SEVERE, "播放失敗", e);
}
}
@Override
public void run() {
Random rand = new Random();
while (running) {
String next = playlist.get(rand.nextInt(playlist.size()));
System.out.println("切換曲目: " + next);
play(next);
try {
Thread.sleep(10_000); // 每首播放 10 秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
shutdown();
}
public void shutdown() {
running = false;
if (sequencer.isOpen()) {
sequencer.stop();
sequencer.close(); // 程式結束時統一釋放
LOG.info("Sequencer 已關閉");
}
}
public static void main(String[] args) {
try {
List files = Arrays.asList("1.mid", "2.mid", "3.mid", "4.mid", "5.mid");
MidiPlayer player = new MidiPlayer(files);
new Thread(player).start();
// 模擬運行一段時間後關閉
Thread.sleep(60_000);
player.shutdown();
} catch (Exception e) {
LOG.log(Level.SEVERE, "啟動失敗", e);
}
}
}
MidiSystem.getSequencer() 返回的是系統級資源,重複獲取會創建新實例佔用原生端口。全程複用同一實例,僅切換 Sequence。Sequence 為純資料物件,體積小,啟動時一次性載入 Map,播放時 O(1) 取用,避免重複 I/O 解析。sequencer.setSequence() 可在播放中直接切換樂譜,無需停止/關閉/重開。shutdown() 統一釋放資源,配合 volatile running 標誌實現線程安全停止。sequencer.addMetaEventListener(event -> { if (event.getType() == 0x2F) { /* 曲目自然結束 */ } }) 實現無縫銜接。sequencer.getTransmitter().setReceiver(...) 接入自定義 Receiver 實現音量包絡、變速。JavaFX MediaPlayer (MP3/WAV) 或 JLayer/Tritonus 庫,統一播放介面。jcmd VM.native_memory 監控原生記憶體,確認無洩漏。Java MIDI 播放的核心陷阱在於 Sequencer 的生命週期管理。記住鐵律:一個應用、一個 Sequencer、開啟一次、關閉一次。配合 Sequence 快取與優雅關閉機制,即可實現穩定、長時間運行的背景音樂系統,徹底告別 OutOfMemoryError。