Report graph scatter passivity probes

This commit is contained in:
Meta Vens 2026-05-27 21:10:12 +01:00
commit 95366b05e5
5 changed files with 129 additions and 6 deletions

View file

@ -176,6 +176,12 @@ patches can read those bargraph zones after processing blocks, so passivity
checks can sample probe values over time without turning probes into audio
channels.
The first native probe report on the nasal-vowel graph shows same-path and
nasal branch energy ratios essentially at 1.0. That is useful: the current
tract does not appear to be exploding at the basic scatter sites. The audible
failure is more likely missing frequency-dependent tract losses/radiation color
and closure pressure storage than a gross non-passive junction.
The next coherent implementation target is to make that law passive and
radiation-aware:

View file

@ -683,6 +683,7 @@ public static class FaustEmitter
.Concat(segments.Select(segment => $"l{segment.Index}"))
.ToList();
var nextStates = new List<string>(stateCount);
var debugProbeKeepAlive = new List<string>();
source.AppendLine($"{name}_graph(x) = {name}_graph_loop ~ si.bus({stateCount}) : (si.block({stateCount}), _) with {{");
source.AppendLine($" {name}_graph_loop({string.Join(", ", stateInputs)}) = {string.Join(", ", NextStatesPlaceholder(stateCount))}, {name}_graph_out with {{");
@ -723,7 +724,7 @@ public static class FaustEmitter
}
var safeConnection = SafeIdentifier(connection.Name);
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports, options))
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports, options, debugProbeKeepAlive))
{
continue;
}
@ -764,7 +765,7 @@ public static class FaustEmitter
var sourceInjection = NodeSourceIdentifier(name, node);
var reflection = $"{name}_graph_node_reflection_{node.Name}";
if (TryEmitTwoPortPathScatter(source, name, node, ports, sourceInjection, options))
if (TryEmitTwoPortPathScatter(source, name, node, ports, sourceInjection, options, debugProbeKeepAlive))
{
// Emitted as a local area-discontinuity junction.
}
@ -822,7 +823,12 @@ public static class FaustEmitter
}
nextStates.AddRange(segments.Select(segment => $"{name}_graph_next_l{segment.Index}"));
source.Replace(string.Join(", ", NextStatesPlaceholder(stateCount)), string.Join(", ", nextStates));
source.AppendLine($" {name}_graph_out = ({(radiated.Count == 0 ? "0.0" : string.Join(" + ", radiated))}) * {name}_graph_radiation_gain;");
var graphOutput = $"({(radiated.Count == 0 ? "0.0" : string.Join(" + ", radiated))}) * {name}_graph_radiation_gain";
if (options.DebugProbeUi && debugProbeKeepAlive.Count > 0)
{
graphOutput = $"attach(({graphOutput}), {string.Join(" + ", debugProbeKeepAlive)})";
}
source.AppendLine($" {name}_graph_out = {graphOutput};");
source.AppendLine(" };");
source.AppendLine("};");
source.AppendLine($"{name}_acoustic_graph_radiated = {name}_graph({name}_graph_drive);");
@ -1625,7 +1631,8 @@ public static class FaustEmitter
AcousticConnection connection,
string safeConnection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports,
FaustExportOptions options)
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
if (ports.Count != 3 || connection.Law != AcousticConnectionLaw.AreaScattering)
{
@ -1673,6 +1680,8 @@ public static class FaustEmitter
var energyOut = string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphOutgoing(voiceName, item.port)}, 2.0)"));
source.AppendLine($" {voiceName}_graph_connection_energy_in_{safeConnection} = {ProbeSignal(options, $"/debug/{voiceName}/connection/{connection.Name}/energy_in", energyIn, 0, 4)};");
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;
}
@ -1683,7 +1692,8 @@ public static class FaustEmitter
AcousticGraphNode node,
IReadOnlyList<AcousticGraphPort> ports,
string sourceInjection,
FaustExportOptions options)
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
if (ports.Count != 2 ||
!ports[0].Segment.Path.Name.Equals(ports[1].Segment.Path.Name, StringComparison.OrdinalIgnoreCase) ||
@ -1709,6 +1719,8 @@ public static class FaustEmitter
var energyOut = $"({firstArea}) * pow({firstOutgoing}, 2.0) + ({secondArea}) * pow({secondOutgoing}, 2.0)";
source.AppendLine($" {voiceName}_graph_area_energy_in_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/area/{node.Name}/energy_in", energyIn, 0, 4)};");
source.AppendLine($" {voiceName}_graph_area_energy_out_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/area/{node.Name}/energy_out", energyOut, 0, 4)};");
debugProbeKeepAlive.Add($"{voiceName}_graph_area_energy_in_{node.Name}");
debugProbeKeepAlive.Add($"{voiceName}_graph_area_energy_out_{node.Name}");
return true;
}

View file

