1use std::collections::HashMap;
5
6use ravenrs_widgets::SoundClip;
7
8const MAGIC: &[u8; 4] = b"RVN1";
9const HEADER: usize = 12;
10const ENTRY: usize = 16;
11const RATE: u32 = 16_000;
13const LEVEL: f32 = 0.12;
15const LOUDEST: f32 = 0.95;
16
17const STEPS: [i32; 89] = [
18 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88,
19 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658,
20 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660,
21 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818,
22 18500, 20350, 22385, 24623, 27086, 29794, 32767,
23];
24const STEP_CHANGE: [i32; 16] = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8];
25
26#[derive(Default)]
28struct Adpcm {
29 sample: i32,
30 step: i32,
31}
32
33#[cfg_attr(feature = "live", ravenrs_live::expose)]
34impl Adpcm {
35 fn take(&mut self, code: u8) -> i32 {
36 let step = STEPS[self.step as usize];
37 let mut change = step >> 3;
38 if code & 4 != 0 {
39 change += step;
40 }
41 if code & 2 != 0 {
42 change += step >> 1;
43 }
44 if code & 1 != 0 {
45 change += step >> 2;
46 }
47 self.sample += if code & 8 != 0 { -change } else { change };
48 self.sample = self.sample.clamp(-32768, 32767);
49 self.step = (self.step + STEP_CHANGE[code as usize]).clamp(0, 88);
50 self.sample
51 }
52
53 fn code_for(&self, sample: i32) -> u8 {
54 let mut step = STEPS[self.step as usize];
55 let difference = sample - self.sample;
56 let mut code = if difference < 0 { 8 } else { 0 };
57 let mut left = difference.abs();
58 for bit in [4, 2, 1] {
59 if left >= step {
60 code |= bit;
61 left -= step;
62 }
63 step >>= 1;
64 }
65 code
66 }
67}
68
69fn encode(samples: &[f32]) -> Vec<u8> {
70 let mut state = Adpcm::default();
71 let mut out = vec![0_u8; samples.len().div_ceil(2)];
72 for (index, sample) in samples.iter().enumerate() {
73 let wanted = (sample.clamp(-1.0, 1.0) * 32767.0) as i32;
74 let code = state.code_for(wanted);
75 state.take(code);
76 out[index / 2] |= if index % 2 == 0 { code } else { code << 4 };
77 }
78 out
79}
80
81fn decode(bytes: &[u8], samples: usize) -> Vec<f32> {
82 let mut state = Adpcm::default();
83 (0..samples)
84 .map(|index| {
85 let byte = bytes.get(index / 2).copied().unwrap_or(0);
86 let code = if index % 2 == 0 { byte & 15 } else { byte >> 4 };
87 state.take(code) as f32 / 32768.0
88 })
89 .collect()
90}
91
92fn resampled(samples: &[f32], rate: u32) -> Vec<f32> {
94 const REACH: isize = 12;
95 if rate == RATE || samples.is_empty() {
96 return samples.to_vec();
97 }
98 let ratio = f64::from(rate) / f64::from(RATE);
99 let cutoff = (f64::from(RATE) / f64::from(rate)).min(1.0) * 0.95;
100 let length = (samples.len() as f64 / ratio) as usize;
101 (0..length)
102 .map(|index| {
103 let center = index as f64 * ratio;
104 let nearest = center.round() as isize;
105 let mut sum = 0.0;
106 let mut weights = 0.0;
107 for offset in -REACH..=REACH {
108 let place = nearest + offset;
109 let Some(sample) = usize::try_from(place).ok().and_then(|place| samples.get(place)) else {
110 continue;
111 };
112 let distance = place as f64 - center;
113 let angle = std::f64::consts::PI * distance * cutoff;
114 let sinc = if angle.abs() < 1e-9 { 1.0 } else { angle.sin() / angle };
115 let window = 0.5 + 0.5 * (std::f64::consts::PI * distance / (REACH as f64 + 1.0)).cos();
116 sum += f64::from(*sample) * sinc * window;
117 weights += sinc * window;
118 }
119 if weights.abs() < 1e-9 { 0.0 } else { (sum / weights) as f32 }
120 })
121 .collect()
122}
123
124fn leveled(samples: &[f32]) -> Vec<f32> {
126 let energy: f32 = samples.iter().map(|sample| sample * sample).sum();
127 let level = (energy / samples.len().max(1) as f32).sqrt();
128 let peak = samples.iter().fold(0.0_f32, |peak, sample| peak.max(sample.abs()));
129 if level <= f32::EPSILON || peak <= f32::EPSILON {
130 return samples.to_vec();
131 }
132 let gain = (LEVEL / level).min(LOUDEST / peak);
133 samples.iter().map(|sample| sample * gain).collect()
134}
135
136fn key_of(text: &str) -> u64 {
138 text.bytes()
139 .fold(0xcbf2_9ce4_8422_2325, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3))
140}
141
142#[derive(Clone, Debug)]
145#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
146pub struct Narration {
147 bytes: &'static [u8],
148 entries: std::rc::Rc<HashMap<u64, (usize, usize)>>,
149}
150
151#[cfg_attr(feature = "live", ravenrs_live::expose)]
152impl Narration {
153 pub fn new(bytes: &'static [u8]) -> Self {
156 Self { bytes, entries: std::rc::Rc::new(entries_of(bytes).unwrap_or_default()) }
157 }
158
159 pub fn clip(&self, text: &str) -> Option<SoundClip> {
161 let (samples, start) = *self.entries.get(&key_of(text))?;
162 let body = self.bytes.get(start..start + samples.div_ceil(2))?;
163 Some(SoundClip::new(decode(body, samples), RATE))
164 }
165
166 pub fn seconds(&self, text: &str) -> Option<f64> {
168 let (samples, _) = *self.entries.get(&key_of(text))?;
169 Some(samples as f64 / f64::from(RATE))
170 }
171
172 pub fn len(&self) -> usize {
174 self.entries.len()
175 }
176
177 pub fn is_empty(&self) -> bool {
179 self.entries.is_empty()
180 }
181
182 pub fn pack(recordings: &[(String, Vec<f32>)], rate: u32) -> Vec<u8> {
184 Self::pack_onto(b"", recordings, rate)
185 }
186
187 pub fn pack_onto(bytes: &[u8], recordings: &[(String, Vec<f32>)], rate: u32) -> Vec<u8> {
192 let mut entries: Vec<(u64, usize, Vec<u8>)> = recordings
193 .iter()
194 .map(|(text, samples)| {
195 let samples = leveled(&resampled(samples, rate));
196 (key_of(text), samples.len(), encode(&samples))
197 })
198 .collect();
199 for (key, (samples, start)) in entries_of(bytes).unwrap_or_default() {
200 let new = entries.iter().any(|entry| entry.0 == key);
201 if let Some(body) = bytes.get(start..start + samples.div_ceil(2)).filter(|_| !new) {
202 entries.push((key, samples, body.to_vec()));
203 }
204 }
205 entries.sort_by_key(|entry| entry.0);
207 entries.dedup_by_key(|entry| entry.0);
208 let mut out = Vec::new();
209 out.extend_from_slice(MAGIC);
210 out.extend_from_slice(&RATE.to_le_bytes());
211 out.extend_from_slice(&(entries.len() as u32).to_le_bytes());
212 let mut start = HEADER + entries.len() * ENTRY;
213 for (key, samples, body) in &entries {
214 out.extend_from_slice(&key.to_le_bytes());
215 out.extend_from_slice(&(*samples as u32).to_le_bytes());
216 out.extend_from_slice(&(start as u32).to_le_bytes());
217 start += body.len();
218 }
219 for (_, _, body) in &entries {
220 out.extend_from_slice(body);
221 }
222 out
223 }
224}
225
226fn entries_of(bytes: &[u8]) -> Option<HashMap<u64, (usize, usize)>> {
227 if bytes.get(..4)? != MAGIC || u32::from_le_bytes(bytes.get(4..8)?.try_into().ok()?) != RATE {
228 return None;
229 }
230 let count = u32::from_le_bytes(bytes.get(8..12)?.try_into().ok()?) as usize;
231 let mut entries = HashMap::with_capacity(count);
232 for index in 0..count {
233 let at = HEADER + index * ENTRY;
234 let entry = bytes.get(at..at + ENTRY)?;
235 let key = u64::from_le_bytes(entry[..8].try_into().ok()?);
236 let samples = u32::from_le_bytes(entry[8..12].try_into().ok()?) as usize;
237 let start = u32::from_le_bytes(entry[12..16].try_into().ok()?) as usize;
238 entries.insert(key, (samples, start));
239 }
240 Some(entries)
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn tone(rate: u32, seconds: f32) -> Vec<f32> {
249 (0..(rate as f32 * seconds) as usize)
250 .map(|index| {
251 let time = index as f32 / rate as f32;
252 let turn = std::f32::consts::TAU * time;
253 0.3 * (turn * 180.0).sin() + 0.1 * (turn * 1200.0).sin()
254 })
255 .collect()
256 }
257
258 #[test]
259 fn a_recording_comes_back_close_to_what_went_in() {
260 let bytes = Narration::pack(&[("Oi".to_owned(), tone(24_000, 1.0))], 24_000);
261 let narration = Narration::new(Box::leak(bytes.into_boxed_slice()));
262 let clip = narration.clip("Oi").expect("the recording");
263 assert_eq!(narration.seconds("Oi"), Some(1.0));
264 assert_eq!(narration.seconds("Tchau"), None);
265 assert_eq!(clip.rate(), RATE);
266 assert_eq!(clip.samples().len(), RATE as usize);
267 let wanted = leveled(&tone(RATE, 1.0));
268 let noise: f32 = clip.samples().iter().zip(&wanted).skip(800).map(|(a, b)| (a - b).powi(2)).sum();
269 let signal: f32 = wanted.iter().skip(800).map(|a| a * a).sum();
270 assert!(noise / signal < 0.02, "{}", noise / signal);
271 }
272
273 #[test]
274 fn adding_keeps_what_was_there_byte_for_byte() {
275 let before = Narration::pack(
276 &[("Oi".to_owned(), tone(RATE, 0.5)), ("Tchau".to_owned(), tone(RATE, 0.2))],
277 RATE,
278 );
279 let after = Narration::pack_onto(&before, &[("Novo".to_owned(), tone(RATE, 0.3))], RATE);
280 let (old, new) = (
281 Narration::new(Box::leak(before.clone().into_boxed_slice())),
282 Narration::new(Box::leak(after.into_boxed_slice())),
283 );
284 assert_eq!(new.len(), 3);
285 for text in ["Oi", "Tchau"] {
286 assert_eq!(
287 new.clip(text).map(|clip| clip.samples().to_vec()),
288 old.clip(text).map(|clip| clip.samples().to_vec())
289 );
290 }
291 assert_eq!(new.seconds("Novo"), Some(0.3));
292 let again = Narration::pack_onto(&before, &[("Oi".to_owned(), tone(RATE, 1.0))], RATE);
293 let again = Narration::new(Box::leak(again.into_boxed_slice()));
294 assert_eq!((again.len(), again.seconds("Oi")), (2, Some(1.0)), "a new take replaces the old one");
295 assert_eq!(Narration::pack_onto(b"junk", &[], RATE), Narration::pack(&[], RATE));
296 }
297
298 #[test]
299 fn it_takes_a_quarter_of_the_space_of_plain_samples() {
300 let bytes = Narration::pack(&[("Oi".to_owned(), tone(RATE, 1.0))], RATE);
301 assert!(bytes.len() <= (RATE as usize) / 2 + HEADER + ENTRY + 1);
302 }
303
304 #[test]
305 fn a_text_with_no_recording_has_none() {
306 let bytes = Narration::pack(&[("Oi".to_owned(), tone(RATE, 0.1))], RATE);
307 let narration = Narration::new(Box::leak(bytes.into_boxed_slice()));
308 assert_eq!(narration.len(), 1);
309 assert!(narration.clip("Tchau").is_none());
310 }
311
312 #[test]
313 fn bytes_it_does_not_understand_hold_nothing() {
314 assert!(Narration::new(b"not a narration").is_empty());
315 assert!(Narration::new(b"").is_empty());
316 }
317
318 #[test]
319 fn the_same_recordings_make_the_same_file() {
320 let list = |first: &str, second: &str| {
321 Narration::pack(
322 &[(first.to_owned(), tone(RATE, 0.1)), (second.to_owned(), tone(RATE, 0.1))],
323 RATE,
324 )
325 };
326 assert_eq!(list("a", "b"), list("b", "a"));
327 }
328}