chatsounds/
lib.rs

1#![warn(clippy::nursery, clippy::pedantic)]
2#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
3
4mod cache;
5mod channel_volume;
6mod error;
7mod fetching;
8mod parsing;
9mod types;
10
11use std::{
12    collections::{HashMap, VecDeque},
13    io::{BufReader, Cursor},
14    path::{Path, PathBuf},
15    sync::Arc,
16};
17
18pub use bytes::Bytes;
19use rand::prelude::*;
20pub use rodio;
21use rodio::{Decoder, Sink, SpatialSink};
22#[cfg(feature = "playback")]
23use rodio::{OutputStream, OutputStreamHandle};
24
25pub use self::{
26    channel_volume::ChannelVolumeSink,
27    error::Error,
28    fetching::{GitHubApiFileEntry, GitHubApiTrees, GitHubMsgpackEntries},
29    types::Chatsound,
30};
31use self::{
32    error::Result,
33    parsing::{parse, ModifierTrait},
34    types::{BoxSource, ChatsoundsSink},
35};
36
37#[cfg(feature = "memory")]
38type FsMemory = Arc<async_lock::RwLock<HashMap<String, Bytes>>>;
39
40pub struct Chatsounds {
41    #[cfg(feature = "fs")]
42    cache_path: PathBuf,
43    // [sentence]: Chatsound[]
44    map_store: HashMap<String, Vec<Chatsound>>,
45
46    #[cfg(feature = "playback")]
47    max_sinks: usize,
48    #[cfg(feature = "playback")]
49    volume: f32,
50
51    #[cfg(feature = "playback")]
52    _output_stream: OutputStream,
53    #[cfg(feature = "playback")]
54    output_stream_handle: OutputStreamHandle,
55    #[cfg(feature = "playback")]
56    sinks: VecDeque<Box<dyn ChatsoundsSink>>,
57
58    #[cfg(feature = "memory")]
59    fs_memory: FsMemory,
60}
61
62// TODO ???
63#[allow(clippy::non_send_fields_in_send_ty)]
64unsafe impl Send for Chatsounds {}
65unsafe impl Sync for Chatsounds {}
66
67impl Chatsounds {
68    pub fn new(#[cfg(feature = "fs")] cache_path: &Path) -> Result<Self> {
69        #[cfg(feature = "fs")]
70        let cache_path = cache_path.canonicalize().map_err(|err| Error::Io {
71            err,
72            path: cache_path.into(),
73        })?;
74        #[cfg(feature = "fs")]
75        ensure!(cache_path.is_dir(), Error::DirMissing { path: cache_path });
76
77        #[cfg(feature = "playback")]
78        let (output_stream, output_stream_handle) = OutputStream::try_default()?;
79
80        Ok(Self {
81            #[cfg(feature = "fs")]
82            cache_path,
83            map_store: HashMap::new(),
84
85            #[cfg(feature = "playback")]
86            max_sinks: 16,
87            #[cfg(feature = "playback")]
88            volume: 0.1,
89
90            #[cfg(feature = "playback")]
91            _output_stream: output_stream,
92            #[cfg(feature = "playback")]
93            output_stream_handle,
94            #[cfg(feature = "playback")]
95            sinks: VecDeque::new(),
96
97            #[cfg(feature = "memory")]
98            fs_memory: Default::default(),
99        })
100    }
101
102    #[must_use]
103    pub fn get(&self, sentence: &str) -> Option<&Vec<Chatsound>> {
104        self.map_store.get(sentence)
105    }
106
107    #[must_use]
108    pub fn search(&self, search: &str) -> Vec<(usize, &String)> {
109        #[cfg(feature = "rayon")]
110        use rayon::prelude::*;
111
112        #[cfg(feature = "rayon")]
113        let iter = HashMap::par_iter;
114        #[cfg(not(feature = "rayon"))]
115        let iter = HashMap::iter;
116
117        #[cfg(feature = "rayon")]
118        let sort_by = <[(usize, &String)]>::par_sort_unstable_by;
119        #[cfg(not(feature = "rayon"))]
120        let sort_by = <[(usize, &String)]>::sort_unstable_by;
121
122        let mut positions: Vec<_> = iter(&self.map_store)
123            .map(|(key, _value)| key)
124            .filter_map(|sentence| twoway::find_str(sentence, search).map(|pos| (pos, sentence)))
125            .collect();
126
127        sort_by(
128            positions.as_mut_slice(),
129            |(pos1, str1): &(usize, &String), (pos2, str2): &(usize, &String)| {
130                pos1.partial_cmp(pos2)
131                    .unwrap()
132                    .then_with(|| str1.len().partial_cmp(&str2.len()).unwrap())
133            },
134        );
135
136        positions
137    }
138
139    #[cfg(feature = "playback")]
140    pub fn stop_all(&mut self) {
141        for sink in self.sinks.drain(..) {
142            sink.stop();
143        }
144    }
145
146    #[cfg(feature = "playback")]
147    pub fn set_volume(&mut self, volume: f32) {
148        let volume = volume.max(0.0);
149
150        self.volume = volume;
151
152        for sink in &mut self.sinks {
153            sink.set_volume(volume);
154        }
155    }
156
157    #[cfg(feature = "playback")]
158    #[must_use]
159    pub const fn volume(&self) -> f32 {
160        self.volume
161    }
162
163    #[cfg(feature = "playback")]
164    pub async fn play<R: RngCore>(&mut self, text: &str, rng: R) -> Result<Arc<Sink>> {
165        let sink = Arc::new(Sink::try_new(&self.output_stream_handle)?);
166
167        sink.set_volume(self.volume);
168
169        for source in self.get_sources(text, rng).await? {
170            sink.append(source);
171        }
172
173        self.sinks.push_back(Box::new(sink.clone()));
174        if self.sinks.len() == self.max_sinks {
175            self.sinks.pop_front();
176        }
177
178        Ok(sink)
179    }
180
181    #[cfg(feature = "playback")]
182    pub async fn play_spatial<R: RngCore>(
183        &mut self,
184        text: &str,
185        rng: R,
186        emitter_pos: [f32; 3],
187        left_ear_pos: [f32; 3],
188        right_ear_pos: [f32; 3],
189    ) -> Result<Arc<SpatialSink>> {
190        let sink = Arc::new(SpatialSink::try_new(
191            &self.output_stream_handle,
192            emitter_pos,
193            left_ear_pos,
194            right_ear_pos,
195        )?);
196
197        sink.set_volume(self.volume);
198
199        for source in self.get_sources(text, rng).await? {
200            sink.append(source);
201        }
202
203        self.sinks.push_back(Box::new(sink.clone()));
204        if self.sinks.len() == self.max_sinks {
205            self.sinks.pop_front();
206        }
207
208        Ok(sink)
209    }
210
211    #[cfg(feature = "playback")]
212    pub async fn play_channel_volume<R: RngCore>(
213        &mut self,
214        text: &str,
215        rng: R,
216        channel_volumes: Vec<f32>,
217    ) -> Result<Arc<ChannelVolumeSink>> {
218        let sink = Arc::new(ChannelVolumeSink::try_new(
219            &self.output_stream_handle,
220            channel_volumes,
221        )?);
222
223        sink.sink.set_volume(self.volume);
224
225        for source in self.get_sources(text, rng).await? {
226            sink.append(source);
227        }
228
229        self.sinks.push_back(Box::new(sink.clone()));
230        if self.sinks.len() == self.max_sinks {
231            self.sinks.pop_front();
232        }
233
234        Ok(sink)
235    }
236
237    pub async fn get_sources<R: RngCore>(
238        &mut self,
239        text: &str,
240        mut rng: R,
241    ) -> Result<Vec<BoxSource>> {
242        let mut sources = Vec::new();
243
244        let parsed_chatsounds = parse(text)?;
245        for parsed_chatsound in parsed_chatsounds {
246            let chatsound = if let Some(chatsounds) = self.get(&parsed_chatsound.sentence) {
247                // TODO random hashed number passed in?
248                parsed_chatsound
249                    .choose(chatsounds, &mut rng)
250                    .ok_or_else(|| Error::EmptyChoose { text: text.into() })?
251                    .to_owned()
252            } else {
253                continue;
254            };
255
256            let mut source: BoxSource = {
257                #[cfg(feature = "fs")]
258                let cache = &self.cache_path;
259                #[cfg(feature = "memory")]
260                let cache = self.fs_memory.clone();
261
262                let bytes = chatsound.load(cache).await?;
263
264                let reader = BufReader::new(Cursor::new(bytes));
265                Box::new(Decoder::new(reader).map_err(|err| Error::RodioDecoder {
266                    err,
267                    sound_path: chatsound.sound_path,
268                })?)
269            };
270            for modifier in parsed_chatsound.modifiers {
271                source = modifier.modify(source);
272            }
273
274            sources.push(source);
275        }
276
277        Ok(sources)
278    }
279
280    #[cfg(feature = "playback")]
281    pub fn sleep_until_end(&mut self) {
282        for sink in self.sinks.drain(..) {
283            sink.sleep_until_end();
284        }
285    }
286}