CrashAudio Architecture for Sound Engineers

What the engine actually is, what it does to a signal, and where its edges are.
Every number here was read out of the implementation rather than from design
docs; where the code and its own comments disagree, that is called out.

One-line summary: a polyphonic, layered subtractive/FM/sample/wavetable
synth with a per-track DSP chain, 44.1 kHz stereo out, driven either from MML
strings or from multi-track song banks.


1. Signal flow

                    ┌──────────────────── VOICE (×32) ────────────────────┐
                    │                                                     │
  note on ─────────▶│  LAYER 0 ─┐   source: oscillator | sample | table   │
                    │  LAYER 1 ─┤   per-layer: volume, detune, octave,    │
                    │  LAYER n ─┤               pitch env, FM target/depth │
                    │           │                                          │
                    │           ▼                                          │
                    │        Σ layers                                      │
                    │           │                                          │
                    │           ▼                                          │
                    │      biquad filter  ◀── filter envelope              │
                    │           │                                          │
                    │           ▼                                          │
                    │    amplitude ADSR   ◀── ONE per voice, not per layer │
                    │           │                                          │
                    │           ▼                                          │
                    │       tremolo LFO                                    │
                    │           │                                          │
                    │           ▼                                          │
                    │   instrument volume × note velocity                  │
                    │           │                                          │
                    │           ▼                                          │
                    │   constant-power pan  ──────────────▶ L / R          │
                    └──────────────────────────────────────────────────────┘
                                        │
                                        ▼
                          Σ voices  ×  polyphony gain          (0.893 / √N)
                                        │
                                        ▼
                            per-track effect chain              (up to 12 kinds)
                                        │
                                        ▼
                              × master volume
                                        │
                                        ▼
                                   soft clip
                                        │
                                        ▼
                        [Retro only] 15 kHz one-pole low-pass
                                        │
                                        ▼
                              44.1 kHz stereo out

Pitch modulation (vibrato LFO, pitch envelopes, MML bends) is applied to the
frequency feeding each layer, before the sources run — not shown above to keep
the amplitude path readable.


2. Voice architecture

A voice renders one note. An instrument is a stack of layers, all
rendered by the same voice and summed before the filter.

Per layer you get an independent source, volume, detune (cents), octave
(a float multiplier of 2ⁿ, so fractional values give non-octave ratios — the
built-in organ uses 1.585 for a twelfth), an optional pitch envelope, and FM
routing.

The significant architectural constraint: amplitude ADSR, filter, tremolo,
pan and velocity all live at voice level. Layers share one amplitude contour.
You can build a static blend of sources, but not a sampled attack that decays
under a synthesized sustain — both halves would rise and fall together. (Layers
do have their own pitch and FM-depth envelopes; only amplitude is shared.)

In practice this matters less than it sounds, because a sample layer carries
its own amplitude shape in the audio itself and can be set to one-shot or looped
independently — see §4.2.


3. Sources — oscillators

Wave Band-limited? Notes
sine n/a pure
square yes — polyBLEP fixed 50% duty; ignores dutyCycle
pulse yes — polyBLEP variable duty, clamped 0.01–0.99
sawtooth yes — polyBLEP
triangle no — naive aliases at high pitch
noise n/a 15-bit LFSR, NES topology
custom no additive sine series from a harmonics[] array

Duty cycle is the whole character of a pulse. Harmonic amplitudes follow
|sin(πhd)| / h, so at 50% every even harmonic cancels (hollow), at 25% they
return at 0.71 of the fundamental (reedy), and at 12.5% the series is nearly
flat through h6 — 0.92, 0.80, 0.65, 0.48, 0.31 — which is why narrow pulses cut
through a dense mix. Duty is recoverable from a spectrum by inverting
h2/h1 = |cos(πd)|.

Note the asymmetry in that table: square and pulse are separate wave types
here, and square silently discards dutyCycle. Industry synths expose one
pulse oscillator with a width control and treat square as width = 50%. Setting
dutyCycle on a square layer is a no-op, not an error.

Noise is an NES-style LFSR (XOR of bits 0 and 1, 15-bit) rather than white
noise, clocked at frequency × 16 so musical note pitches map onto a useful
hiss-to-rumble range. Low notes rumble, high notes hiss, and octave changes shift
timbre by 2×. Consequence: noise is pitched — the played note sets its
brightness, so percussion patches usually pin it with percussion +
fixedFrequency.

Custom/additive sums harmonics[i] · sin(2π·(i+1)·φ) with no band-limiting
and no normalisation — N harmonics cost N sin() per sample per voice, and the
output peak is whatever the series sums to. Scale the array yourself.


4. Sources — sampled and wavetable

4.1 Which to reach for

  • sample — any recorded audio: instruments, one-shots, drums. Full rompler
    behaviour.
  • wavetable — Serum/Vital-format single-cycle frame stacks, for morphing
    synthetic timbres.

These are not interchangeable. A wavetable reduces audio to single cycles, which
discards attack transients, breath/bow noise and formant movement — i.e. most of
what identifies a real instrument. Use sample when you want the source to
sound like itself.

