//! Nominal Direct Sound, PSG pulse channels 1–2, and noise channel 4. No host audio output. mod modulation; mod noise; mod pulse; use noise::Noise; use pulse::Pulse; const SEQUENCER_PERIOD: u32 = 32768; // 16,777,216 Hz / 512 Hz. pub const SOUND_START: u32 = 0x0400_0060; pub const SOUNDCNT_H: u32 = 0x0400_0082; pub const SOUNDCNT_X: u32 = 0x0400_0084; pub const SOUNDBIAS: u32 = 0x0400_0088; pub const WAVE_RAM: u32 = 0x0400_0090; pub const FIFO_A: u32 = 0x0400_00a0; pub const FIFO_B: u32 = 0x0400_00a4; /// Instantaneous digital mixer levels, not a sampled audio stream. /// Bias is applied, clipped to ten bits, then centered at 512: -512..=511. /// Master disable returns zero. PWM quantization and analog filtering are absent. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct StereoLevel { pub left: i16, pub right: i16, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] struct Fifo { words: [u32; 7], read: usize, len: usize, playback: u32, remaining: u8, sample: i8, } impl Fifo { fn reset_queue(&mut self) { self.words = [0; 7]; self.read = 0; self.len = 0; } fn write(&mut self, address: u32, bytes: &[u8]) { if self.len == 7 { self.reset_queue(); return; } let slot = (self.read + self.len) % 7; for (offset, &byte) in bytes.iter().enumerate() { let shift = ((address & 3) as usize + offset) * 8; self.words[slot] = (self.words[slot] & !(255 << shift)) | (u32::from(byte) << shift); } self.len += 1; } fn clock(&mut self, overflows: u64) -> bool { let mut request = false; // At most 32 queued/in-flight bytes, then one empty sample. This bounds // device-only advances even with a timer overflow on every clock. for _ in 0..overflows.min(33) { request |= self.len <= 3; if self.remaining == 0 && self.len != 0 { self.playback = self.words[self.read]; self.read = (self.read + 1) % 7; self.len -= 1; self.remaining = 4; } self.sample = self.playback as i8; if self.remaining != 0 { self.playback >>= 8; self.remaining -= 1; } } request } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub(crate) struct Audio { enabled: bool, psg: [u16; 17], pulses: [Pulse; 2], noise: Noise, sequencer_phase: u16, next_step: u8, control: u16, bias: u16, wave: [[u8; 16]; 2], fifo: [Fifo; 2], requests: [bool; 2], } impl Audio { pub(crate) fn mapped(address: u32) -> bool { matches!(address, SOUND_START..=0x0400_008b | WAVE_RAM..=0x0400_00a7) } pub(crate) fn unsupported(&self, address: u32, value: u8) -> Option<&'static str> { if (self.enabled && address == SOUNDCNT_H && value & 3 == 3) || (address == SOUNDCNT_X && value & 0x80 != 0 && self.control & 3 == 3) { return Some("reserved PSG volume selection"); } if self.enabled && address == 0x0400_0075 && value & 0x80 != 0 { Some("PSG channel trigger (synthesis not implemented)") } else { None } } fn wave_bank(&self) -> usize { 1 ^ usize::from((self.psg[8] >> 6) & 1) } /// Channel 2 has no sweep register. Its register gap must not alias any /// pulse state, particularly the shared engine's sweep configuration. fn pulse_register(address: u32) -> Option<(usize, u32)> { match address { SOUND_START..=0x0400_0065 => Some((0, address - SOUND_START)), 0x0400_0068..=0x0400_0069 => Some((1, address - 0x0400_0066)), 0x0400_006c..=0x0400_006d => Some((1, address - 0x0400_0068)), _ => None, } } pub(crate) fn read8(&self, address: u32) -> Option { if (0x0400_0078..=0x0400_007d).contains(&address) { return Some(self.noise.read(address - 0x0400_0078)); } if let Some((index, offset)) = Self::pulse_register(address) { return Some(self.pulses[index].read(offset)); } if (WAVE_RAM..FIFO_A).contains(&address) { return Some(self.wave[self.wave_bank()][(address - WAVE_RAM) as usize]); } let value = match address & !1 { SOUND_START..=0x0400_0080 => { let mask = match address & !1 { 0x0400_0074 => 0x4000, 0x0400_0070 => 0x00e0, 0x0400_0072 => 0xe000, 0x0400_0080 => 0xff77, _ => 0, }; self.psg[((address - SOUND_START) / 2) as usize] & mask } SOUNDCNT_H => self.control, SOUNDCNT_X => { (u16::from(self.enabled) << 7) | u16::from(self.pulses[0].modulation.active) | (u16::from(self.pulses[1].modulation.active) << 1) | (u16::from(self.noise.modulation.active) << 3) } SOUNDBIAS => self.bias, 0x0400_0086 | 0x0400_008a => 0, _ => return None, // FIFO is write-only; open-bus readback is not modeled. }; Some(value.to_le_bytes()[(address & 1) as usize]) } /// One bus access. Partial FIFO writes advance a whole word, not one byte. pub(crate) fn write(&mut self, address: u32, bytes: &[u8]) { if (FIFO_A..=FIFO_B + 3).contains(&address) { self.fifo[((address - FIFO_A) / 4) as usize].write(address, bytes); return; } for (offset, &value) in bytes.iter().enumerate() { let address = address + offset as u32; if (WAVE_RAM..FIFO_A).contains(&address) { self.wave[self.wave_bank()][(address - WAVE_RAM) as usize] = value; continue; } if let Some((index, offset)) = Self::pulse_register(address) { if self.enabled { self.pulses[index].write(offset, value, self.next_step); } continue; } if (0x0400_0078..=0x0400_007d).contains(&address) { if self.enabled { self.noise .write(address - 0x0400_0078, value, self.next_step); } continue; } let shift = (address & 1) * 8; let merge = |old: u16| (old & !(255 << shift)) | (u16::from(value) << shift); match address & !1 { SOUND_START..=0x0400_0080 if self.enabled => { let register = &mut self.psg[((address - SOUND_START) / 2) as usize]; *register = merge(*register); } SOUNDCNT_H => { self.control = merge(self.control) & 0x770f; if address & 1 != 0 { for (index, mask) in [8, 128].into_iter().enumerate() { if value & mask != 0 { self.fifo[index].reset_queue(); } } } } SOUNDCNT_X if address & 1 == 0 => { if self.enabled && value & 0x80 == 0 { self.psg = [0; 17]; self.pulses = [Pulse::default(); 2]; self.noise = Noise::default(); for fifo in &mut self.fifo { fifo.reset_queue(); } } if !self.enabled && value & 0x80 != 0 { self.next_step = 0; } self.enabled = value & 0x80 != 0; } SOUNDBIAS => self.bias = merge(self.bias) & 0xc3fe, _ => {} } } } /// Free-running 512 Hz divider; master enable resets the sequencer step, /// not the divider phase. STOP is gated by the memory bus before this call. pub(crate) fn advance(&mut self, mut cycles: u32) { if !self.enabled { self.sequencer_phase = ((u64::from(self.sequencer_phase) + u64::from(cycles)) % u64::from(SEQUENCER_PERIOD)) as u16; return; } while cycles != 0 { let count = cycles.min(SEQUENCER_PERIOD - u32::from(self.sequencer_phase)); for pulse in &mut self.pulses { pulse.advance(count); } self.noise.advance(count); let phase = u32::from(self.sequencer_phase) + count; if phase == SEQUENCER_PERIOD { self.sequencer_phase = 0; for pulse in &mut self.pulses { pulse.clock(self.next_step); } self.noise.clock(self.next_step); self.next_step = (self.next_step + 1) & 7; } else { self.sequencer_phase = phase as u16; } cycles -= count; } } pub(crate) fn timer_overflows(&mut self, timer: usize, count: u64) { if !self.enabled || timer > 1 { return; } for index in 0..2 { let selected = usize::from((self.control >> (10 + index * 4)) & 1); if selected == timer { self.requests[index] |= self.fifo[index].clock(count); } } } pub(crate) fn take_requests(&mut self) -> [bool; 2] { std::mem::take(&mut self.requests) } pub(crate) fn level(&self) -> StereoLevel { if !self.enabled { return StereoLevel::default(); } let mut sides = [0i16; 2]; // right, left for index in 0..2 { let volume = if self.control & (4 << index) != 0 { 4 } else { 2 }; for (side, level) in sides.iter_mut().enumerate() { if self.control & (1 << (8 + index * 4 + side)) != 0 { *level += i16::from(self.fifo[index].sample) * volume; } } } let psg_control = self.psg[16]; let ratio = [1, 2, 4, 0][usize::from(self.control & 3)]; for (side, level) in sides.iter_mut().enumerate() { let mut psg_sample = 0; for (index, pulse) in self.pulses.iter().enumerate() { if psg_control & (1 << (8 + side * 4 + index)) != 0 { psg_sample += pulse.sample(); } } if psg_control & (1 << (11 + side * 4)) != 0 { psg_sample += self.noise.sample(); } let volume = ((psg_control >> (side * 4)) & 7) as i16 + 1; // Sum PSG channels before scaling, so fractional bits are discarded once. *level += (psg_sample * volume * ratio) >> 2; *level = (*level + (self.bias & 0x3ff) as i16).clamp(0, 1023) - 512; } StereoLevel { left: sides[1], right: sides[0], } } }