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

Java 音頻播放器實戰:基於 javax.sound.sampled/midi 的封裝設計

     編輯:Java 相關知識

Java 音頻 API 現狀與挑戰

Java 早期僅支援 Applet 播放音頻,JDK 1.3+ 引入 javax.sound 套件,提供 Clip(採樣音頻 WAV/AIFF/AU)與 Sequencer(MIDI)兩大引擎。但官方 API 存在資源釋放不徹底、記憶體洩漏、重複播放崩潰等問題,需二次封裝才能生產可用。


架構設計:三層封裝

  1. 資源載入層:URI(網絡/本地統一)、SoundData(位元組快取+元數據)
  2. 播放引擎層:SoundPlay 統一管理 Clip/Sequencer 生命週期
  3. 業務調用層:簡單的 load()、play()、autoPlay()、stop()、reset()

核心類實作

1. 統一資源標識:URI

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); // 本地文件讀取
    }
}

2. 音頻數據載體:SoundData

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...
}

3. 播放核心:SoundPlay

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

InputStream 轉 byte[] 工具方法

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();
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved