Skip to main content

rill_lofi/emulators/
nes_chip.rs

1#[derive(Clone)]
2struct NesPulseChannel {
3    duty_cycle: f32,
4    frequency: f32,
5    volume: f32,
6    phase: f32,
7    enabled: bool,
8}
9
10#[derive(Clone)]
11struct NesTriangleChannel {
12    frequency: f32,
13    volume: f32,
14    phase: f32,
15    linear_counter: u8,
16    enabled: bool,
17}
18
19struct NesNoiseChannel {
20    mode: NoiseMode,
21    frequency: f32,
22    volume: f32,
23    shift_register: u16,
24    tick_counter: f32,
25    enabled: bool,
26}
27
28#[derive(Clone)]
29struct NesDpcmChannel {
30    sample_rate: f32,
31    delta: f32,
32    sample_buffer: Vec<i8>,
33    position: usize,
34    current_output: f32,
35    tick_counter: f32,
36    enabled: bool,
37}
38
39#[derive(Debug, Clone, Copy)]
40enum NoiseMode {
41    Short,
42    Long,
43}
44
45/// NES 2A03 APU sweep unit for pulse channels.
46///
47/// Modifies the channel's period at a configurable rate and direction.
48/// Clocked at ~120 Hz. Can mute the channel when the period underflows
49/// or exceeds 11 bits.
50struct NesSweepUnit {
51    enabled: bool,
52    reload: bool,
53    divider_period: u8,
54    divider_counter: u8,
55    negate: bool,
56    shift: u8,
57    target_period: u16,
58}
59
60impl NesSweepUnit {
61    fn new() -> Self {
62        Self {
63            enabled: false,
64            reload: false,
65            divider_period: 0,
66            divider_counter: 0,
67            negate: false,
68            shift: 0,
69            target_period: 0,
70        }
71    }
72
73    fn write_register(&mut self, value: u8) {
74        self.enabled = (value & 0x80) != 0;
75        self.divider_period = (value >> 4) & 0x07;
76        self.negate = (value & 0x08) != 0;
77        self.shift = value & 0x07;
78        self.reload = true;
79    }
80
81    fn set_target_period(&mut self, period: u16) {
82        self.target_period = period & 0x07FF;
83    }
84
85    /// Clock the sweep unit. Returns the current period,
86    /// or `None` if the channel should be silenced.
87    fn clock(&mut self) -> Option<u16> {
88        if self.divider_period == 0 && !self.reload {
89            return Some(self.target_period);
90        }
91        if self.reload {
92            self.divider_counter = self.divider_period;
93            self.reload = false;
94        }
95        if self.divider_counter > 0 {
96            self.divider_counter -= 1;
97        }
98        if self.divider_counter == 0 && self.divider_period > 0 {
99            let delta = self.target_period >> self.shift as u32;
100            let new = if self.negate {
101                self.target_period.saturating_sub(delta + 1)
102            } else {
103                self.target_period + delta
104            };
105            if !(8..=0x07FF).contains(&new) {
106                return None;
107            }
108            self.target_period = new;
109            self.divider_counter = self.divider_period;
110        }
111        Some(self.target_period)
112    }
113}
114
115/// Pure NES 2A03 APU chip emulation logic.
116///
117/// No graph node, no lofi processing. Directly testable.
118/// Registers are memory-mapped at $4000–$4015 (22 bytes).
119pub struct NesChip {
120    pulse1: NesPulseChannel,
121    pulse2: NesPulseChannel,
122    triangle: NesTriangleChannel,
123    noise: NesNoiseChannel,
124    dpcm: NesDpcmChannel,
125    sweep1: NesSweepUnit,
126    sweep2: NesSweepUnit,
127    sweep_phase: f32,
128}
129
130impl Default for NesChip {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl NesChip {
137    /// Create with default power-on state.
138    pub fn new() -> Self {
139        Self {
140            pulse1: NesPulseChannel {
141                duty_cycle: 0.25,
142                frequency: 440.0,
143                volume: 0.5,
144                phase: 0.0,
145                enabled: true,
146            },
147            pulse2: NesPulseChannel {
148                duty_cycle: 0.125,
149                frequency: 660.0,
150                volume: 0.3,
151                phase: 0.0,
152                enabled: true,
153            },
154            triangle: NesTriangleChannel {
155                frequency: 220.0,
156                volume: 0.4,
157                phase: 0.0,
158                linear_counter: 0,
159                enabled: true,
160            },
161            noise: NesNoiseChannel {
162                mode: NoiseMode::Short,
163                frequency: 1000.0,
164                volume: 0.2,
165                shift_register: 1,
166                tick_counter: 0.0,
167                enabled: true,
168            },
169            dpcm: NesDpcmChannel {
170                sample_rate: 22050.0,
171                delta: 0.01,
172                sample_buffer: Vec::new(),
173                position: 0,
174                current_output: 0.0,
175                tick_counter: 0.0,
176                enabled: false,
177            },
178            sweep1: NesSweepUnit::new(),
179            sweep2: NesSweepUnit::new(),
180            sweep_phase: 0.0,
181        }
182    }
183
184    /// Write register data. `regs` must be 22 bytes ($4000–$4015).
185    pub fn write_registers(&mut self, regs: &[u8]) {
186        if regs.len() < 22 {
187            return;
188        }
189
190        let duty_table: [f32; 4] = [0.125, 0.25, 0.5, 0.75];
191
192        // Pulse 1 ($4000–$4003)
193        self.pulse1.duty_cycle = duty_table[((regs[0] >> 6) & 0x03) as usize];
194        self.pulse1.volume = (regs[0] & 0x0F) as f32 / 15.0;
195        let p1_period = (regs[2] as u16) | (((regs[3] as u16) & 0x07) << 8);
196        self.pulse1.frequency = if p1_period > 0 {
197            1_789_773.0 / (16.0 * (p1_period + 1) as f32)
198        } else {
199            0.0
200        };
201        self.sweep1.write_register(regs[1]);
202        self.sweep1.set_target_period(p1_period);
203
204        // Pulse 2 ($4004–$4007)
205        self.pulse2.duty_cycle = duty_table[((regs[4] >> 6) & 0x03) as usize];
206        self.pulse2.volume = (regs[4] & 0x0F) as f32 / 15.0;
207        let p2_period = (regs[6] as u16) | (((regs[7] as u16) & 0x07) << 8);
208        self.pulse2.frequency = if p2_period > 0 {
209            1_789_773.0 / (16.0 * (p2_period + 1) as f32)
210        } else {
211            0.0
212        };
213        self.sweep2.write_register(regs[5]);
214        self.sweep2.set_target_period(p2_period);
215
216        // Triangle ($4008–$400B)
217        self.triangle.volume = if (regs[8] & 0x80) != 0 { 0.4 } else { 0.0 };
218        self.triangle.linear_counter = regs[8] & 0x7F;
219        let tri_period = (regs[10] as u16) | (((regs[11] as u16) & 0x07) << 8);
220        self.triangle.frequency = if tri_period > 0 {
221            1_789_773.0 / (32.0 * (tri_period + 1) as f32)
222        } else {
223            0.0
224        };
225
226        // Noise ($400C–$400F)
227        self.noise.mode = if (regs[12] & 0x80) != 0 {
228            NoiseMode::Short
229        } else {
230            NoiseMode::Long
231        };
232        self.noise.volume = (regs[12] & 0x0F) as f32 / 15.0;
233        let noise_period_idx = regs[14] & 0x0F;
234        let noise_periods: [u16; 16] = [
235            4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068,
236        ];
237        self.noise.frequency = 1_789_773.0 / noise_periods[noise_period_idx as usize] as f32;
238
239        // Channel enable ($4015)
240        let enable = regs[21];
241        self.pulse1.enabled = (enable & 0x01) != 0;
242        self.pulse2.enabled = (enable & 0x02) != 0;
243        self.triangle.enabled = (enable & 0x04) != 0;
244        self.noise.enabled = (enable & 0x08) != 0;
245        self.dpcm.enabled = (enable & 0x10) != 0;
246    }
247
248    /// Generate one audio sample. `sample_rate` is the output sample rate.
249    pub fn generate_sample(&mut self, sample_rate: f32) -> f32 {
250        // Sweep clock at ~120 Hz
251        self.sweep_phase += 120.0 / sample_rate;
252        while self.sweep_phase >= 1.0 {
253            self.sweep_phase -= 1.0;
254
255            let p1 = self.sweep1.clock();
256            if let Some(period) = p1 {
257                self.pulse1.frequency = if period > 0 {
258                    1_789_773.0 / (16.0 * (period + 1) as f32)
259                } else {
260                    0.0
261                };
262            } else {
263                self.pulse1.enabled = false;
264            }
265
266            let p2 = self.sweep2.clock();
267            if let Some(period) = p2 {
268                self.pulse2.frequency = if period > 0 {
269                    1_789_773.0 / (16.0 * (period + 1) as f32)
270                } else {
271                    0.0
272                };
273            } else {
274                self.pulse2.enabled = false;
275            }
276        }
277
278        // Pulse 1
279        let p1 = if self.pulse1.frequency > 0.0 && self.pulse1.enabled {
280            self.pulse1.phase += self.pulse1.frequency / sample_rate;
281            if self.pulse1.phase >= 1.0 {
282                self.pulse1.phase -= 1.0;
283            }
284            (if self.pulse1.phase < self.pulse1.duty_cycle {
285                1.0
286            } else {
287                -1.0
288            }) * self.pulse1.volume
289        } else {
290            0.0
291        };
292
293        // Pulse 2
294        let p2 = if self.pulse2.frequency > 0.0 && self.pulse2.enabled {
295            self.pulse2.phase += self.pulse2.frequency / sample_rate;
296            if self.pulse2.phase >= 1.0 {
297                self.pulse2.phase -= 1.0;
298            }
299            (if self.pulse2.phase < self.pulse2.duty_cycle {
300                1.0
301            } else {
302                -1.0
303            }) * self.pulse2.volume
304        } else {
305            0.0
306        };
307
308        // Triangle
309        let tri = if self.triangle.frequency > 0.0 && self.triangle.enabled {
310            self.triangle.phase += self.triangle.frequency / sample_rate;
311            if self.triangle.phase >= 1.0 {
312                self.triangle.phase -= 1.0;
313            }
314            (if self.triangle.phase < 0.5 {
315                self.triangle.phase * 4.0 - 1.0
316            } else {
317                3.0 - self.triangle.phase * 4.0
318            }) * self.triangle.volume
319        } else {
320            0.0
321        };
322
323        // Noise
324        let ns = if self.noise.enabled {
325            self.generate_noise(sample_rate)
326        } else {
327            0.0
328        };
329
330        // DPCM
331        let dpcm = if self.dpcm.enabled {
332            self.generate_dpcm(sample_rate)
333        } else {
334            0.0
335        };
336
337        let pulse_mix = (p1 + p2) * 0.5;
338        let tnd_mix = (tri * 3.0 + ns * 2.0 + dpcm) / 6.0;
339        (pulse_mix * 0.5 + tnd_mix * 0.5) * 0.5
340    }
341
342    /// Reset all channels and sweep units to initial state.
343    pub fn reset(&mut self) {
344        self.pulse1.phase = 0.0;
345        self.pulse2.phase = 0.0;
346        self.triangle.phase = 0.0;
347        self.noise.shift_register = 1;
348        self.noise.tick_counter = 0.0;
349        self.dpcm.position = 0;
350        self.dpcm.current_output = 0.0;
351        self.dpcm.tick_counter = 0.0;
352        self.sweep1 = NesSweepUnit::new();
353        self.sweep2 = NesSweepUnit::new();
354        self.sweep_phase = 0.0;
355    }
356
357    fn generate_noise(&mut self, sample_rate: f32) -> f32 {
358        let ticks_per_sample = sample_rate / self.noise.frequency;
359        self.noise.tick_counter += 1.0;
360        if self.noise.tick_counter >= ticks_per_sample {
361            self.noise.tick_counter = 0.0;
362            let feedback = match self.noise.mode {
363                NoiseMode::Short => {
364                    (self.noise.shift_register & 0x0001)
365                        ^ ((self.noise.shift_register >> 6) & 0x0001)
366                }
367                NoiseMode::Long => {
368                    (self.noise.shift_register & 0x0001)
369                        ^ ((self.noise.shift_register >> 1) & 0x0001)
370                }
371            };
372            self.noise.shift_register >>= 1;
373            self.noise.shift_register |= feedback << 14;
374        }
375        let sample = if (self.noise.shift_register & 0x0001) == 0 {
376            1.0
377        } else {
378            -1.0
379        };
380        sample * self.noise.volume
381    }
382
383    fn generate_dpcm(&mut self, sample_rate: f32) -> f32 {
384        if self.dpcm.sample_buffer.is_empty()
385            || self.dpcm.position >= self.dpcm.sample_buffer.len() * 8
386        {
387            return self.dpcm.current_output;
388        }
389        let ticks_per_sample = sample_rate / self.dpcm.sample_rate;
390        self.dpcm.tick_counter += 1.0;
391        if self.dpcm.tick_counter >= ticks_per_sample {
392            self.dpcm.tick_counter = 0.0;
393            let byte_idx = self.dpcm.position / 8;
394            let bit_idx = self.dpcm.position % 8;
395            if byte_idx < self.dpcm.sample_buffer.len() {
396                let bit = (self.dpcm.sample_buffer[byte_idx] >> bit_idx) & 1;
397                if bit != 0 {
398                    self.dpcm.current_output =
399                        (self.dpcm.current_output + self.dpcm.delta).min(1.0);
400                } else {
401                    self.dpcm.current_output =
402                        (self.dpcm.current_output - self.dpcm.delta).max(-1.0);
403                }
404                self.dpcm.position += 1;
405            }
406        }
407        self.dpcm.current_output
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_nes_chip_silent_when_all_disabled() {
417        let mut chip = NesChip::new();
418        let mut regs = [0u8; 22];
419        regs[21] = 0x00; // all channels disabled
420        chip.write_registers(&regs);
421        let s = chip.generate_sample(44100.0);
422        assert!(s.abs() < 0.001, "all disabled should be silent, got {}", s);
423    }
424
425    #[test]
426    fn test_nes_chip_produces_audio() {
427        let mut chip = NesChip::new();
428        let mut regs = [0u8; 22];
429        // Pulse 1: 50% duty, max volume, period=0x100 (~438 Hz)
430        regs[0] = 0x8F; // duty=10 (50%), volume=15
431        regs[2] = 0x00; // period low
432        regs[3] = 0x01; // period high
433        regs[21] = 0x01; // pulse1 enabled
434        chip.write_registers(&regs);
435        let mut max_abs = 0.0f32;
436        for _ in 0..1024 {
437            let s = chip.generate_sample(44100.0);
438            max_abs = max_abs.max(s.abs());
439        }
440        assert!(max_abs > 0.1, "should produce audio, max_abs={}", max_abs);
441    }
442
443    #[test]
444    fn test_nes_chip_reset() {
445        let mut chip = NesChip::new();
446        let mut regs = [0u8; 22];
447        regs[21] = 0x01; // pulse1 enabled
448        chip.write_registers(&regs);
449        for _ in 0..100 {
450            chip.generate_sample(44100.0);
451        }
452        chip.reset();
453        assert_eq!(chip.pulse1.phase, 0.0);
454        assert_eq!(chip.noise.shift_register, 1);
455    }
456
457    #[test]
458    fn test_sweep_unit_decreases_period() {
459        let mut unit = NesSweepUnit::new();
460        unit.write_register(0xCF); // enabled, divider=4, negate, shift=7
461        unit.set_target_period(0x100);
462        let initial = unit.target_period;
463        // Clock many times; negate + shift=7 should decrease period
464        let mut last = initial;
465        for _ in 0..200 {
466            if let Some(p) = unit.clock() {
467                last = p;
468            }
469        }
470        assert!(
471            last < initial,
472            "sweep should decrease period (negate), initial={}, last={}",
473            initial,
474            last
475        );
476    }
477
478    #[test]
479    fn test_sweep_mutes_on_underflow() {
480        let mut unit = NesSweepUnit::new();
481        unit.write_register(0xCF); // aggressive sweep
482        unit.set_target_period(0x10);
483        let mut muted = false;
484        for _ in 0..200 {
485            if unit.clock().is_none() {
486                muted = true;
487                break;
488            }
489        }
490        assert!(muted, "should mute when period underflows below 8");
491    }
492
493    #[test]
494    fn test_sweep_disabled_when_divider_zero() {
495        let mut unit = NesSweepUnit::new();
496        unit.write_register(0x08); // enabled=false equivalent? Actually bit7=0
497        unit.set_target_period(0x100);
498        let initial = unit.target_period;
499        for _ in 0..100 {
500            unit.clock();
501        }
502        assert_eq!(
503            unit.target_period, initial,
504            "disabled sweep should not change period"
505        );
506    }
507}