|

楼主 |
发表于 2022-9-25 10:49:09
|
显示全部楼层
本帖最后由 gameboy 于 2022-9-25 10:52 编辑
NES 模拟器开发教程 14 - APU 方波
1. 简介
方波波形很简单,只有高低变化,波形如下
---- ---- ----
| | | | | |
| | | | | |
| | | | | |
---- ---- ---- ----
方波有几个属性:
- 频率
对应高低变化的频率 - 峰值
方波的高度,也就高低电压的差值 - 占空比
高电压在整个周期中的占比,例如占空比 25% 如下:
-- -- --
| | | | | |
| | | | | |
| | | | | |
------ ------ ------ ----
NES 中存在两个方波通道,除了 sweep 的行为有一点点区别以外,其它都是一样的
2. 寄存器注:本章建议结合后面的时序图一起看
每个方波通道需要 4 个寄存器,通道 1 寄存器地址为 0x4000 到 0x4003,通道 2 寄存器地址为 0x4004 到 0x4007,两个通道寄存器功能都一样
- 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 分频后每个 tick 按照表中的序列使用 0 或 1 与音量相乘 - l
表示 envelope loop 标志 - c
表示是否为常量音量 - vvvv
如果 c 置位,表示音量大小,否则表示 envelope 的分频计数
- 0x4001 / 0x4005
比特位:EPPP.NSSS
- E
表示是否使能 sweep - PPP
sweep 的分频计数 - N
sweep 是否为负,用来控制频率随时间增大还是减小 - SSS
位移量,用于每个 sweep 周期将 timer 右移对应的位移量得到增量
- 0x4002 / 0x4006
比特位:LLLL.LLLL
- LLLLLLLL
timer 的低 8 位(一共 11 位,用于将 cpu 二分频后的时钟继续分频)
- 0x4003 / 0x4007
比特位:llll.lHHH
- lllll
length counter 分频计数 - HHH
timer 的高 3 位,和 0x4002 / 0x4006 组成完整的计数
3. sweep 区别
前面讲过两个 channel 在 sweep 的时候有一点点区别,这里解释下:
当负数标志置位后,channel 1 在 sweep 的时候会多减 1,比如算出来 sweep 为 -10,则 channel 1 timer - 11,channel 2 timer - 10
4. 时序
以通道 1 为例,整个时序如下:
4. 实现
示例代码如下:
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. 总结
方波几乎是 APU 里最复杂的通道了,其他通道相对简单后面就不多做介绍了,参考其他源码和 nes dev 就能写出来。简单来讲,就是提供寄存器,修改声音的相关属性
|
|