code wiki / _hdl_build / nx_vox_tract.nx
nx_vox_tract.nx
buildroot/runtime/_hdl_build/nx_vox_tract.nx
about
nx_vox_tract.nx -- the TIMBRE half of animal-sound synthesis: a vocal tract as a physical tube.
nx_vox_source produces a glottal flow. That flow sounds like a buzz until it passes through a
throat, and the shape of the throat is most of what makes a rabbit sound like a rabbit and a dog
sound like a dog. This organ is that throat, modelled as a chain of cylindrical sections.
WHAT IS GENUINELY NEW HERE, AND WHAT IS DELIBERATELY NOT. A tree-wide audit ran before a line was
written (operator: "make sure we arent duplicating stuff"), and it found that most of this problem is
already solved:
* nx_lpc_levinson ALREADY emits PARCOR reflection coefficients in Q30. Reflection coefficients are
exactly what a tube chain is made of, so this organ adopts that representation verbatim rather
than inventing a second one -- a tract built here is interchangeable with the voice stack's.
* nx_lpc_synth ALREADY implements the all-pole filter in direct form. It is imported and USED.
What did not exist anywhere in 16000+ files, and is therefore all this organ adds:
* vt_area_to_k -- tube GEOMETRY (cross-sectional areas) -> reflection coefficients. The whole
point: the existing coefficients are FITTED to a recording, these are DERIVED
from a shape you can state, so a species can be described instead of sampled.
* vt_k_to_a -- the step-up recursion, so a geometry can drive the existing direct-form filter.
* vt_ladder -- the Kelly-Lochbaum scattering ladder.
WHY A LADDER AT ALL, IF THE DIRECT FORM ALREADY EXISTS. They compute the same transfer function, so
the ladder has to earn its place: it is the PHYSICAL structure. Each coefficient is one tube
junction, forward and backward travelling waves are separately available, |k|<1 guarantees stability
by construction, and a section can be moved while the filter runs (which is what articulation is).
The direct form has none of that. To prove this is a second REALISATION and not a second
IMPLEMENTATION, nx_vox_tract_gate asserts the ladder and nx_lpc_synth agree sample-for-sample on the
same geometry. If they ever diverge, one of them is wrong and the gate says so.
TRACT LENGTH SETS THE FORMANTS, and that is the species signature. For a uniform tube the
resonances sit at (2n-1)*c/(4L), so a 150 mm dog throat puts its first formant near 570 Hz and a
15 mm mouse throat puts it near 5700 Hz. Section count is NOT free: a waveguide advances one section
per sample, so N = 2*L*rate/c. The organ derives N rather than letting a caller pick a number that
silently means a different tube than the one they asked for.
license_tier: ORIGINAL expect_exit: 0
dependencies 2 imports · 0 importers
imports: nx_syscalls.nxnx_lpc_synth.nx
imported by: nobody (leaf or entry point)
structs
| none |
consts
| 40 | const VT_Q30: i64 = 1073741824 // 2^30 -- the PARCOR scale nx_lpc_levinson already uses |
| 41 | const VT_C: i64 = 343000 // speed of sound, mm/s |
| 42 | const VT_MAXSEC: i64 = 96 // sections; 96 covers a 170 mm tract at 96 kHz |
| 43 | const VT_SHAPE_N: i64 = 8 // control points in a species shape profile |
| 138 | const VT_SP_LEN: i64 = 0 // mm, glottis to lips |
| 139 | const VT_SP_F0: i64 = 1 // Hz, typical fundamental for the source driving it |
| 140 | const VT_SP_SHAPE: i64 = 2 // VT_SHAPE_N relative areas follow |
| 141 | const VT_SP_N: i64 = 10 // 2 + VT_SHAPE_N |
| 143 | const VT_NSPECIES: i64 = 5 |
| 144 | const VT_SP_RABBIT: i64 = 0 |
| 145 | const VT_SP_RODENT: i64 = 1 |
| 146 | const VT_SP_BIRD: i64 = 2 |
| 147 | const VT_SP_DOG: i64 = 3 |
| 148 | const VT_SP_UNIFORM: i64 = 4 // a plain tube: the control case, no shaping at all |
functions
| 48 | func vt_nsec(len_mm: i64, rate: i64) -> i64 called by 1: vt_speak |
| 60 | func vt_area_to_k(areas: *i64, nsec: i64, k_out: *i64) -> i64 called by 1: vt_speak |
| 86 | func vt_k_to_a(k: *i64, order: i64, a_out: *i64) -> i64 calls 1: sys_mmap |
| 110 | func vt_ladder(exc: *i64, n: i64, k: *i64, order: i64, out: *i64) -> i64 |
| 157 | func vt_species_defaults(tbl: *i64) -> i64 |
| 218 | func vt_species_areas(tbl: *i64, sp: i64, nsec: i64, areas: *i64) -> i64 called by 1: vt_speak |
| 239 | func vt_f1_uniform(len_mm: i64) -> i64 |
| 245 | func vt_speak(tbl: *i64, sp: i64, exc: *i64, n: i64, rate: i64, out: *i64) -> i64 |