1use std::str::FromStr;
2
3use derive_builder::Builder;
4use fundsp::DEFAULT_SR;
5
6use crate::warmup::WarmUp;
7
8pub use crate::chart::Layout;
9
10const DEFAULT_HEIGHT: usize = 500;
11
12#[derive(Debug, Clone, Builder)]
13pub struct SnapshotConfig {
15 #[builder(default = "fundsp::DEFAULT_SR")]
20 pub sample_rate: f64,
21 #[builder(default = "1024")]
25 pub num_samples: usize,
26 #[builder(default = "Processing::default()")]
30 pub processing_mode: Processing,
31 #[builder(default = "WarmUp::None")]
35 pub warm_up: WarmUp,
36 #[builder(default = "false")]
44 pub allow_abnormal_samples: bool,
45
46 #[builder(
52 default = "SnapshotOutputMode::SvgChart(SvgChartConfig::default())",
53 try_setter,
54 setter(into)
55 )]
56 pub output_mode: SnapshotOutputMode,
57
58 #[builder(default = "OutputAssertion::NonZero")]
65 pub output_assertion: OutputAssertion,
66}
67
68#[derive(Debug, Clone, Copy, Default)]
69pub enum SvgPreserveAspectRatioAlignment {
70 #[default]
71 None,
72 XMinYMin,
73 XMidYMin,
74 XMaxYMin,
75 XMinYMid,
76 XMidYMid,
77 XMaxYMid,
78 XMinYMax,
79 XMidYMax,
80 XMaxYMax,
81}
82
83impl std::fmt::Display for SvgPreserveAspectRatioAlignment {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 SvgPreserveAspectRatioAlignment::None => write!(f, "none"),
87 SvgPreserveAspectRatioAlignment::XMinYMin => write!(f, "xMinYMin"),
88 SvgPreserveAspectRatioAlignment::XMidYMin => write!(f, "xMidYMin"),
89 SvgPreserveAspectRatioAlignment::XMaxYMin => write!(f, "xMaxYMin"),
90 SvgPreserveAspectRatioAlignment::XMinYMid => write!(f, "xMinYMid"),
91 SvgPreserveAspectRatioAlignment::XMidYMid => write!(f, "xMidYMid"),
92 SvgPreserveAspectRatioAlignment::XMaxYMid => write!(f, "xMaxYMid"),
93 SvgPreserveAspectRatioAlignment::XMinYMax => write!(f, "xMinYMax"),
94 SvgPreserveAspectRatioAlignment::XMidYMax => write!(f, "xMidYMax"),
95 SvgPreserveAspectRatioAlignment::XMaxYMax => write!(f, "xMaxYMax"),
96 }
97 }
98}
99
100impl FromStr for SvgPreserveAspectRatioAlignment {
101 type Err = ();
102
103 fn from_str(input: &str) -> Result<SvgPreserveAspectRatioAlignment, Self::Err> {
104 match input {
105 "none" => Ok(SvgPreserveAspectRatioAlignment::None),
106 "xMinYMin" => Ok(SvgPreserveAspectRatioAlignment::XMinYMin),
107 "xMidYMin" => Ok(SvgPreserveAspectRatioAlignment::XMidYMin),
108 "xMaxYMin" => Ok(SvgPreserveAspectRatioAlignment::XMaxYMin),
109 "xMinYMid" => Ok(SvgPreserveAspectRatioAlignment::XMinYMid),
110 "xMidYMid" => Ok(SvgPreserveAspectRatioAlignment::XMidYMid),
111 "xMaxYMid" => Ok(SvgPreserveAspectRatioAlignment::XMaxYMid),
112 "xMinYMax" => Ok(SvgPreserveAspectRatioAlignment::XMinYMax),
113 "xMidYMax" => Ok(SvgPreserveAspectRatioAlignment::XMidYMax),
114 "xMaxYMax" => Ok(SvgPreserveAspectRatioAlignment::XMaxYMax),
115 _ => Err(()),
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, Default)]
121pub enum SvgPreserveAspectRatioKwd {
122 #[default]
123 None,
124 Meet,
125 Slice,
126}
127
128impl FromStr for SvgPreserveAspectRatioKwd {
129 type Err = ();
130
131 fn from_str(input: &str) -> Result<SvgPreserveAspectRatioKwd, Self::Err> {
132 match input {
133 "meet" => Ok(SvgPreserveAspectRatioKwd::Meet),
134 "slice" => Ok(SvgPreserveAspectRatioKwd::Slice),
135 "" => Ok(SvgPreserveAspectRatioKwd::None),
136 _ => Err(()),
137 }
138 }
139}
140
141impl std::fmt::Display for SvgPreserveAspectRatioKwd {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 SvgPreserveAspectRatioKwd::None => write!(f, ""),
145 SvgPreserveAspectRatioKwd::Meet => write!(f, " meet"),
146 SvgPreserveAspectRatioKwd::Slice => write!(f, " slice"),
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, Default, Builder)]
152#[builder(default)]
153pub struct SvgPreserveAspectRatio {
154 pub alignment: SvgPreserveAspectRatioAlignment,
155 pub kwd: SvgPreserveAspectRatioKwd,
156}
157
158impl std::fmt::Display for SvgPreserveAspectRatio {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 if let SvgPreserveAspectRatioAlignment::None = self.alignment {
161 write!(f, "none")
162 } else {
163 write!(f, "{}{}", self.alignment, self.kwd)
164 }
165 }
166}
167
168impl FromStr for SvgPreserveAspectRatio {
169 type Err = ();
170
171 fn from_str(input: &str) -> Result<SvgPreserveAspectRatio, Self::Err> {
172 let parts: Vec<&str> = input.split_whitespace().collect();
173 if parts.is_empty() {
174 return Err(());
175 }
176
177 let alignment = SvgPreserveAspectRatioAlignment::from_str(parts[0])?;
178 let kwd = if parts.len() > 1 {
179 SvgPreserveAspectRatioKwd::from_str(parts[1])?
180 } else {
181 SvgPreserveAspectRatioKwd::None
182 };
183
184 Ok(SvgPreserveAspectRatio { alignment, kwd })
185 }
186}
187
188impl SvgPreserveAspectRatio {
189 pub fn center() -> Self {
192 Self {
193 alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
194 kwd: SvgPreserveAspectRatioKwd::None,
195 }
196 }
197
198 pub fn scale_to_fit() -> Self {
200 Self {
201 alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
202 kwd: SvgPreserveAspectRatioKwd::Meet,
203 }
204 }
205
206 pub fn scale_to_fill() -> Self {
208 Self {
209 alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
210 kwd: SvgPreserveAspectRatioKwd::Slice,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Builder)]
216pub struct SvgChartConfig {
217 #[builder(default)]
224 pub chart_layout: Layout,
225 #[builder(default)]
229 pub with_inputs: bool,
230 #[builder(default, setter(strip_option))]
234 pub svg_width: Option<usize>,
235 #[builder(default = "DEFAULT_HEIGHT")]
242 pub svg_height_per_channel: usize,
243 #[builder(default, try_setter, setter(strip_option, into))]
247 pub preserve_aspect_ratio: Option<SvgPreserveAspectRatio>,
248
249 #[builder(default = "true")]
254 pub show_labels: bool,
255 #[builder(default)]
261 pub format_x_axis_labels_as_time: bool,
262 #[builder(default = "Some(5)")]
266 pub max_labels_x_axis: Option<usize>,
267 #[builder(default, setter(into, strip_option))]
271 pub chart_title: Option<String>,
272 #[builder(default, setter(into, each(into, name = "output_title")))]
276 pub output_titles: Vec<String>,
277 #[builder(default, setter(into, each(into, name = "input_title")))]
281 pub input_titles: Vec<String>,
282
283 #[builder(default)]
288 pub show_grid: bool,
289 #[builder(default = "2.0")]
293 pub line_width: f32,
294
295 #[builder(default = "\"#000000\".to_string()", setter(into))]
300 pub background_color: String,
301 #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
305 pub output_colors: Option<Vec<String>>,
306 #[builder(default, setter(into, strip_option, each(into, name = "input_color")))]
310 pub input_colors: Option<Vec<String>>,
311}
312
313#[derive(Debug, Clone)]
314pub enum WavOutput {
315 Wav16,
316 Wav32,
317}
318
319#[derive(Debug, Clone)]
320pub enum SnapshotOutputMode {
321 SvgChart(SvgChartConfig),
322 Wav(WavOutput),
323}
324
325#[derive(Debug, Clone, Copy, Default)]
331pub enum OutputAssertion {
332 #[default]
336 NonZero,
337 Skip,
339 VariesFrom(f32),
344}
345
346#[derive(Debug, Clone, Copy, Default)]
348pub enum Processing {
349 #[default]
350 Tick,
352 Batch(u8),
356}
357
358impl TryFrom<SvgChartConfigBuilder> for SnapshotOutputMode {
359 type Error = SvgChartConfigBuilderError;
360
361 fn try_from(value: SvgChartConfigBuilder) -> Result<Self, Self::Error> {
362 let inner = value.build()?;
363 Ok(SnapshotOutputMode::SvgChart(inner))
364 }
365}
366
367impl From<WavOutput> for SnapshotOutputMode {
368 fn from(value: WavOutput) -> Self {
369 SnapshotOutputMode::Wav(value)
370 }
371}
372
373impl From<SvgChartConfig> for SnapshotOutputMode {
374 fn from(value: SvgChartConfig) -> Self {
375 SnapshotOutputMode::SvgChart(value)
376 }
377}
378
379impl Default for SnapshotConfig {
380 fn default() -> Self {
381 Self {
382 num_samples: 1024,
383 sample_rate: DEFAULT_SR,
384 processing_mode: Processing::default(),
385 warm_up: WarmUp::default(),
386 allow_abnormal_samples: false,
387 output_mode: SnapshotOutputMode::SvgChart(SvgChartConfig::default()),
388 output_assertion: OutputAssertion::NonZero,
389 }
390 }
391}
392
393impl Default for SvgChartConfig {
394 fn default() -> Self {
395 Self {
396 svg_width: None,
397 svg_height_per_channel: DEFAULT_HEIGHT,
398 preserve_aspect_ratio: None,
399 with_inputs: false,
400 chart_title: None,
401 output_titles: Vec::new(),
402 input_titles: Vec::new(),
403 show_grid: false,
404 show_labels: true,
405 max_labels_x_axis: Some(5),
406 output_colors: None,
407 input_colors: None,
408 background_color: "#000000".to_string(),
409 line_width: 2.0,
410 chart_layout: Layout::default(),
411 format_x_axis_labels_as_time: false,
412 }
413 }
414}
415
416impl SnapshotConfig {
417 pub fn file_name(&self, name: Option<&'_ str>) -> String {
421 match &self.output_mode {
422 SnapshotOutputMode::SvgChart(svg_chart_config) => match name {
423 Some(name) => format!("{name}.svg"),
424 None => match &svg_chart_config.chart_title {
425 Some(name) => format!("{name}.svg"),
426 None => ".svg".to_string(),
427 },
428 },
429 SnapshotOutputMode::Wav(_) => match name {
430 Some(name) => format!("{name}.wav"),
431 None => ".wav".to_string(),
432 },
433 }
434 }
435
436 pub fn maybe_title(&mut self, name: &str) {
440 if matches!(
441 self.output_mode,
442 SnapshotOutputMode::SvgChart(SvgChartConfig {
443 chart_title: None,
444 ..
445 })
446 ) && let SnapshotOutputMode::SvgChart(ref mut svg_chart_config) = self.output_mode
447 {
448 svg_chart_config.chart_title = Some(name.to_string());
449 }
450 }
451}
452
453impl SnapshotConfigBuilder {
455 fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
458 if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
460 return chart;
461 }
462 self.output_mode = Some(SnapshotOutputMode::SvgChart(SvgChartConfig::default()));
464 match self.output_mode {
465 Some(SnapshotOutputMode::SvgChart(ref mut chart)) => chart,
466 _ => unreachable!("Output mode was just set to SvgChart"),
467 }
468 }
469
470 pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
472 self.legacy_svg_mut().chart_layout = value;
473 self
474 }
475
476 pub fn with_inputs(&mut self, value: bool) -> &mut Self {
478 self.legacy_svg_mut().with_inputs = value;
479 self
480 }
481
482 pub fn svg_width(&mut self, value: usize) -> &mut Self {
484 self.legacy_svg_mut().svg_width = Some(value);
485 self
486 }
487
488 pub fn svg_height_per_channel(&mut self, value: usize) -> &mut Self {
490 self.legacy_svg_mut().svg_height_per_channel = value;
491 self
492 }
493
494 pub fn show_labels(&mut self, value: bool) -> &mut Self {
496 self.legacy_svg_mut().show_labels = value;
497 self
498 }
499
500 pub fn format_x_axis_labels_as_time(&mut self, value: bool) -> &mut Self {
502 self.legacy_svg_mut().format_x_axis_labels_as_time = value;
503 self
504 }
505
506 pub fn max_labels_x_axis(&mut self, value: Option<usize>) -> &mut Self {
508 self.legacy_svg_mut().max_labels_x_axis = value;
509 self
510 }
511
512 pub fn chart_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
514 self.legacy_svg_mut().chart_title = Some(value.into());
515 self
516 }
517
518 pub fn output_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
520 self.legacy_svg_mut().output_titles.push(value.into());
521 self
522 }
523
524 pub fn input_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
526 self.legacy_svg_mut().input_titles.push(value.into());
527 self
528 }
529
530 pub fn output_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
532 self.legacy_svg_mut().output_titles = value.into();
533 self
534 }
535
536 pub fn input_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
538 self.legacy_svg_mut().input_titles = value.into();
539 self
540 }
541
542 pub fn show_grid(&mut self, value: bool) -> &mut Self {
544 self.legacy_svg_mut().show_grid = value;
545 self
546 }
547
548 pub fn line_width(&mut self, value: f32) -> &mut Self {
550 self.legacy_svg_mut().line_width = value;
551 self
552 }
553
554 pub fn background_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
556 self.legacy_svg_mut().background_color = value.into();
557 self
558 }
559
560 pub fn output_colors(&mut self, colors: Vec<String>) -> &mut Self {
562 self.legacy_svg_mut().output_colors = Some(colors);
563 self
564 }
565
566 pub fn output_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
568 let chart = self.legacy_svg_mut();
569 chart
570 .output_colors
571 .get_or_insert_with(Vec::new)
572 .push(value.into());
573 self
574 }
575
576 pub fn input_colors(&mut self, colors: Vec<String>) -> &mut Self {
578 self.legacy_svg_mut().input_colors = Some(colors);
579 self
580 }
581
582 pub fn input_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
584 let chart = self.legacy_svg_mut();
585 chart
586 .input_colors
587 .get_or_insert_with(Vec::new)
588 .push(value.into());
589 self
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn test_default_builder() {
599 SnapshotConfigBuilder::default()
600 .build()
601 .expect("defaul config builds");
602 }
603
604 #[test]
605 fn legacy_config_compat() {
606 SnapshotConfigBuilder::default()
607 .chart_title("Complete Waveform Test")
608 .show_grid(true)
609 .show_labels(true)
610 .with_inputs(true)
611 .output_color("#FF6B6B")
612 .input_color("#95E77E")
613 .background_color("#2C3E50")
614 .line_width(3.0)
615 .svg_width(1200)
616 .svg_height_per_channel(120)
617 .build()
618 .expect("legacy config builds");
619 }
620}