rustynes_apu/envelope.rs
1//! Envelope generator (pulse channels + noise channel).
2//!
3//! Per `docs/apu-2a03.md` §State and the NESdev wiki "APU Envelope" page.
4//! Two modes:
5//! - **Constant volume**: output = `volume_or_period`.
6//! - **Decay**: a 4-bit counter decays from 15 to 0 (looping if `loop_flag`).
7//!
8//! The envelope is clocked once per quarter-frame. A "start" flag (set by
9//! `$4003`/`$4007`/`$400F` writes) reloads the decay counter and the divider
10//! on the next clock.
11
12/// Envelope generator.
13#[derive(Debug, Clone, Copy, Default)]
14pub struct Envelope {
15 /// Restart flag — set by length-load writes; consumed at next quarter clock.
16 pub start: bool,
17 /// Loop flag (a.k.a. length-counter halt, depending on the channel).
18 pub loop_flag: bool,
19 /// Constant-volume bit. When set, output = `volume_or_period`.
20 pub constant: bool,
21 /// Volume (constant mode) or divider period - 1 (decay mode).
22 pub volume_or_period: u8,
23 /// Internal divider counter.
24 pub divider: u8,
25 /// Internal decay counter (4-bit, 15 -> 0).
26 pub decay: u8,
27}
28
29impl Envelope {
30 /// Quarter-frame clock.
31 pub fn clock(&mut self) {
32 if self.start {
33 // Reload: decay = 15, divider = period.
34 self.start = false;
35 self.decay = 15;
36 self.divider = self.volume_or_period;
37 } else if self.divider == 0 {
38 self.divider = self.volume_or_period;
39 if self.decay > 0 {
40 self.decay -= 1;
41 } else if self.loop_flag {
42 self.decay = 15;
43 }
44 } else {
45 self.divider -= 1;
46 }
47 }
48
49 /// Output volume (0..=15).
50 #[must_use]
51 pub const fn output(&self) -> u8 {
52 if self.constant {
53 self.volume_or_period
54 } else {
55 self.decay
56 }
57 }
58}