//! NES-authentic nonlinear DAC emulation. //! //! The NES 2A03 uses a nonlinear DAC. The output voltage is not linear //! with the digital value. This module emulates that characteristic. //! //! Reference: https://www.nesdev.org/wiki/APU_Mixer#Emulation //! The pulse channels use a nonlinear mix, and the combined output //! has a characteristic "crunchy" sound. /// NES-style nonlinear DAC emulation. /// /// The DAC maps linear float values [-1, 1] through a nonlinear curve /// that mimics the NES 2A03's analog output stage. pub struct NesDac { /// Lookup table for the nonlinear transfer function (256 entries). table: [f32; 256], } impl NesDac { pub fn new() -> Self { // Build nonlinear transfer table. // The NES DAC has a characteristic curve where: // - Near zero, output is more sensitive (steeper) // - Near extremes, output compresses (shallower) // We approximate with a tanh-like curve plus slight asymmetry. let mut table = [0.0f32; 256]; for i in 0..256u16 { let linear = (i as f32 / 127.5) - 1.0; // -1..1 // Nonlinear transfer: combination of tanh and cubic let tanh_part = linear.tanh(); // Add slight asymmetry (NES DAC is not perfectly symmetric) let asymmetric = 0.05 * linear * linear * linear; // Quantize to 8-bit levels (NES is 8-bit internally for mixed output) let quantized = ((tanh_part + asymmetric) * 63.0).round() / 63.0; table[i as usize] = quantized; } Self { table } } /// Process a sample through the nonlinear DAC. #[inline] pub fn process(&mut self, input: f32) -> f32 { let clamped = input.clamp(-1.0, 1.0); let idx = ((clamped + 1.0) * 127.5) as usize; self.table[idx.min(255)] } /// Process a buffer in-place. pub fn process_buffer(&mut self, samples: &mut [f32]) { for s in samples.iter_mut() { *s = self.process(*s); } } } impl Default for NesDac { fn default() -> Self { Self::new() } } /// NES hardware-accurate channel mixing. /// /// The NES mixes channels with specific relative weights: /// - Pulse 1 & 2: equal weight /// - Triangle: ~3x quieter than pulse (due to higher impedance) /// - Noise: same as pulse /// - DPCM: ~2x quieter than pulse /// /// The mix is also nonlinear: the combined output is not a simple sum. pub struct HardwareMixer { dac: NesDac, } impl HardwareMixer { pub fn new() -> Self { Self { dac: NesDac::new() } } /// Mix NES-style channels with hardware-accurate weights. /// /// - `pulse`: combined pulse output (already mixed) /// - `triangle`: triangle output /// - `noise`: noise output /// - `dpcm`: DPCM output #[inline] pub fn mix(&mut self, pulse: f32, triangle: f32, noise: f32, dpcm: f32) -> f32 { // NES mixing: nonlinear combination // Reference formula from nesdev.org: // output = 95.88 - (8128 / (pulse_sum + 244)) // for the pulse+triangle path // // Simplified for float [-1, 1]: let pulse_mix = pulse * 0.4; let tri_mix = triangle * 0.15; let noise_mix = noise * 0.4; let dpcm_mix = dpcm * 0.2; let mixed = pulse_mix + tri_mix + noise_mix + dpcm_mix; self.dac.process(mixed) } /// Process a buffer of pre-mixed samples through the DAC. pub fn process(&mut self, samples: &mut [f32]) { self.dac.process_buffer(samples); } } impl Default for HardwareMixer { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_dac_nonlinear() { let mut dac = NesDac::new(); // Linear input 0.5 should produce different output than 0.5 * 2 of input 0.25 let out_quarter = dac.process(0.25); let out_half = dac.process(0.5); // Nonlinear: 2 * out(0.25) != out(0.5) assert!( (2.0 * out_quarter - out_half).abs() > 0.01, "DAC should be nonlinear: 2*f(0.25)={}, f(0.5)={}", 2.0 * out_quarter, out_half ); } #[test] fn test_dac_zero() { let mut dac = NesDac::new(); let out = dac.process(0.0); assert!(out.abs() < 0.02, "DAC at zero should be near zero: {}", out); } #[test] fn test_dac_clamps() { let mut dac = NesDac::new(); let out_pos = dac.process(2.0); let out_neg = dac.process(-2.0); // Quantized to 63 levels, so max is 1.0 but might be slightly less assert!(out_pos >= 0.8 && out_pos <= 1.0, "pos: {}", out_pos); assert!(out_neg <= -0.8 && out_neg >= -1.0, "neg: {}", out_neg); } #[test] fn test_dac_quantization() { let mut dac = NesDac::new(); // Very small changes should be quantized away let out1 = dac.process(0.001); let out2 = dac.process(0.002); assert!( (out1 - out2).abs() < 0.001, "Small differences should be quantized" ); } #[test] fn test_hardware_mixer() { let mut mixer = HardwareMixer::new(); let out = mixer.mix(0.5, 0.3, 0.2, 0.1); assert!(out.abs() > 0.0); assert!(out <= 1.0); } #[test] fn test_hardware_mixer_triangle_quieter() { let mut mixer = HardwareMixer::new(); let out_pulse = mixer.mix(1.0, 0.0, 0.0, 0.0); let out_triangle = mixer.mix(0.0, 1.0, 0.0, 0.0); // Triangle should be quieter than pulse assert!( out_triangle.abs() < out_pulse.abs(), "Triangle should be quieter: tri={}, pulse={}", out_triangle, out_pulse ); } #[test] fn test_dac_process_buffer() { let mut dac = NesDac::new(); let mut samples = vec![0.0, 0.5, -0.5, 1.0, -1.0]; dac.process_buffer(&mut samples); // Should still be in valid range for s in &samples { assert!(s.abs() <= 1.0); } } }