【web音频学习(三)】 Web Audio API的使用

204 阅读5分钟

Web Audio API 并不会取代 <audio> 音频元素,倒不如说它是 <audio> 的补充更好,就好比如 <canvas><img> 共存的关系。你使用来实现音频的方式取决于你的使用情况。如果你只是想控制一个简单的音轨的播放,<audio> 或许是一个更好更快的选择。如果你想实现更多复杂的音频处理,以及播放,Web Audio API 提供了更多的优势以及控制。

音频导向图

Web Audio API 基本的音频操作是基于多个 audio nodes 音频节点连接起来形成一个音频导向图

音频节点可以通过各自的输入与输出相连,形成从多个声源开始,经过处理节点,终止于末节点的链式结构(有时你不需要末节点,比如你只是想数字化处理某些音频数据的时候)。

一个简单、典型的网页音频接口的操作流程可以是这样的:

  1. 创建一个音频环境。
  2. 在音频环境中,创建声源——例如 <audio> 标签。
  3. 创建处理节点——例如混响,双二阶滤波,声相控制,音频振幅压缩。
  4. 选择音频的终点——例如系统的扬声器。
  5. 连接声源和特效,以及特效和终点。

image-20250922095631554

每个输入和输出都可以包括几个声道,声道代表了一个特定的音效通道。各种声道分离结构都可以使用,包括单声道立体声四声道5.1等等。

image-20250922102709340

我们的音箱看起来像这样:

<body>
    <audio id="audio"
        src="https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3"
        crossorigin="anonymous"></audio>

    <button id="play">播放</button>
    <button id="pause">暂停</button>

    <div>
        <label>音量控制</label>
        <input type="range" id="volume" min="0" max="1" value="0.5" step="0.01">
    </div>

    <div>
        <label>左右扬声器平衡</label>
        <input type="range" id="panner" min="-1" max="1" value="0" step="0.01">
    </div>

    <script>
        const playButton = document.getElementById('play')
        const pauseButton = document.getElementById('pause')
        const audioElement = document.getElementById('audio')
        const volumeInput = document.getElementById('volume')
        const pannerInput = document.getElementById('panner')

        const audioContext = new AudioContext()
        const source = audioContext.createMediaElementSource(audioElement)
        const gainNode = audioContext.createGain()
        const pannerNode = audioContext.createStereoPanner()

        gainNode.gain.value = volumeInput.value
        pannerNode.pan.value = pannerInput.value

        source.connect(gainNode)
        gainNode.connect(pannerNode)
        pannerNode.connect(audioContext.destination)

        playButton.addEventListener('click', async () => {
            if (audioContext.state === 'suspended') {
                await audioContext.resume()
            }
            audioElement.play()
        })

        pauseButton.addEventListener('click', () => {
            audioElement.pause()
        })

        volumeInput.addEventListener('input', () => {
            gainNode.gain.value = volumeInput.value
        })

        pannerInput.addEventListener('input', () => {
            pannerNode.pan.value = pannerInput.value
        })

    </script>
</body>

image-20250922200527110

  • createMediaElementSource:将<audio>标签元素转换为音频源节点。
  • createGain:调节音频的音量。
  • createStereoPanner:调节音频在立体声场中的左右分布。

其音频节点:

  • createOscillator:生成基础音频波形(如正弦波、方波)。
  • createBufferSource:播放内存中的音频片段。
  • createBiquadFilter:对音频进行频率过滤(如低通、高通)。
  • createDelay:为音频添加延迟 / 回声效果。
  • createConvolver:模拟不同空间的声学效果(如混响)。
  • createAnalyser:获取音频数据用于可视化(如频谱图)。
  • destination:音频的最终输出目标(如扬声器)。

音频上下文

为了能通过 Web Audio API 执行任何操作,我们需要创建音频上下文实例。这能让我们访问 API 所有的特性和功能。

const audioContext = new (window.AudioContext || window.webkitAudioContext)({
    // 可选,默认由系统决定
    sampleRate: 44100,
});

sampleRate

console.log(audioContext.sampleRate);   // 44100(只读)

参与 音频播放

state

console.log(audioContext.state);        // 初始多为 "suspended"
状态值含义说明
suspended暂停状态,不会处理音频。浏览器为了节省资源,默认会延迟启动,直到用户与页面交互。
running运行状态,音频正常流动。通常在用户点击、触摸等交互后调用 audioContext.resume() 进入。
closed已关闭状态,上下文被永久关闭,无法再使用。

currentTime

为整个音频上下文提供唯一时间基准,所有什么时候开始 / 停止 / 变化都必须用这个数字告诉浏览器。

从音频上下文诞生那一刻起就单调递增不可回拨不可手动设置

给所有音频事件做秒表——start()stop()setValueAtTime() 等时间参数都要以它为准。

// 开始播放音频
oscillatorNode.start()
// 播放 0.25 停止播放
oscillatorNode.stop(audioContext.currentTime + 0.25)

destination

任何节点想被用户听见,最终都要直接或间接连到 destination,他代表浏览器默认输出设备(扬声器 / 耳机)。

// 创建音频源数据节点
const oscillatorNode = audioContext.createOscillator();
// 将音频数据传入输出设备播放
oscillatorNode.connect(audioContext.destination);

音频数据来源

Javascript 生成声音节点

