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    /// Assertion applied to the output samples after processing.
59    ///
60    /// Default - [`OutputAssertion::NonZero`]: panics when all output samples are `0.0`.
61    ///
62    /// Use [`OutputAssertion::Skip`] to opt out, or [`OutputAssertion::VariesFrom`] to
63    /// check that the output differs from an arbitrary baseline value.
64    #[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    /// Center content: alignment only (XMidYMid), no scaling keyword.
190    /// Useful when you want the SVG to define size explicitly without forcing fit/fill behavior.
191    pub fn center() -> Self {
192        Self {
193            alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
194            kwd: SvgPreserveAspectRatioKwd::None,
195        }
196    }
197
198    /// Scale to fit: center and scale uniformly so the whole viewBox is visible (xMidYMid meet).
199    pub fn scale_to_fit() -> Self {
200        Self {
201            alignment: SvgPreserveAspectRatioAlignment::XMidYMid,
202            kwd: SvgPreserveAspectRatioKwd::Meet,
203        }
204    }
205
206    /// Scale to fill: center and scale uniformly so the viewBox is completely covered (may crop) (xMidYMid slice).
207    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    // Chart configuration
218    /// Chart layout
219    ///
220    /// Whether to plot channels on separate charts or combined charts.
221    ///
222    /// Default - `Layout::Separate`
223    #[builder(default)]
224    pub chart_layout: Layout,
225    /// Whether to include inputs in snapshot
226    ///
227    /// Default - `false`
228    #[builder(default)]
229    pub with_inputs: bool,
230    /// Optional width of the SVG `viewBox`
231    ///
232    /// `None` means proportional to num_samples
233    #[builder(default, setter(strip_option))]
234    pub svg_width: Option<usize>,
235    /// Height of one chart row in the SVG `viewBox`
236    ///
237    /// For `Layout::SeparateChannels`, one row equals one channel.
238    /// For combined layouts, one row equals one combined chart.
239    ///
240    /// Default - 500
241    #[builder(default = "DEFAULT_HEIGHT")]
242    pub svg_height_per_channel: usize,
243    /// SVG aspect ratio preservation
244    ///
245    /// Default - `None`
246    #[builder(default, try_setter, setter(strip_option, into))]
247    pub preserve_aspect_ratio: Option<SvgPreserveAspectRatio>,
248
249    // Chart labels
250    /// Show ax- labels
251    ///
252    /// Default - `true`
253    #[builder(default = "true")]
254    pub show_labels: bool,
255    /// X axis labels format
256    ///
257    /// Whether to format X axis labels as time
258    ///
259    /// Default - `false`
260    #[builder(default)]
261    pub format_x_axis_labels_as_time: bool,
262    /// Maximum number of labels along X axis
263    ///
264    /// Default - `Some(5)`
265    #[builder(default = "Some(5)")]
266    pub max_labels_x_axis: Option<usize>,
267    /// Optional chart title
268    ///
269    /// Default - `None`
270    #[builder(default, setter(into, strip_option))]
271    pub chart_title: Option<String>,
272    /// Optional titles for output channels
273    ///
274    /// Default - empty `Vec`
275    #[builder(default, setter(into, each(into, name = "output_title")))]
276    pub output_titles: Vec<String>,
277    /// Optional titles for input channels
278    ///
279    /// Default - empty `Vec`
280    #[builder(default, setter(into, each(into, name = "input_title")))]
281    pub input_titles: Vec<String>,
282
283    // Lines
284    /// Show grid lines on the chart
285    ///
286    /// Default - `false`
287    #[builder(default)]
288    pub show_grid: bool,
289    /// Waveform line thickness
290    ///
291    /// Default - 2.0
292    #[builder(default = "2.0")]
293    pub line_width: f32,
294
295    // Chart colors
296    /// Chart background color (hex string)
297    ///
298    /// Default - "#000000" (black)
299    #[builder(default = "\"#000000\".to_string()", setter(into))]
300    pub background_color: String,
301    /// Custom colors for output channels (hex strings)
302    ///
303    /// Default - `None` (uses default palette)
304    #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
305    pub output_colors: Option<Vec<String>>,
306    /// Custom colors for input channels (hex strings)
307    ///
308    /// Default - `None` (uses default palette)
309    #[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/// Controls the assertion applied to output samples after processing.
326///
327/// By default, `NonZero` asserts that at least one output sample across all channels
328/// is not `0.0`. Use `Skip` to opt out, or `VariesFrom` to check against an
329/// arbitrary baseline value.
330#[derive(Debug, Clone, Copy, Default)]
331pub enum OutputAssertion {
332    /// Assert that at least one output sample (across all channels) is not `0.0`.
333    ///
334    /// Panics if all output samples are `0.0`.
335    #[default]
336    NonZero,
337    /// Skip the output assertion entirely.
338    Skip,
339    /// Assert that at least one output sample (across all channels) differs from
340    /// the given `baseline` value.
341    ///
342    /// Panics if all output samples equal `baseline`.
343    VariesFrom(f32),
344}
345
346/// Processing mode for snapshotting an audio unit.
347#[derive(Debug, Clone, Copy, Default)]
348pub enum Processing {
349    #[default]
350    /// Process one sample at a time.
351    Tick,
352    /// Process a batch of samples at a time.
353    ///
354    /// max batch size is 64 [fundsp::MAX_BUFFER_SIZE]
355    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    /// Intnded for internal use only
418    ///
419    /// Used by macros to determine snapshot filename
420    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    /// Intnded for internal use only
437    ///
438    /// Used by macros to set chart title if not already set
439    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
453/// Legacy (v1.x) compatibility helpers
454impl SnapshotConfigBuilder {
455    /// Internal helper to ensure we have a mutable reference to an underlying `SvgChartConfig`
456    /// Creating a default one if `output_mode` is `None` or replacing a `Wav` variant.
457    fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
458        // If already a chart, return it.
459        if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
460            return chart;
461        }
462        // Otherwise replace (None or Wav) with default chart.
463        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    /// Set chart layout.
471    pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
472        self.legacy_svg_mut().chart_layout = value;
473        self
474    }
475
476    /// Include inputs in chart.
477    pub fn with_inputs(&mut self, value: bool) -> &mut Self {
478        self.legacy_svg_mut().with_inputs = value;
479        self
480    }
481
482    /// Set fixed SVG width.
483    pub fn svg_width(&mut self, value: usize) -> &mut Self {
484        self.legacy_svg_mut().svg_width = Some(value);
485        self
486    }
487
488    /// Set SVG height per channel.
489    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    /// Toggle label visibility.
495    pub fn show_labels(&mut self, value: bool) -> &mut Self {
496        self.legacy_svg_mut().show_labels = value;
497        self
498    }
499
500    /// Format X axis labels as time.
501    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    /// Set maximum number of X axis labels.
507    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    /// Set chart title.
513    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    /// Add an output channel title.
519    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    /// Add an input channel title.
525    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    /// Add output channels' titles.
531    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    /// Add input channels' titles.
537    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    /// Show grid lines.
543    pub fn show_grid(&mut self, value: bool) -> &mut Self {
544        self.legacy_svg_mut().show_grid = value;
545        self
546    }
547
548    /// Set waveform line width.
549    pub fn line_width(&mut self, value: f32) -> &mut Self {
550        self.legacy_svg_mut().line_width = value;
551        self
552    }
553
554    /// Set background color.
555    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    /// Replace all output channel colors.
561    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    /// Append one output channel color.
567    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    /// Replace all input channel colors.
577    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    /// Append one input channel color.
583    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}