feat(audio): implement nominal Direct Sound playback and FIFO DMA
This commit is contained in:
parent
894f524cda
commit
5888c4d292
21 changed files with 933 additions and 129 deletions
219
crates/gba-core/src/audio.rs
Normal file
219
crates/gba-core/src/audio.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
//! Nominal Direct Sound device state. No host audio or PSG synthesis.
|
||||
|
||||
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],
|
||||
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
|
||||
&& matches!(
|
||||
address,
|
||||
0x0400_0065 | 0x0400_006d | 0x0400_0075 | 0x0400_007d
|
||||
)
|
||||
&& 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)
|
||||
}
|
||||
|
||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||
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_0060 => 0x007f,
|
||||
0x0400_0062 | 0x0400_0068 => 0xffc0,
|
||||
0x0400_0064 | 0x0400_006c | 0x0400_0074 => 0x4000,
|
||||
0x0400_0070 => 0x00e0,
|
||||
0x0400_0072 => 0xe000,
|
||||
0x0400_0078 => 0xff00,
|
||||
0x0400_007c => 0x40ff,
|
||||
0x0400_0080 => 0xff77,
|
||||
_ => 0,
|
||||
};
|
||||
self.psg[((address - SOUND_START) / 2) as usize] & mask
|
||||
}
|
||||
SOUNDCNT_H => self.control,
|
||||
SOUNDCNT_X => u16::from(self.enabled) << 7,
|
||||
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;
|
||||
}
|
||||
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];
|
||||
for fifo in &mut self.fifo {
|
||||
fifo.reset_queue();
|
||||
}
|
||||
}
|
||||
self.enabled = value & 0x80 != 0;
|
||||
}
|
||||
SOUNDBIAS => self.bias = merge(self.bias) & 0xc3fe,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
for level in &mut sides {
|
||||
*level = (*level + (self.bias & 0x3ff) as i16).clamp(0, 1023) - 512;
|
||||
}
|
||||
StereoLevel {
|
||||
left: sides[1],
|
||||
right: sides[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue