最近想着做一个音乐游戏的DEMO试试,但是毫无头绪不知道怎么开始。看了下邦邦的视频决定先把节拍器做出来,再用节拍器提供的事件去支撑起其它的部分。
因为完全没做过音乐游戏,而且也对游戏引擎以及声音方面没有什么了解,所以开始在网上海搜节拍器的代码并拿来测试。
最后得到俩解决方案:1.在Unity里写一个;2.使用专业的音频插件(如CRIWARE或者WWISE),使用里面的节拍器。
因为只是尝试做做DEMO,决定先不引入音频插件了。
在冲浪查询的时候发现Unity的官方文档中已经写好了一个!真是太棒了,马上嫖过来用。为了方便之后使用,我也决定把这份代码尽可能看懂。
下面是Unity的源码
MonoBehaviour.OnAudioFilterRead(float[], int)
AudioSettings.dspTime
提取基本代码
首先我大概跑了一下,看了一下,提取出了我认为是节拍器最基本的部分。
signatureHi / signatureLo => 这个就是我们所说的几几拍。
signatureHi是每小节的拍子数,signatureLo是指x分音符为1拍。accent是指当前小结内的拍子数。
1 2 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++; } } }
|
1 2 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赋值,其实这些就是为了播放节拍器音效而存在的。
1 2 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++; }
|
摸了
感觉自己的知识面又窄又浅,还是需要多看书才行。