From 0cc03f8b750a9c5ec4f98d65bb43d10cd9ed9ce5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 28 May 2026 12:41:51 +0100 Subject: [PATCH] Name graph radiation impedance primitive --- docs/acoustic-graph-dsp-audit.md | 68 +++++++++++++++---- src/AquaSynth.Core/FaustExport.cs | 59 ++++++++++++---- state/evidence.jsonl | 1 + state/spine.yaml | 4 +- tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs | 2 + 5 files changed, 103 insertions(+), 31 deletions(-) diff --git a/docs/acoustic-graph-dsp-audit.md b/docs/acoustic-graph-dsp-audit.md index 8d65f96..66981e0 100644 --- a/docs/acoustic-graph-dsp-audit.md +++ b/docs/acoustic-graph-dsp-audit.md @@ -235,21 +235,25 @@ own contact loss scale and frequency-dependent wall loss. What it simulates: boundary flow emitted from lip, nostril, beak, or other openings. -Current state: less wrong after cutting generated lip double gates. Radiation -now reads boundary flow and generated tract lip opening is path geometry, not -also output aperture. +Current state: less wrong after cutting generated lip double gates and moving +the graph output constants into one named radiation impedance expression. +Radiation now reads boundary flow, generated tract lip opening is path +geometry, and emitted graph Faust names per-port reference area, +differentiation, high-pass cutoff, admittance, and flow. Smells: -- The 30/70 raw/high-pass blend is a voicing scar. It approximates radiation - color, but it is not a clear radiation impedance model. -- The admittance expression `sqrt(area / (area + 1.0))` contains an unexplained - unit constant. +- The graph no longer has inline `30/70` raw/high-pass blend or `area + 1.0` + admittance denominator, but the helper still uses compact empirical kind + constants. Those are now in one organ, not scattered through graph lowering. - `RadiationBoundaryReflectionExpression` is useful for standalone radiation openings, but dangerous when the path end area already represents aperture. +- Radiation still has no frequency-dependent load matched to tube radius and + characteristic impedance. -Verdict: keep boundary-flow readout. Replace color/admittance constants with a -named radiation impedance primitive. +Verdict: keep boundary-flow readout and the named primitive. Replace the +empirical primitive with a radius/characteristic-impedance model once source +and closure storage stop dominating the audible failure. ### Generated PT-Style Tract @@ -292,16 +296,20 @@ faster than global scores. Cut or replace these before piling on more tuning: -1. Unowned radiation constants: 30/70 high-pass blend and `area + 1.0`. -2. Unowned segment contact-loss constant `0.02`. -3. Turbulence/release scalar constants without a named pressure-flow model. -4. Nearest-node source attachment as the only interior source placement model. +1. Unowned segment contact-loss constant `0.02`. +2. Turbulence/release scalar constants without a named pressure-flow model. +3. Nearest-node source attachment as the only interior source placement model. Cut in the 2026-05-28 N-port pass: - Generic connection fallback pressure/admittance blend. - Shape-specific three-port branch scattering path. +Cut in the 2026-05-28 radiation pass: + +- Inline graph radiation `30/70` raw/high-pass blend. +- Inline graph radiation `area + 1.0` admittance denominator. + ## Keep List These foundations deserve to stay: @@ -323,8 +331,8 @@ radiation impedance: - Closure reservoir: a stateful pressure store attached to a constriction owner, charged while local area is sealed and released through the graph when area opens. -- Radiation impedance: replace output coloring constants with a named local - radiation model. +- Radiation impedance: upgrade the named empirical local model to a + radius/characteristic-impedance model. Until those exist, the graph can imitate some Pink Trombone pressure, but it is not yet a clean general vocal-acoustic machine. @@ -350,3 +358,33 @@ with peak `0.000009`. This is not a victory lap. It is a floor repair. The next real smells are still radiation impedance constants, graph-native closure reservoir state, and source impedance at junction nodes. + +## 2026-05-28 Radiation Primitive Cut + +The second audit cut moved graph radiation coloring and admittance into a named +local primitive. The emitter now names per-radiation-terminal: + +- `graph_radiation_reference_area_*` +- `graph_radiation_differentiation_*` +- `graph_radiation_highpass_*` +- `graph_radiation_admittance_*` +- `graph_radiation_flow_*` + +This removes the inline `flow * 0.30 + highpass(flow) * 0.70` expression and +the inline `area + 1.0` admittance denominator from graph lowering. The current +primitive is still empirical, but it has one owner. + +Latest utterance artifact: +`artifacts/parity/pink-trombone-utterance-logmel/20260528T113949841`. +`mama/papa/thrombosis` cosine is `0.7648/0.8550/0.4556`. `mama/papa` +candidate separability is `0.2044` versus reference `0.3791`, so this cleanup +does not fix articulation. + +Latest static artifact: +`artifacts/parity/pink-trombone-logmel/20260528T113949813`. +Static fixtures still pass; `closure-release` remains sealed-static evidence +with peak `0.000011`. + +The conclusion is plain: radiation needed an owner, but the voice still needs +graph-native closure pressure storage and source impedance before it will stop +leaning on boundary color. diff --git a/src/AquaSynth.Core/FaustExport.cs b/src/AquaSynth.Core/FaustExport.cs index f6733f1..2d4106d 100644 --- a/src/AquaSynth.Core/FaustExport.cs +++ b/src/AquaSynth.Core/FaustExport.cs @@ -490,12 +490,10 @@ public static class FaustEmitter var opening = $"max(0.0, {parameters.Expression(OwnerField(radiationPath, "opening"), port.Opening)})"; var reflection = $"min(1.0, abs({parameters.Expression(OwnerField(radiationPath, "reflection"), port.Reflection)}))"; var portLoss = $"clip01({parameters.Expression(OwnerField(radiationPath, "loss"), port.Loss)})"; - var highpass = port.Kind is AcousticRadiationKind.Lip or AcousticRadiationKind.Beak - ? "80.0" - : port.Kind == AcousticRadiationKind.Nostril - ? "40.0" - : "20.0"; - radiationExpressions.Add($"(({name}_acoustic_body : fi.highpass(1, {highpass})) * {opening} * (0.55 + 0.45 * {reflection}) * {portLoss})"); + var aperture = RadiationApertureExpression(patch, parameters, port); + var highpass = RadiationHighpassExpression(port.Kind, aperture, false); + var reflectionWeight = $"(0.55 + 0.45 * {reflection})"; + radiationExpressions.Add($"(({name}_acoustic_body : fi.highpass(1, {highpass})) * {opening} * {reflectionWeight} * {portLoss})"); } if (radiationExpressions.Count == 0) { @@ -798,20 +796,20 @@ public static class FaustEmitter var radiationPath = $"/acoustic/radiation/{radiationIndex}"; var opening = RadiationApertureExpression(patch, parameters, radiationPort); var loss = $"clip01({parameters.Expression(OwnerField(radiationPath, "loss"), radiationPort.Loss)})"; - var highpass = radiationPort.Kind is AcousticRadiationKind.Lip or AcousticRadiationKind.Beak - ? "220.0" - : radiationPort.Kind == AcousticRadiationKind.Nostril - ? "90.0" - : "20.0"; - var filter = $"fi.highpass(1, {highpass})"; var safeTerminal = SafeIdentifier(terminal.Name); var terminalArea = $"{name}_graph_terminal_area_{safeTerminal}"; + var referenceArea = $"{name}_graph_radiation_reference_area_{safeTerminal}"; + var differentiation = $"{name}_graph_radiation_differentiation_{safeTerminal}"; + var highpass = $"{name}_graph_radiation_highpass_{safeTerminal}"; 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} = {ProbeSignal(options, $"/debug/{name}/radiation/{terminal.Name}/admittance", $"sqrt(clip01(({terminalArea}) / max(0.000001, ({terminalArea}) + 1.0)))", 0, 1)};"); + source.AppendLine($" {referenceArea} = {RadiationReferenceAreaExpression(radiationPort.Kind, opening)};"); + source.AppendLine($" {differentiation} = {RadiationDifferentiationExpression(radiationPort.Kind, opening)};"); + source.AppendLine($" {highpass} = {RadiationHighpassExpression(radiationPort.Kind, opening, true)};"); + source.AppendLine($" {admittance} = {ProbeSignal(options, $"/debug/{name}/radiation/{terminal.Name}/admittance", $"sqrt(clip01(({terminalArea}) / max(0.000001, ({terminalArea}) + ({referenceArea}))))", 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) * {loss} * {admittance})"); + radiated.Add($"(({flowName} * (1.0 - ({differentiation})) + ({flowName} : fi.highpass(1, {highpass})) * ({differentiation})) * {loss} * {admittance})"); } } @@ -865,6 +863,39 @@ public static class FaustEmitter return $"(({closedReflection}) * (1.0 - clip01({aperture})) + ({openReflection}) * clip01({aperture}))"; } + private static string RadiationReferenceAreaExpression(AcousticRadiationKind kind, string aperture) => kind switch + { + AcousticRadiationKind.Lip or AcousticRadiationKind.Beak => $"max(0.18, 1.50 * 1.50 * (0.35 + 0.65 * clip01({aperture})))", + AcousticRadiationKind.Nostril => $"max(0.08, 0.70 * 0.70 * (0.50 + 0.50 * clip01({aperture})))", + AcousticRadiationKind.Membrane => "0.35", + AcousticRadiationKind.Vent => "0.80", + _ => "1.0" + }; + + private static string RadiationDifferentiationExpression(AcousticRadiationKind kind, string aperture) => kind switch + { + AcousticRadiationKind.Lip or AcousticRadiationKind.Beak => $"(0.55 + 0.25 * clip01({aperture}))", + AcousticRadiationKind.Nostril => $"(0.45 + 0.20 * clip01({aperture}))", + AcousticRadiationKind.Membrane => "0.20", + AcousticRadiationKind.Vent => $"(0.30 + 0.20 * clip01({aperture}))", + _ => "0.50" + }; + + private static string RadiationHighpassExpression(AcousticRadiationKind kind, string aperture, bool graph) => kind switch + { + AcousticRadiationKind.Lip or AcousticRadiationKind.Beak => graph + ? $"(120.0 + 180.0 * clip01({aperture}))" + : $"(60.0 + 50.0 * clip01({aperture}))", + AcousticRadiationKind.Nostril => graph + ? $"(55.0 + 75.0 * clip01({aperture}))" + : $"(30.0 + 30.0 * clip01({aperture}))", + AcousticRadiationKind.Membrane => "15.0", + AcousticRadiationKind.Vent => graph + ? $"(20.0 + 35.0 * clip01({aperture}))" + : $"(15.0 + 20.0 * clip01({aperture}))", + _ => "20.0" + }; + private static string AcousticSourceExpression( SynthPatch patch, AcousticSourcePort port, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 7dfddf6..00a8a4e 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -197,3 +197,4 @@ {"evidence":"Repaired graph voice sample-flow ownership: bounded the glottal source with local tanh normalization, added area-dependent segment propagation loss, removed the hidden 0.15 generated nasal side-port damper so velum diameter squared owns nasal admittance, stopped generated tract lips from binding lip opening into radiation-port opening, and stopped multiplying graph boundary flow by aperture after boundary reflection/path geometry already handle opening. Focused structural tests, utterance smoke, and static smoke passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T070759312`: mama/papa/thrombosis cosine 0.7918/0.8540/0.4366, candidate mama/papa separability 0.3651 versus reference 0.3791. Latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T071240442` treats closure-release as sealed static with peak 0.000005.","claim":"The phone-vibration collapse came from multiple model ownership leaks, not one missing gain knob: unbounded source dominance, immortal closed-section wave storage, damped nasal admittance, and lip radiation double-gating. The graph is still quiet and under-articulated, but different articulations now drive different sample-flow paths. Next fix is true closure reservoir/release through the scattering graph.","timestamp":"2026-05-28T07:15:00+01:00","topic":"acoustic-graph-voice-model-repair"} {"evidence":"Added `docs/acoustic-graph-dsp-audit.md`, auditing each graph-voice component by asking what it simulates and whether the implementation makes sense. The audit keeps path graphs with typed terminals, KL/PT two-port and nasal branch scatter, source/topology separation, velum side-port area, generated lip geometry ownership, area-dependent segment loss, and utterance separability. It marks the generic connection fallback pressure/admittance blend, radiation color/admittance constants, the segment loss constant, turbulence/release scalar constants, and nearest-node source attachment as Jenga. It recommends replacing the generic fallback with a passive general N-port junction, adding graph-native closure reservoir state, and replacing radiation constants with a named radiation impedance primitive.","claim":"The current graph voice is viable but not clean. Further tuning should start by cutting unowned fallback laws and constants, not by adding more source gain or metric-specific compensation.","timestamp":"2026-05-28T07:45:00+01:00","topic":"acoustic-graph-dsp-audit"} {"evidence":"Replaced the graph connection fallback in `FaustExport` with one general passive N-port area-scattering law for all non-bypass acoustic connections, removed the PT-shaped three-port branch recognizer, and updated structural tests to assert `graph_connection_reflection_*`/`graph_connection_energy_*` instead of `graph_connection_pressure_*`. Focused structural, utterance, and static PT graph tests passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T113334390`: mama/papa/thrombosis cosine 0.7799/0.8554/0.4555 with mama/papa candidate separability 0.2469 versus reference 0.3791. Latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T113334415` passes with sealed-static closure peak 0.000009.","claim":"Generic connection pressure/admittance fallback was unowned DSP authority. Non-bypass graph connections now share the same traveling-wave N-port area law, leaving closure storage, radiation impedance, and source impedance as the next real smells instead of topology-shaped compatibility code.","timestamp":"2026-05-28T11:45:00+01:00","topic":"acoustic-graph-nport-connection-cut"} +{"evidence":"Moved graph radiation output coloring and admittance constants into a named local radiation primitive in `FaustExport`: emitted graph Faust now names `graph_radiation_reference_area_*`, `graph_radiation_differentiation_*`, `graph_radiation_highpass_*`, `graph_radiation_admittance_*`, and `graph_radiation_flow_*` instead of inlining the old `flow * 0.30 + highpass(flow) * 0.70` and `area + 1.0` expressions. Focused structural/Faust checks, accepted utterance smoke, and static PT graph smoke passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T113949841`: mama/papa/thrombosis cosine 0.7648/0.8550/0.4556, mama/papa candidate separability 0.2044 versus reference 0.3791. Latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T113949813` passes with sealed-static closure peak 0.000011.","claim":"Radiation color was unowned inline math. It now has a single local primitive, but articulation did not materially improve, so the live audible failure is still closure pressure storage/source impedance rather than boundary coloring alone.","timestamp":"2026-05-28T12:05:00+01:00","topic":"acoustic-graph-radiation-primitive-cut"} diff --git a/state/spine.yaml b/state/spine.yaml index ee4b230..3072700 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -1,6 +1,6 @@ project: AquaSynth spine_version: 1 -updated: 2026-05-28T11:45:00+01:00 +updated: 2026-05-28T12:05:00+01:00 prime_directive: > Build coherent synth machinery. References exist to reveal missing structure, @@ -125,7 +125,7 @@ live_invariants: - 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. - Generated lip radiation terminals no longer bake first-frame lip opening into terminal `area_scale`; lip opening owns live path area plus radiation aperture. This removes a stale initial-state authority where words starting with closed lips kept closed-lip admittance. Static PT graph artifact `artifacts/parity/pink-trombone-logmel/20260527T231611586` passes, while utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260527T231056242` still fails `mama/papa` collapse and silent `thrombosis`. Next work remains source/reservoir/radiation coupling, not restoring terminal-scale double gates. - Graph voice repair pass on 2026-05-28 fixed four model-level ownership leaks: glottal excitation is source-normalized with local `tanh`; segment propagation loss is area-dependent so closures absorb instead of ringing forever; generated nasal side-port area maps velum diameter squared directly instead of through the old hidden 0.15 damper; generated tract lips use live path-end geometry as the aperture owner and no longer double-gate radiation opening/boundary flow. Utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T070759312` passes smoke and no longer collapses `mama/papa` (`referenceLogMel=0.3791`, `candidateLogMel=0.3651`); `thrombosis` improves to cosine 0.4366 but remains weak/quiet. Static artifact `artifacts/parity/pink-trombone-logmel/20260528T071240442` passes with `closure-release` demoted to sealed-static evidence (`peak=0.000005`). Next pressure is true graph-native closure reservoir/release through the scattering path. - - `docs/acoustic-graph-dsp-audit.md` is the current Jenga audit for graph voice DSP. It says the path graph, typed terminals, KL/PT two-port scatter, source/topology separation, velum side-port area, generated lip geometry ownership, area-dependent segment loss, and utterance separability should stay. The first audit cut replaced the generic connection fallback pressure/admittance blend and PT-shaped three-port branch recognizer with one passive N-port area-scattering law for all non-bypass connection groups; emitted Faust no longer contains `graph_connection_pressure_*` for graph connections. Latest artifacts after the cut are `artifacts/parity/pink-trombone-utterance-logmel/20260528T113334390` and `artifacts/parity/pink-trombone-logmel/20260528T113334415`; smoke passes, `thrombosis` improves slightly, and `mama/papa` remain differentiated but quiet/under-articulated. Remaining suspect owners are unowned radiation constants, unowned segment loss constant, turbulence/release scalar constants, and nearest-node source attachment. Next coherent build is graph-native closure reservoir plus a named radiation impedance primitive. + - `docs/acoustic-graph-dsp-audit.md` is the current Jenga audit for graph voice DSP. It says the path graph, typed terminals, KL/PT two-port scatter, source/topology separation, velum side-port area, generated lip geometry ownership, area-dependent segment loss, and utterance separability should stay. The first audit cut replaced the generic connection fallback pressure/admittance blend and PT-shaped three-port branch recognizer with one passive N-port area-scattering law for all non-bypass connection groups; emitted Faust no longer contains `graph_connection_pressure_*` for graph connections. The second audit cut moved graph radiation color/admittance into one named local primitive with emitted `graph_radiation_reference_area_*`, `graph_radiation_differentiation_*`, `graph_radiation_highpass_*`, `graph_radiation_admittance_*`, and `graph_radiation_flow_*` signals instead of inline `30/70` and `area + 1.0` expressions. Latest artifacts after radiation primitive extraction are `artifacts/parity/pink-trombone-utterance-logmel/20260528T113949841` and `artifacts/parity/pink-trombone-logmel/20260528T113949813`; smoke passes, but `mama/papa` separability and articulation did not materially improve. Remaining suspect owners are unowned segment loss constant, turbulence/release scalar constants, nearest-node source attachment, and the still-empirical radiation primitive. Next coherent build is graph-native closure reservoir/source impedance. - 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. diff --git a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs index c60877f..3728fb4 100644 --- a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs @@ -331,6 +331,8 @@ public sealed class PatchScriptTests Assert.Contains("graph_area_reflection_", export.Source); Assert.Contains("graph_area_energy_in_", export.Source); Assert.Contains("graph_area_energy_out_", export.Source); + 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_flow_voices_0_lip", export.Source); Assert.Contains("patch_param_0) * (patch_param_0", export.Source);