Chapter1-DJ最爱的音频动感图像(III)妈妈叫你不要玩音乐,现在知道当DJ很难了吧

加油!这章节快完成了

https://ithelp.ithome.com.tw/upload/images/20210911/201351975SuixoaMT9.jpg
根据上面这张图,我们写好了以下的程序码,就成功得到经由傅立叶转换的音频讯号了,取得了名为dataArray音频阵列,耶比~~可以开始图像化了!

// 创建音讯物件
let AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();
// 创建节点
let audio = document.querySelector("#Music");
let source = audioCtx.createMediaElementSource(audio);
let gainNode = audioCtx.createGain();
let analyserNode = audioCtx.createAnalyser();
// 连接节点
source.connect(gainNode);
gainNode.connect(analyserNode);
analyserNode.connect(audioCtx.destination);
// 对每个节点进行设定
gainNode.gain.value = 1;
analyserNode.fftSize = 2048;
// 利用analyserNode取得当下的音频讯号
let bufferLength = analyserNode.frequencyBinCount;
let dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);

咦? 你说这样有点跳太快了吗

名词解释

开玩笑的啦,待会就一步一步拆解给大家看呗,今天有不少知识点,先让大家熟悉今天的六个名词

  1. 元件HTMLMediaElement
    其实在HTML写一个audio标签的时候,就会用到它。
    (video标签也是唷!因为这个节点不只被HTMLAudioElement继承,还被HTMLVedioElement继承。摁...你说继承甚麽意思呢,没关系我们後面的章节会提到)
  2. 节点MediaElementAudioSourceNode
    该节点可以将HTMLMediaElement,当作音讯来源Source。
  3. 节点GainNode
    接上原始音讯source後可以设定gainNode.gain.value的值来使调整该节点的音量
    (0~1等同於习惯上的0~100音量,大於1则会放大音量)
  4. 节点analyserNode
    该节点接上原始音讯source後,可以取得每个拿来做音频分析。
    (频率讯号经由傅立叶转换过)
  5. 方法AudioNode.connect()
    connect方法扮演一个至关重要的角色,可以让音讯经由多个节点(例如A-B-C-D),并分别做不同的音讯处理
  6. 音讯输出BaseAudioContext.destination
    音讯处理完後,最後要记得连接(connect)到这个音讯的最终目的地(预设就是你的喇叭),如果没有连接,就没有声音!

可是,这样看不太懂

名词有点多对吧!没关系我们慢慢来,先从最简单的开始:「如果我今天要在底层实作一个音乐播放器,可以怎麽做?」

  • 步骤 1 在HTML中设置audio标签,并用js取得这一个HTMLMediaElement: audio
  • 步骤 2 透过createMediaElementSource方法来创建一个音讯来源(节点)
  • 步骤 3 将音讯来源连接到音讯输出口
// 步骤 1
let audio = document.querySelector("#Music");
// 步骤 2
let AudioContext = window.AudioContext || window.webkitAudioContext; // 跨浏览器写法
let audioCtx = new AudioContext();
let source = audioCtx.createMediaElementSource(audio);
// 步骤 3
source.connect(audioCtx.destination);

如果少了步骤3,你会发现这个ID为#Music的audio元件不能播放了!因为在步骤2的时候,我们已经把它音讯放到了audio当中做处理,必须主动执行步骤3接到音讯出口,才能让它有音源可以播放。也就是说,你现在是DJ了!可以决定任何一个混音源(节点),最後要不要接上去。

再来看看一开始的程序码

以下是用来制作昨天的Demo时,用到的5个步骤:

  • 步骤 1 取得HTMLMediaElement
  • 步骤 2 分别用createMediaElementSource、createGain、createAnalyser方法创建三个节点
  • 步骤 3 把节点依序连接起来
  • 步骤 4 对节点的参数依序做设定
  • 步骤 5 利用今天的主角analyserNode取得音频讯号并存到dataArray
