BA Barış Akın~/blog

·9 min read·7 views

Aurora Synth: a free wavetable synth, sampler and modeled piano with makam tuning, written in Rust

Three engines in one instrument: mipmapped wavetables, an SFZ sampler and a piano modeled with waveguide strings. Makam tuning in Arel-Ezgi-Uzdilek commas, a choir synthesised without recordings, and kanun, oud, bağlama and ney built from CC0 recordings. VST3, CLAP and a standalone app for macOS and Windows.

Aurora Synth: a free wavetable synth, sampler and modeled piano with makam tuning, written in Rust

Aurora is three sound engines in one instrument: a wavetable synth, an SFZ sampler and a physically modeled piano. All three run through the same filters, the same mod matrix and the same effects rack, and makam and maqam tunings are built in. It runs on macOS and Windows as VST3, CLAP and a standalone app. It's free; the download page is aurorasynth.baltavista.com.

In this post I'll look inside it: how the oscillators avoid aliasing, how the piano's strings are computed, how Arel-Ezgi-Uzdilek commas are mapped onto keys, and how a choir gets synthesised without a single recording. The code is Rust from top to bottom: nih-plug on the plugin side, egui for the interface. About 15 thousand lines in total.

The Aurora interface: three oscillators and a mixer on top, a filter graph and draggable envelopes in the middle, LFOs, mod matrix, effects, oscilloscope, spectrum and keyboard below

Signal path

SYNTH  : OSC 1/2/3 + Sub + Noise + Ring ─┐
SAMPLED: SFZ sampler ────────────────────┼→ Filter 1 ⇄ Filter 2 → Amp → FX chain → Master
MODELED: waveguide piano ────────────────┘

You pick the engine in the Master panel: SYNTH, MODELED or SAMPLED. Everything after that is shared. So a sampled kanun can go through the same ladder filter, mod matrix and OTT as a supersaw lead. All 94 factory presets are built on this one voice architecture; all ~245 parameters can be automated from the DAW and are saved with the project.

Oscillators and wavetables

The classic shapes (sine, triangle, saw, pulse) are generated with PolyBLEP: a small polynomial correction is added right around each sharp corner of the waveform, which suppresses most of the harmonics that would spill over Nyquist.

Wavetables are harder, because the waveform is an arbitrary single cycle that can contain anything up to 1024 harmonics. Aurora stores each table as a per-octave mipmap: level m contains only harmonics 1 to 1024 >> m. The oscillator looks at the frequency it's playing and picks the level whose top harmonic still stays below Nyquist. High notes read from a copy with fewer harmonics, and nothing folds back.

There are six built-in tables (Classic, Harmonic, Vocal, FM, Sync, Fold) of 16 frames each. WT position is a mod matrix destination, so an LFO or an envelope can sweep through the table. You can also import tables as WAV: Serum's 2048-sample frames with a clm header, 256/512/1024 frame sizes, or a single cycle.

On top of that: warp modes (Sync, Bend, Mirror, Squeeze/PWM, Quantize), phase-modulation FM (2→1, 3→2), hard sync to OSC 1, and up to 7-voice unison with detune and stereo spread.

Filters

The two filters can be wired in series or in parallel (with a balance control). There are nine modes:

  • SVF: LP24, LP12, BP, HP, Notch. A zero-delay-feedback (ZDF) state variable filter in Simper/Zavalishin's TPT form; two stages in a row for 24 dB/oct.
  • Ladder: a saturating ZDF Moog-style 4-pole.
  • Comb+ / Comb−: comb filters with positive and negative feedback.
  • Formant: a vowel filter that morphs through A-E-I-O-U with the cutoff.

Drive and the nonlinear filters can run at 2× the sample rate (Off, Auto, Always). Auto only switches it on when drive is up or a saturating filter like the ladder is selected, so a clean SVF doesn't waste CPU.

The filter graph in the interface isn't a drawing: it's computed from inside the filter code, as the true magnitude of the digital transfer function. Click and drag on it to change cutoff and resonance.

