Skip to main content

mittens_engine/engine/ecs/system/
audio_decode_thread.rs

1//! Audio decoding worker thread.
2//!
3//! Owns the symphonia decoder. Receives `LoadClipRequest`s from the main
4//! thread via mpsc and ships completed assets to the audio render thread
5//! over a wait-free `rtrb` producer.
6//!
7//! Phase 5 ships full-buffer messages only (short clips). Streaming for
8//! long BGM lands when needed — same protocol, smaller chunks.
9
10use std::sync::Arc;
11use std::sync::mpsc::{Receiver, Sender, channel};
12use std::thread::JoinHandle;
13
14use super::audio_decode::{DecodeError, decode_audio_file};
15use super::audio_sample_format_convert::{ConvertError, PlaybackFormat, convert_sample_format};
16
17/// Request from main → decode thread.
18#[derive(Debug)]
19pub struct LoadClipRequest {
20    pub clip_id: u64,
21    pub uri: String,
22    pub target: PlaybackFormat,
23}
24
25/// Completion message from decode → RT thread.
26#[derive(Debug, Clone)]
27pub enum LoadedClipMessage {
28    Loaded {
29        clip_id: u64,
30        samples: Arc<Vec<f32>>,
31        channels: u16,
32        sample_rate: u32,
33    },
34    Failed {
35        clip_id: u64,
36        reason: String,
37    },
38}
39
40/// Sender + handle to the worker.
41pub struct DecodeThreadHandle {
42    pub tx: Sender<LoadClipRequest>,
43    pub thread: Option<JoinHandle<()>>,
44}
45
46impl DecodeThreadHandle {
47    pub fn shutdown(mut self) {
48        drop(self.tx);
49        if let Some(t) = self.thread.take() {
50            let _ = t.join();
51        }
52    }
53}
54
55/// Spawn the worker. Completion messages go to `complete_tx` (main
56/// thread). The main thread forwards them to the audio RT thread.
57pub fn spawn_decode_thread(complete_tx: Sender<LoadedClipMessage>) -> DecodeThreadHandle {
58    let (tx, rx): (Sender<LoadClipRequest>, Receiver<LoadClipRequest>) = channel();
59
60    let thread = std::thread::Builder::new()
61        .name("cat-engine-audio-decode".into())
62        .spawn(move || worker_main(rx, complete_tx))
63        .expect("failed to spawn audio decode thread");
64
65    DecodeThreadHandle {
66        tx,
67        thread: Some(thread),
68    }
69}
70
71fn worker_main(rx: Receiver<LoadClipRequest>, complete_tx: Sender<LoadedClipMessage>) {
72    while let Ok(req) = rx.recv() {
73        let LoadClipRequest {
74            clip_id,
75            uri,
76            target,
77        } = req;
78
79        let msg = match decode_and_convert(&uri, target) {
80            Ok((samples, channels, sample_rate)) => LoadedClipMessage::Loaded {
81                clip_id,
82                samples,
83                channels,
84                sample_rate,
85            },
86            Err(reason) => LoadedClipMessage::Failed { clip_id, reason },
87        };
88        // If the receiver has been dropped (e.g. shutdown), exit quietly.
89        if complete_tx.send(msg).is_err() {
90            break;
91        }
92    }
93}
94
95fn decode_and_convert(
96    uri: &str,
97    target: PlaybackFormat,
98) -> Result<(Arc<Vec<f32>>, u16, u32), String> {
99    let decoded = decode_audio_file(uri).map_err(|e: DecodeError| e.to_string())?;
100    let converted =
101        convert_sample_format(decoded, target).map_err(|e: ConvertError| e.to_string())?;
102    Ok((converted.samples, converted.channels, converted.sample_rate))
103}