音频可视化中的信号处理方案

51次阅读

共计 3315 个字符,预计需要花费 9 分钟才能阅读完成。

声明: 原创文章,未经允许不得转载。

音频可视化是一个“听”起来非常“美”好的话题,其复杂程度很大程度上依赖视觉方案(一些例子),不同的视觉方案决定了你的技术方案选型,比如 three.js,pixi.js 等引擎。

不管你选用什么渲染方案,处理音频信号部分是相通的,本文会围绕音频信号的处理进行阐述,期望能够给大家普及一下音频相关的基础知识(由于能力所限难免疏错,欢迎指出)。

前五部分主要是一些理论性的基础概念,如果你不敢兴趣可以直接跳过。

  • github 地址:sound-processor
  • 三个示例:
  1. demo1;

  1. demo2;

  1. demo3;

一、什么是声音?

声音来源于 振动 ,通过声波传播,人耳中无数 毛细胞 会将振动信号转换成电信号并通过听觉神经传递给大脑,形成人主观意识上的“声音”。声波进入人耳后,因为耳蜗的特殊构造,不同部位对声音的敏感程度是不一样的:

高频声音会被耳蜗近根部位置所感知,低频声音在近端部位置被感知,因此人对不同频率声的感受是 非线性 的,这是后续 声学计权 的基础。

二、声学计权

声学计权常见的有 频率计权 时间计权,其作用在于模拟人耳对不同频率声音的非线性感受:

  • 对低频部分声音不敏感;
  • 最灵敏的区域在 1~5K Hz 之间;
  • 上限在 15~20K Hz 之间;

人耳听觉范围如图所示:

2.1 频率计权

频率计权是作用在音频信号的频谱上的,常用的有:A、B、C、D 四种:

其中 A 计权 是最接近人主观感受的,它会削弱低频和高频部分中人耳不敏感的部分,所以音频可视化里要选择 A 计权方式,详细说明可阅读 wiki。

2.2 时间计权

现实里声音一般是连续的,人对声音的主管感受也是声音累加的结果(想象一下,第一波声波引起耳膜振动,振动还没停止,第二波声音就来了,因此实际耳膜的振动是声波在时间上累加的结果),时间计权就是就连续时间内声音的平均值。对于变化较快的信号,我们可以使用 125ms 的区间来求平均,对于变化缓慢的可以采用 1000ms 的区间。

三、声音测量

声音测量最常用的物理量是 声压,描述声压的大小通常用声压级(Sound Pressure Level,SPL)。人耳可听的声压范围为 2×10-5Pa~20Pa,对应的声压级范围为 0~120dB。

常见声音的声压

声压常常用 分贝 来度量,这里要说明一点,分贝本身只是一种度量方式,代表测量值和参考值的对数比率:

声压级的定义:

其中 P 是测量幅值,P ref代表人耳能听见 1000 Hz 的最小声压:20 uP

四、倍频程

首先,连续的信号包含了大量的数据,我们没有必要全部处理,因此我们一般会进行采样,将连续的频率划分成一个一个区间来分析,频程 就代表一段频率区间,倍频程 代表频率划分的一种方案。具体来说倍频程中一段区间的上限频率与下限频率之比是常数:

具体可以看这篇文章《什么是倍频程》

当 N 等于 1,就是 1 倍频程,简称倍频程,如果 N 等于 2,则为 1/2 倍频程。频程划分好之后,将分布于频程内的频谱求均方值得到的就是 倍频程功率谱

五、webaudio 对音频的处理

在 web 端做音频可视化离不开 webaudio 的 API,其中最重要的就是getByteFrequencyData(文档),这个方法能获取时域信号转换之后的频域信号,详细过程如下:

  1. 获取原始的时域信号;
  2. 对其应用Blackman window (布莱克曼窗函数),其作用是补偿 DFT 造成的信号畸变和能量泄漏;
  3. 快速傅里叶变换,将时域变成频域;
  4. Smooth over time,这一步是在时间维度对信号进行加权平均(webaudio 只采用了 2 帧);
  5. 按照上文的声压公式转换为 dB;
  6. 归一化,webaudio 采用的归一化方式如下:

