如何将MediaRecorder API获取的音频Blob编码为LINEAR16?
实时麦克风音频转Google Speech To Text的编码转换问题
我想把用户麦克风的音频实时发送到Google Speech To Text API,打算用它的RecognizeStream功能。具体实现是网页端用MediaRecorder API加WebSocket连接Node.js服务器,每2秒把ondataavailable事件拿到的音频Blob发往服务器,再由服务器调用Google库的recognizeStream.write(blob)传给API,但现在API无响应且超时,排查后确认是编码问题导致的。
我对音频编码不熟悉,找不到无需物理文件、直接在代码里把原始Blob转成LINEAR16 Blob的方法,还要尽可能降低延迟。试过用Sox软件和对应的npm包转换,但没法实现无文件操作。
前端发送音频代码
const mediaConstraints = { audio: true }; mediaStream = await navigator.mediaDevices.getUserMedia(mediaConstraints); mediaRecorder = new MediaRecorder(mediaStream); let isRecording = false; mediaRecorder.ondataavailable = (event) => { if (isRecording && event.data.size > 0) { socket.emit('audioChunk', event.data); } };
Node.js后端接收及处理代码
const encoding = 'LINEAR16'; const languageCode = 'fr-FR'; const sampleRateHertz = 16000; const request = { config: { encoding: encoding, languageCode: languageCode, sampleRateHertz: sampleRateHertz, }, interimResults: true, // 如果需要中间结果,设为true }; io.on('connection', (socket) => { console.log('用户已连接'); const recognizeStream = client .streamingRecognize(request) .on('error', console.error) .on('data', data => process.stdout.write( data.results[0] && data.results[0].alternatives[0] ? `转录结果: ${data.results[0].alternatives[0].transcript}\n` : '\n\n达到转录时间限制,请按Ctrl+C\n' ) ); socket.on('audioChunk', (chunk) => { const blob = new Blob([chunk], {type: 'audio/webm;codecs=opus'}); // 此处需要转换编码 recognizeStream.write(blob.binaryData); });
解决方案:实时转码为LINEAR16
方案1:前端直接采集LINEAR16格式(推荐,降低后端压力)
MediaRecorder默认输出WebM/Opus,无法直接输出LINEAR16,但可以用AudioContext直接处理原始音频数据,转换成16kHz、16位单声道PCM(即LINEAR16):
// 前端替换MediaRecorder的代码 const mediaConstraints = { audio: { sampleRate: 16000, channelCount: 1 } }; const mediaStream = await navigator.mediaDevices.getUserMedia(mediaConstraints); const audioContext = new AudioContext({ sampleRate: 16000 }); const source = audioContext.createMediaStreamSource(mediaStream); // 缓冲区大小选4096/8192,平衡延迟与性能 const processor = audioContext.createScriptProcessor(4096, 1, 1); source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = (event) => { if (!isRecording) return; // 获取原始Float32格式PCM数据 const inputBuffer = event.inputBuffer.getChannelData(0); // 转换为16位整数(LINEAR16格式) const int16Array = new Int16Array(inputBuffer.length); for (let i = 0; i < inputBuffer.length; i++) { // 将范围-1~1的Float32转成范围-32768~32767的Int16 const sample = Math.max(-1, Math.min(1, inputBuffer[i])); int16Array[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; } // 转成Blob发送 const blob = new Blob([int16Array], { type: 'audio/L16;rate=16000' }); socket.emit('audioChunk', blob); };
此时后端无需转码,直接传入Google流:
// 后端修改接收部分 socket.on('audioChunk', (chunk) => { recognizeStream.write(chunk); });
方案2:后端实时转码
如果必须在后端处理,可以用轻量库或ffmpeg实现无文件转码:
用@discordjs/opus转码(低延迟)
先安装依赖:npm install @discordjs/opus
const { OpusDecoder } = require('@discordjs/opus'); // 初始化解码器,匹配前端Opus参数 const decoder = new OpusDecoder(16000, 1); socket.on('audioChunk', (chunk) => { const buffer = Buffer.from(chunk); // 解码Opus到Float32格式PCM const pcmFloat = decoder.decode(buffer); // 转成Int16Array(LINEAR16) const int16Array = new Int16Array(pcmFloat.length); for (let i = 0; i < pcmFloat.length; i++) { const sample = Math.max(-1, Math.min(1, pcmFloat[i])); int16Array[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; } // 写入Google流 recognizeStream.write(Buffer.from(int16Array)); });
用fluent-ffmpeg转码
安装依赖:npm install fluent-ffmpeg
const ffmpeg = require('fluent-ffmpeg'); socket.on('audioChunk', (chunk) => { const buffer = Buffer.from(chunk); // 实时转码,不生成物理文件 ffmpeg() .input(buffer) .inputFormat('webm') .audioCodec('pcm_s16le') // LINEAR16对应编码 .audioChannels(1) .audioFrequency(16000) .format('s16le') .on('data', (data) => { recognizeStream.write(data); }) .on('error', (err) => { console.error('转码错误:', err); }) .run(); });
注意事项
- 前端采集时指定
sampleRate:16000和channelCount:1,与Google API配置一致,避免额外转码 - 前端使用
ScriptProcessor时,缓冲区大小不要过大,否则会增加延迟 - 后端优先选择轻量转码库,比ffmpeg延迟更低
内容的提问来源于stack exchange,提问作者Martin
相关产品推荐
相关产品推荐

