Skip to main content

ravenrs_widgets/
sound.rs

1//! Sound and a spoken voice, as a service the platform layer provides.
2
3use std::cell::Cell;
4use std::fmt;
5use std::rc::Rc;
6use std::sync::Arc;
7
8const RATE: u32 = 44_100;
9
10/// A short sound in memory: one channel, ready to be played many times. Cloning
11/// shares the samples.
12///
13/// The ready-made ones are computed, so they add nothing to an app's size.
14#[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    /// A sound from its samples, between -1 and 1, at `rate` a second.
24    pub fn new(samples: impl Into<Arc<[f32]>>, rate: u32) -> Self {
25        Self { samples: samples.into(), rate: rate.max(1) }
26    }
27
28    /// The press and release of a mouse button.
29    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    /// A key being pressed: softer than a click, since many come in a row.
44    pub fn key() -> Self {
45        Self::new(snap(0.022, 0.0014, 1_700.0, 0.20, 41), RATE)
46    }
47
48    /// Its samples.
49    pub fn samples(&self) -> &[f32] {
50        &self.samples
51    }
52
53    /// How many samples make a second.
54    pub fn rate(&self) -> u32 {
55        self.rate
56    }
57
58    /// How long it lasts, in seconds.
59    pub fn seconds(&self) -> f32 {
60        self.samples.len() as f32 / self.rate as f32
61    }
62}
63
64/// A burst of noise over a short tone, both dying out fast, the way plastic
65/// sounds when struck.
66fn 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            // Xorshift: a noise that is the same on every run and every machine.
73            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
87/// Plays sounds and speaks. Platform layers install one with
88/// [`crate::Ui::set_sound`]; until then, nothing is heard.
89pub trait SoundProvider {
90    /// Starts `clip`, over whatever is already playing.
91    fn play(&self, clip: &SoundClip);
92    /// Says `text` with the system's voice for `language`, such as `pt-BR`, in
93    /// place of anything still being said.
94    fn speak(&self, text: &str, language: &str);
95    /// Stops what is being said.
96    fn stop_speaking(&self);
97    /// Says a recording, in place of anything still being said, so that
98    /// [`SoundProvider::stop_speaking`] stops it. By default it only plays.
99    fn speak_clip(&self, clip: &SoundClip) {
100        self.play(clip);
101    }
102}
103
104/// A provider that makes no sound (tests, headless use).
105#[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/// A shared handle to the sound system, from [`crate::BuildCx::sound`] or
118/// [`crate::Ui::sound`].
119///
120/// ```
121/// use ravenrs_widgets::{Silence, Sound, SoundClip};
122///
123/// let sound = Sound::new(Silence);
124/// sound.play(&SoundClip::click());
125/// sound.speak("Clique em Salvar.", "pt-BR");
126/// ```
127#[derive(Clone)]
128#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
129pub struct Sound {
130    provider: Rc<dyn SoundProvider>,
131    /// Goes up each time something takes the place of what was being said, since a provider
132    /// does not report it, and whoever started it needs to know.
133    turn: Rc<Cell<u64>>,
134}
135
136#[cfg_attr(feature = "live", ravenrs_live::expose)]
137impl Sound {
138    /// Wraps a provider.
139    pub fn new(provider: impl SoundProvider + 'static) -> Self {
140        Self { provider: Rc::new(provider), turn: Rc::default() }
141    }
142
143    /// Starts `clip`, over whatever is already playing.
144    pub fn play(&self, clip: &SoundClip) {
145        self.provider.play(clip);
146    }
147
148    /// Says `text` with the system's voice for `language`, in place of anything
149    /// still being said.
150    pub fn speak(&self, text: &str, language: &str) {
151        self.next_turn();
152        self.provider.speak(text, language);
153    }
154
155    /// Says a recording, in place of anything still being said.
156    pub fn speak_clip(&self, clip: &SoundClip) {
157        self.next_turn();
158        self.provider.speak_clip(clip);
159    }
160
161    /// Stops what is being said.
162    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    /// Which saying is the current one: a number that changes when another starts or this
172    /// one is stopped.
173    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}