Skip to main content

max / audiofiles

9.1 KB · 286 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 /// Auto-trim leading and trailing silence below `threshold_db` (dBFS).
62 /// Implemented by the forge batch DSP ([`crate::forge::batch`]).
63 TrimSilence {
64 threshold_db: f64,
65 },
66 }
67
68 impl EditOperation {
69 /// Short display name for status messages.
70 pub fn display_name(&self) -> &'static str {
71 match self {
72 EditOperation::Trim { .. } => "Trim",
73 EditOperation::NormalizePeak { .. } => "Normalize (Peak)",
74 EditOperation::NormalizeLufs { .. } => "Normalize (LUFS)",
75 EditOperation::Reverse => "Reverse",
76 EditOperation::Gain { .. } => "Gain",
77 EditOperation::FadeIn { .. } => "Fade In",
78 EditOperation::FadeOut { .. } => "Fade Out",
79 EditOperation::RemoveDcOffset => "Remove DC Offset",
80 EditOperation::MonoToStereo => "Mono → Stereo",
81 EditOperation::StereoToMono => "Stereo → Mono",
82 EditOperation::InsertSilence { .. } => "Insert Silence",
83 EditOperation::RemoveRange { .. } => "Remove Range",
84 EditOperation::TrimSilence { .. } => "Trim Silence",
85 }
86 }
87 }
88
89 /// Apply an edit operation to interleaved sample data in-place.
90 ///
91 /// Returns the (possibly changed) channel count — channel conversion operations
92 /// modify the number of channels.
93 pub fn apply_edit(
94 samples: &mut Vec<f32>,
95 channels: u16,
96 sample_rate: u32,
97 operation: &EditOperation,
98 ) -> Result<u16, CoreError> {
99 match operation {
100 EditOperation::Trim {
101 start_frame,
102 end_frame,
103 } => {
104 trim::apply_trim(samples, channels, *start_frame, *end_frame)?;
105 Ok(channels)
106 }
107 EditOperation::NormalizePeak { target_db } => {
108 normalize::apply_normalize_peak(samples, *target_db)?;
109 Ok(channels)
110 }
111 EditOperation::NormalizeLufs { target_lufs } => {
112 normalize::apply_normalize_lufs(samples, channels, sample_rate, *target_lufs)?;
113 Ok(channels)
114 }
115 EditOperation::Reverse => {
116 reverse::apply_reverse(samples, channels);
117 Ok(channels)
118 }
119 EditOperation::Gain { db } => {
120 gain::apply_gain(samples, *db);
121 Ok(channels)
122 }
123 EditOperation::FadeIn { frames, curve } => {
124 fade::apply_fade_in(samples, channels, *frames, *curve);
125 Ok(channels)
126 }
127 EditOperation::FadeOut { frames, curve } => {
128 fade::apply_fade_out(samples, channels, *frames, *curve);
129 Ok(channels)
130 }
131 EditOperation::RemoveDcOffset => {
132 dc_offset::apply_remove_dc_offset(samples, channels);
133 Ok(channels)
134 }
135 EditOperation::MonoToStereo => channel_convert::apply_mono_to_stereo(samples, channels),
136 EditOperation::StereoToMono => channel_convert::apply_stereo_to_mono(samples, channels),
137 EditOperation::InsertSilence {
138 start_frame,
139 duration_frames,
140 } => {
141 silence::apply_insert_silence(samples, channels, *start_frame, *duration_frames)?;
142 Ok(channels)
143 }
144 EditOperation::RemoveRange {
145 start_frame,
146 end_frame,
147 } => {
148 silence::apply_remove_range(samples, channels, *start_frame, *end_frame)?;
149 Ok(channels)
150 }
151 EditOperation::TrimSilence { threshold_db } => {
152 crate::forge::batch::trim_silence(samples, channels, *threshold_db);
153 Ok(channels)
154 }
155 }
156 }
157
158 #[cfg(test)]
159 mod tests {
160 use super::*;
161
162 #[test]
163 fn apply_edit_dispatches_trim() {
164 let mut samples = vec![0.1, 0.2, 0.3, 0.4, 0.5];
165 let op = EditOperation::Trim {
166 start_frame: 1,
167 end_frame: 4,
168 };
169 apply_edit(&mut samples, 1, 44100, &op).unwrap();
170 assert_eq!(samples, vec![0.2, 0.3, 0.4]);
171 }
172
173 #[test]
174 fn apply_edit_dispatches_reverse() {
175 let mut samples = vec![1.0, 2.0, 3.0];
176 let op = EditOperation::Reverse;
177 apply_edit(&mut samples, 1, 44100, &op).unwrap();
178 assert_eq!(samples, vec![3.0, 2.0, 1.0]);
179 }
180
181 #[test]
182 fn apply_edit_dispatches_gain() {
183 let mut samples = vec![0.5, -0.5];
184 let op = EditOperation::Gain { db: 0.0 };
185 apply_edit(&mut samples, 1, 44100, &op).unwrap();
186 assert_eq!(samples, vec![0.5, -0.5]);
187 }
188
189 #[test]
190 fn apply_edit_dispatches_normalize_peak() {
191 let mut samples = vec![0.5, -0.5];
192 let op = EditOperation::NormalizePeak { target_db: 0.0 };
193 apply_edit(&mut samples, 1, 44100, &op).unwrap();
194 let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs()));
195 assert!((peak - 1.0).abs() < 0.01);
196 }
197
198 #[test]
199 fn apply_edit_dispatches_fade_in() {
200 let mut samples = vec![1.0; 4];
201 let op = EditOperation::FadeIn {
202 frames: 4,
203 curve: FadeCurve::Linear,
204 };
205 apply_edit(&mut samples, 1, 44100, &op).unwrap();
206 assert!((samples[0]).abs() < 0.001);
207 }
208
209 #[test]
210 fn apply_edit_dispatches_fade_out() {
211 let mut samples = vec![1.0; 4];
212 let op = EditOperation::FadeOut {
213 frames: 4,
214 curve: FadeCurve::Linear,
215 };
216 apply_edit(&mut samples, 1, 44100, &op).unwrap();
217 // Last frame should be zero (fade reaches silence)
218 assert!((samples[3]).abs() < 0.001);
219 }
220
221 #[test]
222 fn edit_operation_display_names() {
223 assert_eq!(EditOperation::Reverse.display_name(), "Reverse");
224 assert_eq!(
225 EditOperation::Trim {
226 start_frame: 0,
227 end_frame: 1
228 }
229 .display_name(),
230 "Trim"
231 );
232 assert_eq!(EditOperation::Gain { db: 0.0 }.display_name(), "Gain");
233 }
234
235 #[test]
236 fn edit_operation_serializable() {
237 let op = EditOperation::FadeIn {
238 frames: 100,
239 curve: FadeCurve::SCurve,
240 };
241 let json = serde_json::to_string(&op).unwrap();
242 let decoded: EditOperation = serde_json::from_str(&json).unwrap();
243 assert_eq!(decoded.display_name(), "Fade In");
244 }
245
246 #[test]
247 fn apply_edit_dispatches_dc_offset() {
248 let mut samples = vec![1.0, 1.0, 1.0]; // DC offset of 1.0
249 let ch = apply_edit(&mut samples, 1, 44100, &EditOperation::RemoveDcOffset).unwrap();
250 assert_eq!(ch, 1);
251 assert!(samples.iter().all(|s| s.abs() < 1e-6));
252 }
253
254 #[test]
255 fn apply_edit_dispatches_mono_to_stereo() {
256 let mut samples = vec![0.5, -0.5];
257 let ch = apply_edit(&mut samples, 1, 44100, &EditOperation::MonoToStereo).unwrap();
258 assert_eq!(ch, 2);
259 assert_eq!(samples, vec![0.5, 0.5, -0.5, -0.5]);
260 }
261
262 #[test]
263 fn apply_edit_dispatches_stereo_to_mono() {
264 let mut samples = vec![0.4, 0.6, 0.2, 0.8];
265 let ch = apply_edit(&mut samples, 2, 44100, &EditOperation::StereoToMono).unwrap();
266 assert_eq!(ch, 1);
267 assert_eq!(samples.len(), 2);
268 }
269
270 #[test]
271 fn apply_edit_dispatches_trim_silence() {
272 // mono: silence, content, silence -> trims to the content frames.
273 let mut samples = vec![0.0, 0.0, 0.8, -0.7, 0.0];
274 let ch = apply_edit(&mut samples, 1, 44100, &EditOperation::TrimSilence { threshold_db: -60.0 }).unwrap();
275 assert_eq!(ch, 1);
276 assert_eq!(samples, vec![0.8, -0.7]);
277 }
278
279 #[test]
280 fn channel_conversion_display_names() {
281 assert_eq!(EditOperation::RemoveDcOffset.display_name(), "Remove DC Offset");
282 assert_eq!(EditOperation::MonoToStereo.display_name(), "Mono → Stereo");
283 assert_eq!(EditOperation::StereoToMono.display_name(), "Stereo → Mono");
284 }
285 }
286