Separate graph source injection from topology

This commit is contained in:
Meta Vens 2026-05-28 00:07:13 +01:00
commit 0970ebacd5
5 changed files with 93 additions and 7 deletions

View file

@ -232,6 +232,25 @@ wave-convention fix, `papa` rose to cosine `0.8828` with near-unity RMS, while
`thrombosis` still fails because it goes too silent even though speech-band
ratio is finally present.
The user-listening report after that pass overruled the flattering metric:
`mama` and `papa` are still nearly the same vibrating-phone sound. The harness
now writes utterance separability evidence and fails when candidate utterances
are more mutually similar than the PT references by a wide log-mel margin.
Latest collapse artifact:
`artifacts/parity/pink-trombone-utterance-logmel/20260527T225833392`.
It reports `mama/papa` reference log-mel similarity `0.3791` versus candidate
similarity `0.6069`, with candidate envelope similarity `0.8885`. That is not
speech parity; it is spectral collapse with a nice report.
Source terminals are also no longer topology owners unless they sit on a path
boundary. Interior source ports now attach to the nearest existing graph node
and inject energy there, while non-source terminals own node area/reflection.
This is the right ownership cut: geometry creates junctions; source ports add
energy to traveling waves. The change worsened some output levels because it
removed fake source-created impedance slices, but that is a downstream source,
closure, and radiation problem, not grounds to let source placement mutate the
tract.
The next coherent implementation target is source/boundary coupling, not more
global gain:

View file

