arci_speak_audio/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    collections::HashMap,
5    fs::File,
6    io,
7    path::{Path, PathBuf},
8};
9
10use arci::{Speaker, WaitFuture};
11use thiserror::Error;
12use tokio::sync::oneshot;
13use tracing::error;
14
15#[derive(Error, Debug)]
16#[non_exhaustive]
17pub enum Error {
18    #[error("io: {:?}", .0)]
19    Io(#[from] std::io::Error),
20    #[error("rodio: {:?}", .0)]
21    Decoder(#[from] rodio::decoder::DecoderError),
22    #[error("rodio: {:?}", .0)]
23    Stream(#[from] rodio::StreamError),
24    #[error("rodio: {:?}", .0)]
25    Play(#[from] rodio::PlayError),
26    #[error("not found: {:?}", .0)]
27    HashNotFound(String),
28}
29
30#[derive(Debug)]
31pub struct AudioSpeaker {
32    message_to_file_path: HashMap<String, PathBuf>,
33}
34
35impl AudioSpeaker {
36    /// Creates a new `AudioSpeaker`.
37    pub fn new(hashmap: HashMap<String, PathBuf>) -> Self {
38        Self {
39            message_to_file_path: hashmap,
40        }
41    }
42}
43
44impl Speaker for AudioSpeaker {
45    fn speak(&self, message: &str) -> Result<WaitFuture, arci::Error> {
46        match self.message_to_file_path.get(message) {
47            Some(path) => play_audio_file(path),
48            None => Err(Error::HashNotFound(message.to_string())),
49        }
50        .map_err(|e| arci::Error::Other(e.into()))
51    }
52}
53
54fn play_audio_file(path: &Path) -> Result<WaitFuture, Error> {
55    let file = File::open(path)?;
56    let source = rodio::Decoder::new(io::BufReader::new(file))?;
57
58    let (sender, receiver) = oneshot::channel();
59    std::thread::spawn(move || {
60        let res: Result<_, Error> = (|| {
61            // NOTE: Dropping `_stream` stops the audio from playing.
62            let (_stream, stream_handle) = rodio::OutputStream::try_default()?;
63            let sink = rodio::Sink::try_new(&stream_handle)?;
64            sink.append(source);
65            sink.sleep_until_end();
66            Ok(())
67        })();
68        let _ = sender.send(res);
69    });
70
71    Ok(WaitFuture::new(async move {
72        receiver
73            .await
74            .map_err(|e| arci::Error::Other(e.into()))?
75            .map_err(|e| arci::Error::Other(e.into()))
76    }))
77}
78
79#[cfg(test)]
80mod test {
81    use super::*;
82
83    #[test]
84    fn test_audio_speaker_new() {
85        let audio_speaker = AudioSpeaker::new(HashMap::from([(
86            String::from("name"),
87            PathBuf::from("path"),
88        )]));
89        assert_eq!(
90            audio_speaker.message_to_file_path["name"],
91            PathBuf::from("path")
92        );
93    }
94
95    #[test]
96    fn test_audio_speaker_speak() {
97        let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
98        let root_dir = manifest_dir.parent().unwrap();
99        let audio_path = root_dir.join("openrr-apps/audio/sine.mp3");
100        let audio_speaker = AudioSpeaker::new(HashMap::from([(String::from("name"), audio_path)]));
101
102        assert!(audio_speaker.speak("name").is_ok());
103        assert!(audio_speaker.speak("not_exist").is_err());
104    }
105
106    #[test]
107    fn test_play_audio_file() {
108        let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
109        let root_dir = manifest_dir.parent().unwrap();
110        let audio_path = root_dir.join("openrr-apps/audio/sine.mp3");
111        let fake_path = root_dir.join("fake/audio/sine.mp3");
112
113        assert!(play_audio_file(&audio_path).is_ok());
114        assert!(play_audio_file(&fake_path).is_err());
115    }
116}