Skip to main content

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}