Modulation

Three envelopes (filter, amp and a separate mod envelope), two LFOs with five shapes or a 16-step drawable shape, four macro knobs, velocity, mod wheel, aftertouch, key and random can all be sources. Each of the 8 slots in the matrix also has a Via scaler: "LFO 1 to pitch, by as much as the mod wheel". When a parameter is modulated a ring appears around its knob, showing both the modulation range and the live value.

The envelopes use analog-style exponential curves and are edited by dragging their nodes. The envelope and LFO positions of the playing voices show live on top of the graphs.

The modeled piano

The MODELED engine uses no samples. Each key has 1 to 3 slightly detuned strings (one in the low bass, two in the middle, three in the treble). Each string is a digital waveguide: a delay line and three filters that close the loop.

  1. Loss filter. A one-pole low-pass; the high partials lose a little more on each round trip, so the note darkens over time.
  2. Dispersion allpasses. A real piano string is stiff, so its partials aren't exact multiples but stretched slightly upward. A chain of up to four first-order allpasses imitates that stretch.
  3. Thiran fractional delay. The delay line has an integer length, but accurate tuning needs a fractional one. A Thiran allpass covers the remaining fraction.

The subtle part: the loss filter and the allpasses also delay the signal a bit, and that delay depends on frequency. Ignore it and the treble plays noticeably flat. Aurora computes the phase delay of each of these filters at the fundamental, per string, and subtracts it from the length of the delay line:

let w = 2.0 * PI * f / sr;
let mut fixed = lp_delay(b, w);
for d in self.disp.iter_mut().take(n_disp) {
    d.a = disp_a;
    fixed += ap_delay(disp_a, w);
}
let total = sr / f - fixed;
let m = (total - 0.5).floor().max(2.0);
let frac = (total - m).clamp(0.1, 1.9);
self.thiran.a = (1.0 - frac) / (1.0 + frac);

The decay time changes with the key too: from ~16 seconds in the low bass to ~1 second at the very top. So that the loss filter doesn't use up that whole budget on its own for high notes, it's softened until its loss at the fundamental is no more than half the budget.

The hammer that excites the string is a velocity-dependent half-sine pulse: harder strikes give a shorter and therefore brighter pulse. The pulse goes through a comb filter set by where the hammer hits the string (between 1/16 and 1/5 of its length); this imitates how, on a real piano, the partials with a node at the strike point are missing. Then come the dampers (no dampers in the top octave, like on a real piano), the sustain pedal and a small bank of resonators for the soundboard colour.

The sampler

The SAMPLED engine is an SFZ player: regions, velocity layers, loops with loop crossfades, release triggers with rt_decay, amp_veltrack, a per-region low-pass, round robin with lorand/hirand. Samples can be WAV or FLAC; an SFZ that names x.wav also finds x.flac. Folders without an SFZ, named like A0v1.wav, are read from their file names.

It solves two practical problems automatically:

  • Loudness. Different libraries have very different levels. Aurora measures the RMS of the loudest 50 ms window in the first second of each set and matches the sets to the Salamander piano. The window approach is fair to both plucked and bowed sounds.
  • Memory. Some libraries have 16 velocity layers. Aurora thins them to 6, makes sure the whole velocity range is still covered, and only loads the files that remain.

The preset browser: presets in the Lead, Pluck and Bass categories, with category filters at the top

Makam tuning

It plays makam, not just twelve notes. Pick a makam and a root, and every engine (synth, sampler, piano) retunes to that makam's intervals. The on-screen keyboard shows each key's deviation from 12-TET in cents.

The Turkish makams sit at their positions in Arel-Ezgi-Uzdilek theory: the octave is divided into 53 Holdrian commas (1 comma ≈ 22.64 cents) and each makam is defined by the comma positions of its degrees above the root:

const KOMA: f32 = 1200.0 / 53.0;

5 => &[5, 17, 22, 31, 40, 44],  // Hicaz: Hicaz tetrachord + Rast pentachord
3 => &[9, 17, 22, 31, 40, 48],  // Rast: Rast tetrachord + Rast pentachord

