浏览器如何处理音频流:MediaRecorder 与 Web Audio API

有没有想过,网站是如何让您录制语音笔记、玩带有动态音效的互动游戏,甚至将音乐可视化的?这些神奇的事情就发生在浏览器中,由专门用于处理音频流的复杂 JavaScript API 驱动。

如果您刚开始接触涉及声音的 Web 开发,您很快就会遇到两个主要 API:MediaRecorder API 和 Web Audio API。这两个 API 都能处理音频,但它们的用途和运行方式却截然不同。

本文以初学者易于理解的方式来分解这些 API,探索它们的工作原理,查看一些代码示例,并弄清楚何时使用哪种 API。

音频流到底是什么?

在浏览器中,音频流本质上是连续的音频数据流。可以把它想象成一条声音信息的河流。

音频流的来源有多种:

  • 麦克风:使用 navigator.mediaDevices.getUserMedia() 方法,我们可以请求访问用户的麦克风并获取实时音频流。
  • 音频文件:可以加载音频文件(如 MP3 或 WAV),并将其内容作为流进行处理。
  • Web Audio API 生成:Web Audio API 本身可以从头开始创建音频流(如合成器)。

获取实时音频流(如从麦克风获取)的最常用方法是使用 getUserMedia。它会返回一个 MediaStream 对象,这是 MediaRecorder 和 Web Audio API 都能使用的原始数据。

// 示例:获取麦克风流
async function getMicrophoneStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
    console.log("Microphone access granted!");
    // 现在 'stream' 保存了实时音频数据流
    return stream;
  } catch (err) {
    console.error("Error accessing microphone:", err);
    alert("Could not access the microphone. Please check permissions.");
    return null;
  }
}

// Later you might call:
// const myAudioStream = await getMicrophoneStream();
// if (myAudioStream) { /* Do something with the stream */ }

现在,让我们看看两个 API 如何处理这个 MediaStream。

MediaRecorder API 与 Web Audio API

MediaRecorder API:

可以将其视为简易录音机。它的主要功能是获取 MediaStream 并将其录制成标准音频文件格式(如 WebM、Ogg 或 MP4,具体取决于浏览器)。它是一种简单高效的音频采集工具。

Web Audio API:

可将其视为高级音频实验室。它专为实时处理、分析和合成音频而设计。它可以对声音进行精细控制,让您以各种方式对其进行操作(调节音量、添加效果、可视化频率、从头开始创建声音)。

MediaRecorder API

当您的目标仅仅是从流中捕获音频(或视频)并将其保存为文件时,MediaRecorder API 就是理想选择。

工作原理

  • 获取 MediaStream:通常来自 getUserMedia(麦克风),也可能来自 <video><canvas>元素(尽管我们关注的是音频)。
  • 创建 MediaRecorder 实例:创建一个新的 MediaRecorder 对象,并将流传递给其构造函数。您可以选择指定所需的输出格式(mimeType)。
  • 监听数据: 为 ondataavailable 事件添加一个事件监听器。该事件会在录音时定期触发(停止时也会触发一次),为您提供已录制的音频数据块。
  • 开始录制:调用 start() 方法。您可以选择向 start() 方法传递一个 timeslice 参数(以毫秒为单位),使 ondataavailable 每 X 毫秒触发一次。如果不提供 timeslice 参数,通常只会在记录停止时触发一次。
  • 收集数据块:在 ondataavailable 事件处理程序中,将接收到的数据块(即 Blob 对象)收集到一个数组中。
  • 停止录制:调用 stop() 方法。这会触发最后一个 ondataavailable 事件,然后触发一个 stop 事件。
  • 合成最终文件:录音停止后,将所有收集到的数据块合成一个 Blob。该 Blob 代表最终音频文件(如 .webm 或 .ogg 文件)。
  • 使用 Blob:可以为 Blob 创建一个可下载链接,将其上传到服务器,或在 <audio>元素中播放。

