关于Web Audio中Analyzer输出的频率数据分布问题

1266 words, 6 minutes to read / August 19, 2022

随着 iOS 16 beta 5 的更新,细心的小伙伴可能注意到了,系统在播放音乐时,音乐控制台的右上角多了一个小小的频谱显示器。对我这种喜欢音乐的人来说,看着它随着音乐一起动起来,心情似乎也变得好起来。那么问题来了,浏览器上是否可以实现这样的功能?答案是可以,但是似乎有一点小问题。


我们先抛开顾虑,直接查 MDN 堆代码看看效果。

<div id="app">
  <audio id="music" src="test-2.mp3"></audio>
  <button @click="play">play</button>
  <div style="display: flex; height: 60px; align-items: flex-end; margin-top: 20px">
    <div class="item" v-for="item in fData" :style="{height: item / 255 * 100 + '%'}"></div>
  </div>
</div>
  
<script src="vue.js"></script>
.item {
  width: 6px;
  min-height: 6px;
  background: #333333;
  align-items: flex-end;
  border-radius: 6px;
  margin-right: 3px;
}
new Vue({
  el: '#app',
  data: function () {
    return {
      fData: []
    }
  },
  methods: {
    play() {
      let _ = this
      let audio = document.getElementById('music')
      audio.play()
      
      let audioContext = new AudioContext()
      let audioSrc = audioContext.createMediaElementSource(audio)
      let analyzer = audioContext.createAnalyser()
      analyzer.fftSize = 32
      
      audioSrc.connect(analyzer)
      analyzer.connect(audioContext.destination)
      
      let bufferLength = analyzer.frequencyBinCount
      let frequencyData = new Uint8Array(bufferLength)
      
      setInterval(() => {
        analyzer.getByteFrequencyData(frequencyData)
        _.fData = _.uint8ArrayToArray(frequencyData)
      }, 1000 / 24)
    },
    uint8ArrayToArray(uint8Array) {
      let array = []
      
      for (let i = 0; i < uint8Array.byteLength; i++) {
        array[i] = uint8Array[i]
      }
      
      return array
    }
  }
})

结果:

这样一通操作下来,一个简单的频谱显示器就完成了(老有成就感了),大概原理也很简单:

  1. 创建一个音频 上下文Audio Context
  2. 使用上下文映射 <audio> 并赋值为 audioSrc
  3. 创建 分析仪 节点(Analyzer Node
  4. audioSrc 连接 分析仪分析仪 再连接到 ctx.destination 即用户端输出
  5. 图形化 分析仪 的数据

现在,真正的问题来了:不管我尝试什么音乐,我的频谱图永远都是这样的 ⬇️(成就感啪一下没了)

Untitled

可能会有一些小差异,但是呈现的效果基本差不多:低频看起来很足,高频几乎没有

这时候,作为一名 “准专业编曲师”,我有一种直觉,Web Audio API 给出来的频谱数据,可能是线性分布的。为了验证我的猜想,我制作了一段从 30Hz 到 20000Hz 慢慢上升的正弦波音频,调高分析仪的 fftSize ,放进代码里看看输出:

再来看看同样的音频在 Ableton Live 中 Spectrum 线性模式下的输出:

这表现可以说,基本一致,同时也验证了我的猜想 —— Web Audio API 输出的频谱数据是线性分布的。线性分布是什么分布?很好理解:一个坐标系中,X 轴上 10 到 20 需要走的距离,与 2000 到 2010 需要走的距离一样。然而对于人来讲,线性分布这个解决方案,并不是最优的。我们可以先看一些专业 EQ 插件的截图,看看他们是怎么做频谱分布的。

FabFilter Pro Q3:

Untitled

Eiosis Air EQ Premium:

Untitled

SlateDigital Inf EQ:

Untitled

Ableton Live EQ Eight

Untitled

可以看到,这些分布基本都是在类似指数分布的频率上进行分段式的对数分布,而且似乎是遵循着等响曲线做出的优化。

我们经常听到 “指数级增长” 这个词,举个简单的例子:10、100、1000、10000,这个数列就是指数增长。上述频谱图中,频率分布基本是按照指数来的。

关于对数我们不需要了解太多,对数分布也可以根据上面我对线性分布的理解做出延伸:一个坐标系中,X 轴上 10 到 20 需要走的距离,与 2000 到 2010,同样是 10 的差距,但是需要走的距离却一样。如果我们以 Ableton Live 更加青睐的分布规则,把频谱分析仪的可视化面板看作是一个坐标系,则 X 轴上,10 - 10000 被分成三段,分别为 10 - 100、100 - 1000、1000 - 10000,每一段距离都一样且被分成 9 份进行对数分布。

说到等响曲线,它并不像对数那样客观,而是一个主观概念。此概念阐述的内容如果用大白话来讲就是:同样音量但不同频率的声音,给人听起来的强弱是不一样的。即:60Db 的 4000Hz 正弦波与 60Db 的 10000Hz 正弦波,前者听起来会更响一些。此概念也同样可以用来解释,在删除歌曲 10kHz 以上部分的时候(可以理解为对半切),你不会觉得整首歌的质量下降了 50%,主观上听起来可能只有 10%。

个人认为,频谱这样的分布模式,其实是为了弥补人耳的缺点,在视觉上模拟出与听觉类似的 “缺陷”,真正做到音视一致从而提升用户体验。


关于如何在浏览器里实现,其实很简单,我们只需要模拟指数级分布就好,不用去考虑分段中的对数分布。演示地址: https://jw1.dev/frequency-test/test-2.html

现在再来看一下对比:

Untitled

Untitled

哪个体验更好,一眼看过去便知道了。

至此,一个不算完美但是看起来还不错的频谱显示器就算是完成了,我也给 webAudio 开源项目提了issue,希望未来能够添加支持,为开发者带来更佳的体验!

✌️