insta_fun/
config.rs

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)]
13/// Configuration for snapshotting an audio unit.
14pub struct SnapshotConfig {
15    // Audio configuration
16    /// Sample rate of the audio unit.
17    ///
18    /// Default is 44100.0 [fundsp::DEFAULT_SR]
19    #[builder(default = "fundsp::DEFAULT_SR")]
20    pub sample_rate: f64,
21    /// Number of samples to generate.
22    ///
23    /// Default is 1024
24    #[builder(default = "1024")]
25    pub num_samples: usize,
26    /// Processing mode for snapshotting an audio unit.
27    ///
28    /// Default - `Tick`
29    #[builder(default = "Processing::default()")]
30    pub processing_mode: Processing,
31    /// Warm-up mode for snapshotting an audio unit.
32    ///
33    /// Default - `WarmUp::None`
34    #[builder(default = "WarmUp::None")]
35    pub warm_up: WarmUp,
36    /// How to handle abnormal samples: `NaN`,`±Infinity`
37    ///
38    /// When set to `true` abnormal samples are allowed during processing,
39    /// but skipped in actual output. Plotted with labeled dots.
40    ///
41    /// When set to `false` and encoutered abnormal samples,
42    /// the snapshotting process will panic.
43    #[builder(default = "false")]
44    pub allow_abnormal_samples: bool,
45
46    /// Snaphsot output mode
47    ///
48    /// Use configurable chart for visual snapshots
49    ///
50    /// Use Wav16 or Wav32 for audial snapshots
51    #[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    /// Center content: alignment only (XMidYMid), no scaling keyword.
181    /// Useful when you want the SVG to define size explicitly without forcing fit/fill behavior.
182    pub fn center() -> Self {
183        Self {
184            alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
185            kwd: SvgPreserveAspectRatioKwd::None,
186        }
187    }
188
189    /// Scale to fit: center and scale uniformly so the whole viewBox is visible (xMidYMid meet).
190    pub fn scale_to_fit() -> Self {
191        Self {
192            alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
193            kwd: SvgPreserveAspectRatioKwd::Meet,
194        }
195    }
196
197    /// Scale to fill: center and scale uniformly so the viewBox is completely covered (may crop) (xMidYMid slice).
198    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    // Chart configuration
209    /// Chart layout
210    ///
211    /// Whether to plot channels on separate charts or combined charts.
212    ///
213    /// Default - `Layout::Separate`
214    #[builder(default)]
215    pub chart_layout: Layout,
216    /// Whether to include inputs in snapshot
217    ///
218    /// Default - `false`
219    #[builder(default)]
220    pub with_inputs: bool,
221    /// Optional width of the SVG `viewBox`
222    ///
223    /// `None` means proportional to num_samples
224    #[builder(default, setter(strip_option))]
225    pub svg_width: Option<usize>,
226    /// Height of **one** channel in the SVG `viewBox`
227    ///
228    /// Default - 500
229    #[builder(default = "DEFAULT_HEIGHT")]
230    pub svg_height_per_channel: usize,
231    /// SVG aspect ratio preservation
232    ///
233    /// Default - `None`
234    #[builder(default, try_setter, setter(strip_option, into))]
235    pub preserve_aspect_ratio: Option<SvgPreserveAspectRatio>,
236
237    // Chart labels
238    /// Show ax- labels
239    ///
240    /// Default - `true`
241    #[builder(default = "true")]
242    pub show_labels: bool,
243    /// X axis labels format
244    ///
245    /// Whether to format X axis labels as time
246    ///
247    /// Default - `false`
248    #[builder(default)]
249    pub format_x_axis_labels_as_time: bool,
250    /// Maximum number of labels along X axis
251    ///
252    /// Default - `Some(5)`
253    #[builder(default = "Some(5)")]
254    pub max_labels_x_axis: Option<usize>,
255    /// Optional chart title
256    ///
257    /// Default - `None`
258    #[builder(default, setter(into, strip_option))]
259    pub chart_title: Option<String>,
260    /// Optional titles for output channels
261    ///
262    /// Default - empty `Vec`
263    #[builder(default, setter(into, each(into, name = "output_title")))]
264    pub output_titles: Vec<String>,
265    /// Optional titles for input channels
266    ///
267    /// Default - empty `Vec`
268    #[builder(default, setter(into, each(into, name = "input_title")))]
269    pub input_titles: Vec<String>,
270
271    // Lines
272    /// Show grid lines on the chart
273    ///
274    /// Default - `false`
275    #[builder(default)]
276    pub show_grid: bool,
277    /// Waveform line thickness
278    ///
279    /// Default - 2.0
280    #[builder(default = "2.0")]
281    pub line_width: f32,
282
283    // Chart colors
284    /// Chart background color (hex string)
285    ///
286    /// Default - "#000000" (black)
287    #[builder(default = "\"#000000\".to_string()", setter(into))]
288    pub background_color: String,
289    /// Custom colors for output channels (hex strings)
290    ///
291    /// Default - `None` (uses default palette)
292    #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
293    pub output_colors: Option<Vec<String>>,
294    /// Custom colors for input channels (hex strings)
295    ///
296    /// Default - `None` (uses default palette)
297    #[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/// Processing mode for snapshotting an audio unit.
314#[derive(Debug, Clone, Copy, Default)]
315pub enum Processing {
316    #[default]
317    /// Process one sample at a time.
318    Tick,
319    /// Process a batch of samples at a time.
320    ///
321    /// max batch size is 64 [fundsp::MAX_BUFFER_SIZE]
322    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    /// Intnded for internal use only
384    ///
385    /// Used by macros to determine snapshot filename
386    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    /// Intnded for internal use only
403    ///
404    /// Used by macros to set chart title if not already set
405    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
419/// Legacy (v1.x) compatibility helpers
420impl SnapshotConfigBuilder {
421    /// Internal helper to ensure we have a mutable reference to an underlying `SvgChartConfig`
422    /// Creating a default one if `output_mode` is `None` or replacing a `Wav` variant.
423    fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
424        // If already a chart, return it.
425        if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
426            return chart;
427        }
428        // Otherwise replace (None or Wav) with default chart.
429        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    /// Set chart layout.
437    pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
438        self.legacy_svg_mut().chart_layout = value;
439        self
440    }
441
442    /// Include inputs in chart.
443    pub fn with_inputs(&mut self, value: bool) -> &mut Self {
444        self.legacy_svg_mut().with_inputs = value;
445        self
446    }
447
448    /// Set fixed SVG width.
449    pub fn svg_width(&mut self, value: usize) -> &mut Self {
450        self.legacy_svg_mut().svg_width = Some(value);
451        self
452    }
453
454    /// Set SVG height per channel.
455    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    /// Toggle label visibility.
461    pub fn show_labels(&mut self, value: bool) -> &mut Self {
462        self.legacy_svg_mut().show_labels = value;
463        self
464    }
465
466    /// Format X axis labels as time.
467    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    /// Set maximum number of X axis labels.
473    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    /// Set chart title.
479    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    /// Add an output channel title.
485    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    /// Add an input channel title.
491    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    /// Add output channels' titles.
497    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    /// Add input channels' titles.
503    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    /// Show grid lines.
509    pub fn show_grid(&mut self, value: bool) -> &mut Self {
510        self.legacy_svg_mut().show_grid = value;
511        self
512    }
513
514    /// Set waveform line width.
515    pub fn line_width(&mut self, value: f32) -> &mut Self {
516        self.legacy_svg_mut().line_width = value;
517        self
518    }
519
520    /// Set background color.
521    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    /// Replace all output channel colors.
527    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    /// Append one output channel color.
533    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    /// Replace all input channel colors.
543    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    /// Append one input channel color.
549    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}