VOLTAreference

Reference / Design a sound

Wavetable synthesis

Design a sound · playable examples

Open in the workbench →
On this page · 5 sections

Move through a collection of waveforms to change a tone continuously, using stock tables or tables you define yourself.

Hear it and change it#

Listen for: the tone changing within a held note, as the oscillator moves through the supplied harmonics table. The melody itself stays simple.

Listen to this example · four bars, then the tail
tempo = 120bpm
timingVersion = 2
tablepad = patch {
  source = wavetable "harmonics" (freq pitch)
    (lfoSine 0.2Hz |> scale [0.1, 0.8]),
  ampEnv = adsr 450ms 400ms 0.7 700ms,
  gain = 0.25
}
play tablepad ("c4 g4" |> slow 4) |> reverb 0.53 0.25

Make it yours: try the formant table for a more vowel-like motion. This changes the wavetable; the scan rate stays the same. See Wavetables.

What it is#

A wavetable oscillator scans a table of single-cycle waveforms with a continuous position control: 0 plays the first frame, 1 the last, and positions between interpolate. Because position is an ordinary signal argument, any modulator (LFO, envelope, macro, pattern) drives the table-scan — that is the morphing-timbre move at the heart of the feature. Playback is band-limited via per-frame mip pyramids, so high notes don't alias.

Tables come from three sources, all resolving to the same frame/mip format: the seven stock tables (basicShapes, harmonics, pwm, formant, drawbar, fold, crush), in-language wavetableOf [[harmonics], …] definitions, and sample-derived wavetableOf (sample "x") definitions (single-cycle frames extracted from a recording). All three forms work on desktop and browser. A sample-derived table requires its source recording to be available before playback.

Surface#

-- file-backed table by name
source = wavetable "harmonics" (freq pitch) (lfoSine @ 1/2c |> scale [0, 1])

-- in-language table from harmonic-amplitude lists (one inner list per frame)
glassy = wavetableOf [
  [1, 0,   0.5, 0,   0.25],
  [1, 0.6, 0.4, 0.3, 0.2, 0.1],
]
lead = wavetable glassy (freq pitch) (lfoSine @ 1/4c |> scale [0, 1])

-- per-partial phase: each partial is `[amplitude, phase]`, phase in turns (0..1)
phased = wavetableOf [
  [ [1, 0], [0, 0], [0.5, 0.25] ],
  [ [1, 0], [0.6, 0.5], [0.4, 0] ],
]

-- sample-derived table: chop a recording into single-cycle frames
moogTable = wavetableOf (sample "moog") (frames 48) (startAt 0.25)
pitchMod  = wobble |> scale [0Cents, 24Cents]   -- a vibrato in cents
voice     = wavetable moogTable (freq pitch + pitchMod) filtEnv

wavetable takes (name | Wavetable-value) freq position. The first argument is either a string literal (resolved against the /wavetables/ manifest) or a wavetableOf binding. freq is Sig Control Hz; position is Sig Control Normalized. Audio-rate phase modulation is available via wavetable … |> phaseMod mod depth (see FM, phase and ring modulation). |> phase p sets the note-on table-scan position in cycles; without it each voice keeps the seeded start used to decorrelate unison stacks.

In an in-language harmonic table, each partial is either a bare amplitude (sine-phase) or an [amplitude, phase] pair with the phase in turns (0..1); the two forms may be freely mixed (a scalar is just phase 0). Static per-partial phase shapes the waveform (not just its magnitude spectrum); live, evolving per-partial phase is the additive oscillator's job (see Additive synthesis).

Shipped file-backed tables: basicShapes, harmonics, pwm, formant, drawbar, fold, crush. Showcase songs: examples/songs/morph.volta (file-backed), petrichor.volta / filament.volta / gristmill.volta (sample-derived).

Sample → wavetable bridge#

wavetableOf (sample "x") records only a spec in the IR (WavetableDef::FromSample); the engine owns the analysis, run once when the sample arrives. synthesize_table_from_sample estimates the source period by autocorrelation, places frameCount frame regions linearly from startOffset · duration, snaps each to the nearest positive-going zero crossing (so every frame starts at 0 moving positive — smooth morphing), reads one period, and resamples to 2048. Each frame is then DFT'd back to harmonics and fed through the same mips_for band-limiting path as every other table — one band-limiting implementation across all sources. A missing sample, or a reference before its sample has decoded, falls back to silence (same as any not-yet-loaded wavetable). Optional shaping: frames N, startAt F.

Notes & limits#

  • A voice that plays before its table is installed renders silence rather than failing (a startup note or two may be silent — consistent with ARCHITECTURE §5.5).
  • Frame-length policy: short harmonic frames are zero-padded to the longest by the lowerer; amplitudes are per-frame unit-peak normalized to avoid level jumps while morphing.
  • Sample-derived tables use a single median-pitch estimate, so a vibrato source yields slightly off-pitch frames; only single-zone samples are supported. No formant/PSOLA resynthesis, no spectral-freeze mode.
  • The asset drawer does not yet list in-language / sample-derived tables. That and generated/time-domain wavetableOf frames are deferred — see Wavetable synthesis §8.5 and Wavetable From Sample §8 / ideas.md.