GB/音频/方波通道

方波通道 1 具有扫频和包络功能. 从功能组件来说, 方波通道 1 主要包含定时器, 长度计数器, 音量调节器和扫频器几个功能组件. 它与方波通道 2 几乎一致, 唯一区别是方波通道 2 不包含扫频器. 下面将先行介绍方波通道 1 的寄存器与其功能细节.

FF10

名称说明
Bit 6-4Sweep Time扫频器周期
Bit 3Sweep Increase/Decrease0: 扫频器提升频率 1: 扫频器减少频率
Bit 2-0Number of sweep shift扫频器的移位值

Sweep Time

  • 000: sweep off - no freq change
  • 001: 7.8 ms (1/128Hz)
  • 010: 15.6 ms (2/128Hz)
  • 011: 23.4 ms (3/128Hz)
  • 100: 31.3 ms (4/128Hz)
  • 101: 39.1 ms (5/128Hz)
  • 110: 46.9 ms (6/128Hz)
  • 111: 54.7 ms (7/128Hz)

每个 shift 的频率变化(NR13, NR14)通过以下公式计算, 其中 X(0) 是初始频率, X(t-1) 是计算后的频率:

X(t) = X(t-1) +/- X(t-1)/2^n

FF11

名称说明
Bit 7-6Wave Pattern Duty波形占空比模式
Bit 5-0Sound length声音长度

Wave Duty

Wave Duty 用于控制方波通道 1 的波形占空比. 方波通道 1 的音频输出会被裁剪, 在以 8 为周期的时间内, 只有其中固定的时间段会有声波输出, 其余时间无论原始声波的振幅是多少, 都以 0 对待. 下表列出了 Wave Duty 从 0 到 3 这四种情况对应的占空比信息. 当 Waveform 中的某位为 0 时, 即表示在以 8 为周期的时间内不允许该时间段输出音频信号.

DutyWaveformRatio
000000000112.5%
011000000125%
101000011150%
110111111075%

Sound Length

用于长度计数器(LengthCounter) 计数的初始值. 当 NR14 的第 6 位被设置为 1 时, 长度计数器将加载 Sound Length.

FF12

名称说明
Bit 7-4Initial Volume of envelope范围为 (00-0F) (0=No Sound)
Bit 3Envelope Direction0: 减少, 1: 增加
Bit 2-0Number of envelope sweep范围 0-7, 如果是 0, 则停止音量包络

FF13

方波通道 1 的音频频率是 11 位无符号整数, 0xff13 寄存器存储该数字的低 8 为. 高 3 位存储在 0xff14 寄存器中.

FF14

名称说明
Bit 7Initial写入 1, 则重启音频系统
Bit 6Counter/consecutive selection写入 1, 则长度计数器到期后停止输出
Bit 2-0Frequency's higher 3 bits频率的高 3 位

频率用来设置音频通道内部计数器的值, 其公式是

Peroid = (2048 - Frequency) * 4

方波通道 2 与方波通道 1 的区别

方波通道 2 与方波通道 1 首先在寄存器地址上有区别:

寄存器名称方波通道 1 地址方波通道 2 地址
NRX00xff10-
NRX10xff110xff16
NRX20xff120xff17
NRX30xff130xff18
NRX40xff140xff19

其次方波通道 2 不包含扫频器. 在代码实现上只需要屏蔽扫频器相关代码, 即可无缝迁移方波通道 1 的代码到方波通道 2. 不再赘述.

代码实现

为了保存音频数据, 代码内将用到 BlipBuf 库. 对该库做一次简单的封装:

struct Blip {
    data: BlipBuf,
    from: u32, // 最后一次改变振幅的时间点
    ampl: i32, // 当前振幅
}

impl Blip {
    fn power_up(data: BlipBuf) -> Self {
        Self {
            data,
            from: 0x0000_0000,
            ampl: 0x0000_0000,
        }
    }

    fn set(&mut self, time: u32, ampl: i32) {
        self.from = time;
        let d = ampl - self.ampl;
        self.ampl = ampl;
        self.data.add_delta(time, d);
    }
}

