Skip to main content

max / audiofiles

7.6 KB · 241 lines History Blame Raw
1 //! Audio conversion pipeline: channel count and sample rate conversion for export.
2
3 use rubato::{Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction};
4
5 use super::ExportChannels;
6 use crate::error::CoreError;
7 use tracing::instrument;
8
9 /// Audio data after conversion, ready for encoding.
10 pub struct ConvertedAudio {
11 pub samples: Vec<f32>,
12 pub sample_rate: u32,
13 pub channels: u16,
14 }
15
16 /// Convert channel count: mono mixdown, stereo upmix, or passthrough.
17 #[instrument(skip_all)]
18 pub fn convert_channels(
19 samples: &[f32],
20 src_channels: u16,
21 target: &ExportChannels,
22 ) -> (Vec<f32>, u16) {
23 if src_channels == 0 || samples.is_empty() {
24 return (Vec::new(), src_channels.max(1));
25 }
26 match target {
27 ExportChannels::Original => (samples.to_vec(), src_channels),
28 ExportChannels::Mono => {
29 if src_channels == 1 {
30 return (samples.to_vec(), 1);
31 }
32 let ch = src_channels as usize;
33 let num_frames = samples.len() / ch;
34 let mut mono = Vec::with_capacity(num_frames);
35 for frame in 0..num_frames {
36 let mut sum = 0.0f32;
37 for c in 0..ch {
38 sum += samples[frame * ch + c];
39 }
40 mono.push(sum / ch as f32);
41 }
42 (mono, 1)
43 }
44 ExportChannels::Stereo => {
45 if src_channels == 2 {
46 return (samples.to_vec(), 2);
47 }
48 if src_channels == 1 {
49 // Mono -> stereo: duplicate each sample
50 let mut stereo = Vec::with_capacity(samples.len() * 2);
51 for &s in samples {
52 stereo.push(s);
53 stereo.push(s);
54 }
55 return (stereo, 2);
56 }
57 // Multi-channel -> stereo: take first two channels
58 let ch = src_channels as usize;
59 let num_frames = samples.len() / ch;
60 let mut stereo = Vec::with_capacity(num_frames * 2);
61 for frame in 0..num_frames {
62 let base = frame * ch;
63 stereo.push(samples[base]);
64 stereo.push(samples.get(base + 1).copied().unwrap_or(0.0));
65 }
66 (stereo, 2)
67 }
68 }
69 }
70
71 /// Resample interleaved audio from src_rate to dst_rate using rubato.
72 /// Returns samples unchanged if rates match.
73 #[instrument(skip_all)]
74 pub fn resample(
75 samples: &[f32],
76 channels: u16,
77 src_rate: u32,
78 dst_rate: u32,
79 ) -> Result<Vec<f32>, CoreError> {
80 if src_rate == dst_rate {
81 return Ok(samples.to_vec());
82 }
83 if channels == 0 || src_rate == 0 || dst_rate == 0 {
84 return Err(CoreError::Export(format!(
85 "invalid resample params: channels={channels}, src_rate={src_rate}, dst_rate={dst_rate}"
86 )));
87 }
88
89 let ch = channels as usize;
90 let num_frames = samples.len() / ch;
91
92 // De-interleave into per-channel vectors
93 let mut channel_bufs: Vec<Vec<f32>> = vec![Vec::with_capacity(num_frames); ch];
94 for frame in 0..num_frames {
95 for c in 0..ch {
96 channel_bufs[c].push(samples[frame * ch + c]);
97 }
98 }
99
100 let params = SincInterpolationParameters {
101 sinc_len: 256,
102 f_cutoff: 0.95,
103 interpolation: SincInterpolationType::Linear,
104 oversampling_factor: 256,
105 window: WindowFunction::BlackmanHarris2,
106 };
107
108 let ratio = dst_rate as f64 / src_rate as f64;
109 let chunk_size = 1024;
110
111 let mut resampler = SincFixedIn::<f32>::new(ratio, 2.0, params, chunk_size, ch)
112 .map_err(|e| CoreError::Export(format!("resampler init: {e}")))?;
113
114 let mut output_channels: Vec<Vec<f32>> = vec![Vec::new(); ch];
115 let mut pos = 0;
116
117 while pos < num_frames {
118 let end = (pos + chunk_size).min(num_frames);
119 let actual_len = end - pos;
120
121 let input_chunk: Vec<Vec<f32>> = channel_bufs
122 .iter()
123 .map(|buf| {
124 let mut chunk = buf[pos..end].to_vec();
125 // Pad last chunk to chunk_size if needed
126 chunk.resize(chunk_size, 0.0);
127 chunk
128 })
129 .collect();
130
131 let output_chunk = resampler
132 .process(&input_chunk, None)
133 .map_err(|e| CoreError::Export(format!("resample: {e}")))?;
134
135 // For the last chunk, we may have processed padding — calculate expected output
136 let expected_output = if actual_len < chunk_size {
137 (actual_len as f64 * ratio).ceil() as usize
138 } else {
139 output_chunk[0].len()
140 };
141
142 for (c, chunk) in output_chunk.into_iter().enumerate() {
143 let chunk_len = chunk.len();
144 let take = expected_output.min(chunk_len);
145 output_channels[c].extend_from_slice(&chunk[..take]);
146 }
147
148 pos += chunk_size;
149 }
150
151 // Re-interleave
152 let out_frames = output_channels[0].len();
153 let mut interleaved = Vec::with_capacity(out_frames * ch);
154 for frame in 0..out_frames {
155 for channel in &output_channels {
156 interleaved.push(channel[frame]);
157 }
158 }
159
160 Ok(interleaved)
161 }
162
163 /// Apply channel conversion then resampling.
164 pub fn apply_conversion(
165 samples: &[f32],
166 src_channels: u16,
167 src_rate: u32,
168 target_channels: &ExportChannels,
169 target_rate: Option<u32>,
170 ) -> Result<ConvertedAudio, CoreError> {
171 let (converted, out_channels) = convert_channels(samples, src_channels, target_channels);
172 let dst_rate = target_rate.unwrap_or(src_rate);
173 let resampled = resample(&converted, out_channels, src_rate, dst_rate)?;
174
175 Ok(ConvertedAudio {
176 samples: resampled,
177 sample_rate: dst_rate,
178 channels: out_channels,
179 })
180 }
181
182 #[cfg(test)]
183 mod tests {
184 use super::*;
185
186 #[test]
187 fn mono_mixdown() {
188 // Stereo: L=0.4, R=0.6 -> mono: 0.5
189 let samples = vec![0.4, 0.6, 0.2, 0.8];
190 let (out, ch) = convert_channels(&samples, 2, &ExportChannels::Mono);
191 assert_eq!(ch, 1);
192 assert_eq!(out.len(), 2);
193 assert!((out[0] - 0.5).abs() < 1e-6);
194 assert!((out[1] - 0.5).abs() < 1e-6);
195 }
196
197 #[test]
198 fn stereo_upmix() {
199 let samples = vec![0.3, -0.3, 0.7];
200 let (out, ch) = convert_channels(&samples, 1, &ExportChannels::Stereo);
201 assert_eq!(ch, 2);
202 assert_eq!(out, vec![0.3, 0.3, -0.3, -0.3, 0.7, 0.7]);
203 }
204
205 #[test]
206 fn passthrough() {
207 let samples = vec![0.1, 0.2, 0.3, 0.4];
208 let (out, ch) = convert_channels(&samples, 2, &ExportChannels::Original);
209 assert_eq!(ch, 2);
210 assert_eq!(out, samples);
211 }
212
213 #[test]
214 fn resample_noop() {
215 let samples = vec![0.1, 0.2, 0.3, 0.4];
216 let out = resample(&samples, 1, 44100, 44100).unwrap();
217 assert_eq!(out, samples);
218 }
219
220 #[test]
221 fn resample_changes_length() {
222 // Use multiple full chunks to avoid edge effects. 4096 mono samples at 44100 -> 48000.
223 let num_samples = 4096;
224 let samples: Vec<f32> = (0..num_samples).map(|i| i as f32 / num_samples as f32).collect();
225 let out = resample(&samples, 1, 44100, 48000).unwrap();
226 // Output should be longer than input since 48000 > 44100
227 assert!(
228 out.len() > num_samples,
229 "expected output longer than input ({num_samples}), got {}",
230 out.len()
231 );
232 // And roughly in the right ratio (within 15% to account for resampler latency/padding)
233 let expected = (num_samples as f64 * 48000.0 / 44100.0) as usize;
234 assert!(
235 out.len() > expected / 2,
236 "output too short: expected ~{expected}, got {}",
237 out.len()
238 );
239 }
240 }
241