【web音频学习(五)】音频数值参数更改

82 阅读9分钟

重采样

重采样是音频处理中调整采样率的过程,目的是使音频数据适配不同的播放设备或处理需求。采样率定义为每秒的采样点数,单位为 Hz,直接影响音频的频率响应范围和数据量。

AudioContext.decodeAudioData 的自动重采样

decodeAudioData 是 Web Audio API 中用于解码编码音频格式(MP3、WAV 等)的心方法,其核心特性之一是:会根据 AudioContext 的基准采样率自动重采样,确保生成的 AudioBuffer 采样率与基准值一致,避免播放时出现快放 / 慢放。

<body>
    <button id="playBtn">播放</button>
    <script>
        (async () => {
            const playButton = document.getElementById('playBtn')
​
            playButton.addEventListener('click', async () => {
                // 1. 拉取远程 MP3 音频(假设原始采样率为 44100Hz)
                const audioUrl = 'https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3'
                const response = await fetch(audioUrl)
                const arrayBuffer = await response.arrayBuffer()
​
                // 2. 创建音频上下文,固定基准采样率为 16000Hz
                const audioContext = new AudioContext({
                    sampleRate: 16000 // 基准采样率 = 目标播放采样率
                })
​
                // 3. 自动重采样:无论原始音频采样率是多少,解码后 audioBuffer 采样率 = 16000Hz
                const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
                console.log('自动重采样后采样率:', audioBuffer.sampleRate) // 输出 16000
​
                // 4. 播放:audioBuffer 采样率与硬件播放采样率一致,正常语速播放
                const source = audioContext.createBufferSource()
                source.buffer = audioBuffer
                source.connect(audioContext.destination)
                source.start()
            })
        })()
    </script>
</body>
  • 触发条件:当原始音频(如示例中 MP3)的采样率 ≠ AudioContext.sampleRate 时,decodeAudioData 自动执行重采样。
  • 最终结果audioBuffer.sampleRate 强制对齐 AudioContext.sampleRate,确保后续播放时无速度 / 音调异常。
  • 局限性:仅支持解码 编码音频格式(MP3、WAV、AAC 等),无法直接处理 裸 PCM 二进制数据(无格式头的纯采样数据)。

PCM 转 WAV:让 decodeAudioData 支持裸 PCM

裸 PCM 数据仅包含采样点二进制信息,无采样率、位深等元数据,decodeAudioData 无法解析。需先将其封装为 WAV 格式(补充格式头),再进行解码。

<body>
    <button id="playBtn">播放</button>
    <script>
        (async () => {
            const playButton = document.getElementById('playBtn')
​
            // 步骤1:从 MP3 解码获取裸 PCM 数据(模拟“仅拥有裸 PCM”场景)
            async function generatePcm() {
                const audioUrl = 'https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3'
                const response = await fetch(audioUrl)
                const arrayBuffer = await response.arrayBuffer()
​
                // 临时音频上下文:解码 MP3 得到 44100Hz 的 PCM(Float32 类型,单声道)
                const tempAudioContext = new AudioContext({ sampleRate: 44100 })
                const audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer)
                return audioBuffer.getChannelData(0) // 返回裸 PCM 数据
            }
​
            // 步骤2:将裸 PCM 封装为 WAV 格式(补充 WAV 头信息)
            function pcmToWav(pcm32Buffer, sampleRate) {
                const bitsPerSample = 16; // 16位深度(PCM 存储格式)
                const numChannels = 1;    // 单声道
                const pcm16Buffer = new Int16Array(pcm32Buffer.length);
​
                // 1. Float32 类型 PCM 转为 16 位整数(WAV 常用格式)
                for (let i = 0; i < pcm32Buffer.length; i++) {
                    // 限制范围:16位整数的取值范围是 [-32768, 32767]
                    pcm16Buffer[i] = Math.max(-0x8000, Math.min(0x7FFF, Math.round(pcm32Buffer[i] * 0x7FFF)));
                }
​
                // 2. 创建 WAV 缓冲区(44字节头 + PCM 数据)
                const wavBuffer = new ArrayBuffer(44 + pcm16Buffer.byteLength);
                const view = new DataView(wavBuffer);
​
                // 3. 写入 WAV 头信息(固定格式,描述音频参数)
                const writeString = (offset, str) => {
                    for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
                };
                writeString(0, "RIFF");                // RIFF 标识
                view.setUint32(4, pcm16Buffer.byteLength + 36, true); // 文件总大小
                writeString(8, "WAVE");                // WAVE 标识
                writeString(12, "fmt ");               // 格式块标识
                view.setUint32(16, 16, true);          // 格式块大小(16 表示 PCM 格式)
                view.setUint16(20, 1, true);           // 音频格式(1 = PCM)
                view.setUint16(22, numChannels, true); // 声道数
                view.setUint32(24, sampleRate, true);  // 采样率(与原始 PCM 一致)
                // 字节率 = 采样率 × 声道数 × 位深/8
                view.setUint32(28, sampleRate * numChannels * bitsPerSample / 8, true);
                // 块对齐 = 声道数 × 位深/8
                view.setUint16(32, numChannels * bitsPerSample / 8, true);
                view.setUint16(34, bitsPerSample, true); // 位深
                writeString(36, "data");               // 数据块标识
                view.setUint32(40, pcm16Buffer.byteLength, true); // PCM 数据大小
​
                // 4. 写入 PCM 数据
                const pcmView = new Int16Array(wavBuffer, 44);
                pcmView.set(pcm16Buffer);
​
                return wavBuffer; // 返回 WAV 格式的二进制数据
            }
​
            // 步骤3:播放逻辑(WAV 解码 + 自动重采样)
            playButton.addEventListener('click', async () => {
                // 获取裸 PCM 数据
                const pcm = await generatePcm();
                // PCM 转 WAV(原始 PCM 采样率为 44100Hz)
                const wavBuffer = pcmToWav(pcm, 44100);
                // 创建目标音频上下文(基准采样率 16000Hz,触发自动重采样)
                const audioContext = new AudioContext({ sampleRate: 16000 });
                // 解码 WAV 并自动重采样为 16000Hz
                const audioBuffer = await audioContext.decodeAudioData(wavBuffer);
                // 播放
                const source = audioContext.createBufferSource();
                source.buffer = audioBuffer;
                source.connect(audioContext.destination);
                source.start();
            })
        })()
    </script>
