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

Java 讀取 WAV 音頻文件並繪製波形圖完整實現

     編輯:Java 相關知識

從零構建 Java 音頻波形可視化:WAV 解析、繪製面板與開源項目實踐

音頻波形圖是音頻編輯、語音識別、聲紋分析等應用的基礎視覺化組件。本文提供完整的 WAV 文件解析器、Swing 波形繪製面板,並介紹開源項目 WaveAccess 的架構設計。

WAV 文件格式基礎 (RIFF/WAVE)

WAV 遵循 RIFF 容器格式,核心 Chunk 結構:

RIFF Chunk
├── ChunkID: "RIFF" (4 bytes)
├── ChunkSize: 文件大小 - 8
├── Format: "WAVE" (4 bytes)
├── fmt Sub-chunk
│   ├── Subchunk1ID: "fmt " (4 bytes)
│   ├── Subchunk1Size: 16 (PCM)
│   ├── AudioFormat: 1 (PCM)
│   ├── NumChannels: 1/2 (單/雙聲道)
│   ├── SampleRate: 采樣率 (如 44100)
│   ├── ByteRate: SampleRate × NumChannels × BitsPerSample/8
│   ├── BlockAlign: NumChannels × BitsPerSample/8
│   └── BitsPerSample: 8/16/24/32
└── data Sub-chunk
    ├── Subchunk2ID: "data" (4 bytes)
    ├── Subchunk2Size: 音頻數據大小
    └── Audio Data: 采樣點數據

核心解析器:WaveFileReader

import java.io.*;

public class WaveFileReader {
    // WAV 頭部字段
    private String chunkID;       // "RIFF"
    private long chunkSize;
    private String format;        // "WAVE"
    private String subchunk1ID;   // "fmt "
    private long subchunk1Size;
    private int audioFormat;      // 1=PCM
    private int numChannels;      // 聲道數
    private long sampleRate;      // 采樣率
    private long byteRate;
    private int blockAlign;
    private int bitsPerSample;    // 8 或 16
    private String subchunk2ID;   // "data"
    private long subchunk2Size;
    
    // 解析後的音頻數據 [channel][sampleIndex]
    private int[][] data;
    private int dataLength;       // 每聲道采樣點數
    private boolean success = false;

    public WaveFileReader(String filename) {
        parseFile(filename);
    }

    private void parseFile(String filename) {
        try (FileInputStream fis = new FileInputStream(filename);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            
            // 讀取並驗證 RIFF/WAVE 標識
            chunkID = readString(bis, 4);
            if (!"RIFF".equals(chunkID)) throw new IllegalArgumentException("非 RIFF 文件");
            
            chunkSize = readLong(bis);
            format = readString(bis, 4);
            if (!"WAVE".equals(format)) throw new IllegalArgumentException("非 WAVE 文件");
            
            // 讀取 fmt 子塊
            subchunk1ID = readString(bis, 4);
            if (!"fmt ".equals(subchunk1ID)) throw new IllegalArgumentException("缺少 fmt 子塊");
            
            subchunk1Size = readLong(bis);
            audioFormat = readInt(bis);
            numChannels = readInt(bis);
            sampleRate = readLong(bis);
            byteRate = readLong(bis);
            blockAlign = readInt(bis);
            bitsPerSample = readInt(bis);
            
            // 讀取 data 子塊
            subchunk2ID = readString(bis, 4);
            if (!"data".equals(subchunk2ID)) throw new IllegalArgumentException("缺少 data 子塊");
            
            subchunk2Size = readLong(bis);
            
            // 計算采樣點數
            dataLength = (int) (subchunk2Size / (bitsPerSample / 8) / numChannels);
            data = new int[numChannels][dataLength];
            
            // 讀取音頻數據(小端序)
            for (int i = 0; i < dataLength; i++) {
                for (int ch = 0; ch < numChannels; ch++) {
                    if (bitsPerSample == 8) {
                        data[ch][i] = bis.read() & 0xFF; // 無符號 8-bit
                    } else if (bitsPerSample == 16) {
                        data[ch][i] = readInt(bis); // 有符號 16-bit 小端序
                    }
                }
            }
            
            success = true;
            
        } catch (Exception e) {
            e.printStackTrace();
            success = false;
        }
    }