4.2 `sample`

  • baseNote declares the MIDI note the file plays at natively; playback
    resamples by 2^((note − baseNote)/12).
  • loopMode: auto (engine detects a sustain loop at load), manual (explicit
    loopPoints), or oneShot (forced no loop; the layer outputs silence once
    it runs past the end).
  • pitchMode: resample (pitch and duration coupled, the classic behaviour) or
    stretch (timestretch-based, so a baked-in decay keeps its natural length
    across the keyboard — worth the extra CPU for pianos and plucks).

A looped sample layer gives you attack and sustain from one layer, at pitch.
No layering required.

4.3 `wavetable`

Serum/Vital WAV interchange: single-cycle frames concatenated, frame size read
from the Serum clm RIFF chunk, else a supplied frameSize, else probed
against 4096/2048/1024/512/256. Anti-aliased by 11-band per-octave band-limited
mipmaps (FFT → zero above Nyquist → IFFT, crossfaded).

Two edges worth knowing:

  • The file's sample count must be an exact multiple of the frame size.
    That is correct for the interchange format and true of pack content, but
    ordinary recordings essentially never satisfy it.
  • A wavetable that fails to load is logged, not raised — the layer falls
    back to its oscillator. Several instruments pointing at different unloadable
    files therefore all render as the same default waveform, which presents as
    "wavetables sound characterless and identical."

WavetableSource declares wav, wavAnalyzed and inline; only wav is
implemented.
The other two are matched nowhere and are silently skipped.
Morph position is static per instrument (no envelope/LFO routing yet), and
unison is declared in the format but not rendered.


5. Modulation

5.1 Amplitude ADSR

All four segments are linear ramps. Attack rises linearly to 1.0, decay
falls linearly to the sustain level, release falls linearly from wherever the
level was. (The decay arm is commented "exponential decay" but computes a
constant per-sample decrement — the comment is wrong, the behaviour is linear.)

Engineering consequence: plucks and percussive decays read as more abrupt than
an exponential envelope of the same time, because a linear fall spends far less
time in the quiet tail. Compensate with a longer decay than you would dial on an
analogue-modelled synth.

Two useful behaviours:

  • Retrigger does not reset the level. A note re-attacked during its release
    ramps from the current level, so overlapping notes don't click.
  • Voice stealing uses a forced 5 ms release, not a hard cut.

Times are clamped to a 0.1 ms minimum.

5.2 Pitch envelope

Unlike the amplitude ADSR, the pitch envelope is exponential —
start · (end/start)^t — matching exponentialRampToValueAtTime, because pitch
perception is logarithmic and a linear Hz ramp sounds wrong.

Available per layer and per instrument. absolute: true treats start/end as Hz
(fixed regardless of the played note — how drums and SFX stay consistent);
false treats them as multipliers of the played pitch. An instrument-level
envelope overrides all layers. Absolute envelopes settle at end; multiplier
envelopes return to the neutral 1.0 once the sweep completes.

5.3 LFOs

vibrato (pitch, depth in cents) and tremolo (amplitude, depth 0–1), each
with an optional delay before onset. The type field is informational — what
a given LFO does is decided by which slot it is assigned to.

5.4 FM

Phase modulation, not true frequency modulation — the same choice the DX7 and
modern FM plugins make. A layer with fmTarget set stops contributing to the
audio sum and instead adds to the target layer's phase, scaled by fmDepth.

  • feedback folds a fraction of an operator's own averaged last-two outputs
    back into its phase — the YM2612's two-sample tap. Pushing it warps a sine
    toward a saw; ~1–3 is a gritty edge, useful range up to ~6.
  • fmEnvelope is an ADSR on modulation depth, so brightness evolves
    independently of amplitude (bright bite settling to a mellow body). Absent =
    constant depth.

6. Filter

Standard RBJ-cookbook biquad, 12 dB/octave: lowpass, highpass,
bandpass, notch. Parameters are cutoff in Hz and resonance as Q.

One filter per voice, applied to the summed layers before the amplitude
envelope. An optional filter envelope drives cutoff with its own ADSR plus a
peak multiplier for the attack.

There is no per-layer filtering, no filter drive/saturation stage, and no slope
selection.


7. Polyphony, allocation and gain staging

  • 32 voices in Modern, 8 in Retro.
  • Allocation is by age stamp; when the pool is exhausted the oldest released
    voice is stolen first, with the 5 ms fade noted above.

The polyphony gain law

Every mixer has exactly one ceiling, and a chord stacks voices into it. Square
and pulse waves sit near full amplitude for most of their cycle, so a six-note
chord wants six times the headroom a single note needs — and hard-limiting that
waveshapes every cycle flat, which on an already-square signal is just
distortion.

CrashAudio scales the voice sum by:

gain = 0.893 / √N            N = actively sounding voices

