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); } }
关键说明
- 占位头部初始化:创建WAV文件时先写入符合格式的头部,但将总长度和数据长度字段设为0占位,避免因未知总长度导致的格式问题。
- 实时数据写入:每次生成的采样数据同时写入
SourceDataLine(播放)和RandomAccessFile(保存),实现同步操作。 - 停止时修正头部:通过JVM关闭钩子,在程序终止时自动修改头部的长度字段,使WAV文件符合标准格式,可正常播放。
- 避免文件覆盖:不同于之前每次调用
AudioSystem.write()覆盖文件的方式,此方案持续追加数据到同一文件,最终通过修正头部完成完整文件的生成。
内容的提问来源于stack exchange,提问作者prismofeverything
相关产品推荐
相关产品推荐