    // --- 低級讀取輔助方法 (小端序) ---
    private String readString(BufferedInputStream bis, int len) throws IOException {
        byte[] buf = new byte[len];
        if (bis.read(buf) != len) throw new IOException("文件截斷");
        return new String(buf);
    }
    
    private int readInt(BufferedInputStream bis) throws IOException {
        byte[] buf = new byte[2];
        if (bis.read(buf) != 2) throw new IOException("文件截斷");
        return (buf[0] & 0xFF) | ((buf[1] & 0xFF) << 8); // 小端序
    }
    
    private long readLong(BufferedInputStream bis) throws IOException {
        long val = 0;
        for (int i = 0; i < 4; i++) {
            int b = bis.read();
            if (b == -1) throw new IOException("文件截斷");
            val |= ((long) b & 0xFF) << (i * 8);
        }
        return val;
    }

    // --- Getter 方法 ---
    public boolean isSuccess() { return success; }
    public int getBitsPerSample() { return bitsPerSample; }
    public long getSampleRate() { return sampleRate; }
    public int getNumChannels() { return numChannels; }
    public int getDataLength() { return dataLength; }
    public int[][] getData() { return data; }
}

波形繪製面板:DrawPanel

import java.awt.*;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class DrawPanel extends JPanel {
    private final int[] data; // 單聲道數據

    public DrawPanel(int[] data) {
        this.data = data;
        setBackground(Color.WHITE);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (data == null || data.length == 0) return;

        int width = getWidth();
        int height = getHeight();
        int len = data.length;
        
        // 縮放比例:水平壓縮以適應面板寬度
        int step = Math.max(1, len / width);
        
        // 垂直縮放:16-bit 有符號範圍 -32768~32767 映射到面板高度
        double scaleY = (height / 2.0) / 32768.0;
        int centerY = height / 2;

        g.setColor(Color.RED);
        
        int prevX = 0;
        int prevY = centerY - (int)(data[0] * scaleY);
        
        for (int i = 0, x = 0; i < len; i += step, x++) {
            int y = centerY - (int)(data[i] * scaleY);
            g.drawLine(prevX, prevY, x, y);
            prevX = x;
            prevY = y;
        }
    }
}

主程序啟動演示

import javax.swing.*;

public class WaveFormDemo {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            String filename = args.length > 0 ? args[0] : "test.wav";
            
            WaveFileReader reader = new WaveFileReader(filename);
            if (!reader.isSuccess()) {
                JOptionPane.showMessageDialog(null, "無效的 WAV 文件: " + filename);
                return;
            }
            
            // 取第一聲道繪製
            int[] channelData = reader.getData()[0];
            
            JFrame frame = new JFrame("波形圖: " + filename);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new DrawPanel(channelData));
            frame.setSize(1000, 500);
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

開源項目:WaveAccess

作者已將完整代碼開源至 GitHub:https://github.com/sintrb/WaveAccess/

  • 支援 8/16/24/32-bit PCM、浮點數 WAV
  • 提供 WAV 寫入、剪切、合併、格式轉換
  • 包含單元測試、Maven 依賴、完整文檔
  • 歡迎貢獻 PR、Issue 反饋

擴展方向

功能技術點建議庫
MP3/AAC 解碼需原生解碼器JLayer, JAVE2 (FFmpeg 包裝)
實時波形/頻譜FFT、重採樣、雙緩衝TarsosDSP, JAudioLibs
大文件流式繪製虛擬化渲染、LOD自定義 Canvas + 區塊加載
Web 端波形Web Audio API + Canvaswavesurfer.js, waveform.js

常見問題排查

  • 播放速度/音調異常:確認 sampleRate 讀取正確,播放時匹配
  • 波形靜音/爆音:檢查 bitsPerSample、聲道數、字節序處理
  • 大文件 OOM:改用流式處理、分塊讀取、或 mmap 內存映射
  • 非 PCM 格式:WAV 也可封裝 MP3/ADPCM,需檢查 audioFormat != 1 並解碼
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved