Skip to main content

secs_project/
dsp_profile.rs

1use crate::pyjson::Shape;
2use crate::{ProjectError, PyJson, SpeakerConfig};
3use std::collections::BTreeMap;
4
5/// The `dsp` object [`DspProfile::to_json`] writes (F16).
6pub(crate) static DSP_SHAPE: Shape = Shape::Record {
7    optional: &[],
8    nested: &[],
9};
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct DspProfile {
13    pub profile: String,
14    pub fir_taps: i64,
15    pub fir_budget: i64,
16    pub iir_bands: i64,
17    pub support_mode: String,
18    pub support_taps: i64,
19    pub allpass_bands: i64,
20    pub bm_path_delays: i64,
21    pub bm_path_polarity: bool,
22    pub bm_path_allpass: i64,
23    pub path_peq_bands: i64,
24    pub path_allpass_bands: i64,
25    pub per_output_lp: bool,
26    pub fir_taps_max: i64,
27    pub fir_taps_auto: Option<bool>,
28    pub support_taps_auto: Option<bool>,
29    pub name: String,
30}
31impl Default for DspProfile {
32    fn default() -> Self {
33        Self {
34            profile: "tesira".into(),
35            fir_taps: 2048,
36            fir_budget: 32768,
37            iir_bands: 16,
38            support_mode: "iir".into(),
39            support_taps: 1024,
40            allpass_bands: 16,
41            bm_path_delays: 16,
42            bm_path_polarity: false,
43            bm_path_allpass: 0,
44            path_peq_bands: 0,
45            path_allpass_bands: 0,
46            per_output_lp: false,
47            fir_taps_max: 2048,
48            fir_taps_auto: Some(true),
49            support_taps_auto: Some(true),
50            name: String::new(),
51        }
52    }
53}
54impl DspProfile {
55    pub fn presets() -> BTreeMap<&'static str, Self> {
56        let rows = [
57            (
58                "camilla",
59                "PC · CamillaDSP",
60                65536,
61                65536 * 32,
62                64,
63                64,
64                64,
65                true,
66                4,
67                16,
68                8,
69                true,
70            ),
71            (
72                "tesira",
73                "Biamp Tesira SERVER (DSP-2 ×3)",
74                2048,
75                51200,
76                16,
77                16,
78                16,
79                false,
80                0,
81                0,
82                0,
83                false,
84            ),
85            (
86                "qsys",
87                "Q-SYS Core 8 Flex / 110f",
88                16384,
89                65536,
90                24,
91                16,
92                16,
93                true,
94                2,
95                4,
96                2,
97                true,
98            ),
99            (
100                "minidsp",
101                "miniDSP Flex / 2x4 HD",
102                2048,
103                4096,
104                10,
105                0,
106                0,
107                false,
108                0,
109                0,
110                0,
111                false,
112            ),
113            (
114                "custom",
115                "사용자 정의",
116                2048,
117                32768,
118                16,
119                16,
120                16,
121                false,
122                0,
123                0,
124                0,
125                false,
126            ),
127        ];
128        rows.into_iter()
129            .map(
130                |(key, name, taps, budget, iir, ap, bmd, pol, bmap, ppeq, pap, lp)| {
131                    (
132                        key,
133                        Self {
134                            profile: key.into(),
135                            fir_taps: taps,
136                            fir_taps_max: taps,
137                            fir_budget: budget,
138                            iir_bands: iir,
139                            support_mode: "iir".into(),
140                            support_taps: 1024,
141                            allpass_bands: ap,
142                            bm_path_delays: bmd,
143                            bm_path_polarity: pol,
144                            bm_path_allpass: bmap,
145                            path_peq_bands: ppeq,
146                            path_allpass_bands: pap,
147                            per_output_lp: lp,
148                            fir_taps_auto: Some(true),
149                            support_taps_auto: Some(true),
150                            name: name.into(),
151                        },
152                    )
153                },
154            )
155            .collect()
156    }
157    pub fn allocate_taps(
158        &self,
159        config: &SpeakerConfig,
160        phase: &str,
161        path_count: i64,
162        min_taps: i64,
163    ) -> (i64, i64) {
164        let bands = config
165            .speakers
166            .iter()
167            .map(|s| s.bands.len() as i64)
168            .sum::<i64>()
169            .max(1);
170        let chan = if phase == "mixed" { bands } else { 0 };
171        let sup = if ["hybrid", "fir"].contains(&self.support_mode.as_str()) {
172            path_count
173        } else {
174            0
175        };
176        let consumers = chan + sup;
177        if consumers <= 0 {
178            return (self.fir_taps, self.support_taps);
179        }
180        let ceiling = self.fir_taps_max.max(min_taps);
181        let ceiling_pow = 1_i64 << (63 - ceiling.leading_zeros());
182        let target = self.fir_budget / consumers;
183        let mut taps = if target >= 1 {
184            1_i64 << (63 - target.leading_zeros())
185        } else {
186            min_taps
187        };
188        taps = taps.max(min_taps).min(ceiling_pow);
189        while taps > min_taps && taps * consumers > self.fir_budget {
190            taps /= 2;
191        }
192        (
193            if chan > 0 { taps } else { self.fir_taps },
194            if sup > 0 { taps } else { self.support_taps },
195        )
196    }
197    pub fn to_json(&self) -> PyJson {
198        PyJson::Object(vec![
199            ("profile".into(), PyJson::Str(self.profile.clone())),
200            ("fir_taps".into(), PyJson::Int(self.fir_taps.to_string())),
201            (
202                "fir_taps_max".into(),
203                PyJson::Int(self.fir_taps_max.to_string()),
204            ),
205            (
206                "fir_budget".into(),
207                PyJson::Int(self.fir_budget.to_string()),
208            ),
209            ("iir_bands".into(), PyJson::Int(self.iir_bands.to_string())),
210            (
211                "support_mode".into(),
212                PyJson::Str(self.support_mode.clone()),
213            ),
214            (
215                "support_taps".into(),
216                PyJson::Int(self.support_taps.to_string()),
217            ),
218            (
219                "allpass_bands".into(),
220                PyJson::Int(self.allpass_bands.to_string()),
221            ),
222            (
223                "bm_path_delays".into(),
224                PyJson::Int(self.bm_path_delays.to_string()),
225            ),
226            (
227                "bm_path_polarity".into(),
228                PyJson::Bool(self.bm_path_polarity),
229            ),
230            (
231                "bm_path_allpass".into(),
232                PyJson::Int(self.bm_path_allpass.to_string()),
233            ),
234            (
235                "path_peq_bands".into(),
236                PyJson::Int(self.path_peq_bands.to_string()),
237            ),
238            (
239                "path_allpass_bands".into(),
240                PyJson::Int(self.path_allpass_bands.to_string()),
241            ),
242            ("per_output_lp".into(), PyJson::Bool(self.per_output_lp)),
243            (
244                "fir_taps_auto".into(),
245                PyJson::Bool(self.fir_taps_auto.unwrap_or(false)),
246            ),
247            (
248                "support_taps_auto".into(),
249                PyJson::Bool(self.support_taps_auto.unwrap_or(false)),
250            ),
251            ("name".into(), PyJson::Str(self.name.clone())),
252        ])
253    }
254    pub fn from_json(value: PyJson) -> Result<Self, ProjectError> {
255        let object = value
256            .object()
257            .ok_or_else(|| ProjectError::Invalid("dsp must be an object".into()))?;
258        let get = |key: &str| {
259            object
260                .iter()
261                .find(|(name, _)| name == key)
262                .map(|(_, value)| value)
263        };
264        let profile = crate::project::string(get("profile"), "tesira");
265        let preset = Self::presets().remove(profile.as_str());
266        let fir_taps = crate::project::integer(get("fir_taps"), 2048, "dsp.fir_taps")?;
267        let fallback = preset.as_ref().cloned().unwrap_or_default();
268        Ok(Self {
269            profile,
270            fir_taps,
271            fir_taps_max: crate::project::integer(
272                get("fir_taps_max"),
273                preset.as_ref().map_or(fir_taps, |v| v.fir_taps_max),
274                "dsp.fir_taps_max",
275            )?,
276            fir_budget: crate::project::integer(get("fir_budget"), 32768, "dsp.fir_budget")?,
277            iir_bands: crate::project::integer(get("iir_bands"), 16, "dsp.iir_bands")?,
278            support_mode: crate::project::string(get("support_mode"), "iir"),
279            support_taps: crate::project::integer(get("support_taps"), 1024, "dsp.support_taps")?,
280            allpass_bands: crate::project::integer(
281                get("allpass_bands"),
282                fallback.allpass_bands,
283                "dsp.allpass_bands",
284            )?,
285            bm_path_delays: crate::project::integer(
286                get("bm_path_delays"),
287                fallback.bm_path_delays,
288                "dsp.bm_path_delays",
289            )?,
290            fir_taps_auto: crate::project::optional_bool(get("fir_taps_auto")),
291            support_taps_auto: crate::project::optional_bool(get("support_taps_auto")),
292            bm_path_polarity: get("bm_path_polarity")
293                .and_then(PyJson::bool)
294                .unwrap_or(fallback.bm_path_polarity),
295            bm_path_allpass: crate::project::integer(
296                get("bm_path_allpass"),
297                fallback.bm_path_allpass,
298                "dsp.bm_path_allpass",
299            )?,
300            path_peq_bands: crate::project::integer(
301                get("path_peq_bands"),
302                fallback.path_peq_bands,
303                "dsp.path_peq_bands",
304            )?,
305            path_allpass_bands: crate::project::integer(
306                get("path_allpass_bands"),
307                fallback.path_allpass_bands,
308                "dsp.path_allpass_bands",
309            )?,
310            per_output_lp: get("per_output_lp")
311                .and_then(PyJson::bool)
312                .unwrap_or(fallback.per_output_lp),
313            name: crate::project::string(get("name"), ""),
314        })
315    }
316
317    pub fn fir_used(
318        &self,
319        config: &SpeakerConfig,
320        phase: &str,
321        path_count: i64,
322    ) -> BTreeMap<&'static str, i64> {
323        let bands = config
324            .speakers
325            .iter()
326            .map(|s| s.bands.len() as i64)
327            .sum::<i64>();
328        let chan = if phase == "minphase" {
329            0
330        } else {
331            bands * self.fir_taps
332        };
333        let sup = if self.support_mode == "iir" {
334            0
335        } else {
336            path_count * self.support_taps
337        };
338        [("chan", chan), ("sup", sup), ("total", chan + sup)]
339            .into_iter()
340            .collect()
341    }
342}