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}