1//! Sega CD audio resampling, filtering, and mixing code 2//! 3//! Reuses some resampling/filtering code from [`genesis_core::audio`] 4 5use bincode::{Decode, Encode}; 6use dsp::design::FilterType; 7use dsp::iir::{FirstOrderIirFilter, IirFilter, SecondOrderIirFilter}; 8use genesis_config::{GenesisEmulatorConfig, LowPassSettings}; 9 10pub const SEGA_CD_MCLK_FREQUENCY: f64 = 50_000_000.0; 11pub const PCM_FREQUENCY: f64 = SEGA_CD_MCLK_FREQUENCY / 4.0 / 384.0; 12pub const CD_DA_FREQUENCY: f64 = 44_100.0; 13 14fn new_pcm_low_pass<const N: usize>(cutoff: f64) -> IirFilter<N> { 15 dsp::design::butterworth(cutoff, SEGA_CD_MCLK_FREQUENCY / 4.0 / 384.0, FilterType::LowPass) 16} 17 18fn new_cd_da_low_pass(cutoff: f64) -> FirstOrderIirFilter { 19 dsp::design::butterworth(cutoff, CD_DA_FREQUENCY, FilterType::LowPass) 20} 21 22#[derive(Debug, Clone, Encode, Decode)] 23pub struct SegaCdAudioFilter { 24 gen_low_pass_setting: LowPassSettings, 25 pcm_lpf_enabled: bool, 26 pcm_lpf_cutoff: u32, 27 apply_gen_lpf_to_pcm: bool, 28 apply_gen_lpf_to_cd_da: bool, 29 pcm_lpf_l: SecondOrderIirFilter, 30 pcm_lpf_r: SecondOrderIirFilter, 31 pcm_gen_lpf_l: FirstOrderIirFilter, 32 pcm_gen_lpf_r: FirstOrderIirFilter, 33 cd_da_gen_lpf_l: FirstOrderIirFilter, 34 cd_da_gen_lpf_r: FirstOrderIirFilter, 35} 36 37impl SegaCdAudioFilter { 38 #[must_use] 39 pub fn new(config: &GenesisEmulatorConfig) -> Self { 40 let gen_cutoff: f64 = config.genesis_lpf_cutoff.into(); 41 let pcm_cutoff: f64 = config.sega_cd.pcm_lpf_cutoff.into(); 42 43 Self { 44 gen_low_pass_setting: LowPassSettings::from_config(config), 45 pcm_lpf_enabled: config.sega_cd.pcm_lpf_enabled, 46 pcm_lpf_cutoff: config.sega_cd.pcm_lpf_cutoff, 47 apply_gen_lpf_to_pcm: config.sega_cd.apply_genesis_lpf_to_pcm, 48 apply_gen_lpf_to_cd_da: config.sega_cd.apply_genesis_lpf_to_cd_da, 49 pcm_lpf_l: new_pcm_low_pass(pcm_cutoff), 50 pcm_lpf_r: new_pcm_low_pass(pcm_cutoff), 51 pcm_gen_lpf_l: new_pcm_low_pass(gen_cutoff), 52 pcm_gen_lpf_r: new_pcm_low_pass(gen_cutoff), 53 cd_da_gen_lpf_l: new_cd_da_low_pass(gen_cutoff), 54 cd_da_gen_lpf_r: new_cd_da_low_pass(gen_cutoff), 55 } 56 } 57 58 #[must_use] 59 pub fn filter_pcm(&mut self, (mut sample_l, mut sample_r): (f64, f64)) -> (f64, f64) { 60 if self.pcm_lpf_enabled { 61 sample_l = self.pcm_lpf_l.filter(sample_l); 62 sample_r = self.pcm_lpf_r.filter(sample_r); 63 } 64 65 if self.gen_low_pass_setting.genesis_enabled && self.apply_gen_lpf_to_pcm { 66 sample_l = self.pcm_gen_lpf_l.filter(sample_l); 67 sample_r = self.pcm_gen_lpf_r.filter(sample_r); 68 } 69 70 (sample_l, sample_r) 71 } 72 73 #[must_use] 74 pub fn filter_cd_da(&mut self, (sample_l, sample_r): (f64, f64)) -> (f64, f64) { 75 if !self.gen_low_pass_setting.genesis_enabled || !self.apply_gen_lpf_to_cd_da { 76 return (sample_l, sample_r); 77 } 78 79 (self.cd_da_gen_lpf_l.filter(sample_l), self.cd_da_gen_lpf_r.filter(sample_r)) 80 } 81 82 pub fn reload_config(&mut self, config: &GenesisEmulatorConfig) { 83 if self.gen_low_pass_setting == LowPassSettings::from_config(config) 84 && self.pcm_lpf_enabled == config.sega_cd.pcm_lpf_enabled 85 && self.pcm_lpf_cutoff == config.sega_cd.pcm_lpf_cutoff 86 && self.apply_gen_lpf_to_pcm == config.sega_cd.apply_genesis_lpf_to_pcm 87 && self.apply_gen_lpf_to_cd_da == config.sega_cd.apply_genesis_lpf_to_cd_da 88 { 89 return; 90 } 91 92 *self = Self::new(config); 93 } 94}