</body>

自动采样率不影响时长,仅影响采样点数量

以 “时长固定为 6.912s 的单声道音频” 为例,不同采样率下 AudioBuffer 的核心参数遵循固定规律,印证 采样率不影响时长,仅影响采样点数量 的核心结论。

基准采样率音频时长(audioBuffer.duration采样点总数(audioBuffer.length单声道采样数据长度(getChannelData(0).length
16000Hz6.912s(固定)110592(16000 × 6.912)110592(单声道场景下,与采样点总数一致)
44100Hz6.912s(固定)304819(44100 × 6.912,四舍五入)304819(同上)
  • 采样率不影响音频时长audioBuffer.duration 由 “总采样点数量 / 采样率” 计算得出,重采样时 “采样点数量” 与 “采样率” 同比变化,因此时长始终不变(如 110592 / 16000 = 304819 / 44100 ≈ 6.912s)。
  • 采样率与采样点数量正相关:遵循公式 采样点总数 = 采样率 × 音频时长,采样率越高,单位时间内的采样点越多,能保留的高频细节(如乐器泛音)越丰富,但音频数据量也越大(存储 / 带宽消耗更高)。

手动重采样

除了 decodeAudioData 的自动重采样,也可通过代码手动实现重采样(如线性插值法),核心是通过 “插值计算” 在原采样点之间生成新采样点,实现采样率转换。

<body>
    <button id="playBtn">播放</button>
    <script>
        (async () => {
            const playButton = document.getElementById('playBtn')
​
            // 步骤1:获取原始 PCM 数据(假设采样率 44100Hz)
            async function generatePcm() {
                const audioUrl = 'https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3'
                const response = await fetch(audioUrl)
                const arrayBuffer = await response.arrayBuffer()
                const tempAudioContext = new AudioContext()
                const audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer)
                return {
                    pcm: audioBuffer.getChannelData(0),
                    sampleRate: audioBuffer.sampleRate // 记录原始采样率(避免硬编码)
                }
            }
​
            // 步骤2:线性插值重采样函数
            function transSamplingRate(data, fromRate, toRate) {
                // 若采样率相同,直接返回原数据(避免无用计算)
                if (fromRate === toRate) return new Float32Array(data);
​
                // 1. 计算重采样后的数据长度(目标采样点数量)
                const fitCount = Math.round(data.length * (toRate / fromRate));
                const newData = new Float32Array(fitCount);
​
                // 2. 计算“原始采样点”与“目标采样点”的映射比例
                const springFactor = (data.length - 1) / (fitCount - 1);
​
                // 3. 处理第一个采样点(直接复制,避免边界误差)
                newData[0] = data[0];
​
                // 4. 线性插值计算中间采样点
                for (let i = 1; i < fitCount - 1; i++) {
                    const tmp = i * springFactor; // 目标采样点在原始数据中的“虚拟位置”
                    const before = Math.floor(tmp); // 虚拟位置左侧的原始采样点索引
                    const after = Math.ceil(tmp);  // 虚拟位置右侧的原始采样点索引
                    const atPoint = tmp - before;  // 虚拟位置在两个原始点之间的比例(0~1)
​
                    // 线性插值公式:new = 左点 + (右点 - 左点) × 比例
                    newData[i] = data[before] + (data[after] - data[before]) * atPoint;
                }
​
                // 5. 处理最后一个采样点(直接复制,避免边界误差)
                newData[fitCount - 1] = data[data.length - 1];
​
                return newData;
            }
​
            // 步骤3:播放手动重采样后的 PCM
            playButton.addEventListener('click', async () => {
                // 获取原始 PCM 及采样率
                const { pcm, sampleRate: originalRate } = await generatePcm();
                // 手动重采样:44100Hz → 16000Hz
                const transPcm = transSamplingRate(pcm, originalRate, 16000);
​
                // 创建音频上下文(采样率与重采样后一致)
                const audioContext = new AudioContext({ sampleRate: 16000 });
                // 创建 AudioBuffer 并写入重采样后的 PCM
                const audioBuffer = audioContext.createBuffer(
                    1,                  // 单声道
                    transPcm.length,    // 重采样后的采样点数量
                    16000               // 与重采样后采样率一致
                );
                audioBuffer.getChannelData(0).set(transPcm);
​
                // 播放
                const source = audioContext.createBufferSource();
                source.buffer = audioBuffer;
                source.connect(audioContext.destination);
                source.start();
            })
        })()
    </script>
</body>
  • 优点:实现简单、计算量小,适合对实时性要求高的场景(如语音通话),能满足大部分基础音频处理需求。
  • 缺点:高频信号还原精度较低(插值仅考虑相邻两个采样点,可能丢失细节),若需更高音质,可使用更复杂的算法(如 sinc 插值、立方插值)。

总结:自动重采样 vs 手动重采样

对比维度自动重采样(decodeAudioData手动重采样(线性插值)
实现复杂度低(API 自动处理)中(需手动实现插值逻辑)
适用场景编码音频格式(MP3、WAV)的采样率转换裸 PCM 数据的采样率转换,或自定义需求
音质较高(浏览器底层优化)中等(线性插值),可通过复杂算法提升
依赖格式需编码音频格式(或 PCM 转 WAV)直接支持裸 PCM 数据

实际开发中,优先使用 decodeAudioData 的自动重采样(简单高效);若需处理裸 PCM 或自定义重采样逻辑,再考虑手动实现(如线性插值法)。

位深

32 位浮点转16 位整数

在PCM 转 WAV 流程中,32 位浮点 PCM 与 16 位整数 PCM 的转换是关键步骤,其本质是量化精度的适配——浏览器 Web Audio API 内部用 32 位浮点处理音频(保证动态范围),而 WAV 格式常用 16 位整数存储(平衡音质与文件大小),二者转换需严格遵循数值范围映射规则。

function float32ToInt16(float32Array) {
     // 1. 初始化16位整数数组(长度与原PCM一致)
    const int16Array = new Int16Array(float32Array.length);
    for (let i = 0; i < float32Array.length; i++) {
        // 2. 范围钳位:确保浮点数值不超出[-1, 1](避免转换后超出16位整数范围)
        const value = Math.max(-1, Math.min(1, float32Array[i]));
        
        // 3. 数值映射:将[-1, 1]映射到[-32768, 32767]
        // 负数:用 -32768(0x8000)作为基准,保证负数值范围完整
        // 正数:用 32767(0x7FFF)作为基准,避免正数最大值溢出
        int16Array[i] = value < 0 ? value * 0x8000 : value * 0x7FFF;
    }
    return int16Array;
}

16 位整数 转3 2 位浮点

当需要将 WAV 中的 16 位整数 PCM 重新导入 Web Audio API 处理时,需转回 32 位浮点,核心是 反向映射

function int16ToFloat32(int16Array) {
    const float32Array = new Float32Array(int16Array.length);
    for (let i = 0; i < int16Array.length; i++) {
        // 用 0x8000(32768)作为分母,统一映射到[-1, 1]
        // 原因:16位整数的“数值跨度”是 65536(32768 - (-32768)),除以 32768 后正好覆盖[-1, 1]
        float32Array[i] = int16Array[i] / 0x8000;
    }
    return float32Array;
}

声道

在Web Audio APi 开发过程中一般都是单声道,声道转换并不常见。

// 单声道(Float32Array)转立体声(2声道 AudioBuffer)
function monoToStereo(monoPcm, sampleRate) {
    const audioContext = new AudioContext();
    // 创建2声道缓冲区(长度、采样率与单声道一致)
    const stereoBuffer = audioContext.createBuffer(2, monoPcm.length, sampleRate);
    // 左声道、右声道均写入单声道数据
    stereoBuffer.getChannelData(0).set(monoPcm); // 左声道
    stereoBuffer.getChannelData(1).set(monoPcm); // 右声道
    return stereoBuffer;
}