Keep physical branch admittance ownership

This commit is contained in:
Meta Vens 2026-05-27 17:55:29 +01:00
commit ac33a38e92
6 changed files with 93 additions and 31 deletions

View file

@ -67,21 +67,18 @@ For a research-backed waveguide junction, nasal opening should affect the side
port admittance. The two oral traveling-wave ports should keep clean continuity
through the main tube while the nasal port steals energy according to its area.
The current generated PT graph still uses `AcousticConnection.Coupling` as a
global connection scalar. That scalar damps every port in the connection,
including the main oral path. It accidentally prevents flooding, but it also
means velopharyngeal coupling is partly acting as a main-airway clamp.
Generated PT graphs now move the nasal limit onto the nasal terminal and keep
the connection itself fully coupled. This preserves the physical ownership:
velopharyngeal opening is side-port admittance, not a main-airway clamp.
A test cut moved the limit onto the nasal terminal and restored full connection
coupling. That is closer to the research shape, but the current connection law
then over-radiated the body by roughly 3-5x RMS on the utterance fixtures. Even
shrinking nasal admittance did not fix it. The failure is therefore in the
multi-port lowering/radiation normalization, not in the DSL's ability to name
the anatomy.
That change makes the broken downstream law louder. Current utterance fixtures
over-radiate by roughly 3-5x RMS and articulation worsens. This is not evidence
that side-port admittance is wrong; it is evidence that connection scattering
and radiation normalization were relying on the old global damper.
Required cut: connection scattering needs per-port admittance authority and
passivity checks. Generated nasal branches should not rely on global connection
coupling as a gain patch.
Required cut: connection scattering needs passivity checks and radiation needs
normalization against local port admittance/boundary aperture. Generated nasal
branches must not rely on global connection coupling as a gain patch.
### Transformed Control Bindings
@ -143,16 +140,21 @@ scattering.
## Next Cut
The next coherent implementation target is a passive three-port branch law:
The live implementation now has a named three-port branch law for connection
groups with two same-path ports plus one side-branch port. It is intentionally
kept even though it exposes over-radiation, because it moves ownership to the
physically correct port.
1. Build a named lowering path for connection groups with two same-path ports
plus one branch port.
2. Use segment areas for the two through-path ports and terminal area for the
side port.
3. Apply branch opening/coupling to the side-port admittance only.
4. Keep the main path continuous when side admittance approaches zero.
5. Assert this with generated Faust structure tests before re-golfing PT
utterances.
The next coherent implementation target is to make that law passive and
radiation-aware:
1. Normalize branch scattering by power/admittance, not by a global damping
scalar.
2. Keep the main path continuous when side admittance approaches zero.
3. Normalize radiation against local aperture/admittance so removing a damper
does not become raw loudness.
4. Add graph-level probes for per-port incoming/outgoing flow before further
PT audio golf.
If that law still floods, radiation normalization should be audited at the same
layer. Do not fix this with another global gain.

View file

