首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java MIDI 音樂播放實作:解決長時間運行記憶體洩漏的關鍵技巧

     編輯:Java 程式設計入門

背景與痛點

Java 多媒體能力有限,但輕量級背景音樂 (MIDI) 仍有需求。網路常見教程示範「每次播放創建 Sequencer、播放完 close()」,看似正確,實則隱藏嚴重記憶體洩漏:Sequencer 內部持有原生資源,close() 並不保證立即釋放,頻繁創建導致堆積/原生記憶體雙重暴漲,最終拋 OutOfMemoryError。

核心類別速覽

類別角色
MidiSystemMIDI 系統入口,獲取 Sequencer、載入 Sequence
Sequencer播放引擎,控制播放/暫停/循環、註冊事件監聽
SequenceMIDI 樂譜資料結構,含多軌、速度、分辨率、MIDI 事件

錯誤示範 (每次播放 new Sequencer)

// ❌ 錯誤模式:迴圈中反覆創建/關閉
while (true) {
    Sequencer seq = MidiSystem.getSequencer();
    seq.open();
    seq.setSequence(MidiSystem.getSequence(new File("song.mid")));
    seq.start();
    Thread.sleep(10000);
    seq.close(); // 以為釋放了,實際未必
}

正確架構:單例 Sequencer + Sequence 快取

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);
        }
    }
}

關鍵設計解析

  1. 單一 Sequencer 實例:MidiSystem.getSequencer() 返回的是系統級資源,重複獲取會創建新實例佔用原生端口。全程複用同一實例,僅切換 Sequence。
  2. Sequence 預載快取:Sequence 為純資料物件,體積小,啟動時一次性載入 Map,播放時 O(1) 取用,避免重複 I/O 解析。
  3. 非阻塞切歌:sequencer.setSequence() 可在播放中直接切換樂譜,無需停止/關閉/重開。
  4. 優雅關閉:提供 shutdown() 統一釋放資源,配合 volatile running 標誌實現線程安全停止。
  5. 異常隔離:單曲載入/播放失敗不影響整體播放迴圈,記錄日誌繼續下一首。

進階擴展建議

  • 事件監聽: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。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved