NES 模拟器开发教程 13 - APU 简介

通过前面的教程基本已经能玩游戏了,但是有音乐才算得上完整,下面介绍 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容