1#![doc = include_str!("../README.md")]
2#![warn(rust_2018_idioms)]
3#![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 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 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}