Skip to content

Commit

Permalink
Split APU into modules
Browse files Browse the repository at this point in the history
  • Loading branch information
aelred committed Sep 29, 2024
1 parent 765ba9f commit 935d0ed
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 183 deletions.
56 changes: 56 additions & 0 deletions src/apu/envelope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#[derive(Default)]
// An envelope changes a sound's volume over time.
// In the NES APU, it can set a constant volume or a decay.
pub struct Envelope {
pub constant_volume: bool,
pub looping: bool,
pub start: bool,
pub divider: u8,
pub decay_level: u8,
pub volume: u8,
}

impl Envelope {
pub fn start(&mut self) {
self.start = true;
}

pub fn set_constant_volume(&mut self, constant_volume: bool) {
self.constant_volume = constant_volume;
}

pub fn set_volume(&mut self, volume: u8) {
self.volume = volume;
}

pub fn clock(&mut self) {
if !self.start {
self.clock_divider();
} else {
self.start = false;
self.decay_level = 15;
self.divider = self.volume;
}
}

pub fn clock_divider(&mut self) {
if self.divider == 0 {
self.divider = self.volume;
if self.decay_level > 0 {
self.decay_level -= 1;
} else if self.looping {
self.decay_level = 15;
}
} else {
self.divider -= 1;
}
}

pub fn volume(&self) -> u8 {
if self.constant_volume {
self.volume
} else {
self.decay_level
}
}
}
191 changes: 8 additions & 183 deletions src/apu/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Emulates the APU (audio processing unit)
use bitflags::bitflags;
use pulse::PulseGenerator;

mod envelope;
mod pulse;

#[derive(Default)]
pub struct APU {
Expand All @@ -20,12 +24,12 @@ impl APU {

match (self.mode_toggle, cycles) {
(_, 3728) | (_, 11185) => {
self.pulse_1.envelope.clock();
self.pulse_2.envelope.clock();
self.pulse_1.clock_envelope();
self.pulse_2.clock_envelope();
}
(_, 7456) | (false, 14914) | (true, 18640) => {
self.pulse_1.envelope.clock();
self.pulse_2.envelope.clock();
self.pulse_1.clock_envelope();
self.pulse_2.clock_envelope();
self.pulse_1.clock_length_counter();
self.pulse_2.clock_length_counter();
}
Expand Down Expand Up @@ -81,133 +85,6 @@ impl APU {
}
}

#[derive(Default)]
// A 'pulse wave' is a rectangular wave (alternating from high to low).
struct PulseGenerator {
enabled: bool,
// `timer` starts at `timer_initial` and counts down to 0.
// When it reaches 0, it is reloaded with `timer_initial` and `sequencer` is incremented.
// A lower `timer_initial` value results in a higher frequency.
timer_initial: u16,
timer: u16,
// The index into the waveform.
sequencer: u8,
duty_cycle: u8,
length_counter: u8,
length_counter_halt: bool,
envelope: Envelope,
}

impl PulseGenerator {
fn set_enabled(&mut self, enabled: bool) {
if !enabled {
self.length_counter = 0;
}
self.enabled = enabled;
}

fn write_flags(&mut self, value: u8) {
let flags = PulseFlags::from_bits_truncate(value);
self.duty_cycle = flags.bits() >> 6;
self.length_counter_halt = flags.contains(PulseFlags::LENGTH_COUNTER_HALT);
self.envelope.constant_volume = flags.contains(PulseFlags::CONSTANT_VOLUME);
self.envelope.volume = (flags & PulseFlags::VOLUME).bits();
}

fn write_timer(&mut self, value: u8) {
// Set the low bits of the timer
self.timer_initial = (self.timer_initial & 0xFF00) | value as u16;
}

fn write_length(&mut self, value: u8) {
// Set the high bits of the timer
let value = Length::from_bits_truncate(value);
let timer_high = (value & Length::TIMER_HIGH).bits();
self.timer_initial = (self.timer_initial & 0x00FF) | ((timer_high as u16) << 8);
let length_index = (value & Length::LENGTH_COUNTER).bits() >> 3;

if self.enabled {
self.length_counter = LENGTH_COUNTER_TABLE[length_index as usize];
}

self.sequencer = 0;
self.envelope.start = true;
}

fn halted(&self) -> bool {
self.length_counter == 0
}

// Low-frequency clock to stop sound after a certain time
fn clock_length_counter(&mut self) {
if self.length_counter > 0 && !self.length_counter_halt {
self.length_counter -= 1;
}
}

// High-frequency tick to control waveform generation
fn tick(&mut self) -> u8 {
let playing = !self.halted();
let volume = self.envelope.volume();
let waveform = PULSE_DUTY_WAVEFORM[self.duty_cycle as usize];
let value = (waveform.rotate_right(self.sequencer as u32) & 0b1) * volume * playing as u8;

if self.timer == 0 {
self.timer = self.timer_initial;
self.sequencer = self.sequencer.wrapping_add(1);
} else {
self.timer -= 1;
}

value
}
}

#[derive(Default)]
// An envelope changes a sound's volume over time.
// In the NES APU, it can set a constant volume or a decay.
struct Envelope {
constant_volume: bool,
looping: bool,
start: bool,
divider: u8,
decay_level: u8,
volume: u8,
}

impl Envelope {
fn clock(&mut self) {
if !self.start {
self.clock_divider();
} else {
self.start = false;
self.decay_level = 15;
self.divider = self.volume;
}
}

fn clock_divider(&mut self) {
if self.divider == 0 {
self.divider = self.volume;
if self.decay_level > 0 {
self.decay_level -= 1;
} else if self.looping {
self.decay_level = 15;
}
} else {
self.divider -= 1;
}
}

fn volume(&self) -> u8 {
if self.constant_volume {
self.volume
} else {
self.decay_level
}
}
}

bitflags! {
struct Status: u8 {
const PULSE_1 = 0b0000_0001;
Expand Down Expand Up @@ -239,13 +116,6 @@ bitflags! {
}
}

const PULSE_DUTY_WAVEFORM: [u8; 4] = [
0b00000010, // 12.5% duty cycle
0b00000110, // 25% duty cycle
0b00011110, // 50% duty cycle
0b11111001, // 25% negated duty cycle
];

// I swear, there's a pattern here:
// https://www.nesdev.org/wiki/APU_Length_Counter
#[cfg_attr(any(), rustfmt::skip)]
Expand All @@ -269,48 +139,3 @@ const LENGTH_COUNTER_TABLE: [u8; 32] = [
16, /* trip. quaver */ 28,
32, /* trip. crotchet */ 30,
];

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_pulse_generator_produces_rectangle_wave() {
let mut pulse = PulseGenerator {
enabled: true,
timer_initial: 8,
timer: 8,
sequencer: 0,
length_counter: 5,
length_counter_halt: false,
// Set duty to 25% and volume goes up to 11
duty_cycle: 1,
envelope: Envelope {
constant_volume: true,
looping: false,
start: false,
divider: 0,
decay_level: 0,
volume: 11,
},
};

// Get two periods of the waveform
let wave: Vec<u8> = std::iter::repeat_with(|| pulse.tick())
.take(9 * 16)
.collect();

// Each part of wave is repeated `timer + 1 = 9` times
assert_eq!(
wave,
[
vec![0; 9],
vec![11; 2 * 9],
vec![0; 6 * 9],
vec![11; 2 * 9],
vec![0; 5 * 9]
]
.concat()
);
}
}
Loading

0 comments on commit 935d0ed

Please sign in to comment.