Replace generic acoustic connection fallback

This commit is contained in:
Meta Vens 2026-05-28 12:37:07 +01:00
commit a0839a3d2e
5 changed files with 115 additions and 102 deletions

View file

@ -127,38 +127,50 @@ Verdict: keep the scatter law. Revisit source injection weighting separately.
What it simulates: PT-style oral/nasal side branch scattering.
Current state: coherent for the one common case: two same-path ports plus one
side branch. Removing the hidden `0.15` velum damper fixed a real nasal failure.
Current state: replaced. The old recognized three-port branch path has been
collapsed into the same general N-port scatter law used for every area
scattering connection.
Smells:
- The implementation recognizes one special topology shape. It is not yet a
general N-port passive junction.
- Source injection is added per port inside the branch law. That needs a clearer
impedance story if sources can live at branch nodes.
- The new general law still uses scalar terminal area/admittance, not a
frequency-dependent junction impedance.
Verdict: good enough for nasal branches, not enough for arbitrary alien tract
graphs. General N-port scattering should replace the shape-specific path.
Verdict: keep the generalization. It removes the PT-shaped topology shortcut,
but source impedance at junction nodes still needs a named model.
### Generic Connection Fallback
What it simulates: arbitrary connection scattering when a connection is not the
recognized three-port branch.
Current state: suspect. It computes a pressure-like expression, applies a
handmade port admittance blend, mixes with bypassed incoming waves, and injects
sources. This is exactly the kind of helper that survives because it is useful,
not because it has a single physical invariant.
Current state: cut. The lowering no longer emits
`graph_connection_pressure_*`, no longer uses the handmade
`0.6 + 0.4 * sqrt(...)` port-admittance blend, and no longer keeps a
shape-specific three-port branch path. Area-scattering connections now emit one
passive N-port traveling-wave law:
`r_p = (2 * A_p - sum(A)) / sum(A)`
`out_p = r_p * in_p + (1 + r_p) * sum(other inputs)`
Connection coupling now crossfades between that scattered wave and the local
incoming wave. Bypass remains explicitly bypass.
Smells:
- `0.6 + 0.4 * sqrt(...)` is an unexplained compensator.
- It mixes pressure-continuity language into a graph whose main convention is
KL/PT traveling waves.
- It has no passivity proof.
- Port area selection is still simple: ordinary isolated junction labels use
adjacent segment area, while branch/radiation/source terminals use their
declared terminal area. That is coherent enough for the present graph, but it
should be documented as terminal admittance ownership before more exotic
junctions rely on it.
- Source injection is still added as node pressure energy after scattering,
divided by local node port count. That is not a full source impedance law.
Verdict: highest-priority Jenga. Replace with a general passive scattering law
or restrict supported connections until that exists.
Verdict: the highest-priority fallback Jenga is gone. Keep passivity probes on
this law while cutting source impedance and closure storage next.
### Glottal Source
@ -280,11 +292,15 @@ faster than global scores.
Cut or replace these before piling on more tuning:
1. Generic connection fallback pressure/admittance blend.
2. Unowned radiation constants: 30/70 high-pass blend and `area + 1.0`.
3. Unowned segment contact-loss constant `0.02`.
4. Turbulence/release scalar constants without a named pressure-flow model.
5. Nearest-node source attachment as the only interior source placement model.
1. Unowned radiation constants: 30/70 high-pass blend and `area + 1.0`.
2. Unowned segment contact-loss constant `0.02`.
3. Turbulence/release scalar constants without a named pressure-flow model.
4. Nearest-node source attachment as the only interior source placement model.
Cut in the 2026-05-28 N-port pass:
- Generic connection fallback pressure/admittance blend.
- Shape-specific three-port branch scattering path.
## Keep List
@ -300,11 +316,10 @@ These foundations deserve to stay:
## Next Coherent Build
The next implementation should not be another gain pass. Build a general
passive N-port junction and a closure reservoir primitive:
The next implementation should not be another gain pass. The general passive
N-port junction now exists; the remaining build targets are closure storage and
radiation impedance:
- N-port junction: one law for any connection group, using each connected port's
admittance/area, with a passivity probe and no shape-specific fallback.
- Closure reservoir: a stateful pressure store attached to a constriction owner,
charged while local area is sealed and released through the graph when area
opens.
@ -313,3 +328,25 @@ passive N-port junction and a closure reservoir primitive:
Until those exist, the graph can imitate some Pink Trombone pressure, but it is
not yet a clean general vocal-acoustic machine.
## 2026-05-28 N-Port Connection Cut
The first audit cut replaced connection fallback lowering with one general
area-scattering law for all non-bypass acoustic connections. This removes two
old authorities at once: the pressure-like fallback expression and the
PT-shaped three-port branch recognizer.
Latest utterance artifact:
`artifacts/parity/pink-trombone-utterance-logmel/20260528T113334390`.
The change keeps smoke tests passing. `thrombosis` improves slightly to cosine
`0.4555`, while `mama/papa` remain differentiated (`candidateLogMel=0.2469`
versus reference `0.3791`) but still under-articulated and quiet.
Latest static artifact:
`artifacts/parity/pink-trombone-logmel/20260528T113334415`.
Static fixtures still pass; `closure-release` remains sealed-static evidence
with peak `0.000009`.
This is not a victory lap. It is a floor repair. The next real smells are still
radiation impedance constants, graph-native closure reservoir state, and source
impedance at junction nodes.

