Skip to main content

rustynes_apu/
pulse.rs

1//! Pulse channel (1 of 2). 4-step duty sequencer + envelope + sweep + length.
2//!
3//! Per `docs/apu-2a03.md` §Behavior and NESdev wiki "APU Pulse" page.
4//!
5//! Two pulse channels share the same architecture but differ in sweep
6//! negation: pulse 1 uses one's-complement (`!t`), pulse 2 uses two's-
7//! complement (`-t`).  This produces an audible difference at low periods.
8
9use crate::envelope::Envelope;
10use crate::length::LengthCounter;
11
12/// Duty waveforms for the pulse channels.  Each entry is the 8-step output
13/// pattern for one of the four duty values.  Index = duty selection
14/// (`$4000` bits 6-7).  Step index runs 0..8 with 0 being the "current"
15/// position; the LSB is the output bit at the current step.
16const DUTY_TABLE: [[u8; 8]; 4] = [
17    [0, 1, 0, 0, 0, 0, 0, 0], // 12.5%
18    [0, 1, 1, 0, 0, 0, 0, 0], // 25.0%
19    [0, 1, 1, 1, 1, 0, 0, 0], // 50.0%
20    [1, 0, 0, 1, 1, 1, 1, 1], // 25.0% negated
21];
22
23/// Pulse channel state.
24#[derive(Debug, Clone, Copy)]
25pub struct Pulse {
26    /// Duty selection (0..=3).
27    pub(crate) duty: u8,
28    /// Step index into the duty table (0..=7). Decremented on each timer underflow.
29    pub(crate) step: u8,
30    /// 11-bit timer reload (from `$4002`/`$4003` low+high writes).
31    pub(crate) timer_period: u16,
32    /// Internal countdown timer.
33    pub(crate) timer: u16,
34    /// Envelope generator.
35    pub envelope: Envelope,
36    /// Length counter.
37    pub length: LengthCounter,
38    /// Sweep enabled (`$4001` bit 7).
39    pub(crate) sweep_enabled: bool,
40    /// Sweep divider period (3 bits, +1 -> 1..=8).
41    pub(crate) sweep_period: u8,
42    /// Sweep negate flag.
43    pub(crate) sweep_negate: bool,
44    /// Sweep shift count (3 bits).
45    pub(crate) sweep_shift: u8,
46    /// Sweep reload flag — set by `$4001` write; consumed at next half-frame.
47    pub(crate) sweep_reload: bool,
48    /// Internal sweep divider.
49    pub(crate) sweep_divider: u8,
50    /// Pulse 1 vs pulse 2 (controls one's-complement vs two's-complement).
51    pub(crate) is_pulse1: bool,
52}
53
54impl Pulse {
55    /// Construct a new pulse channel. `is_pulse1=true` for the pulse-1 sweep
56    /// negation flavor (one's complement).
57    #[must_use]
58    pub const fn new(is_pulse1: bool) -> Self {
59        Self {
60            duty: 0,
61            step: 0,
62            timer_period: 0,
63            timer: 0,
64            envelope: Envelope {
65                start: false,
66                loop_flag: false,
67                constant: false,
68                volume_or_period: 0,
69                divider: 0,
70                decay: 0,
71            },
72            length: LengthCounter {
73                count: 0,
74                halt: false,
75                new_halt: false,
76                enabled: false,
77                reload_val: 0,
78                previous_count: 0,
79            },
80            sweep_enabled: false,
81            sweep_period: 0,
82            sweep_negate: false,
83            sweep_shift: 0,
84            sweep_reload: false,
85            sweep_divider: 0,
86            is_pulse1,
87        }
88    }
89
90    /// `$4000` / `$4004` write: duty + length-halt + envelope.
91    pub fn write_ctrl(&mut self, value: u8) {
92        self.duty = (value >> 6) & 0x03;
93        let halt = (value & 0x20) != 0;
94        // Length-halt is deferred (applied after the same-cycle half-frame
95        // clock, per `LengthCounter::reload`); the envelope loop flag is not.
96        self.length.set_halt(halt);
97        self.envelope.loop_flag = halt;
98        self.envelope.constant = (value & 0x10) != 0;
99        self.envelope.volume_or_period = value & 0x0F;
100    }
101
102    /// `$4001` / `$4005` write: sweep config.
103    pub fn write_sweep(&mut self, value: u8) {
104        self.sweep_enabled = (value & 0x80) != 0;
105        self.sweep_period = (value >> 4) & 0x07;
106        self.sweep_negate = (value & 0x08) != 0;
107        self.sweep_shift = value & 0x07;
108        self.sweep_reload = true;
109    }
110
111    /// `$4002` / `$4006` write: timer low.
112    pub fn write_timer_lo(&mut self, value: u8) {
113        self.timer_period = (self.timer_period & 0xFF00) | u16::from(value);
114    }
115
116    /// `$4003` / `$4007` write: length load + timer high.
117    pub fn write_timer_hi(&mut self, value: u8) {
118        self.timer_period = (self.timer_period & 0x00FF) | (u16::from(value & 0x07) << 8);
119        self.length.load(value);
120        self.step = 0;
121        self.envelope.start = true;
122    }
123
124    /// One APU clock (half CPU clock).
125    pub fn clock_timer(&mut self) {
126        if self.timer == 0 {
127            self.timer = self.timer_period;
128            // 8-step duty sequencer, decremented (NESdev wiki).
129            self.step = (self.step + 1) & 0x07;
130        } else {
131            self.timer -= 1;
132        }
133    }
134
135    /// Half-frame clock: sweep + length.
136    pub fn clock_half_frame(&mut self) {
137        // Sweep first (because length might mute the channel).
138        let target = self.sweep_target();
139        if self.sweep_divider == 0 && self.sweep_enabled && self.sweep_shift > 0 && !self.muted() {
140            // Apply sweep: write the new period.
141            self.timer_period = target;
142        }
143        if self.sweep_divider == 0 || self.sweep_reload {
144            self.sweep_divider = self.sweep_period;
145            self.sweep_reload = false;
146        } else {
147            self.sweep_divider -= 1;
148        }
149        self.length.clock();
150    }
151
152    /// Quarter-frame clock: envelope.
153    pub fn clock_quarter_frame(&mut self) {
154        self.envelope.clock();
155    }
156
157    /// Compute the sweep target period (one's vs two's complement per channel).
158    fn sweep_target(&self) -> u16 {
159        let shifted = self.timer_period >> self.sweep_shift;
160        if self.sweep_negate {
161            if self.is_pulse1 {
162                self.timer_period.wrapping_sub(shifted).wrapping_sub(1)
163            } else {
164                self.timer_period.wrapping_sub(shifted)
165            }
166        } else {
167            self.timer_period.wrapping_add(shifted)
168        }
169    }
170
171    /// Sweep mute: timer < 8 OR target > $7FF.
172    #[must_use]
173    pub fn muted(&self) -> bool {
174        self.timer_period < 8 || self.sweep_target() > 0x7FF
175    }
176
177    /// Per-cycle output volume (0..=15).
178    #[must_use]
179    pub fn output(&self) -> u8 {
180        if self.length.count == 0
181            || self.muted()
182            || DUTY_TABLE[self.duty as usize][self.step as usize] == 0
183        {
184            0
185        } else {
186            self.envelope.output()
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn write_ctrl_sets_duty_and_envelope_period() {
197        let mut p = Pulse::new(true);
198        p.write_ctrl(0b1011_0101); // duty=2, halt=1, const=1, period=5
199        assert_eq!(p.duty, 2);
200        // Halt is deferred: latched in `new_halt`, promoted to `halt` by
201        // `LengthCounter::reload` (after the half-frame clock).
202        assert!(p.length.new_halt);
203        p.length.reload();
204        assert!(p.length.halt);
205        assert!(p.envelope.loop_flag);
206        assert!(p.envelope.constant);
207        assert_eq!(p.envelope.volume_or_period, 5);
208    }
209
210    #[test]
211    fn timer_underflow_advances_duty_step() {
212        let mut p = Pulse::new(true);
213        p.timer_period = 1;
214        p.timer = 0;
215        p.clock_timer();
216        assert_eq!(p.step, 1);
217    }
218
219    #[test]
220    fn pulse1_sweep_negation_is_ones_complement() {
221        let mut p = Pulse::new(true);
222        p.timer_period = 0x100;
223        p.sweep_negate = true;
224        p.sweep_shift = 1;
225        // shifted = 0x80; 0x100 - 0x80 - 1 = 0x7F.
226        assert_eq!(p.sweep_target(), 0x7F);
227    }
228
229    #[test]
230    fn pulse2_sweep_negation_is_twos_complement() {
231        let mut p = Pulse::new(false);
232        p.timer_period = 0x100;
233        p.sweep_negate = true;
234        p.sweep_shift = 1;
235        // 0x100 - 0x80 = 0x80.
236        assert_eq!(p.sweep_target(), 0x80);
237    }
238
239    #[test]
240    fn sweep_mutes_when_period_too_low() {
241        let mut p = Pulse::new(true);
242        p.timer_period = 7;
243        assert!(p.muted());
244        p.timer_period = 8;
245        assert!(!p.muted());
246    }
247
248    #[test]
249    fn sweep_mutes_when_target_above_7ff() {
250        let mut p = Pulse::new(true);
251        p.timer_period = 0x780;
252        p.sweep_negate = false;
253        p.sweep_shift = 1; // target = 0x780 + 0x3C0 = 0xB40 > $7FF
254        assert!(p.muted());
255    }
256
257    #[test]
258    fn output_zero_when_length_zero() {
259        let mut p = Pulse::new(true);
260        p.length.count = 0;
261        p.envelope.constant = true;
262        p.envelope.volume_or_period = 15;
263        assert_eq!(p.output(), 0);
264    }
265
266    #[test]
267    fn timer_hi_write_resets_duty_phase_not_divider() {
268        // NESdev "APU Pulse": writing $4003/$4007 resets the duty sequencer
269        // phase to step 0 but does NOT reset the timer divider.
270        let mut p = Pulse::new(true);
271        p.step = 5;
272        p.timer = 42;
273        p.write_timer_hi(0x03);
274        assert_eq!(p.step, 0, "duty sequencer phase must reset to 0");
275        assert_eq!(p.timer, 42, "timer divider must be preserved");
276        assert!(p.envelope.start, "envelope restart flag must be set");
277    }
278
279    #[test]
280    fn length_load_only_when_enabled() {
281        let mut p = Pulse::new(true);
282        p.length.enabled = false;
283        p.write_timer_hi(0x08);
284        // The load is deferred; resolve it (no half-frame clock in between, so
285        // `reload` applies it in-cycle). A disabled channel still ignores it.
286        p.length.reload();
287        assert_eq!(p.length.count, 0);
288        p.length.enabled = true;
289        p.write_timer_hi(0x08);
290        p.length.reload();
291        assert_ne!(p.length.count, 0);
292    }
293}