Derive live tract waveguide reflections

This commit is contained in:
Meta Vens 2026-05-25 21:27:02 +01:00
commit c85c213992
7 changed files with 57 additions and 19 deletions

View file

@ -68,6 +68,9 @@ named failure against Pink Trombone, but it does not own the anatomy:
- Consumes area/reflection fields and source/injection events.
- First oral-tube lowering exists: generated right/left section equations
consume `tract_shape` reflection coefficients and boundary reflections.
- Waveguide lowering now derives live per-section diameter targets, areas,
and reflection coefficients from the tract shape plus tongue, constriction,
and lip controls before scattering.
True substep execution remains missing.
6. `tract_motion`: control-rate slew and obstruction history.
@ -94,10 +97,11 @@ pressure instead of silently inventing all of them inside one helper. The
waveguide lowering consumes the derived oral reflection field as right/left
section state equations.
The next cut is timing and proof: the oral/nasal tube now consumes the full
reflection coefficient fields, distributes frication into section updates, and
derives release bursts from obstruction history. PT's two tract steps per
output sample and rendered parity fixtures still need low-level owners.
The next cut is timing and proof: the oral/nasal tube now derives moving
per-section diameter/area/reflection fields, distributes frication into section
updates, and derives release bursts from obstruction history. PT's two tract
steps per output sample and rendered parity fixtures still need low-level
owners.
## Cut Line

View file

@ -75,7 +75,6 @@ Current AquaSynth can express:
Current AquaSynth cannot exactly express:
- per-section dynamic diameter target arrays;
- a subgraph that steps twice per output sample;
- exact Pink Trombone LF glottal coefficient updates.
@ -103,6 +102,8 @@ Trombone anatomy:
position and width when waveguide propagation is active.
- `tract_motion` smooths tract controls and lets the waveguide derive burst
pressure from obstruction history.
- waveguide lowering derives live per-section diameter targets, areas, and
reflection coefficients from shape and gesture controls.
- The proxy script parses and emits Faust with live controls, but its missing
features explicitly name the waveguide authorities AquaSynth does not own.
- `tools/vocal-tract-playground` exposes the same control surface for fast

View file

