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}