如何用Java播放含原始大端16位有符号值的音频文件
我有一个程序会生成short[]类型的音频波形(有符号、大端、44.1KHz单声道)并写入文件,该文件并非.wav或其他编码格式,仅为原始16位采样数据。用另一个程序尝试打开该文件播放时,代码抛出javax.sound.sampled.UnsupportedAudioFileException: Stream of unsupported format异常,代码如下:
float sampleRate = 44100; int sampleSizeBits = 16; int numChannels = 1; boolean signed = true; boolean bigEndian = true; AudioFormat format = new AudioFormat(sampleRate, sampleSizeBits, numChannels, signed, bigEndian); BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(FILE)); // 抛出异常的代码行 AudioInputStream audioStream = AudioSystem.getAudioInputStream(inputStream); DataLine.Info info = new DataLine.Info(Clip.class, format); SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(info); sourceDataLine.open(format); sourceDataLine.start(); byte[] bufferBytes = new byte[4096]; int readBytes = -1; while ((readBytes = audioStream.read(bufferBytes)) != -1) { sourceDataLine.write(bufferBytes, 0, readBytes); } sourceDataLine.drain(); sourceDataLine.close(); audioStream.close();
更新内容
调试发现AudioInputStream仅尝试以下音频文件阅读器,无法读取仅包含大端16位有符号值字节对的原始文件:
- WaveExtensibleFileReader
- WaveFloatFileReader
- WaveFileReader
- SoftMidiAudioFileReader
- AuFileReader
- AiffFileReader
想请教几个问题:
- Java是否没有可直接读取此类原始音频文件的库方法?
- 是否有其他可用库,还是必须将数据写入标准.wav文件?
- 是否必须自行处理文件读取并调用
sourceDataLine.write()?有没有便捷方法可指定格式后自动播放并回调?
核心问题原因
AudioSystem.getAudioInputStream(inputStream)是用于读取带文件头的标准音频格式文件(如WAV、AIFF)的方法,你的文件是无格式头的原始采样数据,因此该方法无法识别,直接抛出异常。Java标准库确实没有内置直接读取原始音频数据生成AudioInputStream的方法,它默认只处理带格式封装的音频文件。
解决方法选项
选项1:手动读取原始数据并写入SourceDataLine
这是最直接的方案,无需额外库,直接处理文件读取并写入数据行即可(你的数据格式与指定的AudioFormat一致,无需额外转换)。修改后的代码示例:
float sampleRate = 44100; int sampleSizeBits = 16; int numChannels = 1; boolean signed = true; boolean bigEndian = true; AudioFormat format = new AudioFormat(sampleRate, sampleSizeBits, numChannels, signed, bigEndian); // 直接读取原始文件流,无需AudioInputStream BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(FILE)); // 修正DataLine.Info的目标类型为SourceDataLine DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(info); sourceDataLine.open(format); sourceDataLine.start(); byte[] bufferBytes = new byte[4096]; int readBytes = -1; while ((readBytes = inputStream.read(bufferBytes)) != -1) { sourceDataLine.write(bufferBytes, 0, readBytes); } sourceDataLine.drain(); sourceDataLine.close(); inputStream.close();
如果需要回调逻辑,可以封装一个播放线程实现:
public class RawAudioPlayer { public interface PlaybackCallback { void onPlaybackComplete(); void onPlaybackError(Exception e); } public static void playRawAudio(File file, AudioFormat format, PlaybackCallback callback) { new Thread(() -> { try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file)); SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(new DataLine.Info(SourceDataLine.class, format))) { sourceDataLine.open(format); sourceDataLine.start(); byte[] buffer = new byte[4096]; int read; while ((read = inputStream.read(buffer)) != -1) { sourceDataLine.write(buffer, 0, read); } sourceDataLine.drain(); sourceDataLine.close(); callback.onPlaybackComplete(); } catch (Exception e) { callback.onPlaybackError(e); } }).start(); } }
使用示例:
AudioFormat format = new AudioFormat(44100, 16, 1, true, true); RawAudioPlayer.playRawAudio(new File(FILE), format, new RawAudioPlayer.PlaybackCallback() { @Override public void onPlaybackComplete() { System.out.println("播放完成"); } @Override public void onPlaybackError(Exception e) { e.printStackTrace(); } });
选项2:将原始数据封装为WAV格式再播放
如果需要兼容AudioInputStream的API,可以临时将原始数据包装成WAV格式的字节流(内存或临时文件),WAV文件头结构固定,可手动构造:
public static AudioInputStream wrapRawToWav(InputStream rawStream, AudioFormat format) throws IOException { long frameLength = rawStream.available() / format.getFrameSize(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); // 构造RIFF块 dos.writeBytes("RIFF"); dos.writeInt((int)(36 + frameLength * format.getFrameSize())); dos.writeBytes("WAVE"); // 构造fmt块(PCM格式) dos.writeBytes("fmt "); dos.writeInt(16); dos.writeShort(1); dos.writeShort(format.getChannels()); dos.writeInt((int)format.getSampleRate()); dos.writeInt((int)(format.getSampleRate() * format.getFrameSize())); dos.writeShort(format.getFrameSize()); dos.writeShort(format.getSampleSizeInBits()); // 构造data块 dos.writeBytes("data"); dos.writeInt((int)(frameLength * format.getFrameSize())); // 写入原始音频数据 byte[] buffer = new byte[4096]; int read; while ((read = rawStream.read(buffer)) != -1) { dos.write(buffer, 0, read); } dos.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); return AudioSystem.getAudioInputStream(bais); }
包装后即可用你原本的代码通过AudioInputStream播放。
选项3:使用第三方库
如果不想自行实现,可以使用TarsosDSP等第三方音频库,这类库支持直接读取原始音频数据并播放。不过多数场景下,前两个选项已足够解决问题,无需额外引入依赖。
总结
- Java标准库没有直接读取无格式头原始音频的
AudioInputStream实现,需自行处理或封装为标准格式。 - 优先选择手动读取原始数据写入
SourceDataLine的方案,简单高效且无额外依赖。 - 若需要回调,可自行封装播放线程;若要兼容
AudioInputStreamAPI,可临时封装为WAV格式。
内容的提问来源于stack exchange,提问作者Bitbang3r