数据处理:

  • MediaRecorder 会将数据流中的原始音频数据编码为由 mimeType 指定的压缩格式(如 audio/webm;codecs=opus 或 audio/ogg;codecs=opus)。
  • 它以 Blob 对象的形式传输编码后的数据。Blob(二进制大对象)是一种代表原始、不可变数据的对象,本质上就像内存中的文件。
  • 使用 MediaRecorder,不能直接访问原始音频样本(如单个声波值)。您只需获取随时可用的编码文件块。

代码示例:录制麦克风音频

<!DOCTYPE html>
<html>
<head>
    <title>MediaRecorder Example</title>
</head>
<body>
    <h1>Simple Audio Recorder</h1>
    <button id="startButton">Start Recording</button>
    <button id="stopButton" disabled>Stop Recording</button>
    <audio id="audioPlayback" controls></audio>
    <a id="downloadLink" style="display: none;">Download Recording</a>

    <script>
        const startButton = document.getElementById('startButton');
        const stopButton = document.getElementById('stopButton');
        const audioPlayback = document.getElementById('audioPlayback');
        const downloadLink = document.getElementById('downloadLink');

        let mediaRecorder;
        let audioChunks = [];
        let audioStream;

        startButton.addEventListener('click', async () => {
            try {
                // 1. Get microphone stream
                audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });

                // 2. Create MediaRecorder instance
                // Let the browser pick the best mimeType
                mediaRecorder = new MediaRecorder(audioStream);

                // 3. Listen for data
                mediaRecorder.ondataavailable = event => {
                    if (event.data.size > 0) {
                        audioChunks.push(event.data);
                        console.log("Received chunk:", event.data);
                    }
                };

                // Handle stop event
                mediaRecorder.onstop = () => {
                    console.log("Recording stopped.");
                    // 7. Assemble the final file (Blob)
                    const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType || 'audio/webm' }); // Use recorded mimeType

                    // 8. Use the Blob
                    const audioUrl = URL.createObjectURL(audioBlob);
                    audioPlayback.src = audioUrl;
                    downloadLink.href = audioUrl;
                    // Use a more descriptive filename
                    const fileExtension = (mediaRecorder.mimeType || 'audio/webm').split('/')[1].split(';')[0];
                    downloadLink.download = `recording.${fileExtension}`;
                    downloadLink.style.display = 'block';


                    // Clean up
                    audioChunks = []; // Reset chunks for next recording
                    // Stop the tracks of the stream to turn off the mic light
                    audioStream.getTracks().forEach(track => track.stop());


                    startButton.disabled = false;
                    stopButton.disabled = true;
                };

                // 4. Start recording
                mediaRecorder.start(); // You could use mediaRecorder.start(1000); for 1-second chunks
                console.log("Recording started...");

                startButton.disabled = true;
                stopButton.disabled = false;
                downloadLink.style.display = 'none'; // Hide link during recording
                audioPlayback.src = ''; // Clear previous playback

            } catch (err) {
                console.error("Error starting recording:", err);
                alert("Could not start recording. Check microphone permissions.");
            }
        });

        stopButton.addEventListener('click', () => {
            if (mediaRecorder && mediaRecorder.state === 'recording') {
                // 6. Stop recording
                mediaRecorder.stop();
                // The rest happens in the 'onstop' handler
            }
        });
    </script>
</body>
</html>

MediaRecorder 的用例:

  • 聊天应用中的语音消息录制。
  • 简单的音频笔记应用程序。
  • 捕获用户音频反馈。
  • 直接在浏览器中使用基本的播客录制工具。

The Web Audio API

The Web Audio API 是一个完全不同的工具。它主要不是用于录制文件,而是用于底层上处理声音。

工作原理

核心概念是音频上下文(Audio Context)和音频路由图(Audio Routing Graph)。

创建 Audio Context:

这是管理一切的中心对象。所有音频操作都在此上下文中进行。

const audioContext = new ( window.AudioContext || window.webkitAudioContext ) ( ) ;

创建音频节点(Audio Nodes)