@ -710,6 +710,11 @@ public static class FaustEmitter
}
var safeConnection = SafeIdentifier(connection.Name);
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports, sources))
{
continue;
}
var connectionPressure = ConnectionPressureExpression(name, connection, ports);
var connectionMaxArea = MaxExpression(ports.Select(item => ConnectionPortAreaExpression(name, item.terminal, item.nodePortCount)));
source.AppendLine($" {name}_graph_connection_pressure_{safeConnection} = {connectionPressure};");
@ -1589,6 +1594,60 @@ public static class FaustEmitter
private static string ConnectionPortAreaExpression(string voiceName, AcousticTerminal terminal, int nodePortCount) =>
$"({voiceName}_graph_terminal_area_{SafeIdentifier(terminal.Name)} / {F(Math.Max(1, nodePortCount))})";
private static bool TryEmitThreePortBranchScatter(
StringBuilder source,
string voiceName,
AcousticConnection connection,
string safeConnection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports,
IReadOnlyDictionary<string, AcousticSourcePort> sources)
{
if (ports.Count != 3 || connection.Law != AcousticConnectionLaw.AreaScattering)
{
return false;
}
var pathGroups = ports
.GroupBy(item => item.port.Segment.Path.Name, StringComparer.OrdinalIgnoreCase)
.OrderByDescending(group => group.Count())
.ToList();
if (pathGroups.Count != 2 || pathGroups[0].Count() != 2 || pathGroups[1].Count() != 1)
{
return false;
}
var through = pathGroups[0].ToList();
if (!through[0].node.Name.Equals(through[1].node.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var side = pathGroups[1].Single();
var portItems = new List<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)>(through)
{
side
};
var areaExpressions = new Dictionary<AcousticGraphPort, string>();
foreach (var item in through)
{
areaExpressions[item.port] = GraphPortArea(voiceName, item.port);
}
areaExpressions[side.port] = ConnectionPortAreaExpression(voiceName, side.terminal, side.nodePortCount);
var areaSum = string.Join(" + ", portItems.Select(item => areaExpressions[item.port]));
var weightedIncoming = string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * {GraphIncoming(voiceName, item.port)}"));
source.AppendLine($" {voiceName}_graph_connection_pressure_{safeConnection} = 2.0 * ({weightedIncoming}) / max(0.000001, {areaSum});");
foreach (var item in portItems)
{
var incoming = GraphIncoming(voiceName, item.port);
var outgoing = GraphOutgoing(voiceName, item.port);
var sourceInjection = NodeSourceExpression(voiceName, item.node, sources);
source.AppendLine($" {outgoing} = ({voiceName}_graph_connection_pressure_{safeConnection} - {incoming} + ({sourceInjection}) / {F(Math.Max(1, item.nodePortCount))}) * {voiceName}_graph_connection_loss_{safeConnection};");
}
return true;
}
private static bool TryEmitTwoPortPathScatter(
StringBuilder source,
string voiceName,

View file

@ -1417,7 +1417,7 @@ public static class PatchScript
0,
AcousticBranchKind.Nasal,
nasal.Velum,
0.15f,
1,
true);
AddAcousticBranchRecord(branch);
AddAcousticTerminalRecord(new AcousticTerminal(
@ -1433,7 +1433,7 @@ public static class PatchScript
branch.ToPosition,
AcousticTerminalKind.Junction,
branch.Name,
MathF.Pow(Math.Max(0, branch.Opening), 2) / Math.Max(0.000001f, nasalArea.Diameters[0] * nasalArea.Diameters[0])));
0.15f * MathF.Pow(Math.Max(0, branch.Opening), 2) / Math.Max(0.000001f, nasalArea.Diameters[0] * nasalArea.Diameters[0])));
AddAcousticConnectionRecord(new AcousticConnection(
BranchConnectionName(branch),
[BranchFromTerminalName(branch), BranchToTerminalName(branch)],

View file

@ -178,4 +178,4 @@
{"evidence":"Added `AudioArticulationComparison` to `AudioComparison` and updated Pink Trombone parity reports with envelope cosine, active-frame ratio, silence mismatch, envelope/spectral flux ratios, motor-band ratio, speech-band ratio, articulation score, and utterance verdicts. Focused tests passed. Latest utterance artifact artifacts/parity/pink-trombone-utterance-logmel/20260527T130026316 reports mama/papa/thrombosis as `not-accepted-articulation` with articulation scores 0.1930/0.1829/0.1848 despite log-mel cosine 0.5712/0.8659/0.4379. Static reports also expose weak speech-band ratios instead of hiding behind cosine.","claim":"Log-mel cosine is a spectral witness, not a speech-articulation authority. The parity harness must surface silence-map, envelope, flux, motor-band, and speech-band failures so filtered buzz cannot forge a listening receipt.","timestamp":"2026-05-27T13:05:00+01:00","topic":"voice-articulation-metric-witness"}
{"evidence":"Segment delay lowering no longer clamps physical acoustic graph segments to one sample before wave-clock lowering; `WaveClockPolicy` now owns the legal delay floor. Generated `tract propagation=graph` patches use `HalfSampleGrid` instead of first-order Thiran, matching the PT/Kelly-Lochbaum half-sample regime. Focused graph, Faust validation, utterance, and static parity tests passed. Latest utterance artifact artifacts/parity/pink-trombone-utterance-logmel/20260527T133539554: mama cosine/articulation 0.5982/0.1952, papa 0.8615/0.1979, thrombosis 0.5217/0.2640. Latest static artifact artifacts/parity/pink-trombone-logmel/20260527T133424894. Re-testing the literal SndKit reflection sign made thrombosis loud/unstable; adding a broader glottal harmonic tail worsened voiced-band behavior, so both were cut.","claim":"Physical tract length must lower to sub-sample graph delays; forcing each section to one sample made the tract acoustically overlong and hid failures behind a low resonance crutch. Half-sample graph timing is the correct next foundation, but articulation still fails because body/radiation and pressure-release behavior are incomplete.","timestamp":"2026-05-27T13:40:00+01:00","topic":"acoustic-graph-half-sample-clock"}
{"evidence":"Focused PT tests passed. Utterance artifact artifacts/parity/pink-trombone-utterance-logmel/20260527T150836912: mama/papa/thrombosis cosine 0.8337/0.8893/0.5317, rmsRatio 1.0768/1.5626/0.4004, articulation 0.2118/0.1890/0.2594. Static artifact artifacts/parity/pink-trombone-logmel/20260527T150749857: open/front/nasal/ma/sibilant/closure cosine 0.6942/0.6732/0.7386/0.7067/0.4548/0.5449. A long noisy release transient was cut because it drove papa motor-band ratio to 47.0313.","claim":"Generated graph nasal branches no longer mirror velum into connection coupling. Velum owns nasal entrance area and nostril aperture; `AcousticConnection.Coupling` is a structural max-admittance limit. A fixed 0.15 generated nasal coupling avoids the previous double gate without flooding the graph. `TurbulenceJet` transient lowering now emits a short pressure pulse rather than filtered noise, but true plosive intelligibility still needs graph pressure storage/release around severe closures.","timestamp":"2026-05-27T15:03:00+01:00","topic":"pink-trombone-nasal-coupling-authority"}
{"evidence":"Added docs/acoustic-graph-flow-audit.md after comparing current Aqua graph lowering with VocalTractLab, Story/TubeTalker, Julius Smith waveguide modeling, ArtiSynth, and PT/SndKit pressure. Tested moving nasal coupling from global connection damping to side-port admittance and tested a special three-port branch scatter; both compiled but over-radiated utterances around 3-5x RMS, with mama/papa/thrombosis articulation falling to roughly 0.173/0.176/0.141, so both code experiments were cut. The audit identifies the live fault as lowering/radiation normalization and transformed admittance, not missing DSL vocabulary.","claim":"Aqua's graph records can express the research-backed vocal-tract architecture, but current Faust lowering still mangles samples at multi-port branch junctions by using global connection coupling as an accidental damper. The next coherent cut is transformed side-port admittance plus passive three-port/radiation normalization tests, not more PT-shaped source tweaks.","timestamp":"2026-05-27T16:12:00+01:00","topic":"acoustic-graph-flow-audit"}
{"evidence":"Added docs/acoustic-graph-flow-audit.md after comparing current Aqua graph lowering with VocalTractLab, Story/TubeTalker, Julius Smith waveguide modeling, ArtiSynth, and PT/SndKit pressure. Restored the physical ownership fix: generated nasal branches keep full connection coupling, put the side-port admittance limit on the nasal terminal, and emit a named three-port branch scatter for two same-path ports plus one side branch. Focused structural/Faust and PT parity tests passed. Latest utterance artifact artifacts/parity/pink-trombone-utterance-logmel/20260527T165339231: mama/papa/thrombosis cosine 0.7973/0.8895/0.5398, rmsRatio 3.7320/4.8039/2.9997, articulation 0.1734/0.1766/0.1410. Latest static artifact artifacts/parity/pink-trombone-logmel/20260527T165305967: open/front/nasal/ma/sibilant/closure cosine 0.6157/0.6879/0.6338/0.6089/0.4868/0.6525. The utterance over-radiation is kept as evidence of downstream passivity/radiation-normalization debt, not grounds to revert the physical ownership fix.","claim":"Aqua's graph records can express the research-backed vocal-tract architecture, but current Faust lowering still mangles samples at multi-port branch junctions because radiation normalization and passivity were leaning on a global connection damper. The next coherent cut is passive/admittance-normalized three-port scattering plus radiation normalization probes, not more PT-shaped source tweaks or reverting the side-port ownership fix.","timestamp":"2026-05-27T16:12:00+01:00","topic":"acoustic-graph-flow-audit"}

View file

@ -119,8 +119,8 @@ live_invariants:
- The physical voice research packet now includes a practical implementation/researcher map (`research/physical-voice-modeling/PEOPLE.md`) and talk/transcript ledger (`research/physical-voice-modeling/talks/TALKS.md`). Birkholz/VocalTractLab pressures multi-level articulatory control and gestures; Story/TubeTalker pressures continuous parametric area functions and length/coupling; Smith pressures bidirectional delay-line scattering and fractional-delay waveguides; Fels/ArtiSynth pressures separation between biomechanics, acoustics, timeline controls, and diagnostics. These are reusable DSL semantics, not modules to clone.
- Physical voice implementation has started moving PT pressure into neutral acoustic owners: `AcousticPath`, `AcousticTerminal`, `AcousticConnection`, `AcousticSourcePort`, `AcousticBranch`, `AcousticRadiationPort`, `WaveClockPolicy`, and `AcousticPortNetwork`. Parser commands `path`, `terminal`, `connect`, `source_port`, `branch`, `radiation_port`, `wave_clock`, and `acoustic_network` now exist. Existing `tract_shape`, `glottis`, `tract_injection`, `nasal_branch`, and `tract` commands populate acoustic records as compatibility aliases while the current `Voice.Tract` Faust renderer still owns audio.
- `acoustic` / `acoustic_voice` can now render an `AcousticPortNetwork` through a compact Faust response proxy or, when typed terminals are present, a compiled bidirectional Faust segment graph. Graph lowering splits paths at terminal positions, aggregates co-located terminals into graph nodes, orders connection ports deterministically, scatters area-weighted/pressure/bypass connection groups, injects source terminals even at connected nodes, and sums radiation terminals. Segment delay follows physical segment length and the network `wave_clock` strategy through unit-grid, half-sample, linear fractional, Lagrange, or Thiran/allpass Faust delay primitives. Source, branch, radiation, terminal, and connection controls can bind through ordinary Aqua parameters into emitted Faust. PT-shaped `tract` authoring now routes accepted parity through `propagation=graph`; the old waveguide parity lane is not a comparison baseline or historical pressure lane.
- Pink Trombone log-mel parity now writes graph candidates only. Generated tract graphs mirror live `/pink/...` tract bindings into acoustic source, terminal, radiation, and path-area fields. Graph generation stopped baking first-frame tongue/constriction/lip morphology into the path; `AcousticPath.AreaControl` now turns tongue/constriction/lip controls into live area expressions at graph sample points. Velum owns nasal branch entrance area/aperture, while connection coupling is a structural admittance limit and no longer mirrors velum as a second gate. The first nasal tube sample is no longer allowed to decide the velum opening. Graph radiation now uses aperture as a boundary impedance owner: lip/beak opening blends boundary reflection from near-closed termination to declared open-end reflection, and radiation reads boundary flow (`incoming - outgoing`) through a stronger radiation slope instead of raw incoming pressure. Latest static graph evidence after structural nasal coupling is under `artifacts/parity/pink-trombone-logmel/20260527T150749857`: open/front/nasal/ma/sibilant/closure cosine 0.6942/0.6732/0.7386/0.7067/0.4548/0.5449.
- `docs/acoustic-graph-flow-audit.md` compares Aqua's graph to Birkholz/VTL, Story/TubeTalker, Smith waveguides, ArtiSynth, and PT/SndKit. The graph records can name the research-backed architecture, but current Faust lowering still mangles multi-port branch sample flow: global connection coupling is acting as an accidental damper, transformed admittance cannot be expressed by raw `ParameterBinding`, and radiation level still depends on connection-law damping. Two code experiments were cut: moving nasal coupling purely to side-port admittance and a special three-port scatter both compiled but over-radiated utterances 3-5x RMS. Next cut is transformed side-port admittance plus passive three-port/radiation normalization tests.
- Pink Trombone log-mel parity now writes graph candidates only. Generated tract graphs mirror live `/pink/...` tract bindings into acoustic source, terminal, radiation, and path-area fields. Graph generation stopped baking first-frame tongue/constriction/lip morphology into the path; `AcousticPath.AreaControl` now turns tongue/constriction/lip controls into live area expressions at graph sample points. Velum owns nasal branch entrance area/aperture; generated nasal connections keep full connection coupling, and the nasal terminal carries the side-port admittance limit. The first nasal tube sample is no longer allowed to decide the velum opening. Graph radiation now uses aperture as a boundary impedance owner: lip/beak opening blends boundary reflection from near-closed termination to declared open-end reflection, and radiation reads boundary flow (`incoming - outgoing`) through a stronger radiation slope instead of raw incoming pressure. Latest static graph evidence after side-port branch ownership is under `artifacts/parity/pink-trombone-logmel/20260527T165305967`: open/front/nasal/ma/sibilant/closure cosine 0.6157/0.6879/0.6338/0.6089/0.4868/0.6525; utterances over-radiate 3-5x RMS, exposing normalization debt.
- `docs/acoustic-graph-flow-audit.md` compares Aqua's graph to Birkholz/VTL, Story/TubeTalker, Smith waveguides, ArtiSynth, and PT/SndKit. The graph records can name the research-backed architecture, but current Faust lowering still mangles multi-port branch sample flow. The kept physical fix moves nasal coupling from global connection damping into side-port admittance and emits a named three-port branch scatter for two same-path ports plus one side branch. This worsens current utterance RMS/articulation because radiation normalization and passivity checks were leaning on the old damper. Do not revert that ownership fix just because the next broken subsystem screams; next cut is passive/admittance-normalized three-port scattering and radiation normalization probes.
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns continuous diameter/area geometry over normalized position, physical length (`length_cm`), interpolation/resampling, derived reflection coefficients, and per-cell acoustic delay; `AcousticAreaControl` owns live sampled morphology controls on a path; `WaveClockPolicy` owns the legal delay floor instead of forcing every segment to one sample; generated graph tracts now use `HalfSampleGrid` to match the PT/Kelly-Lochbaum half-sample regime; ordinary same-path graph interiors now own signed two-port area-discontinuity scattering; `glottis` owns excitation controls with mild harmonic waveshaping and a small higher-harmonic brightness term for graph sources; `tract_injection` owns positioned frication/burst controls; `AcousticSourcePort.Transient` owns graph-native release/click energy as a pressure pulse rather than filtered noise; sustained `TurbulenceJet` lowering owns a narrow-open aperture gate so closed constrictions no longer hiss; `AcousticSourcePositionControl` owns differentiable runtime weights over fixed source terminals so moving articulators can move source energy without recompiling the graph; `AcousticRadiationPort` owns aperture-dependent boundary reflection, flow radiation, and current post-flow normalization; `nasal_branch` owns a second diameter/area tube and branch aperture while `AcousticConnection.Coupling` owns structural max admittance; `tract_motion` owns control slew and obstruction threshold; `AcousticPortNetwork` owns graph lowering. `PinkTromboneReferenceRenderer` ports the pinned MIT `chdh/pink-trombone-mod` DSP source (`external/pink-trombone-mod` at 359c2d3b42b10280404c1650dc601902112b4c90), and reference WAVs before `artifacts/parity/pink-trombone-utterances/20260526T211902375` are invalid. The accepted PT utterance curriculum is mama, papa, and thrombosis; lulek was cut because its control sketch taught a bad artifact. Latest graph-only utterance smoke under `artifacts/parity/pink-trombone-utterance-logmel/20260527T150836912`: mama/papa/thrombosis remain `not-accepted-articulation` with cosine 0.8337/0.8893/0.5317 and articulation scores 0.2118/0.1890/0.2594. `AudioComparison.Articulation` reports envelope cosine, active-frame ratio, silence mismatch, envelope/spectral flux ratios, motor-band ratio, speech-band ratio, and an articulation score so log-mel cannot forge a listening receipt. A direct LF coefficient source was tested and cut because it overdrove the graph and broke static open vowel; a broader harmonic-tail source was tested and cut because it worsened voiced-band behavior. A naive midpoint admittance scatter was tested and cut because it collapsed `mama`; the kept area-discontinuity scatter needed the opposite reflection sign for this graph state convention, and re-testing the literal SndKit sign after the clock fix made `thrombosis` loud/unstable. A long noisy release transient was tested and cut because it turned papa into motor-band energy; deterministic pressure release with PT-scale decay is structurally kept but still not sufficient. Next pressure is true graph pressure storage/release at severe closures; output coloring alone is not allowed to impersonate articulation.
- 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.

View file

@ -638,8 +638,9 @@ public sealed class PatchScriptTests
Assert.Contains(patch.AcousticPaths, path => path.Name == "voices_0_oral" && path.AreaControl is not null);
Assert.Contains(patch.AcousticSourcePorts, port => port.Name.StartsWith("voices_0_inj_", StringComparison.Ordinal) && port.Transient > 0 && port.PositionControl is not null);
Assert.Contains(patch.AcousticTerminals, terminal => terminal.Name.StartsWith("voices_0_area_", StringComparison.Ordinal));
Assert.Contains(patch.AcousticConnections, connection => connection.Name.Contains("_nose_connection", StringComparison.Ordinal) && MathF.Abs(connection.Coupling - 0.15f) < 0.0001f);
Assert.Contains(patch.AcousticConnections, connection => connection.Name.Contains("_nose_connection", StringComparison.Ordinal) && MathF.Abs(connection.Coupling - 1f) < 0.0001f);
Assert.DoesNotContain(patch.ParameterBindings, binding => binding.FieldPath.StartsWith("/acoustic/connections/", StringComparison.OrdinalIgnoreCase) && binding.FieldPath.EndsWith("/coupling", StringComparison.OrdinalIgnoreCase));
Assert.Contains("graph_connection_pressure_voices_0_nose_connection", export.Source);
Assert.Contains("acoustic_graph_radiated", export.Source);
Assert.Contains("graph_terminal_area_voices_0_area_", export.Source);
Assert.DoesNotContain("wg_diameter_target_", export.Source);