refactor(audio): share PSG length and envelope state
This commit is contained in:
parent
aee3621513
commit
ca2134f23c
5 changed files with 153 additions and 123 deletions
|
|
@ -1,5 +1,6 @@
|
|||
//! Nominal Direct Sound and PSG pulse channels 1–2. No host audio output.
|
||||
|
||||
mod modulation;
|
||||
mod pulse;
|
||||
use pulse::Pulse;
|
||||
|
||||
|
|
@ -144,8 +145,8 @@ impl Audio {
|
|||
SOUNDCNT_H => self.control,
|
||||
SOUNDCNT_X => {
|
||||
(u16::from(self.enabled) << 7)
|
||||
| u16::from(self.pulses[0].active)
|
||||
| (u16::from(self.pulses[1].active) << 1)
|
||||
| u16::from(self.pulses[0].modulation.active)
|
||||
| (u16::from(self.pulses[1].modulation.active) << 1)
|
||||
}
|
||||
SOUNDBIAS => self.bias,
|
||||
0x0400_0086 | 0x0400_008a => 0,
|
||||
|
|
|
|||
98
crates/gba-core/src/audio/modulation.rs
Normal file
98
crates/gba-core/src/audio/modulation.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
//! Shared nominal PSG length, envelope, activity, and logical DAC gate.
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Modulation {
|
||||
pub(super) active: bool,
|
||||
pub(super) envelope: u8,
|
||||
pub(super) length_enabled: bool,
|
||||
pub(super) length: u8,
|
||||
pub(super) volume: u8,
|
||||
envelope_ticks: u8,
|
||||
envelope_running: bool,
|
||||
}
|
||||
|
||||
impl Modulation {
|
||||
pub(super) fn write_length(&mut self, value: u8) {
|
||||
self.length = 64 - (value & 63);
|
||||
}
|
||||
|
||||
fn dac_enabled(&self) -> bool {
|
||||
self.envelope & 0xf8 != 0
|
||||
}
|
||||
|
||||
fn envelope_can_run(&self) -> bool {
|
||||
self.envelope & 7 != 0
|
||||
&& if self.envelope & 8 != 0 {
|
||||
self.volume < 15
|
||||
} else {
|
||||
self.volume > 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn write_envelope(&mut self, value: u8) {
|
||||
let was_running = self.envelope_running;
|
||||
self.envelope = value;
|
||||
if !self.dac_enabled() {
|
||||
self.active = false;
|
||||
}
|
||||
self.envelope_running = self.envelope_can_run();
|
||||
if !was_running && self.envelope_running {
|
||||
self.envelope_ticks = value & 7;
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply length-enable edge rules and restart modulation when bit 7 is set.
|
||||
/// Return the trigger strobe even with the logical DAC gate disabled.
|
||||
pub(super) fn write_control(&mut self, value: u8, next_step: u8) -> bool {
|
||||
let extra_length_clock = next_step & 1 != 0;
|
||||
if !self.length_enabled && value & 0x40 != 0 && extra_length_clock {
|
||||
self.clock_length();
|
||||
}
|
||||
self.length_enabled = value & 0x40 != 0;
|
||||
if value & 0x80 == 0 {
|
||||
return false;
|
||||
}
|
||||
self.active = self.dac_enabled();
|
||||
if self.length == 0 {
|
||||
self.length = 64;
|
||||
if self.length_enabled && extra_length_clock {
|
||||
self.length -= 1;
|
||||
}
|
||||
}
|
||||
self.volume = self.envelope >> 4;
|
||||
self.envelope_ticks = if self.envelope & 7 == 0 {
|
||||
8
|
||||
} else {
|
||||
self.envelope & 7
|
||||
};
|
||||
self.envelope_running = self.envelope_can_run();
|
||||
true
|
||||
}
|
||||
|
||||
fn clock_length(&mut self) {
|
||||
if self.length != 0 {
|
||||
self.length -= 1;
|
||||
if self.length == 0 {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clock(&mut self, step: u8) {
|
||||
if step & 1 == 0 && self.length_enabled {
|
||||
self.clock_length();
|
||||
}
|
||||
if step == 7 && self.active && self.envelope_running {
|
||||
self.envelope_ticks -= 1;
|
||||
if self.envelope_ticks == 0 {
|
||||
if self.envelope & 8 != 0 {
|
||||
self.volume += 1;
|
||||
} else {
|
||||
self.volume -= 1;
|
||||
}
|
||||
self.envelope_ticks = self.envelope & 7;
|
||||
self.envelope_running = self.envelope_can_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,15 @@
|
|||
//! Shared nominal pulse oscillator and modulation units. No host-clock dependency.
|
||||
//! Shared nominal pulse oscillator and sweep unit. No host-clock dependency.
|
||||
//! Channel 2 never receives sweep-register writes, so its sweep remains disabled.
|
||||
use super::modulation::Modulation;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Pulse {
|
||||
pub(super) modulation: Modulation,
|
||||
sweep: u8,
|
||||
duty: u8,
|
||||
envelope: u8,
|
||||
frequency: u16,
|
||||
length_enabled: bool,
|
||||
length: u8,
|
||||
pub(super) active: bool,
|
||||
phase: u8,
|
||||
remaining: u32,
|
||||
volume: u8,
|
||||
envelope_ticks: u8,
|
||||
envelope_running: bool,
|
||||
sweep_shadow: u16,
|
||||
sweep_ticks: u8,
|
||||
sweep_running: bool,
|
||||
|
|
@ -26,8 +21,8 @@ impl Pulse {
|
|||
match offset {
|
||||
0 => self.sweep,
|
||||
2 => self.duty << 6,
|
||||
3 => self.envelope,
|
||||
5 => u8::from(self.length_enabled) << 6,
|
||||
3 => self.modulation.envelope,
|
||||
5 => u8::from(self.modulation.length_enabled) << 6,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -36,19 +31,6 @@ impl Pulse {
|
|||
16 * (2048 - u32::from(self.frequency))
|
||||
}
|
||||
|
||||
fn dac_enabled(&self) -> bool {
|
||||
self.envelope & 0xf8 != 0
|
||||
}
|
||||
|
||||
fn envelope_can_run(&self) -> bool {
|
||||
self.envelope & 7 != 0
|
||||
&& if self.envelope & 8 != 0 {
|
||||
self.volume < 15
|
||||
} else {
|
||||
self.volume > 0
|
||||
}
|
||||
}
|
||||
|
||||
fn sweep_period(&self) -> u8 {
|
||||
let period = (self.sweep >> 4) & 7;
|
||||
if period == 0 {
|
||||
|
|
@ -72,57 +54,28 @@ impl Pulse {
|
|||
match offset {
|
||||
0 => {
|
||||
if self.subtracted && self.sweep & 8 != 0 && value & 8 == 0 {
|
||||
self.active = false;
|
||||
self.modulation.active = false;
|
||||
}
|
||||
self.sweep = value & 0x7f;
|
||||
}
|
||||
2 => {
|
||||
self.duty = value >> 6;
|
||||
self.length = 64 - (value & 63);
|
||||
}
|
||||
3 => {
|
||||
let was_running = self.envelope_running;
|
||||
self.envelope = value;
|
||||
if !self.dac_enabled() {
|
||||
self.active = false;
|
||||
}
|
||||
self.envelope_running = self.envelope_can_run();
|
||||
if !was_running && self.envelope_running {
|
||||
self.envelope_ticks = value & 7;
|
||||
}
|
||||
self.modulation.write_length(value);
|
||||
}
|
||||
3 => self.modulation.write_envelope(value),
|
||||
4 => self.frequency = (self.frequency & 0x700) | u16::from(value),
|
||||
5 => {
|
||||
self.frequency = (self.frequency & 255) | (u16::from(value & 7) << 8);
|
||||
let extra_length_clock = next_step & 1 != 0;
|
||||
if !self.length_enabled && value & 0x40 != 0 && extra_length_clock {
|
||||
self.clock_length();
|
||||
}
|
||||
self.length_enabled = value & 0x40 != 0;
|
||||
if value & 0x80 != 0 {
|
||||
self.active = self.dac_enabled();
|
||||
if self.length == 0 {
|
||||
self.length = 64;
|
||||
if self.length_enabled && extra_length_clock {
|
||||
self.length -= 1;
|
||||
}
|
||||
}
|
||||
if self.modulation.write_control(value, next_step) {
|
||||
// Retrigger reloads the oscillator timer but preserves duty position.
|
||||
// Sub-divider alignment and hardware startup latency are not modeled.
|
||||
self.remaining = self.period();
|
||||
self.volume = self.envelope >> 4;
|
||||
self.envelope_ticks = if self.envelope & 7 == 0 {
|
||||
8
|
||||
} else {
|
||||
self.envelope & 7
|
||||
};
|
||||
self.envelope_running = self.envelope_can_run();
|
||||
self.sweep_shadow = self.frequency;
|
||||
self.sweep_ticks = self.sweep_period();
|
||||
self.sweep_running = self.sweep & 0x77 != 0;
|
||||
self.subtracted = false;
|
||||
if self.sweep & 7 != 0 && self.calculate_sweep() > 2047 {
|
||||
self.active = false;
|
||||
self.modulation.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +85,7 @@ impl Pulse {
|
|||
|
||||
/// Batch oscillator edges until the next modulation event or bus access.
|
||||
pub(super) fn advance(&mut self, cycles: u32) {
|
||||
if !self.active || cycles == 0 {
|
||||
if !self.modulation.active || cycles == 0 {
|
||||
return;
|
||||
}
|
||||
if cycles < self.remaining {
|
||||
|
|
@ -145,19 +98,8 @@ impl Pulse {
|
|||
self.remaining = period - rest % period;
|
||||
}
|
||||
|
||||
fn clock_length(&mut self) {
|
||||
if self.length != 0 {
|
||||
self.length -= 1;
|
||||
if self.length == 0 {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clock(&mut self, step: u8) {
|
||||
if step & 1 == 0 && self.length_enabled {
|
||||
self.clock_length();
|
||||
}
|
||||
self.modulation.clock(step);
|
||||
if step & 3 == 2 && self.sweep_running {
|
||||
self.sweep_ticks -= 1;
|
||||
if self.sweep_ticks == 0 {
|
||||
|
|
@ -165,41 +107,29 @@ impl Pulse {
|
|||
if self.sweep & 0x70 != 0 {
|
||||
let next = self.calculate_sweep();
|
||||
if next > 2047 {
|
||||
self.active = false;
|
||||
self.modulation.active = false;
|
||||
} else if self.sweep & 7 != 0 {
|
||||
self.frequency = next;
|
||||
self.sweep_shadow = next;
|
||||
if self.calculate_sweep() > 2047 {
|
||||
self.active = false;
|
||||
self.modulation.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step == 7 && self.active && self.envelope_running {
|
||||
self.envelope_ticks -= 1;
|
||||
if self.envelope_ticks == 0 {
|
||||
if self.envelope & 8 != 0 {
|
||||
self.volume += 1;
|
||||
} else {
|
||||
self.volume -= 1;
|
||||
}
|
||||
self.envelope_ticks = self.envelope & 7;
|
||||
self.envelope_running = self.envelope_can_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Signed pulse amplitude before PSG master volume and stereo routing.
|
||||
pub(super) fn sample(&self) -> i16 {
|
||||
if !self.active {
|
||||
if !self.modulation.active {
|
||||
return 0;
|
||||
}
|
||||
let high = [0x80u8, 0x81, 0xe1, 0x7e][usize::from(self.duty)] & (1 << self.phase) != 0;
|
||||
if high {
|
||||
i16::from(self.volume)
|
||||
i16::from(self.modulation.volume)
|
||||
} else {
|
||||
-i16::from(self.volume)
|
||||
-i16::from(self.modulation.volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,23 +59,23 @@ fn length_load_enable_extra_clock_expiry_and_zero_reload() {
|
|||
let mut pulse = start(1000, 2, 0xf0, 0);
|
||||
pulse.write(2, 63, 0); // One remaining length tick.
|
||||
pulse.clock(0); // Disabled length does not expire.
|
||||
assert!(pulse.active);
|
||||
assert!(pulse.modulation.active);
|
||||
pulse.write(5, 0x43, 1); // Enable before a step that does not clock length.
|
||||
assert_eq!(pulse.length, 0);
|
||||
assert!(!pulse.active);
|
||||
assert_eq!(pulse.modulation.length, 0);
|
||||
assert!(!pulse.modulation.active);
|
||||
pulse.write(5, 0xc3, 1); // Trigger with empty length reloads 63 in this phase.
|
||||
assert_eq!(pulse.length, 63);
|
||||
assert!(pulse.active);
|
||||
assert_eq!(pulse.modulation.length, 63);
|
||||
assert!(pulse.modulation.active);
|
||||
for _ in 0..62 {
|
||||
pulse.clock(0);
|
||||
}
|
||||
assert!(pulse.active);
|
||||
assert!(pulse.modulation.active);
|
||||
pulse.clock(0);
|
||||
assert!(!pulse.active);
|
||||
assert!(!pulse.modulation.active);
|
||||
pulse.clock(0);
|
||||
assert_eq!(pulse.length, 0);
|
||||
assert_eq!(pulse.modulation.length, 0);
|
||||
pulse.write(5, 0xc3, 0);
|
||||
assert_eq!(pulse.length, 64);
|
||||
assert_eq!(pulse.modulation.length, 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -83,11 +83,11 @@ fn simultaneous_length_enable_and_trigger_reload_after_extra_expiry() {
|
|||
let mut pulse = start(1000, 0, 0xf0, 0);
|
||||
pulse.write(2, 63, 0);
|
||||
pulse.write(5, 0xc3, 1);
|
||||
assert_eq!(pulse.length, 63);
|
||||
assert!(pulse.active);
|
||||
assert_eq!(pulse.modulation.length, 63);
|
||||
assert!(pulse.modulation.active);
|
||||
pulse.write(2, 62, 0);
|
||||
pulse.write(5, 0xc3, 1); // Already enabled: do not add another extra decrement.
|
||||
assert_eq!(pulse.length, 2);
|
||||
assert_eq!(pulse.modulation.length, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -101,23 +101,23 @@ fn envelopes_step_at_selected_period_saturate_and_do_not_clear_status() {
|
|||
} else {
|
||||
12u8.saturating_sub(tick)
|
||||
};
|
||||
assert_eq!(pulse.volume, expected);
|
||||
assert!(pulse.active);
|
||||
assert_eq!(pulse.modulation.volume, expected);
|
||||
assert!(pulse.modulation.active);
|
||||
}
|
||||
}
|
||||
let mut pulse = start(0, 1, 0x53, 0);
|
||||
pulse.clock(7);
|
||||
pulse.clock(7);
|
||||
assert_eq!(pulse.volume, 5);
|
||||
assert_eq!(pulse.modulation.volume, 5);
|
||||
pulse.clock(7);
|
||||
assert_eq!(pulse.volume, 4);
|
||||
assert_eq!(pulse.modulation.volume, 4);
|
||||
pulse.write(3, 0xa0, 0); // Period zero holds current volume until retrigger.
|
||||
for _ in 0..16 {
|
||||
pulse.clock(7);
|
||||
}
|
||||
assert_eq!(pulse.volume, 4);
|
||||
assert_eq!(pulse.modulation.volume, 4);
|
||||
pulse.write(5, 0x80, 0);
|
||||
assert_eq!(pulse.volume, 10);
|
||||
assert_eq!(pulse.modulation.volume, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -125,31 +125,31 @@ fn live_envelope_configuration_retains_volume_and_dac_off_requires_retrigger() {
|
|||
let mut pulse = start(0, 1, 0x52, 0);
|
||||
pulse.clock(7);
|
||||
pulse.write(3, 0xa9, 0); // Change direction/period, not current volume or remaining tick.
|
||||
assert_eq!(pulse.volume, 5);
|
||||
assert_eq!(pulse.modulation.volume, 5);
|
||||
pulse.clock(7);
|
||||
assert_eq!(pulse.volume, 6);
|
||||
assert_eq!(pulse.modulation.volume, 6);
|
||||
pulse.write(3, 0, 0);
|
||||
assert!(!pulse.active);
|
||||
assert!(!pulse.modulation.active);
|
||||
assert_eq!(pulse.sample(), 0);
|
||||
pulse.write(3, 0x08, 0); // Rising envelope with initial zero has DAC enabled.
|
||||
assert!(!pulse.active);
|
||||
assert!(!pulse.modulation.active);
|
||||
pulse.write(5, 0x80, 0);
|
||||
assert!(pulse.active);
|
||||
assert_eq!(pulse.volume, 0);
|
||||
assert!(pulse.modulation.active);
|
||||
assert_eq!(pulse.modulation.volume, 0);
|
||||
pulse.write(3, 7, 0); // Period alone cannot enable DAC.
|
||||
pulse.write(5, 0x80, 0);
|
||||
assert!(!pulse.active);
|
||||
assert!(!pulse.modulation.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweep_checks_trigger_overflow_and_second_calculation_after_update() {
|
||||
assert!(!start(1500, 1, 0xf0, 0x11).active);
|
||||
assert!(!start(1500, 1, 0xf0, 1).active); // Time zero still checks at trigger.
|
||||
assert!(!start(1500, 1, 0xf0, 0x11).modulation.active);
|
||||
assert!(!start(1500, 1, 0xf0, 1).modulation.active); // Time zero still checks at trigger.
|
||||
let mut pulse = start(1000, 1, 0xf0, 0x11);
|
||||
assert!(pulse.active);
|
||||
assert!(pulse.modulation.active);
|
||||
pulse.clock(2);
|
||||
assert_eq!(pulse.frequency, 1500);
|
||||
assert!(!pulse.active); // Next calculated 2250 disables before another sweep tick.
|
||||
assert!(!pulse.modulation.active); // Next calculated 2250 disables before another sweep tick.
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -173,17 +173,17 @@ fn sweep_zero_period_shift_zero_and_subtraction_direction_rules() {
|
|||
}
|
||||
assert_eq!(pulse.frequency, 500); // Time zero suppresses periodic changes.
|
||||
let mut pulse = start(1200, 1, 0xf0, 0x10);
|
||||
assert!(pulse.active); // Shift zero skips trigger-time overflow check.
|
||||
assert!(pulse.modulation.active); // Shift zero skips trigger-time overflow check.
|
||||
pulse.clock(2);
|
||||
assert!(!pulse.active); // Timed overflow check still applies.
|
||||
assert!(!pulse.modulation.active); // Timed overflow check still applies.
|
||||
let mut pulse = start(1000, 1, 0xf0, 0x19);
|
||||
pulse.clock(2);
|
||||
assert_eq!(pulse.frequency, 500);
|
||||
pulse.write(0, 0x11, 0);
|
||||
assert!(!pulse.active); // Clearing negate after subtraction disables the channel.
|
||||
assert!(!pulse.modulation.active); // Clearing negate after subtraction disables the channel.
|
||||
let mut pulse = start(1000, 1, 0xf0, 0x18);
|
||||
pulse.write(0, 0x10, 0); // No subtraction calculation has happened.
|
||||
assert!(pulse.active);
|
||||
assert!(pulse.modulation.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue