sound.speech.klatt

index ยท sound.speech

Overview

A cascade/parallel formant synthesizer, simplified from Klatt (1980). It is pure Lua: a whole utterance renders to a buffer in milliseconds under LuaJIT, so it needs no C.

Input: phoneme events { ph, stress, dur, f0 } and a voice knob table. Output: a mono sound.buffer at 16 kHz (RATE).

Stages:

  1. Expand: events become segments. A stop becomes closure, burst and aspiration; an affricate becomes closure and frication. HH takes the neighbouring vowel's formants.
  2. Tracks: segments become 5 ms parameter frames. At each boundary the neighbour with the higher rank, the consonant, fixes the formants, and the transition happens inside the lower-ranked vowel. This is the Holmes/rsynth scheme. Source amplitudes ramp in about 12 ms and dip at boundaries. f0 interpolates between segment midpoints.
  3. Render: frames become samples. Voicing is a differentiated Rosenberg glottal pulse with flutter. Noise is a seeded LCG, so the output depends only on the events and the voice. The cascade branch (nasal pole, nasal zero, R1 to R5) shapes voiced and aspirated sound. The parallel branch (A2 to A6 and a bypass, alternating signs) shapes frication and bursts.

Knobs read here: formant scales every resonator; breath moves voicing into aspiration; flutter wobbles f0; growl alternates period length and amplitude, a creaky voice; quantize holds frames in blocks of N; gain.

Functions

NameSignature
renderrender(events, voice, opts) -> buf, info/err

render(events, voice, opts) -> buf, info/err

Render phoneme events into a mono 16 kHz sample buffer

With opts.yield_every = N, the render calls coroutine.yield(done_fraction) every N parameter frames. speech.say_job uses it to slice a long render. The render state lives in locals, so a sliced render gives the same bytes as a whole one.