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")]
230 pub svg_height_per_channel: usize,
231 #[builder(default, try_setter, setter(strip_option, into))]
235 pub preserve_aspect_ratio: Option<SvgPreserveAspectRatio>,
236
237 #[builder(default = "true")]
242 pub show_labels: bool,
243 #[builder(default)]
249 pub format_x_axis_labels_as_time: bool,
250 #[builder(default = "Some(5)")]
254 pub max_labels_x_axis: Option<usize>,
255 #[builder(default, setter(into, strip_option))]
259 pub chart_title: Option<String>,
260 #[builder(default, setter(into, each(into, name = "output_title")))]
264 pub output_titles: Vec<String>,
265 #[builder(default, setter(into, each(into, name = "input_title")))]
269 pub input_titles: Vec<String>,
270
271 #[builder(default)]
276 pub show_grid: bool,
277 #[builder(default = "2.0")]
281 pub line_width: f32,
282
283 #[builder(default = "\"#000000\".to_string()", setter(into))]
288 pub background_color: String,
289 #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
293 pub output_colors: Option<Vec<String>>,
294 #[builder(default, setter(into, strip_option, each(into, name = "input_color")))]
298 pub input_colors: Option<Vec<String>>,
299}
300
301#[derive(Debug, Clone)]
302pub enum WavOutput {
303 Wav16,
304 Wav32,
305}
306
307#[derive(Debug, Clone)]
308pub enum SnapshotOutputMode {
309 SvgChart(SvgChartConfig),
310 Wav(WavOutput),
311}
312
313#[derive(Debug, Clone, Copy, Default)]
315pub enum Processing {
316 #[default]
317 Tick,
319 Batch(u8),
323}
324
325impl TryFrom<SvgChartConfigBuilder> for SnapshotOutputMode {
326 type Error = SvgChartConfigBuilderError;
327
328 fn try_from(value: SvgChartConfigBuilder) -> Result<Self, Self::Error> {
329 let inner = value.build()?;
330 Ok(SnapshotOutputMode::SvgChart(inner))
331 }
332}
333
334impl From<WavOutput> for SnapshotOutputMode {
335 fn from(value: WavOutput) -> Self {
336 SnapshotOutputMode::Wav(value)
337 }
338}
339
340impl From<SvgChartConfig> for SnapshotOutputMode {
341 fn from(value: SvgChartConfig) -> Self {
342 SnapshotOutputMode::SvgChart(value)
343 }
344}
345
346impl Default for SnapshotConfig {
347 fn default() -> Self {
348 Self {
349 num_samples: 1024,
350 sample_rate: DEFAULT_SR,
351 processing_mode: Processing::default(),
352 warm_up: WarmUp::default(),
353 allow_abnormal_samples: false,
354 output_mode: SnapshotOutputMode::SvgChart(SvgChartConfig::default()),
355 }
356 }
357}
358
359impl Default for SvgChartConfig {
360 fn default() -> Self {
361 Self {
362 svg_width: None,
363 svg_height_per_channel: DEFAULT_HEIGHT,
364 preserve_aspect_ratio: None,
365 with_inputs: false,
366 chart_title: None,
367 output_titles: Vec::new(),
368 input_titles: Vec::new(),
369 show_grid: false,
370 show_labels: true,
371 max_labels_x_axis: Some(5),
372 output_colors: None,
373 input_colors: None,
374 background_color: "#000000".to_string(),
375 line_width: 2.0,
376 chart_layout: Layout::default(),
377 format_x_axis_labels_as_time: false,
378 }
379 }
380}
381
382impl SnapshotConfig {
383 pub fn file_name(&self, name: Option<&'_ str>) -> String {
387 match &self.output_mode {
388 SnapshotOutputMode::SvgChart(svg_chart_config) => match name {
389 Some(name) => format!("{name}.svg"),
390 None => match &svg_chart_config.chart_title {
391 Some(name) => format!("{name}.svg"),
392 None => ".svg".to_string(),
393 },
394 },
395 SnapshotOutputMode::Wav(_) => match name {
396 Some(name) => format!("{name}.wav"),
397 None => ".wav".to_string(),
398 },
399 }
400 }
401
402 pub fn maybe_title(&mut self, name: &str) {
406 if matches!(
407 self.output_mode,
408 SnapshotOutputMode::SvgChart(SvgChartConfig {
409 chart_title: None,
410 ..
411 })
412 ) && let SnapshotOutputMode::SvgChart(ref mut svg_chart_config) = self.output_mode
413 {
414 svg_chart_config.chart_title = Some(name.to_string());
415 }
416 }
417}
418
419impl SnapshotConfigBuilder {
421 fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
424 if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
426 return chart;
427 }
428 self.output_mode = Some(SnapshotOutputMode::SvgChart(SvgChartConfig::default()));
430 match self.output_mode {
431 Some(SnapshotOutputMode::SvgChart(ref mut chart)) => chart,
432 _ => unreachable!("Output mode was just set to SvgChart"),
433 }
434 }
435
436 pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
438 self.legacy_svg_mut().chart_layout = value;
439 self
440 }
441
442 pub fn with_inputs(&mut self, value: bool) -> &mut Self {
444 self.legacy_svg_mut().with_inputs = value;
445 self
446 }
447
448 pub fn svg_width(&mut self, value: usize) -> &mut Self {
450 self.legacy_svg_mut().svg_width = Some(value);
451 self
452 }
453
454 pub fn svg_height_per_channel(&mut self, value: usize) -> &mut Self {
456 self.legacy_svg_mut().svg_height_per_channel = value;
457 self
458 }
459
460 pub fn show_labels(&mut self, value: bool) -> &mut Self {
462 self.legacy_svg_mut().show_labels = value;
463 self
464 }
465
466 pub fn format_x_axis_labels_as_time(&mut self, value: bool) -> &mut Self {
468 self.legacy_svg_mut().format_x_axis_labels_as_time = value;
469 self
470 }
471
472 pub fn max_labels_x_axis(&mut self, value: Option<usize>) -> &mut Self {
474 self.legacy_svg_mut().max_labels_x_axis = value;
475 self
476 }
477
478 pub fn chart_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
480 self.legacy_svg_mut().chart_title = Some(value.into());
481 self
482 }
483
484 pub fn output_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
486 self.legacy_svg_mut().output_titles.push(value.into());
487 self
488 }
489
490 pub fn input_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
492 self.legacy_svg_mut().input_titles.push(value.into());
493 self
494 }
495
496 pub fn output_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
498 self.legacy_svg_mut().output_titles = value.into();
499 self
500 }
501
502 pub fn input_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
504 self.legacy_svg_mut().input_titles = value.into();
505 self
506 }
507
508 pub fn show_grid(&mut self, value: bool) -> &mut Self {
510 self.legacy_svg_mut().show_grid = value;
511 self
512 }
513
514 pub fn line_width(&mut self, value: f32) -> &mut Self {
516 self.legacy_svg_mut().line_width = value;
517 self
518 }
519
520 pub fn background_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
522 self.legacy_svg_mut().background_color = value.into();
523 self
524 }
525
526 pub fn output_colors(&mut self, colors: Vec<String>) -> &mut Self {
528 self.legacy_svg_mut().output_colors = Some(colors);
529 self
530 }
531
532 pub fn output_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
534 let chart = self.legacy_svg_mut();
535 chart
536 .output_colors
537 .get_or_insert_with(Vec::new)
538 .push(value.into());
539 self
540 }
541
542 pub fn input_colors(&mut self, colors: Vec<String>) -> &mut Self {
544 self.legacy_svg_mut().input_colors = Some(colors);
545 self
546 }
547
548 pub fn input_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
550 let chart = self.legacy_svg_mut();
551 chart
552 .input_colors
553 .get_or_insert_with(Vec::new)
554 .push(value.into());
555 self
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 #[test]
564 fn test_default_builder() {
565 SnapshotConfigBuilder::default()
566 .build()
567 .expect("defaul config builds");
568 }
569
570 #[test]
571 fn legacy_config_compat() {
572 SnapshotConfigBuilder::default()
573 .chart_title("Complete Waveform Test")
574 .show_grid(true)
575 .show_labels(true)
576 .with_inputs(true)
577 .output_color("#FF6B6B")
578 .input_color("#95E77E")
579 .background_color("#2C3E50")
580 .line_width(3.0)
581 .svg_width(1200)
582 .svg_height_per_channel(120)
583 .build()
584 .expect("legacy config builds");
585 }
586}