slewed by a one-pole filter with a 15 ms time constant so notes entering and
leaving a chord glide rather than step (no zipper, no pumping as a chord's notes
release at different times). The 0.893 is sized so a worst-case full-velocity
square chord peaks near 1.75× pre-clip, leaving the soft-clip to round only the
loudest coincident peaks.

A solo voice sits about 1 dB under unity — inaudible, and a little global
headroom. The trade is that dense chords sit a decibel or two below where naive
summing would put them, in exchange for not breaking up. This is also how
acoustic instruments behave: a six-note piano chord is fuller than one note but
nowhere near six times louder, because the strings don't peak together.

Soft clip

Linear below 0.5, then compressed:

|x| ≤ 0.5  →  x
|x| > 0.5  →  sign(x) · (0.5 + (1 − e^(−2(|x|−0.5))) · 0.5)

So the bottom half of the range is untouched and the top saturates smoothly
instead of tearing.

Velocity

MML V0–V15 maps linearly to amplitude (volume / 15). It is not a dB
map, so the steps are not perceptually even — the top of the range is much
closer together than the bottom. Worth knowing when writing dynamics.


8. Modern vs Retro

INSTRUMENTMODE "modern" | "retro", switchable at any time.

Modern Retro
Voices 32 8 (SNES S-DSP cap)
Sample interpolation linear 4-tap Gaussian
Output filter none 15 kHz one-pole low-pass

Retro is a deliberate downgrade for period feel, and the constraint is the
point — the 8-voice cap with oldest-released stealing is what produces the
characteristic mid-90s behaviour of a busy arrangement dropping its own tails.
Gaussian interpolation supplies the soft top end.

The polyphony gain law and soft clip live on the shared path and apply in
both modes. Synthesized waveforms are otherwise largely unaffected by the
mode switch; the audible difference is concentrated in sampled instruments and
the output low-pass.


9. Track effects

A per-track DSP chain declared inside the MML string as $NAME:p[:q], where p
and q are 0–100 intensities (p = 0 bypasses, except $PAN). Declaration
order is chain order
, the settings are track-global, and last value wins.

ECHO (tempo-synced delay; q = note division) · REVERB (q = room size) ·
CHORUS · FLANGER · PHASER · TREMOLO · VIBRATO · DRIVE (q = post
tone) · BITCRUSH (p = bit reduction, q = sample-rate reduction) · FILTER
(lowpass; q = resonance) · HIGHPASS · PAN (0 = hard left, 50 = centre,
100 = hard right).

Because effects ride inside the MML string, they behave identically in PLAY,
PLAY BACKGROUND, and multi-track song banks.

Two implementation details that affect how tails behave:

  • The polyphony gain is applied upstream of the chains, to the signal
    feeding both the dry mix and every bus. So a reverb or echo tail decays from
    what it was actually fed rather than from a louder ghost of it.
  • Every bus runs its chain on every sample, even with all its voices idle.
    Pushing silence through a chain is what drains it; gating bus processing on
    voice activity would chop the tail the moment you released the key.

When no track declares an effect, the engine takes a dry path that skips the bus
machinery entirely — you don't pay for effects you aren't using.


10. Control surface

MML is the note language: notes A–G with #/+/-, octaves O0–O6
plus </>, lengths L1–L64, tempo T32–T255, rests R/P, volume
V0–V15, instruments I<n>, articulation MN (7/8) / ML (full) / MS
(3/4), dotted notes, ties ^, pitch bend ~ (semitone delta) and portamento
& (glide to the next note), chords [CEG], and numeric notes N0–N116
(N0 = rest, N1 = C0, N116 = G9 — wider than the O range reaches).

Instruments are 67 built-ins, overridable and extendable by .cbi JSON
banks. A bank keyed above the built-in range simply adds instruments.

Rendering: songs are pre-rendered through a shared song_render path used by
both native and WASM, so a pre-rendered track honours every per-note feature.
Any new per-note behaviour has to be added there as well as to the live
synthesiser, or background music silently falls back to the plain oscillator.


11. Known characteristics and limitations

Collected so they are not discovered by ear:

  1. One amplitude envelope per voice. No per-layer amplitude contours, so
    sampled-attack-over-synth-sustain layering is not expressible.
  2. ADSR segments are linear, not exponential. Decays read abrupt relative to
    analogue-modelled synths.
  3. triangle and custom are not band-limited. They alias at high pitch;
    square, pulse and sawtooth are polyBLEP-corrected.
  4. square silently ignores dutyCycle. Use pulse when duty matters.
  5. Noise is pitched by the played note (LFSR clocked at f × 16). Pin it with
    percussion + fixedFrequency for consistent percussion.
  6. Velocity is linear, not dB-mapped.
  7. Wavetables: only the wav source is implemented; morph position is
    static; unison is declared but not rendered; load failures fall back silently
    to the oscillator.
  8. Narrow pulses carry a large DC offset (−0.75 before the envelope at 12.5%
    duty, as on the real hardware). It costs headroom when several are stacked.
  9. One filter per voice, 12 dB/oct, no drive stage, no slope choice.