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}