Fluent-FFmpeg生成WAV流返回Asterisk无法正常工作问题排查
AWS Polly转MP3后流式生成WAV无法在Asterisk中使用
背景与正常场景
使用AWS Polly生成文本转语音的MP3文件,再通过fluent-ffmpeg转换为Asterisk要求的WAV格式(参数:8000Hz采样率、单声道、300Hz高通滤波、3400Hz低通滤波)。
当生成中间文件并通过sendFile返回时,文件在本地和Asterisk均正常工作,代码如下:
_convertMp3ToWav(mp3Buffer, destPath) { const { options } = this; return new Promise((resolve, reject) => { ffmpeg(mp3Buffer) .audioFilter(`highpass=f=300, lowpass=f=3400`) .outputOptions([`-ar 8000`, `-ac 1`]) .output(destPath) .on('error', (err) => { log.error(`An error occured: ${err?.message || err?.stack}`); reject({ err: 'Failed to convert from mp3 to wav' }); }) .on('end', async () => { log.info(`successfully converted mp3 to wav at ${destPath}`); resolve({ msg: "voice file generated" }); }).run(); }); } res.status(200).sendFile(wavFilePath)
流式处理的异常场景
尝试避免生成中间文件,直接流式生成WAV Buffer返回,代码如下:
_convertMp3ToWav(mp3Buffer) { return new Promise((resolve, reject) => { // create a writable output stream to store wav stream const outputStream = new Stream.Writable(); const buffer = []; // redefining _write function to write wav stream outputStream._write = function (chunk, encoding, done) { buffer.push(chunk); done(); }; // convert mp3 buffer to wav buffer ffmpeg(mp3Buffer) .audioFilter(`highpass=f=300, lowpass=f=3400`) .outputOptions([`-ar 8000`, `-ac 1`]) .output(outputStream) .format('wav') .on('error', (err) => { log.error(`An error occured: ${err?.message || err?.stack}`); reject({ err: 'Failed to convert from mp3 to wav' }); }) .on('end', async () => { try { // create wav buffer const wavBuffer = Buffer.concat(buffer); log.info(`successfully converted mp3 to wav buffer`); resolve({ wavBuffer }); } catch (err) { log.error(`failed to create wav buffer : ${err?.message || err?.stack}`); reject({ err: 'Failed to create wav buffer' }); } }).run(); }); } const buffer = await this._convertMp3ToWav(bufferStream); res.send(buffer.wavBuffer);
即使设置Content-Type: audio/wav,文件本地可播放,但无法在Asterisk中正常工作。
更新尝试:直接写入Response
尝试直接将转换后的流写入Response,代码如下:
_convertMp3ToWav(mp3Buffer, res) { return new Promise((resolve, reject) => { // create a writable output stream to send wav stream in response const outputStream = new Stream.Writable(); outputStream._write = function (chunk, encoding, done) { res.write(chunk, encoding); done(); }; // convert mp3 buffer to wav buffer ffmpeg(mp3Buffer) .audioFilter(`highpass=f=300, lowpass=f=3400`) .outputOptions([`-ar 8000`, `-ac 1`]) .output(outputStream) .format('wav') .on('error', (err) => { reject({ err: 'Failed to convert from mp3 to wav' }); }) .on('end', async () => { try { // end the response stream res.end(); resolve(); } catch (err) { reject({ err: 'Failed to send wav buffer in response' }); } }).run(); }); }
流式生成的文件仍无法在Asterisk中播放,检查元数据发现错误:
at least one of the list chunks has an incorrect length
其他Asterisk要求的属性均正常,且本地可播放。
解决方法
问题根源:ffmpeg流式输出WAV时,会先写入占位的Chunk Size值(无法提前获知总文件大小),而Asterisk对WAV格式的头部校验严格,不接受占位值。以下是两种可靠解决方案:
方案1:修正WAV头部的Chunk Size
通过pipe()获取ffmpeg输出的WAV流,手动修正头部的两个关键大小字段:
_convertMp3ToWav(mp3Buffer) { return new Promise((resolve, reject) => { const chunks = []; ffmpeg(mp3Buffer) .audioFilter(`highpass=f=300, lowpass=f=3400`) .outputOptions([`-ar 8000`, `-ac 1`, `-f wav`, `-c:a pcm_s16le`]) .on('error', (err) => { log.error(`转换出错: ${err?.message || err?.stack}`); reject({ err: 'MP3转WAV失败' }); }) .on('data', (chunk) => { chunks.push(chunk); }) .on('end', () => { const wavBuffer = Buffer.concat(chunks); // 修正WAV头部的Chunk Size(仅当头部完整时) if (wavBuffer.length >= 44) { // 修正总文件大小:整个Buffer长度减8 wavBuffer.writeUInt32LE(wavBuffer.length - 8, 4); // 修正音频数据大小:Buffer长度减44(标准WAV头部长度) const dataSize = wavBuffer.length - 44; wavBuffer.writeUInt32LE(dataSize, 40); } resolve({ wavBuffer }); }) .pipe(); }); }
返回时设置正确的响应头:
const { wavBuffer } = await this._convertMp3ToWav(bufferStream); res.set({ 'Content-Type': 'audio/wav', 'Content-Length': wavBuffer.length }); res.send(wavBuffer);
方案2:手动构造标准WAV头部
先将MP3转换为原始PCM流,再手动构造符合标准的WAV头部,彻底避免ffmpeg的流式头部问题:
_convertMp3ToWav(mp3Buffer) { return new Promise((resolve, reject) => { const pcmChunks = []; ffmpeg(mp3Buffer) .audioFilter(`highpass=f=300, lowpass=f=3400`) .outputOptions([`-ar 8000`, `-ac 1`, `-f s16le`, `-c:a pcm_s16le`]) .on('error', (err) => { log.error(`转换出错: ${err?.message || err?.stack}`); reject({ err: 'MP3转PCM失败' }); }) .on('data', (chunk) => { pcmChunks.push(chunk); }) .on('end', () => { const pcmBuffer = Buffer.concat(pcmChunks); // 构造标准WAV头部(44字节) const wavHeader = Buffer.alloc(44); // RIFF标识 wavHeader.write('RIFF', 0); // 总文件大小:头部+PCM数据大小-8 wavHeader.writeUInt32LE(44 + pcmBuffer.length - 8, 4); // WAVE标识 wavHeader.write('WAVE', 8); // fmt子块标识 wavHeader.write('fmt ', 12); // fmt子块大小(PCM固定为16) wavHeader.writeUInt32LE(16, 16); // 音频格式(PCM固定为1) wavHeader.writeUInt16LE(1, 20); // 声道数 wavHeader.writeUInt16LE(1, 22); // 采样率 wavHeader.writeUInt32LE(8000, 24); // 字节率:采样率×声道数×位深/8 = 8000×1×16/8=16000 wavHeader.writeUInt32LE(16000, 28); // 块对齐:声道数×位深/8 = 1×2=2 wavHeader.writeUInt16LE(2, 32); // 位深(16位) wavHeader.writeUInt16LE(16, 34); // data子块标识 wavHeader.write('data', 36); // data子块大小(PCM数据长度) wavHeader.writeUInt32LE(pcmBuffer.length, 40); // 拼接头部与PCM数据 const wavBuffer = Buffer.concat([wavHeader, pcmBuffer]); resolve({ wavBuffer }); }) .pipe(); }); }
返回时同样设置Content-Type和Content-Length即可。
内容的提问来源于stack exchange,提问作者Code Guru
相关产品推荐
相关产品推荐

