aboutsummaryrefslogtreecommitdiffstats
path: root/src/audio.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-05-05 21:12:32 +0200
committersalmonik <raisersoft@proton.me>2026-05-05 21:12:32 +0200
commitbd29978b2a0d0d6def486fcdaa63bc8934d68045 (patch)
tree5df0efb17d350ae7f095eb9c0940b1bbf568de96 /src/audio.rs
downloadrsplayer-bd29978b2a0d0d6def486fcdaa63bc8934d68045.tar.gz
rsplayer-bd29978b2a0d0d6def486fcdaa63bc8934d68045.zip
fix(daemon): robust auto-advance; ensure next track starts even if playback fails
Diffstat (limited to 'src/audio.rs')
-rw-r--r--src/audio.rs418
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 @@
1use std::path::PathBuf;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::mpsc::{self, Receiver, Sender};
4use std::sync::{Arc, Mutex};
5use std::thread;
6
7#[allow(dead_code)]
8pub enum AudioCommand {
9 Play(PathBuf),
10 Pause,
11 Resume,
12 Stop,
13 SetVolume(f32),
14}
15
16pub 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
23impl 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(&current_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
91struct 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
101fn 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
129fn 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
377fn 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}