1use crate::buffer::AudioBuffer;
6use crate::ctx::OpCtx;
7use crate::error::{Error, Result};
8use crate::units::{self, Gain, Grid, SyncTime, Time, TimeRange};
9use serde::Serialize;
10use serde_json::{Map, Value};
11use std::collections::BTreeMap;
12use std::path::PathBuf;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ParamKind {
19 Flag,
21 Switch,
23 Int,
24 Float,
25 Str,
26 Path,
27 Time,
29 Range,
31 Gain,
33 Freq,
35 Ratio,
37 Percent,
39 Grid,
41 FreqRange,
43 Choice,
45 Pitch,
47 List,
49}
50
51impl ParamKind {
52 pub fn type_name(self) -> &'static str {
53 match self {
54 ParamKind::Flag => "flag",
55 ParamKind::Switch => "on|off",
56 ParamKind::Int => "integer",
57 ParamKind::Float => "number",
58 ParamKind::Str => "text",
59 ParamKind::Path => "path",
60 ParamKind::Time => "time",
61 ParamKind::Range => "range",
62 ParamKind::Gain => "gain",
63 ParamKind::Freq => "frequency",
64 ParamKind::Ratio => "ratio",
65 ParamKind::Percent => "percent",
66 ParamKind::Grid => "grid",
67 ParamKind::FreqRange => "frequency range",
68 ParamKind::Choice => "choice",
69 ParamKind::Pitch => "cents|semitones",
70 ParamKind::List => "list",
71 }
72 }
73
74 pub fn validate(self, raw: &str, choices: &[&str]) -> Result<()> {
76 if matches!(raw, "auto" | "off")
79 && matches!(
80 self,
81 ParamKind::Time
82 | ParamKind::Gain
83 | ParamKind::Freq
84 | ParamKind::Percent
85 | ParamKind::Ratio
86 | ParamKind::Pitch
87 | ParamKind::FreqRange
88 | ParamKind::Range
89 )
90 {
91 return Ok(());
92 }
93 match self {
94 ParamKind::Flag | ParamKind::Switch => parse_bool(raw).map(|_| ()),
95 ParamKind::Int => {
96 raw.trim().parse::<i64>().map(|_| ()).map_err(|_| Error::usage(format!("'{raw}' is not an integer")))
97 }
98 ParamKind::Float => {
99 raw.trim().parse::<f64>().map(|_| ()).map_err(|_| Error::usage(format!("'{raw}' is not a number")))
100 }
101 ParamKind::Str | ParamKind::Path | ParamKind::List => Ok(()),
102 ParamKind::Time => {
103 if raw.contains('/') {
104 Grid::parse(raw).map(|_| ())
105 } else {
106 Time::parse(raw).map(|_| ())
107 }
108 }
109 ParamKind::Range => TimeRange::parse(raw).map(|_| ()),
110 ParamKind::Gain => Gain::parse(raw).map(|_| ()),
111 ParamKind::Freq => units::parse_freq(raw).map(|_| ()),
112 ParamKind::Ratio => units::parse_ratio(raw).map(|_| ()),
113 ParamKind::Percent => units::parse_percent(raw).map(|_| ()),
114 ParamKind::Grid => {
115 if raw == "auto" || raw == "off" {
116 Ok(())
117 } else {
118 Grid::parse(raw).map(|_| ())
119 }
120 }
121 ParamKind::Pitch => units::parse_cents(raw).map(|_| ()),
122 ParamKind::FreqRange => units::parse_num_range(raw, units::parse_freq).map(|_| ()),
123 ParamKind::Choice => {
124 if choices.is_empty() || choices.contains(&raw) {
125 Ok(())
126 } else {
127 Err(Error::usage(format!("'{raw}' is not one of: {}", choices.join(", "))))
128 }
129 }
130 }
131 }
132}
133
134#[derive(Clone, Debug, Serialize)]
136pub struct ParamSpec {
137 pub name: &'static str,
139 pub kind: ParamKind,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub default: Option<&'static str>,
142 pub help: &'static str,
143 #[serde(skip_serializing_if = "<[_]>::is_empty")]
144 pub choices: &'static [&'static str],
145 pub repeatable: bool,
146 pub required: bool,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub short: Option<char>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub range: Option<&'static str>,
152 #[serde(skip)]
154 pub advanced: bool,
155}
156
157pub fn param(name: &'static str, kind: ParamKind) -> ParamSpec {
159 ParamSpec {
160 name,
161 kind,
162 default: None,
163 help: "",
164 choices: &[],
165 repeatable: false,
166 required: false,
167 short: None,
168 range: None,
169 advanced: false,
170 }
171}
172
173impl ParamSpec {
174 pub fn default(mut self, d: &'static str) -> Self {
175 self.default = Some(d);
176 self
177 }
178 pub fn help(mut self, h: &'static str) -> Self {
179 self.help = h;
180 self
181 }
182 pub fn choices(mut self, c: &'static [&'static str]) -> Self {
183 self.choices = c;
184 self
185 }
186 pub fn repeatable(mut self) -> Self {
187 self.repeatable = true;
188 self
189 }
190 pub fn required(mut self) -> Self {
191 self.required = true;
192 self
193 }
194 pub fn short(mut self, c: char) -> Self {
195 self.short = Some(c);
196 self
197 }
198 pub fn range(mut self, r: &'static str) -> Self {
199 self.range = Some(r);
200 self
201 }
202 pub fn advanced(mut self) -> Self {
203 self.advanced = true;
204 self
205 }
206 pub fn key(&self) -> String {
208 self.name.replace('-', "_")
209 }
210}
211
212#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, PartialOrd, Ord)]
214#[serde(rename_all = "snake_case")]
215pub enum Category {
216 Inspect,
217 Edit,
218 Dynamics,
219 Tone,
220 Space,
221 Restoration,
222 Panning,
223 Clean,
224 Separate,
225 Pitch,
226 Midi,
227 Score,
228 Chains,
229 Plugins,
230 Mastering,
231 Media,
232 Models,
233 Agents,
234 System,
235 Docs,
236}
237
238impl Category {
239 pub fn title(self) -> &'static str {
240 match self {
241 Category::Inspect => "Inspect",
242 Category::Edit => "Edit",
243 Category::Dynamics => "Dynamics",
244 Category::Tone => "Tone",
245 Category::Space => "Space and time",
246 Category::Restoration => "Restoration",
247 Category::Panning => "Panning",
248 Category::Clean => "Clean",
249 Category::Separate => "Separate",
250 Category::Pitch => "Pitch",
251 Category::Midi => "MIDI",
252 Category::Score => "Scores",
253 Category::Chains => "Chains",
254 Category::Plugins => "Plugins and sounds",
255 Category::Mastering => "Mastering",
256 Category::Media => "Media bridge",
257 Category::Models => "Models and system",
258 Category::Agents => "Agents",
259 Category::System => "System",
260 Category::Docs => "Docs",
261 }
262 }
263}
264
265#[derive(Clone, Copy, Debug, Serialize)]
267pub struct Inputs {
268 pub min: usize,
269 pub max: Option<usize>,
271 pub kind: InputKind,
272}
273
274#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
275#[serde(rename_all = "snake_case")]
276pub enum InputKind {
277 Audio,
278 Midi,
279 Score,
280 Any,
281 None,
282}
283
284impl Inputs {
285 pub const NONE: Inputs = Inputs { min: 0, max: Some(0), kind: InputKind::None };
286 pub const ONE_AUDIO: Inputs = Inputs { min: 1, max: Some(1), kind: InputKind::Audio };
287 pub const MANY_AUDIO: Inputs = Inputs { min: 1, max: None, kind: InputKind::Audio };
288 pub const ONE_MIDI: Inputs = Inputs { min: 1, max: Some(1), kind: InputKind::Midi };
289 pub const MANY_MIDI: Inputs = Inputs { min: 1, max: None, kind: InputKind::Midi };
290 pub const ONE_ANY: Inputs = Inputs { min: 1, max: Some(1), kind: InputKind::Any };
291 pub const MANY_ANY: Inputs = Inputs { min: 1, max: None, kind: InputKind::Any };
292 pub const OPTIONAL_ANY: Inputs = Inputs { min: 0, max: None, kind: InputKind::Any };
293 pub fn exactly(n: usize, kind: InputKind) -> Inputs {
294 Inputs { min: n, max: Some(n), kind }
295 }
296}
297
298#[derive(Clone, Debug, Serialize)]
300pub struct Example {
301 pub cmd: &'static str,
302 pub explain: &'static str,
303}
304
305pub type TransformFn = fn(&Args, AudioBuffer, &mut OpCtx) -> Result<AudioBuffer>;
307pub type CustomFn = fn(&Args, &mut OpCtx) -> Result<Value>;
309
310#[derive(Clone, Copy)]
311pub enum Runner {
312 Transform(TransformFn),
315 Custom(CustomFn),
318 Chain,
320}
321
322impl std::fmt::Debug for Runner {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 match self {
325 Runner::Transform(_) => write!(f, "Transform"),
326 Runner::Custom(_) => write!(f, "Custom"),
327 Runner::Chain => write!(f, "Chain"),
328 }
329 }
330}
331
332#[derive(Clone, Debug, Serialize)]
334pub struct Preset {
335 pub name: &'static str,
336 pub help: &'static str,
337 pub values: &'static [(&'static str, &'static str)],
338}
339
340#[derive(Clone, Debug, Serialize)]
342pub struct OpSpec {
343 pub name: &'static str,
345 pub category: Category,
346 pub summary: &'static str,
348 pub about: &'static str,
350 pub params: Vec<ParamSpec>,
351 pub inputs: Inputs,
352 pub examples: Vec<Example>,
353 pub presets: Vec<Preset>,
354 pub auto_mix: bool,
357 pub best_effort: bool,
359 pub aliases: Vec<&'static str>,
361 #[serde(skip_serializing_if = "Option::is_none")]
364 pub positional: Option<&'static str>,
365 #[serde(skip)]
366 pub runner: Runner,
367}
368
369impl OpSpec {
370 pub fn new(name: &'static str, category: Category, summary: &'static str, runner: Runner) -> OpSpec {
371 let inputs = match runner {
372 Runner::Transform(_) | Runner::Chain => Inputs::ONE_AUDIO,
373 Runner::Custom(_) => Inputs::ONE_ANY,
374 };
375 OpSpec {
376 name,
377 category,
378 summary,
379 about: "",
380 params: Vec::new(),
381 inputs,
382 examples: Vec::new(),
383 presets: Vec::new(),
384 auto_mix: matches!(runner, Runner::Transform(_)),
385 best_effort: false,
386 aliases: Vec::new(),
387 positional: None,
388 runner,
389 }
390 }
391
392 pub fn transform(name: &'static str, category: Category, summary: &'static str, f: TransformFn) -> OpSpec {
393 OpSpec::new(name, category, summary, Runner::Transform(f))
394 }
395
396 pub fn custom(name: &'static str, category: Category, summary: &'static str, f: CustomFn) -> OpSpec {
397 OpSpec::new(name, category, summary, Runner::Custom(f))
398 }
399
400 pub fn about(mut self, a: &'static str) -> Self {
401 self.about = a;
402 self
403 }
404 pub fn param(mut self, p: ParamSpec) -> Self {
405 self.params.push(p);
406 self
407 }
408 pub fn params(mut self, ps: impl IntoIterator<Item = ParamSpec>) -> Self {
409 self.params.extend(ps);
410 self
411 }
412 pub fn inputs(mut self, i: Inputs) -> Self {
413 self.inputs = i;
414 self
415 }
416 pub fn example(mut self, cmd: &'static str, explain: &'static str) -> Self {
417 self.examples.push(Example { cmd, explain });
418 self
419 }
420 pub fn preset(
421 mut self,
422 name: &'static str,
423 help: &'static str,
424 values: &'static [(&'static str, &'static str)],
425 ) -> Self {
426 self.presets.push(Preset { name, help, values });
427 self
428 }
429 pub fn no_mix(mut self) -> Self {
430 self.auto_mix = false;
431 self
432 }
433 pub fn best_effort(mut self) -> Self {
434 self.best_effort = true;
435 self
436 }
437 pub fn positional(mut self, name: &'static str) -> Self {
439 self.positional = Some(name);
440 self.inputs = Inputs::NONE;
441 self
442 }
443 pub fn alias(mut self, a: &'static str) -> Self {
444 self.aliases.push(a);
445 self
446 }
447
448 pub fn id(&self) -> String {
450 self.name.replace([' ', '-'], "_")
451 }
452
453 pub fn is_transform(&self) -> bool {
454 matches!(self.runner, Runner::Transform(_))
455 }
456
457 pub fn find_param(&self, name: &str) -> Option<&ParamSpec> {
458 let norm = name.replace('_', "-");
459 self.params.iter().find(|p| p.name == norm)
460 }
461
462 pub fn effective_params(&self) -> Vec<ParamSpec> {
464 let mut ps = self.params.clone();
465 if self.auto_mix && self.find_param("mix").is_none() {
466 ps.push(
467 param("mix", ParamKind::Percent)
468 .default("100%")
469 .help("Dry/wet balance: 0% returns the input unchanged, 100% is fully processed"),
470 );
471 }
472 if !self.presets.is_empty() && self.find_param("preset").is_none() {
473 let _names: Vec<&str> = self.presets.iter().map(|p| p.name).collect();
474 ps.push(
475 param("preset", ParamKind::Str)
476 .help("Load a named preset (built-in or user); explicit flags override its values"),
477 );
478 }
479 ps
480 }
481}
482
483pub fn parse_bool(s: &str) -> Result<bool> {
485 match s.trim().to_ascii_lowercase().as_str() {
486 "" | "on" | "true" | "yes" | "1" => Ok(true),
487 "off" | "false" | "no" | "0" => Ok(false),
488 other => Err(Error::usage(format!("'{other}' is not on/off or true/false"))),
489 }
490}
491
492#[derive(Clone, Debug, Default)]
495pub struct Args {
496 values: BTreeMap<String, Vec<String>>,
497 explicit: Vec<String>,
499}
500
501impl Args {
502 pub fn new() -> Args {
503 Args::default()
504 }
505
506 pub fn build(
509 spec: &OpSpec,
510 raw: Vec<(String, String)>,
511 user_presets: &dyn Fn(&str, &str) -> Option<Vec<(String, String)>>,
512 ) -> Result<Args> {
513 let params = spec.effective_params();
514 let mut args = Args::new();
515 let mut explicit: BTreeMap<String, Vec<String>> = BTreeMap::new();
516 for (k, v) in raw {
517 let name = k.replace('_', "-");
518 let p = params.iter().find(|p| p.name == name).ok_or_else(|| {
519 let known: Vec<&str> = params.iter().map(|p| p.name).collect();
520 Error::usage(format!("'{}' does not accept '{}'. Accepted: {}", spec.name, name, known.join(", ")))
521 .with_hint(format!("ferraus schema {}", spec.name))
522 })?;
523 p.kind.validate(&v, p.choices).map_err(|e| Error::usage(format!("--{}: {}", p.name, e.message)))?;
524 let entry = explicit.entry(name.clone()).or_default();
525 if p.repeatable {
526 entry.push(v);
527 } else if p.kind == ParamKind::List && !entry.is_empty() {
528 let joined = format!("{},{}", entry[0], v);
531 *entry = vec![joined];
532 } else {
533 *entry = vec![v];
534 }
535 }
536 let own_preset = spec.params.iter().any(|p| p.name == "preset");
539 if let Some(pname) = explicit.get("preset").and_then(|v| v.first()).cloned().filter(|_| !own_preset) {
540 let values: Vec<(String, String)> = if let Some(p) = spec.presets.iter().find(|p| p.name == pname) {
541 p.values.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
542 } else if let Some(u) = user_presets(spec.name, &pname) {
543 u
544 } else {
545 let names: Vec<&str> = spec.presets.iter().map(|p| p.name).collect();
546 return Err(Error::usage(format!(
547 "unknown preset '{pname}' for '{}'; built-in presets: {}",
548 spec.name,
549 names.join(", ")
550 ))
551 .with_hint(format!("ferraus presets list {}", spec.name)));
552 };
553 for (k, v) in values {
554 let name = k.replace('_', "-");
555 if !explicit.contains_key(&name) {
556 if let Some(p) = params.iter().find(|p| p.name == name) {
557 p.kind.validate(&v, p.choices)?;
558 args.values.insert(name, vec![v]);
559 }
560 }
561 }
562 }
563 for (k, v) in explicit {
564 args.explicit.push(k.clone());
565 args.values.insert(k, v);
566 }
567 for p in ¶ms {
568 if !args.values.contains_key(p.name) {
569 if let Some(d) = p.default {
570 args.values.insert(p.name.to_string(), vec![d.to_string()]);
571 } else if p.required {
572 return Err(Error::usage(format!("'{}' requires --{}", spec.name, p.name))
573 .with_hint(format!("ferraus docs {}", spec.name)));
574 }
575 }
576 }
577 Ok(args)
578 }
579
580 pub fn set(&mut self, name: &str, value: impl Into<String>) -> &mut Self {
582 self.values.insert(name.replace('_', "-"), vec![value.into()]);
583 self
584 }
585
586 pub fn with(mut self, name: &str, value: impl Into<String>) -> Self {
587 self.set(name, value);
588 self
589 }
590
591 pub fn remove(&mut self, name: &str) {
592 self.values.remove(&name.replace('_', "-"));
593 }
594
595 pub fn is_explicit(&self, name: &str) -> bool {
596 self.explicit.iter().any(|n| n == name)
597 }
598
599 pub fn has(&self, name: &str) -> bool {
600 self.values.contains_key(name)
601 }
602
603 pub fn raw(&self, name: &str) -> Option<&str> {
604 self.values.get(name).and_then(|v| v.last()).map(|s| s.as_str())
605 }
606
607 pub fn all(&self, name: &str) -> Vec<String> {
608 self.values.get(name).cloned().unwrap_or_default()
609 }
610
611 pub fn list(&self, name: &str) -> Vec<String> {
613 self.all(name).iter().flat_map(|v| units::split_list(v)).collect()
614 }
615
616 pub fn str(&self, name: &str) -> Option<String> {
617 self.raw(name).map(|s| s.to_string())
618 }
619
620 pub fn str_or(&self, name: &str, d: &str) -> String {
621 self.raw(name).unwrap_or(d).to_string()
622 }
623
624 fn need<'a>(&'a self, name: &str) -> Result<&'a str> {
625 self.raw(name).ok_or_else(|| Error::usage(format!("missing --{name}")))
626 }
627
628 fn ctx<T>(&self, name: &str, r: Result<T>) -> Result<T> {
629 r.map_err(|e| Error::usage(format!("--{name}: {}", e.message)))
630 }
631
632 pub fn flag(&self, name: &str) -> bool {
633 self.raw(name).map(|v| parse_bool(v).unwrap_or(false)).unwrap_or(false)
634 }
635
636 pub fn switch(&self, name: &str, default: bool) -> bool {
638 self.raw(name).map(|v| parse_bool(v).unwrap_or(default)).unwrap_or(default)
639 }
640
641 pub fn f64(&self, name: &str) -> Result<f64> {
642 let v = self.need(name)?;
643 self.ctx(name, v.trim().parse::<f64>().map_err(|_| Error::usage(format!("'{v}' is not a number"))))
644 }
645
646 pub fn opt_f64(&self, name: &str) -> Result<Option<f64>> {
647 match self.raw(name) {
648 None | Some("auto") => Ok(None),
649 Some(_) => self.f64(name).map(Some),
650 }
651 }
652
653 pub fn int(&self, name: &str) -> Result<i64> {
654 let v = self.need(name)?;
655 self.ctx(name, v.trim().parse::<i64>().map_err(|_| Error::usage(format!("'{v}' is not an integer"))))
656 }
657
658 pub fn usize(&self, name: &str) -> Result<usize> {
659 let v = self.int(name)?;
660 if v < 0 {
661 return Err(Error::usage(format!("--{name} must not be negative")));
662 }
663 Ok(v as usize)
664 }
665
666 pub fn time(&self, name: &str) -> Result<Time> {
667 let v = self.need(name)?;
668 self.ctx(name, Time::parse(v))
669 }
670
671 pub fn opt_time(&self, name: &str) -> Result<Option<Time>> {
672 match self.raw(name) {
673 None => Ok(None),
674 Some(_) => self.time(name).map(Some),
675 }
676 }
677
678 pub fn seconds(&self, name: &str) -> Result<f64> {
680 let v = self.need(name)?;
681 self.ctx(name, units::parse_duration_s(v))
682 }
683
684 pub fn sync_time(&self, name: &str) -> Result<SyncTime> {
685 let v = self.need(name)?;
686 self.ctx(name, SyncTime::parse(v))
687 }
688
689 pub fn range(&self, name: &str) -> Result<Option<TimeRange>> {
690 match self.raw(name) {
691 None => Ok(None),
692 Some(v) => self.ctx(name, TimeRange::parse(v)).map(Some),
693 }
694 }
695
696 pub fn ranges(&self, name: &str) -> Result<Vec<TimeRange>> {
697 self.all(name).iter().map(|v| self.ctx(name, TimeRange::parse(v))).collect()
698 }
699
700 pub fn gain(&self, name: &str) -> Result<Gain> {
701 let v = self.need(name)?;
702 self.ctx(name, Gain::parse(v))
703 }
704
705 pub fn db(&self, name: &str) -> Result<f64> {
707 Ok(self.gain(name)?.db())
708 }
709
710 pub fn opt_db(&self, name: &str) -> Result<Option<f64>> {
711 match self.raw(name) {
712 None | Some("auto") => Ok(None),
713 Some(_) => self.db(name).map(Some),
714 }
715 }
716
717 pub fn freq(&self, name: &str) -> Result<f64> {
718 let v = self.need(name)?;
719 self.ctx(name, units::parse_freq(v))
720 }
721
722 pub fn opt_freq(&self, name: &str) -> Result<Option<f64>> {
723 match self.raw(name) {
724 None | Some("auto") | Some("off") => Ok(None),
725 Some(_) => self.freq(name).map(Some),
726 }
727 }
728
729 pub fn freq_range(&self, name: &str) -> Result<(f64, f64)> {
730 let v = self.need(name)?;
731 self.ctx(name, units::parse_num_range(v, units::parse_freq))
732 }
733
734 pub fn ratio(&self, name: &str) -> Result<f64> {
735 let v = self.need(name)?;
736 self.ctx(name, units::parse_ratio(v))
737 }
738
739 pub fn percent(&self, name: &str) -> Result<f64> {
741 let v = self.need(name)?;
742 self.ctx(name, units::parse_percent(v))
743 }
744
745 pub fn grid(&self, name: &str) -> Result<Option<Grid>> {
746 match self.raw(name) {
747 None | Some("auto") | Some("off") => Ok(None),
748 Some(v) => self.ctx(name, Grid::parse(v)).map(Some),
749 }
750 }
751
752 pub fn cents(&self, name: &str) -> Result<f64> {
753 let v = self.need(name)?;
754 self.ctx(name, units::parse_cents(v))
755 }
756
757 pub fn semitones(&self, name: &str) -> Result<f64> {
758 let v = self.need(name)?;
759 self.ctx(name, units::parse_semitones(v))
760 }
761
762 pub fn path(&self, name: &str) -> Option<PathBuf> {
763 self.raw(name).map(PathBuf::from)
764 }
765
766 pub fn choice(&self, name: &str) -> String {
767 self.raw(name).unwrap_or("").to_string()
768 }
769
770 pub fn to_json(&self) -> Map<String, Value> {
772 let mut m = Map::new();
773 for (k, v) in &self.values {
774 let key = k.replace('-', "_");
775 if v.len() == 1 {
776 m.insert(key, Value::String(v[0].clone()));
777 } else {
778 m.insert(key, Value::Array(v.iter().cloned().map(Value::String).collect()));
779 }
780 }
781 m
782 }
783
784 pub fn pairs(&self) -> Vec<(String, String)> {
786 self.values.iter().flat_map(|(k, vs)| vs.iter().map(move |v| (k.clone(), v.clone()))).collect()
787 }
788}
789
790pub fn json_to_pairs(v: &Value) -> Result<Vec<(String, String)>> {
792 let mut out = Vec::new();
793 let obj = match v {
794 Value::Object(o) => o,
795 Value::Null => return Ok(out),
796 _ => return Err(Error::usage("arguments must be a JSON object")),
797 };
798 for (k, val) in obj {
799 match val {
800 Value::Null => {}
801 Value::Array(items) => {
802 for it in items {
803 out.push((k.clone(), scalar_to_string(it)?));
804 }
805 }
806 other => out.push((k.clone(), scalar_to_string(other)?)),
807 }
808 }
809 Ok(out)
810}
811
812fn scalar_to_string(v: &Value) -> Result<String> {
813 Ok(match v {
814 Value::String(s) => s.clone(),
815 Value::Bool(b) => {
816 if *b {
817 "true".into()
818 } else {
819 "false".into()
820 }
821 }
822 Value::Number(n) => n.to_string(),
823 Value::Null => String::new(),
824 Value::Array(a) => a.iter().map(scalar_to_string).collect::<Result<Vec<_>>>()?.join(","),
825 Value::Object(_) => serde_json::to_string(v)?,
826 })
827}
828
829pub fn parse_stage_string(s: &str) -> Result<(String, Vec<(String, String)>)> {
831 let (name, rest) = match s.split_once(':') {
832 Some((n, r)) => (n.trim(), r),
833 None => (s.trim(), ""),
834 };
835 if name.is_empty() {
836 return Err(Error::usage(format!("empty effect name in '{s}'")));
837 }
838 let mut pairs = Vec::new();
839 let mut current: Option<(String, String)> = None;
841 for part in rest.split(',') {
842 let part = part.trim();
843 if part.is_empty() {
844 continue;
845 }
846 let looks_like_key = part
847 .split_once('=')
848 .map(|(k, _)| k.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'))
849 .unwrap_or(false)
850 || (!part.contains('=')
851 && part.starts_with(|c: char| c.is_ascii_alphabetic())
852 && part.chars().all(|c| c.is_ascii_alphabetic() || c == '-' || c == '_'));
853 if looks_like_key {
854 if let Some(kv) = current.take() {
855 pairs.push(kv);
856 }
857 match part.split_once('=') {
858 Some((k, v)) => current = Some((k.trim().to_string(), v.trim().to_string())),
859 None => current = Some((part.to_string(), "true".to_string())),
860 }
861 } else if let Some((_, v)) = current.as_mut() {
862 v.push(',');
863 v.push_str(part);
864 } else {
865 return Err(Error::usage(format!("cannot parse effect parameters '{rest}'")));
866 }
867 }
868 if let Some(kv) = current.take() {
869 pairs.push(kv);
870 }
871 Ok((name.to_string(), pairs))
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877
878 fn dummy(_: &Args, b: AudioBuffer, _: &mut OpCtx) -> Result<AudioBuffer> {
879 Ok(b)
880 }
881
882 fn spec() -> OpSpec {
883 OpSpec::transform("compress", Category::Dynamics, "Compressor", dummy)
884 .param(param("threshold", ParamKind::Gain).default("-18dB"))
885 .param(param("ratio", ParamKind::Ratio).default("3:1"))
886 .param(param("detector", ParamKind::Choice).choices(&["rms", "peak"]).default("rms"))
887 .preset("vocal", "", &[("threshold", "-20dB"), ("ratio", "4:1")])
888 }
889
890 #[test]
891 fn defaults_and_explicit() {
892 let s = spec();
893 let a = Args::build(&s, vec![("ratio".into(), "4:1".into())], &|_, _| None).unwrap();
894 assert_eq!(a.ratio("ratio").unwrap(), 4.0);
895 assert_eq!(a.db("threshold").unwrap(), -18.0);
896 assert_eq!(a.percent("mix").unwrap(), 1.0);
897 assert!(a.is_explicit("ratio"));
898 assert!(!a.is_explicit("threshold"));
899 }
900
901 #[test]
902 fn preset_then_override() {
903 let s = spec();
904 let a = Args::build(&s, vec![("preset".into(), "vocal".into()), ("ratio".into(), "2:1".into())], &|_, _| None)
905 .unwrap();
906 assert_eq!(a.db("threshold").unwrap(), -20.0);
907 assert_eq!(a.ratio("ratio").unwrap(), 2.0);
908 }
909
910 #[test]
911 fn json_array_keeps_every_list_item() {
912 let s = OpSpec::transform("mix", Category::Edit, "Mix", dummy).param(param("gains", ParamKind::List));
913 let pairs = json_to_pairs(&serde_json::json!({"gains": ["0dB", "-6dB"]})).unwrap();
914 let a = Args::build(&s, pairs, &|_, _| None).unwrap();
915 assert_eq!(a.list("gains"), vec!["0dB".to_string(), "-6dB".to_string()]);
916 }
917
918 #[test]
919 fn rejects_unknown_and_bad_values() {
920 let s = spec();
921 assert!(Args::build(&s, vec![("nope".into(), "1".into())], &|_, _| None).is_err());
922 assert!(Args::build(&s, vec![("detector".into(), "lol".into())], &|_, _| None).is_err());
923 assert!(Args::build(&s, vec![("threshold".into(), "loud".into())], &|_, _| None).is_err());
924 }
925
926 #[test]
927 fn stage_strings() {
928 let (n, p) = parse_stage_string("denoise:auto").unwrap();
929 assert_eq!(n, "denoise");
930 assert_eq!(p, vec![("auto".to_string(), "true".to_string())]);
931 let (n, p) = parse_stage_string("fade:in=1s,out=2s").unwrap();
932 assert_eq!(n, "fade");
933 assert_eq!(p.len(), 2);
934 let (_, p) = parse_stage_string("eq:band=peak:1kHz:+3dB,band=highpass:80Hz:0dB").unwrap();
935 assert_eq!(p.len(), 2);
936 let (_, p) = parse_stage_string("mix:gains=0dB,-3dB").unwrap();
937 assert_eq!(p, vec![("gains".to_string(), "0dB,-3dB".to_string())]);
938 let (n, p) = parse_stage_string("reverse").unwrap();
939 assert_eq!(n, "reverse");
940 assert!(p.is_empty());
941 }
942}