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
59#[derive(Debug, Clone, Copy, Default)]
60pub enum SvgPreserveAspectRatioAlignment {
61 #[default]
62 None,
63 XMinYMin,
64 XMidYMin,
65 XMaxYMin,
66 XMinYMid,
67 XMidYMid,
68 XMaxYMid,
69 XMinYMax,
70 XMidYMax,
71 XMaxYMax,
72}
73
74impl std::fmt::Display for SvgPreserveAspectRatioAlignment {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 SvgPreserveAspectRatioAlignment::None => write!(f, "none"),
78 SvgPreserveAspectRatioAlignment::XMinYMin => write!(f, "xMinYMin"),
79 SvgPreserveAspectRatioAlignment::XMidYMin => write!(f, "xMidYMin"),
80 SvgPreserveAspectRatioAlignment::XMaxYMin => write!(f, "xMaxYMin"),
81 SvgPreserveAspectRatioAlignment::XMinYMid => write!(f, "xMinYMid"),
82 SvgPreserveAspectRatioAlignment::XMidYMid => write!(f, "xMidYMid"),
83 SvgPreserveAspectRatioAlignment::XMaxYMid => write!(f, "xMaxYMid"),
84 SvgPreserveAspectRatioAlignment::XMinYMax => write!(f, "xMinYMax"),
85 SvgPreserveAspectRatioAlignment::XMidYMax => write!(f, "xMidYMax"),
86 SvgPreserveAspectRatioAlignment::XMaxYMax => write!(f, "xMaxYMax"),
87 }
88 }
89}
90
91impl FromStr for SvgPreserveAspectRatioAlignment {
92 type Err = ();
93
94 fn from_str(input: &str) -> Result<SvgPreserveAspectRatioAlignment, Self::Err> {
95 match input {
96 "none" => Ok(SvgPreserveAspectRatioAlignment::None),
97 "xMinYMin" => Ok(SvgPreserveAspectRatioAlignment::XMinYMin),
98 "xMidYMin" => Ok(SvgPreserveAspectRatioAlignment::XMidYMin),
99 "xMaxYMin" => Ok(SvgPreserveAspectRatioAlignment::XMaxYMin),
100 "xMinYMid" => Ok(SvgPreserveAspectRatioAlignment::XMinYMid),
101 "xMidYMid" => Ok(SvgPreserveAspectRatioAlignment::XMidYMid),
102 "xMaxYMid" => Ok(SvgPreserveAspectRatioAlignment::XMaxYMid),
103 "xMinYMax" => Ok(SvgPreserveAspectRatioAlignment::XMinYMax),
104 "xMidYMax" => Ok(SvgPreserveAspectRatioAlignment::XMidYMax),
105 "xMaxYMax" => Ok(SvgPreserveAspectRatioAlignment::XMaxYMax),
106 _ => Err(()),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, Default)]
112pub enum SvgPreserveAspectRatioKwd {
113 #[default]
114 None,
115 Meet,
116 Slice,
117}
118
119impl FromStr for SvgPreserveAspectRatioKwd {
120 type Err = ();
121
122 fn from_str(input: &str) -> Result<SvgPreserveAspectRatioKwd, Self::Err> {
123 match input {
124 "meet" => Ok(SvgPreserveAspectRatioKwd::Meet),
125 "slice" => Ok(SvgPreserveAspectRatioKwd::Slice),
126 "" => Ok(SvgPreserveAspectRatioKwd::None),
127 _ => Err(()),
128 }
129 }
130}
131
132impl std::fmt::Display for SvgPreserveAspectRatioKwd {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 SvgPreserveAspectRatioKwd::None => write!(f, ""),
136 SvgPreserveAspectRatioKwd::Meet => write!(f, " meet"),
137 SvgPreserveAspectRatioKwd::Slice => write!(f, " slice"),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, Default, Builder)]
143#[builder(default)]
144pub struct SvgPreserveAspectRatio {
145 pub alignment: SvgPreserveAspectRatioAlignment,
146 pub kwd: SvgPreserveAspectRatioKwd,
147}
148
149impl std::fmt::Display for SvgPreserveAspectRatio {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 if let SvgPreserveAspectRatioAlignment::None = self.alignment {
152 write!(f, "none")
153 } else {
154 write!(f, "{}{}", self.alignment, self.kwd)
155 }
156 }
157}
158
159impl FromStr for SvgPreserveAspectRatio {
160 type Err = ();
161
162 fn from_str(input: &str) -> Result<SvgPreserveAspectRatio, Self::Err> {
163 let parts: Vec<&str> = input.split_whitespace().collect();
164 if parts.is_empty() {
165 return Err(());
166 }
167
168 let alignment = SvgPreserveAspectRatioAlignment::from_str(parts[0])?;
169 let kwd = if parts.len() > 1 {
170 SvgPreserveAspectRatioKwd::from_str(parts[1])?
171 } else {
172 SvgPreserveAspectRatioKwd::None
173 };
174
175 Ok(SvgPreserveAspectRatio { alignment, kwd })
176 }
177}
178
179impl SvgPreserveAspectRatio {
180 pub fn center() -> Self {
183 Self {
184 alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
185 kwd: SvgPreserveAspectRatioKwd::None,
186 }
187 }
188
189 pub fn scale_to_fit() -> Self {
191 Self {
192 alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
193 kwd: SvgPreserveAspectRatioKwd::Meet,
194 }
195 }
196
197 pub fn scale_to_fill() -> Self {
199 Self {
200 alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
201 kwd: SvgPreserveAspectRatioKwd::Slice,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Builder)]
207pub struct SvgChartConfig {
208 #[builder(default)]
215 pub chart_layout: Layout,
216 #[builder(default)]
220 pub with_inputs: bool,
221 #[builder(default, setter(strip_option))]
225 pub svg_width: Option<usize>,
226 #[builder(default = "DEFAULT_HEIGHT")]
233 pub svg_height_per_channel: usize,
234 #[builder(default, try_setter, setter(strip_option, into))]
238 pub preserve_aspect_ratio: Option<SvgPreserveAspectRatio>,
239
240 #[builder(default = "true")]
245 pub show_labels: bool,
246 #[builder(default)]
252 pub format_x_axis_labels_as_time: bool,
253 #[builder(default = "Some(5)")]
257 pub max_labels_x_axis: Option<usize>,
258 #[builder(default, setter(into, strip_option))]
262 pub chart_title: Option<String>,
263 #[builder(default, setter(into, each(into, name = "output_title")))]
267 pub output_titles: Vec<String>,
268 #[builder(default, setter(into, each(into, name = "input_title")))]
272 pub input_titles: Vec<String>,
273
274 #[builder(default)]
279 pub show_grid: bool,
280 #[builder(default = "2.0")]
284 pub line_width: f32,
285
286 #[builder(default = "\"#000000\".to_string()", setter(into))]
291 pub background_color: String,
292 #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
296 pub output_colors: Option<Vec<String>>,
297 #[builder(default, setter(into, strip_option, each(into, name = "input_color")))]
301 pub input_colors: Option<Vec<String>>,
302}
303
304#[derive(Debug, Clone)]
305pub enum WavOutput {
306 Wav16,
307 Wav32,
308}
309
310#[derive(Debug, Clone)]
311pub enum SnapshotOutputMode {
312 SvgChart(SvgChartConfig),
313 Wav(WavOutput),
314}
315
316#[derive(Debug, Clone, Copy, Default)]
318pub enum Processing {
319 #[default]
320 Tick,
322 Batch(u8),
326}
327
328impl TryFrom<SvgChartConfigBuilder> for SnapshotOutputMode {
329 type Error = SvgChartConfigBuilderError;
330
331 fn try_from(value: SvgChartConfigBuilder) -> Result<Self, Self::Error> {
332 let inner = value.build()?;
333 Ok(SnapshotOutputMode::SvgChart(inner))
334 }
335}
336
337impl From<WavOutput> for SnapshotOutputMode {
338 fn from(value: WavOutput) -> Self {
339 SnapshotOutputMode::Wav(value)
340 }
341}
342
343impl From<SvgChartConfig> for SnapshotOutputMode {
344 fn from(value: SvgChartConfig) -> Self {
345 SnapshotOutputMode::SvgChart(value)
346 }
347}
348
349impl Default for SnapshotConfig {
350 fn default() -> Self {
351 Self {
352 num_samples: 1024,
353 sample_rate: DEFAULT_SR,
354 processing_mode: Processing::default(),
355 warm_up: WarmUp::default(),
356 allow_abnormal_samples: false,
357 output_mode: SnapshotOutputMode::SvgChart(SvgChartConfig::default()),
358 }
359 }
360}
361
362impl Default for SvgChartConfig {
363 fn default() -> Self {
364 Self {
365 svg_width: None,
366 svg_height_per_channel: DEFAULT_HEIGHT,
367 preserve_aspect_ratio: None,
368 with_inputs: false,
369 chart_title: None,
370 output_titles: Vec::new(),
371 input_titles: Vec::new(),
372 show_grid: false,
373 show_labels: true,
374 max_labels_x_axis: Some(5),
375 output_colors: None,
376 input_colors: None,
377 background_color: "#000000".to_string(),
378 line_width: 2.0,
379 chart_layout: Layout::default(),
380 format_x_axis_labels_as_time: false,
381 }
382 }
383}
384
385impl SnapshotConfig {
386 pub fn file_name(&self, name: Option<&'_ str>) -> String {
390 match &self.output_mode {
391 SnapshotOutputMode::SvgChart(svg_chart_config) => match name {
392 Some(name) => format!("{name}.svg"),
393 None => match &svg_chart_config.chart_title {
394 Some(name) => format!("{name}.svg"),
395 None => ".svg".to_string(),
396 },
397 },
398 SnapshotOutputMode::Wav(_) => match name {
399 Some(name) => format!("{name}.wav"),
400 None => ".wav".to_string(),
401 },
402 }
403 }
404
405 pub fn maybe_title(&mut self, name: &str) {
409 if matches!(
410 self.output_mode,
411 SnapshotOutputMode::SvgChart(SvgChartConfig {
412 chart_title: None,
413 ..
414 })
415 ) && let SnapshotOutputMode::SvgChart(ref mut svg_chart_config) = self.output_mode
416 {
417 svg_chart_config.chart_title = Some(name.to_string());
418 }
419 }
420}
421
422impl SnapshotConfigBuilder {
424 fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
427 if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
429 return chart;
430 }
431 self.output_mode = Some(SnapshotOutputMode::SvgChart(SvgChartConfig::default()));
433 match self.output_mode {
434 Some(SnapshotOutputMode::SvgChart(ref mut chart)) => chart,
435 _ => unreachable!("Output mode was just set to SvgChart"),
436 }
437 }
438
439 pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
441 self.legacy_svg_mut().chart_layout = value;
442 self
443 }
444
445 pub fn with_inputs(&mut self, value: bool) -> &mut Self {
447 self.legacy_svg_mut().with_inputs = value;
448 self
449 }
450
451 pub fn svg_width(&mut self, value: usize) -> &mut Self {
453 self.legacy_svg_mut().svg_width = Some(value);
454 self
455 }
456
457 pub fn svg_height_per_channel(&mut self, value: usize) -> &mut Self {
459 self.legacy_svg_mut().svg_height_per_channel = value;
460 self
461 }
462
463 pub fn show_labels(&mut self, value: bool) -> &mut Self {
465 self.legacy_svg_mut().show_labels = value;
466 self
467 }
468
469 pub fn format_x_axis_labels_as_time(&mut self, value: bool) -> &mut Self {
471 self.legacy_svg_mut().format_x_axis_labels_as_time = value;
472 self
473 }
474
475 pub fn max_labels_x_axis(&mut self, value: Option<usize>) -> &mut Self {
477 self.legacy_svg_mut().max_labels_x_axis = value;
478 self
479 }
480
481 pub fn chart_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
483 self.legacy_svg_mut().chart_title = Some(value.into());
484 self
485 }
486
487 pub fn output_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
489 self.legacy_svg_mut().output_titles.push(value.into());
490 self
491 }
492
493 pub fn input_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
495 self.legacy_svg_mut().input_titles.push(value.into());
496 self
497 }
498
499 pub fn output_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
501 self.legacy_svg_mut().output_titles = value.into();
502 self
503 }
504
505 pub fn input_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
507 self.legacy_svg_mut().input_titles = value.into();
508 self
509 }
510
511 pub fn show_grid(&mut self, value: bool) -> &mut Self {
513 self.legacy_svg_mut().show_grid = value;
514 self
515 }
516
517 pub fn line_width(&mut self, value: f32) -> &mut Self {
519 self.legacy_svg_mut().line_width = value;
520 self
521 }
522
523 pub fn background_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
525 self.legacy_svg_mut().background_color = value.into();
526 self
527 }
528
529 pub fn output_colors(&mut self, colors: Vec<String>) -> &mut Self {
531 self.legacy_svg_mut().output_colors = Some(colors);
532 self
533 }
534
535 pub fn output_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
537 let chart = self.legacy_svg_mut();
538 chart
539 .output_colors
540 .get_or_insert_with(Vec::new)
541 .push(value.into());
542 self
543 }
544
545 pub fn input_colors(&mut self, colors: Vec<String>) -> &mut Self {
547 self.legacy_svg_mut().input_colors = Some(colors);
548 self
549 }
550
551 pub fn input_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
553 let chart = self.legacy_svg_mut();
554 chart
555 .input_colors
556 .get_or_insert_with(Vec::new)
557 .push(value.into());
558 self
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn test_default_builder() {
568 SnapshotConfigBuilder::default()
569 .build()
570 .expect("defaul config builds");
571 }
572
573 #[test]
574 fn legacy_config_compat() {
575 SnapshotConfigBuilder::default()
576 .chart_title("Complete Waveform Test")
577 .show_grid(true)
578 .show_labels(true)
579 .with_inputs(true)
580 .output_color("#FF6B6B")
581 .input_color("#95E77E")
582 .background_color("#2C3E50")
583 .line_width(3.0)
584 .svg_width(1200)
585 .svg_height_per_channel(120)
586 .build()
587 .expect("legacy config builds");
588 }
589}