Skip to main content

insta_fun/
snapshot.rs

1use fundsp::prelude::*;
2
3use crate::abnormal::AbnormalSample;
4use crate::chart::generate_svg;
5use crate::config::{Processing, SnapshotConfig, SnapshotOutputMode, SvgChartConfig};
6use crate::input::InputSource;
7use crate::meta::SnapshotMetadata;
8use crate::meta_dashboard::generate_meta_dashboard_svg;
9use crate::wav::generate_wav;
10
11/// Describes a non-finite sample value captured during audio unit processing.
12///
13/// Abnormal samples are collected when [`SnapshotConfig::allow_abnormal_samples`] is `true`;
14/// the offending sample is replaced with `0.0` and recorded in
15/// [`AudioUnitSnapshotData::abnormalities`]. When `allow_abnormal_samples` is `false` (default)
16/// encountering any abnormal sample panics immediately.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SnapshotAbnormalSample {
19    /// The sample value was `f32::NAN`.
20    Nan,
21    /// The sample value was `f32::NEG_INFINITY`.
22    NegInf,
23    /// The sample value was `f32::INFINITY`.
24    PosInf,
25}
26
27impl std::fmt::Display for SnapshotAbnormalSample {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            SnapshotAbnormalSample::Nan => write!(f, "NaN"),
31            SnapshotAbnormalSample::NegInf => write!(f, "-∞"),
32            SnapshotAbnormalSample::PosInf => write!(f, "∞"),
33        }
34    }
35}
36
37impl From<AbnormalSample> for SnapshotAbnormalSample {
38    fn from(value: AbnormalSample) -> Self {
39        match value {
40            AbnormalSample::Nan => Self::Nan,
41            AbnormalSample::NegInf => Self::NegInf,
42            AbnormalSample::PosInf => Self::PosInf,
43        }
44    }
45}
46
47impl From<SnapshotAbnormalSample> for AbnormalSample {
48    fn from(value: SnapshotAbnormalSample) -> Self {
49        match value {
50            SnapshotAbnormalSample::Nan => Self::Nan,
51            SnapshotAbnormalSample::NegInf => Self::NegInf,
52            SnapshotAbnormalSample::PosInf => Self::PosInf,
53        }
54    }
55}
56
57/// Raw sample buffers and metadata captured from one run of an audio unit.
58///
59/// Returned by the `snapshot_audio_unit_data*` family of functions and passed to the
60/// closure argument of [`assert_audio_unit_meta_data_snapshot!`].
61///
62/// All sample buffers are indexed as `[channel][sample]`.
63#[derive(Debug, Clone)]
64pub struct AudioUnitSnapshotData {
65    /// Input samples fed to the unit, one `Vec<f32>` per input channel.
66    pub input_data: Vec<Vec<f32>>,
67    /// Output samples produced by the unit, one `Vec<f32>` per output channel.
68    pub output_data: Vec<Vec<f32>>,
69    /// Abnormal (NaN / ±∞) samples per output channel.
70    ///
71    /// Each entry is `(sample_index, kind)`. Only populated when
72    /// [`SnapshotConfig::allow_abnormal_samples`] is `true`; otherwise the processing
73    /// function panics on the first abnormal sample.
74    pub abnormalities: Vec<Vec<(usize, SnapshotAbnormalSample)>>,
75    /// Sample rate used during processing, in Hz.
76    pub sample_rate: f64,
77    /// Number of output samples captured (excluding warmup).
78    pub num_samples: usize,
79    /// Index of the first captured sample relative to the start of the stream
80    /// (i.e. the warmup length in samples).
81    pub start_sample: usize,
82}
83
84/// Create a snapshot of audio unit outputs (default: SVG; configure `output_mode` for WAV)
85/// ## Example
86///
87/// ```
88/// use insta_fun::prelude::*;
89/// use fundsp::prelude::*;
90///
91/// let unit = sine_hz::<f32>(440.0);
92/// let svg = snapshot_audio_unit(unit);
93/// println!("{}", svg.len());
94/// ```
95pub fn snapshot_audio_unit<N>(unit: N) -> Vec<u8>
96where
97    N: AudioUnit,
98{
99    snapshot_audio_unit_with_input_and_options(unit, InputSource::None, SnapshotConfig::default())
100}
101
102/// Capture raw sample data from an audio unit using default settings.
103///
104/// Equivalent to calling [`snapshot_audio_unit_data_with_input_and_options`] with
105/// [`InputSource::None`] and a default [`SnapshotConfig`].
106///
107/// ## Example
108///
109/// ```
110/// use insta_fun::prelude::*;
111/// use fundsp::prelude::*;
112///
113/// let data = snapshot_audio_unit_data(sine_hz::<f32>(440.0));
114/// assert_eq!(data.output_data.len(), 1);
115/// assert_eq!(data.output_data[0].len(), data.num_samples);
116/// ```
117pub fn snapshot_audio_unit_data<N>(unit: N) -> AudioUnitSnapshotData
118where
119    N: AudioUnit,
120{
121    snapshot_audio_unit_data_with_input_and_options(unit, InputSource::None, SnapshotConfig::default())
122}
123
124/// Create a snapshot of audio unit outputs with options (SVG or WAV via `output_mode`)
125///
126/// ## Example
127///
128/// ```
129/// use insta_fun::prelude::*;
130/// use fundsp::prelude::*;
131///
132/// let unit = sine_hz::<f32>(440.0);
133/// let svg = snapshot_audio_unit_with_options(unit, SnapshotConfig::default());
134/// println!("{}", svg.len());
135/// ```
136pub fn snapshot_audio_unit_with_options<N>(unit: N, options: SnapshotConfig) -> Vec<u8>
137where
138    N: AudioUnit,
139{
140    snapshot_audio_unit_with_input_and_options(unit, InputSource::None, options)
141}
142
143/// Capture raw sample data from an audio unit with custom [`SnapshotConfig`] settings.
144///
145/// Equivalent to calling [`snapshot_audio_unit_data_with_input_and_options`] with
146/// [`InputSource::None`].
147///
148/// ## Example
149///
150/// ```
151/// use insta_fun::prelude::*;
152/// use fundsp::prelude::*;
153///
154/// let config = SnapshotConfigBuilder::default().num_samples(512).build().unwrap();
155/// let data = snapshot_audio_unit_data_with_options(sine_hz::<f32>(440.0), config);
156/// assert_eq!(data.num_samples, 512);
157/// ```
158pub fn snapshot_audio_unit_data_with_options<N>(unit: N, options: SnapshotConfig) -> AudioUnitSnapshotData
159where
160    N: AudioUnit,
161{
162    snapshot_audio_unit_data_with_input_and_options(unit, InputSource::None, options)
163}
164
165/// Create a snapshot of audio unit inputs and outputs (SVG by default; WAV if selected)
166///
167/// ## Example
168///
169/// ```
170/// use insta_fun::prelude::*;
171/// use fundsp::prelude::*;
172///
173/// let unit = sine_hz::<f32>(440.0);
174/// let svg = snapshot_audio_unit_with_input(unit, InputSource::None);
175/// println!("{}", svg.len());
176/// ```
177pub fn snapshot_audio_unit_with_input<N>(unit: N, input_source: InputSource) -> Vec<u8>
178where
179    N: AudioUnit,
180{
181    snapshot_audio_unit_with_input_and_options(
182        unit,
183        input_source,
184        SnapshotConfig {
185            ..SnapshotConfig::default()
186        },
187    )
188}
189
190/// Capture raw sample data from an audio unit driven by the given [`InputSource`].
191///
192/// Uses a default [`SnapshotConfig`]. For full control over both input and config
193/// use [`snapshot_audio_unit_data_with_input_and_options`].
194///
195/// ## Example
196///
197/// ```
198/// use insta_fun::prelude::*;
199/// use fundsp::prelude::*;
200///
201/// let data = snapshot_audio_unit_data_with_input(lowpass_hz(1000.0, 0.7), InputSource::impulse());
202/// assert_eq!(data.input_data.len(), 1);
203/// assert_eq!(data.output_data.len(), 1);
204/// ```
205pub fn snapshot_audio_unit_data_with_input<N>(unit: N, input_source: InputSource) -> AudioUnitSnapshotData
206where
207    N: AudioUnit,
208{
209    snapshot_audio_unit_data_with_input_and_options(
210        unit,
211        input_source,
212        SnapshotConfig {
213            ..SnapshotConfig::default()
214        },
215    )
216}
217
218/// Create a snapshot (inputs & outputs) with options (choose SVG chart or WAV via `output_mode`)
219///
220/// ## Example
221///
222/// ```
223/// use insta_fun::prelude::*;
224/// use fundsp::prelude::*;
225///
226/// let config = SnapshotConfig::default();
227/// let unit = sine_hz::<f32>(440.0);
228/// let svg = snapshot_audio_unit_with_input_and_options(unit, InputSource::None, config);
229/// println!("{}", svg.len());
230/// ```
231pub fn snapshot_audio_unit_with_input_and_options<N>(
232    unit: N,
233    input_source: InputSource,
234    config: SnapshotConfig,
235) -> Vec<u8>
236where
237    N: AudioUnit,
238{
239    let snapshot_data = capture_audio_unit_data(unit, input_source, &config);
240    render_snapshot_output(&snapshot_data, &config.output_mode)
241}
242
243/// Capture raw sample data from an audio unit with a given [`InputSource`] and [`SnapshotConfig`].
244///
245/// This is the most flexible entry point for raw data capture. The returned
246/// [`AudioUnitSnapshotData`] gives direct access to input/output buffers and any
247/// recorded abnormal samples, without writing any SVG or WAV file.
248///
249/// ## Example
250///
251/// ```
252/// use insta_fun::prelude::*;
253/// use fundsp::prelude::*;
254///
255/// let config = SnapshotConfigBuilder::default().num_samples(64).build().unwrap();
256/// let data = snapshot_audio_unit_data_with_input_and_options(
257///     pass(),
258///     InputSource::Generator(Box::new(|i, _| if i % 2 == 0 { 1.0 } else { -1.0 })),
259///     config,
260/// );
261/// assert_eq!(data.output_data[0].len(), 64);
262/// ```
263pub fn snapshot_audio_unit_data_with_input_and_options<N>(
264    unit: N,
265    input_source: InputSource,
266    config: SnapshotConfig,
267) -> AudioUnitSnapshotData
268where
269    N: AudioUnit,
270{
271    capture_audio_unit_data(unit, input_source, &config)
272}
273
274/// Render a metadata dashboard as a single-page SVG.
275///
276/// This API is used by `assert_audio_unit_meta_data_snapshot!` after users compute
277/// metadata from `AudioUnitSnapshotData` in their lambda.
278pub fn snapshot_metadata_dashboard(metadata: &SnapshotMetadata) -> Vec<u8> {
279    snapshot_metadata_dashboard_with_chart_options(metadata, SvgChartConfig::default())
280}
281
282/// Render a metadata dashboard as a single-page SVG using chart options.
283pub fn snapshot_metadata_dashboard_with_chart_options(
284    metadata: &SnapshotMetadata,
285    chart_config: SvgChartConfig,
286) -> Vec<u8> {
287    if let Err(err) = metadata.validate() {
288        panic!("invalid metadata dashboard payload: {err}");
289    }
290    generate_meta_dashboard_svg(metadata, &chart_config)
291        .as_bytes()
292        .to_vec()
293}
294
295/// Render a metadata dashboard using styling from `SnapshotConfig`.
296///
297/// The dashboard output is always SVG. If `config.output_mode` is WAV,
298/// this function falls back to default `SvgChartConfig`.
299pub fn snapshot_metadata_dashboard_with_snapshot_config(
300    metadata: &SnapshotMetadata,
301    config: &SnapshotConfig,
302) -> Vec<u8> {
303    let chart_config = match &config.output_mode {
304        SnapshotOutputMode::SvgChart(chart) => chart.clone(),
305        SnapshotOutputMode::Wav(_) => SvgChartConfig::default(),
306    };
307    snapshot_metadata_dashboard_with_chart_options(metadata, chart_config)
308}
309
310fn capture_audio_unit_data<N>(
311    mut unit: N,
312    mut input_source: InputSource,
313    config: &SnapshotConfig,
314) -> AudioUnitSnapshotData
315where
316    N: AudioUnit,
317{
318    let num_inputs = N::inputs(&unit);
319    let num_outputs = N::outputs(&unit);
320
321    unit.set_sample_rate(config.sample_rate);
322    unit.reset();
323    unit.allocate();
324
325    let input_data = input_source.make_data(num_inputs, config.num_samples);
326
327    let mut output_data: Vec<Vec<f32>> = vec![vec![]; num_outputs];
328
329    let warmup_samples = config
330        .warm_up
331        .warm_up_samples(config.sample_rate, num_inputs);
332
333    let num_warmup_samples = warmup_samples
334        .iter()
335        .map(|ch| ch.len())
336        .next()
337        .unwrap_or_default();
338
339    let mut abnormalities: Vec<Vec<(usize, SnapshotAbnormalSample)>> = vec![vec![]; num_outputs];
340
341    let mut checked_sample = |mut sample: f32, ch: usize, i: usize| {
342        if sample.is_nan() || sample.is_infinite() {
343            let abnormality = SnapshotAbnormalSample::from(AbnormalSample::from(sample));
344
345            if config.allow_abnormal_samples {
346                abnormalities[ch].push((i, abnormality));
347                sample = 0.0;
348            } else {
349                panic!("Output channel #[{ch}] at sample [{i}] produced [{abnormality}] sample");
350            }
351        }
352        sample
353    };
354
355    (0..num_warmup_samples).for_each(|i| {
356        let mut input_frame = vec![0.0; num_inputs];
357        for ch in 0..num_inputs {
358            input_frame[ch] = warmup_samples[ch][i];
359        }
360        let mut output_frame = vec![0.0; num_outputs];
361        unit.tick(&input_frame, &mut output_frame);
362        // do nothing, warmup samples
363    });
364
365    match config.processing_mode {
366        Processing::Tick => {
367            (0..config.num_samples).for_each(|i| {
368                let mut input_frame = vec![0.0; num_inputs];
369                for ch in 0..num_inputs {
370                    input_frame[ch] = input_data[ch][i];
371                }
372                let mut output_frame = vec![0.0; num_outputs];
373                unit.tick(&input_frame, &mut output_frame);
374                for ch in 0..num_outputs {
375                    let sample = checked_sample(output_frame[ch], ch, i);
376                    output_data[ch].push(sample);
377                }
378            });
379        }
380        Processing::Batch(batch_size) => {
381            assert!(
382                batch_size <= MAX_BUFFER_SIZE as u8,
383                "Batch size must be less than or equal to [{MAX_BUFFER_SIZE}]"
384            );
385
386            let samples_index = (0..config.num_samples).collect::<Vec<_>>();
387            for chunk in samples_index.chunks(batch_size as usize) {
388                let mut input_buff = BufferVec::new(num_inputs);
389                for (frame_index, input_index) in chunk.iter().enumerate() {
390                    for (ch, input) in input_data.iter().enumerate() {
391                        let value: f32 = input[*input_index];
392                        input_buff.set_f32(ch, frame_index, value);
393                    }
394                }
395                let input_ref = input_buff.buffer_ref();
396                let mut output_buf = BufferVec::new(num_outputs);
397                let mut output_ref = output_buf.buffer_mut();
398
399                unit.process(chunk.len(), &input_ref, &mut output_ref);
400
401                for (ch, data) in output_data.iter_mut().enumerate() {
402                    data.extend(
403                        output_buf
404                            .channel_f32(ch)
405                            .iter()
406                            .enumerate()
407                            .map(|(i, &value)| checked_sample(value, ch, i + chunk[0])),
408                    );
409                }
410            }
411        }
412    }
413
414    AudioUnitSnapshotData {
415        input_data,
416        output_data,
417        abnormalities,
418        sample_rate: config.sample_rate,
419        num_samples: config.num_samples,
420        start_sample: config.warm_up.num_samples(config.sample_rate),
421    }
422}
423
424fn render_snapshot_output(data: &AudioUnitSnapshotData, output_mode: &SnapshotOutputMode) -> Vec<u8> {
425    match output_mode {
426        SnapshotOutputMode::SvgChart(svg_chart_config) => {
427            let abnormalities = data
428                .abnormalities
429                .iter()
430                .map(|channel| {
431                    channel
432                        .iter()
433                        .map(|(i, abnormality)| (*i, AbnormalSample::from(*abnormality)))
434                        .collect::<Vec<_>>()
435                })
436                .collect::<Vec<_>>();
437
438            generate_svg(
439                &data.input_data,
440                &data.output_data,
441                &abnormalities,
442                svg_chart_config,
443                data.sample_rate,
444                data.num_samples,
445                data.start_sample,
446            )
447            .as_bytes()
448            .to_vec()
449        }
450        SnapshotOutputMode::Wav(wav_output) => {
451            generate_wav(&data.output_data, wav_output, data.sample_rate, data.num_samples)
452        }
453    }
454}