Vue3 + js-audio-recorder 保姆级教程:从录音权限获取到PCM/WAV文件下载
Vue3 js-audio-recorder 全栈实战从零构建高可用音频录制系统在Web应用中实现音频录制功能一直是前端开发中的常见需求尤其是随着WebRTC技术的普及浏览器端音频处理能力得到了显著提升。本文将带你深入探索如何在Vue3项目中利用js-audio-recorder这个轻量级库构建一个功能完善、性能优异的音频录制系统。1. 环境准备与项目初始化首先确保你的开发环境已经安装了Node.js建议版本16和npm/yarn。我们使用Vite作为构建工具它能提供更快的开发体验。npm create vitelatest vue3-audio-recorder --template vue-ts cd vue3-audio-recorder npm install js-audio-recorder安装完成后我们需要在vite.config.ts中确认配置支持TypeScriptimport { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()] })2. 核心录音功能实现我们将使用Vue3的Composition API来封装录音逻辑创建一个可复用的useAudioRecorder组合式函数。// src/composables/useAudioRecorder.ts import Recorder from js-audio-recorder import { ref, onUnmounted } from vue export default function useAudioRecorder() { const recorder refRecorder | null(null) const isRecording ref(false) const isPaused ref(false) const duration ref(0) const timer refNodeJS.Timeout | null(null) const initRecorder () { recorder.value new Recorder({ sampleBits: 16, sampleRate: 48000, numChannels: 1 }) } const startRecording async () { if (!recorder.value) initRecorder() try { await Recorder.getPermission() recorder.value.start() isRecording.value true isPaused.value false timer.value setInterval(() { duration.value recorder.value?.duration || 0 }, 100) } catch (error) { console.error(麦克风权限获取失败:, error) throw error } } const pauseRecording () { if (recorder.value isRecording.value) { recorder.value.pause() isPaused.value true clearInterval(timer.value!) } } const resumeRecording () { if (recorder.value isPaused.value) { recorder.value.resume() isPaused.value false timer.value setInterval(() { duration.value recorder.value?.duration || 0 }, 100) } } const stopRecording () { if (recorder.value isRecording.value) { recorder.value.stop() isRecording.value false clearInterval(timer.value!) } } onUnmounted(() { if (timer.value) clearInterval(timer.value) if (recorder.value) recorder.value.destroy() }) return { recorder, isRecording, isPaused, duration, startRecording, pauseRecording, resumeRecording, stopRecording } }3. 音频播放与文件处理录音完成后我们通常需要实现播放和文件下载功能。js-audio-recorder提供了丰富的API支持这些操作。// 在useAudioRecorder.ts中继续扩展 const playAudio () { if (recorder.value !isRecording.value) { recorder.value.play() } } const pausePlayback () { if (recorder.value) { recorder.value.pausePlay() } } const stopPlayback () { if (recorder.value) { recorder.value.stopPlay() } } const downloadWAV (filename recording_${Date.now()}) { if (recorder.value !isRecording.value) { recorder.value.downloadWAV(filename) } } const downloadPCM (filename recording_${Date.now()}) { if (recorder.value !isRecording.value) { recorder.value.downloadPCM(filename) } } const getAudioBlob (format: wav | pcm wav) { if (!recorder.value) return null return format wav ? recorder.value.getWAVBlob() : recorder.value.getPCMBlob() }4. 完整组件实现与UI集成现在我们将上述功能集成到一个完整的Vue组件中并使用Element Plus作为UI框架。!-- src/components/AudioRecorder.vue -- template div classaudio-recorder el-card template #header div classcard-header h3音频录制器/h3 span录制时长: {{ formatTime(duration) }}/span /div /template div classcontrols el-button typeprimary clickstartRecording :disabledisRecording !isPaused 开始录音 /el-button el-button typewarning clickpauseRecording :disabled!isRecording || isPaused 暂停 /el-button el-button typesuccess clickresumeRecording :disabled!isRecording || !isPaused 继续 /el-button el-button typedanger clickstopRecording :disabled!isRecording 停止 /el-button /div div classplayback v-if!isRecording duration 0 h4播放控制/h4 el-button typeprimary clickplayAudio播放/el-button el-button typewarning clickpausePlayback暂停/el-button el-button typedanger clickstopPlayback停止/el-button /div div classdownload v-if!isRecording duration 0 h4文件导出/h4 el-button clickdownloadWAV下载WAV/el-button el-button clickdownloadPCM下载PCM/el-button /div /el-card /div /template script setup langts import { ref } from vue import useAudioRecorder from /composables/useAudioRecorder import { ElButton, ElCard } from element-plus const { recorder, isRecording, isPaused, duration, startRecording, pauseRecording, resumeRecording, stopRecording, playAudio, pausePlayback, stopPlayback, downloadWAV, downloadPCM } useAudioRecorder() const formatTime (seconds: number) { const mins Math.floor(seconds / 60) const secs Math.floor(seconds % 60) return ${mins}:${secs 10 ? 0 : }${secs} } /script style scoped .audio-recorder { max-width: 600px; margin: 0 auto; } .card-header { display: flex; justify-content: space-between; align-items: center; } .controls, .playback, .download { margin: 20px 0; } h4 { margin: 10px 0; } /style5. 高级功能与性能优化5.1 音频可视化我们可以利用Web Audio API实现音频波形可视化。首先在组件中添加canvas元素template !-- 其他代码不变 -- div classvisualizer canvas refcanvas/canvas /div /template然后扩展我们的组合式函数// 在useAudioRecorder.ts中添加 const setupVisualizer (canvas: HTMLCanvasElement) { if (!recorder.value) return const audioContext new (window.AudioContext || (window as any).webkitAudioContext)() const analyser audioContext.createAnalyser() analyser.fftSize 256 const source audioContext.createMediaStreamSource(recorder.value.getStream()) source.connect(analyser) const bufferLength analyser.frequencyBinCount const dataArray new Uint8Array(bufferLength) const ctx canvas.getContext(2d)! const WIDTH canvas.width const HEIGHT canvas.height const draw () { requestAnimationFrame(draw) analyser.getByteFrequencyData(dataArray) ctx.fillStyle rgb(200, 200, 200) ctx.fillRect(0, 0, WIDTH, HEIGHT) const barWidth (WIDTH / bufferLength) * 2.5 let x 0 for (let i 0; i bufferLength; i) { const barHeight dataArray[i] / 2 ctx.fillStyle rgb(${barHeight 100}, 50, 50) ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight) x barWidth 1 } } draw() }5.2 音频质量配置js-audio-recorder支持多种音频参数配置我们可以提供选项让用户自定义interface AudioConfig { sampleBits: 8 | 16 sampleRate: 11025 | 16000 | 22050 | 24000 | 44100 | 48000 numChannels: 1 | 2 } const defaultConfig: AudioConfig { sampleBits: 16, sampleRate: 48000, numChannels: 1 } const config refAudioConfig({ ...defaultConfig }) const updateConfig (newConfig: PartialAudioConfig) { config.value { ...config.value, ...newConfig } }5.3 内存管理与性能优化长时间录音可能会占用大量内存我们需要做好资源管理const clearRecording () { if (recorder.value) { recorder.value.destroy() recorder.value null duration.value 0 } } onUnmounted(() { clearRecording() if (timer.value) clearInterval(timer.value) })6. 常见问题与解决方案6.1 麦克风权限问题现代浏览器要求用户明确授权才能访问麦克风。我们可以实现更友好的权限处理const checkPermission async () { try { const stream await navigator.mediaDevices.getUserMedia({ audio: true }) stream.getTracks().forEach(track track.stop()) return true } catch (error) { console.error(麦克风访问被拒绝:, error) return false } } // 在组件中提示用户 const handleStartRecording async () { const hasPermission await checkPermission() if (!hasPermission) { ElMessage.error(需要麦克风权限才能录音) return } await startRecording() }6.2 跨浏览器兼容性不同浏览器对音频API的支持程度不同我们可以添加特性检测const isSupported () { return !!( navigator.mediaDevices navigator.mediaDevices.getUserMedia (window.AudioContext || (window as any).webkitAudioContext) ) } if (!isSupported()) { ElMessage.warning(您的浏览器不支持音频录制功能) }6.3 移动端适配移动设备上的音频处理有一些特殊考虑const isMobile /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) if (isMobile) { // 移动端可能需要不同的采样率配置 updateConfig({ sampleRate: 16000 }) }7. 测试与调试技巧7.1 单元测试我们可以为组合式函数编写单元测试// tests/useAudioRecorder.spec.ts import { renderHook } from testing-library/vue import useAudioRecorder from /composables/useAudioRecorder describe(useAudioRecorder, () { it(should initialize correctly, () { const { result } renderHook(() useAudioRecorder()) expect(result.value.isRecording.value).toBe(false) expect(result.value.isPaused.value).toBe(false) expect(result.value.duration.value).toBe(0) }) // 更多测试用例... })7.2 调试工具Chrome开发者工具提供了强大的媒体调试功能打开Chrome DevTools (F12)转到Application标签在左侧菜单中选择Media这里可以查看所有活动的媒体流和录制状态7.3 性能监控对于长时间录音监控内存使用情况很重要setInterval(() { const memory performance.memory console.log(内存使用: ${memory.usedJSHeapSize / 1024 / 1024} MB) }, 5000)8. 实际应用场景扩展8.1 语音识别集成我们可以将录音与Web Speech API结合实现语音转文字功能const recognizeSpeech async (audioBlob: Blob) { const audioContext new AudioContext() const arrayBuffer await audioBlob.arrayBuffer() const audioBuffer await audioContext.decodeAudioData(arrayBuffer) // 这里需要调用语音识别服务API // 实际项目中可以使用Web Speech API或第三方服务 } // 在组件中使用 const handleSpeechRecognition async () { const blob getAudioBlob(wav) if (blob) { const text await recognizeSpeech(blob) console.log(识别结果:, text) } }8.2 音频编辑功能添加简单的音频剪辑功能const trimAudio (startTime: number, endTime: number) { if (!recorder.value) return null const originalBlob recorder.value.getWAVBlob() // 这里需要实现音频裁剪逻辑 // 实际项目中可能需要使用第三方库或Web Audio API }8.3 云端存储集成将录音文件上传到云存储服务const uploadToCloud async (provider: aws | azure | firebase) { const blob getAudioBlob(wav) if (!blob) return const formData new FormData() formData.append(file, blob, recording_${Date.now()}.wav) try { let endpoint switch (provider) { case aws: endpoint YOUR_AWS_ENDPOINT break case azure: endpoint YOUR_AZURE_ENDPOINT break case firebase: endpoint YOUR_FIREBASE_ENDPOINT break } const response await fetch(endpoint, { method: POST, body: formData }) return await response.json() } catch (error) { console.error(上传失败:, error) throw error } }