Skip to main content

max / audiofiles

8.3 KB · 267 lines History Blame Raw
1 //! Destructive sample editing: trim, normalize, reverse, gain, fade, DC offset, channel conversion.
2 //!
3 //! Each operation is a pure function operating on `Vec<f32>` sample data.
4 //! The [`EditOperation`] enum dispatches to the correct function via [`apply_edit`].
5
6 pub mod channel_convert;
7 pub mod dc_offset;
8 pub mod fade;
9 pub mod gain;
10 pub mod normalize;
11 pub mod reverse;
12 pub mod silence;
13 pub mod trim;
14 pub mod worker;
15
16 use crate::error::CoreError;
17
18 pub use fade::FadeCurve;
19
20 /// A destructive edit operation to apply to sample data.
21 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22 pub enum EditOperation {
23 Trim {
24 start_frame: usize,
25 end_frame: usize,
26 },
27 NormalizePeak {
28 target_db: f64,
29 },
30 NormalizeLufs {
31 target_lufs: f64,
32 },
33 Reverse,
34 Gain {
35 db: f64,
36 },
37 FadeIn {
38 frames: usize,
39 curve: FadeCurve,
40 },
41 FadeOut {
42 frames: usize,
43 curve: FadeCurve,
44 },
45 /// Remove DC offset (center waveform around zero).
46 RemoveDcOffset,
47 /// Convert mono to stereo (duplicate channels).
48 MonoToStereo,
49 /// Convert stereo/multi-channel to mono (average channels).
50 StereoToMono,
51 /// Insert silence at a frame position.
52 InsertSilence {
53 start_frame: usize,
54 duration_frames: usize,
55 },
56 /// Remove a range of frames (silence or otherwise).
57 RemoveRange {
58 start_frame: usize,
59 end_frame: usize,
60 },
61 }
62
63 impl EditOperation {
64 /// Short display name for status messages.
65 pub fn display_name(&self) -> &'static str {
66 match self {
67 EditOperation::Trim { .. } => "Trim",
68 EditOperation::NormalizePeak { .. } => "Normalize (Peak)",
69 EditOperation::NormalizeLufs { .. } => "Normalize (LUFS)",
70 EditOperation::Reverse => "Reverse",
71 EditOperation::Gain { .. } => "Gain",
72 EditOperation::FadeIn { .. } => "Fade In",
73 EditOperation::FadeOut { .. } => "Fade Out",
74 EditOperation::RemoveDcOffset => "Remove DC Offset",
75 EditOperation::MonoToStereo => "Mono → Stereo",
76 EditOperation::StereoToMono => "Stereo → Mono",
77 EditOperation::InsertSilence { .. } => "Insert Silence",
78 EditOperation::RemoveRange { .. } => "Remove Range",
79 }
80 }
81 }
82
83 /// Apply an edit operation to interleaved sample data in-place.
84 ///
85 /// Returns the (possibly changed) channel count — channel conversion operations
86 /// modify the number of channels.
87 pub fn apply_edit(
88 samples: &mut Vec<f32>,
89 channels: u16,
90 sample_rate: u32,
91 operation: &EditOperation,
92 ) -> Result<u16, CoreError> {
93 match operation {
94 EditOperation::Trim {
95 start_frame,
96 end_frame,
97 } => {
98 trim::apply_trim(samples, channels, *start_frame, *end_frame)?;
99 Ok(channels)
100 }
101 EditOperation::NormalizePeak { target_db } => {
102 normalize::apply_normalize_peak(samples, *target_db)?;
103 Ok(channels)
104 }
105 EditOperation::NormalizeLufs { target_lufs } => {
106 normalize::apply_normalize_lufs(samples, channels, sample_rate, *target_lufs)?;
107 Ok(channels)
108 }
109 EditOperation::Reverse => {
110 reverse::apply_reverse(samples, channels);
111 Ok(channels)
112 }
113 EditOperation::Gain { db } => {
114 gain::apply_gain(samples, *db);
115 Ok(channels)
116 }
117 EditOperation::FadeIn { frames, curve } => {
118 fade::apply_fade_in(samples, channels, *frames, *curve);
119 Ok(channels)
120 }
121 EditOperation::FadeOut { frames, curve } => {
122 fade::apply_fade_out(samples, channels, *frames, *curve);
123 Ok(channels)
124 }
125 EditOperation::RemoveDcOffset => {
126 dc_offset::apply_remove_dc_offset(samples, channels);
127 Ok(channels)
128 }
129 EditOperation::MonoToStereo => channel_convert::apply_mono_to_stereo(samples, channels),
130 EditOperation::StereoToMono => channel_convert::apply_stereo_to_mono(samples, channels),
131 EditOperation::InsertSilence {
132 start_frame,
133 duration_frames,
134 } => {
135 silence::apply_insert_silence(samples, channels, *start_frame, *duration_frames)?;
136 Ok(channels)
137 }
138 EditOperation::RemoveRange {
139 start_frame,
140 end_frame,
141 } => {
142 silence::apply_remove_range(samples, channels, *start_frame, *end_frame)?;
143 Ok(channels)
144 }
145 }
146 }
147
148 #[cfg(test)]
149 mod tests {
150 use super::*;
151
152 #[test]
153 fn apply_edit_dispatches_trim() {
154 let mut samples = vec![0.1, 0.2, 0.3, 0.4, 0.5];
155 let op = EditOperation::Trim {
156 start_frame: 1,
157 end_frame: 4,
158 };
159 apply_edit(&mut samples, 1, 44100, &op).unwrap();
160 assert_eq!(samples, vec![0.2, 0.3, 0.4]);
161 }
162
163 #[test]
164 fn apply_edit_dispatches_reverse() {
165 let mut samples = vec![1.0, 2.0, 3.0];
166 let op = EditOperation::Reverse;
167 apply_edit(&mut samples, 1, 44100, &op).unwrap();
168 assert_eq!(samples, vec![3.0, 2.0, 1.0]);
169 }
170
171 #[test]
172 fn apply_edit_dispatches_gain() {
173 let mut samples = vec![0.5, -0.5];
174 let op = EditOperation::Gain { db: 0.0 };
175 apply_edit(&mut samples, 1, 44100, &op).unwrap();
176 assert_eq!(samples, vec![0.5, -0.5]);
177 }
178
179 #[test]
180 fn apply_edit_dispatches_normalize_peak() {
181 let mut samples = vec![0.5, -0.5];
182 let op = EditOperation::NormalizePeak { target_db: 0.0 };
183 apply_edit(&mut samples, 1, 44100, &op).unwrap();
184 let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs()));
185 assert!((peak - 1.0).abs() < 0.01);
186 }
187
188 #[test]
189 fn apply_edit_dispatches_fade_in() {
190 let mut samples = vec![1.0; 4];
191 let op = EditOperation::FadeIn {
192 frames: 4,
193 curve: FadeCurve::Linear,
194 };
195 apply_edit(&mut samples, 1, 44100, &op).unwrap();
196 assert!((samples[0]).abs() < 0.001);
197 }
198
199 #[test]
200 fn apply_edit_dispatches_fade_out() {
201 let mut samples = vec![1.0; 4];
202 let op = EditOperation::FadeOut {
203 frames: 4,
204 curve: FadeCurve::Linear,
205 };
206 apply_edit(&mut samples, 1, 44100, &op).unwrap();
207 // Last frame should be zero (fade reaches silence)
208 assert!((samples[3]).abs() < 0.001);
209 }
210
211 #[test]
212 fn edit_operation_display_names() {
213 assert_eq!(EditOperation::Reverse.display_name(), "Reverse");
214 assert_eq!(
215 EditOperation::Trim {
216 start_frame: 0,
217 end_frame: 1
218 }
219 .display_name(),
220 "Trim"
221 );
222 assert_eq!(EditOperation::Gain { db: 0.0 }.display_name(), "Gain");
223 }
224
225 #[test]
226 fn edit_operation_serializable() {
227 let op = EditOperation::FadeIn {
228 frames: 100,
229 curve: FadeCurve::SCurve,
230 };
231 let json = serde_json::to_string(&op).unwrap();
232 let decoded: EditOperation = serde_json::from_str(&json).unwrap();
233 assert_eq!(decoded.display_name(), "Fade In");
234 }
235
236 #[test]
237 fn apply_edit_dispatches_dc_offset() {
238 let mut samples = vec![1.0, 1.0, 1.0]; // DC offset of 1.0
239 let ch = apply_edit(&mut samples, 1, 44100, &EditOperation::RemoveDcOffset).unwrap();
240 assert_eq!(ch, 1);
241 assert!(samples.iter().all(|s| s.abs() < 1e-6));
242 }
243
244 #[test]
245 fn apply_edit_dispatches_mono_to_stereo() {
246 let mut samples = vec![0.5, -0.5];
247 let ch = apply_edit(&mut samples, 1, 44100, &EditOperation::MonoToStereo).unwrap();
248 assert_eq!(ch, 2);
249 assert_eq!(samples, vec![0.5, 0.5, -0.5, -0.5]);
250 }
251
252 #[test]
253 fn apply_edit_dispatches_stereo_to_mono() {
254 let mut samples = vec![0.4, 0.6, 0.2, 0.8];
255 let ch = apply_edit(&mut samples, 2, 44100, &EditOperation::StereoToMono).unwrap();
256 assert_eq!(ch, 1);
257 assert_eq!(samples.len(), 2);
258 }
259
260 #[test]
261 fn channel_conversion_display_names() {
262 assert_eq!(EditOperation::RemoveDcOffset.display_name(), "Remove DC Offset");
263 assert_eq!(EditOperation::MonoToStereo.display_name(), "Mono → Stereo");
264 assert_eq!(EditOperation::StereoToMono.display_name(), "Stereo → Mono");
265 }
266 }
267