Skip to main content

rustynes_apu/
triangle.rs

1//! Triangle channel: 32-step waveform + linear counter + length counter.
2//!
3//! Per `docs/apu-2a03.md` §Behavior and NESdev wiki "APU Triangle" page.
4//!
5//! - Timer counts at the **CPU** clock (not the APU clock — twice as fast as
6//!   the pulse channels' timers for the same period value).
7//! - Length and linear counters both gate the sequencer; if either is 0 the
8//!   sequencer freezes (output holds last value, no click).
9
10use crate::length::LengthCounter;
11
12/// 32-step triangle output sequence (15 down to 0 then 0 up to 15).
13const TRIANGLE_TABLE: [u8; 32] = [
14    15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
15    13, 14, 15,
16];
17
18/// Triangle channel state.
19#[derive(Debug, Clone, Copy)]
20pub struct Triangle {
21    /// 11-bit timer reload (counts at CPU clock).
22    pub(crate) timer_period: u16,
23    /// Internal countdown timer.
24    pub(crate) timer: u16,
25    /// Sequencer step (0..=31).
26    pub(crate) step: u8,
27    /// Length counter (uses control bit `$4008` bit 7 as halt-flag too).
28    pub length: LengthCounter,
29    /// Linear counter reload value (`$4008` bits 0-6).
30    pub(crate) linear_reload_value: u8,
31    /// Linear counter current value.
32    pub(crate) linear_counter: u8,
33    /// Linear counter control flag (`$4008` bit 7) — if clear, the linear
34    /// counter clears its reload-flag at the end of the frame; if set, the
35    /// reload-flag stays set forever (linear counter behaves like a length
36    /// counter halt).
37    pub(crate) linear_control: bool,
38    /// Linear-counter reload flag — set by `$400B` write, consumed at quarter
39    /// frame.
40    pub(crate) linear_reload_flag: bool,
41}
42
43impl Default for Triangle {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl Triangle {
50    /// New triangle channel.
51    #[must_use]
52    pub const fn new() -> Self {
53        Self {
54            timer_period: 0,
55            timer: 0,
56            step: 0,
57            length: LengthCounter {
58                count: 0,
59                halt: false,
60                new_halt: false,
61                enabled: false,
62                reload_val: 0,
63                previous_count: 0,
64            },
65            linear_reload_value: 0,
66            linear_counter: 0,
67            linear_control: false,
68            linear_reload_flag: false,
69        }
70    }
71
72    /// `$4008` write: control bit + linear counter reload value.
73    pub fn write_linear(&mut self, value: u8) {
74        self.linear_control = (value & 0x80) != 0;
75        // Length counter halt = control bit (per NESdev wiki). Deferred:
76        // applied after the same-cycle half-frame clock (`LengthCounter::reload`).
77        self.length.set_halt(self.linear_control);
78        self.linear_reload_value = value & 0x7F;
79    }
80
81    /// `$400A` write: timer low.
82    pub fn write_timer_lo(&mut self, value: u8) {
83        self.timer_period = (self.timer_period & 0xFF00) | u16::from(value);
84    }
85
86    /// `$400B` write: length load + timer high. Sets the linear reload flag.
87    pub fn write_timer_hi(&mut self, value: u8) {
88        self.timer_period = (self.timer_period & 0x00FF) | (u16::from(value & 0x07) << 8);
89        self.length.load(value);
90        self.linear_reload_flag = true;
91    }
92
93    /// One CPU clock.
94    pub fn clock_timer(&mut self) {
95        // Ultrasonic-silence (NESdev wiki "APU Triangle"): a timer period below
96        // 2 (frequency above ~55.9 kHz) would clock the sequencer faster than
97        // hardware can follow; the real channel effectively halts there and the
98        // output holds its current step. Most emulators freeze the sequencer to
99        // avoid the resulting pop (Mega Man 2's "Crash Man" stage relies on
100        // this). We hold the sequencer — output stays at the current step.
101        if self.timer_period < 2 {
102            return;
103        }
104        if self.timer == 0 {
105            self.timer = self.timer_period;
106            // Only advance sequencer if both gates are open.
107            if self.length.count > 0 && self.linear_counter > 0 {
108                self.step = (self.step + 1) & 0x1F;
109            }
110        } else {
111            self.timer -= 1;
112        }
113    }
114
115    /// Quarter-frame clock: linear counter.
116    pub fn clock_quarter_frame(&mut self) {
117        if self.linear_reload_flag {
118            self.linear_counter = self.linear_reload_value;
119        } else if self.linear_counter > 0 {
120            self.linear_counter -= 1;
121        }
122        // Control bit clear -> reload flag cleared at quarter clock.
123        if !self.linear_control {
124            self.linear_reload_flag = false;
125        }
126    }
127
128    /// Half-frame clock: length counter.
129    pub fn clock_half_frame(&mut self) {
130        self.length.clock();
131    }
132
133    /// Per-cycle output (0..=15).
134    ///
135    /// The ultrasonic-silence behavior is implemented in [`Self::clock_timer`]
136    /// by freezing the sequencer when `timer_period < 2`; the output simply
137    /// holds the current step (so it does not pop), matching hardware.
138    #[must_use]
139    pub fn output(&self) -> u8 {
140        TRIANGLE_TABLE[self.step as usize]
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn linear_reload_on_quarter_frame() {
150        let mut t = Triangle::new();
151        t.write_linear(0x40); // control=0, reload=0x40
152        t.write_timer_hi(0x08);
153        assert!(t.linear_reload_flag);
154        t.clock_quarter_frame();
155        assert_eq!(t.linear_counter, 0x40);
156        assert!(!t.linear_reload_flag); // control=0 clears flag
157    }
158
159    #[test]
160    fn linear_control_keeps_reload_flag() {
161        let mut t = Triangle::new();
162        t.write_linear(0xC0); // control=1, reload=0x40
163        t.write_timer_hi(0x08);
164        t.clock_quarter_frame();
165        assert!(t.linear_reload_flag);
166    }
167
168    #[test]
169    fn sequencer_advances_on_timer_underflow() {
170        let mut t = Triangle::new();
171        t.length.count = 5;
172        t.linear_counter = 5;
173        // A non-ultrasonic period (>= 2) so the sequencer is not frozen.
174        t.timer_period = 2;
175        t.timer = 0;
176        t.clock_timer();
177        assert_eq!(t.step, 1);
178    }
179
180    #[test]
181    fn sequencer_frozen_when_length_zero() {
182        let mut t = Triangle::new();
183        t.length.count = 0;
184        t.linear_counter = 5;
185        // Non-ultrasonic period so only the length gate (not the ultrasonic
186        // freeze) is what holds the sequencer.
187        t.timer_period = 2;
188        t.timer = 0;
189        t.clock_timer();
190        assert_eq!(t.step, 0);
191    }
192
193    #[test]
194    fn ultrasonic_period_freezes_sequencer() {
195        // Period < 2 (ultrasonic): hardware halts the sequencer; the step must
196        // not advance even with both gates open and the timer expired.
197        let mut t = Triangle::new();
198        t.length.count = 5;
199        t.linear_counter = 5;
200        t.timer_period = 1;
201        t.timer = 0;
202        t.clock_timer();
203        assert_eq!(t.step, 0, "step must not advance at period<2");
204        // Period 0 is also ultrasonic-silenced.
205        t.timer_period = 0;
206        t.timer = 0;
207        t.clock_timer();
208        assert_eq!(t.step, 0, "step must not advance at period==0");
209    }
210
211    #[test]
212    fn period_two_resumes_clocking() {
213        // The threshold is strictly < 2; period == 2 still clocks normally.
214        let mut t = Triangle::new();
215        t.length.count = 5;
216        t.linear_counter = 5;
217        t.timer_period = 2;
218        t.timer = 0;
219        t.clock_timer();
220        assert_eq!(t.step, 1, "step must advance at period==2");
221    }
222}