Expose graph probes through Faust UI

This commit is contained in:
Meta Vens 2026-05-27 18:51:23 +01:00
commit fa8c6e2fe9
5 changed files with 51 additions and 23 deletions

View file

@ -168,6 +168,11 @@ the only loudness fault. Ordinary two-port area discontinuities now expose the
same area-weighted energy probes, so vowel-body scattering can be inspected at
the layer where tract resonances should actually form.
By default those probes remain ordinary named Faust locals. When
`FaustExportOptions.DebugProbeUi` is enabled, the same signals are wrapped in
Faust `vbargraph` UI probes under `/debug/...`, exposing passivity and radiation
telemetry without changing the patch's audio output arity.
The next coherent implementation target is to make that law passive and
radiation-aware:

View file

@ -4,7 +4,7 @@ using System.Text;
namespace AquaSynth.Dsl;
public sealed record FaustExportOptions(string Name = "aquasynth_patch", bool Stereo = false);
public sealed record FaustExportOptions(string Name = "aquasynth_patch", bool Stereo = false, bool DebugProbeUi = false);
public sealed record FaustExport(string Source, IReadOnlyList<string> Warnings);
@ -77,13 +77,13 @@ public static class FaustEmitter
for (var i = 0; i < patch.Voices.Count; i++)
{
var name = $"voice_{i}";
EmitVoice(source, patch, patch.Voices[i], VoicePath(i), name, parameters, warnings);
EmitVoice(source, patch, patch.Voices[i], VoicePath(i), name, parameters, warnings, options);
voices.Add(name);
}
for (var i = 0; i < patch.SpectralBanks.Count; i++)
{
var name = $"spectral_{i}";
EmitSpectralBank(source, patch, patch.SpectralBanks[i], i, name, parameters, warnings);
EmitSpectralBank(source, patch, patch.SpectralBanks[i], i, name, parameters, warnings, options);
voices.Add(name);
}
for (var i = 0; i < patch.OperatorGraphs.Count; i++)
@ -140,6 +140,7 @@ public static class FaustEmitter
string name,
ParameterMap parameters,
List<string> warnings,
FaustExportOptions options,
string? oscillatorOverride = null)
{
if (voice.Filter.LowPassResonance != 0 ||
@ -183,12 +184,12 @@ public static class FaustEmitter
var dutyExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "osc/duty"), voice.Oscillator.Duty)} + {parameters.Expression(OwnerField(ownerPath, "duty/ramp"), voice.Duty.RampPerSecond)} * age + patch_mod_duty + {duty})";
var fmIndex = $"max(0.0, {parameters.Expression(OwnerField(ownerPath, "fm/index"), voice.Fm.Index)} + patch_mod_fm_index + {fmIndexMod}) * {FmDecay(parameters.Expression(OwnerField(ownerPath, "fm/decay"), voice.Fm.IndexDecaySeconds), voice.Fm.IndexDecaySeconds, parameters.IsBound(OwnerField(ownerPath, "fm/decay")))}";
var oscillator = oscillatorOverride ??
(voice.Tract is { Propagation: TractPropagationMode.Graph } && voice.AcousticNetwork is { } tractGraph
? AcousticNetworkExpression(source, patch, tractGraph, ownerPath, name, frequency, parameters, warnings)
(voice.Tract is { Propagation: TractPropagationMode.Graph } && voice.AcousticNetwork is { } tractGraph
? AcousticNetworkExpression(source, patch, tractGraph, ownerPath, name, frequency, parameters, warnings, options)
: voice.Tract is { } tract
? TractExpression(source, tract, ownerPath, name, frequency, noteGate, parameters)
: voice.AcousticNetwork is { } acousticNetwork
? AcousticNetworkExpression(source, patch, acousticNetwork, ownerPath, name, frequency, parameters, warnings)
? AcousticNetworkExpression(source, patch, acousticNetwork, ownerPath, name, frequency, parameters, warnings, options)
: OscillatorExpression(patch, voice, ownerPath, frequency, dutyExpression, fmIndex, parameters));
var envelope = voice.RateLevelEnvelope is not null
? RateLevelEnvelopeExpression(voice.RateLevelEnvelope, noteGate)
@ -299,7 +300,8 @@ public static class FaustEmitter
int bankIndex,
string name,
ParameterMap parameters,
List<string> warnings)
List<string> warnings,
FaustExportOptions options)
{
const int tableSize = 131072;
var table = PadSynthWaveform.Generate(bank, tableSize);
@ -311,7 +313,7 @@ public static class FaustEmitter
source.AppendLine($"{name}_read_frac = {name}_read_pos - float({name}_read_index);");
source.AppendLine($"{name}_read_next = ({name}_read_index + 1) % {tableSize};");
source.AppendLine($"{name}_wavetable = ({name}_wave, {name}_read_index : rdtable) * (1 - {name}_read_frac) + ({name}_wave, {name}_read_next : rdtable) * {name}_read_frac;");
EmitVoice(source, patch, bank.Treatment, SpectralPath(bankIndex), name, parameters, warnings, $"{name}_wavetable");
EmitVoice(source, patch, bank.Treatment, SpectralPath(bankIndex), name, parameters, warnings, options, $"{name}_wavetable");
}
private static string NoteFrequencyExpression(StringBuilder source, Playback playback, Note note, string name, string fieldPath, ParameterMap parameters)
@ -394,7 +396,8 @@ public static class FaustEmitter
string name,
string frequency,
ParameterMap parameters,
List<string> warnings)
List<string> warnings,
FaustExportOptions options)
{
var paths = patch.AcousticPaths.ToDictionary(path => path.Name, StringComparer.OrdinalIgnoreCase);
var sources = patch.AcousticSourcePorts.ToDictionary(port => port.Name, StringComparer.OrdinalIgnoreCase);
@ -411,7 +414,7 @@ public static class FaustEmitter
if (network.Terminals.Count > 0)
{
var graph = AcousticGraphExpression(source, patch, network, name, frequency, parameters, warnings, paths, sources, radiation, terminals, connections, waveClocks);
var graph = AcousticGraphExpression(source, patch, network, name, frequency, parameters, warnings, paths, sources, radiation, terminals, connections, waveClocks, options);
if (graph.Length > 0)
{
return graph;
@ -518,7 +521,8 @@ public static class FaustEmitter
IReadOnlyDictionary<string, AcousticRadiationPort> radiation,
IReadOnlyDictionary<string, AcousticTerminal> terminals,
IReadOnlyDictionary<string, AcousticConnection> connections,
IReadOnlyDictionary<string, WaveClockPolicy> waveClocks)
IReadOnlyDictionary<string, WaveClockPolicy> waveClocks,
FaustExportOptions options)
{
var waveClock = ResolveWaveClock(network, waveClocks, warnings, name);
var networkTerminals = new List<AcousticTerminal>();
@ -719,7 +723,7 @@ public static class FaustEmitter
}
var safeConnection = SafeIdentifier(connection.Name);
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports))
if (TryEmitThreePortBranchScatter(source, name, connection, safeConnection, ports, options))
{
continue;
}
@ -760,7 +764,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))
if (TryEmitTwoPortPathScatter(source, name, node, ports, sourceInjection, options))
{
// Emitted as a local area-discontinuity junction.
}
@ -800,8 +804,8 @@ public static class FaustEmitter
var admittance = $"{name}_graph_radiation_admittance_{safeTerminal}";
var flow = $"({string.Join(" + ", ports.Select(port => $"({GraphIncoming(name, port)} - {GraphOutgoing(name, port)})"))})";
var flowName = $"{name}_graph_radiation_flow_{safeTerminal}";
source.AppendLine($" {admittance} = sqrt(clip01(({terminalArea}) / max(0.000001, ({terminalArea}) + 1.0)));");
source.AppendLine($" {flowName} = {flow};");
source.AppendLine($" {admittance} = {ProbeSignal(options, $"/debug/{name}/radiation/{terminal.Name}/admittance", $"sqrt(clip01(({terminalArea}) / max(0.000001, ({terminalArea}) + 1.0)))", 0, 1)};");
source.AppendLine($" {flowName} = {ProbeSignal(options, $"/debug/{name}/radiation/{terminal.Name}/flow", flow, -2, 2)};");
radiated.Add($"(({flowName} * 0.30 + ({flowName} : {filter}) * 0.70) * {opening} * {loss} * {admittance})");
}
}
@ -1620,7 +1624,8 @@ public static class FaustEmitter
string voiceName,
AcousticConnection connection,
string safeConnection,
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports)
IReadOnlyList<(AcousticGraphNode node, AcousticTerminal terminal, AcousticGraphPort port, int nodePortCount)> ports,
FaustExportOptions options)
{
if (ports.Count != 3 || connection.Law != AcousticConnectionLaw.AreaScattering)
{
@ -1664,8 +1669,10 @@ public static class FaustEmitter
var sourceInjection = NodeSourceIdentifier(voiceName, item.node);
source.AppendLine($" {outgoing} = ({voiceName}_graph_connection_pressure_{safeConnection} - {incoming} + ({sourceInjection}) / {F(Math.Max(1, item.nodePortCount))}) * {voiceName}_graph_connection_loss_{safeConnection};");
}
source.AppendLine($" {voiceName}_graph_connection_energy_in_{safeConnection} = {string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphIncoming(voiceName, item.port)}, 2.0)"))};");
source.AppendLine($" {voiceName}_graph_connection_energy_out_{safeConnection} = {string.Join(" + ", portItems.Select(item => $"({areaExpressions[item.port]}) * pow({GraphOutgoing(voiceName, item.port)}, 2.0)"))};");
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)"));
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)};");
return true;
}
@ -1675,7 +1682,8 @@ public static class FaustEmitter
string voiceName,
AcousticGraphNode node,
IReadOnlyList<AcousticGraphPort> ports,
string sourceInjection)
string sourceInjection,
FaustExportOptions options)
{
if (ports.Count != 2 ||
!ports[0].Segment.Path.Name.Equals(ports[1].Segment.Path.Name, StringComparison.OrdinalIgnoreCase) ||
@ -1697,11 +1705,18 @@ public static class FaustEmitter
source.AppendLine($" {voiceName}_graph_area_reflection_{node.Name} = {reflection};");
source.AppendLine($" {firstOutgoing} = {secondIncoming} + {voiceName}_graph_area_reflection_{node.Name} * ({firstIncoming} + {secondIncoming}) + ({sourceInjection}) / 2;");
source.AppendLine($" {secondOutgoing} = {firstIncoming} - {voiceName}_graph_area_reflection_{node.Name} * ({firstIncoming} + {secondIncoming}) + ({sourceInjection}) / 2;");
source.AppendLine($" {voiceName}_graph_area_energy_in_{node.Name} = ({firstArea}) * pow({firstIncoming}, 2.0) + ({secondArea}) * pow({secondIncoming}, 2.0);");
source.AppendLine($" {voiceName}_graph_area_energy_out_{node.Name} = ({firstArea}) * pow({firstOutgoing}, 2.0) + ({secondArea}) * pow({secondOutgoing}, 2.0);");
var energyIn = $"({firstArea}) * pow({firstIncoming}, 2.0) + ({secondArea}) * pow({secondIncoming}, 2.0)";
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)};");
return true;
}
private static string ProbeSignal(FaustExportOptions options, string label, string expression, float min, float max) =>
options.DebugProbeUi
? $"(({expression}) : vbargraph(\"{Escape(label)}\", {F(min)}, {F(max)}))"
: expression;
private static string MaxExpression(IEnumerable<string> expressions)
{
using var enumerator = expressions.GetEnumerator();

View file

@ -185,3 +185,5 @@
{"evidence":"Moved graph source evaluation into recursive node context by emitting per-node incident pressure and node-local source sums, and moved generated tract injection source banks from graph nodes to cell centers with `1 / sections` index scaling. Focused structural and PT parity tests passed. Latest utterance artifact 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. Latest static artifact artifacts/parity/pink-trombone-logmel/20260527T172843945: closure-release rmsRatio 1.0078, cosine 0.6505.","claim":"Moving turbulence/release sources into graph node context and cell-centered geometry is architecturally correct even though papa articulation still worsens. PT-class source injection is cell-local wave excitation, not a click born on the radiation terminal; the remaining plosive failure points to missing upstream pressure storage rather than source placement.","timestamp":"2026-05-27T17:32:00+01:00","topic":"acoustic-graph-cell-centered-sources"}
{"evidence":"Added a release-timed burst-noise term to `TurbulenceJet` and softened radiation from an over-bright 15/85 raw/highpassed boundary-flow blend to 30/70 after local admittance weighting. Focused PT parity tests passed. Latest utterance artifact 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. Latest static artifact artifacts/parity/pink-trombone-logmel/20260527T174139661: closure-release rmsRatio 0.9900, speechBandRatio 0.0842.","claim":"Plosive release should include turbulent burst energy, and radiation should not be an almost pure differentiator when tract body is weak. The kept change is modest because stronger source burst did not solve papa; remaining speech failure is tract resonance/scattering and pressure storage, not source loudness.","timestamp":"2026-05-27T17:45:00+01:00","topic":"acoustic-graph-release-burst-radiation-slope"}
{"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":"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-27T17:50:00+01:00
updated: 2026-05-27T17:58:00+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,
@ -124,6 +124,7 @@ live_invariants:
- 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.
- `FaustExportOptions.DebugProbeUi` wraps graph probe signals in Faust `vbargraph` UI controls under `/debug/...` without changing audio output arity. Use it for acoustic graph passivity/radiation inspection; leave it off for normal parity renders.
- 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

@ -329,6 +329,11 @@ public sealed class PatchScriptTests
Assert.Contains("graph_radiation_admittance_voices_0_lip", export.Source);
Assert.Contains("graph_radiation_flow_voices_0_lip", export.Source);
Assert.Contains("patch_param_0) * (patch_param_0", export.Source);
var debugExport = FaustEmitter.Emit(patch, new FaustExportOptions("tract_graph_boundaries_debug", DebugProbeUi: true));
Assert.Contains("vbargraph(\"/debug/voice_0/connection/voices_0_nose_connection/energy_in\"", debugExport.Source);
Assert.Contains("vbargraph(\"/debug/voice_0/radiation/voices_0_lip/flow\"", debugExport.Source);
Assert.Contains("process = ", debugExport.Source);
}
[Fact]
@ -1282,7 +1287,7 @@ public sealed class PatchScriptTests
wave_clock name=continuous strategy=thiran order=1 max_delay=4096 smoothing_ms=3
acoustic_network name=humanish path=oral wave_clock=continuous sources=folds radiation=mouth,nostril terminals=trachea_bottom,oral_back,nasal_gate connections=velopharynx
acoustic network=humanish freq=130 gain=.1
""", new FaustExportOptions("acoustic_graph_validation"));
""", new FaustExportOptions("acoustic_graph_validation", DebugProbeUi: true));
var validation = await FaustCompiler.ValidateAsync(export.Source);
if (validation is null)