callcc.dev

本篇文章是开发前期调研笔记的整理,不包含实现的具体细节。
APU(Audio Process Unit) 是 GameBoy 的音频处理模块,负责音频的生成、混合、输出等。在 APU 中,存在3种 channel(共 4 个 channel),分别是 pulse channel(CH1 & CH2)、wave channel(CH3)、noise channel(CH4)。pulse channel 用来生成数字音频,wave channel 用来播放数字音频,noise channel 用来生成白噪音。这四个 channel 的数据经过 mixer 混合,然后经过放大器,最后输出。
Architecture
Architecture

寄存器总览

Registers
Registers
APU 的行为受到上图 Sound Controller 一栏共21个寄存器控制。首先约定一些寄存器变量的表示方法:
  • NRxy 是寄存器的通用表示方法,例如可以用 NRx1 来表示 NR11、NR21、NR31、NR41。
  • NRxy.z,表示寄存器的某bit,例如 NR11.0 表示 NR11 的 0bit。
  • NRxy.z0..=NRxy.z1,表示寄存器的 bit 范围,比如 NR11.0..= NR11.3 表示 NR11 寄存器的 0bit 到 3bit。

Pulse channel(CH1 & CH2)

Frequency Sweep

Frequency Sweep
Frequency Sweep
Frequency Sweep 控制频率的变化,在 APU 中,频率用 Period Value(简称 PV) 来进行表示,采样频率和 PV 的换算公式简化版为
PV = ((NR13 & 0x7) << 8) | NR14 SampleRate = 4 * (2048 - PV)
PV 的变化受到三个参数的控制,分别是 pace、direction、step,利用这三个参数,我们可以通过公式
Step = NR10 & 0x7 Pace = (NR10 >> 4) & 0x7 Direction = NR10.3 PV1 = IF Direction == 1 THEN PV0 - (PV0 / (2 ^ Step)) ELSE PV0 + (PV0 / (2 ^ Step)) END
来计算得到新的 PV。其中 PV0 为当前的 PV,PV1 为新的 PV,+/- 取决于 direction,pace 为变换速度(也就是多久触发一次重新计算,上图的纵向虚线间距不一样也就是 pace 不一样的表现)。
值得注意的一点是,如果 PV 的值溢出 0x000..=0x7FF 这个范围,则 channel 停止工作。 此外,CH2 虽然和 CH1 一样都是 pulse channel,但是 CH2 没有 Frequency Sweep 这个功能。

Envelope

Envelope
Envelope
Envelope 控制振幅的变化。和 Frequency Sweep 类似,也有三个参数控制振幅的变化,分别是 initial volume、direction、pace。振幅的最大值是 0xF,变化公式为
InitialVolume = NR12.4..=NR12.7 Direction = NR12.3 Pace = NR12.0..=NR12.2 V1 = IF Direction == 1 THEN V0 + 1 ELSE V0 - 1 END

Duty Cycle

Duty Cycle(NR11.6..=NR11.7) 是用来控制输出波形的,APU 中可以输出四种波形
Waveform
Waveform

有了 Frequency Sweep、Envelope、Duty Cycle,我们也就可以控制频率、振幅、波形了。往下我们来看看控制 channel 是否工作的组件。

Length timer

这个组件的功能是控制 channel 工作多长时间之后停止。它本质上是一个计数器,初始值是 NR11.0..=NR11.5,当它的值达到 64 的时候,channel 就停止工作。

DAC

Pulse channel 的 DAC 是在 envelope 里的,一旦 DAC 停止,则 channel 停止工作。DAC 停止条件是 NR12.3..=NR12.7 == 0

综上所述我们可以知道 pulse channel 停止工作存在三种情况
  • DAC off
  • Length timer expired
  • Period Value 值不在 0x000..=0x7FF 内
当 channel 停止工作后,程序需要 trigger channel 来让它重新工作,这个操作是通过向 NR14.7 写入 1 来完成的。但 trigger 不一定会让 channel 进入工作状态,依旧受到以上三种状态的控制。

Wave channel(CH3)

Wave RAM

Wave channel 是用来播放程序提供的一小段音频数据的,这个音频数据则被放置在 Wave RAM 中。这块内存在 0xFF30..=0xFF3F 中,可以知道这里有 16 bytes 的数据,而因为 APU 的音频 Bit Depth 是 4bits,也就是每个采样数据只需要用 4 bits 来表示,所以 Wave RAM 一种存放了 32 个采样数据。

Frequency

参考 pulse channel,采样率为 SampleRate = 2 * (2048 - PV)

Volume

Wave channel 没有 pulse channel 的 envelope 功能,它只有四档可调的音量,由 NR32.5..=NR32.6 控制
  • 00:Mute
  • 01:100% 样本音量
  • 10:50% 样本音量
  • 11:25% 样本音量

Length timer

参考 pulse channel。它的初始值存放在 NR31,因此它的计数器达到 256 时停止工作,而不是 pulse channel 的 64。

DAC

不同于 pulse channel 的 DAC 控制是在 envelope 里的,wave channel 有单独的 DAC 控制开关 NR30.7。

综上所述我们可以知道 wave channel 停止工作存在两种情况
  • DAC off
  • Length timer expired
要让 wave channel 重新工作,也是通过 trigger 来实现。

Noise channel(CH4)

Frequency

采样率简化后的计算公式为
ClockShift = NR43.4..=NR43.7 ClockDivider = NR43.0..=NR43.2 IF ClockDivider == 0 THEN (1 << (ClockShift + 3)) ELSE ClockDivider * (1 << (ClockShift + 4)) END

Envelope

参考 pulse channel。

Length counter

参考 pulse channel。

DAC

参考 pulse channel。

Master control

NR52

NR52 控制 APU、以及 4 个 channel 是否工作的。

NR51

NR51 控制每个 channel 输出的声道,因为支持立体声,因此 8 bits 刚好用来控制 4 个 channel。

NR50

NR50 主要关注的是左右声道的音量。
MasterLeftVolume = NR50.4..=NR50.6 MasterRightVolume = NR50.0..=NR50.2

Mixer

4 个 channel 输出的信号,会经过 mixer 进行混合之后再输出,根据 傅立叶变换,其实就是把 4 个 channel 的值进行相加。
LeftVolumeCoefficient = (MasterLeftVolume + 1) / 8.0 * (1 / 15.0) * 0.25 RightVolumeCoefficient = (MasterRightVolume + 1) / 8.0 * (1 / 15.0) * 0.25 OutputLeft = (CH1.Left + CH2.Left + CH3.Left + CH4.Left) * LeftVolumeCoefficient OutputRight = (CH1.Right + CH2.Right + CH3.Right + CH4.Right) * RightVolumeCoefficient

其他特殊行为

音频生成

实践中,可以用 blip-buf来方便开发,基本原理是通过插值来输出预期采样率的数据。为了用于 Rust 代码中编译到 WASM,我用 Rust 重写了一版 blip_buf-rs

参考资料