Each degree is converted to cents and placed on the nearest keyboard key; the difference is how far that key is shifted. In Hicaz on D, for example, E♭ moves +13 cents, F♯ −15 cents and B +6 cents. The result is a 128-entry table holding a pitch offset for every MIDI note, which the engines add to their notes.

Besides Rast, Uşşak, Hicaz, Hicazkar, Saba, Hüseyni, Nihavend and Kürdi there are the Arabic quarter-tone Bayati and Rast, 5-limit just intonation and Pythagorean tuning. If that's not enough, Scala .scl files load too: 53-EDO, 24-EDO or any scale you like, with any number of notes per octave.

A choir without recordings

The "Elven" choir and solo soprano in the Voices pack contain no recordings at all; they're generated entirely by additive synthesis in a Python script. Every note is an ensemble: one glottal-like harmonic source per singer, each with its own detune, vibrato, pitch drift and breath noise.

The detail that makes it convincing is how the formants are applied. The vowel formants (for "aah", 850, 1220 and 2810 Hz, for example) are evaluated at each harmonic's instantaneous frequency. As vibrato moves a harmonic towards and away from a formant peak, that harmonic's level changes with it; that's where the shimmer of a real voice comes from. A static filter doesn't give you that.

Turkish and Arabic instruments

The kanun, bağlama, ud, ney, darbuka and def come from CC0 recordings, but those recordings aren't ready-made sample sets: a chromatic run on a kanun, a ney improvisation. Another Python script turns them into instruments: it finds the notes by onset detection for the plucked instruments and by tracking stretches of steady pitch for the ney, measures each note's pitch, picks the best take for each semitone and corrects the remaining detune with tune in the SFZ. These presets also come in makam-tuned versions: Rast on G, Uşşak, Hicaz and Hüseyni on A, Bayati on D.

Effects and the rest

The effects chain is made of chips that you drag to reorder: drive, bitcrusher, phaser, flanger, chorus, tempo-synced ping-pong delay, 3-band EQ, compressor, OTT multiband compression and an FDN reverb. The reverb has four diffusion allpasses after the predelay and eight delay lines with a Householder feedback matrix, each line with its own damping and gentle modulation.

There's also a tempo-synced arpeggiator with six modes; 16-voice poly, mono and legato with glide; an oscilloscope, FFT spectrum analyzer and stereo meters; and you can play from the on-screen or the computer keyboard.

In a DAW and without one

An Ableton Live 12 session where every track is an Aurora instance; Aurora's violin sampler is open on the right

The plugin is built as VST3 and CLAP with nih-plug; MIDI comes from the DAW with sample-accurate timing, and the arpeggiator and LFOs follow the DAW tempo. It's been tested in Ableton Live 12; in the session above every track is an Aurora: sampled strings, harpsichord and synth layers all playing at once. There's no AU version yet, so Logic and GarageBand can't load it; the standalone app is there for them.

In the standalone app audio comes from cpal, MIDI from midir, and there's a built-in MIDI file player: drag a .mid onto the window, choose which parts play, change the tempo and play along on the keyboard. In multi-part files the lead melody is picked automatically. Both features have a video on the site.

The macOS package is universal for Apple Silicon and Intel (merged with lipo); the Windows package is cross-compiled from macOS with mingw-w64. The sample packs are FLAC and the same for both systems: Piano (Salamander Grand and Upright KW, ~93 MB), Orchestral (VSCO 2 CE strings, woodwinds, brass and harpsichord, ~255 MB), World (~32 MB) and Voices (~22 MB). The synth presets need no pack at all; presets that need one are marked in the browser.

Try it

You can download Aurora from aurorasynth.baltavista.com. A good place to start: open a Hicaz-tuned kanun preset from the Turkish & Arabic category and watch the cent deviations above the keys on the on-screen keyboard.

BA
Barış Akın

Software solutions architect with 25+ years of experience. FinTech and distributed systems; creator of OxiDB, Rill and Cobra.