audio_batch_speedup/
lib.rs1#![warn(clippy::cargo)]
2
3use bitflags::bitflags;
4use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
5use log::{debug, error};
6use rayon::prelude::*;
7use std::fs::File;
8use std::io::Read;
9use std::path::Path;
10use std::process::Command;
11use std::sync::atomic::{AtomicUsize, Ordering};
12use walkdir::WalkDir;
13
14bitflags! {
15 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17 pub struct AudioFormat: u32 {
18 const OGG = 1 << 0;
20 const MP3 = 1 << 1;
22 const WAV = 1 << 2;
24 const FLAC = 1 << 3;
26 const AAC = 1 << 4;
28 const OPUS = 1 << 5;
30 const ALAC = 1 << 6;
32 const WMA = 1 << 7;
34 const ALL = Self::OGG.bits() | Self::MP3.bits() | Self::WAV.bits() | Self::FLAC.bits() | Self::AAC.bits() | Self::OPUS.bits() | Self::ALAC.bits() | Self::WMA.bits();
36 }
37}
38
39impl Default for AudioFormat {
40 fn default() -> Self {
41 Self::ALL
42 }
43}
44
45fn detect_audio_format(path: &Path) -> Option<AudioFormat> {
55 let mut file = File::open(path).ok()?;
57 let mut buffer = [0; 12]; file.read_exact(&mut buffer).ok()?;
59
60 if &buffer[0..4] == b"OggS" {
62 return Some(AudioFormat::OGG);
63 }
64 if &buffer[0..3] == b"ID3" || (buffer[0] == 0xFF && (buffer[1] & 0xF6) == 0xF2) {
66 return Some(AudioFormat::MP3);
67 }
68 if &buffer[0..4] == b"RIFF" && &buffer[8..12] == b"WAVE" {
70 return Some(AudioFormat::WAV);
71 }
72 if &buffer[0..4] == b"fLaC" {
74 return Some(AudioFormat::FLAC);
75 }
76 if buffer[0..4] == [0x30, 0x26, 0xB2, 0x75] {
83 return Some(AudioFormat::WMA);
85 }
86
87 if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
89 match extension.to_lowercase().as_str() {
90 "ogg" => return Some(AudioFormat::OGG),
91 "mp3" => return Some(AudioFormat::MP3),
92 "wav" => return Some(AudioFormat::WAV),
93 "flac" => return Some(AudioFormat::FLAC),
94 "m4a" | "aac" => return Some(AudioFormat::AAC),
95 "opus" => return Some(AudioFormat::OPUS),
96 "alac" => return Some(AudioFormat::ALAC),
97 "wma" => return Some(AudioFormat::WMA),
98 _ => {}
99 }
100 }
101
102 None
103}
104
105pub fn process_audio_files(
129 folder: impl AsRef<Path>,
130 speed: f32,
131 formats: AudioFormat,
132) -> std::io::Result<()> {
133 let folder = folder.as_ref();
134
135 let files: Vec<_> = WalkDir::new(folder)
137 .into_iter()
138 .filter_map(|e| e.ok())
139 .filter(|e| e.path().is_file()) .collect();
141
142 let process_pb = ProgressBar::new(files.len() as u64);
143 process_pb.set_style(
144 ProgressStyle::default_bar()
145 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")
146 .expect("Internal Error: Failed to set progress bar style")
147 .progress_chars("#>-"),
148 );
149
150 let error_count = AtomicUsize::new(0);
151 let skipped_count = AtomicUsize::new(0);
152
153 files
155 .into_par_iter()
156 .progress_with(process_pb.clone())
157 .for_each(|entry| {
158 let path = entry.path();
159 if !path.is_file() {
160 return;
161 }
162
163 let detected_format = detect_audio_format(path);
164
165 let Some(detected_format) = detected_format else {
166 debug!("Skipping file (format not detected): {}", path.display());
167 skipped_count.fetch_add(1, Ordering::AcqRel);
168 return;
169 };
170
171 if !formats.contains(detected_format) {
172 debug!("Skipping file (format not selected): {}", path.display());
173 skipped_count.fetch_add(1, Ordering::AcqRel);
174 return;
175 }
176
177 let file_name = match path.file_name().and_then(|s| s.to_str()) {
178 Some(name) => name,
179 None => {
180 error!("Failed to get file name for {}", path.display());
181 error_count.fetch_add(1, Ordering::AcqRel);
182 return;
183 }
184 };
185
186 let output_file = path.with_file_name(format!("temp_{}", file_name));
187
188 let input_path_str = match path.to_str() {
189 Some(s) => s,
190 None => {
191 error!("Failed to convert input path to string: {}", path.display());
192 error_count.fetch_add(1, Ordering::AcqRel);
193 return;
194 }
195 };
196
197 let output_file_str = match output_file.to_str() {
198 Some(s) => s,
199 None => {
200 error!(
201 "Failed to convert output path to string: {}",
202 output_file.display()
203 );
204 error_count.fetch_add(1, Ordering::AcqRel);
205 return;
206 }
207 };
208
209 let status = Command::new("ffmpeg")
210 .args([
211 "-i",
212 input_path_str,
213 "-filter:a",
214 &format!("atempo={}", speed),
215 "-vn",
216 "-map_metadata",
217 "0",
218 output_file_str,
219 "-y",
220 "-loglevel",
221 "error",
222 ])
223 .status();
224
225 match status {
226 Ok(exit_status) => {
227 if exit_status.success() {
228 if let Err(e) = std::fs::rename(&output_file, path) {
229 error!(
230 "Error renaming file from {} to {}: {}",
231 output_file.display(),
232 path.display(),
233 e
234 );
235 error_count.fetch_add(1, Ordering::AcqRel);
236 }
237 } else {
238 error!(
239 "ffmpeg failed for {}. Exit code: {:?}",
240 path.display(),
241 exit_status.code()
242 );
243 error_count.fetch_add(1, Ordering::AcqRel);
244 if output_file.exists()
246 && let Err(e) = std::fs::remove_file(&output_file)
247 {
248 error!("Error removing temp file {}: {}", output_file.display(), e);
249 }
250 }
251 }
252 Err(e) => {
253 error!("Error executing ffmpeg for {}: {}", path.display(), e);
254 error_count.fetch_add(1, Ordering::AcqRel);
255 if output_file.exists()
257 && let Err(e) = std::fs::remove_file(&output_file)
258 {
259 error!("Error removing temp file {}: {}", output_file.display(), e);
260 }
261 }
262 }
263 });
264
265 process_pb.finish_with_message("Processing complete!");
266
267 let errors = error_count.load(Ordering::Relaxed);
268 let skipped = skipped_count.load(Ordering::Relaxed);
269
270 if errors > 0 {
271 log::error!("Finished with {} errors.", errors);
272 }
273 if skipped > 0 {
274 log::info!("Skipped {} files.", skipped);
275 }
276
277 Ok(())
278}