Feed radiation load back into acoustic graph

This commit is contained in:
Meta Vens 2026-05-29 12:37:32 +01:00
commit 25e8850f27
5 changed files with 147 additions and 10 deletions

View file

@ -514,3 +514,40 @@ Metrics still do not move. That is useful and damning: the source-local
reservoir sign/direction is no longer the main missing piece. The graph needs a
contact/closure primitive that participates in scattering and flow, not a
separate source pulse hoping the node will turn it into articulation.
## 2026-05-29 Radiation Boundary Load And Node-Law Cut
Radiation terminals are no longer output-only observers at one-port boundaries.
For a boundary radiation node, graph lowering now emits a named
`graph_radiation_boundary_load_*` value from the radiation admittance,
differentiation, and loss. The reflected boundary wave is filtered before it
returns to the delay line:
`reflected = raw * (1 - load) + lowpass(raw, radiation_cutoff) * load`
The output still reads boundary flow as `incoming - outgoing`, but the outgoing
wave now carries radiation load back into the tract. This is not a full
frequency-dependent radiation impedance model. It is the first graph-owned
load term that changes the pressure field instead of only coloring the output.
The remaining unconnected multi-port node fallback was also cut. Multi-port
nodes not owned by an explicit connection now use the same area-scattering
wave convention and emit node area-energy probes. The old
`graph_node_pressure_*` expression should not reappear as a quiet fallback.
Latest graph probe:
`artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T112805836`.
The new lip boundary load peaks at `0.516196`, lip flow remains alive at
`0.586863`, modal source out is `1.234403`, and the sibilant area-34 source
out is `0.099318`.
Latest utterance artifact:
`artifacts/parity/pink-trombone-utterance-logmel/20260529T112806745`.
The accepted-utterance diagnostic still fails articulation. `thrombosis`
cosine is `0.3287`, RMS ratio is `0.0770`, active ratio is `0.3151`, and
silence mismatch is `0.7205`. This confirms the boundary-load cut is a
coherence repair, not the missing speech primitive.
Next pressure remains contact as constrained-flow history and source impedance
as a port law. Do not interpret this pass as permission to restore global
drive or output EQ.

View file

