1use crate::pyjson::Shape;
2use crate::{ProjectError, PyJson, SpeakerConfig};
3use std::collections::BTreeMap;
4
5pub(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}