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