@ -756,20 +756,18 @@ 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, debugProbeKeepAlive))
if (TryEmitRadiationBoundaryScatter(source, patch, name, node, ports, sourceInjection, radiation, parameters, options, debugProbeKeepAlive))
{
// Emitted as a radiation boundary load, so reflected energy is
// shaped before it re-enters the graph.
}
else if (TryEmitTwoPortPathScatter(source, name, node, ports, sourceInjection, options, debugProbeKeepAlive))
{
// Emitted as a local area-discontinuity junction.
}
else if (ports.Count > 1)
{
var nodePressure = $"{name}_graph_node_pressure_{node.Name}";
source.AppendLine($" {nodePressure} = 2.0 * ({string.Join(" + ", ports.Select(port => GraphIncoming(name, port)))}) / {F(ports.Count)};");
foreach (var port in ports)
{
var outgoing = GraphOutgoing(name, port);
var incoming = GraphIncoming(name, port);
source.AppendLine($" {outgoing} = ({nodePressure} - {incoming}) * (1.0 - min(0.98, abs({reflection}))) + {incoming} * {reflection} + ({sourceInjection}) / {F(ports.Count)};");
}
EmitUnconnectedNodeAreaScatter(source, name, node, ports, sourceInjection, options, debugProbeKeepAlive);
}
else foreach (var port in ports)
{
@ -1529,6 +1527,86 @@ public static class FaustEmitter
return true;
}
private static bool TryEmitRadiationBoundaryScatter(
StringBuilder source,
SynthPatch patch,
string voiceName,
AcousticGraphNode node,
IReadOnlyList<AcousticGraphPort> ports,
string sourceInjection,
IReadOnlyDictionary<string, AcousticRadiationPort> radiation,
ParameterMap parameters,
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
if (ports.Count != 1)
{
return false;
}
var terminal = node.Terminals.FirstOrDefault(terminal =>
terminal.Kind == AcousticTerminalKind.Radiation &&
radiation.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port));
if (terminal is null)
{
return false;
}
var radiationPort = radiation[terminal.Port.Length == 0 ? terminal.Name : terminal.Port];
var radiationIndex = AcousticRadiationPortIndex(patch, radiationPort);
var radiationPath = $"/acoustic/radiation/{radiationIndex}";
var safeTerminal = SafeIdentifier(terminal.Name);
var port = ports[0];
var incoming = GraphIncoming(voiceName, port);
var outgoing = GraphOutgoing(voiceName, port);
var reflection = $"{voiceName}_graph_terminal_reflection_{safeTerminal}";
var loss = $"clip01({parameters.Expression(OwnerField(radiationPath, "loss"), radiationPort.Loss)})";
var highpass = $"{voiceName}_graph_radiation_highpass_{safeTerminal}";
var admittance = $"{voiceName}_graph_radiation_admittance_{safeTerminal}";
var differentiation = $"{voiceName}_graph_radiation_differentiation_{safeTerminal}";
var load = $"{voiceName}_graph_radiation_boundary_load_{safeTerminal}";
var raw = $"{voiceName}_graph_radiation_reflected_raw_{safeTerminal}";
var loaded = $"{voiceName}_graph_radiation_reflected_loaded_{safeTerminal}";
source.AppendLine($" {load} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{terminal.Name}/boundary_load", $"clip01(({admittance}) * ({differentiation}) * ({loss}))", 0, 1)};");
source.AppendLine($" {raw} = ({incoming}) * ({reflection}) + ({sourceInjection});");
source.AppendLine($" {loaded} = ({raw}) * (1.0 - ({load})) + (({raw}) : fi.lowpass(1, {highpass})) * ({load});");
source.AppendLine($" {outgoing} = {loaded};");
debugProbeKeepAlive.Add(load);
return true;
}
private static void EmitUnconnectedNodeAreaScatter(
StringBuilder source,
string voiceName,
AcousticGraphNode node,
IReadOnlyList<AcousticGraphPort> ports,
string sourceInjection,
FaustExportOptions options,
List<string> debugProbeKeepAlive)
{
var areaSum = string.Join(" + ", ports.Select(port => GraphPortArea(voiceName, port)));
foreach (var port in ports)
{
var incoming = GraphIncoming(voiceName, port);
var outgoing = GraphOutgoing(voiceName, port);
var area = GraphPortArea(voiceName, port);
var reflection = $"{voiceName}_graph_node_area_reflection_{node.Name}_{port.Segment.Index}_{(port.AtStart ? "start" : "end")}";
var otherIncoming = string.Join(" + ", ports.Where(other => other != port).Select(other => GraphIncoming(voiceName, other)));
var scattered = $"{voiceName}_graph_node_area_scattered_{node.Name}_{port.Segment.Index}_{(port.AtStart ? "start" : "end")}";
source.AppendLine($" {reflection} = (2.0 * ({area}) - ({areaSum})) / max(0.000001, {areaSum});");
source.AppendLine($" {scattered} = {reflection} * {incoming} + (1.0 + {reflection}) * ({otherIncoming});");
source.AppendLine($" {outgoing} = {scattered} + ({sourceInjection}) / {F(Math.Max(1, ports.Count))};");
}
var energyIn = string.Join(" + ", ports.Select(port => $"({GraphPortArea(voiceName, port)}) * pow({GraphIncoming(voiceName, port)}, 2.0)"));
var energyOut = string.Join(" + ", ports.Select(port => $"({GraphPortArea(voiceName, port)}) * pow({GraphOutgoing(voiceName, port)}, 2.0)"));
source.AppendLine($" {voiceName}_graph_node_area_energy_in_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/node/{node.Name}/area_energy_in", energyIn, 0, 4)};");
source.AppendLine($" {voiceName}_graph_node_area_energy_out_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/node/{node.Name}/area_energy_out", energyOut, 0, 4)};");
debugProbeKeepAlive.Add($"{voiceName}_graph_node_area_energy_in_{node.Name}");
debugProbeKeepAlive.Add($"{voiceName}_graph_node_area_energy_out_{node.Name}");
}
private static string ProbeSignal(FaustExportOptions options, string label, string expression, float min, float max) =>
options.DebugProbeUi
? $"attach(({expression}), ({expression}) : vbargraph(\"{Escape(label)}\", {F(min)}, {F(max)}))"

View file

