You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java音频:如何将生成的字节持续写入音频文件?

实时播放音频采样并持续保存为WAV文件

问题描述

已实现通过SourceDataLine循环播放生成的音频采样:

import java.nio.ByteBuffer;
import javax.sound.sampled.*;

int SAMPLE_SIZE = 2;

AudioFormat format = new AudioFormat(44100, 16, 1, true, true);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
line.open(format, 4410);
line.start();
ByteBuffer buffer = ByteBuffer.allocate(line.getBufferSize());

while (true) {
  buffer.clear();
  int available = line.available() / SAMPLE_SIZE;
  for (int i = 0; i < available; i++) {
    // generate some bytes ....
    buffer.putShort(....);
  }

  line.write(buffer.array(), 0, buffer.position());
}

现在需要在播放的同时,将采样持续保存到WAV文件,程序随时停止都能得到包含所有已生成采样的完整文件。尝试过用AudioInputStream配合AudioSystem.write(),但只能保存单次缓冲区的采样,无法持续写入。

解决方案

核心问题是WAV文件头部需要记录总数据长度,而实时生成时无法提前获知总长度。可以通过先写占位头部、持续追加数据、停止时修正头部的方式实现:

完整代码示例

import java.io.*;
import java.nio.ByteBuffer;
import javax.sound.sampled.*;

public class AudioRecorder {
    private static final int SAMPLE_SIZE = 2;
    private static final AudioFormat FORMAT = new AudioFormat(44100, 16, 1, true, true);
    private static RandomAccessFile wavFile;
    private static long dataSize = 0; // 记录实际写入的音频数据字节数

    public static void main(String[] args) throws Exception {
        // 初始化音频播放线路
        DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, FORMAT);
        SourceDataLine playLine = (SourceDataLine) AudioSystem.getLine(lineInfo);
        playLine.open(FORMAT, 4410);
        playLine.start();
        ByteBuffer buffer = ByteBuffer.allocate(playLine.getBufferSize());

        // 初始化WAV文件,写入占位头部
        initWavFile(new File("output.wav"));

        // 注册关闭钩子:程序停止时自动修正WAV头部并释放资源
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                fixWavHeader();
                wavFile.close();
                playLine.stop();
                playLine.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }));

        // 循环生成、播放并保存音频采样
        while (true) {
            buffer.clear();
            int availableFrames = playLine.available() / SAMPLE_SIZE;
            for (int i = 0; i < availableFrames; i++) {
                // 替换为你的采样生成逻辑,这里以440Hz正弦波为例
                double angle = i / (FORMAT.getSampleRate() / 440.0);
                short sample = (short) (Math.sin(angle * 2 * Math.PI) * Short.MAX_VALUE);
                buffer.putShort(sample);
            }

            // 写入播放线路
            playLine.write(buffer.array(), 0, buffer.position());
            // 写入WAV文件的数据部分
            wavFile.write(buffer.array(), 0, buffer.position());
            dataSize += buffer.position();
        }
    }

    // 初始化WAV文件,写入占位的头部信息
    private static void initWavFile(File targetFile) throws IOException {
        wavFile = new RandomAccessFile(targetFile, "rw");
        // RIFF块
        wavFile.writeBytes("RIFF");
        wavFile.writeInt(0); // 总文件长度占位,后续修正
        wavFile.writeBytes("WAVE");
        // fmt块(PCM格式)
        wavFile.writeBytes("fmt ");
        wavFile.writeInt(16); // PCM格式的fmt块固定长度
        wavFile.writeShort((short) 1); // PCM编码标识
        wavFile.writeShort((short) FORMAT.getChannels());
        wavFile.writeInt((int) FORMAT.getSampleRate());
        int byteRate = (int) (FORMAT.getSampleRate() * FORMAT.getChannels() * FORMAT.getSampleSizeInBits() / 8);
        wavFile.writeInt(byteRate);
        wavFile.writeShort((short) (FORMAT.getChannels() * FORMAT.getSampleSizeInBits() / 8)); // 块对齐
        wavFile.writeShort((short) FORMAT.getSampleSizeInBits());
        // data块
        wavFile.writeBytes("data");
        wavFile.writeInt(0); // 数据长度占位,后续修正
    }

    // 修正WAV头部的长度字段,确保文件可正常播放
    private static void fixWavHeader() throws IOException {
        // 修正RIFF总长度:总文件大小 = 44(头部) + dataSize - 8(RIFF标识和长度字段的8字节)
        wavFile.seek(4);
        wavFile.writeInt((int) (36 + dataSize));
        // 修正data块的实际数据长度
        wavFile.seek(40);
        wavFile.writeInt((int) dataSize);
    }
}

关键说明

  1. 占位头部初始化:创建WAV文件时先写入符合格式的头部,但将总长度和数据长度字段设为0占位,避免因未知总长度导致的格式问题。
  2. 实时数据写入:每次生成的采样数据同时写入SourceDataLine(播放)和RandomAccessFile(保存),实现同步操作。
  3. 停止时修正头部:通过JVM关闭钩子,在程序终止时自动修改头部的长度字段,使WAV文件符合标准格式,可正常播放。
  4. 避免文件覆盖:不同于之前每次调用AudioSystem.write()覆盖文件的方式,此方案持续追加数据到同一文件,最终通过修正头部完成完整文件的生成。

内容的提问来源于stack exchange,提问作者prismofeverything

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 21:50:24