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 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#[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 #[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 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}