前言
最近实现了一个文字转语音的功能,语音引擎返回的是pcm格式的数据。需要转化成wav格式前端才能播放。本文首先会给出解决方案,后续会讲背后的原理。
- 场景
1. pcm wav 转化工具类
入参和出参都为byte[]
,理论上有了 byte[]
就可以输出为文件,或者用于网络交互。
- 浏览器播放的短音频,区分一下声道数、采样率即可。
- 讯飞api文档中
audio/L16;rate=8000
表示单声道8000的采样率
package com.james.convert;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 CHANNELS = 1;/*** 采样率*/private static final Integer RATE = 8000;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(RATE,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 * RATE);writeShort(bos, (short) (CHANNELS * 2) * RATE);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/