audio_batch_speedup/
lib.rs

1#![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    /// Represents the supported audio formats for processing.
16    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17    pub struct AudioFormat: u32 {
18        /// Ogg Vorbis format.
19        const OGG = 1 << 0;
20        /// MPEG Audio Layer III (MP3) format.
21        const MP3 = 1 << 1;
22        /// Waveform Audio File Format (WAV).
23        const WAV = 1 << 2;
24        /// Free Lossless Audio Codec (FLAC) format.
25        const FLAC = 1 << 3;
26        /// Advanced Audio Coding (AAC) format (often in MP4 containers).
27        const AAC = 1 << 4;
28        /// Opus Interactive Audio Codec (often in Ogg or WebM containers).
29        const OPUS = 1 << 5;
30        /// Apple Lossless Audio Codec (ALAC) format.
31        const ALAC = 1 << 6;
32        /// Windows Media Audio (WMA) format.
33        const WMA = 1 << 7;
34        /// All supported formats.
35        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
52/// Detects the audio format of a file based on its magic bytes or file extension.
53///
54/// # Arguments
55///
56/// * `path` - The path to the audio file.
57///
58/// # Returns
59///
60/// * `Option<AudioFormat>` - The detected audio format, or `None` if it cannot be determined.
61fn detect_audio_format(path: &Path) -> Option<AudioFormat> {
62    // Try to detect by magic bytes first
63    if let Ok(mut file) = File::open(path) {
64        let mut buffer = [0; 12]; // Read enough bytes for common headers
65
66        if file.read_exact(&mut buffer).is_ok() {
67            // OGG (OggS)
68            if buffer[0..4] == [0x4F, 0x67, 0x67, 0x53] {
69                return Some(AudioFormat::OGG);
70            }
71            // MP3 (ID3 tag or starts with 0xFF FB/FA)
72            if buffer[0..3] == [0x49, 0x44, 0x33]
73                || (buffer[0] == 0xFF && (buffer[1] & 0xF6) == 0xF2)
74            {
75                return Some(AudioFormat::MP3);
76            }
77            // WAV (RIFF header with WAVE)
78            if buffer[0..4] == [0x52, 0x49, 0x46, 0x46] && buffer[8..12] == [0x57, 0x41, 0x56, 0x45]
79            {
80                return Some(AudioFormat::WAV);
81            }
82            // FLAC (fLaC)
83            if buffer[0..4] == [0x66, 0x4C, 0x61, 0x43] {
84                return Some(AudioFormat::FLAC);
85            }
86            // AAC (often in MP4/M4A containers, which start with 'ftyp' or 'moov')
87            // This is harder to detect purely by magic bytes without parsing the container.
88            // We'll rely more on extension for AAC/M4A.
89            // OPUS (often in Ogg containers, so OggS will catch it, or WebM)
90            // ALAC (often in MP4/M4A containers)
91            // WMA (ASF header)
92            if buffer[0..4] == [0x30, 0x26, 0xB2, 0x75] {
93                // GUID for ASF header
94                return Some(AudioFormat::WMA);
95            }
96        }
97    }
98
99    // Fallback to file extension
100    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
117/// Process all audio files in the specified folder recursively with the given speed multiplier.
118///
119/// # Arguments
120///
121/// * `folder` - Path to the folder containing audio files
122/// * `speed` - Speed multiplier (e.g., 1.5 for 1.5x speed)
123/// * `formats` - A bitflags object indicating which audio formats to process.
124///
125/// # Returns
126///
127/// * `Result<()>` - Ok(()) if successful, or an error if processing fails
128///
129/// # Example
130///
131/// ```no_run
132/// use std::path::Path;
133/// use audio_batch_speedup::{process_audio_files, AudioFormat};
134///
135/// let folder = Path::new("path/to/audio/files");
136/// let speed = 1.5;
137/// let formats = AudioFormat::OGG | AudioFormat::MP3;
138/// process_audio_files(folder, speed, formats).unwrap();
139/// ```
140pub 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    // Collect all files that need to be processed
148    let files: Vec<_> = WalkDir::new(folder)
149        .into_iter()
150        .filter_map(|e| e.ok())
151        .filter(|e| e.path().is_file()) // Only count files for the progress bar
152        .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    // Process all files in parallel
166    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}