// 步骤 1
let AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();
// 步骤 2
let audio = document.querySelector("#Music");
let source = audioCtx.createMediaElementSource(audio);
let gainNode = audioCtx.createGain();
let analyserNode = audioCtx.createAnalyser();
// 步骤 3
source.connect(gainNode);
gainNode.connect(analyserNode);
analyserNode.connect(audioCtx.destination);
// 步骤 4
gainNode.gain.value = 1;
analyserNode.fftSize = 2048;
// 步骤 5
let bufferLength = analyserNode.frequencyBinCount;
let dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);

这样是不是清楚多了呢?

AnalyserNode(专门为视觉化设计的节点)

这个步骤5是相当关键的一环,允许你取得音频并进行分析,先来解释出现的新东西:

  • analyserNode.fftSize
    影响傅立叶转换(FFT)的精准程度,有电子学基础的朋友应该就不陌生,这个值必须刚好为2的次方项,范围则是介於2^5-2^15次方之间,预设为2048。
  • analyserNode.frequencyBinCount
    唯读属性,会是fftSize的一半,也是待会用getByteFrequencyData方法拿到阵列的长度

关於傅立叶(微积分),简单来说,就是你做越多次,效果越好,花的时间越多;做越少次,效果越差(失真),花的时间越少。还是不太理解的朋友没关系,你就把fftSize设个256也很够用了!那麽你只需要准备长度为analyserNode.frequencyBinCount(刚好会是128)的阵列去装讯号就好。若是上面的例子来说,长度会刚好是1024。

讲到这聪明的你也应该明白了吧,如果不担心取得的音讯失真的话,fftSize简直是越小越好,阵列越短,对於效能的优化当然是越好!而dataArray代表了每个频段的信号大小,fftSize越小,显然频宽越大,也因此才会失真。

所以怎麽看懂傅立叶转换後的讯号?

这边还有几个知识点要补充:

  • dataArray每个索引的值都介於0~255之间
  • dataArray[index]中频率跟index成正比,也就是频率=index*(阵列长度/总频率)=index*频宽

而总频率又被记录在一开始的音源audioContext中,预设为48000。这边就先爆个雷,明天会用到以下程序码:

let bands = audioCtx.sampleRate / (analyserNode.fftSize / 2); // 每个区段的频宽
let highestBands = 16000; // 16kHz高音频以下的音乐
let index = highestBands / bands;

假如我想绘制的频率信号,最多只到高音频16kHz,那就用这个去除以阵列每个间隔的频率宽度bands,就可以得到16kHz对应於dataArray的索引位置index了!

那麽明天我们就可以把拿到手的dataArray图像化罗!

後记

整理文章真的是一门大学问...光是懂还不行,要讲得清楚,安排前後上下文,本来以为分享技术这件事很简单,没想到这麽花时间!再次赞叹网路上,有这麽多无私分享的大神。

该系列文章内文和程序码皆出自本人撰写,名词解释参考MDN Web Docs文件。


<<:  Day 11-假物件 (Fake) - 虚设常式 (Stub)-3 (核心技术-3)

>>:  [30天 Vue学好学满 DAY11] v-on

【Day 27】NumPy (4):np.sqrt(), np.square()

前言 今天要来介绍一下用於数学运算的函式,sqrt 开根号,以及 square 平方 NumPy n...

DAY 28 - 殭屍女孩 (1)

大家好~ 我是五岁~ 今天要来画可爱的殭屍女孩~ 风格类似是中国殭屍,她外观上有两个巨大的手手可以跟...

【从零开始的 C 语言笔记】第十九篇-While Loop(1)

不怎麽重要的前言 上一篇介绍了for loop的概念,让大家面对在有重复性、明确次数的处理时,可以使...

Two Sum 演算法初阶题,Ruby 30 天刷题修行篇第九话

大家好,我是阿飞,今天的题目是演算法初阶题目 Two Sum,让我们一起来看看: 题目来源 Code...

Day2 让我们开始吧

工欲善其事,必先利其器,好的开始是成功的一半~ 今天来把要使用的环境建立好 这次我选用的是线上编辑器...