简介:本文详解uniapp中H5录音、上传、实时语音识别及波形可视化的实现方案,覆盖H5/App/小程序多端兼容技巧,提供完整代码示例与性能优化策略。
uniapp跨平台录音需针对不同运行环境选择适配API:
navigator.mediaDevices.getUserMedia({audio: true})获取麦克风权限,MediaRecorder实现录音功能。wx.getRecorderManager()(微信)或my.getRecorderManager()(支付宝),需注意小程序对录音时长的限制(通常60秒)。关键代码示例(H5录音):
// H5录音初始化startH5Record() {return new Promise((resolve, reject) => {navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {const mediaRecorder = new MediaRecorder(stream);const audioChunks = [];mediaRecorder.ondataavailable = event => {audioChunks.push(event.data);};mediaRecorder.onstop = () => {const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });resolve(audioBlob);stream.getTracks().forEach(track => track.stop());};mediaRecorder.start(100); // 每100ms收集一次数据this.mediaRecorder = mediaRecorder;}).catch(err => reject(err));});}
多端权限处理需区分场景:
navigator.permissions.query()检测麦克风权限,捕获NotAllowedError异常。uni.authorize({scope: 'scope.record'})预授权,处理uni.onMicrophoneError事件。wx.onVoiceRecordEnd事件,处理用户拒绝授权的fail回调。权限检测最佳实践:
async checkRecordPermission() {#ifdef H5try {const permission = await navigator.permissions.query({ name: 'microphone' });return permission.state === 'granted';} catch (e) {// 兼容旧浏览器return !!navigator.mediaDevices;}#endif#ifdef MP-WEIXINreturn new Promise(resolve => {wx.getSetting({success(res) {resolve(res.authSetting['scope.record']);}});});#endif}
针对大文件上传场景,建议采用以下策略:
Blob.slice()方法将音频分割为2MB的片段。Promise.all限制最大并发数(通常3-5个)。分片上传示例:
async uploadAudioInChunks(file, chunkSize = 2 * 1024 * 1024) {const totalChunks = Math.ceil(file.size / chunkSize);const uploadTasks = [];for (let i = 0; i < totalChunks; i++) {const start = i * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('chunkIndex', i);formData.append('totalChunks', totalChunks);uploadTasks.push(uni.uploadFile({url: 'https://your-api.com/upload',filePath: chunk, // H5环境需转换为Blob URLname: 'file',formData}));}return Promise.all(uploadTasks);}
libsamplerate.js进行重采样,或通过Web Worker调用lamejs进行MP3编码。uni-audio-compress插件。wx.compressAudioAPI(微信基础库2.10.0+)。推荐架构:
前端WebSocket实现:
class ASRClient {constructor(url) {this.socket = null;this.audioProcessor = null;}connect() {this.socket = new WebSocket(url);this.socket.onopen = () => {console.log('ASR服务连接成功');this.audioProcessor = this.setupAudioProcessor();};this.socket.onmessage = (event) => {const result = JSON.parse(event.data);this.emit('recognitionResult', result);};}setupAudioProcessor() {const audioContext = new (window.AudioContext || window.webkitAudioContext)();const processor = audioContext.createScriptProcessor(4096, 1, 1);processor.onaudioprocess = (audioEvent) => {if (this.socket.readyState === WebSocket.OPEN) {const inputBuffer = audioEvent.inputBuffer.getChannelData(0);const float32Array = new Float32Array(inputBuffer);this.socket.send(this.encodeAudio(float32Array));}};return processor;}encodeAudio(buffer) {// 实现PCM到16位整数的转换(示例简化)const int16Buffer = new Int16Array(buffer.length);for (let i = 0; i < buffer.length; i++) {int16Buffer[i] = buffer[i] * 32767;}return new Blob([int16Buffer], { type: 'audio/pcm' });}}
| 方案 | 适用场景 | 准确率 | 延迟 | 包体积 |
|---|---|---|---|---|
| WebAssembly | H5离线场景 | 75-85% | 200ms+ | 5MB+ |
| 原生插件 | App高性能需求 | 85-95% | 50ms | 10MB+ |
| 小程序扩展 | 微信/支付宝平台 | 80-90% | 100ms | 3MB |
波形绘制核心步骤:
AudioContext并解码音频文件。AnalyserNode获取频域数据。完整实现示例:
class AudioVisualizer {constructor(canvasId) {this.canvas = document.getElementById(canvasId);this.ctx = this.canvas.getContext('2d');this.audioContext = new (window.AudioContext || window.webkitAudioContext)();this.analyser = this.audioContext.createAnalyser();this.analyser.fftSize = 2048;}async visualize(audioBlob) {const arrayBuffer = await audioBlob.arrayBuffer();const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);const source = this.audioContext.createBufferSource();source.buffer = audioBuffer;source.connect(this.analyser);this.analyser.connect(this.audioContext.destination);this.drawWaveform(audioBuffer);}drawWaveform(audioBuffer) {const channelData = audioBuffer.getChannelData(0);const bufferLength = channelData.length;const sliceWidth = this.canvas.width / bufferLength;this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.lineWidth = 2;this.ctx.strokeStyle = '#00ff00';this.ctx.beginPath();let x = 0;for (let i = 0; i < bufferLength; i++) {const v = channelData[i] * this.canvas.height / 2;const y = v + this.canvas.height / 2;if (i === 0) {this.ctx.moveTo(x, y);} else {this.ctx.lineTo(x, y);}x += sliceWidth;}this.ctx.stroke();}}
模块划分:
/components/AudioRecorder.vue:封装跨端录音组件/services/asr.js:ASR服务接口/utils/audioProcessor.js:音频处理工具集/visualization/Waveform.vue:波形可视化组件状态管理:
{recording: false,audioUrl: null,asrResult: '',uploadProgress: 0,visualizerData: null}
环境检测工具:
const envUtils = {isH5: () => process.env.VUE_APP_PLATFORM === 'h5',isApp: () => /(android|ios)/i.test(process.env.VUE_APP_PLATFORM),isMiniProgram: () => /(mp-weixin|mp-alipay)/i.test(process.env.VUE_APP_PLATFORM)};
iOS录音失败:
<input>标签触发录音权限(iOS安全策略)
<input type="file" accept="audio/*" capture="microphone" style="display:none" ref="audioInput"><script>methods: {triggeriOSPermission() {this.$refs.audioInput.click();setTimeout(() => this.startRecord(), 500);}}</script>
小程序录音时长限制:
RecorderManager.onStop回调处理分段H5兼容性问题:
MediaRecorder的mimeType支持情况for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return ‘audio/wav’; // 最终降级方案
}
```
本文提供的方案已在多个uniapp项目中验证,开发者可根据实际需求调整实现细节。建议通过uni.getSystemInfoSync()获取设备信息,动态选择最优的录音参数(采样率、位深等)。对于商业项目,建议结合后端服务实现更精准的语音识别和存储管理。