NES 模擬器開(kāi)發(fā)教程 14 - APU 方波

1. 簡(jiǎn)介

方波波形很簡(jiǎn)單运准,只有高低變化,波形如下

    +----+    +----+    +----+
    |    |    |    |    |    |
    |    |    |    |    |    |
    |    |    |    |    |    |
----+    +----+    +----+    +----

方波有幾個(gè)屬性:

  • 頻率
    對(duì)應(yīng)高低變化的頻率
  • 峰值
    方波的高度,也就高低電壓的差值
  • 占空比
    高電壓在整個(gè)周期中的占比豺撑,例如占空比 25% 如下:
          +--+      +--+      +--+
          |  |      |  |      |  |
          |  |      |  |      |  |
          |  |      |  |      |  |
    ------+  +------+  +------+  +----
    

NES 中存在兩個(gè)方波通道,除了 sweep 的行為有一點(diǎn)點(diǎn)區(qū)別以外黔牵,其它都是一樣的

2. 寄存器

注:本章建議結(jié)合后面的時(shí)序圖一起看

每個(gè)方波通道需要 4 個(gè)寄存器聪轿,通道 1 寄存器地址為 0x4000 到 0x4003,通道 2 寄存器地址為 0x4004 到 0x4007荧止,兩個(gè)通道寄存器功能都一樣

  • 0x4000 / 0x4004
    比特位:DDlc.vvvv

    • DD
      用于索引占空比表屹电,占空比表如下:
      [
        [0, 0, 0, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 1, 1],
        [0, 0, 0, 0, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 0, 0],
      ];
      
      該位可以索引到具體的表阶剑,然后 timer 分頻后每個(gè) tick 按照表中的序列使用 0 或 1 與音量相乘
    • l
      表示 envelope loop 標(biāo)志
    • c
      表示是否為常量音量
    • vvvv
      如果 c 置位跃巡,表示音量大小,否則表示 envelope 的分頻計(jì)數(shù)
  • 0x4001 / 0x4005
    比特位:EPPP.NSSS

    • E
      表示是否使能 sweep
    • PPP
      sweep 的分頻計(jì)數(shù)
    • N
      sweep 是否為負(fù)牧愁,用來(lái)控制頻率隨時(shí)間增大還是減小
    • SSS
      位移量素邪,用于每個(gè) sweep 周期將 timer 右移對(duì)應(yīng)的位移量得到增量
  • 0x4002 / 0x4006
    比特位:LLLL.LLLL

    • LLLLLLLL
      timer 的低 8 位(一共 11 位,用于將 cpu 二分頻后的時(shí)鐘繼續(xù)分頻)
  • 0x4003 / 0x4007
    比特位:llll.lHHH

    • lllll
      length counter 分頻計(jì)數(shù)
    • HHH
      timer 的高 3 位猪半,和 0x4002 / 0x4006 組成完整的計(jì)數(shù)

3. sweep 區(qū)別

前面講過(guò)兩個(gè) channel 在 sweep 的時(shí)候有一點(diǎn)點(diǎn)區(qū)別兔朦,這里解釋下:
當(dāng)負(fù)數(shù)標(biāo)志置位后,channel 1 在 sweep 的時(shí)候會(huì)多減 1磨确,比如算出來(lái) sweep 為 -10沽甥,則 channel 1 timer - 11,channel 2 timer - 10

4. 時(shí)序

以通道 1 為例乏奥,整個(gè)時(shí)序如下:


apu-pulse.drawio.png

4. 實(shí)現(xiàn)

示例代碼如下:

import { uint8 } from '../api/types';
import { IChannel } from '../api/apu';
import { DUTY_TABLE, LENGTH_TABLE } from './table';

export class Pulse implements IChannel {
  public volume = 0; // 0-15
  public lengthCounter = 0; // 5bit

  private duty = 0; // 2bit
  private isEnvelopeLoop = false;
  private isConstantVolume = false;
  private envelopeValue = 0; // 4bit
  private envelopeVolume = 0; // 4bit
  private envelopeCounter = 0;

  private isSweepEnabled = false;
  private sweepPeriod = 0; // 3bit
  private isSweepNegated = false;
  private sweepShift = 0; // 3bit
  private sweepCounter = 0;

  private timer = 0; // 11bit

  private internalTimer = 0;
  private counter = 0;

  private enable = false;

  constructor(
    private readonly channel: number,
  ) {}

  public get isEnabled(): boolean {
    return this.enable;
  }

  public set isEnabled(isEnabled: boolean) {
    this.enable = isEnabled;
    if (!isEnabled) {
      this.lengthCounter = 0;
    }
  }

  public clock(): void {
    if (!this.isEnabled) {
      return;
    }

    if (this.internalTimer === 0) {
      this.internalTimer = this.timer;
      this.step();
    } else {
      this.internalTimer--;
    }
  }

