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
用于索引占空比表屹电,占空比表如下:
該位可以索引到具體的表阶剑,然后 timer 分頻后每個(gè) tick 按照表中的序列使用 0 或 1 與音量相乘[ [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], ];
- l
表示 envelope loop 標(biāo)志 - c
表示是否為常量音量 - vvvv
如果 c 置位跃巡,表示音量大小,否則表示 envelope 的分頻計(jì)數(shù)
- DD
-
0x4001 / 0x4005
比特位:EPPP.NSSS
- E
表示是否使能 sweep - PPP
sweep 的分頻計(jì)數(shù) - N
sweep 是否為負(fù)牧愁,用來(lái)控制頻率隨時(shí)間增大還是減小 - SSS
位移量素邪,用于每個(gè) sweep 周期將 timer 右移對(duì)應(yīng)的位移量得到增量
- E
-
0x4002 / 0x4006
比特位:LLLL.LLLL
- LLLLLLLL
timer 的低 8 位(一共 11 位,用于將 cpu 二分頻后的時(shí)鐘繼續(xù)分頻)
- LLLLLLLL
-
0x4003 / 0x4007
比特位:llll.lHHH
- lllll
length counter 分頻計(jì)數(shù) - HHH
timer 的高 3 位猪半,和 0x4002 / 0x4006 組成完整的計(jì)數(shù)
- lllll
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)屬性