Derive live tract waveguide reflections
This commit is contained in:
parent
ae57fa96be
commit
c85c213992
7 changed files with 57 additions and 19 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;");
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue