Skip to main content

audio_code/audio/
synth.rs

1//! A tiny synthesizer for designing clicks, beeps and effects in code.
2
3use std::f32::consts::TAU;
4
5use super::SoundData;
6
7const RATE: u32 = 44_100;
8
9/// Basic wave shapes.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Wave {
12    /// Smooth and pure.
13    Sine,
14    /// Hollow, retro.
15    Square,
16    /// Soft, between sine and square.
17    Triangle,
18    /// Bright and buzzy.
19    Saw,
20    /// White noise: clicks, steps, hiss, explosions.
21    Noise,
22}
23
24/// Builds a sound from waves and envelopes. Pass it to
25/// [`Audio::register`](super::Audio::register) or
26/// [`Ui::set_sound_data`](crate::ui::Ui::set_sound_data), or call [`Synth::build`].
27///
28/// Settings like `volume` and `decay` apply to the part they are called on; parts added
29/// with [`Synth::then`] and [`Synth::with`] keep their own settings.
30///
31/// ```
32/// use audio_code::audio::Synth;
33///
34/// // A short, soft click.
35/// let click = Synth::sine(1500.0).length(0.03).decay(120.0).volume(0.4).build();
36///
37/// // A rising "pling" made of two notes.
38/// let pling = Synth::triangle(660.0).length(0.06)
39///     .then(Synth::triangle(990.0).length(0.12).decay(25.0))
40///     .build();
41///
42/// // A noisy thump: noise layered with a low sine sweep.
43/// let thump = Synth::sine(150.0).sweep_to(50.0).length(0.15).decay(30.0)
44///     .with(Synth::noise().length(0.05).decay(80.0).volume(0.3))
45///     .build();
46/// ```
47#[derive(Debug, Clone)]
48pub struct Synth {
49    wave: Wave,
50    from: f32,
51    to: f32,
52    length: f32,
53    attack: f32,
54    decay: f32,
55    release: f32,
56    volume: f32,
57    layers: Vec<Synth>,
58    next: Option<Box<Synth>>,
59}
60
61impl Synth {
62    /// A synth with the given wave and frequency, 0.1 seconds long.
63    pub fn new(wave: Wave, frequency: f32) -> Self {
64        Self {
65            wave,
66            from: frequency,
67            to: frequency,
68            length: 0.1,
69            attack: 0.002,
70            decay: 0.0,
71            release: 0.005,
72            volume: 0.5,
73            layers: Vec::new(),
74            next: None,
75        }
76    }
77
78    /// Sine wave.
79    pub fn sine(frequency: f32) -> Self {
80        Self::new(Wave::Sine, frequency)
81    }
82
83    /// Square wave.
84    pub fn square(frequency: f32) -> Self {
85        Self::new(Wave::Square, frequency)
86    }
87
88    /// Triangle wave.
89    pub fn triangle(frequency: f32) -> Self {
90        Self::new(Wave::Triangle, frequency)
91    }
92
93    /// Sawtooth wave.
94    pub fn saw(frequency: f32) -> Self {
95        Self::new(Wave::Saw, frequency)
96    }
97
98    /// White noise.
99    pub fn noise() -> Self {
100        Self::new(Wave::Noise, 0.0)
101    }
102
103    /// Silence, useful as a pause between parts: `a.then(Synth::silence(0.1)).then(b)`.
104    pub fn silence(seconds: f32) -> Self {
105        Self::sine(0.0).volume(0.0).length(seconds)
106    }
107
108    /// Length in seconds.
109    pub fn length(mut self, seconds: f32) -> Self {
110        self.length = seconds.max(0.0);
111        self
112    }
113
114    /// Glide the frequency linearly to `frequency` (in hertz) over the whole length.
115    pub fn sweep_to(mut self, frequency: f32) -> Self {
116        self.to = frequency;
117        self
118    }
119
120    /// Fade-in time in seconds (default 2 ms).
121    pub fn attack(mut self, seconds: f32) -> Self {
122        self.attack = seconds.max(0.0);
123        self
124    }
125
126    /// Exponential fade out speed: the volume is multiplied by `e^(-rate * seconds)`.
127    /// 0 = none (default), 10 rings for about 0.7 s, 30 is a pluck (0.2 s), 120 a click
128    /// (60 ms), 300 a tick (20 ms).
129    pub fn decay(mut self, rate: f32) -> Self {
130        self.decay = rate.max(0.0);
131        self
132    }
133
134    /// Linear fade-out at the end in seconds (default 5 ms, prevents clicks).
135    pub fn release(mut self, seconds: f32) -> Self {
136        self.release = seconds.max(0.0);
137        self
138    }
139
140    /// Linear loudness, 0.0 (silent) to 1.0 (full scale), default 0.5.
141    pub fn volume(mut self, volume: f32) -> Self {
142        self.volume = volume;
143        self
144    }
145
146    /// Plays `other` at the same time (mixed in).
147    pub fn with(mut self, other: Synth) -> Self {
148        self.layers.push(other);
149        self
150    }
151
152    /// Plays `other` after this one.
153    pub fn then(mut self, other: Synth) -> Self {
154        let next = match self.next.take() {
155            Some(next) => next.then(other),
156            None => other,
157        };
158        self.next = Some(Box::new(next));
159        self
160    }
161
162    fn render(&self) -> Vec<f32> {
163        let frames = (self.length * RATE as f32) as usize;
164        let mut out = Vec::with_capacity(frames);
165        let mut phase = 0.0f32;
166        for i in 0..frames {
167            let t = i as f32 / RATE as f32;
168            let progress = if frames > 1 { i as f32 / (frames - 1) as f32 } else { 0.0 };
169            let freq = self.from + (self.to - self.from) * progress;
170            phase = (phase + freq / RATE as f32).fract();
171            let raw = match self.wave {
172                Wave::Sine => (phase * TAU).sin(),
173                Wave::Square => {
174                    if phase < 0.5 {
175                        1.0
176                    } else {
177                        -1.0
178                    }
179                }
180                Wave::Triangle => 1.0 - 4.0 * (phase - 0.5).abs(),
181                Wave::Saw => 2.0 * phase - 1.0,
182                Wave::Noise => fastrand::f32() * 2.0 - 1.0,
183            };
184            let mut env = 1.0;
185            if self.attack > 0.0 {
186                env *= (t / self.attack).min(1.0);
187            }
188            if self.decay > 0.0 {
189                env *= (-t * self.decay).exp();
190            }
191            // Time until the last sample, so the release reaches exactly zero.
192            let left = (frames - 1 - i) as f32 / RATE as f32;
193            if self.release > 0.0 && left < self.release {
194                env *= (left / self.release).max(0.0);
195            }
196            out.push(raw * env * self.volume);
197        }
198        for layer in &self.layers {
199            let other = layer.render();
200            if other.len() > out.len() {
201                out.resize(other.len(), 0.0);
202            }
203            for (o, s) in out.iter_mut().zip(other) {
204                *o += s;
205            }
206        }
207        if let Some(next) = &self.next {
208            out.extend(next.render());
209        }
210        out
211    }
212
213    /// Renders the sound.
214    pub fn build(&self) -> SoundData {
215        SoundData::from_samples(self.render(), 1, RATE)
216    }
217}
218
219impl From<Synth> for SoundData {
220    fn from(synth: Synth) -> Self {
221        synth.build()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn lengths_add_up() {
231        let s = Synth::sine(440.0).length(0.1).then(Synth::noise().length(0.05)).then(Synth::saw(100.0).length(0.05));
232        assert!((s.build().duration() - 0.2).abs() < 0.001);
233        let layered = Synth::sine(440.0).length(0.1).with(Synth::sine(880.0).length(0.3));
234        assert!((layered.build().duration() - 0.3).abs() < 0.001);
235    }
236
237    #[test]
238    fn stays_in_range_and_fades() {
239        let data = Synth::square(300.0).volume(1.0).length(0.05).build();
240        assert!(data.samples.iter().all(|s| s.abs() <= 1.0));
241        assert!(data.samples.last().unwrap().abs() < 1e-3);
242        assert!(data.samples[0].abs() < 1e-3);
243    }
244}