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 AudioFormat::OGG
42 | AudioFormat::MP3
43 | AudioFormat::WAV
44 | AudioFormat::FLAC
45 | AudioFormat::AAC
46 | AudioFormat::OPUS
47 | AudioFormat::ALAC
48 | AudioFormat::WMA
49 }
50}
51
52fn detect_audio_format(path: &Path) -> Option<AudioFormat> {
62 if let Ok(mut file) = File::open(path) {
64 let mut buffer = [0; 12]; if file.read_exact(&mut buffer).is_ok() {
67 if buffer[0..4] == [0x4F, 0x67, 0x67, 0x53] {
69 return Some(AudioFormat::OGG);
70 }
71 if buffer[0..3] == [0x49, 0x44, 0x33]
73 || (buffer[0] == 0xFF && (buffer[1] & 0xF6) == 0xF2)
74 {
75 return Some(AudioFormat::MP3);
76 }
77 if buffer[0..4] == [0x52, 0x49, 0x46, 0x46] && buffer[8..12] == [0x57, 0x41, 0x56, 0x45]
79 {
80 return Some(AudioFormat::WAV);
81 }
82 if buffer[0..4] == [0x66, 0x4C, 0x61, 0x43] {
84 return Some(AudioFormat::FLAC);
85 }
86 if buffer[0..4] == [0x30, 0x26, 0xB2, 0x75] {
93 return Some(AudioFormat::WMA);
95 }
96 }
97 }
98
99 if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
101 match extension.to_lowercase().as_str() {
102 "ogg" => return Some(AudioFormat::OGG),
103 "mp3" => return Some(AudioFormat::MP3),
104 "wav" => return Some(AudioFormat::WAV),
105 "flac" => return Some(AudioFormat::FLAC),
106 "m4a" | "aac" => return Some(AudioFormat::AAC),
107 "opus" => return Some(AudioFormat::OPUS),
108 "alac" => return Some(AudioFormat::ALAC),
109 "wma" => return Some(AudioFormat::WMA),
110 _ => {}
111 }
112 }
113
114 None
115}
116
117pub fn process_audio_files(
141 folder: impl AsRef<Path>,
142 speed: f32,
143 formats: AudioFormat,
144) -> std::io::Result<()> {
145 let folder = folder.as_ref();
146
147 let files: Vec<_> = WalkDir::new(folder)
149 .into_iter()
150 .filter_map(|e| e.ok())
151 .filter(|e| e.path().is_file()) .collect();
153
154 let process_pb = ProgressBar::new(files.len() as u64);
155 process_pb.set_style(
156 ProgressStyle::default_bar()
157 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")
158 .expect("Internal Error: Failed to set progress bar style")
159 .progress_chars("#>-"),
160 );
161
162 let error_count = AtomicUsize::new(0);
163 let skipped_count = AtomicUsize::new(0);
164
165 files
167 .into_par_iter()
168 .progress_with(process_pb.clone())
169 .for_each(|entry| {
170 let path = entry.path();
171 if !path.is_file() {
172 return;
173 }
174
175 let detected_format = detect_audio_format(path);
176
177 if detected_format.is_none() || !formats.contains(detected_format.unwrap()) {
178 debug!(
179 "Skipping file (unsupported format or not selected): {}",
180 path.display()
181 );
182 skipped_count.fetch_add(1, Ordering::Relaxed);
183 return;
184 }
185
186 let output_file = path.with_file_name(format!(
187 "temp_{}",
188 path.file_name().unwrap().to_str().unwrap()
189 ));
190
191 let status = Command::new("ffmpeg")
192 .args([
193 "-i",
194 path.to_str().unwrap(),
195 "-filter:a",
196 &format!("atempo={}", speed),
197 "-vn",
198 output_file.to_str().unwrap(),
199 "-y",
200 "-loglevel",
201 "error",
202 ])
203 .status();
204
205 if let Err(e) = status {
206 error!("Error processing {}: {}", path.display(), e);
207 error_count.fetch_add(1, Ordering::Relaxed);
208 return;
209 }
210
211 if status.unwrap().success() {
212 if let Err(e) = std::fs::rename(&output_file, path) {
213 error!("Error renaming file {}: {}", output_file.display(), e);
214 error_count.fetch_add(1, Ordering::Relaxed);
215 }
216 } else {
217 if output_file.exists() {
218 if let Err(e) = std::fs::remove_file(&output_file) {
219 error!("Error removing temp file {}: {}", output_file.display(), e);
220 }
221 }
222 error!("Error processing {}", path.display());
223 error_count.fetch_add(1, Ordering::Relaxed);
224 }
225 });
226
227 process_pb.finish_with_message("Processing complete!");
228
229 let errors = error_count.load(Ordering::Relaxed);
230 let skipped = skipped_count.load(Ordering::Relaxed);
231
232 if errors > 0 {
233 log::warn!("Finished with {} errors.", errors);
234 }
235 if skipped > 0 {
236 log::info!("Skipped {} files.", skipped);
237 }
238
239 Ok(())
240}