最近想着做一个音乐游戏的DEMO试试,但是毫无头绪不知道怎么开始。看了下邦邦的视频决定先把节拍器做出来,再用节拍器提供的事件去支撑起其它的部分。
因为完全没做过音乐游戏,而且也对游戏引擎以及声音方面没有什么了解,所以开始在网上海搜节拍器的代码并拿来测试。
最后得到俩解决方案:1.在Unity里写一个;2.使用专业的音频插件(如CRIWARE或者WWISE),使用里面的节拍器。
因为只是尝试做做DEMO,决定先不引入音频插件了。
在冲浪查询的时候发现Unity的官方文档中已经写好了一个!真是太棒了,马上嫖过来用。为了方便之后使用,我也决定把这份代码尽可能看懂。
下面是Unity的源码
MonoBehaviour.OnAudioFilterRead(float[], int)
AudioSettings.dspTime
提取基本代码
首先我大概跑了一下,看了一下,提取出了我认为是节拍器最基本的部分。
signatureHi / signatureLo => 这个就是我们所说的几几拍。
signatureHi是每小节的拍子数,signatureLo是指x分音符为1拍。accent是指当前小结内的拍子数。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 
 | [RequireComponent(typeof(AudioSource))]
 public class MetronomeExample : MonoBehaviour
 {
 public double bpm = 128.0f;
 public int signatureHi = 4;
 public int signatureLo = 4;
 private double nextTick = 0.0F;
 private double sampleRate = 0.0F;
 private int accent= 0;
 private bool running = false;
 
 public event System.Action<int, int> OnBeatTick;
 
 private void Start()
 {
 accent = signatureHi;
 sampleRate = AudioSettings.outputSampleRate;
 nextTick = AudioSettings.dspTime * sampleRate;
 running = true;
 }
 
 private void OnAudioFilterRead(float[] data, int channels)
 {
 if (!running)
 return;
 
 double samplesPerTick = sampleRate * (60.0f / bpm) * (4.0f / signatureLo);
 double sample = AudioSettings.dspTime * sampleRate;
 int dataLen = data.Length / channels;
 int n = 0;
 while (n < dataLen)
 {
 while (sample + n >= nextTick)
 {
 nextTick += samplesPerTick;
 if (++accent > signatureHi)
 {
 accent = 1;
 }
 OnBeatTick?.Invoke(accent, signatureHi);
 Debug.Log($"Tick: {accent} / {signatureHi}");
 }
 n++;
 }
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 | nextTick = AudioSettings.dspTime * sampleRate; ```
 从 Start() 开始,先做节拍器的初始化。 因为需要准确的音频数据,所以我们会使用音频相关的函数来操作,而不是使用 Update()。
 通过当前的时间 * 当前的采样率,大概就能得到当前的音频信息总量。
 
 ``` CSharp
 OnAudioFilterRead(float[] data, int channels)
 ```
 接着我们可以把 OnAudioFilterRead() 看作 Update() 来使用。
 channels 大概是声道数,比如我平时用的辣鸡机器是双声道(左右的),那么 channels 的值就是2。
 data[] 中存储的是当前这一次遍历的音频数据,也会包含不同声道的音频信息,比如\[0L, 1R, 2L, 3R....\]之类的。
 
 ``` CSharp
 double samplesPerTick = sampleRate * (60.0f / bpm) * (4.0f / signatureLo);
 double sample = AudioSettings.dspTime * sampleRate;
 ```
 这里是用与计算拍子的间隔时间。(60.0f / bpm) 所计算的是对于 4/4 拍而言的拍子间隔时间。
 而 (4.0f / signatureLo) 可以算出一个参数,并乘给 4/4 拍而言的拍子间隔时间。
 sample 可以粗浅的认为是当前的时间。
 
 ``` CSharp
 while (n < dataLen)
 {
 while (sample + n >= nextTick)
 {
 
 
 nextTick += samplesPerTick;
 if (++accent > signatureHi)
 {
 accent = 1;
 }
 OnBeatTick?.Invoke(accent, signatureHi);
 Debug.Log($"Tick: {accent} / {signatureHi}");
 }
 n++;
 }
 
 | 
这边遍历一下接下来会播放的音频数据。根据数据的长度来判断是否会需要发出节拍器的 tick。
节拍器音效
在官方源码中还用到了一些其他的变量,并给data赋值,其实这些就是为了播放节拍器音效而存在的。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | while (n<dataLen)
 {
 float x = gain * amp * Mathf.Sin(phase);
 int i = 0;
 while (i < channels)
 {
 data[n * channels + i] += x;
 i++;
 }
 while (sample + n >= nextTick)
 {
 nextTick += samplesPerTick;
 amp = 1.0F;
 if (++accent > signatureHi)
 {
 accent = 1;
 amp *= 2.0F;
 }
 OnBeatTick?.Invoke(accent, signatureHi);
 Debug.Log($"Tick: {accent} / {signatureHi}");
 }
 phase += amp* 0.3F;
 amp *= 0.993F;
 n++;
 }
 
 | 
摸了
感觉自己的知识面又窄又浅,还是需要多看书才行。