@ -398,6 +398,7 @@ public static class FaustEmitter
var tenseness = $"clip01({parameters.Expression(OwnerField(tractPath, "tenseness"), tract.Tenseness)})";
var motion = tract.Motion;
var diameterSlew = parameters.Expression(OwnerField(tractPath, "motion/diameter_slew"), motion?.DiameterSlewPerSecond ?? 18);
var shapeReturn = parameters.Expression(OwnerField(tractPath, "motion/shape_return"), motion?.ShapeReturnPerSecond ?? 8);
var constrictionSlew = parameters.Expression(OwnerField(tractPath, "motion/constriction_slew"), motion?.ConstrictionSlewPerSecond ?? 24);
var velumSlew = parameters.Expression(OwnerField(tractPath, "motion/velum_slew"), motion?.VelumSlewPerSecond ?? 16);
var obstructionThreshold = parameters.Expression(OwnerField(tractPath, "motion/obstruction_threshold"), motion?.ObstructionThreshold ?? 0.05f);
@ -465,7 +466,21 @@ public static class FaustEmitter
source.AppendLine($"{name}_tract_raw = {name}_tract_excitation + {name}_tract_injection_pressure;");
if (tract.Propagation == TractPropagationMode.Waveguide && areaFunction is not null)
{
return TractWaveguideExpression(source, tract, areaFunction, name, glottalReflection, lipReflection, velum, injectionWidth);
return TractWaveguideExpression(
source,
tract,
areaFunction,
name,
glottalReflection,
lipReflection,
velum,
injectionWidth,
tongueIndex,
tongueDiameter,
constrictionIndex,
constrictionDiameter,
lipOpening,
shapeReturn);
}
source.AppendLine($"{name}_tract_oral = {name}_tract_raw * 0.18 + ({name}_tract_raw : fi.resonbp({name}_tract_f1, {name}_tract_q, 1.0)) * (0.75 + {name}_tract_lip * 0.65) + ({name}_tract_raw : fi.resonbp({name}_tract_f2, {name}_tract_q, 1.0)) * (0.85 + {name}_tract_tongue_close) + ({name}_tract_raw : fi.resonbp({name}_tract_f3, {name}_tract_q, 1.0)) * (0.35 + {name}_tract_constriction_close);");
@ -489,22 +504,37 @@ public static class FaustEmitter
string glottalReflection,
string lipReflection,
string velum,
string injectionWidth)
string injectionWidth,
string tongueIndex,
string tongueDiameter,
string constrictionIndex,
string constrictionDiameter,
string lipOpening,
string shapeReturn)
{
var sections = areaFunction.Sections;
var reflections = areaFunction.ReflectionCoefficients;
var loss = F(Math.Clamp(tract.WaveguideLoss, 0, 1));
source.AppendLine($"{name}_wg_loss = {loss};");
source.AppendLine($"{name}_wg_substeps = {Math.Max(1, tract.Substeps)};");
source.AppendLine($"{name}_wg_injection_cell = {name}_tract_injection_pos * {F(sections)};");
for (var i = 0; i < reflections.Count; i++)
{
source.AppendLine($"{name}_wg_k_{i + 1} = {F(reflections[i])};");
}
for (var i = 0; i < sections; i++)
{
var rest = F(areaFunction.Diameters[i]);
var tongueWidth = F(Math.Max(1, sections * 0.18));
var constrictionWidth = F(Math.Max(1, sections * 0.09));
var target = i == sections - 1
? lipOpening
: $"{rest} + ({tongueDiameter} - {rest}) * exp(-pow(({F(i)} - {tongueIndex}) / {tongueWidth}, 2.0))";
target = $"min({target}, {constrictionDiameter} + max(0.0, ({target}) - {constrictionDiameter}) * (1.0 - exp(-pow(({F(i)} - {constrictionIndex}) / {constrictionWidth}, 2.0))))";
source.AppendLine($"{name}_wg_diameter_target_{i} = max(0.0, {target});");
source.AppendLine($"{name}_wg_diameter_{i} = slew({shapeReturn}, {name}_wg_diameter_target_{i});");
source.AppendLine($"{name}_wg_area_{i} = max(0.000001, {name}_wg_diameter_{i} * {name}_wg_diameter_{i});");
source.AppendLine($"{name}_wg_inject_{i} = {name}_tract_injection_pressure * clip01(1.0 - abs({name}_wg_injection_cell - {F(i)}) / {injectionWidth});");
}
for (var i = 1; i < sections; i++)
{
source.AppendLine($"{name}_wg_k_{i} = ({name}_wg_area_{i - 1} - {name}_wg_area_{i}) / ({name}_wg_area_{i - 1} + {name}_wg_area_{i});");
}
var nasal = tract.Nasal is { AreaFunction: { } nasalShape } ? tract.Nasal : null;
var junction = nasal is null ? -1 : Math.Clamp(nasal.JunctionIndex, 1, sections - 2);
@ -528,17 +558,15 @@ public static class FaustEmitter
var noseReflections = nasalFunction.ReflectionCoefficients;
var noseLoss = F(Math.Clamp(nasal.Loss, 0, 1));
var noseReflection = F(nasal.Reflection);
var leftArea = F(areaFunction.Areas[Math.Clamp(junction - 1, 0, areaFunction.Areas.Count - 1)]);
var rightArea = F(areaFunction.Areas[Math.Clamp(junction, 0, areaFunction.Areas.Count - 1)]);
source.AppendLine($"{name}_nose_loss = {noseLoss};");
for (var i = 0; i < noseReflections.Count; i++)
{
source.AppendLine($"{name}_nose_k_{i + 1} = {F(noseReflections[i])};");
}
source.AppendLine($"{name}_nose_area = max(0.000001, (0.01 + {velum} * 0.39) * (0.01 + {velum} * 0.39));");
source.AppendLine($"{name}_nose_sum = {leftArea} + {rightArea} + {name}_nose_area;");
source.AppendLine($"{name}_nose_reflect_left = (2.0 * {leftArea} - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($"{name}_nose_reflect_right = (2.0 * {rightArea} - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($"{name}_nose_sum = {name}_wg_area_{junction - 1} + {name}_wg_area_{junction} + {name}_nose_area;");
source.AppendLine($"{name}_nose_reflect_left = (2.0 * {name}_wg_area_{junction - 1} - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($"{name}_nose_reflect_right = (2.0 * {name}_wg_area_{junction} - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($"{name}_nose_reflect_nose = (2.0 * {name}_nose_area - {name}_nose_sum) / {name}_nose_sum;");
source.AppendLine($"{name}_wg_l_{junction - 1} = ((({name}_nose_reflect_left * {name}_wg_r_{junction - 1}) + (1.0 + {name}_nose_reflect_left) * ({name}_wg_l_{junction} + {name}_nose_l_0) + {name}_wg_inject_{junction - 1} * 0.5) * {name}_wg_loss) : mem;");
source.AppendLine($"{name}_wg_r_{junction} = ((({name}_nose_reflect_right * {name}_wg_l_{junction}) + (1.0 + {name}_nose_reflect_right) * ({name}_wg_r_{junction - 1} + {name}_nose_l_0) + {name}_wg_inject_{junction} * 0.5) * {name}_wg_loss) : mem;");

View file

@ -71,6 +71,7 @@ public static class PinkTromboneReference
new("nose_junction", "generated three-way junction", "Waveguide lowering can emit a velum-controlled oral/nasal junction from local areas."),
new("positioned_turbulence_applied_to_waveguide_cells", "position-weighted waveguide injection", "Frication pressure can be distributed into oral waveguide section updates by injection position and width."),
new("tract_shape_motion", "tract_motion slew semantics", "AquaSynth can author tract control slew and obstruction thresholds for moving diameter targets."),
new("diameter_authority", "dynamic waveguide section diameters", "Waveguide lowering derives live per-section diameter targets before recalculating section areas and reflection coefficients."),
new("closure_transients", "obstruction-history burst", "Waveguide lowering derives burst pressure from prior obstruction state and current opening."),
new("runtime_controls", "/pink/frequency,/pink/intensity,/pink/tenseness,/pink/tongue/index,/pink/tongue/diameter,/pink/velum,/pink/turbulence", "AquaSynth can expose stable runtime parameter paths."),
new("glottal_pitch_pressure_proxy", "tract LF-style source", "AquaSynth can drive pitch/loudness-like controls without recompiling."),
@ -79,7 +80,7 @@ public static class PinkTromboneReference
],
[
new("twice_rate_tract_stepping", "two tract steps per audio sample", "AquaSynth voice graphs do not express a subgraph with its own stepping rate."),
new("lf_glottal_exactness", "Pink Trombone LF-style coefficients", "The proxy uses ordinary oscillators/noise, not the LF coefficient path from tenseness.")
new("lf_glottal_exactness", "Pink Trombone LF-style coefficients", "The proxy uses an LF-like glottal source but does not yet match Pink Trombone coefficient updates exactly.")
],
"This is a deliberately non-passing rebuild: it keeps the current source-filter proxy parseable while naming the tract-DSP authority AquaSynth lacks.");
}

View file

@ -135,3 +135,4 @@
{"date":"2026-05-26","topic":"pink-trombone-low-level-dsp-primitives","lesson":"PT parity must be targeted at the low-level DSP loop: right/left traveling-wave state, reflection coefficients, scattering junctions, nasal branch junctions, and injected excitation. AquaSynth now has reusable primitives for tract shape, glottal source, tract injection, nasal branch, and generated oral/nasal waveguide lowering; remaining gaps are true twice-rate stepping, cell-local turbulence/transient injection, target diameter slew, and rendered PT fixture scoring.","evidence":"Added `GlottalSource`, `TractInjection`, `NasalBranch`, `TractPropagationMode.Waveguide`, parser commands `glottis`, `tract_injection`, `nasal_branch`, and Faust generation for oral right/left waveguide sections plus a velum-controlled three-way nose junction. Focused verification: `dotnet build tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -v minimal` and focused `dotnet test --no-build` passed."}
{"date":"2026-05-26","topic":"waveguide-cell-injection","lesson":"A positioned injection primitive is not enough; frication has to enter the traveling-wave section updates. AquaSynth waveguide lowering now computes a section-space injection cell and distributes injection pressure across oral waveguide right/left updates by position and width. Remaining PT DSP gaps are true substep execution, obstruction-history transients, diameter target slew, and rendered parity fixtures.","evidence":"Changed Faust waveguide lowering to split glottal excitation from tract injection pressure, emit `wg_injection_cell` and `wg_inject_N` terms, and add those terms to oral waveguide section and nasal-junction updates. Verification required disabling build servers/shared compilation/parallel build after normal dotnet builds hung: `dotnet build tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false -v minimal`; focused `dotnet test --no-build` passed 3 tests."}
{"date":"2026-05-26","topic":"tract-motion-obstruction-history","lesson":"PT-style movement and release bursts are temporal DSP semantics, not preset flavor. AquaSynth now has `tract_motion` to own slew rates and obstruction threshold; waveguide lowering smooths tract controls and derives burst pressure from previous obstruction state opening into current non-obstruction. Remaining PT expression gaps are true two-steps-per-sample execution, dynamic per-section target diameter arrays, and rendered parity fixtures.","evidence":"Added `TractMotion`, parser command `tract_motion`, voice references/inline overrides, Faust `slew(rate,x)`, smoothed tongue/constriction/velum controls, `tract_prev_injection_close`, `tract_obstructed`, and `tract_release_memory` burst generation. Verification: no-hang build/test command with `--disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false`; focused tests passed 3."}
{"date":"2026-05-26","topic":"dynamic-tract-reflection-field","lesson":"A PT-class waveguide cannot keep reflection coefficients as compile-time constants. AquaSynth waveguide lowering now derives live per-section diameter targets from `tract_shape`, tongue, constriction, and lip controls, slews them through `tract_motion`, squares them into section areas, and recomputes reflection coefficients and nasal junction areas as DSP expressions.","evidence":"Replaced static `wg_k_N` constants in `FaustExport.TractWaveguideExpression` with generated `wg_diameter_target_N`, `wg_diameter_N`, `wg_area_N`, and dynamic `wg_k_N` expressions. Focused no-hang build/test command passed 3 tests."}

View file

@ -112,7 +112,7 @@ live_invariants:
- The speech lane is the active center: IPA parity, utterance embeddings, synth-driver automation, rendered loss, and compiled Faust control sweeps take priority over new DX7/Zyn digestion.
- Every learned speech arrow must stay inspectable: utterance metadata -> embedding, embedding plus phonetic features -> automation, automation -> rendered audio, rendered audio -> loss/gradient.
- Pink Trombone parity is structural waveguide pressure, not permission to add more formant proxy knobs. AquaSynth now has a `tract` DSL command that lowers into `Voice.Tract` with PT-shaped runtime controls and a Faust tract-voice proxy; it is still an ordinary voice treatment, not a separate graph owner and not an exact 44-cell/28-cell bidirectional waveguide.
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns section diameters/areas and derived reflection coefficients; `glottis` owns excitation controls; `tract_injection` owns positioned frication/burst controls and now distributes frication pressure into waveguide section updates; `nasal_branch` owns a second diameter/area tube; `tract_motion` owns control slew and obstruction threshold; `propagation=waveguide` lowers tract voices into generated oral/nasal right-left waveguide equations with obstruction-history burst pressure. Remaining low-level PT gaps are true two-steps-per-sample execution, dynamic per-section target diameter arrays, and parity fixtures.
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns section diameters/areas and derived reflection coefficients; `glottis` owns excitation controls; `tract_injection` owns positioned frication/burst controls and now distributes frication pressure into waveguide section updates; `nasal_branch` owns a second diameter/area tube; `tract_motion` owns control slew and obstruction threshold; `propagation=waveguide` lowers tract voices into generated oral/nasal right-left waveguide equations with live per-section diameter/area/reflection fields and obstruction-history burst pressure. Remaining low-level PT gaps are true two-steps-per-sample execution and rendered parity fixtures.
- If dotnet builds hang in this workspace, stop orphan dotnet workers and retry with `--disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false`; plain parallel/shared builds can stall without useful diagnostics.
- eSpeak NG is an early parity reference for timing, IPA coverage, and log-mel pressure, not an anatomy oracle or final target.
- Do not expand into prosody, emotional context, personality, or broad Weksa expressivity until the tiny IPA utterance harness can show useful loss movement.

View file

@ -137,6 +137,8 @@ public sealed class PatchScriptTests
Assert.Contains("tract_injection_close", faust);
Assert.Contains("wg_injection_cell", faust);
Assert.Contains("wg_inject_6", faust);
Assert.Contains("wg_diameter_target_3", faust);
Assert.Contains("wg_area_3", faust);
Assert.Contains("tract_release_memory", faust);
Assert.Contains("tract_obstructed", faust);
Assert.Contains("slew(", faust);
@ -379,6 +381,7 @@ public sealed class PatchScriptTests
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "nose_junction");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "positioned_turbulence_applied_to_waveguide_cells");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "tract_shape_motion");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "diameter_authority");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "closure_transients");
Assert.NotEmpty(patch.TractShapes);
Assert.NotEmpty(patch.GlottalSources);