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