audio_samples/
conversions.rs

1//! # Audio Sample Type Conversions
2//!
3//! This module provides **type-safe conversion operations**
4//! between different audio sample representations for [`AudioSamples`].
5//!
6//! Conversions are exposed through the [`AudioTypeConversion`] trait,
7//! which is automatically implemented for `AudioSamples<T>` where `T` is any supported sample type.
8//! Users do not need to interact with the trait directly in most cases — the methods can be called
9//! directly on an `AudioSamples` instance:
10//!
11//! ```rust
12//! use audio_samples::{AudioSamples, AudioTypeConversion};
13//! use ndarray::array;
14//!
15//! # fn example() {
16//! // Create mono audio data with f32 samples
17//! let audio_f32 = AudioSamples::new_mono(array![0.5f32, -0.3, 0.8], 44_100);
18//!
19//! // Convert to i16 (audio-aware conversion)
20//! let audio_i16 = audio_f32.to_format::<i16>();
21//!
22//! // Consume and convert
23//! let audio_back = audio_i16.to_type::<f32>();
24//! # }
25//! ```
26//!
27//! ## Supported Conversions
28//! Conversions are supported between the following types:
29//!
30//! - `i16`
31//! - [`I24`] (24-bit PCM)
32//! - `i32`
33//! - `f32`
34//! - `f64`
35//!
36//! Conversions are applied element-by-element via the crate's conversion traits.
37//! When converting from floating-point to fixed-width integer formats, extreme values are clamped
38//! to the destination range to avoid overflow.
39//!
40//! ## Typical Use Cases
41//! - Converting audio data from disk to a processing format (e.g. `i16` → `f32`)
42//! - Preparing audio buffers for model input
43//! - Reducing memory footprint after processing (`f64` → `i16`)
44//!
45//! ## Allocation and Shape
46//! All conversion methods allocate a new owned buffer and preserve:
47//! - sample rate
48//! - channel count and sample count
49//! - sample ordering (mono, or `[channel, frame]` indexing for multi-channel)
50//!
51//! Maybe in future versions, zero-allocation conversions could be supported for certain cases where they share the same underlying representation.
52//!
53//! ## Error Handling
54//! These conversion methods do not return `Result` and do not require contiguous storage.
55//! They always produce a new owned `AudioSamples` value.
56//!
57//! ## Audio-aware conversion vs raw casting
58//! - `to_format` / `to_type` use the audio-aware [`ConvertTo`] conversions (e.g. integer PCM to
59//!   floating-point typically maps into $[-1.0, 1.0]$).
60//! - `cast_as` / `cast_to` use raw numeric casting via [`CastFrom`] (no audio scaling).
61//!
62//! ## API Summary
63//! - [`to_format`](crate::AudioTypeConversion::to_format): Borrow and convert (creates a new buffer)
64//! - [`to_type`](crate::AudioTypeConversion::to_type): Consume and convert (creates a new buffer)
65//! - [`cast_as`](crate::AudioTypeConversion::cast_as): Borrow and cast without audio-aware scaling
66//! - [`cast_to`](crate::AudioTypeConversion::cast_to): Consume and cast without audio-aware scaling
67use crate::{AudioSample, AudioSamples, AudioTypeConversion, CastFrom, ConvertTo, I24};
68
69// Single blanket implementation that satisfies all requirements
70impl<'a, T: AudioSample> AudioTypeConversion<'a, T> for AudioSamples<'a, T>
71where
72    i16: ConvertTo<T>,
73    I24: ConvertTo<T>,
74    i32: ConvertTo<T>,
75    f32: ConvertTo<T>,
76    f64: ConvertTo<T>,
77    T: ConvertTo<T>,
78{
79    fn to_format<O>(&self) -> AudioSamples<'static, O>
80    where
81        T: ConvertTo<O>,
82        O: AudioSample + ConvertTo<T>,
83    {
84        self.map_into(O::convert_from)
85    }
86
87    fn to_type<O: AudioSample + ConvertTo<T>>(self) -> AudioSamples<'static, O>
88    where
89        T: ConvertTo<O>,
90    {
91        self.map_into(O::convert_from)
92    }
93
94    fn cast_as<O>(&self) -> AudioSamples<'static, O>
95    where
96        O: AudioSample + CastFrom<T>,
97    {
98        self.map_into(O::cast_from)
99    }
100
101    fn cast_to<O>(self) -> AudioSamples<'static, O>
102    where
103        O: AudioSample + CastFrom<T>,
104    {
105        self.map_into(O::cast_from)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::sample_rate;
113    use approx_eq::assert_approx_eq;
114    use ndarray::array;
115
116    #[test]
117    fn test_mono_f32_to_i16_conversion() {
118        let data = array![0.5f32, -0.3, 0.0, 1.0, -1.0];
119        let audio_f32 = AudioSamples::new_mono(data, sample_rate!(44100));
120
121        let audio_i16 = audio_f32.to_format::<i16>();
122
123        assert_eq!(audio_i16.sample_rate(), sample_rate!(44100));
124        assert_eq!(audio_i16.num_channels(), 1);
125        assert_eq!(audio_i16.samples_per_channel(), 5);
126
127        // Check specific conversions (allow for minor rounding differences)
128        let converted_data = audio_i16;
129        // 0.5 * 32767 can be either 16383 or 16384 depending on rounding
130        assert!(
131            (converted_data[0] - 16383).abs() <= 1,
132            "Expected ~16383±1, got {}",
133            converted_data[0]
134        );
135        assert_eq!(converted_data[2], 0); // 0.0 -> 0
136        assert_eq!(converted_data[3], 32767); // 1.0 -> i16::MAX
137        // -1.0 can map to either -32767 or -32768 depending on implementation
138        assert!(
139            converted_data[4] <= -32767,
140            "Expected <= -32767, got {}",
141            converted_data[4]
142        );
143    }
144
145    #[test]
146    fn test_multi_channel_i16_to_f32_conversion() {
147        // Create 2 channels with 3 samples each: [[ch0_sample0, ch0_sample1, ch0_sample2], [ch1_sample0, ch1_sample1, ch1_sample2]]
148        let data = array![[16384i16, 32767, 0], [-16384, -32768, 8192]];
149        let audio_i16 = AudioSamples::new_multi_channel(data, sample_rate!(48000));
150
151        let audio_f32 = audio_i16.to_format::<f32>();
152        assert_eq!(audio_f32.sample_rate(), sample_rate!(48000));
153        assert_eq!(audio_f32.num_channels(), 2);
154        assert_eq!(audio_f32.samples_per_channel(), 3);
155
156        // Check specific conversions
157        let converted_data = audio_f32;
158        assert_approx_eq!(converted_data[[0, 0]] as f64, 0.5, 1e-4); // 16384/32767 ≈ 0.5
159        assert_approx_eq!(converted_data[[0, 1]] as f64, 1.0, 1e-4); // 32767/32767 = 1.0
160        assert_approx_eq!(converted_data[[0, 2]] as f64, 0.0, 1e-10); // 0/32767 = 0.0
161        assert_approx_eq!(converted_data[[1, 0]] as f64, -0.5, 1e-4); // -16384/32768 ≈ -0.5
162        assert_approx_eq!(converted_data[[1, 1]] as f64, -1.0, 1e-4); // -32768 conversion (asymmetric range)
163        assert_approx_eq!(converted_data[[1, 2]] as f64, 0.25, 1e-4); // 8192/32768 = 0.25
164    }
165
166    #[test]
167    fn test_consuming_conversion() {
168        let data = array![0.1f32, 0.2, 0.3];
169        let audio_f32 = AudioSamples::new_mono(data, sample_rate!(44100));
170
171        // Test consuming conversion
172        let audio_i16 = audio_f32.to_type::<i16>();
173
174        assert_eq!(audio_i16.num_channels(), 1);
175        assert_eq!(audio_i16.samples_per_channel(), 3);
176
177        // Verify conversion accuracy
178        let converted_data = audio_i16;
179        assert_eq!(converted_data[0], 3277); // 0.1 * 32767 ≈ 3277
180        assert_eq!(converted_data[1], 6553); // 0.2 * 32767 ≈ 6553
181        assert_eq!(converted_data[2], 9830); // 0.3 * 32767 ≈ 9830
182    }
183
184    #[test]
185    fn test_convenience_methods() {
186        let data = array![100i16, -200, 300];
187        let audio_i16 = AudioSamples::new_mono(data, sample_rate!(44100));
188
189        // Test convenience methods
190        let audio_f32 = audio_i16.as_f32();
191        let audio_f64 = audio_i16.as_f64();
192        let audio_i32 = audio_i16.as_i32();
193
194        assert_eq!(audio_f32.num_channels(), 1);
195        assert_eq!(audio_f64.num_channels(), 1);
196        assert_eq!(audio_i32.num_channels(), 1);
197
198        // Verify sample rate preservation
199        assert_eq!(audio_f32.sample_rate(), sample_rate!(44100));
200        assert_eq!(audio_f64.sample_rate(), sample_rate!(44100));
201        assert_eq!(audio_i32.sample_rate(), sample_rate!(44100));
202    }
203
204    #[test]
205    fn test_round_trip_conversion() {
206        let original_data = array![0.123f32, -0.456, 0.789, -0.999, 0.0];
207        let audio_original = AudioSamples::new_mono(original_data.clone(), sample_rate!(44100));
208
209        // Convert f32 -> i16 -> f32
210        let audio_i16 = audio_original.to_format::<i16>();
211        let audio_round_trip = audio_i16.to_format::<f32>();
212
213        // Verify structure preservation
214        assert_eq!(audio_round_trip.num_channels(), 1);
215        assert_eq!(audio_round_trip.samples_per_channel(), 5);
216        assert_eq!(audio_round_trip.sample_rate(), sample_rate!(44100));
217
218        // Check that values are approximately preserved (within i16 precision limits)
219        let round_trip_data = audio_round_trip.as_mono().unwrap();
220        for (original, round_trip) in original_data.iter().zip(round_trip_data.iter()) {
221            assert_approx_eq!(*original as f64, *round_trip as f64, 5e-4); // i16 precision
222        }
223    }
224
225    #[test]
226    fn test_edge_cases() {
227        // Test with minimum and maximum values
228        let data = array![f32::MAX, f32::MIN, 0.0f32];
229        let audio_f32 = AudioSamples::new_mono(data, sample_rate!(44100));
230
231        // Convert to i16 (should clamp extreme values)
232        let audio_i16 = audio_f32.to_format::<i16>();
233        let converted_data = audio_i16.as_mono().unwrap();
234
235        // f32::MAX and f32::MIN should be clamped to i16 range
236        assert_eq!(converted_data[0], i16::MAX);
237        assert_eq!(converted_data[1], i16::MIN);
238        assert_eq!(converted_data[2], 0);
239    }
240}