code wiki / _hdl_build / nx_vox_source.nx
nx_vox_source.nx
buildroot/runtime/_hdl_build/nx_vox_source.nx
about
nx_vox_source.nx -- a SELF-OSCILLATING physical voice source. Rung R2 of the bioacoustics lane.
WHAT MAKES THIS DIFFERENT FROM EVERY OTHER AUDIO ORGAN IN THE TREE. nx_audio_bark, nx_tts_formant,
nx_phoneme_synth and friends are all SOURCE-FILTER synthesisers: something generates a pulse train at
a frequency you tell it, and a filter bank shapes it. The frequency is an input. Nothing oscillates
on its own, so nothing can do what real animal larynges do -- jump registers, split into two pitches,
break into chaos -- unless a human scripts each event by hand. That is why nx_audio_bark's header
says "tune by ear": there is nothing to measure because there is no physics.
Here the frequency is an OUTPUT. This is a mass-spring-damper model of two vocal folds, driven by
nothing but a constant subglottal pressure, and it oscillates because of the aerodynamics:
* Each fold is two coupled masses (lower m1, upper m2) -- the standard two-mass idealisation of the
body-cover structure of a real fold.
* The glottal areas are a1 = g0 + xL1 + xR1 and a2 = g0 + xL2 + xR2.
* Bernoulli gives the driving pressure P1 = Ps * (1 - (amin/a1)^2) where amin is the narrowest
point. When the glottis is CONVERGENT (a2 < a1) the narrowest point is downstream, pressure
inside the glottis stays high, and the folds are pushed APART. When it is DIVERGENT (a1 < a2)
the flow separates at the entry, P1 collapses to zero, and the springs pull the folds shut.
* Because m2 lags m1, the glottis is convergent while opening and divergent while closing. Energy
goes in during opening and not during closing. That asymmetry IS the oscillation -- it is why a
larynx works, and it is the whole reason this file exists.
EVERYTHING THE RULER MEASURES FALLS OUT OF THE PHYSICS, unscripted:
* f0 emerges from mass and stiffness; it is never written down anywhere.
* SUBHARMONICS appear when the two folds entrain at 2:1 instead of 1:1.
* BIPHONATION appears when left and right folds are detuned enough to stop entraining and each
runs at its own rate. This is why the model is left/right asymmetric rather than a
single fold mirrored -- a symmetric model can never produce it.
* CHAOS appears at high subglottal pressure, where the collision nonlinearity dominates.
* NON-REPETITION comes from turbulent pressure jitter, so no two cycles are ever identical. A dog
habituates to a sample loop precisely because a loop cannot do this.
The claim "these phenomena are emergent" is not an assertion in a comment -- nx_vox_source_gate
SWEEPS asymmetry and pressure and shows the regimes appearing in the order the physics predicts, as
measured by nx_bioacoustic_bench, which was built and frozen BEFORE this file existed.
Integer fixed-point throughout (Q16), explicit Euler at an oversampled step. Deterministic: the same
parameters give bit-identical audio, which is what makes a regression baseline meaningful.
license_tier: ORIGINAL expect_exit: 0
dependencies 2 imports · 1 importers
imports: nx_syscalls.nxnx_isqrt.nx
imported by: nx_squeaker.nx
structs
| none |
consts
| 45 | const VF_Q: i64 = 65536 |
| 46 | const VF_PI2: i64 = 411774 // 2*pi in Q16 -- same constant the rest of the audio family uses |
| 49 | const VF_P_F0_L: i64 = 0 // Hz natural frequency of the LEFT fold |
| 50 | const VF_P_F0_R: i64 = 1 // Hz natural frequency of the RIGHT fold (detune -> biphonation) |
| 51 | const VF_P_ZETA: i64 = 2 // Q16 damping ratio |
| 52 | const VF_P_KCOUP: i64 = 3 // Q16 spring coupling between the lower and upper mass of one fold |
| 53 | const VF_P_G0: i64 = 4 // Q16 rest half-gap; negative means the folds rest pressed together |
| 54 | const VF_P_PS: i64 = 5 // Q16 subglottal pressure -- the ONLY energy input |
| 55 | const VF_P_KCOLL: i64 = 6 // Q16 extra stiffness when the folds collide (the closure nonlinearity) |
| 56 | const VF_P_F2RATIO: i64 = 7 // Q16 upper-mass natural frequency as a fraction of the lower |
| 57 | const VF_P_DRIVE: i64 = 8 // Q16 aerodynamic force gain |
| 58 | const VF_P_OVS: i64 = 9 // integration steps per audio sample |
| 59 | const VF_P_SEED: i64 = 10 // jitter RNG seed |
| 60 | const VF_P_JITTER: i64 = 11 // Q16 turbulent pressure fluctuation -- why no two cycles repeat |
| 69 | const VF_P_G0_2: i64 = 12 // Q16 |
| 70 | const VF_P_N: i64 = 13 |
| 73 | const VF_S_XL1: i64 = 0 |
| 74 | const VF_S_VL1: i64 = 1 |
| 75 | const VF_S_XL2: i64 = 2 |
| 76 | const VF_S_VL2: i64 = 3 |
| 77 | const VF_S_XR1: i64 = 4 |
| 78 | const VF_S_VR1: i64 = 5 |
| 79 | const VF_S_XR2: i64 = 6 |
| 80 | const VF_S_VR2: i64 = 7 |
| 81 | const VF_S_RNG: i64 = 8 |
| 82 | const VF_S_N: i64 = 9 |
| 98 | const VF_K_WL1: i64 = 0 // Q16 w*dt for the lower-left mass |
| 99 | const VF_K_WL2: i64 = 1 |
| 100 | const VF_K_WR1: i64 = 2 |
| 101 | const VF_K_WR2: i64 = 3 |
| 102 | const VF_K_RL1: i64 = 4 // Q16 2*zeta*w*dt |
| 103 | const VF_K_RL2: i64 = 5 |
| 104 | const VF_K_RR1: i64 = 6 |
| 105 | const VF_K_RR2: i64 = 7 |
| 106 | const VF_K_N: i64 = 8 |
| 107 | const VF_XCLAMP: i64 = 16 * 65536 // +-16.0 displacement; a divergent solve is bounded, not loud |
| 291 | const VF_D_CLOSURES: i64 = 0 |
| 292 | const VF_D_OPEN: i64 = 1 |
| 293 | const VF_D_CLAMP: i64 = 2 |
| 294 | const VF_D_N: i64 = 3 |
functions
| 109 | func vfold_defaults(p: *i64) -> i64 called by 1: sq_render |
| 128 | func vfold_init(p: *i64, k: *i64, s: *i64, rate: i64) -> i64 |
| 152 | func vfold_rng(s: *i64) -> i64 called by 1: vfold_substep |
| 163 | func vfold_substep(s: *i64, p: *i64, k: *i64) -> i64 |
| 264 | func vfold_render(out: *i64, n: i64, rate: i64, p: *i64, s: *i64, k: *i64) -> i64 |
| 296 | func vfold_render_diag(out: *i64, n: i64, rate: i64, p: *i64, s: *i64, k: *i64, diag: *i64) -> i64 calls 1: vfold_substep |
| 336 | func vfold_render_audio(out: *i64, n: i64, rate: i64, p: *i64, amp: i64) -> i64 |