/* * This file is a part of RSPlayer * Licensed under the GPL-3.0 License * Copyright (C) 2026 Salmonik @ salmonik@tuta.io * https://codeberg.org/salmonik/RSPlayer */ use crate::notification; use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::thread; struct HttpMediaSource { reader: Mutex>, buffer: Vec, position: usize, eof: bool, } impl HttpMediaSource { fn new(reader: Box) -> Self { HttpMediaSource { reader: Mutex::new(reader), buffer: Vec::new(), position: 0, eof: false, } } } impl Read for HttpMediaSource { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { if self.position < self.buffer.len() { let available = self.buffer.len() - self.position; let to_copy = buf.len().min(available); buf[..to_copy].copy_from_slice(&self.buffer[self.position..self.position + to_copy]); self.position += to_copy; return Ok(to_copy); } if self.eof { return Ok(0); } let n = self.reader.lock().unwrap().read(buf)?; if n > 0 { self.buffer.extend_from_slice(&buf[..n]); self.position += n; } else { self.eof = true; } Ok(n) } } impl Seek for HttpMediaSource { fn seek(&mut self, pos: SeekFrom) -> std::io::Result { match pos { SeekFrom::Start(offset) => { let offset = offset as usize; while offset > self.buffer.len() && !self.eof { let mut temp = [0u8; 8192]; let n = self.reader.lock().unwrap().read(&mut temp)?; if n == 0 { self.eof = true; } else { self.buffer.extend_from_slice(&temp[..n]); } } self.position = offset.min(self.buffer.len()); Ok(self.position as u64) } SeekFrom::End(_) => Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "cannot seek from end on a stream", )), SeekFrom::Current(offset) => { let new_pos = if offset >= 0 { self.position.saturating_add(offset as usize) } else { self.position.saturating_sub((-offset) as usize) }; while new_pos > self.buffer.len() && !self.eof { let mut temp = [0u8; 8192]; let n = self.reader.lock().unwrap().read(&mut temp)?; if n == 0 { self.eof = true; } else { self.buffer.extend_from_slice(&temp[..n]); } } self.position = new_pos.min(self.buffer.len()); Ok(self.position as u64) } } } } impl symphonia::core::io::MediaSource for HttpMediaSource { fn is_seekable(&self) -> bool { true } fn byte_len(&self) -> Option { None } } #[allow(dead_code)] pub enum AudioCommand { Play(PathBuf), Pause, Resume, Stop, SetVolume(f32), } pub struct AudioEngine { command_tx: Sender, is_playing: Arc, pub current_track: Arc>>, track_ended: Arc, volume: Arc>, position: Arc>, } #[allow(clippy::new_without_default)] 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); let position = Arc::new(Mutex::new(0.0)); let position_clone = Arc::clone(&position); thread::spawn(move || { audio_thread( rx, is_playing_clone, current_track_clone, volume_clone, track_ended_clone, position_clone, ); }); AudioEngine { command_tx: tx, is_playing, current_track, track_ended, volume, position, } } 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 get_volume(&self) -> u8 { self.volume .lock() .map(|v| (*v * 100.0) as u8) .unwrap_or(100) } 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) } pub fn position(&self) -> f64 { self.position.lock().map(|p| *p).unwrap_or(0.0) } } 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, position: 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(); crate::debug_eprintln(format_args!( "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()); } if let Ok(mut pos) = position.lock() { *pos = 0.0; } // Drop the current stream before creating a new one current_stream = None; match open_audio_file(&path) { Ok(mut data) => { crate::debug_eprintln(format_args!( "File: {} Hz, {} channels", data.sample_rate, data.channels )); if data.sample_rate != device_sample_rate { crate::debug_eprintln(format_args!( "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); } // Pre-decode some samples before starting playback to prevent buffer underrun let channels = data.channels as usize; for _ in 0..100 { match data.format.next_packet() { Ok(Some(packet)) => match data.decoder.decode(&packet) { Ok(decoded) => { let num_samples = decoded.samples_interleaved(); let mut samples = vec![0.0f32; num_samples]; decoded.copy_to_slice_interleaved(&mut samples); let resampled = if let Some(ratio) = data.resample_ratio { resample_linear(&samples, ratio, channels) } else { samples }; data.sample_buffer.extend(resampled); } Err(_) => break, }, _ => break, } } data.buffer_pos = 0; 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); let position_clone = Arc::clone(&position); 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() { let mut should_clear = false; 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(Some(packet)) => { match state.decoder.decode(&packet) { Ok(decoded) => { let num_samples = decoded.samples_interleaved(); let mut samples = vec![0.0f32; num_samples]; decoded.copy_to_slice_interleaved( &mut samples, ); // Resample if needed let resampled = if let Some(ratio) = state.resample_ratio { resample_linear( &samples, ratio, channels, ) } else { samples }; // 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, } } _ => 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; } // Track playback position if offset > 0 && let Ok(mut pos) = position_clone.lock() { *pos += offset as f64 / (device_sample_rate as f64 * channels as f64); } // 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); should_clear = true; } } else { for sample in data.iter_mut() { *sample = 0.0; } } if should_clear { *audio_data = None; } } }, |err| { eprintln!("Stream error: {}", err); }, None, ) { Ok(stream) => { if let Err(e) = stream.play() { eprintln!("Failed to start stream: {}", e); notification::notify_error("Failed to start audio stream"); is_playing.store(false, Ordering::SeqCst); } else { current_stream = Some(stream); } } Err(e) => { eprintln!("Failed to create stream: {}", e); notification::notify_error("Failed to create audio stream"); is_playing.store(false, Ordering::SeqCst); } } } Err(e) => { eprintln!("Failed to open audio: {}", e); notification::notify_error("Failed to open audio file"); is_playing.store(false, 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::layouts::CHANNEL_LAYOUT_STEREO; use symphonia::core::codecs::audio::AudioDecoderOptions; use symphonia::core::formats::probe::Hint; use symphonia::core::io::MediaSourceStream; let path_str = path.to_string_lossy(); let is_url = path_str.starts_with("http://") || path_str.starts_with("https://"); let mss: MediaSourceStream = if is_url { let client = reqwest::blocking::Client::builder() .user_agent("RSPlayer/0.8.2") .connect_timeout(std::time::Duration::from_secs(10)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let response = client .get(path_str.as_ref()) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; let status = response.status(); if !status.is_success() { return Err(format!("HTTP error {} for URL", status).into()); } let reader = HttpMediaSource::new(Box::new(response)); MediaSourceStream::new(Box::new(reader), Default::default()) } else { let file = std::fs::File::open(path)?; MediaSourceStream::new(Box::new(file), Default::default()) }; let mut hint = Hint::new(); if is_url && let Some(ext) = Path::new(path_str.as_ref()) .extension() .and_then(|e| e.to_str()) { hint.with_extension(ext); } let format = symphonia::default::get_probe().probe( &hint, mss, Default::default(), Default::default(), )?; let track = format .tracks() .iter() .find(|t| { t.codec_params .as_ref() .and_then(|p| p.audio()) .and_then(|a| a.sample_rate) .is_some() }) .ok_or("No suitable track found")?; let codec_params = track .codec_params .as_ref() .ok_or("No codec parameters")? .audio() .ok_or("Not an audio track")?; let sample_rate = codec_params.sample_rate.unwrap_or(44100); let channels = codec_params .channels .clone() .unwrap_or(CHANNEL_LAYOUT_STEREO) .count() as u16; let decoder = symphonia::default::get_codecs() .make_audio_decoder(codec_params, &AudioDecoderOptions::default())?; Ok(AudioData { format, decoder, sample_rate, channels, sample_buffer: Vec::new(), buffer_pos: 0, resample_ratio: None, }) }