@ -219,3 +219,4 @@
{"evidence":"Added DebugProbeUi source/contact internals and an opt-in thrombosis graph timeline probe gated by `AQUASYNTH_RUN_GRAPH_PROBES=1`. Latest artifact `artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T011547091` shows modal source out 1.335230, modal flow 0.791726, modal pressure drive 0.741528, lip radiation flow 0.337191, lip radiation admittance 0.710362, sibilant injection source area-34 out 0.099288, and lip contact release out 0.032413. The opt-in probe plus graph Faust validation passed in 2m01s; the default focused PT diagnostic still passes with the probe test returning when the env var is absent.","claim":"The graph voice is alive upstream of the final artifact: source and radiation have real signal, while contact release is small/localized and output balance remains weak. Do not restore generic drive or add a blind gain compensator. The next coherent acoustic cut should be either contact/closure participation in scattering or an explicitly owned utterance/radiation output balance, decided from the timeline rather than final log-mel score alone.","timestamp":"2026-05-29T01:20:00+01:00","topic":"thrombosis-graph-timeline-probe"}
{"evidence":"Moved contact release out of the generic node source sum and into ordinary two-port path scattering. The new graph-owned closure primitive emits closure/reservoir/release/out probes and injects release directionally into downstream/upstream traveling waves. Latest opt-in thrombosis probe `artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T021934812`: lip contact out rose from prior 0.032413 to 0.548231, reservoir drive 0.519667, reservoir 6.024680, release 0.485894, lip radiation flow 0.581880, modal source out 1.233802, sibilant area-34 source out 0.099287. Latest accepted-utterance diagnostic `artifacts/parity/pink-trombone-utterance-logmel/20260529T022214016` still fails articulation: thrombosis RMS improves to 0.0761 but cosine stays about 0.331 and silence mismatch worsens to 0.7306. Focused structural graph tests, opt-in thrombosis probe, and accepted-utterance diagnostic passed under normal verbosity.","claim":"Closure pressure now participates in the graph path instead of acting as a decorative source pulse. This is the research-backed ownership cut, but it is not speech parity: contact release reaches radiation, while active-frame duration, output balance, and articulation shape remain weak. Next work should inspect contact filtering/duration or explicit utterance/radiation balance, not restore generic drive or global gain.","timestamp":"2026-05-29T02:30:00+01:00","topic":"graph-owned-closure-reservoir"}
{"date":"2026-05-29","area":"acoustic-graph","summary":"After graph-owned closure release moved into two-port scattering, thrombosis probe energy reaches lip radiation but accepted utterance parity still fails. The missing contracts are not more release gain: boundary/source impedance remain heuristic rather than port laws, contact is still a scalar pulse instead of constrained-flow history, a node pressure fallback survives for unconnected multi-port nodes, and loss/gesture motion are not yet graph-owned enough.","evidence":"docs/acoustic-graph-missing-pieces-audit.md; artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T021934812; artifacts/parity/pink-trombone-utterance-logmel/20260529T022214016"}
{"date":"2026-05-29","area":"acoustic-graph","summary":"Radiation boundaries now feed load back into the graph by filtering reflected boundary waves through a named boundary-load term before the outgoing wave returns to the delay line. The remaining unconnected multi-port node pressure fallback was cut and replaced with area-scattering plus node area-energy probes. Native PT probes still fail accepted utterance articulation, so this is a coherence repair: contact constrained-flow history and source impedance port laws remain the next missing owners.","evidence":"docs/acoustic-graph-dsp-audit.md; artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T112805836; artifacts/parity/pink-trombone-utterance-logmel/20260529T112806745"}

View file

@ -1,6 +1,6 @@
project: AquaSynth
spine_version: 1
updated: 2026-05-29T12:12:57+01:00
updated: 2026-05-29T12:33:04+01:00
prime_directive: >
Build coherent synth machinery. References exist to reveal missing structure,

View file

@ -412,12 +412,15 @@ public sealed class PatchScriptTests
Assert.Contains("graph_radiation_reference_area_voices_0_lip", export.Source);
Assert.Contains("graph_radiation_differentiation_voices_0_lip", export.Source);
Assert.Contains("graph_radiation_admittance_voices_0_lip", export.Source);
Assert.Contains("graph_radiation_boundary_load_voices_0_lip", export.Source);
Assert.Contains("graph_radiation_reflected_loaded_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/node/", debugExport.Source);
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/boundary_load\"", debugExport.Source);
Assert.Contains("vbargraph(\"/debug/voice_0/radiation/voices_0_lip/flow\"", debugExport.Source);
Assert.Contains("process = ", debugExport.Source);
}
@ -446,6 +449,24 @@ public sealed class PatchScriptTests
});
}
[Fact]
public void InteriorRadiationUsesAreaNodeScatterInsteadOfPressureFallback()
{
var export = FaustEmitter.EmitScript("""
path name=tube length_cm=18 diameters=.55,.8,1.1,.9
source_port name=reed path=tube kind=reed position=0 pressure=.55 tension=.4 opening=.35
radiation_port name=side_tap path=tube kind=vent position=.5 opening=.4 reflection=-.35
radiation_port name=mouth path=tube kind=lip position=1 opening=1.1 reflection=-.82
acoustic_network name=tapped path=tube sources=reed radiation=side_tap,mouth
acoustic network=tapped freq=180 gain=.1
""", new FaustExportOptions("interior_radiation_node", DebugProbeUi: true));
Assert.Contains("graph_node_area_scattered_", export.Source);
Assert.Contains("graph_node_area_energy_in_", export.Source);
Assert.Contains("vbargraph(\"/debug/voice_0/node/", export.Source);
Assert.DoesNotContain("graph_node_pressure_", export.Source);
}
[Fact]
public void TractAreaFunctionOwnsContinuousMorphology()
{