fennel_engine/
audio.rs

1//! Audio playback thing
2///
3/// This module provides a simple asynchronous audio player
4///
5/// # Example
6/// ```ignore
7/// game.audio.play_audio(Path::new("examples/music.ogg"), false).await?;
8/// ```
9use std::{
10    io::BufReader,
11    path::{Path, PathBuf},
12};
13
14use tokio::sync::mpsc::{self, Sender};
15
16/// Holds the command channel
17#[derive(Debug, Clone)]
18pub struct Audio {
19    /// Tokio mpsc sender used to communicate with the audio thread
20    pub channel: Sender<AudioCommand>,
21}
22
23/// Commands that can be sent to the audio thread.
24#[derive(Debug, Clone)]
25pub enum AudioCommand {
26    /// Play the file at the given path
27    Play(PathBuf),
28    /// Stop the current playback
29    Stop,
30}
31
32impl Audio {
33    /// Creates a new [`Audio`] instance and spawns the background audio thread
34    ///
35    /// Workflow:
36    /// 1. Opens a default `rodio` output stream.
37    /// 2. Listens on an mpsc channel for `AudioCommand`s.
38    /// 3. On `Play`, stops any existing sink and starts a new one.
39    pub fn new() -> Audio {
40        let (tx, mut rx) = mpsc::channel::<AudioCommand>(16);
41
42        tokio::spawn(async move {
43            println!("hey im the audio thread nya meow meow >:3");
44            let stream_handle = rodio::OutputStreamBuilder::open_default_stream()
45                .expect("audio: failed to initialize stream handle");
46            let mixer = stream_handle.mixer();
47            let mut current_sink: Option<rodio::Sink> = None;
48
49            while let Some(message) = rx.recv().await {
50                match message {
51                    AudioCommand::Play(pathbuf) => {
52                        if let Some(sink) = current_sink.take() {
53                            sink.stop();
54                        }
55
56                        let file =
57                            std::fs::File::open(&pathbuf).expect("audio: failed to open file");
58                        let sink = rodio::play(mixer, BufReader::new(file))
59                            .expect("audio: failed to start playback");
60                        sink.set_volume(1.0);
61                        current_sink = Some(sink);
62                    }
63                    AudioCommand::Stop => {
64                        if let Some(sink) = current_sink.take() {
65                            sink.stop();
66                        }
67                    }
68                }
69            }
70        });
71
72        Self { channel: tx }
73    }
74
75    /// Sends a `Play` command for the given path
76    pub async fn play_audio(
77        &mut self,
78        path: &Path,
79        _interrupt_current_playback: bool,
80    ) -> anyhow::Result<()> {
81        // TODO: use [`ResourceManager`]
82        self.channel
83            .send(AudioCommand::Play(path.to_path_buf()))
84            .await?;
85        Ok(())
86    }
87}
88
89// clippy said i need `Default`
90impl Default for Audio {
91    /// Shortcut for `Audio::new()`
92    fn default() -> Self {
93        Self::new()
94    }
95}