FishPlayer

一个喜欢摸鱼的废物

0%

Unity OnAudioFilterRead 学习笔记

最近想着做一个音乐游戏的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)
{
// 用起来就相当于
// nextTime = Time.time * timeInterval;
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++;
}

摸了

感觉自己的知识面又窄又浅,还是需要多看书才行。