1use std::cell::Cell;
4use std::fmt;
5use std::rc::Rc;
6use std::sync::Arc;
7
8const RATE: u32 = 44_100;
9
10#[derive(Clone, Debug, PartialEq)]
15#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
16pub struct SoundClip {
17 samples: Arc<[f32]>,
18 rate: u32,
19}
20
21#[cfg_attr(feature = "live", ravenrs_live::expose)]
22impl SoundClip {
23 pub fn new(samples: impl Into<Arc<[f32]>>, rate: u32) -> Self {
25 Self { samples: samples.into(), rate: rate.max(1) }
26 }
27
28 pub fn click() -> Self {
30 let press = snap(0.030, 0.0016, 2_300.0, 0.55, 7);
31 let release = snap(0.024, 0.0012, 3_100.0, 0.32, 23);
32 let gap = (0.055 * RATE as f32) as usize;
33 let mut samples = vec![0.0; gap + release.len()];
34 for (sample, value) in samples.iter_mut().zip(&press) {
35 *sample += value;
36 }
37 for (sample, value) in samples[gap..].iter_mut().zip(&release) {
38 *sample += value;
39 }
40 Self::new(samples, RATE)
41 }
42
43 pub fn key() -> Self {
45 Self::new(snap(0.022, 0.0014, 1_700.0, 0.20, 41), RATE)
46 }
47
48 pub fn samples(&self) -> &[f32] {
50 &self.samples
51 }
52
53 pub fn rate(&self) -> u32 {
55 self.rate
56 }
57
58 pub fn seconds(&self) -> f32 {
60 self.samples.len() as f32 / self.rate as f32
61 }
62}
63
64fn snap(seconds: f32, fade: f32, tone: f32, volume: f32, seed: u32) -> Vec<f32> {
67 let mut noise = seed.wrapping_mul(2_654_435_761) | 1;
68 let length = (seconds * RATE as f32) as usize;
69 (0..length)
70 .map(|i| {
71 let t = i as f32 / RATE as f32;
72 noise ^= noise << 13;
74 noise ^= noise >> 17;
75 noise ^= noise << 5;
76 let hiss = noise as f32 / u32::MAX as f32 * 2.0 - 1.0;
77 let ring = (t * tone * std::f32::consts::TAU).sin();
78 let thump = (t * 620.0 * std::f32::consts::TAU).sin();
79 let sample = hiss * (-t / fade).exp() * 0.7
80 + ring * (-t / (fade * 2.0)).exp() * 0.5
81 + thump * (-t / (fade * 4.0)).exp() * 0.3;
82 sample * volume
83 })
84 .collect()
85}
86
87pub trait SoundProvider {
90 fn play(&self, clip: &SoundClip);
92 fn speak(&self, text: &str, language: &str);
95 fn stop_speaking(&self);
97 fn speak_clip(&self, clip: &SoundClip) {
100 self.play(clip);
101 }
102}
103
104#[derive(Debug, Default)]
106#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
107pub struct Silence;
108
109impl SoundProvider for Silence {
110 fn play(&self, _clip: &SoundClip) {}
111
112 fn speak(&self, _text: &str, _language: &str) {}
113
114 fn stop_speaking(&self) {}
115}
116
117#[derive(Clone)]
128#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
129pub struct Sound {
130 provider: Rc<dyn SoundProvider>,
131 turn: Rc<Cell<u64>>,
134}
135
136#[cfg_attr(feature = "live", ravenrs_live::expose)]
137impl Sound {
138 pub fn new(provider: impl SoundProvider + 'static) -> Self {
140 Self { provider: Rc::new(provider), turn: Rc::default() }
141 }
142
143 pub fn play(&self, clip: &SoundClip) {
145 self.provider.play(clip);
146 }
147
148 pub fn speak(&self, text: &str, language: &str) {
151 self.next_turn();
152 self.provider.speak(text, language);
153 }
154
155 pub fn speak_clip(&self, clip: &SoundClip) {
157 self.next_turn();
158 self.provider.speak_clip(clip);
159 }
160
161 pub fn stop_speaking(&self) {
163 self.next_turn();
164 self.provider.stop_speaking();
165 }
166
167 fn next_turn(&self) {
168 self.turn.set(self.turn.get() + 1);
169 }
170
171 pub(crate) fn turn(&self) -> u64 {
174 self.turn.get()
175 }
176}
177
178impl Default for Sound {
179 fn default() -> Self {
180 Self::new(Silence)
181 }
182}
183
184impl fmt::Debug for Sound {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 f.write_str("Sound")
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::SoundClip;
193
194 fn peak(clip: &SoundClip) -> f32 {
195 clip.samples().iter().fold(0.0, |peak: f32, sample| peak.max(sample.abs()))
196 }
197
198 #[test]
199 fn a_click_is_short_audible_and_does_not_clip() {
200 let click = SoundClip::click();
201 assert!(click.seconds() > 0.05 && click.seconds() < 0.15, "{}", click.seconds());
202 assert!(peak(&click) > 0.2 && peak(&click) <= 1.0, "{}", peak(&click));
203 assert_eq!(click, SoundClip::click(), "the same on every run");
204 }
205
206 #[test]
207 fn a_key_is_softer_than_a_click() {
208 assert!(peak(&SoundClip::key()) < peak(&SoundClip::click()));
209 assert!(peak(&SoundClip::key()) > 0.05);
210 }
211}