audio_processor/
lib.rs

1pub mod io;
2pub mod processing;
3pub mod transcoding;
4pub mod errors;
5
6use std::time::Duration;
7use crate::errors::AudioError;
8use crate::transcoding::AudioFormat;
9use crate::processing::{AudioEffect, effect_to_filter};
10
11/// Main struct for processing an audio file.
12#[derive(Debug, Clone)]
13pub struct AudioProcessor {
14    pub file_path: String,
15}
16
17impl AudioProcessor {
18    /// Creates a new audio processor instance from a file path.
19    pub fn new(file_path: &str) -> Result<Self, AudioError> {
20        // Check if file exists; real FFmpeg initialization could be done here.
21        std::fs::metadata(file_path).map_err(|e| AudioError::IoError(e))?;
22        println!("Initializing audio processor for file: {}", file_path);
23        io::load_audio(file_path)?;
24        Ok(AudioProcessor {
25            file_path: file_path.to_string(),
26        })
27    }
28
29    /// Seeks to a given time position and outputs a new file.
30    pub fn seek(&self, position: Duration) -> Result<Self, AudioError> {
31        let output_file = format!("seeked_{}", self.file_path);
32        let pos_str = format!("{}", position.as_secs());
33        // Using "-ss" before input to perform a fast seek (copying streams)
34        let status = std::process::Command::new("ffmpeg")
35            .args(&["-ss", &pos_str, "-i", &self.file_path, "-c", "copy", &output_file, "-y"])
36            .status()
37            .map_err(|e| AudioError::IoError(e))?;
38        if status.success() {
39            println!("Seeked {} seconds into {} -> {}", pos_str, self.file_path, output_file);
40            Ok(AudioProcessor { file_path: output_file })
41        } else {
42            Err(AudioError::FfmpegError("ffmpeg seek failed".to_string()))
43        }
44    }
45
46    /// Trims the audio between start and end durations.
47    /// Returns a new AudioProcessor instance with the trimmed segment.
48    pub fn trim(&self, start: Duration, end: Duration) -> Result<Self, AudioError> {
49        let output_file = format!("trimmed_{}", self.file_path);
50        let start_str = format!("{}", start.as_secs());
51        let end_str = format!("{}", end.as_secs());
52        // "-ss" before input and "-to" after input for trimming without re-encoding.
53        let status = std::process::Command::new("ffmpeg")
54            .args(&["-ss", &start_str, "-to", &end_str, "-i", &self.file_path, "-c", "copy", &output_file, "-y"])
55            .status()
56            .map_err(|e| AudioError::IoError(e))?;
57        if status.success() {
58            println!("Trimmed {} from {} to {} seconds -> {}", self.file_path, start_str, end_str, output_file);
59            Ok(AudioProcessor { file_path: output_file })
60        } else {
61            Err(AudioError::FfmpegError("ffmpeg trim failed".to_string()))
62        }
63    }
64
65    /// Transcodes the current audio to a different format.
66    pub fn transcode(&self, output_format: AudioFormat, output_path: &str) -> Result<(), AudioError> {
67        // Let FFmpeg decide the codec based on output extension.
68        let status = std::process::Command::new("ffmpeg")
69            .args(&["-i", &self.file_path, output_path, "-y"])
70            .status()
71            .map_err(|e| AudioError::IoError(e))?;
72        if status.success() {
73            println!("Transcoded {} to format {:?} -> {}", self.file_path, output_format, output_path);
74            Ok(())
75        } else {
76            Err(AudioError::FfmpegError("ffmpeg transcode failed".to_string()))
77        }
78    }
79
80    /// Adjusts the audio volume by a scaling factor.
81    pub fn adjust_volume(&self, factor: f32) -> Result<Self, AudioError> {
82        let output_file = format!("volume_adjusted_{}", self.file_path);
83        let filter = format!("volume={}", factor);
84        let status = std::process::Command::new("ffmpeg")
85            .args(&["-i", &self.file_path, "-af", &filter, &output_file, "-y"])
86            .status()
87            .map_err(|e| AudioError::IoError(e))?;
88        if status.success() {
89            println!("Adjusted volume of {} by factor {} -> {}", self.file_path, factor, output_file);
90            Ok(AudioProcessor { file_path: output_file })
91        } else {
92            Err(AudioError::FfmpegError("ffmpeg adjust volume failed".to_string()))
93        }
94    }
95
96    /// Changes the playback speed (and optionally pitch) by a factor.
97    pub fn change_speed(&self, factor: f32) -> Result<Self, AudioError> {
98        let output_file = format!("speed_changed_{}", self.file_path);
99        // atempo filter supports 0.5 to 2.0; for other values, chain multiple filters.
100        let filter = format!("atempo={}", factor);
101        let status = std::process::Command::new("ffmpeg")
102            .args(&["-i", &self.file_path, "-filter:a", &filter, &output_file, "-y"])
103            .status()
104            .map_err(|e| AudioError::IoError(e))?;
105        if status.success() {
106            println!("Changed speed of {} by factor {} -> {}", self.file_path, factor, output_file);
107            Ok(AudioProcessor { file_path: output_file })
108        } else {
109            Err(AudioError::FfmpegError("ffmpeg change speed failed".to_string()))
110        }
111    }
112
113    /// Applies an audio effect using FFmpeg filters.
114    pub fn apply_effect(&self, effect: AudioEffect) -> Result<Self, AudioError> {
115        let output_file = format!("effected_{}", self.file_path);
116        // Convert our enum into an FFmpeg filter string.
117        let filter = effect_to_filter(&effect);
118        let status = std::process::Command::new("ffmpeg")
119            .args(&["-i", &self.file_path, "-af", &filter, &output_file, "-y"])
120            .status()
121            .map_err(|e| AudioError::IoError(e))?;
122        if status.success() {
123            println!("Applied effect {:?} on {} -> {}", effect, self.file_path, output_file);
124            Ok(AudioProcessor { file_path: output_file })
125        } else {
126            Err(AudioError::FfmpegError("ffmpeg apply effect failed".to_string()))
127        }
128    }
129
130    /// Merges multiple audio files sequentially (concatenation).
131    /// Uses FFmpeg’s concat demuxer.
132    pub fn merge_audios(audios: &[AudioProcessor], output_path: &str) -> Result<Self, AudioError> {
133        use std::io::Write;
134        use tempfile::NamedTempFile;
135
136        // Create a temporary file listing all input files.
137        let mut list_file = NamedTempFile::new().map_err(|e| AudioError::IoError(e))?;
138        for audio in audios {
139            // The concat demuxer expects lines like: file 'path/to/file'
140            writeln!(list_file, "file '{}'", audio.file_path).map_err(|e| AudioError::IoError(e))?;
141        }
142        list_file.flush().map_err(|e| AudioError::IoError(e))?;
143
144        let status = std::process::Command::new("ffmpeg")
145            .args(&["-f", "concat", "-safe", "0", "-i", list_file.path().to_str().unwrap(), "-c", "copy", output_path, "-y"])
146            .status()
147            .map_err(|e| AudioError::IoError(e))?;
148
149        if status.success() {
150            println!("Merged {} audio files -> {}", audios.len(), output_path);
151            Ok(AudioProcessor { file_path: output_path.to_string() })
152        } else {
153            Err(AudioError::FfmpegError("ffmpeg merge failed".to_string()))
154        }
155    }
156
157    /// Reverses the audio.
158    pub fn reverse(&self) -> Result<Self, AudioError> {
159        let output_file = format!("reversed_{}", self.file_path);
160        let status = std::process::Command::new("ffmpeg")
161            .args(&["-i", &self.file_path, "-af", "areverse", &output_file, "-y"])
162            .status()
163            .map_err(|e| AudioError::IoError(e))?;
164        if status.success() {
165            println!("Reversed audio {} -> {}", self.file_path, output_file);
166            Ok(AudioProcessor { file_path: output_file })
167        } else {
168            Err(AudioError::FfmpegError("ffmpeg reverse failed".to_string()))
169        }
170    }
171
172    /// Normalizes the audio volume.
173    pub fn normalize(&self) -> Result<Self, AudioError> {
174        let output_file = format!("normalized_{}", self.file_path);
175        // Using loudnorm filter for normalization.
176        let status = std::process::Command::new("ffmpeg")
177            .args(&["-i", &self.file_path, "-af", "loudnorm", &output_file, "-y"])
178            .status()
179            .map_err(|e| AudioError::IoError(e))?;
180        if status.success() {
181            println!("Normalized audio {} -> {}", self.file_path, output_file);
182            Ok(AudioProcessor { file_path: output_file })
183        } else {
184            Err(AudioError::FfmpegError("ffmpeg normalize failed".to_string()))
185        }
186    }
187
188    /// Overlays another audio onto this one at a given start time.
189    pub fn overlay(&self, overlay_audio: &AudioProcessor, start_time: Duration) -> Result<Self, AudioError> {
190        let output_file = format!("overlayed_{}", self.file_path);
191        // Using amix to mix two audio streams.
192        // First, apply a delay to the overlay using the "adelay" filter.
193        let delay_ms = start_time.as_millis();
194        let filter = format!("[1]adelay={}|{delay}|{delay}[d]; [0][d]amix=inputs=2:duration=first", delay=delay_ms);
195        let status = std::process::Command::new("ffmpeg")
196            .args(&["-i", &self.file_path, "-i", &overlay_audio.file_path, "-filter_complex", &filter, &output_file, "-y"])
197            .status()
198            .map_err(|e| AudioError::IoError(e))?;
199        if status.success() {
200            println!("Overlayed {} onto {} at {} seconds -> {}", overlay_audio.file_path, self.file_path, start_time.as_secs(), output_file);
201            Ok(AudioProcessor { file_path: output_file })
202        } else {
203            Err(AudioError::FfmpegError("ffmpeg overlay failed".to_string()))
204        }
205    }
206}