跨浏览器音频录制:实现兼容的音频捕获与WAV格式生成

发布于:2025-05-01 ⋅ 阅读:(28) ⋅ 点赞:(0)

在现代Web开发中,音频录制功能越来越受到开发者的关注。无论是在线会议、语音识别还是简单的语音留言,音频录制都是一个重要的功能。然而,实现一个跨浏览器的音频录制功能并非易事,因为不同浏览器对音频录制API的支持存在差异。本文将探讨如何实现一个兼容Chrome、Firefox、Safari和Edge的音频录制功能,并生成标准的WAV格式音频文件。

一、浏览器兼容性概述

在开始之前,我们需要了解不同浏览器对MediaRecorder API的支持情况,以及它们支持的音频格式。

1. Chrome

  • 支持MediaRecorder API。
  • 音频格式:默认生成audio/webm格式。
  • 注意事项:不支持直接生成audio/wav格式。

2. Firefox

  • 支持MediaRecorder API。
  • 音频格式:支持生成audio/wav格式。
  • 注意事项:也支持audio/webm格式。

3. Safari

  • 支持MediaRecorder API。
  • 音频格式:默认生成audio/webm格式。
  • 注意事项:不支持直接生成audio/wav格式。

4. Edge

  • 支持MediaRecorder API。
  • 音频格式:默认生成audio/webm格式。
  • 注意事项:不支持直接生成audio/wav格式。

二、解决方案

为了实现一个跨浏览器的音频录制功能,我们有两种主要的解决方案:

1. 统一使用audio/webm格式

在所有浏览器中使用audio/webm格式进行录制。如果后端服务需要处理audio/wav格式,可以在服务器端将audio/webm转换为audio/wav。这种方法的优点是简单,缺点是需要后端支持格式转换。

2. 在客户端生成audio/wav格式

使用AudioContextScriptProcessorNode(或AudioWorkletNode)来录制音频,并生成标准的audio/wav文件。这种方法的优点是可以在客户端生成标准的WAV文件,无需依赖后端转换。缺点是实现相对复杂。

三、实现客户端生成audio/wav格式

为了实现一个跨浏览器的音频录制功能,我们选择第二种方案:在客户端生成audio/wav格式。以下是实现步骤和代码示例。

1. HTML结构

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Audio Recorder</title>
</head>
<body>
    <button id="startButton">开始录音</button>
    <button id="stopButton" disabled>停止录音</button>
    <script src="recorder.js"></script>
</body>
</html>

2. JavaScript实现

let audioContext;
let processor;
let source;
let audioChunks = [];
let sampleRate = 16000;

document.getElementById('startButton').addEventListener('click', async () => {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate });
        source = audioContext.createMediaStreamSource(stream);
        processor = audioContext.createScriptProcessor(4096, 1, 1);

        processor.onaudioprocess = event => {
            const inputBuffer = event.inputBuffer.getChannelData(0);
            audioChunks.push(new Float32Array(inputBuffer));
        };

        source.connect(processor);
        processor.connect(audioContext.destination);

        document.getElementById('startButton').disabled = true;
        document.getElementById('stopButton').disabled = false;
    } catch (error) {
        console.error('Error accessing microphone:', error);
        alert('无法访问麦克风,请检查权限设置');
    }
});

document.getElementById('stopButton').addEventListener('click', () => {
    processor.disconnect();
    source.disconnect();
    audioContext.close();

    const mergedData = mergeArrays(audioChunks);
    const wavBlob = createWAVBlob(mergedData, sampleRate);

    const url = URL.createObjectURL(wavBlob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'recording.wav';
    a.click();

    document.getElementById('startButton').disabled = false;
    document.getElementById('stopButton').disabled = true;
    audioChunks = [];
});

function mergeArrays(arrays) {
    const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
    const result = new Float32Array(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}

function createWAVBlob(audioData, sampleRate) {
    const numChannels = 1;
    const bitDepth = 16;
    const byteRate = sampleRate * numChannels * bitDepth / 8;
    const blockAlign = numChannels * bitDepth / 8;

    const buffer = new ArrayBuffer(44 + audioData.length * 2);
    const view = new DataView(buffer);

    writeString(view, 0, 'RIFF');
    view.setUint32(4, 36 + audioData.length * 2, true);
    writeString(view, 8, 'WAVE');
    writeString(view, 12, 'fmt ');
    view.setUint32(16, 16, true);
    view.setUint16(20, 1, true);
    view.setUint16(22, numChannels, true);
    view.setUint32(24, sampleRate, true);
    view.setUint32(28, byteRate, true);
    view.setUint16(32, blockAlign, true);
    view.setUint16(34, bitDepth, true);
    writeString(view, 36, 'data');
    view.setUint32(40, audioData.length * 2, true);

    const int16Data = new Int16Array(audioData.length);
    for (let i = 0; i < audioData.length; i++) {
        int16Data[i] = Math.min(1, Math.max(-1, audioData[i])) * 0x7FFF;
    }

    let offset = 44;
    for (let i = 0; i < int16Data.length; i++) {
        view.setInt16(offset, int16Data[i], true);
        offset += 2;
    }

    return new Blob([view], { type: 'audio/wav' });
}

function writeString(view, offset, string) {
    for (let i = 0; i < string.length; i++) {
        view.setUint8(offset + i, string.charCodeAt(i));
    }
}

四、代码说明

1. 开始录音

  • 使用navigator.mediaDevices.getUserMedia获取麦克风权限。
  • 创建AudioContextScriptProcessorNode,捕获音频数据并存储到audioChunks中。

2. 停止录音

  • 断开ScriptProcessorNodeAudioContext的连接。
  • 合并所有音频数据片段,生成audio/wav格式的Blob

3. 生成WAV文件

  • 使用createWAVBlob函数将PCM数据转换为WAV格式。
  • 使用URL.createObjectURL创建一个可下载的链接。

4. 工具函数

  • mergeArrays:合并所有音频数据片段。
  • createWAVBlob:生成WAV文件头并转换音频数据。
  • writeString:将字符串写入DataView

五、优点

  • 跨浏览器兼容性:这种方法在所有现代浏览器中都能正常工作,包括Chrome、Firefox、Safari和Edge。
  • 灵活性:可以在客户端生成标准的WAV文件,无需依赖后端转换。

六、结论

实现一个跨浏览器的音频录制功能并非易事,但通过使用AudioContextScriptProcessorNode,我们可以在客户端生成标准的WAV文件,从而实现一个兼容所有现代浏览器的音频录制功能。希望本文的介绍和代码示例能帮助你实现自己的音频录制功能。



网站公告

今日签到

点亮在社区的每一天
去签到