@ -187,4 +187,5 @@
{"evidence":"Added generated Faust `graph_area_energy_in_*` and `graph_area_energy_out_*` probes for ordinary same-path two-port area discontinuities and fenced them with structural tests.","claim":"Vowel-body failures need observability at the tract scatter sites, not only at radiation output. Area-weighted energy probes now exist for both same-path discontinuities and three-port branches before changing the scattering law again.","timestamp":"2026-05-27T17:50:00+01:00","topic":"acoustic-graph-area-energy-probes"}
{"evidence":"Added `FaustExportOptions.DebugProbeUi`, wrapping graph radiation flow/admittance and area/connection energy probes in Faust `vbargraph` UI signals under `/debug/...`. Focused structural and Faust validation tests passed with debug probes enabled.","claim":"Graph passivity probes need a telemetry surface, not extra audio outputs. Faust UI bargraphs expose internal signal witnesses without changing normal render arity or contaminating parity audio.","timestamp":"2026-05-27T17:58:00+01:00","topic":"acoustic-graph-debug-probe-ui"}
{"evidence":"Extended Mimir's native streaming Faust path to split writable controls from read-only bargraph probe zones. `AquaSynthCompiledPatch.ProbePaths`, `AquaSynthStreamingPatch.ProbePaths`, `ReadProbe`, and `SnapshotProbes` now expose debug probe telemetry after processing blocks. Focused native streaming, debug-probe, graph, and Faust-validation tests passed.","claim":"Graph probe UI is now measurable through the native streaming host without changing DSP audio outputs. This is the right observability layer for passivity and radiation debugging: process blocks, sample `/debug/...` bargraphs, then compare scatter-site energy instead of inferring everything from rendered WAVs.","timestamp":"2026-05-27T18:14:00+01:00","topic":"native-faust-debug-probe-sampling"}
{"evidence":"Added `PinkTromboneGraphDebugProbesWritePassivityReportWhenNativeFaustIsInstalled`; focused probe and native tests passed. Latest artifact `artifacts/parity/pink-trombone-graph-probes/20260527T200828013/passivity-report.txt` shows same-path area scatter and `voices_0_nose_connection` energy out/in ratios at 1.0 for nasal-vowel.","claim":"The current graph's basic same-path and nasal branch scatter are not grossly non-passive under the nasal-vowel probe. The audible speech failure is more likely missing frequency-dependent tract losses/radiation color and closure pressure storage than scatter-site energy explosion.","timestamp":"2026-05-27T20:10:00+01:00","topic":"acoustic-graph-passivity-probe-report"}
{"evidence":"Added `AquaSynthStreamingPatch` on top of the existing native Faust factory/runtime. Compiled patches can now keep a DSP instance alive, cache Faust control zones, report input/output counts, set controls without rebuilding the UI map, and process either raw channel-pointer blocks or managed float[][] blocks. `AquaSynth.Faust` is bumped to 0.1.0-alpha.4 for downstream Fensalir consumption. `dotnet build AquaSynth.sln --no-restore` passed before the existing dirty `src/AquaSynth.Core/FaustExport.cs` worktree edit blocked focused test rebuilds with unrelated compile errors.","claim":"AquaSynth owns streaming Faust DSP sessions as the reusable compiler/runtime boundary; host engines should use the persistent session for audio-rate processing instead of one-shot offline render. The pointer-based API is the realtime surface; the managed array helper is a test/convenience lane.","timestamp":"2026-05-27T18:49:28+01:00","topic":"native-faust-streaming-session"}

View file

@ -1,6 +1,6 @@
project: AquaSynth
spine_version: 1
updated: 2026-05-27T18:14:00+01:00
updated: 2026-05-27T20:10:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -125,6 +125,7 @@ live_invariants:
- 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.
- `FaustExportOptions.DebugProbeUi` wraps graph probe signals in Faust `vbargraph` UI controls under `/debug/...` without changing audio output arity. Native streaming Faust patches now separate writable controls from read-only probe zones and can read/snapshot probe values after `ProcessBlock`; use this for acoustic graph passivity/radiation inspection, leave it off for normal parity renders.
- `PinkTromboneGraphDebugProbesWritePassivityReportWhenNativeFaustIsInstalled` streams the `nasal-vowel` graph with debug probes and writes `artifacts/parity/pink-trombone-graph-probes/<timestamp>/passivity-report.txt`. Latest run `20260527T200828013` shows same-path area and nasal-branch energy out/in ratios at 1.0, so current gross failure is not basic scatter energy explosion; next cuts should inspect frequency-dependent loss/radiation color and closure pressure storage.
- 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

