You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 04:55:04