Skip to main content

audiobox/sfx/
mod.rs

1//! A parametric sound-effect generator in the style of the [`synth`](crate::synth)
2//! module. Requires the `sfx` feature, which enables `synth`.
3//!
4//! An [`Effect`] is a small set of numbers: a waveform, a pitch with slides, vibrato
5//! and an arpeggio step, an envelope with punch, a duty sweep, swept low- and
6//! high-pass filters, a repeat period and a volume. It renders deterministically
7//! into mono [`Audio`], reads and writes a short text form, and can be drawn from a
8//! seed by a [`Preset`].
9//!
10//! ```
11//! use audiobox::sfx::{Effect, Preset};
12//! let coin: Effect = "wave=pulse duty=50 frequency=1046.5 sustain=0.04 punch=0.5
13//!                     decay=0.25 arpeggio=5 arpeggio-time=0.06".parse()?;
14//! let audio = coin.render(44_100)?;
15//! assert_eq!(audio.frames(), 12_789);
16//!
17//! // Try seeds until one sounds right, then keep its text.
18//! let jump = Effect::preset(Preset::Jump, 7);
19//! assert_eq!(jump.to_string().parse::<Effect>()?, jump);
20//! # Ok::<(), audiobox::Error>(())
21//! ```
22//!
23//! # Parameters and text form
24//!
25//! The text form is whitespace-separated `key=value` pairs, in any order, on one
26//! line or several; `;` starts a comment that runs to the end of the line. Missing
27//! keys keep their defaults. [`Effect`]'s `Display` writes the same form, and
28//! parsing it gives back an equal effect: numbers are written in Rust's shortest
29//! form that reads back to the same `f64`.
30//!
31//! | Key | Field | Unit | Range | Default |
32//! | --- | --- | --- | --- | --- |
33//! | `wave` | `waveform` | `pulse`, `triangle`, `saw`, `noise` | | `pulse` |
34//! | `duty` | `waveform` | pulse duty in percent | `12.5`, `25`, `50`, `75` | `50` |
35//! | `noise` | `waveform` | noise mode | `long`, `short` | `long` |
36//! | `frequency` | `frequency` | Hz | 10..=20000 | 440 |
37//! | `slide` | `slide` | octaves per second | -64..=64 | 0 |
38//! | `slide-change` | `slide_change` | octaves per second, per second | -256..=256 | 0 |
39//! | `vibrato-depth` | `vibrato_depth` | semitones | 0..=24 | 0 |
40//! | `vibrato-speed` | `vibrato_speed` | Hz | 0..=100 | 0 |
41//! | `attack` | `attack` | seconds | 0..=10 | 0 |
42//! | `sustain` | `sustain` | seconds | 0..=10 | 0.1 |
43//! | `punch` | `punch` | extra level at the start of the sustain | 0..=1 | 0 |
44//! | `decay` | `decay` | seconds | 0..=10 | 0.2 |
45//! | `duty-sweep` | `duty_sweep` | duty change per second (pulse only) | -10..=10 | 0 |
46//! | `low-pass` | `low_pass` | cutoff in Hz, 0 for off | 0 or 10..=20000 | 0 |
47//! | `low-pass-sweep` | `low_pass_sweep` | octaves per second | -16..=16 | 0 |
48//! | `high-pass` | `high_pass` | cutoff in Hz, 0 for off | 0 or 10..=20000 | 0 |
49//! | `high-pass-sweep` | `high_pass_sweep` | octaves per second | -16..=16 | 0 |
50//! | `arpeggio` | `arpeggio` | semitones | -48..=48 | 0 |
51//! | `arpeggio-time` | `arpeggio_time` | seconds | 0..=10 | 0 |
52//! | `repeat` | `repeat` | seconds, 0 for off | 0 or 0.01..=10 | 0 |
53//! | `volume` | `volume` | linear | 0..=1 | 0.5 |
54//!
55//! `attack + sustain + decay` is the effect's duration. It must be more than 0 and
56//! at most [`MAX_DURATION`] seconds. Values out of range, and `duty` or `duty-sweep`
57//! without a pulse or `noise` without noise, are rejected with
58//! [`ErrorCode::InvalidArgument`](crate::ErrorCode); in text, the error is a
59//! [`ParseError`] with the line and column of the value.
60//!
61//! # Sound
62//!
63//! At time `t` seconds since the start, or since the last repeat, the frequency is
64//! `frequency × 2^(slide·t + slide_change·t²/2)`, raised by `arpeggio` semitones once
65//! `t ≥ arpeggio_time`, and modulated by a vibrato of `vibrato_depth` semitones at
66//! `vibrato_speed` Hz. It is limited to 0..=sample_rate / 2. The pulse duty starts at
67//! `duty` and changes by `duty_sweep` per second, limited to 1..=99%. Noise uses the
68//! synthesizer's shift register, clocked at 93 times the frequency.
69//!
70//! `repeat` restarts the pitch, slides, arpeggio and duty every `repeat` seconds;
71//! the envelope, the vibrato and the filters continue.
72//!
73//! The envelope rises linearly from 0 to 1 over `attack`, then holds for `sustain`
74//! starting at `1 + punch` and falling linearly to 1, then falls linearly to 0 over
75//! `decay`. Each stage is rounded to whole samples, so an effect lasts
76//! `round(attack·rate) + round(sustain·rate) + round(decay·rate)` frames. The output
77//! is the filtered oscillator times the envelope times `volume`, so it peaks at
78//! `volume × (1 + punch)`.
79//!
80//! Both filters are 2-pole (12 dB per octave) Butterworth state-variable filters: the
81//! low-pass passes frequencies below its cutoff and the high-pass those above, and
82//! each attenuates its cutoff by 3 dB. A sweep moves a cutoff by that many octaves per
83//! second from the start of the effect, within 10 Hz..=0.45 × the sample rate.
84//!
85//! Like the synthesizer, rendering uses only basic IEEE arithmetic and the crate's own
86//! series for `2^x`, sine and tangent, so an effect gives the same samples on every
87//! platform.
88mod preset;
89mod text;
90
91pub use crate::synth::ParseError;
92pub use preset::Preset;
93
94use crate::math::{cos_turns, exp2, sin_turns};
95use crate::synth::{Aliasing, Duty, NoiseMode, Oscillator, Waveform};
96use crate::{Audio, Error, Limits, Result};
97
98/// The longest effect, in seconds.
99pub const MAX_DURATION: f64 = 10.0;
100
101/// The parameters of a sound effect. See the [module documentation](self) for their
102/// units, ranges and meaning.
103#[derive(Clone, Debug, PartialEq)]
104pub struct Effect {
105    pub waveform: Waveform,
106    pub frequency: f64,
107    pub slide: f64,
108    pub slide_change: f64,
109    pub vibrato_depth: f64,
110    pub vibrato_speed: f64,
111    pub attack: f64,
112    pub sustain: f64,
113    pub punch: f64,
114    pub decay: f64,
115    pub duty_sweep: f64,
116    pub low_pass: f64,
117    pub low_pass_sweep: f64,
118    pub high_pass: f64,
119    pub high_pass_sweep: f64,
120    pub arpeggio: f64,
121    pub arpeggio_time: f64,
122    pub repeat: f64,
123    pub volume: f64,
124}
125impl Default for Effect {
126    fn default() -> Self {
127        Self {
128            waveform: Waveform::Pulse(Duty::Half),
129            frequency: 440.0,
130            slide: 0.0,
131            slide_change: 0.0,
132            vibrato_depth: 0.0,
133            vibrato_speed: 0.0,
134            attack: 0.0,
135            sustain: 0.1,
136            punch: 0.0,
137            decay: 0.2,
138            duty_sweep: 0.0,
139            low_pass: 0.0,
140            low_pass_sweep: 0.0,
141            high_pass: 0.0,
142            high_pass_sweep: 0.0,
143            arpeggio: 0.0,
144            arpeggio_time: 0.0,
145            repeat: 0.0,
146            volume: 0.5,
147        }
148    }
149}
150
151/// A numeric parameter: its text key, accessors and range.
152pub(crate) struct Field {
153    pub key: &'static str,
154    pub get: fn(&Effect) -> f64,
155    pub set: fn(&mut Effect, f64),
156    pub min: f64,
157    pub max: f64,
158    /// Zero is allowed below `min`, meaning off.
159    pub zero_is_off: bool,
160    /// Written by `Display` even at its default.
161    pub always: bool,
162}
163impl Field {
164    pub fn check(&self, value: f64) -> Result<()> {
165        if value.is_finite()
166            && ((self.min..=self.max).contains(&value) || (self.zero_is_off && value == 0.0))
167        {
168            return Ok(());
169        }
170        let off = if self.zero_is_off { "0 or " } else { "" };
171        Err(Error::invalid(format!(
172            "{} must be {off}{}..={}, not {value}",
173            self.key, self.min, self.max
174        )))
175    }
176}
177
178macro_rules! fields {
179    ($($key:literal $field:ident $min:literal $max:literal $zero:literal $always:literal),* $(,)?) => {
180        pub(crate) const FIELDS: &[Field] = &[$(Field {
181            key: $key,
182            get: |e| e.$field,
183            set: |e, v| e.$field = v,
184            min: $min,
185            max: $max,
186            zero_is_off: $zero,
187            always: $always,
188        }),*];
189    };
190}
191fields! {
192    "frequency" frequency 10.0 20000.0 false true,
193    "slide" slide -64.0 64.0 false false,
194    "slide-change" slide_change -256.0 256.0 false false,
195    "vibrato-depth" vibrato_depth 0.0 24.0 false false,
196    "vibrato-speed" vibrato_speed 0.0 100.0 false false,
197    "attack" attack 0.0 10.0 false true,
198    "sustain" sustain 0.0 10.0 false true,
199    "punch" punch 0.0 1.0 false false,
200    "decay" decay 0.0 10.0 false true,
201    "duty-sweep" duty_sweep -10.0 10.0 false false,
202    "low-pass" low_pass 10.0 20000.0 true false,
203    "low-pass-sweep" low_pass_sweep -16.0 16.0 false false,
204    "high-pass" high_pass 10.0 20000.0 true false,
205    "high-pass-sweep" high_pass_sweep -16.0 16.0 false false,
206    "arpeggio" arpeggio -48.0 48.0 false false,
207    "arpeggio-time" arpeggio_time 0.0 10.0 false false,
208    "repeat" repeat 0.01 10.0 true false,
209    "volume" volume 0.0 1.0 false true,
210}
211
212impl Effect {
213    /// Draws an effect of the given kind from a seed. The same kind and seed always
214    /// give the same parameters, rounded to three decimals so the text stays short.
215    pub fn preset(kind: Preset, seed: u64) -> Self {
216        preset::generate(kind, seed)
217    }
218    /// Parses the text form described in the [module documentation](self).
219    pub fn parse(text: &str) -> std::result::Result<Self, ParseError> {
220        text::parse(text)
221    }
222    /// `attack + sustain + decay`, in seconds.
223    pub fn duration(&self) -> f64 {
224        self.attack + self.sustain + self.decay
225    }
226    /// Checks every parameter's range and the duration.
227    pub fn validate(&self) -> Result<()> {
228        for field in FIELDS {
229            field.check((field.get)(self))?;
230        }
231        if self.duty_sweep != 0.0 && !matches!(self.waveform, Waveform::Pulse(_)) {
232            return Err(Error::invalid("duty-sweep needs a pulse waveform"));
233        }
234        let duration = self.duration();
235        if !(duration > 0.0 && duration <= MAX_DURATION) {
236            return Err(Error::invalid(format!(
237                "attack + sustain + decay must be more than 0 and at most {MAX_DURATION} seconds, not {duration}"
238            )));
239        }
240        Ok(())
241    }
242    /// Renders with band-limited oscillators.
243    pub fn render(&self, sample_rate: u32) -> Result<Audio> {
244        self.render_with(sample_rate, Aliasing::BandLimited)
245    }
246    /// Renders the effect into mono audio.
247    pub fn render_with(&self, sample_rate: u32, aliasing: Aliasing) -> Result<Audio> {
248        self.validate()?;
249        Limits::default().check(0, 1, sample_rate)?;
250        let rate = sample_rate as f64;
251        let samples = |seconds: f64| (seconds * rate).round() as usize;
252        let (attack, sustain, decay) = (
253            samples(self.attack),
254            samples(self.sustain),
255            samples(self.decay),
256        );
257        let total = attack + sustain + decay;
258        Limits::default().check(total, 1, sample_rate)?;
259        let repeat = if self.repeat > 0.0 {
260            samples(self.repeat).max(1)
261        } else {
262            usize::MAX
263        };
264        let arpeggio_at = samples(self.arpeggio_time);
265        let mut oscillator = Oscillator::new(aliasing);
266        let mut low_pass = Svf::default();
267        let mut high_pass = Svf::default();
268        let mut out = Vec::with_capacity(total);
269        for n in 0..total {
270            let m = n % repeat;
271            let t = m as f64 / rate;
272            let mut octaves = self.slide * t + self.slide_change * t * t / 2.0;
273            if m >= arpeggio_at {
274                octaves += self.arpeggio / 12.0;
275            }
276            if self.vibrato_depth > 0.0 {
277                octaves +=
278                    self.vibrato_depth / 12.0 * sin_turns(self.vibrato_speed * n as f64 / rate);
279            }
280            let frequency = self.frequency * exp2(octaves);
281            let mut value = match self.waveform {
282                Waveform::Pulse(duty) => oscillator.next_pulse(
283                    duty.fraction() + self.duty_sweep * t,
284                    frequency,
285                    sample_rate,
286                ),
287                waveform => oscillator.next(waveform, frequency, sample_rate),
288            };
289            let seconds = n as f64 / rate;
290            if self.low_pass > 0.0 {
291                let g = Svf::coefficient(self.low_pass, self.low_pass_sweep, seconds, rate);
292                value = low_pass.process(value, g).0;
293            }
294            if self.high_pass > 0.0 {
295                let g = Svf::coefficient(self.high_pass, self.high_pass_sweep, seconds, rate);
296                value = high_pass.process(value, g).1;
297            }
298            let level = if n < attack {
299                n as f64 / attack as f64
300            } else if n < attack + sustain {
301                1.0 + self.punch * (1.0 - (n - attack) as f64 / sustain as f64)
302            } else {
303                1.0 - (n - attack - sustain) as f64 / decay as f64
304            };
305            out.push((value * level * self.volume) as f32);
306        }
307        Audio::new(vec![out], sample_rate)
308    }
309}
310
311/// Topology-preserving state-variable filter (Zavalishin), with Butterworth damping.
312#[derive(Clone, Copy, Debug, Default)]
313struct Svf {
314    ic1: f64,
315    ic2: f64,
316}
317impl Svf {
318    /// `tan(π·fc/fs)` for a cutoff swept by `sweep` octaves per second.
319    fn coefficient(cutoff: f64, sweep: f64, seconds: f64, rate: f64) -> f64 {
320        let fc = (cutoff * exp2(sweep * seconds)).clamp(10.0, 0.45 * rate);
321        let half_turns = fc / rate / 2.0;
322        sin_turns(half_turns) / cos_turns(half_turns)
323    }
324    /// Returns the low-pass and high-pass outputs.
325    fn process(&mut self, x: f64, g: f64) -> (f64, f64) {
326        const K: f64 = std::f64::consts::SQRT_2;
327        let a1 = 1.0 / (1.0 + g * (g + K));
328        let a2 = g * a1;
329        let a3 = g * a2;
330        let v3 = x - self.ic2;
331        let v1 = a1 * self.ic1 + a2 * v3;
332        let v2 = self.ic2 + a2 * self.ic1 + a3 * v3;
333        self.ic1 = 2.0 * v1 - self.ic1;
334        self.ic2 = 2.0 * v2 - self.ic2;
335        (v2, x - K * v1 - v2)
336    }
337}
338
339impl std::str::FromStr for Effect {
340    type Err = Error;
341    fn from_str(text: &str) -> Result<Self> {
342        Ok(Self::parse(text)?)
343    }
344}
345
346impl std::fmt::Display for Effect {
347    /// Writes one `key=value` per line: the waveform, the frequency, the envelope and
348    /// the volume, then every other parameter that differs from its default.
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        match self.waveform {
351            Waveform::Pulse(duty) => {
352                let duty = match duty {
353                    Duty::Eighth => "12.5",
354                    Duty::Quarter => "25",
355                    Duty::Half => "50",
356                    Duty::ThreeQuarters => "75",
357                };
358                writeln!(f, "wave=pulse duty={duty}")?;
359            }
360            Waveform::Triangle => writeln!(f, "wave=triangle")?,
361            Waveform::Sawtooth => writeln!(f, "wave=saw")?,
362            Waveform::Noise(NoiseMode::Long) => writeln!(f, "wave=noise noise=long")?,
363            Waveform::Noise(NoiseMode::Short) => writeln!(f, "wave=noise noise=short")?,
364        }
365        let default = Effect::default();
366        for field in FIELDS {
367            let value = (field.get)(self);
368            if field.always || value != (field.get)(&default) {
369                writeln!(f, "{}={value}", field.key)?;
370            }
371        }
372        Ok(())
373    }
374}