feat(mixer): 5-channel APU mixing with linear approximation formula
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::native_core::apu::ChannelOutputs;
|
||||
use crate::runtime::VideoMode;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -25,15 +26,53 @@ impl AudioMixer {
|
||||
self.sample_accumulator = 0.0;
|
||||
}
|
||||
|
||||
pub fn push_cycles(&mut self, cpu_cycles: u8, apu_regs: &[u8; 0x20], out: &mut Vec<f32>) {
|
||||
pub fn push_cycles(&mut self, cpu_cycles: u8, channels: ChannelOutputs, out: &mut Vec<f32>) {
|
||||
self.sample_accumulator += self.samples_per_cpu_cycle * f64::from(cpu_cycles);
|
||||
let samples = self.sample_accumulator.floor() as usize;
|
||||
self.sample_accumulator -= samples as f64;
|
||||
|
||||
// Current core does not expose a final mixed PCM stream yet.
|
||||
// Use DMC output level as a stable interim signal in [-1.0, 1.0].
|
||||
let dmc = apu_regs[0x11] & 0x7F;
|
||||
let sample = (f32::from(dmc) / 63.5) - 1.0;
|
||||
let pulse_out = 0.00752 * (f32::from(channels.pulse1) + f32::from(channels.pulse2));
|
||||
let tnd_out = 0.00851 * f32::from(channels.triangle)
|
||||
+ 0.00494 * f32::from(channels.noise)
|
||||
+ 0.00335 * f32::from(channels.dmc);
|
||||
let mixed = pulse_out + tnd_out;
|
||||
let sample = mixed * 2.0 - 1.0;
|
||||
|
||||
out.extend(std::iter::repeat_n(sample, samples));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mixer_silent_channels_produce_negative_one() {
|
||||
let mut mixer = AudioMixer::new(44_100, VideoMode::Ntsc);
|
||||
let channels = ChannelOutputs::default();
|
||||
let mut out = Vec::new();
|
||||
mixer.push_cycles(50, channels, &mut out);
|
||||
assert!(!out.is_empty());
|
||||
for &s in &out {
|
||||
assert!((s - (-1.0)).abs() < 1e-6, "expected -1.0, got {s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixer_max_channels_produce_positive() {
|
||||
let mut mixer = AudioMixer::new(44_100, VideoMode::Ntsc);
|
||||
let channels = ChannelOutputs {
|
||||
pulse1: 15,
|
||||
pulse2: 15,
|
||||
triangle: 15,
|
||||
noise: 15,
|
||||
dmc: 127,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
mixer.push_cycles(50, channels, &mut out);
|
||||
assert!(!out.is_empty());
|
||||
for &s in &out {
|
||||
assert!(s > 0.0, "expected positive sample, got {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ impl NesRuntime {
|
||||
self.bus.begin_frame();
|
||||
while !self.bus.take_frame_complete() {
|
||||
let cycles = self.step_instruction()?;
|
||||
mixer.push_cycles(cycles, self.bus.apu_registers(), out_samples);
|
||||
mixer.push_cycles(cycles, self.bus.apu_channel_outputs(), out_samples);
|
||||
}
|
||||
self.frame_number = self.frame_number.saturating_add(1);
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user