前言
最近实现了一个文字转语音的功能,语音引擎返回的是pcm格式的数据。需要转化成wav格式前端才能播放。本文首先会给出解决方案,后续会讲背后的原理。
- 场景
- git 仓库
https://github.com/ChenghanY/pcm-wav-converter
1. pcm wav 转化工具类
入参和出参都为byte[]
,理论上有了 byte[]
就可以输出为文件,或者用于网络交互。
输出为文件的部分可以看 【Java】pcm 与 wav 格式互转工具类 (附测试用例)
- 浏览器播放的短音频,区分一下声道数、采样率即可。
- 讯飞api文档中
audio/L16;rate=8000
表示单声道8000的采样率
package com.james;
import javax.sound.sampled.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
public class AudioFormatConverter {
/**
* 采样率
*/
private static final Integer RATE = 8000;
/**
* 声道
*/
private static final Integer CHANNELS = 1;
public static byte[] pcmToWav(byte[] pcmBytes) {
return addHeader(pcmBytes, buildHeader(pcmBytes.length));
}
public static byte[] wavToPcm(byte[] wavBytes) {
return removeHeader(changeFormatToWav(wavBytes));
}
private static byte[] addHeader(byte[] pcmBytes, byte[] headerBytes) {
byte[] result = new byte[44 + pcmBytes.length];
System.arraycopy(headerBytes, 0, result, 0, 44);
System.arraycopy(pcmBytes, 0, result, 44, pcmBytes.length);
return result;
}
private static byte[] changeFormatToWav(byte[] audioFileContent) {
AudioFormat format = new AudioFormat(
8_000,
16,
CHANNELS,
true,
false
);
try (final AudioInputStream originalAudioStream = AudioSystem.getAudioInputStream(new ByteArrayInputStream(audioFileContent));
final AudioInputStream formattedAudioStream = AudioSystem.getAudioInputStream(format, originalAudioStream);
final AudioInputStream lengthAddedAudioStream = new AudioInputStream(formattedAudioStream, format, audioFileContent.length);
final ByteArrayOutputStream convertedOutputStream = new ByteArrayOutputStream()) {
AudioSystem.write(lengthAddedAudioStream, AudioFileFormat.Type.WAVE, convertedOutputStream);
return convertedOutputStream.toByteArray();
} catch (UnsupportedAudioFileException | IOException e) {
throw new RuntimeException(e);
}
}
private static byte[] removeHeader(byte[] audioFileContent) {
return Arrays.copyOfRange(audioFileContent, 44, audioFileContent.length);
}
private static byte[] buildHeader(Integer dataLength) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
writeChar(bos, new char[]{'R', 'I', 'F', 'F'});
writeInt(bos, dataLength + (44 - 8));
writeChar(bos, new char[]{'W', 'A', 'V', 'E'});
writeChar(bos, new char[]{'f', 'm', 't', ' '});
writeInt(bos, 16);
writeShort(bos, 0x0001);
writeShort(bos, CHANNELS);
writeInt(bos, AudioFormatConverter.RATE);
writeInt(bos, (short) (CHANNELS * 2) * RATE);
writeShort(bos, (short) (CHANNELS * 2));
writeShort(bos, 16);
writeChar(bos, new char[]{'d', 'a', 't', 'a'});
writeInt(bos, dataLength);
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void writeShort(ByteArrayOutputStream bos, int s) throws IOException {
byte[] arr = new byte[2];
arr[1] = (byte) ((s << 16) >> 24);
arr[0] = (byte) ((s << 24) >> 24);
bos.write(arr);
}
private static void writeInt(ByteArrayOutputStream bos, int n) throws IOException {
byte[] buf = new byte[4];
buf[3] = (byte) (n >> 24);
buf[2] = (byte) ((n << 8) >> 24);
buf[1] = (byte) ((n << 16) >> 24);
buf[0] = (byte) ((n << 24) >> 24);
bos.write(buf);
}
private static void writeChar(ByteArrayOutputStream bos, char[] id) {
for (char c : id) {
bos.write(c);
}
}
}
2. 原理概述
wav格式实际上就是在pcm数据上加了头部,让浏览器能够解析pcm数据,进而能播放音频。可以类比 TCP协议的报文头,报文头携带了数据长度、偏移量等元信息。
3. 重回代码
根据原理概述,把网上的代码重构了一下,明确语义后的形式,也就是上文的两个方法。
public static byte[] pcmToWav(byte[] pcmBytes) {
return addHeader(pcmBytes, buildHeader(pcmBytes.length));
}
public static byte[] wavToPcm(byte[] wavBytes) {
return removeHeader(changeFormatToWav(wavBytes));
}
后记
把一些测试资源放上来,后续整合到仓库中,提供完整的测试用例:
-
音频文件的下载地址
https://samplelib.com/zh/sample-wav.html
https://support.huaweicloud.com/sdkreference-sis/sis_05_0039.html -
pcm转mp3,播放后用于验证pcm文件的正确性
https://www.yayapeiyin.com/pcm-to-mp3/