diff options
Diffstat (limited to '')
| -rw-r--r-- | src/audio.rs | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..86c88f7 --- /dev/null +++ b/src/audio.rs | |||
| @@ -0,0 +1,418 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | use std::sync::atomic::{AtomicBool, Ordering}; | ||
| 3 | use std::sync::mpsc::{self, Receiver, Sender}; | ||
| 4 | use std::sync::{Arc, Mutex}; | ||
| 5 | use std::thread; | ||
| 6 | |||
| 7 | #[allow(dead_code)] | ||
| 8 | pub enum AudioCommand { | ||
| 9 | Play(PathBuf), | ||
| 10 | Pause, | ||
| 11 | Resume, | ||
| 12 | Stop, | ||
| 13 | SetVolume(f32), | ||
| 14 | } | ||
| 15 | |||
| 16 | pub struct AudioEngine { | ||
| 17 | command_tx: Sender<AudioCommand>, | ||
| 18 | is_playing: Arc<AtomicBool>, | ||
| 19 | current_track: Arc<Mutex<Option<std::path::PathBuf>>>, | ||
| 20 | track_ended: Arc<AtomicBool>, | ||
| 21 | } | ||
| 22 | |||
| 23 | impl AudioEngine { | ||
| 24 | pub fn new() -> Self { | ||
| 25 | let (tx, rx) = mpsc::channel(); | ||
| 26 | let is_playing = Arc::new(AtomicBool::new(false)); | ||
| 27 | let is_playing_clone = Arc::clone(&is_playing); | ||
| 28 | let current_track = Arc::new(Mutex::new(None)); | ||
| 29 | let current_track_clone = Arc::clone(¤t_track); | ||
| 30 | let track_ended = Arc::new(AtomicBool::new(false)); | ||
| 31 | let track_ended_clone = Arc::clone(&track_ended); | ||
| 32 | |||
| 33 | let volume = Arc::new(Mutex::new(1.0f32)); | ||
| 34 | let volume_clone = Arc::clone(&volume); | ||
| 35 | |||
| 36 | thread::spawn(move || { | ||
| 37 | audio_thread( | ||
| 38 | rx, | ||
| 39 | is_playing_clone, | ||
| 40 | current_track_clone, | ||
| 41 | volume_clone, | ||
| 42 | track_ended_clone, | ||
| 43 | ); | ||
| 44 | }); | ||
| 45 | |||
| 46 | AudioEngine { | ||
| 47 | command_tx: tx, | ||
| 48 | is_playing, | ||
| 49 | current_track, | ||
| 50 | track_ended, | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | pub fn play(&self, path: PathBuf) { | ||
| 55 | self.track_ended.store(false, Ordering::SeqCst); | ||
| 56 | if let Ok(mut track) = self.current_track.lock() { | ||
| 57 | *track = Some(path.clone()); | ||
| 58 | } | ||
| 59 | let _ = self.command_tx.send(AudioCommand::Play(path)); | ||
| 60 | } | ||
| 61 | |||
| 62 | pub fn pause(&self) { | ||
| 63 | let _ = self.command_tx.send(AudioCommand::Pause); | ||
| 64 | } | ||
| 65 | |||
| 66 | pub fn resume(&self) { | ||
| 67 | let _ = self.command_tx.send(AudioCommand::Resume); | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn stop(&self) { | ||
| 71 | let _ = self.command_tx.send(AudioCommand::Stop); | ||
| 72 | } | ||
| 73 | |||
| 74 | pub fn set_volume(&self, volume: f32) { | ||
| 75 | let _ = self.command_tx.send(AudioCommand::SetVolume(volume)); | ||
| 76 | } | ||
| 77 | |||
| 78 | pub fn is_playing(&self) -> bool { | ||
| 79 | self.is_playing.load(Ordering::SeqCst) | ||
| 80 | } | ||
| 81 | |||
| 82 | pub fn current_track(&self) -> Option<std::path::PathBuf> { | ||
| 83 | self.current_track.lock().ok().and_then(|g| g.clone()) | ||
| 84 | } | ||
| 85 | |||
| 86 | pub fn poll_track_ended(&self) -> bool { | ||
| 87 | self.track_ended.swap(false, Ordering::SeqCst) | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | struct AudioData { | ||
| 92 | format: Box<dyn symphonia::core::formats::FormatReader>, | ||
| 93 | decoder: Box<dyn symphonia::core::codecs::Decoder>, | ||
| 94 | sample_rate: u32, | ||
| 95 | channels: u16, | ||
| 96 | sample_buffer: Vec<f32>, | ||
| 97 | buffer_pos: usize, | ||
| 98 | resample_ratio: Option<f64>, | ||
| 99 | } | ||
| 100 | |||
| 101 | fn resample_linear(input: &[f32], ratio: f64, channels: usize) -> Vec<f32> { | ||
| 102 | if ratio == 1.0 { | ||
| 103 | return input.to_vec(); | ||
| 104 | } | ||
| 105 | let input_frames = input.len() / channels; | ||
| 106 | let output_frames = (input_frames as f64 * ratio).ceil() as usize; | ||
| 107 | let mut output = Vec::with_capacity(output_frames * channels); | ||
| 108 | for i in 0..output_frames { | ||
| 109 | let input_pos = i as f64 / ratio; | ||
| 110 | let idx = input_pos.floor() as usize; | ||
| 111 | let frac = input_pos.fract() as f32; | ||
| 112 | for ch in 0..channels { | ||
| 113 | let cur = if idx * channels + ch < input.len() { | ||
| 114 | input[idx * channels + ch] | ||
| 115 | } else { | ||
| 116 | 0.0 | ||
| 117 | }; | ||
| 118 | let next = if (idx + 1) * channels + ch < input.len() { | ||
| 119 | input[(idx + 1) * channels + ch] | ||
| 120 | } else { | ||
| 121 | cur | ||
| 122 | }; | ||
| 123 | output.push(cur + frac * (next - cur)); | ||
| 124 | } | ||
| 125 | } | ||
| 126 | output | ||
| 127 | } | ||
| 128 | |||
| 129 | fn audio_thread( | ||
| 130 | rx: Receiver<AudioCommand>, | ||
| 131 | is_playing: Arc<AtomicBool>, | ||
| 132 | current_track: Arc<Mutex<Option<std::path::PathBuf>>>, | ||
| 133 | volume: Arc<Mutex<f32>>, | ||
| 134 | track_ended: Arc<AtomicBool>, | ||
| 135 | ) { | ||
| 136 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; | ||
| 137 | |||
| 138 | let host = cpal::default_host(); | ||
| 139 | let device = match host.default_output_device() { | ||
| 140 | Some(d) => d, | ||
| 141 | None => { | ||
| 142 | eprintln!("No output device available"); | ||
| 143 | return; | ||
| 144 | } | ||
| 145 | }; | ||
| 146 | |||
| 147 | let config = match device.default_output_config() { | ||
| 148 | Ok(c) => c, | ||
| 149 | Err(e) => { | ||
| 150 | eprintln!("Failed to get output config: {}", e); | ||
| 151 | return; | ||
| 152 | } | ||
| 153 | }; | ||
| 154 | |||
| 155 | let device_sample_rate = config.sample_rate(); | ||
| 156 | let channels = config.channels(); | ||
| 157 | eprintln!( | ||
| 158 | "Default output: {} Hz, {} channels", | ||
| 159 | device_sample_rate, channels | ||
| 160 | ); | ||
| 161 | |||
| 162 | let mut current_stream: Option<cpal::Stream> = None; | ||
| 163 | let audio_data: Arc<Mutex<Option<AudioData>>> = Arc::new(Mutex::new(None)); | ||
| 164 | |||
| 165 | while let Ok(cmd) = rx.recv() { | ||
| 166 | match cmd { | ||
| 167 | AudioCommand::Play(path) => { | ||
| 168 | is_playing.store(true, Ordering::SeqCst); | ||
| 169 | if let Ok(mut track) = current_track.lock() { | ||
| 170 | *track = Some(path.clone()); | ||
| 171 | } | ||
| 172 | |||
| 173 | // Drop the current stream before creating a new one | ||
| 174 | current_stream = None; | ||
| 175 | |||
| 176 | match open_audio_file(&path) { | ||
| 177 | Ok(mut data) => { | ||
| 178 | eprintln!("File: {} Hz, {} channels", data.sample_rate, data.channels); | ||
| 179 | |||
| 180 | if data.sample_rate != device_sample_rate { | ||
| 181 | eprintln!( | ||
| 182 | "Resampling from {} Hz to {} Hz", | ||
| 183 | data.sample_rate, device_sample_rate | ||
| 184 | ); | ||
| 185 | data.resample_ratio = | ||
| 186 | Some(device_sample_rate as f64 / data.sample_rate as f64); | ||
| 187 | } | ||
| 188 | |||
| 189 | let stream_config: cpal::StreamConfig = | ||
| 190 | device.default_output_config().unwrap().into(); | ||
| 191 | |||
| 192 | let audio_data_clone = Arc::clone(&audio_data); | ||
| 193 | let vol = Arc::clone(&volume); | ||
| 194 | let ended = Arc::clone(&track_ended); | ||
| 195 | let is_playing_clone = Arc::clone(&is_playing); | ||
| 196 | |||
| 197 | if let Ok(mut d) = audio_data.lock() { | ||
| 198 | *d = Some(data); | ||
| 199 | } | ||
| 200 | |||
| 201 | match device.build_output_stream( | ||
| 202 | &stream_config, | ||
| 203 | move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { | ||
| 204 | let vol = vol.lock().map(|v| *v).unwrap_or(1.0); | ||
| 205 | |||
| 206 | if let Ok(mut audio_data) = audio_data_clone.lock() { | ||
| 207 | if let Some(ref mut state) = *audio_data { | ||
| 208 | let channels = state.channels as usize; | ||
| 209 | let mut offset = 0usize; | ||
| 210 | |||
| 211 | // Keep decoding until we fill the buffer or run out of data | ||
| 212 | while offset < data.len() { | ||
| 213 | // First, copy any buffered samples | ||
| 214 | if state.buffer_pos < state.sample_buffer.len() { | ||
| 215 | let remaining = | ||
| 216 | &state.sample_buffer[state.buffer_pos..]; | ||
| 217 | let to_copy = | ||
| 218 | remaining.len().min(data.len() - offset); | ||
| 219 | data[offset..offset + to_copy] | ||
| 220 | .copy_from_slice(&remaining[..to_copy]); | ||
| 221 | state.buffer_pos += to_copy; | ||
| 222 | offset += to_copy; | ||
| 223 | |||
| 224 | // If we've consumed all buffered samples, clear the buffer | ||
| 225 | if state.buffer_pos >= state.sample_buffer.len() { | ||
| 226 | state.sample_buffer.clear(); | ||
| 227 | state.buffer_pos = 0; | ||
| 228 | } | ||
| 229 | continue; // Try to fill more of the buffer | ||
| 230 | } | ||
| 231 | |||
| 232 | // Decode more data | ||
| 233 | match state.format.next_packet() { | ||
| 234 | Ok(packet) => { | ||
| 235 | match state.decoder.decode(&packet) { | ||
| 236 | Ok(decoded) => { | ||
| 237 | let spec = *decoded.spec(); | ||
| 238 | let mut sample_buf = | ||
| 239 | symphonia::core::audio::SampleBuffer::< | ||
| 240 | f32, | ||
| 241 | >::new( | ||
| 242 | decoded.capacity() as u64, spec | ||
| 243 | ); | ||
| 244 | sample_buf | ||
| 245 | .copy_interleaved_ref(decoded); | ||
| 246 | let samples = sample_buf.samples(); | ||
| 247 | |||
| 248 | // Resample if needed | ||
| 249 | let resampled = if let Some(ratio) = | ||
| 250 | state.resample_ratio | ||
| 251 | { | ||
| 252 | resample_linear( | ||
| 253 | samples, ratio, channels, | ||
| 254 | ) | ||
| 255 | } else { | ||
| 256 | samples.to_vec() | ||
| 257 | }; | ||
| 258 | |||
| 259 | // Copy what we can to output | ||
| 260 | let to_copy = resampled | ||
| 261 | .len() | ||
| 262 | .min(data.len() - offset); | ||
| 263 | data[offset..offset + to_copy] | ||
| 264 | .copy_from_slice( | ||
| 265 | &resampled[..to_copy], | ||
| 266 | ); | ||
| 267 | offset += to_copy; | ||
| 268 | |||
| 269 | // Buffer the rest | ||
| 270 | if to_copy < resampled.len() { | ||
| 271 | state.sample_buffer.clear(); | ||
| 272 | state | ||
| 273 | .sample_buffer | ||
| 274 | .extend_from_slice( | ||
| 275 | &resampled[to_copy..], | ||
| 276 | ); | ||
| 277 | state.buffer_pos = 0; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | Err(_) => break, | ||
| 281 | } | ||
| 282 | } | ||
| 283 | Err(_) => break, | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | // Apply volume | ||
| 288 | for sample in data.iter_mut().take(offset) { | ||
| 289 | *sample *= vol; | ||
| 290 | } | ||
| 291 | |||
| 292 | // Fill remaining with silence | ||
| 293 | for sample in data.iter_mut().skip(offset) { | ||
| 294 | *sample = 0.0; | ||
| 295 | } | ||
| 296 | |||
| 297 | // If no more data and offset is 0, track ended | ||
| 298 | if offset == 0 && state.sample_buffer.is_empty() { | ||
| 299 | ended.store(true, Ordering::SeqCst); | ||
| 300 | is_playing_clone.store(false, Ordering::SeqCst); | ||
| 301 | // Clear audio data so we don't keep triggering ended | ||
| 302 | if let Ok(mut audio_data) = audio_data_clone.lock() { | ||
| 303 | *audio_data = None; | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } else { | ||
| 307 | for sample in data.iter_mut() { | ||
| 308 | *sample = 0.0; | ||
| 309 | } | ||
| 310 | } | ||
| 311 | } | ||
| 312 | }, | ||
| 313 | |err| { | ||
| 314 | eprintln!("Stream error: {}", err); | ||
| 315 | }, | ||
| 316 | None, | ||
| 317 | ) { | ||
| 318 | Ok(stream) => { | ||
| 319 | if let Err(e) = stream.play() { | ||
| 320 | eprintln!("Failed to start stream: {}", e); | ||
| 321 | is_playing.store(false, Ordering::SeqCst); | ||
| 322 | track_ended.store(true, Ordering::SeqCst); | ||
| 323 | } else { | ||
| 324 | current_stream = Some(stream); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | Err(e) => { | ||
| 328 | eprintln!("Failed to create stream: {}", e); | ||
| 329 | is_playing.store(false, Ordering::SeqCst); | ||
| 330 | track_ended.store(true, Ordering::SeqCst); | ||
| 331 | } | ||
| 332 | } | ||
| 333 | } | ||
| 334 | Err(e) => { | ||
| 335 | eprintln!("Failed to open audio: {}", e); | ||
| 336 | is_playing.store(false, Ordering::SeqCst); | ||
| 337 | track_ended.store(true, Ordering::SeqCst); | ||
| 338 | } | ||
| 339 | } | ||
| 340 | } | ||
| 341 | AudioCommand::Pause => { | ||
| 342 | if let Some(ref stream) = current_stream { | ||
| 343 | if let Err(e) = stream.pause() { | ||
| 344 | eprintln!("Failed to pause stream: {}", e); | ||
| 345 | } | ||
| 346 | is_playing.store(false, Ordering::SeqCst); | ||
| 347 | } | ||
| 348 | } | ||
| 349 | AudioCommand::Resume => { | ||
| 350 | if let Some(ref stream) = current_stream { | ||
| 351 | if let Err(e) = stream.play() { | ||
| 352 | eprintln!("Failed to resume stream: {}", e); | ||
| 353 | } else { | ||
| 354 | is_playing.store(true, Ordering::SeqCst); | ||
| 355 | } | ||
| 356 | } | ||
| 357 | } | ||
| 358 | AudioCommand::Stop => { | ||
| 359 | current_stream = None; | ||
| 360 | if let Ok(mut d) = audio_data.lock() { | ||
| 361 | *d = None; | ||
| 362 | } | ||
| 363 | is_playing.store(false, Ordering::SeqCst); | ||
| 364 | if let Ok(mut track) = current_track.lock() { | ||
| 365 | *track = None; | ||
| 366 | } | ||
| 367 | } | ||
| 368 | AudioCommand::SetVolume(vol) => { | ||
| 369 | if let Ok(mut volume_ref) = volume.lock() { | ||
| 370 | *volume_ref = vol; | ||
| 371 | } | ||
| 372 | } | ||
| 373 | } | ||
| 374 | } | ||
| 375 | } | ||
| 376 | |||
| 377 | fn open_audio_file(path: &PathBuf) -> Result<AudioData, Box<dyn std::error::Error>> { | ||
| 378 | use symphonia::core::audio::Channels; | ||
| 379 | use symphonia::core::io::MediaSourceStream; | ||
| 380 | |||
| 381 | let file = std::fs::File::open(path)?; | ||
| 382 | let mss = MediaSourceStream::new(Box::new(file), Default::default()); | ||
| 383 | |||
| 384 | let hint = symphonia::core::probe::Hint::new(); | ||
| 385 | let probed = symphonia::default::get_probe().format( | ||
| 386 | &hint, | ||
| 387 | mss, | ||
| 388 | &Default::default(), | ||
| 389 | &Default::default(), | ||
| 390 | )?; | ||
| 391 | |||
| 392 | let format = probed.format; | ||
| 393 | let track = format | ||
| 394 | .tracks() | ||
| 395 | .iter() | ||
| 396 | .find(|t| t.codec_params.sample_rate.is_some()) | ||
| 397 | .ok_or("No suitable track found")?; | ||
| 398 | |||
| 399 | let sample_rate = track.codec_params.sample_rate.unwrap_or(44100); | ||
| 400 | let channels = track | ||
| 401 | .codec_params | ||
| 402 | .channels | ||
| 403 | .unwrap_or(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) | ||
| 404 | .count() as u16; | ||
| 405 | |||
| 406 | let decoder = | ||
| 407 | symphonia::default::get_codecs().make(&track.codec_params, &Default::default())?; | ||
| 408 | |||
| 409 | Ok(AudioData { | ||
| 410 | format, | ||
| 411 | decoder, | ||
| 412 | sample_rate, | ||
| 413 | channels, | ||
| 414 | sample_buffer: Vec::new(), | ||
| 415 | buffer_pos: 0, | ||
| 416 | resample_ratio: None, | ||
| 417 | }) | ||
| 418 | } | ||
