Skip to content

Commit

Permalink
Support pulse sweeping
Browse files Browse the repository at this point in the history
  • Loading branch information
nathsou committed Jul 12, 2023
1 parent d129375 commit e9ab9e6
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 48 deletions.
16 changes: 5 additions & 11 deletions crate/src/apu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use self::pulse::PulseChannel;

mod pulse;

const APU_BUFFER_SIZE: usize = 1024 * 8;
const APU_BUFFER_SIZE: usize = 8 * 1024;
const APU_BUFFER_MASK: u16 = APU_BUFFER_SIZE as u16 - 1;
const CPU_FREQ: f64 = 1789772.5;

Expand Down Expand Up @@ -118,16 +118,8 @@ impl APU {
0x4013 => {}
// Control
0x4015 => {
self.pulse1.enabled = val & 1 != 0;
self.pulse2.enabled = val & 2 != 0;

if !self.pulse1.enabled {
self.pulse1.length_counter = 0;
}

if !self.pulse2.enabled {
self.pulse2.length_counter = 0;
}
self.pulse1.set_enabled(val & 1 != 0);
self.pulse2.set_enabled(val & 2 != 0);
}
// Frame Counter
0x4017 => {
Expand Down Expand Up @@ -204,6 +196,8 @@ impl APU {
if half_frame {
self.pulse1.step_length_counter();
self.pulse2.step_length_counter();
self.pulse1.step_sweep();
self.pulse2.step_sweep();
}
}

Expand Down
118 changes: 82 additions & 36 deletions crate/src/apu/pulse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,40 @@ const DUTY_TABLE: [[u8; 8]; 4] = [
[1, 0, 0, 1, 1, 1, 1, 1],
];

#[derive(Default)]
pub struct PulseChannel {
pub enabled: bool,
pub duty_mode: u8,
pub duty_cycle: u8,
pub timer: u16,
pub timer_reload: u16,
pub length_counter: u8,
pub halt_length_counter: bool,
pub envelope_constant_mode: bool,
pub envelope_constant_volume: u8,
pub envelope_loop: bool,
pub envelope_start: bool,
pub envelope_period: u8,
pub envelope_divider: u8,
pub envelope_decay: u8,
enabled: bool,
duty_mode: u8,
duty_cycle: u8,
timer: u16,
timer_period: u16,
length_counter: u8,
halt_length_counter: bool,
envelope_constant_mode: bool,
envelope_constant_volume: u8,
envelope_loop: bool,
envelope_start: bool,
envelope_period: u8,
envelope_divider: u8,
envelope_decay: u8,
sweep_enabled: bool,
sweep_period: u8,
sweep_negate: bool,
sweep_shift: u8,
sweep_reload: bool,
sweep_divider: u8,
sweep_target_period: u16,
sweep_mute: bool,
}

impl PulseChannel {
pub fn new() -> Self {
Self {
enabled: false,
duty_mode: 0,
duty_cycle: 0,
timer: 0,
timer_reload: 0,
length_counter: 0,
halt_length_counter: false,
envelope_loop: false,
envelope_constant_mode: false,
envelope_start: false,
envelope_period: 0,
envelope_divider: 0,
envelope_decay: 0,
envelope_constant_volume: 0,
}
Self::default()
}

pub fn step(&mut self) {
if self.timer == 0 {
self.timer = self.timer_reload;
self.timer = self.timer_period;
self.duty_cycle = (self.duty_cycle + 1) & 7;
} else {
self.timer -= 1;
Expand Down Expand Up @@ -77,6 +71,14 @@ impl PulseChannel {
}
}

pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;

if !enabled {
self.length_counter = 0;
}
}

pub fn write_control(&mut self, val: u8) {
self.duty_mode = (val >> 6) & 0b11;
self.halt_length_counter = val & 0b0010_0000 != 0;
Expand All @@ -89,21 +91,65 @@ impl PulseChannel {

#[inline]
pub fn write_reload_low(&mut self, val: u8) {
self.timer_reload = (self.timer_reload & 0xFF00) | (val as u16);
self.timer_period = (self.timer_period & 0xFF00) | (val as u16);
}

#[inline]
pub fn write_reload_high(&mut self, val: u8) {
self.timer_reload = (self.timer_reload & 0x00FF) | (((val & 7) as u16) << 8);
self.timer_period = (self.timer_period & 0x00FF) | (((val & 7) as u16) << 8);
self.duty_cycle = 0;
self.envelope_start = true;
self.length_counter = LENGTH_LOOKUP[val as usize >> 3];
}

pub fn write_sweep(&mut self, val: u8) {
self.sweep_enabled = val & 0b1000_0000 != 0;
self.sweep_period = (val >> 4) & 0b111;
self.sweep_negate = val & 0b1000 != 0;
self.sweep_shift = val & 0b111;
self.sweep_reload = true;
}

fn sweep_target_period(&self) -> u16 {
let change_amount = self.timer_period >> self.sweep_shift;

if self.sweep_negate {
if change_amount > self.timer_period {
0
} else {
self.timer_period - change_amount
}
} else {
self.timer_period + change_amount
}
}

pub fn step_sweep(&mut self) {
// When the frame counter sends a half-frame clock (at 120 or 96 Hz), two things happen:
// If the divider's counter is zero, the sweep is enabled, and the sweep unit is not muting the channel: The pulse's period is set to the target period.

// If the divider's counter is zero or the reload flag is true: The divider counter is set to P and the reload flag is cleared. Otherwise, the divider counter is decremented.

// When the sweep unit is muting a pulse channel, the channel's current period remains unchanged, but the sweep unit's divider continues to count down and reload the divider's period as normal. Otherwise, if the enable flag is set and the shift count is non-zero, when the divider outputs a clock, the pulse channel's period is updated to the target period.
// If the shift count is zero, the pulse channel's period is never updated, but muting logic still applies.

let target_period = self.sweep_target_period();
self.sweep_mute = self.timer_period < 8 || target_period > 0x7FF;

if self.sweep_divider == 0 && self.sweep_enabled && !self.sweep_mute {
self.timer_period = target_period;
}

if self.sweep_divider == 0 || self.sweep_reload {
self.sweep_divider = self.sweep_period;
self.sweep_reload = false;
} else {
self.sweep_divider -= 1;
}
}

pub fn output(&self) -> u8 {
if !self.enabled
|| self.timer_reload < 8
|| self.timer_reload > 0x7FF
|| self.sweep_mute
|| self.length_counter == 0
|| DUTY_TABLE[self.duty_mode as usize][self.duty_cycle as usize] == 0
{
Expand Down
2 changes: 1 addition & 1 deletion web/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const SCALING_MODE_MAPPING: Record<StoreData['scalingMode'], HTMLCanvasElement['
async function setup() {
await init();
const store = await createStore();
const syncMode = SYNC_VIDEO;
const syncMode = SYNC_AUDIO;
const audioBufferSize = AUDIO_BUFFER_SIZE_MAPPING[syncMode];
const avoidUnderruns = syncMode === SYNC_BOTH;
const canvas = document.querySelector<HTMLCanvasElement>('#screen')!;
Expand Down

0 comments on commit e9ab9e6

Please sign in to comment.