View file

@ -751,30 +751,7 @@ public static class FaustEmitter
}
var safeConnection = SafeIdentifier(connection.Name);
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports, options, debugProbeKeepAlive))
{
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};");
foreach (var (node, terminal, port, nodePortCount) in ports)
{
var incoming = GraphIncoming(name, port);
var outgoing = GraphOutgoing(name, port);
var sourceInjection = NodeSourceIdentifier(name, node);
var portCoupling = $"{name}_graph_connection_coupling_{safeConnection}";
var portArea = ConnectionPortAreaExpression(name, terminal, nodePortCount);
var portAdmittance = $"(0.6 + 0.4 * sqrt(clip01({portArea} / max(0.000001, {connectionMaxArea}))))";
var bypassInput = ports.Count == 1
? incoming
: $"(({string.Join(" + ", ports.Where(item => item.port != port).Select(item => GraphIncoming(name, item.port)))}) / {F(ports.Count - 1)})";
var scattered = connection.Law == AcousticConnectionLaw.Bypass
? bypassInput
: $"({name}_graph_connection_pressure_{safeConnection} - {incoming})";
source.AppendLine($" {outgoing} = (({scattered}) * ({portCoupling}) * {portAdmittance} + {incoming} * (1.0 - ({portCoupling}) * {portAdmittance}) + ({sourceInjection}) / {F(Math.Max(1, incident[node.Name].Count))}) * {name}_graph_connection_loss_{safeConnection};");
}
EmitConnectionScatter(source, name, connection, safeConnection, ports, options, debugProbeKeepAlive);
}
var radiated = new List<string>();
@ -1645,26 +1622,10 @@ public static class FaustEmitter
_ => 0.000001f
};
private static string ConnectionPressureExpression(
string voiceName,
AcousticConnection connection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports)
{
if (connection.Law == AcousticConnectionLaw.PressureContinuity)
{
return $"2.0 * ({string.Join(" + ", ports.Select(item => GraphIncoming(voiceName, item.port)))}) / {F(Math.Max(1, ports.Count))}";
}
var weightedIncoming = ports
.Select(item => $"{ConnectionPortAreaExpression(voiceName, item.terminal, item.nodePortCount)} * {GraphIncoming(voiceName, item.port)}");
var areaSum = ports.Select(item => ConnectionPortAreaExpression(voiceName, item.terminal, item.nodePortCount));
return $"2.0 * ({string.Join(" + ", weightedIncoming)}) / max(0.000001, {string.Join(" + ", areaSum)})";
}
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(
private static void EmitConnectionScatter(
StringBuilder source,
string voiceName,
AcousticConnection connection,
@ -1673,42 +1634,22 @@ public static class FaustEmitter
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
if (ports.Count != 3 || connection.Law != AcousticConnectionLaw.AreaScattering)
if (connection.Law == AcousticConnectionLaw.Bypass)
{
return false;
EmitBypassConnection(source, voiceName, connection, safeConnection, ports);
return;
}
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 orderedThrough = through.OrderBy(item => item.port.Segment.StartPosition).ToList();
var portItems = new List<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)>
{
orderedThrough[0],
orderedThrough[1],
side
};
var portItems = ports.ToList();
var areaExpressions = new Dictionary<AcousticGraphPort, string>();
foreach (var item in through)
foreach (var item in portItems)
{
areaExpressions[item.port] = GraphPortArea(voiceName, item.port);
areaExpressions[item.port] = ConnectionAreaExpression(voiceName, item);
}
areaExpressions[side.port] = ConnectionPortAreaExpression(voiceName, side.terminal, side.nodePortCount);
var areaSum = string.Join(" + ", portItems.Select(item => areaExpressions[item.port]));
var coupling = $"{voiceName}_graph_connection_coupling_{safeConnection}";
var loss = $"{voiceName}_graph_connection_loss_{safeConnection}";
foreach (var item in portItems)
{
var incoming = GraphIncoming(voiceName, item.port);
@ -1716,8 +1657,10 @@ public static class FaustEmitter
var sourceInjection = NodeSourceIdentifier(voiceName, item.node);
var reflection = $"{voiceName}_graph_connection_reflection_{safeConnection}_{SafeIdentifier(item.terminal.Name)}_{item.port.Segment.Index}_{(item.port.AtStart ? "start" : "end")}";
var otherIncoming = string.Join(" + ", portItems.Where(other => other.port != item.port).Select(other => GraphIncoming(voiceName, other.port)));
var scattered = $"{voiceName}_graph_connection_scattered_{safeConnection}_{SafeIdentifier(item.terminal.Name)}_{item.port.Segment.Index}_{(item.port.AtStart ? "start" : "end")}";
source.AppendLine($" {reflection} = (2.0 * ({areaExpressions[item.port]}) - ({areaSum})) / max(0.000001, {areaSum});");
source.AppendLine($" {outgoing} = ({reflection} * {incoming} + (1.0 + {reflection}) * ({otherIncoming}) + ({sourceInjection}) / {F(Math.Max(1, item.nodePortCount))}) * {voiceName}_graph_connection_loss_{safeConnection};");
source.AppendLine($" {scattered} = {reflection} * {incoming} + (1.0 + {reflection}) * ({otherIncoming});");
source.AppendLine($" {outgoing} = (({scattered}) * ({coupling}) + {incoming} * (1.0 - ({coupling})) + ({sourceInjection}) / {F(Math.Max(1, item.nodePortCount))}) * {loss};");
}
var energyIn = string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphIncoming(voiceName, item.port)}, 2.0)"));
var energyOut = string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphOutgoing(voiceName, item.port)}, 2.0)"));
@ -1725,10 +1668,38 @@ public static class FaustEmitter
source.AppendLine($" {voiceName}_graph_connection_energy_out_{safeConnection} = {ProbeSignal(options, $"/debug/{voiceName}/connection/{connection.Name}/energy_out", energyOut, 0, 4)};");
debugProbeKeepAlive.Add($"{voiceName}_graph_connection_energy_in_{safeConnection}");
debugProbeKeepAlive.Add($"{voiceName}_graph_connection_energy_out_{safeConnection}");
return true;
}
private static void EmitBypassConnection(
StringBuilder source,
string voiceName,
AcousticConnection connection,
string safeConnection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports)
{
var coupling = $"{voiceName}_graph_connection_coupling_{safeConnection}";
var loss = $"{voiceName}_graph_connection_loss_{safeConnection}";
foreach (var item in ports)
{
var incoming = GraphIncoming(voiceName, item.port);
var outgoing = GraphOutgoing(voiceName, item.port);
var sourceInjection = NodeSourceIdentifier(voiceName, item.node);
var otherIncoming = ports.Count == 1
? incoming
: $"(({string.Join(" + ", ports.Where(other => other.port != item.port).Select(other => GraphIncoming(voiceName, other.port)))}) / {F(ports.Count - 1)})";
source.AppendLine($" {outgoing} = (({otherIncoming}) * ({coupling}) + {incoming} * (1.0 - ({coupling})) + ({sourceInjection}) / {F(Math.Max(1, item.nodePortCount))}) * {loss};");
}
}
private static string ConnectionAreaExpression(
string voiceName,
(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount) item) =>
item.terminal.Kind == AcousticTerminalKind.Junction &&
item.terminal.Port.Length == 0 &&
item.node.Terminals.Count == 1
? GraphPortArea(voiceName, item.port)
: ConnectionPortAreaExpression(voiceName, item.terminal, item.nodePortCount);
private static bool TryEmitTwoPortPathScatter(
StringBuilder source,
string voiceName,

View file

@ -196,3 +196,4 @@
{"evidence":"Generated tract lip radiation terminals no longer set terminal `area_scale` from the first frame's lip opening. Lip opening already drives live oral path area and radiation aperture, so the old terminal scale let a word starting with closed lips carry closed-lip admittance through later open vowels. Focused structural tests passed, and static PT graph artifact `artifacts/parity/pink-trombone-logmel/20260527T231611586` passed smoke. The utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T231056242` still fails: thrombosis remains too silent and mama/papa candidate similarity is 0.6343 versus reference 0.3791.","claim":"Generated radiation terminal area scale is not a lip-opening owner. Removing the baked first-frame scale is a coherent authority cut even though it exposes the remaining source/reservoir/radiation coupling failure more harshly.","timestamp":"2026-05-27T23:20:00+01:00","topic":"acoustic-graph-lip-radiation-authority"}
{"evidence":"Repaired graph voice sample-flow ownership: bounded the glottal source with local tanh normalization, added area-dependent segment propagation loss, removed the hidden 0.15 generated nasal side-port damper so velum diameter squared owns nasal admittance, stopped generated tract lips from binding lip opening into radiation-port opening, and stopped multiplying graph boundary flow by aperture after boundary reflection/path geometry already handle opening. Focused structural tests, utterance smoke, and static smoke passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T070759312`: mama/papa/thrombosis cosine 0.7918/0.8540/0.4366, candidate mama/papa separability 0.3651 versus reference 0.3791. Latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T071240442` treats closure-release as sealed static with peak 0.000005.","claim":"The phone-vibration collapse came from multiple model ownership leaks, not one missing gain knob: unbounded source dominance, immortal closed-section wave storage, damped nasal admittance, and lip radiation double-gating. The graph is still quiet and under-articulated, but different articulations now drive different sample-flow paths. Next fix is true closure reservoir/release through the scattering graph.","timestamp":"2026-05-28T07:15:00+01:00","topic":"acoustic-graph-voice-model-repair"}
{"evidence":"Added `docs/acoustic-graph-dsp-audit.md`, auditing each graph-voice component by asking what it simulates and whether the implementation makes sense. The audit keeps path graphs with typed terminals, KL/PT two-port and nasal branch scatter, source/topology separation, velum side-port area, generated lip geometry ownership, area-dependent segment loss, and utterance separability. It marks the generic connection fallback pressure/admittance blend, radiation color/admittance constants, the segment loss constant, turbulence/release scalar constants, and nearest-node source attachment as Jenga. It recommends replacing the generic fallback with a passive general N-port junction, adding graph-native closure reservoir state, and replacing radiation constants with a named radiation impedance primitive.","claim":"The current graph voice is viable but not clean. Further tuning should start by cutting unowned fallback laws and constants, not by adding more source gain or metric-specific compensation.","timestamp":"2026-05-28T07:45:00+01:00","topic":"acoustic-graph-dsp-audit"}
{"evidence":"Replaced the graph connection fallback in `FaustExport` with one general passive N-port area-scattering law for all non-bypass acoustic connections, removed the PT-shaped three-port branch recognizer, and updated structural tests to assert `graph_connection_reflection_*`/`graph_connection_energy_*` instead of `graph_connection_pressure_*`. Focused structural, utterance, and static PT graph tests passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T113334390`: mama/papa/thrombosis cosine 0.7799/0.8554/0.4555 with mama/papa candidate separability 0.2469 versus reference 0.3791. Latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T113334415` passes with sealed-static closure peak 0.000009.","claim":"Generic connection pressure/admittance fallback was unowned DSP authority. Non-bypass graph connections now share the same traveling-wave N-port area law, leaving closure storage, radiation impedance, and source impedance as the next real smells instead of topology-shaped compatibility code.","timestamp":"2026-05-28T11:45:00+01:00","topic":"acoustic-graph-nport-connection-cut"}

View file

@ -1,6 +1,6 @@
project: AquaSynth
spine_version: 1
updated: 2026-05-28T07:45:00+01:00
updated: 2026-05-28T11:45:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -119,13 +119,13 @@ live_invariants:
- Physical voice modeling should generalize around continuous acoustic morphologies, acoustic port networks, source ports, branch/radiation ports, and wave-clock policies. A larynx, syrinx, or alien voice should differ by topology and source-port configuration, not by special-case modules. Faust `physmodels.lib`/`delays.lib` provide useful primitives (`pm.chain`, `pm.l2s`, `pm.waveguideFd*`, `de.fdelay*`, Thiran/allpass and Lagrange delays), but AquaSynth should still generate tract/branch scattering where live area controls and inspectable training parameters matter.
- 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.
- `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 same-path two-port area discontinuities, scatters arbitrary non-bypass connection groups through one passive N-port area law, 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 diameter, which lowers through a squared parameter-binding transform into side-port admittance; 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. Lip opening owns path area and radiation aperture, not terminal area scaling. 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, names per-port boundary flow (`incoming - outgoing`), and weights output by bounded local terminal admittance rather than a fixed 4x output scalar. Latest static graph evidence after radiation admittance normalization is under `artifacts/parity/pink-trombone-logmel/20260527T170548495`: open/front/nasal/ma/sibilant/closure cosine 0.6675/0.7216/0.7251/0.7098/0.4862/0.6513; latest utterance evidence under `artifacts/parity/pink-trombone-utterance-logmel/20260527T170618466`: mama/papa/thrombosis cosine 0.7428/0.7830/0.5442 with rmsRatio 2.7955/2.4594/2.7397 and articulation 0.1288/0.1226/0.1151. The output is less absurdly loud, but articulation and speech-band motion are still the live failure.
- `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 lacks fully shaped graph-native closure pressure storage. The kept physical fixes move nasal coupling from global connection damping into transformed side-port admittance and use one Kelly-Lochbaum/PT-style traveling-wave convention for ordinary two-port area discontinuities and three-port branch junctions. The previous pressure-continuity rewrite was wrong for the graph's wave variables and produced the phone-vibration failure. Radiation now has named flow/admittance probes and local terminal-admittance weighting, reducing over-radiation without restoring the old damper. The graph clock for generated tract graphs is continuous fractional delay so adding terminals does not silently lengthen the morphology. Node source/incident-pressure debug probes expose source-layer failures; a raw release-reservoir boost was tested and cut after the KL fix made it overdrive papa. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T223931573` has papa at cosine 0.8828/RMS 1.0186 while thrombosis still fails from silence/articulation. Next cut is source impedance/constriction filtering for release energy, not global gain or PT-shaped modules.
- PT utterance metrics now include cross-utterance separability because log-mel parity alone hid the user's actual listening failure. Artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T225833392` shows `mama/papa` candidate collapse: reference log-mel similarity 0.3791 versus candidate 0.6069, with candidate envelope similarity 0.8885. Interior source terminals no longer decide topology or node area/reflection; they attach to existing graph nodes and inject energy, while non-source terminals own geometry. Keep this source/topology cut even if other broken layers get louder.
- Generated lip radiation terminals no longer bake first-frame lip opening into terminal `area_scale`; lip opening owns live path area plus radiation aperture. This removes a stale initial-state authority where words starting with closed lips kept closed-lip admittance. Static PT graph artifact `artifacts/parity/pink-trombone-logmel/20260527T231611586` passes, while utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T231056242` still fails `mama/papa` collapse and silent `thrombosis`. Next work remains source/reservoir/radiation coupling, not restoring terminal-scale double gates.
- Graph voice repair pass on 2026-05-28 fixed four model-level ownership leaks: glottal excitation is source-normalized with local `tanh`; segment propagation loss is area-dependent so closures absorb instead of ringing forever; generated nasal side-port area maps velum diameter squared directly instead of through the old hidden 0.15 damper; generated tract lips use live path-end geometry as the aperture owner and no longer double-gate radiation opening/boundary flow. Utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T070759312` passes smoke and no longer collapses `mama/papa` (`referenceLogMel=0.3791`, `candidateLogMel=0.3651`); `thrombosis` improves to cosine 0.4366 but remains weak/quiet. Static artifact `artifacts/parity/pink-trombone-logmel/20260528T071240442` passes with `closure-release` demoted to sealed-static evidence (`peak=0.000005`). Next pressure is true graph-native closure reservoir/release through the scattering path.
- `docs/acoustic-graph-dsp-audit.md` is the current Jenga audit for graph voice DSP. It says the path graph, typed terminals, KL/PT two-port and nasal branch scatter, source/topology separation, velum side-port area, generated lip geometry ownership, area-dependent segment loss, and utterance separability should stay. It flags the generic connection fallback pressure/admittance blend, unowned radiation constants, unowned segment loss constant, turbulence/release scalar constants, and nearest-node source attachment as suspect. Next coherent build is a passive general N-port junction plus graph-native closure reservoir, then a named radiation impedance primitive.
- `docs/acoustic-graph-dsp-audit.md` is the current Jenga audit for graph voice DSP. It says the path graph, typed terminals, KL/PT two-port scatter, source/topology separation, velum side-port area, generated lip geometry ownership, area-dependent segment loss, and utterance separability should stay. The first audit cut replaced the generic connection fallback pressure/admittance blend and PT-shaped three-port branch recognizer with one passive N-port area-scattering law for all non-bypass connection groups; emitted Faust no longer contains `graph_connection_pressure_*` for graph connections. Latest artifacts after the cut are `artifacts/parity/pink-trombone-utterance-logmel/20260528T113334390` and `artifacts/parity/pink-trombone-logmel/20260528T113334415`; smoke passes, `thrombosis` improves slightly, and `mama/papa` remain differentiated but quiet/under-articulated. Remaining suspect owners are unowned radiation constants, unowned segment loss constant, turbulence/release scalar constants, and nearest-node source attachment. Next coherent build is graph-native closure reservoir plus a named radiation impedance primitive.
- Graph source lowering now names per-node incident pressure and node-local source sums inside the recursive graph loop. Generated tract injection source banks now use cell-centered positions and `1 / sections` index scaling, so turbulence/release energy is born inside tube cells rather than directly on graph/radiation nodes. Latest source-position artifact under `artifacts/parity/pink-trombone-utterance-logmel/20260527T172949293`: mama/papa/thrombosis cosine 0.6654/0.7746/0.5522, rmsRatio 3.0010/2.5712/2.4943, articulation 0.1444/0.1177/0.1193. Static closure-release RMS improved to 1.0078 under `artifacts/parity/pink-trombone-logmel/20260527T172843945`, but papa articulation worsened; keep the cell-centered source placement because it is the correct graph-level source geometry, not because metrics liked it.
- TurbulenceJet now includes a release-timed burst-noise component in addition to the deterministic pressure pulse, but a stronger trial barely moved papa; the source is not the main remaining articulation bottleneck. Graph radiation now mixes 30% raw boundary flow with 70% high-passed flow after local admittance weighting, down from the over-bright 15/85 blend. Latest radiation/source artifact under `artifacts/parity/pink-trombone-utterance-logmel/20260527T174236049`: mama/papa/thrombosis cosine 0.6578/0.7692/0.5585, rmsRatio 3.0006/2.6153/2.3488, articulation 0.1437/0.1173/0.1191. Static `closure-release` under `artifacts/parity/pink-trombone-logmel/20260527T174139661` has rmsRatio 0.9900 and speechBandRatio 0.0842. This is still not speaking; next pressure is true tract resonance/passive scattering and pressure storage, not more burst gain.
- Same-path two-port area discontinuities now emit `graph_area_energy_in_*` and `graph_area_energy_out_*` probes alongside the three-port branch `graph_connection_energy_*` probes. Use these to inspect passivity and formant-body failures at the scatter sites rather than judging only final output loudness.

View file

@ -208,7 +208,9 @@ public sealed class PatchScriptTests
var export = FaustEmitter.Emit(patch, new FaustExportOptions("syrinxish"));
Assert.Contains("acoustic_graph_radiated", export.Source);
Assert.Contains("graph_loop", export.Source);
Assert.Contains("graph_connection_pressure_throat_connection", export.Source);
Assert.Contains("graph_connection_reflection_throat_connection", export.Source);
Assert.Contains("graph_connection_energy_in_throat_connection", export.Source);
Assert.DoesNotContain("graph_connection_pressure_throat_connection", export.Source);
Assert.Contains("graph_terminal_area_throat_from", export.Source);
Assert.Contains("graph_node_area_left_labium_right_labium", export.Source);
Assert.Contains("graph_source_right_labium", export.Source);
@ -251,7 +253,9 @@ public sealed class PatchScriptTests
var export = FaustEmitter.Emit(patch, new FaustExportOptions("humanish"));
Assert.Contains("acoustic_graph_radiated", export.Source);
Assert.Contains("graph_connection_pressure_velopharynx", export.Source);
Assert.Contains("graph_connection_reflection_velopharynx", export.Source);
Assert.Contains("graph_connection_energy_in_velopharynx", export.Source);
Assert.DoesNotContain("graph_connection_pressure_velopharynx", export.Source);
Assert.Contains("graph_terminal_area_nasal_gate", export.Source);
Assert.Contains("sqrt(clip01", export.Source);
Assert.Contains("graph_source_folds", export.Source);
@ -717,7 +721,7 @@ public sealed class PatchScriptTests
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 - 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("graph_connection_reflection_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);