Skip to main content

audiobox/synth/
mod.rs

1//! An 8-bit style synthesizer that renders text scores. Requires the `synth` feature.
2//!
3//! A [`Score`] holds up to eight channels. Legacy channels play one note at a time with an
4//! [`Instrument`]: a pulse, triangle, sawtooth or noise [`Waveform`], an ADSR
5//! [`Envelope`], a volume, and slide, vibrato, arpeggio and duty-sweep effects.
6//! Render it offline with [`Score::render`], or chunk by chunk with [`Synth`].
7//! [`Instrument::drum`] selects finite synthesized percussion in two [`DrumKit`]s.
8//! [`Instrument::synthwave`] adds bounded polyphony, unison, stereo spread, resonant
9//! filters, LFOs and glide. [`Synth::new_stereo`] / [`Score::render_stereo`] preserve
10//! stereo; existing constructors remain mono and legacy instruments retain their bits.
11//!
12//! For prepared fractional/changing tempo, [`TempoMap`] converts absolute ticks
13//! once with checked rational nearest-frame rounding. [`FrameScore`] / [`FrameNote`]
14//! use those positions through [`Synth::from_frames`] or its stereo counterpart.
15//! This opt-in path keeps the Score syntax and its integer-tempo PCM unchanged.
16//! Beat LFOs integrate the scheduled tempo clock without phase resets; tick macros
17//! follow that clock relative to the note's start. Repeats/sections are expanded
18//! by the caller, and a tie is one extended note. Multiple bounded sources align
19//! through the existing mixer; no unrestricted Score channel expansion is needed.
20//! [`Synth::prepare_controls`] adds opt-in, bounded source-frame value ramps on
21//! supported parameters while retaining held-note DSP state. [`ControlParameter`]
22//! defines capabilities/units; [`ControlTarget`] selects track/instrument or note.
23//! Structural edits still belong in prepared instruments at note boundaries.
24//!
25//! ```
26//! use audiobox::synth::Score;
27//! let score: Score = "
28//!     tempo 150
29//!     instrument lead wave=pulse duty=25 adsr=5,60,70,40 vibrato=12,160,200
30//!     instrument bass wave=triangle
31//!     channel melody = @lead o5 l8 L [c e g >c< g e]2 c2
32//!     channel bass   = @bass o3 l2 c g c1
33//! ".parse()?;
34//! let audio = score.render(44_100)?;
35//! assert_eq!(audio.channels(), 1);
36//! # Ok::<(), audiobox::Error>(())
37//! ```
38//!
39//! # Score format
40//!
41//! A score is text, one directive per line. `;` starts a comment that runs to the
42//! end of the line. Blank lines are ignored.
43//!
44//! | Directive | Meaning |
45//! | --- | --- |
46//! | `tempo 150` | Quarter notes per minute, 1..=999. Default 120. Once per score. |
47//! | `ticks 48` | Time base: ticks per quarter note, 1..=960. Default 48. Once per score. |
48//! | `instrument NAME key=value ...` | Defines an instrument. Keys are listed below. |
49//! | `pattern NAME = ...` | Defines a named pattern of commands. |
50//! | `channel NAME = ...` | Appends commands to a channel. Channels are created in order of first appearance. |
51//!
52//! Names use letters, digits, `_` and `-`. Repeating `pattern NAME =` or
53//! `channel NAME =` continues the same pattern or channel, so long parts can
54//! span several lines; a repeat may also open on one line and close on another.
55//!
56//! ## Instrument keys
57//!
58//! | Key | Value | Default |
59//! | --- | --- | --- |
60//! | `wave` | `pulse`, `triangle`, `saw` or `noise` | `pulse` |
61//! | `duty` | `12.5`, `25`, `50` or `75`; a comma list starts a duty sweep from the first | `50` |
62//! | `duty-step` | Milliseconds between duty-sweep steps; the last duty holds | `16` |
63//! | `noise` | `long` (hiss) or `short` (metallic) | `long` |
64//! | `adsr` | `attack,decay,sustain,release`: milliseconds, milliseconds, percent, milliseconds | `0,0,100,0` |
65//! | `volume` | 0..=15 | `15` |
66//! | `slide` | Pitch slide from each note's start, in cents per second (signed) | `0` |
67//! | `vibrato` | `depth,period[,delay]`: cents (≤ 1200), milliseconds, milliseconds | none |
68//! | `arpeggio` | Up to 16 semitone offsets, cycled from each note's start, such as `0,4,7` | none |
69//! | `arpeggio-step` | Milliseconds per arpeggio step | `16` |
70//! | `drum` | `kick`, `snare`, `closed-hat`, `open-hat`, `tom`, `clap`, `crash` | none |
71//! | `kit` | `chiptune` or `electronic`; requires `drum` | `electronic` |
72//! | `drum-pitch` | Base Hz at C4, 20..=20000 | preset |
73//! | `drum-tone` / `drum-noise` | Independent finite envelope lengths, 0..=10000 ms | preset |
74//! | `drum-sweep` | Initial cents above the base (0..=4800), sweep milliseconds (0..=1000) | preset |
75//! | `drum-mix` | Noise percentage, 0..=100; tone uses the complement | preset |
76//! | `drum-click` | Separate 3 ms noise transient level, 0..=100 percent | preset |
77//! | `drum-seed` | Fixed noise seed, 0..=4294967295 | `1` |
78//!
79//! Drums allow `volume`, `kit` and `drum-*` keys; melodic waveform, ADSR and modulation
80//! keys are rejected. Notes transpose pitched components relative to C4. Chiptune
81//! kick/tom/snare use triangle tones and clocked 15-bit noise; electronic tones use
82//! sines and filtered noise, with a six-oscillator metal bank for hats/cymbals.
83//! These are synthetic characters, not emulations of a particular console.
84//!
85//! ## Tracker instruments (opt-in)
86//!
87//! [`Instrument::chip`] selects a [`ChipInstrument`]. It is `None` by default, so
88//! old scores retain their sample bits. All new keys opt in, even `quantize=none`.
89//! New note triggers reset chip source phase, noise seed, PCM position and macro
90//! age. Ties continue them; a note carried across `L` restarts them on the next lap.
91//! Source clocks advance at zero gain. Gate/ADSR still apply to every chip source.
92//!
93//! | Key | Value | Default |
94//! | --- | --- | --- |
95//! | `wavetable` | 2..=256 hex nibbles, one unsigned four-bit value each | none |
96//! | `sample` | 1..=65536 hex byte pairs, one unsigned PCM value each | none |
97//! | `sample-bits` / `sample-rate` | PCM depth 2..=8 / original rate 100..=32000 Hz | `4` / `8000` |
98//! | `sample-root` / `sample-loop` | PCM root MIDI note 0..=127 / `on` or `off` | `60` / `off` |
99//! | `noise-period` | NTSC noise period-table index 0..=15; requires `wave=noise` | none |
100//! | `macro-volume` | 1..=64 comma-separated values, 0..=15 | none |
101//! | `macro-duty` | 0..=3 selects 12.5/25/50/75%; pulse only | none |
102//! | `macro-arpeggio` | Relative offsets -48..=48 semitones | none |
103//! | `macro-pitch` | Relative offsets -1200..=1200 cents | none |
104//! | `macro-LANE-step` | `frames,COUNT,HZ` (both 1..=1000) or `ticks,COUNT` (1..=960) | `frames,1,60` |
105//! | `macro-LANE-loop` | Zero-based start index, or `hold` | `hold` |
106//! | `quantize` | `none`, `volume`, `ntsc-pulse`, `ntsc-triangle` | `none` |
107//!
108//! Macro lane names are `volume`, `duty`, `arpeggio` and `pitch`. Each lane has its
109//! own interval. Index zero starts at the note's first sample. Frame clocks are
110//! explicit simulation clocks, independent of callbacks; tick clocks follow score
111//! tempo/TPQ. Steps occur at the first sample at or after the exact rational time,
112//! with no cumulative rounding; note-relative tick steps can be one sample later
113//! than globally floor-rounded score note positions. A looping lane plays its
114//! prefix once, then repeats from its loop index. A held lane keeps its last value
115//! through release. Legacy `arpeggio`/duty sweeps cannot duplicate the same lane.
116//!
117//! Wavetables map 0..15 to -1..+1 and loop once per note period; PCM maps its bit
118//! range to -1..+1 and advances at `sample-rate * note-Hz / root-Hz`. PCM requires
119//! one byte pair per value even at four bits (`000f`, not `0f`); values exceeding
120//! the selected depth are errors. Non-looping PCM stops early at its data end and
121//! clears the envelope. Whole-sample loops continue until gate/release ends. These
122//! sources use sample-and-hold and 32.32 phase steps, with no anti-aliasing. Wave
123//! frequency is limited to Nyquist; PCM playback ratios preserve relative pitch.
124//! Extreme PCM increments saturate (one-shot) or wrap (looping) without overflow.
125//! Custom sources are mutually exclusive; PCM/table sources reject wave/duty/noise
126//! keys. Sample controls without data and macro controls without values are errors.
127//!
128//! Period noise bypasses note pitch and the legacy frequency*93 noise clock. It uses
129//! [`NTSC_CLOCK`] / [`NTSC_NOISE_PERIODS`], with the same seeded 15-bit long (32767)
130//! or short (93) register. Pitch modulation is rejected for fixed noise. In the
131//! band-limited mode register output is averaged within each audio sample.
132//! `quantize=volume` rounds the combined ADSR, instrument/channel volume, velocity
133//! and volume-macro gain to one of 16 levels before the master gain. NTSC models
134//! additionally round the final pitch to an 11-bit timer: pulse divider 16 and timer
135//! 8..=2047, triangle divider 32 and timer 0..=2047. Frequencies outside these ranges
136//! clamp to the nearest timer. Each model requires the matching waveform. These
137//! are period models, not cycle-accurate hardware or a model of hardware mute rules.
138//! See [NESdev noise](https://www.nesdev.org/wiki/APU_Noise),
139//! [pulse](https://www.nesdev.org/wiki/APU_Pulse) and
140//! [triangle](https://www.nesdev.org/wiki/APU_Triangle) for the hardware tables.
141//!
142//! ### Per-note wavetable voices (opt-in)
143//!
144//! A wavetable instrument normally plays every note of its channel on one voice,
145//! like tracker hardware: a new note restarts the table from its first value at
146//! whatever level the envelope had reached, and replaces the previous note's
147//! release. Adding `voices=N` (1..=16) gives the channel a pool of `N` voices for
148//! that instrument instead, using the synthwave allocator described below:
149//!
150//! - Each note starts on a free voice: the table from its first value (phase zero)
151//!   and the ADSR from silence, at the note's own velocity. The previous note keeps
152//!   its voice and plays its release independently, to its end.
153//! - Without a free voice, the oldest releasing voice is taken, then the oldest
154//!   held one. Its note is cut and the new note attacks from silence, which can
155//!   click.
156//! - Note-off, gates, ties, velocity, macros, quantization and loop carries apply
157//!   per voice as on the single voice. Chords (`{c e g}`) are allowed up to `N`
158//!   notes. Switching to an instrument without voices releases the held pooled
159//!   notes and lets their tails finish.
160//! - A note that starts while nothing else sounds renders exactly as on the
161//!   single voice, so only overlapping notes change.
162//!
163//! Only wavetable sources take `voices`, and no other synthwave setting:
164//! other synthwave keys, the waveform source with macros, PCM and period noise
165//! are rejected with a diagnostic. In [`Instrument`] terms, set the wavetable
166//! [`ChipInstrument`] and `synthwave: Some(Synthwave { voices, ..Default::default() })`.
167//! This is per-note articulation for keyboard-like parts, not a model of an
168//! acoustic instrument. Without `voices`, rendering is unchanged.
169//!
170//! ```
171//! use audiobox::synth::Score;
172//! let score: Score = "
173//! instrument table wavetable=0123456789abcdefedcba98765432100 macro-arpeggio=0,7,12 macro-arpeggio-step=ticks,6 macro-arpeggio-loop=0
174//! instrument pulse wave=pulse quantize=ntsc-pulse macro-duty=0,1,2,3 macro-duty-loop=0
175//! instrument noise wave=noise noise=short noise-period=6 macro-volume=15,8,2,0
176//! instrument pcm sample=00040a0f080300 sample-rate=4000 sample-loop=on sample-bits=4
177//! channel a=@table o5 c4
178//! channel b=@pulse o3 c4
179//! channel c=@noise c4
180//! channel d=@pcm o3 c4
181//! ".parse()?;
182//! let audio = score.render(48_000)?;
183//! # Ok::<(), audiobox::Error>(())
184//! ```
185//!
186//! ## Synthwave instruments (opt-in)
187//!
188//! Any synthwave key, or `synthwave=on`, enables [`Synthwave`]. Sources are saw,
189//! pulse/square and triangle, always band-limited, independently of legacy aliasing.
190//! Use `unison` (1..=8), `detune` (0..=100 cents), `spread` (0..=100 percent),
191//! `pan` (-100..=100 percent), `voices` (1..=16, default 8), `glide` (0..=60000 ms)
192//! and `legato=on|off`. Glide/legato require `voices=1`.
193//!
194//! `filter=lowpass|highpass|bandpass` selects a per-note [`VoiceFilter`]; `cutoff`
195//! is Hz, `resonance` is Q*100, `keytrack` is percent relative to C4, `filter-adsr`
196//! is a separate ADSR, and `filter-env` its peak cutoff offset in cents. See the
197//! type's fields for bounds/defaults. Modulated cutoff clamps to 1..=0.45*sample-rate
198//! Hz. LP/HP slopes are 12 dB/octave; band-pass peak is normalized to 1. Resonance
199//! may exceed full scale; the amplitude ADSR follows the filter to bound its tail.
200//!
201//! `lfo-pitch`, `lfo-cutoff`, `lfo-amplitude` and `lfo-pan` accept `millihertz,depth`
202//! or `beats,numerator,denominator,depth`. [`Lfo`] documents units/bounds. These sine
203//! LFOs follow total output frames (including loops) and reset only with the synth.
204//! Chip/drum sources cannot combine with synthwave, except that a wavetable takes
205//! `voices` alone (see per-note wavetable voices above). Each note normalizes its unison
206//! oscillators by count and uses fixed initial phases and linear stereo panning.
207//!
208//! Chords use `{c e g}4!90`, sharing length/gate/velocity. Only notes, accidentals
209//! and local `>`/`<` shifts are allowed inside braces. Members must be distinct,
210//! at most 16 and within the instrument's voice limit; each counts against the
211//! score's note bound. `^` extends the entire group; `&` joins single notes only.
212//! The typed equivalent is [`ChannelWriter::chord`].
213//!
214//! Idle slots are reused first, then oldest releasing, then oldest held voices,
215//! ties by slot index. Stealing is immediate and can click. Lowering the limit on an
216//! instrument change retires higher slots immediately. Legacy/synthwave switches
217//! release the other path's held notes. Chord carries retrigger every held member
218//! at the loop point. Mono legato preserves phase, filter state and both envelopes
219//! across adjacent full-gate notes of the same instrument. Gaps/short gates retrigger;
220//! glide moves between active-note pitches in cents over its prepared duration.
221//!
222//! ```
223//! use audiobox::synth::Score;
224//! let score = Score::parse("tempo 110
225//! instrument bass wave=saw voices=1 glide=80 legato=on filter=lowpass cutoff=500
226//! instrument pad wave=saw unison=5 detune=18 spread=90 adsr=120,150,70,300
227//! instrument lead voices=1 glide=60 legato=on lfo-pitch=5000,12
228//! channel bass=@bass o2 l8 [c c >c< g]2
229//! channel pad=@pad {c e g}1
230//! channel lead=@lead o5 l8 [c e g >c<]2")?;
231//! let stereo = score.render_stereo(48_000)?;
232//! assert_eq!(stereo.channels(), 2);
233//! # Ok::<(), audiobox::Error>(())
234//! ```
235//!
236//! ## Commands
237//!
238//! Commands are written one after another; spaces between them are optional.
239//!
240//! | Command | Meaning |
241//! | --- | --- |
242//! | `c d e f g a b` | A note. Follow with `+` or `#` for sharp, `-` for flat, then an optional length. |
243//! | `c16!96` | Per-note velocity 0..=127, independently of `v`. Omitted velocity is 127. Suffix follows the length/dots. |
244//! | `r` | A rest, with an optional length. |
245//! | length | `4` is a quarter, `8` an eighth, `3` a half-note triplet; dots add half: `4.`, `8..`. `%12` is 12 ticks. Without a number, the default length is used: `c.` dots it. |
246//! | `^8` | Lengthens the previous note or rest. `c4^8` lasts a quarter and an eighth. |
247//! | `&` | Ties two notes of the same pitch: `c4&c8`. |
248//! | `o4` | Sets the octave, 0..=9. `o4 c` is middle C, `o4 a` is 440 Hz. Default 4. |
249//! | `>` `<` | One octave up, one octave down. |
250//! | `l8` | Default length, possibly dotted. Default `l4`. |
251//! | `v12` | Channel volume, 0..=15. Default 15. |
252//! | `q6` | Gate: the following notes sound for 6/8 of their length, then release. 1..=8, default 8. |
253//! | `@NAME` | Selects an instrument. A channel needs one before its first note. |
254//! | `[ ... ]3` | Repeats 3 times (default 2). In `[c d \| e]3`, the part after `\|` is skipped on the last time. |
255//! | `$NAME` | Plays a pattern. Its octave, length, volume and instrument changes carry on after it. |
256//! | `L` | The song's loop point. Channels that mark one must mark the same tick. |
257//!
258//! A length must be a whole number of ticks: at the default 48 ticks per quarter,
259//! `64.` is rejected. Raise `ticks` for finer lengths.
260//! Velocity is not persistent: `c!64 d` gives velocities 64 and 127. A tied
261//! continuation cannot set velocity; it retains the first attack's velocity.
262//!
263//! The song's length is that of its longest channel, including trailing rests. When
264//! it ends, every channel continues from the loop point, or from the start if there
265//! is none. A note that sounds across the loop point restarts there.
266//!
267//! ## Errors and limits
268//!
269//! [`Score::parse`] reports a [`ParseError`] with the line, the column and an
270//! [`ErrorCode`](crate::ErrorCode): `Syntax` for malformed text, `InvalidArgument` for
271//! values out of range, and `LimitExceeded` when the text, the nesting of repeats and
272//! patterns, the number of channels, notes or expanded commands, or the song's duration
273//! exceed its [`ScoreLimits`].
274//!
275//! # Sound
276//!
277//! Notes are equal-tempered with A4 at 440 Hz. Each channel's output is its oscillator
278//! times its envelope, its instrument volume / 15 and its channel volume / 15. The
279//! channels are summed and scaled by [`RenderOptions::gain`]. Oscillator phase
280//! continues across notes, and each attack starts from the channel's current level,
281//! so note changes do not click unless the attack is 0 ms.
282//!
283//! The noise channel's 15-bit shift register is clocked at 93 times the note's
284//! frequency, so in the short mode, whose sequence repeats every 93 steps, it sounds
285//! at the note's pitch. In the long mode, higher notes give brighter hiss.
286//!
287//! Drums are one-shots with finite squared decay and a 1 ms attack (the 3 ms click
288//! has a one-sample attack). Snare tone and noise have separate envelopes; a clap
289//! has bursts at 0, 10 and 20 ms and a final noise tail at 30 ms. Drum gates/ties
290//! change score timing but do not stop the natural tail. A new note on that channel
291//! replaces the previous sound, so a closed hat cuts an open one on the same channel.
292//! Use separate channels for overlapping parts. Each trigger resets phase, filter
293//! memory and the configured seed, giving identical hits at equal pitch/velocity.
294//! Drums are not restarted by a loop carry without an actual written onset.
295//! Their final tails are included in [`Synth::max_frames`]. A one-frame envelope
296//! can be silent at low rates; presets use longer times. Metallic components may
297//! alias and are not subject to the melodic oscillator's band-limiting guarantee.
298//!
299//! ## Aliasing
300//!
301//! [`Aliasing::BandLimited`], the default, corrects the pulse and sawtooth steps with
302//! PolyBLEP and the triangle's corners with PolyBLAMP, and averages noise over each
303//! sample. This removes most aliasing, though not all at very high notes.
304//! [`Aliasing::Naive`] samples the raw waveforms for a harsher, period sound.
305//!
306//! ## Determinism
307//!
308//! The same score, sample rate and options give the same samples on every platform.
309//! Pitch, vibrato and envelopes use only addition, subtraction, multiplication,
310//! division and rounding, which IEEE 754 defines exactly; `sin`, `exp` and `powf` are
311//! replaced by the crate's own series. Times in milliseconds are rounded to whole
312//! samples, and note positions are computed from ticks with integer arithmetic.
313mod chip;
314mod controls;
315pub use chip::{
316    ChipInstrument, ChipSample, ChipSource, MAX_MACRO_VALUES, MAX_SAMPLE_VALUES, Macro, MacroStep,
317    NTSC_CLOCK, NTSC_NOISE_PERIODS, PitchTable, Wavetable, noise_clock,
318};
319pub use controls::{
320    ControlCurve, ControlEvent, ControlId, ControlParameter, ControlReport, ControlStatus,
321    ControlTarget, MAX_SYNTH_CONTROLS,
322};
323mod drum;
324pub use drum::{Drum, DrumKind, DrumKit};
325mod parse;
326mod prepared;
327pub use prepared::{FrameNote, FrameScore, MAX_TEMPO_CHANGES, TempoChange, TempoMap};
328mod render;
329mod score;
330mod synthwave;
331mod wave;
332pub use synthwave::{FilterMode, Lfo, LfoRate, Synthwave, VoiceFilter};
333
334pub use parse::ParseError;
335pub use render::{RenderOptions, Synth};
336pub use score::{
337    Arpeggio, Channel, ChannelWriter, DutySweep, Envelope, Instrument, Length, MAX_CHANNELS,
338    MAX_NESTING, Note, Pitch, Score, ScoreLimits, Vibrato,
339};
340pub use wave::{Aliasing, Duty, Lfsr, NoiseMode, Oscillator, Waveform};
341
342impl Score {
343    /// Parses score text with the default [`ScoreLimits`].
344    pub fn parse(text: &str) -> Result<Score, ParseError> {
345        parse::parse(text, &ScoreLimits::default())
346    }
347    pub fn parse_with_limits(text: &str, limits: &ScoreLimits) -> Result<Score, ParseError> {
348        parse::parse(text, limits)
349    }
350}
351impl std::str::FromStr for Score {
352    type Err = crate::Error;
353    fn from_str(text: &str) -> crate::Result<Self> {
354        Ok(Self::parse(text)?)
355    }
356}