125 lines
3.5 KiB
Rust
125 lines
3.5 KiB
Rust
use crate::native_core::apu::ChannelOutputs;
|
|
use crate::runtime::VideoMode;
|
|
|
|
#[derive(Debug)]
|
|
pub struct AudioMixer {
|
|
sample_rate: u32,
|
|
samples_per_cpu_cycle: f64,
|
|
sample_accumulator: f64,
|
|
last_output_sample: f32,
|
|
}
|
|
|
|
impl AudioMixer {
|
|
pub fn new(sample_rate: u32, mode: VideoMode) -> Self {
|
|
let cpu_hz = mode.cpu_hz();
|
|
Self {
|
|
sample_rate,
|
|
samples_per_cpu_cycle: sample_rate as f64 / cpu_hz,
|
|
sample_accumulator: 0.0,
|
|
last_output_sample: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn sample_rate(&self) -> u32 {
|
|
self.sample_rate
|
|
}
|
|
|
|
pub fn reset(&mut self) {
|
|
self.sample_accumulator = 0.0;
|
|
self.last_output_sample = 0.0;
|
|
}
|
|
|
|
pub fn push_cycles(&mut self, cpu_cycles: u32, channels: ChannelOutputs, out: &mut Vec<f32>) {
|
|
self.sample_accumulator += self.samples_per_cpu_cycle * cpu_cycles as f64;
|
|
let samples = self.sample_accumulator.floor() as usize;
|
|
self.sample_accumulator -= samples as f64;
|
|
|
|
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 sample = pulse_out + tnd_out;
|
|
|
|
if samples == 0 {
|
|
return;
|
|
}
|
|
|
|
let start = self.last_output_sample;
|
|
if samples == 1 {
|
|
out.push(sample);
|
|
} else {
|
|
let denom = samples as f32;
|
|
for idx in 0..samples {
|
|
let t = (idx + 1) as f32 / denom;
|
|
out.push(start + (sample - start) * t);
|
|
}
|
|
}
|
|
self.last_output_sample = sample;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn mixer_silent_channels_produce_zero() {
|
|
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.abs() < 1e-6, "expected 0.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}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mixer_smooths_transition_between_batches() {
|
|
let mut mixer = AudioMixer::new(44_100, VideoMode::Ntsc);
|
|
let mut out = Vec::new();
|
|
|
|
mixer.push_cycles(200, ChannelOutputs::default(), &mut out);
|
|
let before = out.len();
|
|
|
|
mixer.push_cycles(
|
|
200,
|
|
ChannelOutputs {
|
|
pulse1: 15,
|
|
pulse2: 15,
|
|
triangle: 15,
|
|
noise: 15,
|
|
dmc: 127,
|
|
},
|
|
&mut out,
|
|
);
|
|
|
|
let transition = &out[before..];
|
|
assert!(transition.len() > 1);
|
|
assert!(transition[0] < *transition.last().expect("transition sample"));
|
|
assert!(
|
|
transition[0] > 0.0,
|
|
"expected smoothed ramp start, got {}",
|
|
transition[0]
|
|
);
|
|
}
|
|
}
|