@ -557,7 +557,12 @@ public static class FaustEmitter
continue;
}
var ordered = group
var groupTerminals = group.ToList();
var topologyTerminals = groupTerminals
.Where(terminal => terminal.Kind != AcousticTerminalKind.Source || terminal.Position <= 0.0001f || terminal.Position >= 0.9999f)
.ToList();
var ordered = topologyTerminals
.GroupBy(terminal => MathF.Round(Math.Clamp(terminal.Position, 0, 1), 4))
.OrderBy(positionGroup => positionGroup.Key)
.ThenBy(positionGroup => positionGroup.Select(terminal => terminal.Name).Order(StringComparer.OrdinalIgnoreCase).First())
@ -580,6 +585,19 @@ public static class FaustEmitter
})
.ToList();
foreach (var terminal in groupTerminals.Except(topologyTerminals))
{
var nearest = ordered
.OrderBy(node => MathF.Abs(node.Position - Math.Clamp(terminal.Position, 0, 1)))
.ThenBy(node => node.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
if (nearest is not null)
{
((List<AcousticTerminal>)nearest.Terminals).Add(terminal);
terminalNodes[terminal.Name] = nearest;
}
}
for (var i = 0; i < ordered.Count - 1; i++)
{
if (Math.Abs(ordered[i + 1].Position - ordered[i].Position) < 0.0001f)
@ -649,8 +667,13 @@ public static class FaustEmitter
}
foreach (var node in graphNodes)
{
source.AppendLine($"{name}_graph_node_area_{node.Name} = max(0.000001, {string.Join(" + ", node.Terminals.Select(terminal => $"{name}_graph_terminal_area_{SafeIdentifier(terminal.Name)}"))});");
source.AppendLine($"{name}_graph_node_reflection_{node.Name} = ({string.Join(" + ", node.Terminals.Select(terminal => $"{name}_graph_terminal_reflection_{SafeIdentifier(terminal.Name)}"))}) / {F(Math.Max(1, node.Terminals.Count))};");
var areaTerminals = node.Terminals.Where(terminal => terminal.Kind != AcousticTerminalKind.Source).ToList();
if (areaTerminals.Count == 0)
{
areaTerminals = node.Terminals.ToList();
}
source.AppendLine($"{name}_graph_node_area_{node.Name} = max(0.000001, {string.Join(" + ", areaTerminals.Select(terminal => $"{name}_graph_terminal_area_{SafeIdentifier(terminal.Name)}"))});");
source.AppendLine($"{name}_graph_node_reflection_{node.Name} = ({string.Join(" + ", areaTerminals.Select(terminal => $"{name}_graph_terminal_reflection_{SafeIdentifier(terminal.Name)}"))}) / {F(Math.Max(1, areaTerminals.Count))};");
}
foreach (var segment in segments)
{

View file

@ -192,3 +192,4 @@
{"evidence":"Replaced same-path two-port area scatter with an admittance-weighted pressure-continuity law (`junction pressure`, then `pressure - incoming`) and switched generated graph tracts from half-sample grid to continuous fractional delay so added source terminals do not lengthen the tract. Narrow-open turbulence now starts above diameter 0.04 instead of 0.3. Focused graph/Faust checks passed; static PT graph artifact `artifacts/parity/pink-trombone-logmel/20260527T204022933` passed smoke floors. Utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T203950790` still fails at papa with cosine 0.7314, articulation 0.1140, and speechBandRatio 0.0151. A per-segment Faust low-pass loss experiment and a softer radiation blend were tested and cut because they added cost or worsened papa without fixing articulation.","claim":"Passivity probes were not enough: the old two-port scatter could conserve local energy while using the wrong pressure relation for unequal areas. Correct pressure scattering and fractional graph delay are structural fixes even when PT metrics expose new failures. The next coherent target is graph-native closure reservoir/source impedance, not global gain or radiation-color compensation.","timestamp":"2026-05-27T20:45:00+01:00","topic":"acoustic-graph-pressure-law-fractional-delay"}
{"evidence":"Added debug-only graph node incident-pressure/source probes and a papa source-probe artifact. `artifacts/parity/pink-trombone-graph-source-probes/20260527T204750891` showed lip-adjacent plosive source peaks around 0.094 versus glottal source around 3.7. Raising turbulence release-reservoir coupling moved the plosive source peak to about 0.432 in `artifacts/parity/pink-trombone-graph-source-probes/20260527T204918624`; focused source-probe and static PT graph tests passed. The utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T205009225` still fails papa with cosine 0.7242 and articulation 0.1158.","claim":"Papa's plosive failure is now visible at the source layer: closure release was under-coupled relative to the glottal source. Increasing the graph source release reservoir is physically justified, but raw release amplitude alone does not restore speech-band articulation. Next work should shape that release through source impedance/constriction filtering and radiation, not boost the whole voice.","timestamp":"2026-05-27T20:55:00+01:00","topic":"acoustic-graph-source-probes-release-coupling"}
{"evidence":"Re-read the PinkTromboneReferenceRenderer and found the graph lowering had mixed wave conventions: two-port same-path scatter had been rewritten as pressure-continuity over area-weighted pressure variables, while PT/Kelly-Lochbaum traveling waves use area reflection coefficients. Replaced two-port same-path scatter with `r=(A_left-A_right)/(A_left+A_right)`, `right_out=left_in-r*(left_in+right_in)`, `left_out=right_in+r*(left_in+right_in)`, and converted the three-port branch law to the matching PT nasal-junction form `r_p=(2*A_p-sumA)/sumA`, `out_p=r_p*in_p+(1+r_p)*sum(other inputs)`. The raw release-reservoir boost was cut after this wave-convention fix made papa overdrive. Static graph artifact `artifacts/parity/pink-trombone-logmel/20260527T223816035` passed; utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T223931573` improved papa to cosine 0.8828, rmsRatio 1.0186, silenceMismatch 0.0455, while thrombosis still fails from silence/articulation.","claim":"The phone-vibration failure was partly caused by using the wrong wave-variable convention, not by insufficient source gain. Keep the KL/PT traveling-wave convention consistent across two-port and branch junctions. Do not restore the pressure-continuity rewrite unless the graph switches to explicitly power-normalized wave variables everywhere.","timestamp":"2026-05-27T22:45:00+01:00","topic":"acoustic-graph-kelly-lochbaum-wave-convention"}
{"evidence":"Added utterance separability diagnostics after user listening reported that mama and papa still sound like nearly identical phone vibrations despite flattering individual log-mel scores. The utterance harness now renders all accepted fixtures, writes `separability.txt`, and fails when candidate pairs are more mutually similar than reference pairs by more than 0.20 log-mel similarity. Latest artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T225833392` catches mama/papa collapse: referenceLogMel 0.3791, candidateLogMel 0.6069, referenceEnvelope 0.8670, candidateEnvelope 0.8885. Interior source terminals also stopped creating topology nodes or contributing to node area/reflection; they now attach to existing graph nodes and inject energy while geometry terminals own impedance.","claim":"Log-mel cosine can lie by rewarding shared filtered buzz. The parity harness must reject cross-utterance collapse, and graph source ports must inject into wave nodes without mutating the tract topology they are meant to excite.","timestamp":"2026-05-27T23:05:00+01:00","topic":"acoustic-graph-utterance-collapse-source-topology"}

View file

@ -1,6 +1,6 @@
project: AquaSynth
spine_version: 1
updated: 2026-05-27T22:45:00+01:00
updated: 2026-05-27T23:05:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -121,6 +121,7 @@ live_invariants:
- `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 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.
- 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

@ -96,6 +96,8 @@ public sealed class PinkTromboneLogMelParityTests
var artifactDir = ArtifactPath("parity", "pink-trombone-utterance-logmel", DateTimeOffset.UtcNow.ToString("yyyyMMddTHHmmssfff", CultureInfo.InvariantCulture));
var analyzer = new AudioAnalyzer(new AudioAnalysisConfig(SampleRate: 44100));
var reports = new List<string>();
var rendered = new List<RenderedUtterance>();
var smokeFailures = new List<string>();
foreach (var fixture in PinkTromboneUtteranceFixtures.All.Where(fixture => UtteranceParityIds.Contains(fixture.Id)))
{
@ -110,6 +112,7 @@ public sealed class PinkTromboneLogMelParityTests
Assert.True(candidate.Samples.Max(MathF.Abs) > 0.00001f, candidate.Stderr);
var comparison = analyzer.Compare(reference.Samples, candidate.Samples);
rendered.Add(new RenderedUtterance(fixture.Id, reference.Samples, candidate.Samples));
var report = UtteranceReport(fixture, comparison);
reports.Add(report);
@ -121,13 +124,22 @@ public sealed class PinkTromboneLogMelParityTests
File.WriteAllText(Path.Combine(fixtureDir, "report.txt"), report);
File.WriteAllText(Path.Combine(fixtureDir, "controls.csv"), UtteranceControlCsv(fixture));
Assert.True(
comparison.LogMelCosineSimilarity >= UtteranceGraphSmokeCosineFloors.GetValueOrDefault(fixture.Id, 0.1f),
$"{report}{Environment.NewLine}artifacts: {artifactDir}");
if (comparison.LogMelCosineSimilarity < UtteranceGraphSmokeCosineFloors.GetValueOrDefault(fixture.Id, 0.1f))
{
smokeFailures.Add(report);
}
}
Directory.CreateDirectory(artifactDir);
File.WriteAllText(Path.Combine(artifactDir, "summary.txt"), string.Join(Environment.NewLine + Environment.NewLine, reports));
var separability = UtteranceSeparability(analyzer, rendered);
File.WriteAllLines(Path.Combine(artifactDir, "separability.txt"), separability.Select(item => item.Report));
var collapseFailures = separability
.Where(item => item.CandidateLogMelSimilarity > item.ReferenceLogMelSimilarity + 0.20f)
.Select(item => $"candidate collapse: {item.Report}")
.ToArray();
var failures = smokeFailures.Concat(collapseFailures).ToArray();
Assert.True(failures.Length == 0, $"{string.Join(Environment.NewLine, failures)}{Environment.NewLine}artifacts: {artifactDir}");
}
[Fact]
@ -294,6 +306,36 @@ public sealed class PinkTromboneLogMelParityTests
$"{fixture.Id}/graph-utterance: verdict={verdict} cosine={comparison.LogMelCosineSimilarity:0.0000} logMelDistance={comparison.LogMelDistance:0.0000} score={comparison.Score:0.0000} rmsRatio={comparison.RmsRatio:0.0000} centroidRatio={comparison.CentroidRatio:0.0000} articulation={comparison.Articulation.ArticulationScore:0.0000} envCos={comparison.Articulation.EnvelopeCosineSimilarity:0.0000} activeRatio={comparison.Articulation.ActiveFrameRatio:0.0000} silenceMismatch={comparison.Articulation.SilenceMismatch:0.0000} envelopeFluxRatio={comparison.Articulation.EnvelopeFluxRatio:0.0000} spectralFluxRatio={comparison.Articulation.SpectralFluxRatio:0.0000} motorBandRatio={comparison.Articulation.MotorBandRatio:0.0000} speechBandRatio={comparison.Articulation.SpeechBandRatio:0.0000}");
}
private static IReadOnlyList<UtteranceSeparabilityRow> UtteranceSeparability(AudioAnalyzer analyzer, IReadOnlyList<RenderedUtterance> utterances)
{
var rows = new List<UtteranceSeparabilityRow>();
for (var i = 0; i < utterances.Count; i++)
{
for (var j = i + 1; j < utterances.Count; j++)
{
var left = utterances[i];
var right = utterances[j];
var reference = analyzer.Compare(left.ReferenceSamples, right.ReferenceSamples);
var candidate = analyzer.Compare(left.CandidateSamples, right.CandidateSamples);
var report = string.Create(
CultureInfo.InvariantCulture,
$"{left.Id}/{right.Id}: referenceLogMel={reference.LogMelCosineSimilarity:0.0000} candidateLogMel={candidate.LogMelCosineSimilarity:0.0000} referenceEnvelope={reference.Articulation.EnvelopeCosineSimilarity:0.0000} candidateEnvelope={candidate.Articulation.EnvelopeCosineSimilarity:0.0000} referenceArticulation={reference.Articulation.ArticulationScore:0.0000} candidateArticulation={candidate.Articulation.ArticulationScore:0.0000}");
rows.Add(new UtteranceSeparabilityRow(
left.Id,
right.Id,
reference.LogMelCosineSimilarity,
candidate.LogMelCosineSimilarity,
report));
}
}
return rows;
}
private sealed record UtteranceSeparabilityRow(string LeftId, string RightId, float ReferenceLogMelSimilarity, float CandidateLogMelSimilarity, string Report);
private sealed record RenderedUtterance(string Id, float[] ReferenceSamples, float[] CandidateSamples);
private static string ArticulationVerdict(AudioComparison comparison)
{
var articulation = comparison.Articulation;