  public processEnvelope(): void {
    if (this.isConstantVolume) {
      return;
    }

    if (this.envelopeCounter % (this.envelopeValue + 1) === 0) {
      if (this.envelopeVolume === 0) {
        this.envelopeVolume = this.isEnvelopeLoop ? 15 : 0;
      } else {
        this.envelopeVolume--;
      }
    }

    this.envelopeCounter++;
  }

  public processLengthCounter(): void {
    if (!this.isEnvelopeLoop && this.lengthCounter > 0) {
      this.lengthCounter--;
    }
  }

  public processSweep(): void {
    if (!this.isSweepEnabled) {
      return;
    }

    if (this.sweepCounter % (this.sweepPeriod + 1) === 0) {
      // 1. A barrel shifter shifts the channel's 11-bit raw timer period right by the shift count, producing the change amount.
      // 2. If the negate flag is true, the change amount is made negative.
      // 3. The target period is the sum of the current period and the change amount.
      const changeAmount = this.isSweepNegated ? -(this.timer >> this.sweepShift) : this.timer >> this.sweepShift;
      this.timer += changeAmount;

      // The two pulse channels have their adders' carry inputs wired differently,
      // which produces different results when each channel's change amount is made negative:
      //   - Pulse 1 adds the ones' complement (?c ? 1). Making 20 negative produces a change amount of ?21.
      //   - Pulse 2 adds the two's complement (?c). Making 20 negative produces a change amount of ?20.
      if (this.channel === 1 && changeAmount <= 0) {
        this.timer--;
      }
    }

    this.sweepCounter++;
  }

  public write(offset: uint8, data: uint8) {
    switch (offset) {
      case 0:
        this.duty = data >> 6;
        this.isEnvelopeLoop = !!(data & 0x20);
        this.isConstantVolume = !!(data & 0x10);
        this.envelopeValue = data & 0x0F;

        this.envelopeVolume = 15;
        this.envelopeCounter = 0;
        break;
      case 1:
        this.isSweepEnabled = !!(data & 0x80);
        this.sweepPeriod = data >> 4 & 0x07;
        this.isSweepNegated = !!(data & 0x08);
        this.sweepShift = data & 0x07;

        this.sweepCounter = 0;
        break;
      case 2:
        this.timer = this.timer & 0xFF00 | data;
        break;
      case 3:
        this.timer = this.timer & 0x00FF | (data << 8) & 0x07FF;
        this.lengthCounter = LENGTH_TABLE[data >> 3];

        this.internalTimer = 0;
        break;
    }
  }

  private step(): void {
    this.counter++;

    // If at any time the target period is greater than $7FF, the sweep unit mutes the channel
    // If the current period is less than 8, the sweep unit mutes the channel
    if (!this.isEnabled || this.lengthCounter === 0 || this.timer < 8 || this.timer > 0x7FF) {
      this.volume = 0;
    } else if (this.isConstantVolume) {
      this.volume = this.envelopeValue * DUTY_TABLE[this.duty][this.counter & 0x07];
    } else {
      this.volume = this.envelopeVolume * DUTY_TABLE[this.duty][this.counter & 0x07];
    }
  }

}

5. 總結(jié)

方波幾乎是 APU 里最復(fù)雜的通道了摆舟,其他通道相對(duì)簡(jiǎn)單后面就不多做介紹了,參考其他源碼和 nes dev 就能寫(xiě)出來(lái)邓了。簡(jiǎn)單來(lái)講恨诱,就是提供寄存器,修改聲音的相關(guān)屬性

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末骗炉,一起剝皮案震驚了整個(gè)濱河市照宝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌句葵,老刑警劉巖厕鹃,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乍丈,居然都是意外死亡剂碴,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)诗赌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)汗茄,“玉大人,你說(shuō)我怎么就攤上這事铭若。” “怎么了布近?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵狗超,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我绞铃,道長(zhǎng),這世上最難降的妖魔是什么嫂侍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任儿捧,我火速辦了婚禮,結(jié)果婚禮上挑宠,老公的妹妹穿的比我還像新娘菲盾。我一直安慰自己,他們只是感情好各淀,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布懒鉴。 她就那樣靜靜地躺著,像睡著了一般碎浇。 火紅的嫁衣襯著肌膚如雪临谱。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天奴璃,我揣著相機(jī)與錄音悉默,去河邊找鬼。 笑死苟穆,一個(gè)胖子當(dāng)著我的面吹牛抄课,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鞭缭,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼剖膳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了岭辣?” 一聲冷哼從身側(cè)響起吱晒,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沦童,沒(méi)想到半個(gè)月后仑濒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡偷遗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年墩瞳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氏豌。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喉酌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情泪电,我是刑警寧澤般妙,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站相速,受9級(jí)特大地震影響碟渺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜突诬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一苫拍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旺隙,春花似錦绒极、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)伏社。三九已至抠刺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摘昌,已是汗流浹背速妖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留聪黎,地道東北人罕容。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像稿饰,于是被迫代替她去往敵國(guó)和親锦秒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容