Klatt-style cascade/parallel formant synthesizer (after Klatt 1980, JASA; simplified). Pure Lua: synthesis is offline (a whole utterance renders to a buffer in milliseconds under LuaJIT), so there is no C hot path.
Input: a list of phoneme events { ph = "AA", stress = 0|1|2, dur = ms, f0 = Hz } (see sound.speech) + a voice knob table. Output: a MONO
sound.buffer at 16 kHz.
Pipeline:
rank
neighbour (consonant) fixes the value, and the transition is
flown inside the lower-ranked (vowel) segment — the classic
Holmes/rsynth scheme. Source amplitudes use fast (~12 ms)
ramps and dip to min() at boundaries; f0 interpolates between
segment midpoints.Voice knobs consumed here: formant (scales every resonator frequency —
vocal-tract length), breath (shifts voicing into aspiration), flutter
(slow f0 wobble), growl (alternate-pitch-period amplitude + period
modulation — creak/diplophonia, the gravelly menace), quantize (hold
parameter frames in blocks of N — the robot staircase), gain.
| Name | Signature |
|---|---|
| render | render(events, voice, opts) -> buf, info/err |
render(
events,voice,opts) ->buf,info/err
Render phoneme events into a mono 16 kHz sample buffer
opts.yield_every (parameter frames) makes the render cooperative: every N
frames it calls coroutine.yield(done_fraction) — the incremental path
speech.say_job slices a long render across an application's own schedule.
All render state lives in locals a yield cannot touch, so the sliced output
is byte-identical to the monolithic one by construction.