arci_speak_audio/
lib.rs

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