六、音频可视化中的信号处理方案

结合上述内容,我们觉得比较合理的处理方式如下:

6.1 滤波

有人会问,getByteFrequencyData内部不是已经应用了窗函数滤波吗,为什么还要再滤波?

因为 webaudio 内部的窗函数主要是用于补偿信号畸变和能量泄漏,其参数都是固定的。而在音频可视化的场景下,往往视觉感受要优先于数据精确性,因此我们加了一个 高斯滤波 来滤除突刺和平滑信号,“平滑”的程度是可以通过参数任意控制的。

6.2 计权

视觉呈现应该要和人的主观听觉关联,所以计权是必要的,JavaScript 的计权实现 audiojs/a-weighting。另外我们也提供了额外的时间计权,内部会统计 5 个历史数据进行平均。

6.3 频程划分

我们会根据传入的上下限频率区间和置顶的输出频带数自动进行频程划分,核心代码:

// 根据起止频谱、频带数量确定倍频数: N
// fu = 2^(1/N)*fl  => n = 1/N = log2(fu/fl) / bandsQty
let n = Math.log2(endFrequency / startFrequency) / outBandsQty;
n = Math.pow(2, n);  // n = 2^(1/N)
    
const nextBand = {lowerFrequency: Math.max(startFrequency, 0),
    upperFrequency: 0
};
    
for (let i = 0; i < outBandsQty; i++) {
    // 频带的上频点是下频点的 2^n 倍
    const upperFrequency = nextBand.lowerFrequency * n;
    nextBand.upperFrequency = Math.min(upperFrequency, endFrequency);
    
    bands.push({
        lowerFrequency: nextBand.lowerFrequency,
            upperFrequency: nextBand.upperFrequency
    });
    nextBand.lowerFrequency = upperFrequency;
}

七、sound-processor

sound-processor 是一个极小(gzip < 3KB)的处理音频信号的库,作为音频可视化的底层部分,使用相对科学的方法处理原始音频信号并输出符合人类主观听觉的信号,内部的处理流程如下:

7.1 安装

npm install sound-processor

7.2 使用

import {SoundProcessor} from "sound-processor";

const processor = new SoundProcessor(options);
// in means original signal
// analyser is the AnalyserNode
const in = new Uint8Array(analyser.frequencyBinCount)
analyser.getByteFrequencyData(in);
const out = processor.process(in);

7.3 options

  • filterParams: 滤波参数,对象,默认undefined,表示不滤波:

    • sigma:高斯分布的 sigma 参数,默认为 1,表示标准正态分布,sigma 越大平滑效果越明显,一般取 0.1~250 之间;
    • radius:滤波半径,默认为 2;
  • sampleRate:采样率,可以从 webaudio 的 context 中取(audioContext.sampleRate),一般是 48000;
  • fftSize:傅里叶变换参数,默认为 1024;
  • startFrequency:起始频率,默认为 0;
  • endFrequency:截止频率,默认 10000,配合 startFrequency 可以选取任意频段的信号;
  • outBandsQty:输出频带数,对应可视化目标的数量,默认为 fftSize 的一半;
  • tWeight:是否开启时间计权,默认为false
  • aWeight:是否开启 A 计权,默认为true

7.4 频率截取

一般音乐的频率范围在 50~10000 Hz 之间,实际中可以取的小一些,比如100~7000 Hz,对于不同风格以及不同乐器的声音很难取到一个统一的完美区间,另外不同的视觉风格可能也会影响频率区间。


参考材料

  • 为什么要进行声学计权
  • A-weighting
  • 什么是声压级?
  • 什么是倍频程?
  • AnalyserNode.getByteFrequencyData
  • 一步一步教你实现 iOS 音频频谱动画
  • 一维高斯分布

正文完
 0