Feed radiation load back into acoustic graph
This commit is contained in:
parent
5e7d3ef7dc
commit
25e8850f27
5 changed files with 147 additions and 10 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)}))"
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue