通过前面的教程基本已经能玩游戏了,但是有音乐才算得上完整,下面介绍 NES 的 APU
1. 简介
APU 和 PPU 一样,也是比较复杂的芯片,和 PPU 比起来简单一些,但是比 CPU 复杂,毕竟不具备通用性
APU 有 5 通道,2 个方波,1 个三角波,1 个噪声,1 个 DMC
注:DMC 全称为 delta modulation channel,它用来产生方波,三角波,噪声产生不了的声音,声音信息提前存入 rom 中,不考虑精度的情况下,可以生成任意波形,比如鼓声这种复杂的声音,就得用 DMC
NES 有 4 bit DAC,故电压范围为 0 - 15,但是 DMC 除外,它有 7 bit,范围为 0 - 127
注:DAC 叫 “数模转换器”, 作用是把数字量转化为模拟量(电压),音频信号就是典型的模拟信号,其电压随时间变化,所以通过 DAC,可以通过数字的方式生成音频信号。
APU 寄存器分布如下:
通道 | 地址 | 操作 |
---|---|---|
方波1 (pulse1) | 0x4000 - 0x4003 | w |
方波2 (pulse2) | 0x4004 - 0x4007 | w |
三角波 (triangle) | 0x4008 - 0x400B | w |
噪声 (noise) | 0x400C - 0x400F | w |
DMC | 0x4010 - 0x4013 | w |
状态寄存器 | 0x4015 | rw |
帧计数器 | 0x4017 | w |
2. 时钟
声音有长有短,频率也在时刻变化,这些都需要时钟来提供,APU 有两个时钟:
- 基本时钟(APU 周期):CPU clock / 2
用于控制波形频率 - 帧计数器:240Hz
用于控制波形持续时间
3. 状态寄存器
由于 APU 有多个通道,所以提供了专门的 状态寄存器 用于控制通道使能和读取通道相关信息
-
写 0x4015
BIT 作用 0 使能方波 1 1 使能方波 2 2 使能三角波 3 使能噪声 4 使能 DMC 5 - 6 - 7 - -
读 0x4015
BIT 作用 0 方波 1 长度计数器不为 0 1 方波 2 长度计数器不为 0 2 三角波长度计数器不为 0 3 噪声长度计数器不为 0 4 DMC 长度计数器不为 0 5 - 6 帧中断 7 DMC 中断
读 0x4015 后,会清除 帧中断 标志
4. 帧计数器
帧计数器 位于地址 0x4017,用来驱动各通道的长度,包络等单元。该寄存器只用了 2 个 bit,分别控制中断使能与步进方式:
BIT | 作用 |
---|---|
0 | 0:4 步模式,1:5 步模式 |
1 | 中断禁止标志,0:使能中断,1:禁用中断 |
2 - 7 | - |
这里肯定有人不理解什么是 4 步 5 步模式,还记得前面时钟部分讲到有 240Hz 的时钟吗?该时钟每周期会步进一次,如下:
4 步模式 | 5 步模式 | 功能 |
---|---|---|
- - - f | - - - - - | 产生中断 |
- l - l | - l - - l | 驱动长度计数器(Length counter)和扫描单元(Sweep) |
e e e e | e e e - e | 驱动包络(Envelope)与线性计数器(Linear counter) |
注:长度计数器,包络等概念一会再讲
比如,如果当前为 4 步模式,则驱动包络与线性计数器的频率为 240Hz,产生中断的频率为 60Hz,驱动长度计数器和扫描单元的频率为 120Hz
用代码实现也很简单,用 switch case 就行:
// processFrameCounter 调用频率为 240Hz
private processFrameCounter(): void {
if (this.mode === 0) { // 4 Step mode
switch (this.frameCounter % 4) {
case 0:
this.processEnvelopeAndLinearCounter();
break;
case 1:
this.processLengthCounterAndSweep();
this.processEnvelopeAndLinearCounter();
break;
case 2:
this.processEnvelopeAndLinearCounter();
break;
case 3:
this.triggerIRQ();
this.processLengthCounterAndSweep();
this.processEnvelopeAndLinearCounter();
break;
}
} else { // 5 Step mode
switch (this.frameCounter % 5) {
case 0:
this.processEnvelopeAndLinearCounter();
break;
case 1:
this.processLengthCounterAndSweep();
this.processEnvelopeAndLinearCounter();
break;
case 2:
this.processEnvelopeAndLinearCounter();
break;
case 3:
break;
case 4:
this.processLengthCounterAndSweep();
this.processEnvelopeAndLinearCounter();
break;
}
}
}
5. 单元
前面已经见过了长度计数器(Length counter),扫描单元(Sweep),包络(Envelope),线性计数器(Linear counter),每个通道都包含部分上述单元,每通道的单元都可以由该通道的寄存器控制,单元列表如下:
通道 | 单元 |
---|---|
方波1 (pulse1) | Timer, length counter, envelope, sweep |
方波2 (pulse2) | Timer, length counter, envelope, sweep |
三角波 (triangle) | Timer, length counter, linear counter |
噪声 (noise) | Timer, length counter, envelope, linear feedback shift register |
DMC | Timer, memory reader, sample buffer, output unit |
- Timer
每个通道都有,它使用基本时钟(CPU clock / 2),用于控制波形频率 - Length counter(长度计数器)
除 DMC 外其他通道都有,用于控制波形持续时间 - Envelope(包络)
只有方波和噪声通道有,用于控制音量随时间的变化的情况,比如车离你越来越远,音量越来越小的场景 - Sweep(扫描单元)
只有方波通道有,用于控制声音频率随时间变化,可以想象下汽车车速越来越快时发动机声音越来越尖的场景 - Linear counter (线性计数器)
只有三角波通道有,与 Length counter 一样,也用来控制音频持续时间。肯定有人会问这样功能不就重复了吗。其实结合前面的 4 步与 5 步序列我们可以发现,Linear counter 一个周期处理 4 次,Length counter 一个周期只处理 2 次,这样 Linear counter 的精度就是 Length counter 的 2 倍,可以做更高精度的定时 - linear feedback shift register(线性反馈移位寄存器)
只有噪声通道有,用来发生伪随机数,以此来产生噪声 - Memory reader(内存读取单元)
只有 DMC 通道有,读取总线上编码好的数据到 sample buffer - Sample buffer(采样缓冲)
只有 DMC 通道有,缓冲 DMC 数据 - Output unit(输出单元)
只有 DMC 通道有,用于生成音量数据
6. 混音器
复杂的音乐是有各t种音色组合起来的,APU 5 个通道充分运用才能发出动听的音乐。混音器就是用来组合 5 个通道音频整合后输出一个信号的东西
下列公式会将 5 个通道声音整合后输出为一个范围 0 ~ 1.0 的信号:
output = pulse_out + tnd_out
95.88
pulse_out = ------------------------------------
(8128 / (pulse1 + pulse2)) + 100
159.79
tnd_out = -------------------------------------------------------------
1
----------------------------------------------------- + 100
(triangle / 8227) + (noise / 12241) + (dmc / 22638)
具体参考 http://wiki.nesdev.com/w/index.php/APU_Mixer
在模拟的时候混音器有两种实现方式:查表和直接计算,直接计算代码最简单:
output = pulse_out + tnd_out
pulse_out = 0.00752 * (pulse1 + pulse2)
tnd_out = 0.00851 * triangle + 0.00494 * noise + 0.00335 * dmc