Web Audio API 使用节点系统。每个节点代表一个音频源、一个处理步骤(效果)或一个目标。常见的节点类型包括:

  • 音源节点:生成音频或将音频引入图表(例如,用于创建音调的 OscillatorNode 、用于播放已加载音频文件的 AudioBufferSourceNode 、用于使用类似麦克风 MediaStreamAudioSourceNode)。
  • 处理节点:修改音频信号(例如,控制音量的 GainNode、均衡/滤波器的 BiquadFilterNode、回声DelayNode、混响的 ConvolverNode)。
  • 目标节点:通常是 audioContext.destination,表示计算机的扬声器。通常只有一个主目标节点。
  • 分析节点:在不修改音频信号的情况下检查音频信号(例如,AnalyserNode 用于获取频率或时域数据以进行可视化)。

连接节点

使用 connect() 方法将一个节点的输出连接到另一个节点的输入。这将创建一个处理链或图。音频从音源流出,经过处理节点,最后到达目的地。

示例:

Microphone (MediaStreamAudioSourceNode) -> Volume Control (GainNode) -> Speakers (audioContext.destination)

数据处理:

  • Web Audio API 处理原始的、未压缩的音频数据。这些数据通常表示为浮点数数组(通常在 -1.0 到 +1.0 之间),表示特定时间点声波的振幅。这种格式通常称为脉冲编码调制 (PCM)
  • 加载音频文件时,它们会被解码为一个AudioBuffer对象,该对象存储原始 PCM 数据。
  • 在处理实时流或生成声音时,API 会在内部管理节点之间流动的原始数据的缓冲区。
  • 这种原始数据访问允许进行精确的操作、分析和综合。

代码示例:带音量控制的麦克风输入

<!DOCTYPE html>
<html>
<head>
    <title>Web Audio API Example</title>
</head>
<body>
    <h1>Microphone Input Volume Control</h1>
    <button id="startButton">Start Audio</button>
    <label for="volumeSlider">Volume:</label>
    <input type="range" id="volumeSlider" min="0" max="2" step="0.01" value="1">

    <script>
        const startButton = document.getElementById('startButton');
        const volumeSlider = document.getElementById('volumeSlider');
        let audioContext;
        let microphoneSource;
        let gainNode;

        startButton.addEventListener('click', async () => {
            if (!audioContext) {
                // 1. Create AudioContext (only once!)
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
            }

            // Resume context if it was suspended (autoplay policies)
            if (audioContext.state === 'suspended') {
                await audioContext.resume();
            }

            try {
                // Get microphone stream
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

                // 2. Create Source Node from the stream
                microphoneSource = audioContext.createMediaStreamSource(stream);

                // 2. Create Processing Node (Gain for volume)
                gainNode = audioContext.createGain();
                gainNode.gain.value = volumeSlider.value; // Set initial volume

                // 3. Connect the Nodes
                // Mic -> Gain -> Speakers (Destination)
                microphoneSource.connect(gainNode);
                gainNode.connect(audioContext.destination);

                console.log("Audio graph connected: Mic -> Gain -> Speakers");
                startButton.textContent = "Audio Running";
                startButton.disabled = true; // Prevent multiple starts

            } catch (err) {
                console.error("Error setting up Web Audio:", err);
                alert("Could not set up audio. Check microphone permissions.");
            }
        });

        volumeSlider.addEventListener('input', () => {
            if (gainNode) {
                // Update gain value smoothly
                gainNode.gain.setValueAtTime(volumeSlider.value, audioContext.currentTime);
                console.log("Volume set to:", volumeSlider.value);
            }
        });
    </script>
</body>
</html>

结论

现代浏览器提供了许多出色的音频处理工具。MediaRecorder API提供了一种简单易用的音频流捕获和保存方法,非常适合录制任务。另一方面,Web Audio API 则开启了无限的创意可能,通过其强大的基于节点的架构,可以对声音进行精细的操作、分析和合成。

作者:Tihomir Manushev
内容源自medium。

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/yinshipin/57999.html

(1)

相关推荐

发表回复

登录后才能评论