From bd29978b2a0d0d6def486fcdaa63bc8934d68045 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 5 May 2026 21:12:32 +0200 Subject: fix(daemon): robust auto-advance; ensure next track starts even if playback fails --- src/audio.rs | 418 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 src/audio.rs (limited to 'src/audio.rs') 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 @@ +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::{Arc, Mutex}; +use std::thread; + +#[allow(dead_code)] +pub enum AudioCommand { + Play(PathBuf), + Pause, + Resume, + Stop, + SetVolume(f32), +} + +pub struct AudioEngine { + command_tx: Sender, + is_playing: Arc, + current_track: Arc>>, + track_ended: Arc, +} + +impl AudioEngine { + pub fn new() -> Self { + let (tx, rx) = mpsc::channel(); + let is_playing = Arc::new(AtomicBool::new(false)); + let is_playing_clone = Arc::clone(&is_playing); + let current_track = Arc::new(Mutex::new(None)); + let current_track_clone = Arc::clone(¤t_track); + let track_ended = Arc::new(AtomicBool::new(false)); + let track_ended_clone = Arc::clone(&track_ended); + + let volume = Arc::new(Mutex::new(1.0f32)); + let volume_clone = Arc::clone(&volume); + + thread::spawn(move || { + audio_thread( + rx, + is_playing_clone, + current_track_clone, + volume_clone, + track_ended_clone, + ); + }); + + AudioEngine { + command_tx: tx, + is_playing, + current_track, + track_ended, + } + } + + pub fn play(&self, path: PathBuf) { + self.track_ended.store(false, Ordering::SeqCst); + if let Ok(mut track) = self.current_track.lock() { + *track = Some(path.clone()); + } + let _ = self.command_tx.send(AudioCommand::Play(path)); + } + + pub fn pause(&self) { + let _ = self.command_tx.send(AudioCommand::Pause); + } + + pub fn resume(&self) { + let _ = self.command_tx.send(AudioCommand::Resume); + } + + pub fn stop(&self) { + let _ = self.command_tx.send(AudioCommand::Stop); + } + + pub fn set_volume(&self, volume: f32) { + let _ = self.command_tx.send(AudioCommand::SetVolume(volume)); + } + + pub fn is_playing(&self) -> bool { + self.is_playing.load(Ordering::SeqCst) + } + + pub fn current_track(&self) -> Option { + self.current_track.lock().ok().and_then(|g| g.clone()) + } + + pub fn poll_track_ended(&self) -> bool { + self.track_ended.swap(false, Ordering::SeqCst) + } +} + +struct AudioData { + format: Box, + decoder: Box, + sample_rate: u32, + channels: u16, + sample_buffer: Vec, + buffer_pos: usize, + resample_ratio: Option, +} + +fn resample_linear(input: &[f32], ratio: f64, channels: usize) -> Vec { + if ratio == 1.0 { + return input.to_vec(); + } + let input_frames = input.len() / channels; + let output_frames = (input_frames as f64 * ratio).ceil() as usize; + let mut output = Vec::with_capacity(output_frames * channels); + for i in 0..output_frames { + let input_pos = i as f64 / ratio; + let idx = input_pos.floor() as usize; + let frac = input_pos.fract() as f32; + for ch in 0..channels { + let cur = if idx * channels + ch < input.len() { + input[idx * channels + ch] + } else { + 0.0 + }; + let next = if (idx + 1) * channels + ch < input.len() { + input[(idx + 1) * channels + ch] + } else { + cur + }; + output.push(cur + frac * (next - cur)); + } + } + output +} + +fn audio_thread( + rx: Receiver, + is_playing: Arc, + current_track: Arc>>, + volume: Arc>, + track_ended: Arc, +) { + use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + + let host = cpal::default_host(); + let device = match host.default_output_device() { + Some(d) => d, + None => { + eprintln!("No output device available"); + return; + } + }; + + let config = match device.default_output_config() { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to get output config: {}", e); + return; + } + }; + + let device_sample_rate = config.sample_rate(); + let channels = config.channels(); + eprintln!( + "Default output: {} Hz, {} channels", + device_sample_rate, channels + ); + + let mut current_stream: Option = None; + let audio_data: Arc>> = Arc::new(Mutex::new(None)); + + while let Ok(cmd) = rx.recv() { + match cmd { + AudioCommand::Play(path) => { + is_playing.store(true, Ordering::SeqCst); + if let Ok(mut track) = current_track.lock() { + *track = Some(path.clone()); + } + + // Drop the current stream before creating a new one + current_stream = None; + + match open_audio_file(&path) { + Ok(mut data) => { + eprintln!("File: {} Hz, {} channels", data.sample_rate, data.channels); + + if data.sample_rate != device_sample_rate { + eprintln!( + "Resampling from {} Hz to {} Hz", + data.sample_rate, device_sample_rate + ); + data.resample_ratio = + Some(device_sample_rate as f64 / data.sample_rate as f64); + } + + let stream_config: cpal::StreamConfig = + device.default_output_config().unwrap().into(); + + let audio_data_clone = Arc::clone(&audio_data); + let vol = Arc::clone(&volume); + let ended = Arc::clone(&track_ended); + let is_playing_clone = Arc::clone(&is_playing); + + if let Ok(mut d) = audio_data.lock() { + *d = Some(data); + } + + match device.build_output_stream( + &stream_config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let vol = vol.lock().map(|v| *v).unwrap_or(1.0); + + if let Ok(mut audio_data) = audio_data_clone.lock() { + if let Some(ref mut state) = *audio_data { + let channels = state.channels as usize; + let mut offset = 0usize; + + // Keep decoding until we fill the buffer or run out of data + while offset < data.len() { + // First, copy any buffered samples + if state.buffer_pos < state.sample_buffer.len() { + let remaining = + &state.sample_buffer[state.buffer_pos..]; + let to_copy = + remaining.len().min(data.len() - offset); + data[offset..offset + to_copy] + .copy_from_slice(&remaining[..to_copy]); + state.buffer_pos += to_copy; + offset += to_copy; + + // If we've consumed all buffered samples, clear the buffer + if state.buffer_pos >= state.sample_buffer.len() { + state.sample_buffer.clear(); + state.buffer_pos = 0; + } + continue; // Try to fill more of the buffer + } + + // Decode more data + match state.format.next_packet() { + Ok(packet) => { + match state.decoder.decode(&packet) { + Ok(decoded) => { + let spec = *decoded.spec(); + let mut sample_buf = + symphonia::core::audio::SampleBuffer::< + f32, + >::new( + decoded.capacity() as u64, spec + ); + sample_buf + .copy_interleaved_ref(decoded); + let samples = sample_buf.samples(); + + // Resample if needed + let resampled = if let Some(ratio) = + state.resample_ratio + { + resample_linear( + samples, ratio, channels, + ) + } else { + samples.to_vec() + }; + + // Copy what we can to output + let to_copy = resampled + .len() + .min(data.len() - offset); + data[offset..offset + to_copy] + .copy_from_slice( + &resampled[..to_copy], + ); + offset += to_copy; + + // Buffer the rest + if to_copy < resampled.len() { + state.sample_buffer.clear(); + state + .sample_buffer + .extend_from_slice( + &resampled[to_copy..], + ); + state.buffer_pos = 0; + } + } + Err(_) => break, + } + } + Err(_) => break, + } + } + + // Apply volume + for sample in data.iter_mut().take(offset) { + *sample *= vol; + } + + // Fill remaining with silence + for sample in data.iter_mut().skip(offset) { + *sample = 0.0; + } + + // If no more data and offset is 0, track ended + if offset == 0 && state.sample_buffer.is_empty() { + ended.store(true, Ordering::SeqCst); + is_playing_clone.store(false, Ordering::SeqCst); + // Clear audio data so we don't keep triggering ended + if let Ok(mut audio_data) = audio_data_clone.lock() { + *audio_data = None; + } + } + } else { + for sample in data.iter_mut() { + *sample = 0.0; + } + } + } + }, + |err| { + eprintln!("Stream error: {}", err); + }, + None, + ) { + Ok(stream) => { + if let Err(e) = stream.play() { + eprintln!("Failed to start stream: {}", e); + is_playing.store(false, Ordering::SeqCst); + track_ended.store(true, Ordering::SeqCst); + } else { + current_stream = Some(stream); + } + } + Err(e) => { + eprintln!("Failed to create stream: {}", e); + is_playing.store(false, Ordering::SeqCst); + track_ended.store(true, Ordering::SeqCst); + } + } + } + Err(e) => { + eprintln!("Failed to open audio: {}", e); + is_playing.store(false, Ordering::SeqCst); + track_ended.store(true, Ordering::SeqCst); + } + } + } + AudioCommand::Pause => { + if let Some(ref stream) = current_stream { + if let Err(e) = stream.pause() { + eprintln!("Failed to pause stream: {}", e); + } + is_playing.store(false, Ordering::SeqCst); + } + } + AudioCommand::Resume => { + if let Some(ref stream) = current_stream { + if let Err(e) = stream.play() { + eprintln!("Failed to resume stream: {}", e); + } else { + is_playing.store(true, Ordering::SeqCst); + } + } + } + AudioCommand::Stop => { + current_stream = None; + if let Ok(mut d) = audio_data.lock() { + *d = None; + } + is_playing.store(false, Ordering::SeqCst); + if let Ok(mut track) = current_track.lock() { + *track = None; + } + } + AudioCommand::SetVolume(vol) => { + if let Ok(mut volume_ref) = volume.lock() { + *volume_ref = vol; + } + } + } + } +} + +fn open_audio_file(path: &PathBuf) -> Result> { + use symphonia::core::audio::Channels; + use symphonia::core::io::MediaSourceStream; + + let file = std::fs::File::open(path)?; + let mss = MediaSourceStream::new(Box::new(file), Default::default()); + + let hint = symphonia::core::probe::Hint::new(); + let probed = symphonia::default::get_probe().format( + &hint, + mss, + &Default::default(), + &Default::default(), + )?; + + let format = probed.format; + let track = format + .tracks() + .iter() + .find(|t| t.codec_params.sample_rate.is_some()) + .ok_or("No suitable track found")?; + + let sample_rate = track.codec_params.sample_rate.unwrap_or(44100); + let channels = track + .codec_params + .channels + .unwrap_or(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) + .count() as u16; + + let decoder = + symphonia::default::get_codecs().make(&track.codec_params, &Default::default())?; + + Ok(AudioData { + format, + decoder, + sample_rate, + channels, + sample_buffer: Vec::new(), + buffer_pos: 0, + resample_ratio: None, + }) +} -- cgit v1.2.3