<body>
    <button id="play">播放</button>
    <script>
        const playButton = document.getElementById('play')
        playButton.addEventListener('click', async () => {
            const audioContext = new AudioContext()
            const oscillatorNode = audioContext.createOscillator();
            oscillatorNode.connect(audioContext.destination); 
            oscillatorNode.type = 'sine';
            oscillatorNode.frequency.value = "523.25";
            oscillatorNode.start(0);
            oscillatorNode.stop(1); 
        })
    </script>
</body>

PCM 原始数据

<body>
    <button id="play">播放</button>
    <script>
        (async () => {
            const playBtn = document.getElementById('play')

            const audioUrl = `https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3`

            async function convertAudioToPcm(audioUrl) {
                const response = await fetch(audioUrl);
                const arrayBuffer = await response.arrayBuffer();
                const audioContext = new AudioContext();
                const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
                const sampleRate = audioBuffer.sampleRate;
                const channelData = audioBuffer.getChannelData(0);
                const float32Array = new Float32Array(channelData);
                return {
                    arrayBuffer: float32Array,
                    sampleRate: sampleRate
                }
            }

            playBtn.addEventListener('click', async () => {
                const { arrayBuffer, sampleRate } = await convertAudioToPcm(audioUrl)
                const audioContext = new AudioContext();
                const audioBuffer = audioContext.createBuffer(1, arrayBuffer.length, sampleRate);
                audioBuffer.getChannelData(0).set(arrayBuffer);
                const source = audioContext.createBufferSource();
                source.buffer = audioBuffer;
                source.connect(audioContext.destination);
                source.start();
            })
        })()
    </script>
</body>

image-20250922173830872

  • 两次出现的AudioBuffer音频资源对象:
    • audioContext.decodeAudioData(arrayBuffer):对 MP3、WAV 等压缩格式的原始音频文件二进制数据(arrayBuffer)解码后生成。
    • audioContext.createBuffer(1, arrayBuffer.length, sampleRate):手动创建的空AudioBuffer,需手动写入从第一个AudioBuffer提取的Float32Array采样数据。

audio标签生成声音节点

<body>
    <button id="play">播放</button>
    <audio id="audio" src="https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3" crossorigin="anonymous"></audio>
    <script>
        const playButton = document.getElementById('play')
        const audioElement = document.getElementById('audio')

        playButton.addEventListener('click', async () => {
            const audioContext = new AudioContext()
            const sourceNode = audioContext.createMediaElementSource(audioElement)
            sourceNode.connect(audioContext.destination)
            audioElement.play()
        })
    </script>
</body>

调用createMediaElementSource()之后,<audio>标签的出声权被 Web Audio 音频导向图正式接管

浏览器不再把原始 <audio>标签直接送进扬声器,必须接入的节点链(示例里就是destination)。在执行audioElement.play()时,如果这条链有没有通到 destination ,用户听不见声音,一旦忘了 connect(或中途 disconnect),页面会立刻静音,而 <audio> 标签的进度条照样跑:这就是典型的节点已脱离音频导向图

WebRTC MediaStream生成声音节点

<body>
    <button id="play">播放</button>
    <script>
        const playBtn = document.getElementById('play')
        playBtn.addEventListener('click', async () => {
            const audioContext = new AudioContext()
            const stream = await navigator.mediaDevices.getUserMedia({
                audio: true
            })
            const source = audioContext.createMediaStreamSource(stream)
            source.connect(audioContext.destination)
        })
    </script>
</body>

播放多个音频源节点数据

音频节点ended 实现递归播放

<body>
    <button id="play">播放</button>
    <script>
        const playButton = document.getElementById('play')

        playButton.addEventListener('click', async () => {
            const audioContext = new AudioContext()
            const noteFreq = {
                C4: 261.63,
                D4: 293.66,
                E4: 329.63,
                F4: 349.23,
                G4: 392.00,
                A4: 440.00,
                B4: 493.88,
                C5: 523.25,
            };

            function playNote(index) {
                const oscillatorNode = audioContext.createOscillator();
                oscillatorNode.type = 'sine';
                oscillatorNode.frequency.value = Object.values(noteFreq)[index];
                oscillatorNode.connect(audioContext.destination)
                oscillatorNode.start()
                oscillatorNode.stop(audioContext.currentTime + 0.25)
                oscillatorNode.addEventListener('ended', () => {
                    if (index + 1 < Object.values(noteFreq).length) {
                        playNote(index + 1)
                    }
                })
            }

            playNote(0)
        })
    </script>
</body>

image-20250922145009386

currentTime 设置不同时间播放多个音频

<body>
    <button id="play">播放</button>
    <script>
        const playButton = document.getElementById('play')

        playButton.addEventListener('click', async () => {
            const audioContext = new AudioContext()
            const noteFreq = {
                C4: 261.63,
                D4: 293.66,
                E4: 329.63,
                F4: 349.23,
                G4: 392.00,
                A4: 440.00,
                B4: 493.88,
                C5: 523.25,
            };
            const dur = 0.25;
            const now = audioContext.currentTime;
            Object.keys(noteFreq).forEach((n, i) => {
                const oscillatorNode = audioContext.createOscillator();
                oscillatorNode.type = 'sine';
                oscillatorNode.frequency.value = noteFreq[n];
                oscillatorNode.connect(audioContext.destination);
                oscillatorNode.start(now + i * dur);
                oscillatorNode.stop(now + (i + 1) * dur);
            });
        })
    </script>
</body>