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}