@ -130,6 +130,84 @@ public sealed class PinkTromboneLogMelParityTests
File.WriteAllText(Path.Combine(artifactDir, "summary.txt"), string.Join(Environment.NewLine + Environment.NewLine, reports));
}
[Fact]
public void PinkTromboneGraphDebugProbesWritePassivityReportWhenNativeFaustIsInstalled()
{
var fixture = PinkTromboneParityFixtures.ById("nasal-vowel");
var export = FaustEmitter.EmitScript(
fixture.AquaScript,
new FaustExportOptions("pt_probe_nasal_vowel", DebugProbeUi: true));
var artifactDir = ArtifactPath("parity", "pink-trombone-graph-probes", DateTimeOffset.UtcNow.ToString("yyyyMMddTHHmmssfff", CultureInfo.InvariantCulture));
using var compiler = new AquaSynthPatchCompiler();
if (!compiler.TryCompileSource(
new AquaSynthCompileIdentity("pt_probe_nasal_vowel", "pt_probe_nasal_vowel", export.Source),
export.Source,
0.57f,
out var patch,
out var error))
{
if (error?.Contains("Faust toolchain not found", StringComparison.OrdinalIgnoreCase) == true ||
error?.Contains("Faust DLL not found", StringComparison.OrdinalIgnoreCase) == true)
{
return;
}
Assert.Fail($"Native Faust graph probe compile failed: {error}{Environment.NewLine}artifacts: {artifactDir}");
}
Directory.CreateDirectory(artifactDir);
File.WriteAllText(Path.Combine(artifactDir, "candidate-debug.dsp"), export.Source);
using (patch)
using (var stream = patch!.CreateStreamingPatch())
{
Assert.Contains(stream.ProbePaths, path => path.Contains("/energy_in", StringComparison.OrdinalIgnoreCase));
Assert.Contains(stream.ProbePaths, path => path.Contains("/energy_out", StringComparison.OrdinalIgnoreCase));
Assert.Contains(stream.ProbePaths, path => path.Contains("/radiation/", StringComparison.OrdinalIgnoreCase));
var peaks = new Dictionary<string, ProbePeak>(StringComparer.Ordinal);
var blockSize = 128;
var frames = Math.Max(1, (int)MathF.Ceiling(0.57f * patch.Manifest.SampleRate));
var inputs = Enumerable.Range(0, stream.InputCount).Select(_ => new float[blockSize]).ToArray();
var outputs = Enumerable.Range(0, stream.OutputCount).Select(_ => new float[blockSize]).ToArray();
for (var offset = 0; offset < frames; offset += blockSize)
{
var count = Math.Min(blockSize, frames - offset);
stream.ProcessBlock(inputs, outputs, count);
foreach (var (path, value) in stream.SnapshotProbes())
{
if (!TryProbeBase(path, out var basePath, out var kind))
{
continue;
}
peaks.TryGetValue(basePath, out var peak);
peak = kind == "energy_in"
? peak with { EnergyIn = Math.Max(peak.EnergyIn, MathF.Abs(value)) }
: peak with { EnergyOut = Math.Max(peak.EnergyOut, MathF.Abs(value)) };
peaks[basePath] = peak;
}
}
var lines = peaks
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.Select(pair =>
{
var ratio = pair.Value.EnergyOut / Math.Max(1e-6f, pair.Value.EnergyIn);
return string.Create(
CultureInfo.InvariantCulture,
$"{pair.Key},energy_in_peak={pair.Value.EnergyIn:0.000000},energy_out_peak={pair.Value.EnergyOut:0.000000},out_in_ratio={ratio:0.000000}");
})
.ToArray();
File.WriteAllLines(Path.Combine(artifactDir, "passivity-report.txt"), lines);
Assert.NotEmpty(lines);
Assert.Contains(lines, line => line.Contains("/connection/", StringComparison.OrdinalIgnoreCase));
Assert.Contains(lines, line => line.Contains("/area/", StringComparison.OrdinalIgnoreCase));
}
}
private static string Report(PinkTromboneParityFixture fixture, AudioComparison comparison, string candidate) =>
string.Create(
CultureInfo.InvariantCulture,
@ -153,6 +231,31 @@ public sealed class PinkTromboneLogMelParityTests
return "smoke-only";
}
private static bool TryProbeBase(string path, out string basePath, out string kind)
{
const string energyIn = "/energy_in";
const string energyOut = "/energy_out";
if (path.EndsWith(energyIn, StringComparison.OrdinalIgnoreCase))
{
basePath = path[..^energyIn.Length];
kind = "energy_in";
return true;
}
if (path.EndsWith(energyOut, StringComparison.OrdinalIgnoreCase))
{
basePath = path[..^energyOut.Length];
kind = "energy_out";
return true;
}
basePath = "";
kind = "";
return false;
}
private readonly record struct ProbePeak(float EnergyIn, float EnergyOut);
private static string AutomatedGraphSource(PinkTromboneUtteranceFixture fixture)
{
var source = FaustEmitter.EmitScript(UtteranceGraphScript(fixture), new FaustExportOptions($"pt_utterance_{fixture.Id}")).Source;