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

2,058 阅读7分钟

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

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

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

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

  • github地址:sound-processor
  • 三个示例(音频放在git上加载较慢,需要等久一点):
  1. demo1

bar

  1. demo2

circle

  1. demo3

dot

一、什么是声音?

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

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

二、声学计权

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

  • 对低频部分声音不敏感;
  • 最灵敏的区域在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,对于不同风格以及不同乐器的声音很难取到一个统一的完美区间,另外不同的视觉风格可能也会影响频率区间。


参考材料