fennel_core/
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    pub fn new() -> Audio {
35        let (tx, mut rx) = mpsc::channel::<AudioCommand>(16);
36
37        tokio::spawn(async move {
38            println!("hey im the audio thread nya meow meow >:3");
39            let stream_handle = rodio::OutputStreamBuilder::open_default_stream()
40                .expect("audio: failed to initialize stream handle");
41            let mixer = stream_handle.mixer();
42            let mut current_sink: Option<rodio::Sink> = None;
43
44            while let Some(message) = rx.recv().await {
45                match message {
46                    AudioCommand::Play(pathbuf) => {
47                        if let Some(sink) = current_sink.take() {
48                            sink.stop();
49                        }
50
51                        let file =
52                            std::fs::File::open(&pathbuf).expect("audio: failed to open file");
53                        let sink = rodio::play(mixer, BufReader::new(file))
54                            .expect("audio: failed to start playback");
55                        sink.set_volume(1.0);
56                        current_sink = Some(sink);
57                    }
58                    AudioCommand::Stop => {
59                        if let Some(sink) = current_sink.take() {
60                            sink.stop();
61                        }
62                    }
63                }
64            }
65        });
66
67        Self { channel: tx }
68    }
69
70    /// Sends a `Play` command for the given path
71    pub async fn play_audio(
72        &mut self,
73        path: &Path,
74        _interrupt_current_playback: bool,
75    ) -> anyhow::Result<()> {
76        // TODO: use [`ResourceManager`], respect `_interrupt_current_playback`
77        self.channel
78            .send(AudioCommand::Play(path.to_path_buf()))
79            .await?;
80        Ok(())
81    }
82}
83
84// clippy said i need `Default`
85impl Default for Audio {
86    /// Shortcut for `Audio::new()`
87    fn default() -> Self {
88        Self::new()
89    }
90}