Skip to main content

chatsounds/
lib.rs

1#![warn(clippy::nursery, clippy::pedantic)]
2#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
3
4mod cache;
5#[cfg(feature = "playback")]
6mod channel_volume;
7mod error;
8mod fetching;
9mod parsing;
10mod types;
11
12use std::{
13    collections::{HashMap, VecDeque},
14    io::{BufReader, Cursor},
15    path::{Path, PathBuf},
16    sync::Arc,
17};
18
19pub use bytes::Bytes;
20use rand::prelude::*;
21pub use rodio;
22use rodio::{Decoder, Player, SpatialPlayer};
23#[cfg(feature = "playback")]
24use rodio::{
25    DeviceSinkBuilder, MixerDeviceSink,
26    cpal::traits::{DeviceTrait, HostTrait},
27};
28
29#[cfg(feature = "playback")]
30pub use self::channel_volume::ChannelVolumeSink;
31#[cfg(feature = "playback")]
32use self::types::ChatsoundsSink;
33pub use self::{
34    error::Error,
35    fetching::{GitHubApiFileEntry, GitHubApiTrees, GitHubMsgpackEntries},
36    parsing::normalize_sentence,
37    types::Chatsound,
38};
39use self::{
40    error::Result,
41    parsing::{ModifierTrait, parse},
42    types::BoxSource,
43};
44
45#[cfg(feature = "memory")]
46type FsMemory = Arc<async_lock::RwLock<HashMap<String, Bytes>>>;
47
48pub struct Chatsounds {
49    #[cfg(feature = "fs")]
50    cache_path: PathBuf,
51    // [sentence]: Chatsound[]
52    map_store: HashMap<String, Vec<Chatsound>>,
53
54    #[cfg(feature = "playback")]
55    max_sinks: usize,
56    #[cfg(feature = "playback")]
57    volume: f32,
58
59    #[cfg(feature = "playback")]
60    output_stream: MixerDeviceSink,
61    #[cfg(feature = "playback")]
62    sinks: VecDeque<Box<dyn ChatsoundsSink>>,
63
64    #[cfg(feature = "memory")]
65    fs_memory: FsMemory,
66}
67
68// TODO ???
69#[allow(clippy::non_send_fields_in_send_ty)]
70unsafe impl Send for Chatsounds {}
71unsafe impl Sync for Chatsounds {}
72
73impl Chatsounds {
74    pub fn new(#[cfg(feature = "fs")] cache_path: &Path) -> Result<Self> {
75        #[cfg(feature = "fs")]
76        let cache_path = cache_path.canonicalize().map_err(|err| Error::Io {
77            err,
78            path: cache_path.into(),
79        })?;
80        #[cfg(feature = "fs")]
81        ensure!(cache_path.is_dir(), Error::DirMissing { path: cache_path });
82        #[cfg(feature = "fs")]
83        tracing::debug!(cache_path = %cache_path.display(), "using cache directory");
84
85        // `open_default_sink()` discards the `cpal::Device`, so the returned
86        // sink can't tell us what it opened. Open the default device ourselves
87        // (exactly what `open_default_sink` tries first, same buffer tuning via
88        // `from_device`) so we hold the device and can log its name. Fall back
89        // to `open_default_sink` -- which also retries alternate configs and
90        // other devices -- if the direct attempt fails; the device is unknown
91        // in that case.
92        #[cfg(feature = "playback")]
93        let (mut output_stream, device_name) =
94            match rodio::cpal::default_host().default_output_device() {
95                Some(device) => {
96                    let name = device.description().ok().map(|d| d.name().to_string());
97                    match DeviceSinkBuilder::from_device(device)
98                        .and_then(DeviceSinkBuilder::open_stream)
99                    {
100                        Ok(stream) => (stream, name),
101                        Err(_) => (DeviceSinkBuilder::open_default_sink()?, None),
102                    }
103                }
104                None => (DeviceSinkBuilder::open_default_sink()?, None),
105            };
106        #[cfg(feature = "playback")]
107        output_stream.log_on_drop(false);
108        #[cfg(feature = "playback")]
109        tracing::debug!(
110            device = device_name.as_deref().unwrap_or("unknown"),
111            config = ?output_stream.config(),
112            "opened audio output device"
113        );
114
115        Ok(Self {
116            #[cfg(feature = "fs")]
117            cache_path,
118            map_store: HashMap::new(),
119
120            #[cfg(feature = "playback")]
121            max_sinks: 64,
122            #[cfg(feature = "playback")]
123            volume: 0.1,
124
125            #[cfg(feature = "playback")]
126            output_stream,
127            #[cfg(feature = "playback")]
128            sinks: VecDeque::new(),
129
130            #[cfg(feature = "memory")]
131            fs_memory: Default::default(),
132        })
133    }
134
135    #[must_use]
136    pub fn get(&self, sentence: &str) -> Option<&Vec<Chatsound>> {
137        self.map_store.get(&normalize_sentence(sentence))
138    }
139
140    #[must_use]
141    pub fn search(&self, search: &str) -> Vec<(usize, &String)> {
142        #[cfg(feature = "rayon")]
143        use rayon::prelude::*;
144
145        #[cfg(feature = "rayon")]
146        let iter = HashMap::par_iter;
147        #[cfg(not(feature = "rayon"))]
148        let iter = HashMap::iter;
149
150        #[cfg(feature = "rayon")]
151        let sort_by = <[(usize, &String)]>::par_sort_unstable_by;
152        #[cfg(not(feature = "rayon"))]
153        let sort_by = <[(usize, &String)]>::sort_unstable_by;
154
155        let search = normalize_sentence(search);
156
157        let mut positions: Vec<_> = iter(&self.map_store)
158            .map(|(key, _value)| key)
159            .filter_map(|sentence| twoway::find_str(sentence, &search).map(|pos| (pos, sentence)))
160            .collect();
161
162        sort_by(
163            positions.as_mut_slice(),
164            |(pos1, str1): &(usize, &String), (pos2, str2): &(usize, &String)| {
165                pos1.partial_cmp(pos2)
166                    .unwrap()
167                    .then_with(|| str1.len().partial_cmp(&str2.len()).unwrap())
168            },
169        );
170
171        tracing::trace!(search, results = positions.len(), "search");
172        positions
173    }
174
175    #[cfg(feature = "playback")]
176    pub fn stop_all(&mut self) {
177        tracing::debug!(sinks = self.sinks.len(), "stopping all sinks");
178        for sink in self.sinks.drain(..) {
179            sink.stop();
180        }
181    }
182
183    #[cfg(feature = "playback")]
184    pub fn set_volume(&mut self, volume: f32) {
185        let volume = volume.max(0.0);
186
187        self.volume = volume;
188
189        for sink in &mut self.sinks {
190            sink.set_volume(volume);
191        }
192    }
193
194    #[cfg(feature = "playback")]
195    #[must_use]
196    pub const fn volume(&self) -> f32 {
197        self.volume
198    }
199
200    #[cfg(feature = "playback")]
201    pub async fn play<R: Rng>(
202        &mut self,
203        text: &str,
204        rng: R,
205    ) -> Result<(Arc<Player>, Vec<Chatsound>)> {
206        tracing::debug!(text, "play");
207        let sink = Arc::new(Player::connect_new(self.output_stream.mixer()));
208
209        sink.set_volume(self.volume);
210
211        let (sources, chatsounds): (Vec<_>, Vec<_>) =
212            self.get_sources(text, rng).await?.into_iter().unzip();
213        for source in sources {
214            sink.append(source);
215        }
216
217        self.sinks.push_back(Box::new(sink.clone()));
218        if self.sinks.len() == self.max_sinks {
219            tracing::debug!(
220                max_sinks = self.max_sinks,
221                "sink limit reached; dropping oldest sink"
222            );
223            self.sinks.pop_front();
224        }
225
226        Ok((sink, chatsounds))
227    }
228
229    #[cfg(feature = "playback")]
230    pub async fn play_spatial<R: Rng>(
231        &mut self,
232        text: &str,
233        rng: R,
234        emitter_pos: [f32; 3],
235        left_ear_pos: [f32; 3],
236        right_ear_pos: [f32; 3],
237    ) -> Result<(Arc<SpatialPlayer>, Vec<Chatsound>)> {
238        tracing::debug!(text, ?emitter_pos, "play_spatial");
239        let sink = Arc::new(SpatialPlayer::connect_new(
240            self.output_stream.mixer(),
241            emitter_pos,
242            left_ear_pos,
243            right_ear_pos,
244        ));
245
246        sink.set_volume(self.volume);
247
248        let (sources, chatsounds): (Vec<_>, Vec<_>) =
249            self.get_sources(text, rng).await?.into_iter().unzip();
250        for source in sources {
251            sink.append(source);
252        }
253
254        self.sinks.push_back(Box::new(sink.clone()));
255        if self.sinks.len() == self.max_sinks {
256            tracing::debug!(
257                max_sinks = self.max_sinks,
258                "sink limit reached; dropping oldest sink"
259            );
260            self.sinks.pop_front();
261        }
262
263        Ok((sink, chatsounds))
264    }
265
266    #[cfg(feature = "playback")]
267    pub async fn play_channel_volume<R: Rng>(
268        &mut self,
269        text: &str,
270        rng: R,
271        channel_volumes: Vec<f32>,
272    ) -> Result<(Arc<ChannelVolumeSink>, Vec<Chatsound>)> {
273        tracing::debug!(text, ?channel_volumes, "play_channel_volume");
274        let sink = Arc::new(ChannelVolumeSink::connect_new(
275            self.output_stream.mixer(),
276            channel_volumes,
277        )?);
278
279        sink.sink.set_volume(self.volume);
280
281        let (sources, chatsounds): (Vec<_>, Vec<_>) =
282            self.get_sources(text, rng).await?.into_iter().unzip();
283        for source in sources {
284            sink.append(source);
285        }
286
287        self.sinks.push_back(Box::new(sink.clone()));
288        if self.sinks.len() == self.max_sinks {
289            tracing::debug!(
290                max_sinks = self.max_sinks,
291                "sink limit reached; dropping oldest sink"
292            );
293            self.sinks.pop_front();
294        }
295
296        Ok((sink, chatsounds))
297    }
298
299    pub async fn get_sources<R: Rng>(
300        &mut self,
301        text: &str,
302        mut rng: R,
303    ) -> Result<Vec<(BoxSource, Chatsound)>> {
304        let mut sources = Vec::new();
305
306        let parsed_chatsounds = parse(text)?;
307        tracing::debug!(
308            text,
309            count = parsed_chatsounds.len(),
310            "resolving chatsounds"
311        );
312        for parsed_chatsound in parsed_chatsounds {
313            let chatsound = if let Some(chatsounds) = self.get(&parsed_chatsound.sentence) {
314                // TODO random hashed number passed in?
315                parsed_chatsound
316                    .choose(chatsounds, &mut rng)
317                    .ok_or_else(|| Error::EmptyChoose { text: text.into() })?
318                    .to_owned()
319            } else {
320                tracing::debug!(sentence = %parsed_chatsound.sentence, "no chatsound matched sentence");
321                continue;
322            };
323            tracing::trace!(
324                sentence = %parsed_chatsound.sentence,
325                url = %chatsound.get_url(),
326                "chose chatsound"
327            );
328
329            let mut source: BoxSource = {
330                #[cfg(feature = "fs")]
331                let cache = &self.cache_path;
332                #[cfg(feature = "memory")]
333                let cache = self.fs_memory.clone();
334
335                let bytes = chatsound.load(cache).await?;
336
337                let reader = BufReader::new(Cursor::new(bytes));
338                let sound_path = chatsound.sound_path.clone();
339                Box::new(
340                    Decoder::new(reader).map_err(|err| Error::RodioDecoder { err, sound_path })?,
341                )
342            };
343            for modifier in parsed_chatsound.modifiers {
344                source = modifier.modify(source);
345            }
346
347            sources.push((source, chatsound));
348        }
349
350        Ok(sources)
351    }
352
353    #[cfg(feature = "playback")]
354    pub fn sleep_until_end(&mut self) {
355        for sink in self.sinks.drain(..) {
356            sink.sleep_until_end();
357        }
358    }
359}