Skip to main content

audio_samples/
lib.rs

1#![cfg_attr(feature = "simd", feature(portable_simd))]
2#![warn(clippy::all)]
3#![warn(clippy::pedantic)]
4#![warn(clippy::nursery)]
5#![deny(missing_docs)]
6// Additional strictness beyond default groups
7#![warn(clippy::unwrap_used)]
8#![warn(clippy::missing_errors_doc)]
9#![warn(clippy::missing_panics_doc)]
10#![warn(clippy::missing_safety_doc)]
11#![warn(clippy::undocumented_unsafe_blocks)]
12#![warn(clippy::exhaustive_enums)]
13#![warn(clippy::exhaustive_structs)]
14#![warn(clippy::panic_in_result_fn)]
15#![warn(clippy::unnecessary_wraps)]
16// Intentional allowances
17#![allow(clippy::too_many_arguments)]
18#![allow(clippy::too_many_lines)]
19#![allow(clippy::module_name_repetitions)]
20#![allow(clippy::collapsible_if)]
21#![allow(clippy::if_same_then_else)]
22#![allow(clippy::unnecessary_cast)]
23#![allow(clippy::cast_precision_loss)]
24#![allow(clippy::cast_possible_truncation)]
25#![allow(clippy::cast_sign_loss)]
26#![allow(clippy::cast_possible_wrap)]
27#![allow(clippy::needless_pass_by_value)]
28#![allow(clippy::tuple_array_conversions)]
29#![allow(clippy::unsafe_derive_deserialize)]
30#![allow(clippy::multiple_unsafe_ops_per_block)]
31#![allow(clippy::doc_markdown)]
32#![allow(unused_unsafe)]
33
34//! # AudioSamples
35//!
36//! A typed audio processing library for Rust that treats audio as a first-class,
37//! invariant-preserving object rather than an unstructured numeric buffer. is this library?
38//!
39//! `audio_samples` provides a single central type, [`AudioSamples<T>`], that pairs raw
40//! PCM data (backed by [`ndarray`](https://docs.rs/ndarray)) with essential metadata:
41//! sample rate, channel count, and memory layout. Every audio processing operation in the
42//! library is defined as a trait method on this type, ensuring that metadata travels with
43//! the data throughout a processing pipeline. does this library exist?
44//!
45//! Low-level audio APIs in Rust typically expose bare slices or `Vec<f32>` buffers,
46//! leaving metadata management to the caller. This encourages subtle bugs such as
47//! mismatched sample rates after resampling, or interleaved/non-interleaved confusion
48//! when passing buffers between components. `audio_samples` eliminates these error
49//! classes by encoding invariants directly into the type. should it be used?
50//!
51//! Start by creating an [`AudioSamples<T>`] from an ndarray or from one of the
52//! built-in signal generators (see [`utils::generation`]), then chain trait methods
53//! for any further processing. Feature flags keep the dependency footprint small –
54//! only enable what your application needs.
55//!
56//! ## Installation
57//!
58//! ```bash
59//! cargo add audio_samples
60//! ```
61//!
62//! ## Features
63//!
64//! The library uses a modular feature flag system. Only enable what you need.
65//!
66//! | Feature | Description |
67//! |---|---|
68//! | `statistics` | Peak, RMS, variance, zero-crossings, and other statistical measures |
69//! | `processing` | Normalise, scale, clip, DC offset removal, and other sample-level operations (implies `statistics`) |
70//! | `editing` | Time-domain editing: trim, pad, reverse, concatenate, fade (implies `statistics`, `random-generation`) |
71//! | `channels` | Channel operations: mono↔stereo conversion, channel extraction, interleave/deinterleave |
72//! | `transforms` | Spectrogram and frequency-domain transforms via the `spectrograms` crate |
73//! | `iir-filtering` | IIR filter design and application (Butterworth, Chebyshev I) |
74//! | `parametric-eq` | Multi-band parametric equaliser (implies `iir-filtering`) |
75//! | `dynamic-range` | Compression, limiting, and expansion with side-chain support |
76//! | `envelopes` | Amplitude, RMS, and analytical envelope followers (implies `dynamic-range`, `editing`, `random-generation`) |
77//! | `peak-picking` | Onset strength curve peak picking |
78//! | `onset-detection` | Onset detection (implies `transforms`, `peak-picking`, `processing`) |
79//! | `beat-tracking` | Beat tracking and tempo estimation |
80//! | `decomposition` | Audio source decomposition (implies `onset-detection`) |
81//! | `pitch-analysis` | YIN and autocorrelation pitch detection (implies `transforms`) |
82//! | `vad` | Voice activity detection |
83//! | `psychoacoustic` | Psychoacoustic analysis: Bark/Mel band layouts, ATH, masking thresholds, SMR (implies `transforms`) |
84//! | `resampling` | High-quality resampling via the `rubato` crate |
85//! | `plotting` | Signal plotting via `plotly` |
86//! | `fixed-size-audio` | Stack-allocated fixed-size audio buffers |
87//! | `random-generation` | Noise generators and stochastic signal sources (implies `rand`) |
88//! | `full` | Enables all of the above |
89//!
90//! See `Cargo.toml` for the complete dependency graph.
91//!
92//! ## Error Handling
93//!
94//! All fallible operations return [`AudioSampleResult<T>`], which is an alias for
95//! `Result<T, AudioSampleError>`. Errors are structured so that the variant indicates
96//! the category of failure and the inner type provides detail.
97//!
98//! ```rust
99//! use audio_samples::{AudioSampleError, AudioSampleResult, ParameterError};
100//!
101//! let audio_result: AudioSampleResult<()> = Err(AudioSampleError::Parameter(
102//!     ParameterError::invalid_value("window_size", "must be > 0"),
103//! ));
104//!
105//! match audio_result {
106//!     Ok(()) => {}
107//!     Err(AudioSampleError::Conversion(err)) => eprintln!("Conversion failed: {err}"),
108//!     Err(AudioSampleError::Parameter(err)) => eprintln!("Invalid parameter: {err}"),
109//!     Err(other_err) => eprintln!("Other error: {other_err}"),
110//! }
111//! ```
112//!
113//! ## Full Examples
114//!
115//! Examples live in the repository (see `examples/`) and in the crate-level docs on
116//! <https://docs.rs/audio_samples>.
117//!
118//! ## Quick Start
119//!
120//! ### Creating Audio Data
121//!
122//! ```rust
123//! use audio_samples::{AudioSamples, sample_rate};
124//! use ndarray::array;
125//!
126//! // Create mono audio
127//! let data = array![0.1f32, 0.5, -0.3, 0.8, -0.2];
128//! let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
129//!
130//! // Create stereo audio (channels × samples)
131//! let stereo_data = array![
132//!     [0.1f32, 0.5, -0.3],  // Left channel
133//!     [0.8f32, -0.2, 0.4]   // Right channel
134//! ];
135//! let stereo_audio = AudioSamples::new_multi_channel(stereo_data, sample_rate!(44100)).unwrap();
136//! ```
137//!
138//! ### Basic Statistics
139//!
140//! Requires the `statistics` feature.
141//!
142//! ```rust,ignore
143//! use audio_samples::{AudioStatistics, sine_wave, sample_rate};
144//! use std::time::Duration;
145//!
146//! // Generate a 440 Hz sine wave at 44.1 kHz sample rate, amplitude 1.0
147//! let audio = sine_wave::<f32>(440.0, Duration::from_secs_f32(1.0), sample_rate!(44100), 1.0);
148//!
149//! let peak           = audio.peak();
150//! let min            = audio.min_sample();
151//! let max            = audio.max_sample();
152//! let mean           = audio.mean();
153//! let rms            = audio.rms().unwrap();
154//! let variance       = audio.variance().unwrap();
155//! let zero_crossings = audio.zero_crossings();
156//! ```
157//!
158//! ### Processing Operations
159//!
160//! Requires the `processing` feature.
161//!
162//! ```rust,ignore
163//! use audio_samples::{AudioProcessing, NormalizationConfig, AudioSamples, sample_rate};
164//! use ndarray::array;
165//!
166//! let data = array![0.1f32, 0.5, -0.3, 0.8, -0.2];
167//! let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
168//!
169//! // Method chaining: each operation consumes and returns Self
170//! let audio = audio
171//!     .normalize(NormalizationConfig::peak(1.0))
172//!     .unwrap()
173//!     .scale(0.5)
174//!     .remove_dc_offset()
175//!     .unwrap();
176//! ```
177//!
178//! ### Type Conversions
179//!
180//! ```rust
181//! use audio_samples::{AudioSamples, AudioTypeConversion, sample_rate};
182//! use ndarray::array;
183//!
184//! let audio_f32 = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0], sample_rate!(44100)).unwrap();
185//! let audio_i16 = audio_f32.clone().to_type::<i16>();
186//! let audio_f64 = audio_f32.to_type::<f64>();
187//! ```
188//!
189//! ### Iterating Over Audio Data
190//!
191//! ```rust
192//! use audio_samples::{AudioSampleIterators, AudioSamples, sample_rate};
193//! use ndarray::array;
194//!
195//! let audio = AudioSamples::new_mono(
196//!     array![1.0f32, 2.0, 3.0, 4.0],
197//!     sample_rate!(44100),
198//! ).unwrap();
199//!
200//! // Iterate by frames (one sample per channel per time step)
201//! for frame in audio.frames() {
202//!     println!("Frame: {:?}", frame);
203//! }
204//!
205//! // Iterate by channels
206//! for channel in audio.channels() {
207//!     println!("Channel: {:?}", channel);
208//! }
209//! ```
210//!
211//! ### Psychoacoustic Analysis
212//!
213//! Requires the `psychoacoustic` feature.
214//!
215//! ```rust,ignore
216//! use audio_samples::{
217//!     AudioPerceptualAnalysis, BandLayout, PsychoacousticConfig, sine_wave, sample_rate,
218//! };
219//! use non_empty_slice::NonEmptySlice;
220//! use spectrograms::WindowType;
221//! use std::num::NonZeroUsize;
222//! use std::time::Duration;
223//!
224//! let signal = sine_wave::<f32>(440.0, Duration::from_millis(200), sample_rate!(44100), 0.8);
225//!
226//! // 24 Bark critical bands mapped onto 1024 MDCT bins.
227//! let layout = BandLayout::bark(
228//!     NonZeroUsize::new(24).unwrap(),
229//!     44100.0,
230//!     NonZeroUsize::new(1024).unwrap(),
231//! );
232//!
233//! let weights = vec![1.0_f32; 24];
234//! let config = PsychoacousticConfig::new(
235//!     -60.0, 14.5, 5.5, 25.0, 6.0,
236//!     NonEmptySlice::from_slice(&weights).unwrap(),
237//!     1e-10,
238//! );
239//!
240//! let result = signal
241//!     .analyse_psychoacoustic(WindowType::Hanning, &layout, &config)
242//!     .unwrap();
243//!
244//! // Bands with positive SMR are audible above the masking threshold.
245//! for metric in result.band_metrics.as_slice().iter() {
246//!     if metric.signal_to_mask_ratio > 0.0 {
247//!         println!(
248//!             "{:.0} Hz — SMR {:.1} dB (importance {:.2})",
249//!             metric.band.centre_frequency,
250//!             metric.signal_to_mask_ratio,
251//!             metric.importance,
252//!         );
253//!     }
254//! }
255//! ```
256//!
257//! ## Core Type System
258//!
259//! ### Supported Sample Types
260//!
261//! | Type | Width | Notes |
262//! |---|---|---|
263//! | `u8`  | 8-bit unsigned  | Mid-scale (128) represents silence |
264//! | `i16` | 16-bit signed   | CD-quality audio |
265//! | [`I24`](i24::I24) | 24-bit signed | From the `i24` crate |
266//! | `i32` | 32-bit signed   | High-dynamic-range integer audio |
267//! | `f32` | 32-bit float    | Most DSP operations use this type |
268//! | `f64` | 64-bit float    | High-precision processing |
269//!
270//! ### Type System Traits
271//!
272//! - **[`AudioSample`]** – Core trait all sample types implement. Provides constants
273//!   (`MAX`, `MIN`, `BITS`, `BYTES`, `LABEL`) and low-level byte operations.
274//!
275//! - **[`ConvertTo<T>`]** / **[`ConvertFrom<T>`]** – Audio-aware conversions with
276//!   correct scaling between bit depths (e.g. `i16 → f32` normalises to `[-1.0, 1.0]`).
277//!
278//! - **[`CastFrom<T>`]** / **[`CastInto<T>`]** – Raw numeric casts without scaling.
279//!   Use these when you need the raw integer value as a float for computation,
280//!   not as an audio amplitude.
281//!
282//! - **[`AudioTypeConversion`]** – High-level conversion methods on [`AudioSamples<T>`]
283//!   such as `as_f32()`, `as_i16()`, and `as_type::<U>()`.
284//!
285//! ## Signal Generation
286//!
287//! The [`utils::generation`] module provides functions for creating test and
288//! reference signals: [`sine_wave`], [`cosine_wave`], [`square_wave`],
289//! [`triangle_wave`], [`sawtooth_wave`], [`chirp`], [`impulse`], [`silence`],
290//! [`compound_tone`], and [`am_signal`].
291//!
292//! ```rust
293//! use audio_samples::{sine_wave, sample_rate};
294//! use audio_samples::utils::comparison;
295//! use std::time::Duration;
296//!
297//! let a = sine_wave::<f32>(440.0, Duration::from_secs(1), sample_rate!(44100), 1.0);
298//! let b = sine_wave::<f32>(440.0, Duration::from_secs(1), sample_rate!(44100), 1.0);
299//! let corr: f64 = comparison::correlation(&a, &b).unwrap();
300//! assert!(corr > 0.99);
301//! ```
302//!
303//! ## Documentation
304//!
305//! Full API documentation is available at [docs.rs/audio_samples](https://docs.rs/audio_samples).
306//!
307//! ## License
308//!
309//! MIT
310//!
311//! ## Contributing
312//!
313//! Contributions are welcome. Please open an issue or pull request on
314//! [GitHub](https://github.com/jmg049/audio_samples).
315
316// General todos in audio_samples:
317// - Define / decide on a policy for parallel processing
318// - Improve the error system, it has breadth currently, but not depth or aethetics. Rust set a new standard for error handling (i.e. nice errors that tell you exactly what went wrong, where and how to possible fix things.) which other libraries in other languages have tried to emulate.
319// - Better constants and better use of them -- e.g. the LEFT, RIGHT and SUPPORTED_DTYPES are rarely used
320
321/// Creates a `NonZeroUsize` from a compile-time constant with zero-check.
322///
323/// This macro creates a `NonZeroUsize` with a compile-time assertion that the value is
324/// non-zero, making it safe to use `new_unchecked` internally. If the value is zero, the
325/// code will fail to compile.
326///
327/// # Example
328///
329/// ```
330/// # use audio_samples::nzu;
331/// let size = nzu!(1024);
332/// assert_eq!(size.get(), 1024);
333/// ```
334///
335/// This will fail to compile:
336/// ```compile_fail
337/// # use audio_samples::nzu;
338/// let invalid = nzu!(0); // Compile error: assertion failed
339/// ```
340#[macro_export]
341macro_rules! nzu {
342    ($rate:expr) => {{
343        const RATE: usize = $rate;
344        const { assert!(RATE > 0, "non zero usize must be greater than 0") };
345        // SAFETY: We just asserted RATE > 0 at compile time
346        unsafe { ::core::num::NonZeroUsize::new_unchecked(RATE) }
347    }};
348}
349
350pub mod conversions;
351pub mod iterators;
352pub mod operations;
353#[cfg(feature = "resampling")]
354pub mod resampling;
355/// Core trait definitions for audio sample types and operations.
356pub mod traits;
357pub mod utils;
358/// Codec infrastructure: the [`AudioCodec`] trait, `encode`/`decode` free functions,
359/// and the [`PerceptualCodec`] implementation (requires `feature = "psychoacoustic"`).
360#[cfg(feature = "psychoacoustic")]
361pub mod codecs;
362
363mod error;
364mod repr;
365/// Fixed-size audio buffer types whose geometry is encoded in the type system.
366#[cfg(feature = "fixed-size-audio")]
367pub mod fixed_audio;
368
369pub mod simd_conversions;
370
371/// Educational / explainable layer: step-by-step operation explanations with
372/// before/after waveforms and term-maths formula rendering.
373///
374/// Enable with `--features educational`. See [`educational::open_explanation_document`]
375/// for the primary entry point.
376#[cfg(feature = "educational")]
377pub mod educational;
378
379pub use crate::error::{
380    AudioSampleError, AudioSampleResult, ConversionError, FeatureError, LayoutError,
381    ParameterError, ProcessingError,
382};
383pub use crate::repr::AudioData;
384pub use crate::repr::StereoAudioSamples;
385pub use crate::repr::{AudioSamples, SampleType};
386pub use crate::traits::{
387    AudioSample, AudioTypeConversion, CastFrom, CastInto, ConvertFrom, ConvertTo, StandardSample,
388};
389
390pub use crate::iterators::{AudioSampleIterators, ChannelIterator, FrameIterator, PaddingMode};
391
392#[cfg(feature = "editing")]
393pub use crate::iterators::WindowIterator;
394
395pub use crate::utils::generation::{
396    ToneComponent, am_signal, chirp, compound_tone, cosine_wave, impulse, sawtooth_wave, silence,
397    sine_wave, square_wave, triangle_wave,
398};
399
400#[cfg(feature = "channels")]
401pub use crate::utils::generation::{
402    multichannel_compound_tone, stereo_chirp, stereo_silence, stereo_sine_wave,
403};
404
405#[cfg(feature = "statistics")]
406pub use crate::operations::AudioStatistics;
407
408#[cfg(feature = "processing")]
409pub use crate::operations::AudioProcessing;
410
411#[cfg(any(feature = "processing", feature = "peak-picking"))]
412pub use crate::operations::types::{NormalizationConfig, NormalizationMethod};
413
414#[cfg(feature = "channels")]
415pub use crate::operations::AudioChannelOps;
416
417#[cfg(feature = "editing")]
418pub use crate::operations::AudioEditing;
419
420#[cfg(feature = "transforms")]
421pub use crate::operations::AudioTransforms;
422
423#[cfg(feature = "fixed-size-audio")]
424pub use crate::fixed_audio::{FixedSizeAudioSamples, FixedSizeMultiChannelAudioSamples};
425
426#[cfg(feature = "onset-detection")]
427pub use crate::operations::traits::AudioOnsetDetection;
428
429#[cfg(feature = "decomposition")]
430pub use crate::operations::traits::AudioDecomposition;
431
432#[cfg(feature = "dynamic-range")]
433pub use crate::operations::traits::AudioDynamicRange;
434
435#[cfg(feature = "iir-filtering")]
436pub use crate::operations::traits::AudioIirFiltering;
437
438#[cfg(feature = "parametric-eq")]
439pub use crate::operations::traits::AudioParametricEq;
440
441#[cfg(feature = "pitch-analysis")]
442pub use crate::operations::traits::AudioPitchAnalysis;
443
444#[cfg(feature = "vad")]
445pub use crate::operations::traits::AudioVoiceActivityDetection;
446
447#[cfg(feature = "psychoacoustic")]
448pub use crate::codecs::perceptual::{
449    AudioPerceptualAnalysis, AudioCodec,
450    Band, BandLayout, BandMetric, BandMetrics,
451    EncodedSegment,
452    PerceptualAnalysisResult, PerceptualCodec, PerceptualEncodedAudio,
453    StereoPerceptualCodec, StereoPerceptualEncodedAudio,
454    PsychoacousticConfig,
455    apply_temporal_masking, detect_transient_windows,
456    reconstruct_signal, analyse_signal_with_window_size,
457};
458
459#[cfg(feature = "psychoacoustic")]
460pub use crate::codecs::perceptual::bands::scale_band_layout;
461
462#[cfg(feature = "psychoacoustic")]
463pub use crate::codecs::perceptual::masking::{
464    MaskerType, classify_masker_types,
465    absolute_threshold_of_hearing, spreading_attenuation, compute_smr,
466};
467
468#[cfg(feature = "psychoacoustic")]
469pub use crate::codecs::perceptual::stereo::{mid_side_encode, mid_side_decode};
470
471#[cfg(feature = "psychoacoustic")]
472pub use crate::codecs::perceptual::quantization::{
473    BandAllocation, BitAllocationResult,
474    allocate_bits, dequantize, dequantize_band,
475    quantize, quantize_band, step_size_from_allowed_noise,
476};
477
478#[cfg(feature = "psychoacoustic")]
479pub use crate::codecs::perceptual::codec::{decode as codec_decode, encode as codec_encode};
480
481#[cfg(feature = "random-generation")]
482pub use crate::utils::generation::{brown_noise, pink_noise, white_noise};
483
484#[cfg(feature = "educational")]
485pub use explainable::{ExplainMode, Explainable, Explaining, Explanation};
486
487#[cfg(all(feature = "educational", feature = "processing"))]
488pub use crate::operations::traits::AudioProcessingExt;
489
490pub use i24::{I24, PackedStruct}; // Re-export I24 type that has the AudioSample implementation
491
492use ndarray::{Array1, Array2};
493#[cfg(feature = "resampling")]
494pub use resampling::{resample, resample_by_ratio};
495
496/// A tagged result that is either a 1-D mono array or a 2-D multi-channel array.
497///
498/// ## Purpose
499///
500/// Several operations on [`AudioSamples<T>`] produce output whose dimensionality
501/// mirrors the input: a mono input yields a 1-D result; a multi-channel input yields
502/// a 2-D result. `NdResult` captures both possibilities in a single return type,
503/// letting callers branch on the actual dimensionality at runtime rather than
504/// discarding the channel structure.
505///
506/// ## Intended Usage
507///
508/// Pattern-match on the variants directly, or use [`into_array1`](NdResult::into_array1) /
509/// [`into_array2`](NdResult::into_array2) when you already know the expected shape.
510/// Prefer the safe helpers over the `_unchecked` variants unless performance is
511/// critical and the shape has already been verified.
512///
513/// ## Invariants
514///
515/// - The [`Mono`](NdResult::Mono) variant always holds a 1-D array with at least one element.
516/// - The [`MultiChannel`](NdResult::MultiChannel) variant always holds a 2-D array with at
517///   least one channel and at least one sample per channel.
518///
519/// ## Assumptions
520///
521/// Callers must not construct `NdResult` directly with empty arrays.
522/// All library functions that return `NdResult` uphold the non-empty invariants above.
523#[non_exhaustive]
524pub enum NdResult<T>
525where
526    T: StandardSample,
527{
528    /// A single-channel (mono) result stored as a 1-D ndarray.
529    Mono(Array1<T>),
530    /// A multi-channel result stored as a 2-D ndarray (channels × samples).
531    MultiChannel(Array2<T>),
532}
533
534impl<T> NdResult<T>
535where
536    T: StandardSample,
537{
538    /// Extracts the inner 1-D array if this result is [`Mono`](NdResult::Mono).
539    ///
540    /// # Returns
541    ///
542    /// `Some(array)` when the variant is `Mono`; `None` when it is `MultiChannel`.
543    ///
544    /// # Examples
545    ///
546    /// ```rust
547    /// use audio_samples::NdResult;
548    /// use ndarray::array;
549    ///
550    /// let result: NdResult<f32> = NdResult::Mono(array![0.1, 0.2, 0.3]);
551    /// assert!(result.into_array1().is_some());
552    ///
553    /// let result: NdResult<f32> = NdResult::MultiChannel(ndarray::array![[0.1, 0.2], [0.3, 0.4]]);
554    /// assert!(result.into_array1().is_none());
555    /// ```
556    #[inline]
557    #[must_use]
558    pub fn into_array1(self) -> Option<Array1<T>> {
559        match self {
560            Self::Mono(arr) => Some(arr),
561            Self::MultiChannel(_) => None,
562        }
563    }
564
565    /// Extracts the inner 1-D array without checking the variant.
566    ///
567    /// # Safety
568    ///
569    /// The caller must ensure that this `NdResult` is the [`Mono`](NdResult::Mono)
570    /// variant. Calling this on a [`MultiChannel`](NdResult::MultiChannel) value is
571    /// a programmer error and will panic in debug builds. There is no undefined
572    /// behaviour, but the panic is not recoverable.
573    ///
574    /// # Returns
575    ///
576    /// The inner `Array1<T>`.
577    ///
578    /// # Panics
579    ///
580    /// Panics if the variant is `MultiChannel`.
581    ///
582    /// # Examples
583    ///
584    /// ```rust
585    /// use audio_samples::NdResult;
586    /// use ndarray::array;
587    ///
588    /// let result: NdResult<f32> = NdResult::Mono(array![0.1, 0.2, 0.3]);
589    /// // SAFETY: we just constructed a Mono variant above
590    /// let arr = unsafe { result.into_array1_unchecked() };
591    /// assert_eq!(arr.len(), 3);
592    /// ```
593    #[inline]
594    #[must_use]
595    pub unsafe fn into_array1_unchecked(self) -> Array1<T> {
596        match self {
597            Self::Mono(arr) => arr,
598            Self::MultiChannel(_) => panic!("Cannot convert MultiChannel to Array1"),
599        }
600    }
601
602    /// Extracts the inner 2-D array if this result is [`MultiChannel`](NdResult::MultiChannel).
603    ///
604    /// # Returns
605    ///
606    /// `Some(array)` when the variant is `MultiChannel`; `None` when it is `Mono`.
607    ///
608    /// # Examples
609    ///
610    /// ```rust
611    /// use audio_samples::NdResult;
612    /// use ndarray::array;
613    ///
614    /// let result: NdResult<f32> = NdResult::MultiChannel(array![[0.1, 0.2], [0.3, 0.4]]);
615    /// assert!(result.into_array2().is_some());
616    ///
617    /// let result: NdResult<f32> = NdResult::Mono(array![0.1, 0.2, 0.3]);
618    /// assert!(result.into_array2().is_none());
619    /// ```
620    #[inline]
621    #[must_use]
622    pub fn into_array2(self) -> Option<Array2<T>> {
623        match self {
624            Self::Mono(_) => None,
625            Self::MultiChannel(arr) => Some(arr),
626        }
627    }
628
629    /// Extracts the inner 2-D array without checking the variant.
630    ///
631    /// # Safety
632    ///
633    /// The caller must ensure that this `NdResult` is the
634    /// [`MultiChannel`](NdResult::MultiChannel) variant. Calling this on a
635    /// [`Mono`](NdResult::Mono) value is a programmer error and will panic in debug
636    /// builds. There is no undefined behaviour, but the panic is not recoverable.
637    ///
638    /// # Returns
639    ///
640    /// The inner `Array2<T>`.
641    ///
642    /// # Panics
643    ///
644    /// Panics if the variant is `Mono`.
645    ///
646    /// # Examples
647    ///
648    /// ```rust
649    /// use audio_samples::NdResult;
650    /// use ndarray::array;
651    ///
652    /// let result: NdResult<f32> = NdResult::MultiChannel(array![[0.1, 0.2], [0.3, 0.4]]);
653    /// // SAFETY: we just constructed a MultiChannel variant above
654    /// let arr = unsafe { result.into_array2_unchecked() };
655    /// assert_eq!(arr.nrows(), 2);
656    /// ```
657    #[inline]
658    #[must_use]
659    pub unsafe fn into_array2_unchecked(self) -> Array2<T> {
660        match self {
661            Self::Mono(_) => panic!("Cannot convert Mono to Array2"),
662            Self::MultiChannel(arr) => arr,
663        }
664    }
665}