需求:项目是一个支持PC和手机端的 ai 对话平台,需要支持用户语音输入问题,ai 回答。
方案a :浏览器提供 webkitSpeechRecognition 可以直接识别用户语音并转换文字。
使用:
// 检测是否支持 Web Speech API ===
const isWebSpeechSupported = typeof window !== 'undefined' && !!(window.SpeechRecognition || window.webkitSpeechRecognition);
if(!isWebSpeechSupported) return alert("不支持语音识别!")
recognition = new webkitSpeechRecognition();
recognition.lang = 'zh-CN'; // 设置语音识别的语言为中文
recognition.continuous = true
recognition.onresult = (event) => {
const speechResult = event.results[0][0].transcript;
console.log("语音识别结果:", speechResult);
eventBus.emit('senMsg', speechResult); // 自己的业务代码
};
recognition.onerror = (event) => {
console.log("发生错误:", event.error);
};
recognition.start() 开始录音
recognition.stop() 结束录音 -- 会执行 onresult 回调函数返回识别语音文字
recognition.abort() 终止所有操作 - 不会执行 onresult 回调函数
缺陷:SpeechRecognition 不支持IOS平台。
图片来源:ai
方案b:前端录音 + 后端 ASR(语音转文字)
用户点击“开始说话” → 前端调用麦克风录制音频(WAV/MP3) → 上传音频到服务器 → 服务器调用 ASR 引擎(如阿里云、腾讯云、百度、Google Cloud) → 返回识别文本给前端
使用:这里使用百度 ASR,音频流需要转换 PCM 二进制
pcm和blob的区别:pcm是原始音频数据的编码格式, Blob(Binary Large Object)是浏览器中用于封装二进制数据的容器对象
图片来源 :ai
相关代码:
try {
let recognition = null
let chunks: Blob[] = [];
// 请求麦克风权限准备录音
let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
recognition = new MediaRecorder(stream);
// 监听录音数据 并收集到 chunks 二进制数组
recognition.ondataavailable = (e) => {
chunks = []
chunks.length = 0
if (e.data.size > 0) {
chunks.push(e.data);
}
};
// 3. 准备接收录音数据
// 录音结束时处理完整音频
recognition.onstop = async () => {
// ✅ 使用真实的 mimeType
const mimeType = recognition!.mimeType || 'audio/mp4';
const blob = new Blob(chunks, { type: mimeType }); //
// 1. 转 PCM / 看自己的 ASR 云服务是否需要
const pcmData = await audioBlobToPcm(blob);
console.log('PCM 长度:', pcmData.byteLength, '字节');
// 2. 调用 百度 ASR 接口
const result = await fetchAsrBaiduPro(pcmData, "自己的token");
console.log('语音识别返回', result);
if (result.err_no === 0 && result.result?.[0]) {
const text = result.result[0];
console.log('识别结果:', text);
eventBus.emit('senMsg', text); // 处理自己的业务
} else {
console.log('ASR 错误:', result.err_msg);
toast.fail("时间太短,未识别到语音")
// alert(`识别失败: ${result.err_msg}`);
}
// 直接下载音频查看----用于测试音频代码
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = `recording-${Date.now()}.${getExt(mimeType)}`;
// document.body.appendChild(a);
// a.click();
// document.body.removeChild(a);
// // 释放内存(延迟一点,确保下载完成)
// setTimeout(() => URL.revokeObjectURL(url), 1000);
};
} catch (error) {
console.log(error); // 这里监听不同报错处理业务
toast.fail('找不到符合条件的音视频设备')
}
blob 转 PCM 函数封装
// utils/audioToPcm.ts
/**
* 将音频 Blob(如 audio/mp4)解码为 16kHz/16bit/单声道 PCM ArrayBuffer
* @param audioBlob - 录音得到的音频 Blob
* @returns PCM 格式的 ArrayBuffer
*/
export async function audioBlobToPcm(audioBlob: Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = async () => {
try {
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const audioData = await audioContext.decodeAudioData(fileReader.result as ArrayBuffer);
// 重采样到 16kHz(可选,但百度要求 16k)
const sampleRate = 16000;
let offlineContext = new OfflineAudioContext(1, audioData.duration * sampleRate, sampleRate);
let source = offlineContext.createBufferSource();
source.buffer = audioData;
source.connect(offlineContext.destination);
source.start();
const renderedBuffer = await offlineContext.startRendering();
// 转为 16-bit PCM
const float32Array = renderedBuffer.getChannelData(0); // 单声道
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
// 浮点 [-1, 1] → 16位整数 [-32768, 32767]
const s = Math.max(-1, Math.min(1, float32Array[i]));
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
resolve(int16Array.buffer);
} catch (err) {
reject(err);
}
};
fileReader.onerror = () => reject(fileReader.error);
fileReader.readAsArrayBuffer(audioBlob);
});
}
fetch调用 百度 ASR 函数封装:
// utils/asrBaiduPro.ts
interface AsrBaiduProResult {
err_no: number;
err_msg: string;
result?: string[];
sn?: string;
}
/**
* 调用百度语音识别 Pro API(需传入 PCM 音频数据)
* @param pcmData - PCM 音频的 ArrayBuffer(16kHz, 16bit, 单声道)
* @param token - 百度 OAuth2.0 access_token
* @param cuid - 设备唯一标识(建议固定值或生成)
* @returns 识别结果文本数组(result[0] 为主结果)
*/
export async function fetchAsrBaiduPro(
pcmData: ArrayBuffer,
token: string,
cuid: string = 'web-client-' + Date.now()
): Promise<AsrBaiduProResult> {
const rate = 16000;
const channel = 1;
const format = 'pcm';
const devPid = 80001; // 普通话 + 带标点 + 数字识别
// 将 PCM 转为 Base64
const base64Speech = arrayBufferToBase64(pcmData);
const len = pcmData.byteLength;
const payload = {
format,
rate,
channel,
cuid:'自己的标识',
dev_pid: devPid,
token,
len,
speech: base64Speech,
};
const url = 'https://vop.baidu.com/pro_api';
try {
console.log('【ASR】开始请求百度 Pro API...');
const t0 = performance.now();
// xixi 是 vite 的 proxy 代理 https://vop.baidu.com/pro_api
const response = await fetch("/xixi", {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
Accept: 'application/json',
},
body: JSON.stringify(payload),
});
const t1 = performance.now();
console.log(`【ASR】请求耗时: ${(t1 - t0).toFixed(2)} ms`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data: AsrBaiduProResult = await response.json();
return data;
} catch (error) {
console.error('【ASR】请求失败:', error);
throw error;
}
}
// 辅助函数:ArrayBuffer → Base64
function arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
方案c:微信提供的录音 api
主要针对低版本的 IOS 和 微信对 getUserMedia 不支持。
正常使用 getUserMedia必须满足:新版本的 iOS(≥14.3) + 较新版本的微信(≥8.0.0) + 用户主动触发了录音(如点击按钮) + 页面是 HTTPS
developers.weixin.qq.com/doc/service…
方案b和c区别:
来源 ai
兼容性差异:
来源 ai
低版本IOS兼容:
function startRecording() {
// 1. 优先尝试 Web 录音(适合 Android/PC)
if (isWechat() && isIOS()) {
// 2. iOS 微信 → 强制使用 wx.startRecord()
useWechatRecord();
} else {
// 3. 其他环境 → 尝试 Web 录音
useWebRecord().catch(() => {
// 4. 失败则降级到 wx.startRecord()(如果是微信)
if (isWechat()) useWechatRecord();
else alert('当前环境不支持录音');
});
}
}