Skip to main content

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 chart row in the SVG `viewBox`
227    ///
228    /// For `Layout::SeparateChannels`, one row equals one channel.
229    /// For combined layouts, one row equals one combined chart.
230    ///
231    /// Default - 500
232    #[builder(default = "DEFAULT_HEIGHT")]
233    pub svg_height_per_channel: usize,
234    /// SVG aspect ratio preservation
235    ///
236    /// Default - `None`
237    #[builder(default, try_setter, setter(strip_option, into))]
238    pub preserve_aspect_ratio: Option<SvgPreserveAspectRatio>,
239
240    // Chart labels
241    /// Show ax- labels
242    ///
243    /// Default - `true`
244    #[builder(default = "true")]
245    pub show_labels: bool,
246    /// X axis labels format
247    ///
248    /// Whether to format X axis labels as time
249    ///
250    /// Default - `false`
251    #[builder(default)]
252    pub format_x_axis_labels_as_time: bool,
253    /// Maximum number of labels along X axis
254    ///
255    /// Default - `Some(5)`
256    #[builder(default = "Some(5)")]
257    pub max_labels_x_axis: Option<usize>,
258    /// Optional chart title
259    ///
260    /// Default - `None`
261    #[builder(default, setter(into, strip_option))]
262    pub chart_title: Option<String>,
263    /// Optional titles for output channels
264    ///
265    /// Default - empty `Vec`
266    #[builder(default, setter(into, each(into, name = "output_title")))]
267    pub output_titles: Vec<String>,
268    /// Optional titles for input channels
269    ///
270    /// Default - empty `Vec`
271    #[builder(default, setter(into, each(into, name = "input_title")))]
272    pub input_titles: Vec<String>,
273
274    // Lines
275    /// Show grid lines on the chart
276    ///
277    /// Default - `false`
278    #[builder(default)]
279    pub show_grid: bool,
280    /// Waveform line thickness
281    ///
282    /// Default - 2.0
283    #[builder(default = "2.0")]
284    pub line_width: f32,
285
286    // Chart colors
287    /// Chart background color (hex string)
288    ///
289    /// Default - "#000000" (black)
290    #[builder(default = "\"#000000\".to_string()", setter(into))]
291    pub background_color: String,
292    /// Custom colors for output channels (hex strings)
293    ///
294    /// Default - `None` (uses default palette)
295    #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
296    pub output_colors: Option<Vec<String>>,
297    /// Custom colors for input channels (hex strings)
298    ///
299    /// Default - `None` (uses default palette)
300    #[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/// Processing mode for snapshotting an audio unit.
317#[derive(Debug, Clone, Copy, Default)]
318pub enum Processing {
319    #[default]
320    /// Process one sample at a time.
321    Tick,
322    /// Process a batch of samples at a time.
323    ///
324    /// max batch size is 64 [fundsp::MAX_BUFFER_SIZE]
325    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    /// Intnded for internal use only
387    ///
388    /// Used by macros to determine snapshot filename
389    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    /// Intnded for internal use only
406    ///
407    /// Used by macros to set chart title if not already set
408    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
422/// Legacy (v1.x) compatibility helpers
423impl SnapshotConfigBuilder {
424    /// Internal helper to ensure we have a mutable reference to an underlying `SvgChartConfig`
425    /// Creating a default one if `output_mode` is `None` or replacing a `Wav` variant.
426    fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
427        // If already a chart, return it.
428        if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
429            return chart;
430        }
431        // Otherwise replace (None or Wav) with default chart.
432        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    /// Set chart layout.
440    pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
441        self.legacy_svg_mut().chart_layout = value;
442        self
443    }
444
445    /// Include inputs in chart.
446    pub fn with_inputs(&mut self, value: bool) -> &mut Self {
447        self.legacy_svg_mut().with_inputs = value;
448        self
449    }
450
451    /// Set fixed SVG width.
452    pub fn svg_width(&mut self, value: usize) -> &mut Self {
453        self.legacy_svg_mut().svg_width = Some(value);
454        self
455    }
456
457    /// Set SVG height per channel.
458    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    /// Toggle label visibility.
464    pub fn show_labels(&mut self, value: bool) -> &mut Self {
465        self.legacy_svg_mut().show_labels = value;
466        self
467    }
468
469    /// Format X axis labels as time.
470    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    /// Set maximum number of X axis labels.
476    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    /// Set chart title.
482    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    /// Add an output channel title.
488    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    /// Add an input channel title.
494    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    /// Add output channels' titles.
500    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    /// Add input channels' titles.
506    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    /// Show grid lines.
512    pub fn show_grid(&mut self, value: bool) -> &mut Self {
513        self.legacy_svg_mut().show_grid = value;
514        self
515    }
516
517    /// Set waveform line width.
518    pub fn line_width(&mut self, value: f32) -> &mut Self {
519        self.legacy_svg_mut().line_width = value;
520        self
521    }
522
523    /// Set background color.
524    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    /// Replace all output channel colors.
530    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    /// Append one output channel color.
536    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    /// Replace all input channel colors.
546    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    /// Append one input channel color.
552    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}