insta_fun/
config.rs

1use derive_builder::Builder;
2use fundsp::DEFAULT_SR;
3
4use crate::warmup::WarmUp;
5
6pub use crate::chart::Layout;
7
8const DEFAULT_HEIGHT: usize = 500;
9
10#[derive(Debug, Clone, Builder)]
11/// Configuration for snapshotting an audio unit.
12pub struct SnapshotConfig {
13    // Audio configuration
14    /// Sample rate of the audio unit.
15    ///
16    /// Default is 44100.0 [fundsp::DEFAULT_SR]
17    #[builder(default = "fundsp::DEFAULT_SR")]
18    pub sample_rate: f64,
19    /// Number of samples to generate.
20    ///
21    /// Default is 1024
22    #[builder(default = "1024")]
23    pub num_samples: usize,
24    /// Processing mode for snapshotting an audio unit.
25    ///
26    /// Default - `Tick`
27    #[builder(default = "Processing::default()")]
28    pub processing_mode: Processing,
29    /// Warm-up mode for snapshotting an audio unit.
30    ///
31    /// Default - `WarmUp::None`
32    #[builder(default = "WarmUp::None")]
33    pub warm_up: WarmUp,
34    /// How to handle abnormal samples: `NaN`,`±Infinity`
35    ///
36    /// When set to `true` abnormal samples are allowed during processing,
37    /// but skipped in actual output. Plotted with labeled dots.
38    ///
39    /// When set to `false` and encoutered abnormal samples,
40    /// the snapshotting process will panic.
41    #[builder(default = "false")]
42    pub allow_abnormal_samples: bool,
43
44    /// Snaphsot output mode
45    ///
46    /// Use configurable chart for visual snapshots
47    ///
48    /// Use Wav16 or Wav32 for audial snapshots
49    #[builder(
50        default = "SnapshotOutputMode::SvgChart(SvgChartConfig::default())",
51        try_setter,
52        setter(into)
53    )]
54    pub output_mode: SnapshotOutputMode,
55}
56
57#[derive(Debug, Clone, Builder)]
58pub struct SvgChartConfig {
59    // Chart configuration
60    /// Chart layout
61    ///
62    /// Whether to plot channels on separate charts or combined charts.
63    ///
64    /// Default - `Layout::Separate`
65    #[builder(default)]
66    pub chart_layout: Layout,
67    /// Whether to include inputs in snapshot
68    ///
69    /// Default - `false`
70    #[builder(default)]
71    pub with_inputs: bool,
72    /// Optional width of the SVG `viewBox`
73    ///
74    /// `None` means proportional to num_samples
75    #[builder(default, setter(strip_option))]
76    pub svg_width: Option<usize>,
77    /// Height of **one** channel in the SVG `viewBox`
78    ///
79    /// Default - 500
80    #[builder(default = "DEFAULT_HEIGHT")]
81    pub svg_height_per_channel: usize,
82
83    // Chart labels
84    /// Show ax- labels
85    ///
86    /// Default - `true`
87    #[builder(default = "true")]
88    pub show_labels: bool,
89    /// X axis labels format
90    ///
91    /// Whether to format X axis labels as time
92    ///
93    /// Default - `false`
94    #[builder(default)]
95    pub format_x_axis_labels_as_time: bool,
96    /// Maximum number of labels along X axis
97    ///
98    /// Default - `Some(5)`
99    #[builder(default = "Some(5)")]
100    pub max_labels_x_axis: Option<usize>,
101    /// Optional chart title
102    ///
103    /// Default - `None`
104    #[builder(default, setter(into, strip_option))]
105    pub chart_title: Option<String>,
106    /// Optional titles for output channels
107    ///
108    /// Default - empty `Vec`
109    #[builder(default, setter(into, each(into, name = "output_title")))]
110    pub output_titles: Vec<String>,
111    /// Optional titles for input channels
112    ///
113    /// Default - empty `Vec`
114    #[builder(default, setter(into, each(into, name = "input_title")))]
115    pub input_titles: Vec<String>,
116
117    // Lines
118    /// Show grid lines on the chart
119    ///
120    /// Default - `false`
121    #[builder(default)]
122    pub show_grid: bool,
123    /// Waveform line thickness
124    ///
125    /// Default - 2.0
126    #[builder(default = "2.0")]
127    pub line_width: f32,
128
129    // Chart colors
130    /// Chart background color (hex string)
131    ///
132    /// Default - "#000000" (black)
133    #[builder(default = "\"#000000\".to_string()", setter(into))]
134    pub background_color: String,
135    /// Custom colors for output channels (hex strings)
136    ///
137    /// Default - `None` (uses default palette)
138    #[builder(default, setter(into, strip_option, each(into, name = "output_color")))]
139    pub output_colors: Option<Vec<String>>,
140    /// Custom colors for input channels (hex strings)
141    ///
142    /// Default - `None` (uses default palette)
143    #[builder(default, setter(into, strip_option, each(into, name = "input_color")))]
144    pub input_colors: Option<Vec<String>>,
145}
146
147#[derive(Debug, Clone)]
148pub enum WavOutput {
149    Wav16,
150    Wav32,
151}
152
153#[derive(Debug, Clone)]
154pub enum SnapshotOutputMode {
155    SvgChart(SvgChartConfig),
156    Wav(WavOutput),
157}
158
159/// Processing mode for snapshotting an audio unit.
160#[derive(Debug, Clone, Copy, Default)]
161pub enum Processing {
162    #[default]
163    /// Process one sample at a time.
164    Tick,
165    /// Process a batch of samples at a time.
166    ///
167    /// max batch size is 64 [fundsp::MAX_BUFFER_SIZE]
168    Batch(u8),
169}
170
171impl TryFrom<SvgChartConfigBuilder> for SnapshotOutputMode {
172    type Error = SvgChartConfigBuilderError;
173
174    fn try_from(value: SvgChartConfigBuilder) -> Result<Self, Self::Error> {
175        let inner = value.build()?;
176        Ok(SnapshotOutputMode::SvgChart(inner))
177    }
178}
179
180impl From<WavOutput> for SnapshotOutputMode {
181    fn from(value: WavOutput) -> Self {
182        SnapshotOutputMode::Wav(value)
183    }
184}
185
186impl From<SvgChartConfig> for SnapshotOutputMode {
187    fn from(value: SvgChartConfig) -> Self {
188        SnapshotOutputMode::SvgChart(value)
189    }
190}
191
192impl Default for SnapshotConfig {
193    fn default() -> Self {
194        Self {
195            num_samples: 1024,
196            sample_rate: DEFAULT_SR,
197            processing_mode: Processing::default(),
198            warm_up: WarmUp::default(),
199            allow_abnormal_samples: false,
200            output_mode: SnapshotOutputMode::SvgChart(SvgChartConfig::default()),
201        }
202    }
203}
204
205impl Default for SvgChartConfig {
206    fn default() -> Self {
207        Self {
208            svg_width: None,
209            svg_height_per_channel: DEFAULT_HEIGHT,
210            with_inputs: false,
211            chart_title: None,
212            output_titles: Vec::new(),
213            input_titles: Vec::new(),
214            show_grid: false,
215            show_labels: true,
216            max_labels_x_axis: Some(5),
217            output_colors: None,
218            input_colors: None,
219            background_color: "#000000".to_string(),
220            line_width: 2.0,
221            chart_layout: Layout::default(),
222            format_x_axis_labels_as_time: false,
223        }
224    }
225}
226
227impl SnapshotConfig {
228    /// Intnded for internal use only
229    ///
230    /// Used by macros to determine snapshot filename
231    pub fn file_name(&self, name: Option<&'_ str>) -> String {
232        match &self.output_mode {
233            SnapshotOutputMode::SvgChart(svg_chart_config) => match name {
234                Some(name) => format!("{name}.svg"),
235                None => match &svg_chart_config.chart_title {
236                    Some(name) => format!("{name}.svg"),
237                    None => ".svg".to_string(),
238                },
239            },
240            SnapshotOutputMode::Wav(_) => match name {
241                Some(name) => format!("{name}.wav"),
242                None => ".wav".to_string(),
243            },
244        }
245    }
246
247    /// Intnded for internal use only
248    ///
249    /// Used by macros to set chart title if not already set
250    pub fn maybe_title(&mut self, name: &str) {
251        if matches!(
252            self.output_mode,
253            SnapshotOutputMode::SvgChart(SvgChartConfig {
254                chart_title: None,
255                ..
256            })
257        ) && let SnapshotOutputMode::SvgChart(ref mut svg_chart_config) = self.output_mode
258        {
259            svg_chart_config.chart_title = Some(name.to_string());
260        }
261    }
262}
263
264/// Legacy (v1.x) compatibility helpers
265impl SnapshotConfigBuilder {
266    /// Internal helper to ensure we have a mutable reference to an underlying `SvgChartConfig`
267    /// Creating a default one if `output_mode` is `None` or replacing a `Wav` variant.
268    fn legacy_svg_mut(&mut self) -> &mut SvgChartConfig {
269        // If already a chart, return it.
270        if let Some(SnapshotOutputMode::SvgChart(ref mut chart)) = self.output_mode {
271            return chart;
272        }
273        // Otherwise replace (None or Wav) with default chart.
274        self.output_mode = Some(SnapshotOutputMode::SvgChart(SvgChartConfig::default()));
275        match self.output_mode {
276            Some(SnapshotOutputMode::SvgChart(ref mut chart)) => chart,
277            _ => unreachable!("Output mode was just set to SvgChart"),
278        }
279    }
280
281    /// Set chart layout.
282    pub fn chart_layout(&mut self, value: Layout) -> &mut Self {
283        self.legacy_svg_mut().chart_layout = value;
284        self
285    }
286
287    /// Include inputs in chart.
288    pub fn with_inputs(&mut self, value: bool) -> &mut Self {
289        self.legacy_svg_mut().with_inputs = value;
290        self
291    }
292
293    /// Set fixed SVG width.
294    pub fn svg_width(&mut self, value: usize) -> &mut Self {
295        self.legacy_svg_mut().svg_width = Some(value);
296        self
297    }
298
299    /// Set SVG height per channel.
300    pub fn svg_height_per_channel(&mut self, value: usize) -> &mut Self {
301        self.legacy_svg_mut().svg_height_per_channel = value;
302        self
303    }
304
305    /// Toggle label visibility.
306    pub fn show_labels(&mut self, value: bool) -> &mut Self {
307        self.legacy_svg_mut().show_labels = value;
308        self
309    }
310
311    /// Format X axis labels as time.
312    pub fn format_x_axis_labels_as_time(&mut self, value: bool) -> &mut Self {
313        self.legacy_svg_mut().format_x_axis_labels_as_time = value;
314        self
315    }
316
317    /// Set maximum number of X axis labels.
318    pub fn max_labels_x_axis(&mut self, value: Option<usize>) -> &mut Self {
319        self.legacy_svg_mut().max_labels_x_axis = value;
320        self
321    }
322
323    /// Set chart title.
324    pub fn chart_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
325        self.legacy_svg_mut().chart_title = Some(value.into());
326        self
327    }
328
329    /// Add an output channel title.
330    pub fn output_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
331        self.legacy_svg_mut().output_titles.push(value.into());
332        self
333    }
334
335    /// Add an input channel title.
336    pub fn input_title<S: Into<String>>(&mut self, value: S) -> &mut Self {
337        self.legacy_svg_mut().input_titles.push(value.into());
338        self
339    }
340
341    /// Add output channels' titles.
342    pub fn output_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
343        self.legacy_svg_mut().output_titles = value.into();
344        self
345    }
346
347    /// Add input channels' titles.
348    pub fn input_titles<S: Into<Vec<String>>>(&mut self, value: S) -> &mut Self {
349        self.legacy_svg_mut().input_titles = value.into();
350        self
351    }
352
353    /// Show grid lines.
354    pub fn show_grid(&mut self, value: bool) -> &mut Self {
355        self.legacy_svg_mut().show_grid = value;
356        self
357    }
358
359    /// Set waveform line width.
360    pub fn line_width(&mut self, value: f32) -> &mut Self {
361        self.legacy_svg_mut().line_width = value;
362        self
363    }
364
365    /// Set background color.
366    pub fn background_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
367        self.legacy_svg_mut().background_color = value.into();
368        self
369    }
370
371    /// Replace all output channel colors.
372    pub fn output_colors(&mut self, colors: Vec<String>) -> &mut Self {
373        self.legacy_svg_mut().output_colors = Some(colors);
374        self
375    }
376
377    /// Append one output channel color.
378    pub fn output_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
379        let chart = self.legacy_svg_mut();
380        chart
381            .output_colors
382            .get_or_insert_with(Vec::new)
383            .push(value.into());
384        self
385    }
386
387    /// Replace all input channel colors.
388    pub fn input_colors(&mut self, colors: Vec<String>) -> &mut Self {
389        self.legacy_svg_mut().input_colors = Some(colors);
390        self
391    }
392
393    /// Append one input channel color.
394    pub fn input_color<S: Into<String>>(&mut self, value: S) -> &mut Self {
395        let chart = self.legacy_svg_mut();
396        chart
397            .input_colors
398            .get_or_insert_with(Vec::new)
399            .push(value.into());
400        self
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_default_builder() {
410        SnapshotConfigBuilder::default()
411            .build()
412            .expect("defaul config builds");
413    }
414
415    #[test]
416    fn legacy_config_compat() {
417        SnapshotConfigBuilder::default()
418            .chart_title("Complete Waveform Test")
419            .show_grid(true)
420            .show_labels(true)
421            .with_inputs(true)
422            .output_color("#FF6B6B")
423            .input_color("#95E77E")
424            .background_color("#2C3E50")
425            .line_width(3.0)
426            .svg_width(1200)
427            .svg_height_per_channel(120)
428            .build()
429            .expect("legacy config builds");
430    }
431}