在了解方波通道的组件后, 代码实现异常简单, 主要处理好 next() 函数就可以了.

struct ChannelSquare {
    reg: Rc<RefCell<Register>>,
    timer: Timer,
    lc: LengthCounter,
    ve: VolumeEnvelope,
    fs: FrequencySweep,
    blip: Blip,
    idx: u8,
}

impl ChannelSquare {
    fn power_up(blip: BlipBuf, mode: Channel) -> ChannelSquare {
        let reg = Rc::new(RefCell::new(Register::power_up(mode.clone())));
        ChannelSquare {
            reg: reg.clone(),
            timer: Timer::power_up(8192),
            lc: LengthCounter::power_up(reg.clone()),
            ve: VolumeEnvelope::power_up(reg.clone()),
            fs: FrequencySweep::power_up(reg.clone()),
            blip: Blip::power_up(blip),
            idx: 1,
        }
    }

    // This assumes no volume or sweep adjustments need to be done in the meantime
    fn next(&mut self, cycles: u32) {
        let pat = match self.reg.borrow().get_duty() {
            0 => 0b0000_0001,
            1 => 0b1000_0001,
            2 => 0b1000_0111,
            3 => 0b0111_1110,
            _ => unreachable!(),
        };
        let vol = i32::from(self.ve.volume);
        for _ in 0..self.timer.next(cycles) {
            let ampl = if !self.reg.borrow().get_trigger() || self.ve.volume == 0 {
                0x00
            } else if (pat >> self.idx) & 0x01 != 0x00 {
                vol
            } else {
                vol * -1
            };
            self.blip.set(self.blip.from + self.timer.period, ampl);
            self.idx = (self.idx + 1) % 8;
        }
    }
}

之后为方波通道实现内存读写, 注意当写 0xff14 和 0xff19 内存时, 将触发事件操作. 其中扫频器的事件是方波通道 1 独有的.

impl Memory for ChannelSquare {
    fn get(&self, a: u16) -> u8 {
        match a {
            0xff10 | 0xff15 => self.reg.borrow().nrx0,
            0xff11 | 0xff16 => self.reg.borrow().nrx1,
            0xff12 | 0xff17 => self.reg.borrow().nrx2,
            0xff13 | 0xff18 => self.reg.borrow().nrx3,
            0xff14 | 0xff19 => self.reg.borrow().nrx4,
            _ => unreachable!(),
        }
    }

    fn set(&mut self, a: u16, v: u8) {
        match a {
            0xff10 | 0xff15 => self.reg.borrow_mut().nrx0 = v,
            0xff11 | 0xff16 => {
                self.reg.borrow_mut().nrx1 = v;
                self.lc.n = self.reg.borrow().get_length_load();
            }
            0xff12 | 0xff17 => self.reg.borrow_mut().nrx2 = v,
            0xff13 | 0xff18 => {
                self.reg.borrow_mut().nrx3 = v;
                self.timer.period = period(self.reg.clone());
            }
            0xff14 | 0xff19 => {
                self.reg.borrow_mut().nrx4 = v;
                self.timer.period = period(self.reg.clone());
                // Trigger Event
                //
                // Writing a value to NRx4 with bit 7 set causes the following things to occur:
                //
                //   - Channel is enabled (see length counter).
                //   - If length counter is zero, it is set to 64 (256 for wave channel).
                //   - Frequency timer is reloaded with period.
                //   - Volume envelope timer is reloaded with period.
                //   - Channel volume is reloaded from NRx2.
                //   - Noise channel's LFSR bits are all set to 1.
                //   - Wave channel's position is set to 0 but sample buffer is NOT refilled.
                //   - Square 1's sweep does several things (see frequency sweep).
                //
                // Note that if the channel's DAC is off, after the above actions occur the channel will be immediately
                // disabled again.
                if self.reg.borrow().get_trigger() {
                    self.lc.reload();
                    self.ve.reload();
                    if self.reg.borrow().channel == Channel::Square1 {
                        self.fs.reload();
                    }
                }
            }
            _ => unreachable!(),
        }
    }
}