From 2ee882c612e3f47b75eaa60f62ad07bf5095fc61 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 28 May 2026 14:57:40 +0100 Subject: [PATCH] Add graph source load and syrinx patch --- docs/acoustic-graph-dsp-audit.md | 27 ++++++ patches/advanced/bird-syrinx.aqua | 27 ++++++ patches/aquasynth-patch-cultcache.cc | Bin 17054 -> 17217 bytes patches/library.yaml | 5 + src/AquaSynth.Core/CultCachePatchDocument.cs | 1 + src/AquaSynth.Core/FaustExport.cs | 90 ++++++++++++++++-- src/AquaSynth.Core/Model.cs | 4 +- src/AquaSynth.Core/PatchScript.cs | 16 +++- state/evidence.jsonl | 1 + state/map.yaml | 1 + state/spine.yaml | 1 + tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs | 51 +++++++++- tools/TractGraphRenderer/Program.cs | 36 +++++++ 13 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 patches/advanced/bird-syrinx.aqua diff --git a/docs/acoustic-graph-dsp-audit.md b/docs/acoustic-graph-dsp-audit.md index 8a170d1..8af552b 100644 --- a/docs/acoustic-graph-dsp-audit.md +++ b/docs/acoustic-graph-dsp-audit.md @@ -60,6 +60,33 @@ warning instead of inventing a fallback body. Deletion line: do not restore a fake vocal renderer to make a test pass. If the graph cannot express a vocal behavior, add or fix graph semantics. +## 2026-05-28 Source Load And Contact Cut + +Owner: graph source ports own source impedance/load interaction; graph contact +terminals own closure pressure storage and release at constrictions. + +Inputs: source pressure/tension/opening/noise/balance/impedance, node incident +pressure, contact terminal area, and upstream/downstream traveling waves. + +Outputs: load-aware source flow terms, named `_load_pressure` locals for +non-turbulence sources, and named `graph_contact_*` closure/reservoir/release +signals for contact terminals. + +Derived state: generated tract area terminals are contact-capable graph +terminals. They still derive area from the live tract path geometry; they do +not become a separate morphology owner. + +Forbidden writers: plosive release is not allowed to live only inside a +TurbulenceJet source expression. A source-local burst can remain as excitation, +but graph contact state now has the authority to store and release pressure at +severe constrictions. + +Syrinx proof: `source_port kind=syrinx` is still the generic labial source +kind, now load-aware through the same source impedance path. The checked-in +`patches/advanced/bird-syrinx.aqua` patch uses two labial source ports, two +bronchial paths, one tracheal path, a three-terminal area-scattering merge, +and a beak radiation port. No species module was added. + ## Component Audit ### Topology Construction diff --git a/patches/advanced/bird-syrinx.aqua b/patches/advanced/bird-syrinx.aqua new file mode 100644 index 0000000..c88ab7c --- /dev/null +++ b/patches/advanced/bird-syrinx.aqua @@ -0,0 +1,27 @@ +patch gain=0.42 soft_clip=true + +param path=/bird/left/pressure default=.82 min=0 max=1 step=.001 +param path=/bird/right/pressure default=.70 min=0 max=1 step=.001 +param path=/bird/left/opening default=.24 min=0 max=1 step=.001 +param path=/bird/right/opening default=.21 min=0 max=1 step=.001 +param path=/bird/left/tension default=.38 min=0 max=1 step=.001 +param path=/bird/right/tension default=.45 min=0 max=1 step=.001 +param path=/bird/load default=.85 min=0 max=2 step=.001 +param path=/bird/beak/opening default=.95 min=0 max=1.5 step=.001 + +path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42 +path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40 +path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46 + +source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=@/bird/left/pressure tension=@/bird/left/tension opening=@/bird/left/opening noise=.025 impedance=@/bird/load +source_port name=right_labium path=right_bronchus kind=syrinx position=0 pressure=@/bird/right/pressure tension=@/bird/right/tension opening=@/bird/right/opening noise=.02 balance=.96 impedance=@/bird/load + +terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1 +terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1 +terminal name=trachea_base path=trachea position=0 kind=junction area_scale=1 +connect name=syrinx_merge terminals=left_merge,right_merge,trachea_base law=area_scatter coupling=1 + +radiation_port name=beak path=trachea kind=beak position=1 opening=@/bird/beak/opening reflection=-.72 +wave_clock name=bird_clock strategy=linear max_delay=1024 smoothing_ms=2 +acoustic_network name=bird_syrinx path=trachea wave_clock=bird_clock sources=left_labium,right_labium radiation=beak terminals=left_merge,right_merge,trachea_base connections=syrinx_merge +acoustic network=bird_syrinx freq=1120 gain=.32 sustain=1.08 decay=.08 diff --git a/patches/aquasynth-patch-cultcache.cc b/patches/aquasynth-patch-cultcache.cc index ecec114d93ea2a5c582c848a1ba999b2b4e445af..5b1e366497abb9334030b930a0e9fed9756104ec 100644 GIT binary patch delta 212 zcmbQ&%6PDiaYHn_gt3*0xs|Dro~fCMnX$RCwt<0_fx%JhbCZkNZ!xND_TwmKu2(Eb zEJ@BtE!Iy=DND>tPEFBI$}CFJEv_ue%&X8#EG$i20h3vcB(u}KD6t?zH!rayvn*8s zqFMoL)J?yD#LS}96os6`q|C$|h2s3uqU2QFg8ZTq1&{#>W%-%OsS0WNMG7gIX^Ls7 pMX7lunTbg`sR|`UiJ5tsdFcv?c_|8sr74*unfZB}_i;Zk0{~)DPS5}V delta 54 zcmX@u#yGE)aYHn_gprl0xs`#jo~fa!g{7H+wt<0_fx%I0?a9ULw-}W-`*9RAPyWjz Kw7HDuv>5=c)evd` diff --git a/patches/library.yaml b/patches/library.yaml index 4109863..9a3dcff 100644 --- a/patches/library.yaml +++ b/patches/library.yaml @@ -37,6 +37,11 @@ entries: name: "aurora-pad" source: "BuiltInScripts.ReferenceScripts" notes: "Stock reference patch exported from the in-code catalog." + - path: patches/advanced/bird-syrinx.aqua + family: advanced + name: "bird-syrinx" + source: "Graph-native syrinx patch" + notes: "Paired labial source-port bird voice for differentiable training and audition." - path: patches/advanced/glass-creature.aqua family: advanced name: "glass-creature" diff --git a/src/AquaSynth.Core/CultCachePatchDocument.cs b/src/AquaSynth.Core/CultCachePatchDocument.cs index 0b51b36..f1effb3 100644 --- a/src/AquaSynth.Core/CultCachePatchDocument.cs +++ b/src/AquaSynth.Core/CultCachePatchDocument.cs @@ -140,6 +140,7 @@ public static class CultCachePatchDocumentCatalog new("patches/808/snare.aqua", "808", "snare", "BuiltInScripts.ReferenceScripts", "Stock reference patch exported from the in-code catalog."), new("patches/808/tom.aqua", "808", "tom", "BuiltInScripts.ReferenceScripts", "Stock reference patch exported from the in-code catalog."), new("patches/advanced/aurora-pad.aqua", "advanced", "aurora-pad", "BuiltInScripts.ReferenceScripts", "Stock reference patch exported from the in-code catalog."), + new("patches/advanced/bird-syrinx.aqua", "advanced", "bird-syrinx", "Graph-native syrinx patch", "Paired labial source-port bird voice for differentiable training and audition."), new("patches/advanced/glass-creature.aqua", "advanced", "glass-creature", "BuiltInScripts.ReferenceScripts", "Stock reference patch exported from the in-code catalog."), new("patches/advanced/machine-breath.aqua", "advanced", "machine-breath", "BuiltInScripts.ReferenceScripts", "Stock reference patch exported from the in-code catalog."), new("patches/advanced/ritual-sequence.aqua", "advanced", "ritual-sequence", "BuiltInScripts.ReferenceScripts", "Stock reference patch exported from the in-code catalog."), diff --git a/src/AquaSynth.Core/FaustExport.cs b/src/AquaSynth.Core/FaustExport.cs index b9d758f..007bead 100644 --- a/src/AquaSynth.Core/FaustExport.cs +++ b/src/AquaSynth.Core/FaustExport.cs @@ -838,6 +838,13 @@ public static class FaustEmitter var noise = $"clip01({parameters.Expression(OwnerField(path, "noise"), port.Noise)})"; var transient = $"clip01({parameters.Expression(OwnerField(path, "transient"), port.Transient)})"; var balance = $"({parameters.Expression(OwnerField(path, "balance"), port.Balance)}) * ({SourcePositionWeightExpression(port, path, parameters)})"; + var impedance = $"max(0.0, {parameters.Expression(OwnerField(path, "impedance"), port.Impedance)})"; + var effectivePressure = localPressure is null + ? pressure + : $"clip01(({pressure}) - 0.14 * ({impedance}) * ({localPressure}))"; + var load = localPressure is null + ? "1.0" + : $"(1.0 / (1.0 + 0.25 * ({impedance}) * abs({localPressure})))"; var detune = port.Kind == AcousticSourceKind.Labial ? $" * (1.0 + ({balance} - 0.5) * 0.018)" : ""; var phase = $"os.phasor(1.0, {frequency}{detune})"; var closure = $"clip01((0.12 - ({opening})) / 0.12)"; @@ -854,17 +861,17 @@ public static class FaustEmitter return port.Kind switch { AcousticSourceKind.Glottal => - $"{GlottalSourceExpression(phase, tension, opening, pressure, noise, balance)}", + $"{GlottalSourceExpression(phase, tension, opening, effectivePressure, noise, balance)} * {load}", AcousticSourceKind.Labial => - $"((sin(2.0 * ma.PI * {phase}) - {tension} * 0.35 * sin(4.0 * ma.PI * {phase})) * {pressure} * {opening} + no.noise * {noise} * {pressure} * (1.0 - {tension})) * {balance}", + $"(ma.tanh((sin(2.0 * ma.PI * {phase}) - {tension} * 0.35 * sin(4.0 * ma.PI * {phase})) * (2.0 + 8.0 * {effectivePressure})) * {effectivePressure} * {opening} + no.noise * {noise} * {effectivePressure} * (1.0 - {tension})) * {balance} * {load}", AcousticSourceKind.Reed => - $"(ma.tanh(sin(2.0 * ma.PI * {phase}) * (1.0 + {pressure} * 8.0)) * {opening} + no.noise * {noise} * 0.2) * {balance}", + $"(ma.tanh(sin(2.0 * ma.PI * {phase}) * (1.0 + {effectivePressure} * 8.0)) * {opening} + no.noise * {noise} * 0.2) * {balance} * {load}", AcousticSourceKind.TurbulenceJet => - $"(({turbulence}) * {noise} * {pressure} * ({pressureDrive}) * ({apertureNoiseGate}) * 0.62 + ({turbulence}) * {noise} * {transient} * ({pressureDrive}) * ({releaseBurstGate}) * 1.2 + {transient} * {pressure} * {release} * {releasePressure} * 0.58 * (0.8 + 0.8 * clip01({opening} / 0.8))) * {balance}", + $"(({turbulence}) * {noise} * {effectivePressure} * ({pressureDrive}) * ({apertureNoiseGate}) * 0.62 + ({turbulence}) * {noise} * {transient} * ({pressureDrive}) * ({releaseBurstGate}) * 1.2 + {transient} * {effectivePressure} * {release} * {releasePressure} * 0.58 * (0.8 + 0.8 * clip01({opening} / 0.8))) * {balance} * {load}", AcousticSourceKind.Click => - $"no.noise * {pressure} * max({opening}, {transient}) * exp(0.0 - age * 120.0) * {balance}", + $"no.noise * {effectivePressure} * max({opening}, {transient}) * exp(0.0 - age * 120.0) * {balance} * {load}", AcousticSourceKind.Synthetic => - $"sin(2.0 * ma.PI * {phase}) * {pressure} * {opening} * {balance}", + $"sin(2.0 * ma.PI * {phase}) * {effectivePressure} * {opening} * {balance} * {load}", _ => "0.0" }; } @@ -1357,7 +1364,10 @@ public static class FaustEmitter { if (ports.Count != 2 || !ports[0].Segment.Path.Name.Equals(ports[1].Segment.Path.Name, StringComparison.OrdinalIgnoreCase) || - node.Terminals.Any(terminal => terminal.Kind != AcousticTerminalKind.Junction && terminal.Kind != AcousticTerminalKind.Source)) + node.Terminals.Any(terminal => + terminal.Kind != AcousticTerminalKind.Junction && + terminal.Kind != AcousticTerminalKind.Source && + terminal.Kind != AcousticTerminalKind.Contact)) { return false; } @@ -1419,6 +1429,10 @@ public static class FaustEmitter string frequency, ParameterMap parameters) { + var contactTerms = node.Terminals + .Where(terminal => terminal.Kind == AcousticTerminalKind.Contact) + .Select(terminal => EmitGraphContactSourceExpression(source, voiceName, node, nodePorts, terminal)) + .ToList(); var sourceTerms = node.Terminals .Where(terminal => terminal.Kind == AcousticTerminalKind.Source && sources.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)) .Select(terminal => @@ -1427,12 +1441,53 @@ public static class FaustEmitter var port = sources[sourceName]; return port.Kind == AcousticSourceKind.TurbulenceJet ? EmitGraphTurbulenceSourceExpression(source, patch, voiceName, node, nodePorts, sourceName, port, parameters) - : $"{voiceName}_graph_source_{SafeIdentifier(sourceName)}"; + : EmitGraphLoadedSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters); }) .ToList(); + sourceTerms.AddRange(contactTerms); return sourceTerms.Count == 0 ? "0.0" : string.Join(" + ", sourceTerms); } + private static string EmitGraphLoadedSourceExpression( + StringBuilder source, + SynthPatch patch, + string voiceName, + AcousticGraphNode node, + string sourceName, + AcousticSourcePort port, + string frequency, + ParameterMap parameters) + { + var safeSource = SafeIdentifier(sourceName); + var safeNode = SafeIdentifier(node.Name); + var prefix = $"{voiceName}_graph_source_{safeSource}_{safeNode}"; + var localPressure = $"{voiceName}_graph_node_incident_pressure_{node.Name}"; + source.AppendLine($" {prefix}_load_pressure = {localPressure};"); + source.AppendLine($" {prefix}_out = {AcousticSourceExpression(patch, port, frequency, parameters, localPressure)};"); + return $"{prefix}_out"; + } + + private static string EmitGraphContactSourceExpression( + StringBuilder source, + string voiceName, + AcousticGraphNode node, + IReadOnlyList nodePorts, + AcousticTerminal terminal) + { + var safeTerminal = SafeIdentifier(terminal.Name); + var prefix = $"{voiceName}_graph_contact_{safeTerminal}_{node.Name}"; + var terminalArea = $"{voiceName}_graph_terminal_area_{safeTerminal}"; + var localPressure = $"{voiceName}_graph_node_incident_pressure_{node.Name}"; + var drive = ContactReservoirDriveExpression(voiceName, node, nodePorts, localPressure); + source.AppendLine($" {prefix}_closure = clip01((0.0144 - ({terminalArea})) / 0.0144);"); + source.AppendLine($" {prefix}_release = (max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.992));"); + source.AppendLine($" {prefix}_reservoir_drive = {drive};"); + source.AppendLine($" {prefix}_reservoir = {prefix}_reservoir_drive * {prefix}_closure : + ~ *(0.994);"); + source.AppendLine($" {prefix}_release_pressure = 0.12 + 3.20 * clip01({prefix}_reservoir);"); + source.AppendLine($" {prefix}_out = {prefix}_release * {prefix}_release_pressure * (0.35 + 0.65 * clip01(abs({voiceName}_graph_terminal_reflection_{safeTerminal})));"); + return $"{prefix}_out"; + } + private static string EmitGraphTurbulenceSourceExpression( StringBuilder source, SynthPatch patch, @@ -1495,6 +1550,25 @@ public static class FaustEmitter : $"max(0.0, {fallbackPressure})"; } + private static string ContactReservoirDriveExpression( + string voiceName, + AcousticGraphNode node, + IReadOnlyList nodePorts, + string fallbackPressure) + { + var upstream = nodePorts + .Where(port => !port.AtStart && port.Segment.EndPosition <= node.Position + 0.0001f) + .OrderByDescending(port => port.Segment.StartPosition) + .FirstOrDefault(); + var downstream = nodePorts + .Where(port => port.AtStart && port.Segment.StartPosition >= node.Position - 0.0001f) + .OrderBy(port => port.Segment.EndPosition) + .FirstOrDefault(); + return upstream is not null && downstream is not null + ? $"max(0.0, {GraphIncoming(voiceName, upstream)} - {GraphIncoming(voiceName, downstream)})" + : $"max(0.0, {fallbackPressure})"; + } + private static string GraphIncoming(string voiceName, AcousticGraphPort port) => port.AtStart ? $"{voiceName}_graph_in_{port.Segment.Index}_start" diff --git a/src/AquaSynth.Core/Model.cs b/src/AquaSynth.Core/Model.cs index 7cbfec9..c05db1d 100644 --- a/src/AquaSynth.Core/Model.cs +++ b/src/AquaSynth.Core/Model.cs @@ -266,6 +266,7 @@ public enum AcousticTerminalKind Junction, Source, Radiation, + Contact, Open, Closed, Probe @@ -310,7 +311,8 @@ public sealed record AcousticSourcePort( float Transient = 0, float Balance = 1, bool Active = true, - AcousticSourcePositionControl? PositionControl = null); + AcousticSourcePositionControl? PositionControl = null, + float Impedance = 0.35f); public sealed record AcousticSourcePositionControl( float Index = 0, diff --git a/src/AquaSynth.Core/PatchScript.cs b/src/AquaSynth.Core/PatchScript.cs index 28c642b..31491ca 100644 --- a/src/AquaSynth.Core/PatchScript.cs +++ b/src/AquaSynth.Core/PatchScript.cs @@ -576,7 +576,8 @@ public static class PatchScript GetBoundFloat(fields, line, 0, $"/acoustic/sources/{_acousticSourcePorts.Count}/transient", "transient", "burst"), GetBoundFloat(fields, line, 1, $"/acoustic/sources/{_acousticSourcePorts.Count}/balance", "balance"), TryGetAny(fields, ["active"], out var active) ? ParseBool(active, line) : true, - ParseAcousticSourcePositionControl(fields, line, _acousticSourcePorts.Count)); + ParseAcousticSourcePositionControl(fields, line, _acousticSourcePorts.Count), + GetBoundFloat(fields, line, 0.35f, $"/acoustic/sources/{_acousticSourcePorts.Count}/impedance", "impedance", "load", "source_impedance")); AddAcousticSourcePortRecord(port); AddAcousticTerminalRecord(new AcousticTerminal( port.Name, @@ -1334,7 +1335,12 @@ public static class PatchScript tract.Intensity, tract.Tenseness, tract.Glottis?.Skew ?? 0.42f, - tract.Glottis?.Aspiration ?? 0.08f); + tract.Glottis?.Aspiration ?? 0.08f, + 0, + 1, + true, + null, + 0.10f); var sourceIndex = _acousticSourcePorts.Count; AddAcousticSourcePortRecord(source); MirrorParameterBinding(OwnerField(tractPath, "intensity"), $"/acoustic/sources/{sourceIndex}/pressure"); @@ -1357,7 +1363,7 @@ public static class PatchScript $"{prefix}_area_{section}", primaryPathName, position, - AcousticTerminalKind.Junction, + AcousticTerminalKind.Contact, "", 1, 0); @@ -1392,7 +1398,8 @@ public static class PatchScript injection.Burst, 1, true, - positionControl); + positionControl, + 0.20f); var injectionSourceIndex = _acousticSourcePorts.Count; AddAcousticSourcePortRecord(injectionSource); MirrorParameterBinding(OwnerField(tractPath, "turbulence"), $"/acoustic/sources/{injectionSourceIndex}/pressure"); @@ -2628,6 +2635,7 @@ public static class PatchScript "junction" or "node" or "scatter" => AcousticTerminalKind.Junction, "source" or "excitation" => AcousticTerminalKind.Source, "radiation" or "radiator" or "output" => AcousticTerminalKind.Radiation, + "contact" or "closure" or "obstruction" => AcousticTerminalKind.Contact, "open" => AcousticTerminalKind.Open, "closed" or "wall" => AcousticTerminalKind.Closed, "probe" or "diagnostic" => AcousticTerminalKind.Probe, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 041acec..38495d3 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -202,3 +202,4 @@ {"evidence":"Changed graph source-local closure reservoir charging from `abs(incident_pressure) * closure` to `max(0, incident_pressure) * closure`, so alternating negative pressure no longer charges a plosive reservoir. Accepted utterance smoke and static PT graph smoke passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T114903720`; latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T114953503`. Metrics did not move from the reservoir-naming pass.","claim":"Plosive storage should charge from compression, not rectified vibration. The lack of metric movement says the current audible failure is not the reservoir sign alone; Aqua still needs a contact-aware upstream/downstream closure state and source impedance instead of only a source-local scalar reservoir.","timestamp":"2026-05-28T12:55:00+01:00","topic":"acoustic-graph-positive-pressure-reservoir"} {"evidence":"Changed graph turbulence/release reservoir drive to use directed upstream-minus-downstream pressure at ordinary two-port path nodes (`max(0, upstream_incoming - downstream_incoming)`), with positive local incident pressure as the fallback for boundaries or more complex nodes. Focused structural/Faust checks, accepted utterance smoke, and static PT graph smoke passed. Latest utterance artifact `artifacts/parity/pink-trombone-utterance-logmel/20260528T115452268`; latest static artifact `artifacts/parity/pink-trombone-logmel/20260528T115719882`. Metrics still did not move.","claim":"Source-local reservoir direction is no longer the main explanation for weak plosives. The graph needs a contact/closure primitive that participates in scattering and flow, rather than a separate source pulse injected after the node law.","timestamp":"2026-05-28T13:05:00+01:00","topic":"acoustic-graph-directed-reservoir-drive"} {"evidence":"Removed legacy vocal audio authorities: `TractPropagationMode` now only accepts graph lowering, parser legacy values fail fast, the Faust `Voice.Tract` resonator/waveguide renderers were deleted, the acoustic response proxy fallback was removed, `WaveguideLoss` became graph `PropagationLoss`, `substeps` was rejected, and the Pink Trombone built-in plus graph tests now route through typed `AcousticPortNetwork` lowering. Verification: stale-symbol scan only finds historical evidence and explicit rejection tests; full DSL test suite passed 180 tests.","claim":"Vocal acoustics now have one live authority: the typed acoustic graph. `Voice.Tract`, PT-shaped commands, and built-ins are authoring sugar that populate graph records; they cannot secretly render old proxy/waveguide audio when the graph is missing or weak.","timestamp":"2026-05-28T13:40:00+01:00","topic":"legacy-vocal-renderer-cut"} +{"evidence":"Added `AcousticSourcePort.Impedance`, parser aliases `impedance`/`load`/`source_impedance`, load-aware graph source lowering using node incident pressure, graph contact terminals with named closure/reservoir/release locals, generated tract contact terminals, a syrinx structural test, TractGraphRenderer `mode=syrinx`, and `patches/advanced/bird-syrinx.aqua`. Rendered `artifacts/audition/syrinx-graph-20260528b.wav` and `artifacts/audition/tract-contact-graph-20260528b.wav`; focused graph/PT parity smokes passed.","claim":"Source-tract coupling and closure storage are now graph-owned reusable semantics, not Pink Trombone-shaped gain hacks. A syrinx is expressible as paired loaded labial source ports feeding one acoustic graph through a merge junction and beak radiation, giving Mimir a differentiable bird-voice target surface.","timestamp":"2026-05-28T14:20:00+01:00","topic":"graph-source-load-contact-syrinx"} diff --git a/state/map.yaml b/state/map.yaml index 1f77a63..bf91a37 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -51,6 +51,7 @@ modules: acoustic_graph_lowering: src/AquaSynth.Core/FaustExport.cs pink_trombone_parity_fixtures: src/AquaSynth.Core/PinkTromboneParityFixtures.cs pink_trombone_utterance_fixtures: src/AquaSynth.Core/PinkTromboneUtteranceFixtures.cs + bird_syrinx_patch: patches/advanced/bird-syrinx.aqua pink_trombone_logmel_parity_tests: tests/AquaSynth.Dsl.Tests/PinkTromboneLogMelParityTests.cs pink_trombone_utterance_reference_tests: tests/AquaSynth.Dsl.Tests/PinkTromboneUtteranceReferenceTests.cs faust_emitter: src/AquaSynth.Core/FaustExport.cs diff --git a/state/spine.yaml b/state/spine.yaml index 891a502..e68a630 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -127,6 +127,7 @@ live_invariants: - 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. 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. The third audit cut hoisted graph turbulence/release into named per-source `_closure`, `_release`, `_reservoir`, `_reservoir_drive`, `_pressure_drive`, `_release_pressure`, `_noise_gate`, `_release_gate`, `_sustained`, `_burst_noise`, and `_release_pulse` locals. Reservoir charging now uses directed upstream-minus-downstream pressure for ordinary two-port path nodes and positive local incident pressure as fallback; latest artifacts `artifacts/parity/pink-trombone-utterance-logmel/20260528T115452268` and `artifacts/parity/pink-trombone-logmel/20260528T115719882` still show no metric movement. This points away from source-local reservoir charging and toward missing contact-aware closure state that participates in scattering/flow. Remaining suspect owners are unowned segment loss constant, still-empirical radiation/source constants, nearest-node source attachment, and missing contact-aware closure state. - 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. + - Source ports now have an explicit `impedance`/`load` control and graph lowering uses node incident pressure to make glottal, labial/syrinx, reed, click, synthetic, and turbulence sources load-aware. Generated tract area terminals are now `Contact` terminals, with named graph-owned `graph_contact_*_closure`, `_reservoir`, and `_release` signals so closure pressure storage is no longer only a turbulence-source trick. `patches/advanced/bird-syrinx.aqua` is the first checked-in differentiable syrinx patch: paired labial source ports, bronchial paths, a tracheal path, an area-scattering merge, and beak radiation, all through `AcousticPortNetwork`. - 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. diff --git a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs index 7752449..c5a284c 100644 --- a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs @@ -133,6 +133,7 @@ public sealed class PatchScriptTests Assert.Contains(patch.AcousticSourcePorts, port => port.Name.StartsWith("voices_0_sibilant_", StringComparison.Ordinal) && port.Kind == AcousticSourceKind.TurbulenceJet); Assert.Contains(patch.AcousticBranches, branch => branch.Name == "voices_0_nose" && branch.Kind == AcousticBranchKind.Nasal); Assert.Contains(patch.AcousticRadiationPorts, port => port.Name == "voices_0_lip" && port.Kind == AcousticRadiationKind.Lip); + Assert.Contains(patch.AcousticTerminals, terminal => terminal.Name.StartsWith("voices_0_area_", StringComparison.Ordinal) && terminal.Kind == AcousticTerminalKind.Contact); Assert.Contains(patch.WaveClocks, clock => clock.Name == "voices_0_clock" && clock.Strategy == WaveClockDelayStrategy.FractionalLinear); Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/glottis/0/tenseness"); @@ -143,6 +144,7 @@ public sealed class PatchScriptTests Assert.Contains("graph_source_voices_0_sibilant_", faust); Assert.Contains("graph_terminal_area_voices_0_area_", faust); Assert.Contains("graph_connection_reflection_voices_0_nose_connection", faust); + Assert.Contains("graph_contact_voices_0_area_", faust); Assert.Contains("de.fdelay", faust); Assert.Contains("_reservoir_drive", faust); Assert.Contains("_release_pulse", faust); @@ -155,11 +157,12 @@ public sealed class PatchScriptTests { var patch = PatchScript.Parse(""" param path=/voice/left_pressure default=.7 min=0 max=1 step=.001 + param path=/voice/source_load default=.65 min=0 max=2 step=.001 param path=/voice/throat_opening default=.8 min=0 max=1 step=.001 param path=/voice/mouth_opening default=1.2 min=0 max=2 step=.001 path name=trachea length_cm=12 diameters=.4,.7,1,1 path name=oral length_cm=17 diameters=.6,1.1,1.6,1.2,.8 - source_port name=left_labium path=trachea kind=labial position=0 pressure=@/voice/left_pressure tension=.55 opening=.4 noise=.02 + source_port name=left_labium path=trachea kind=labial position=0 pressure=@/voice/left_pressure tension=.55 opening=.4 noise=.02 impedance=@/voice/source_load source_port name=right_labium path=trachea kind=labial position=0 pressure=.65 tension=.5 opening=.45 noise=.03 balance=.8 position_index=.12 position_width=.05 position_index_scale=1 branch name=throat from_path=trachea from_position=.9 to_path=oral kind=bronchial opening=@/voice/throat_opening coupling=.7 radiation_port name=mouth path=oral kind=lip position=1 opening=@/voice/mouth_opening reflection=-.82 @@ -189,6 +192,7 @@ public sealed class PatchScriptTests Assert.Equal(AcousticRadiationKind.Lip, Assert.Single(patch.AcousticRadiationPorts).Kind); Assert.Equal(WaveClockDelayStrategy.FractionalThiran, Assert.Single(patch.WaveClocks).Strategy); Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/pressure"); + Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/impedance"); Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/branches/0/opening"); Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/radiation/0/opening"); @@ -204,12 +208,57 @@ public sealed class PatchScriptTests Assert.Contains("graph_terminal_area_throat_from", export.Source); Assert.Contains("graph_node_area_left_labium_right_labium", export.Source); Assert.Contains("graph_source_right_labium", export.Source); + Assert.Contains("_load_pressure", export.Source); Assert.Contains("de.fdelay1a", export.Source); Assert.Contains("patch_param_0", export.Source); Assert.Contains("patch_param_1", export.Source); Assert.Contains("patch_param_2", export.Source); } + [Fact] + public void SyrinxVoiceUsesPairedLoadedLabialSourcesThroughOneGraph() + { + var patch = PatchScript.Parse(""" + param path=/bird/left/pressure default=.78 min=0 max=1 step=.001 + param path=/bird/right/pressure default=.66 min=0 max=1 step=.001 + param path=/bird/left/opening default=.34 min=0 max=1 step=.001 + param path=/bird/right/opening default=.29 min=0 max=1 step=.001 + param path=/bird/load default=.85 min=0 max=2 step=.001 + param path=/bird/beak/opening default=.9 min=0 max=1.5 step=.001 + path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42 + path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40 + path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46 + source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=@/bird/left/pressure tension=.42 opening=@/bird/left/opening noise=.025 impedance=@/bird/load + source_port name=right_labium path=right_bronchus kind=syrinx position=0 pressure=@/bird/right/pressure tension=.49 opening=@/bird/right/opening noise=.02 balance=.96 impedance=@/bird/load + terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1 + terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1 + terminal name=trachea_base path=trachea position=0 kind=junction area_scale=1 + connect name=syrinx_merge terminals=left_merge,right_merge,trachea_base law=area_scatter coupling=1 + radiation_port name=beak path=trachea kind=beak position=1 opening=@/bird/beak/opening reflection=-.72 + wave_clock name=bird_clock strategy=linear max_delay=1024 smoothing_ms=2 + acoustic_network name=bird_syrinx path=trachea wave_clock=bird_clock sources=left_labium,right_labium radiation=beak terminals=left_merge,right_merge,trachea_base connections=syrinx_merge + acoustic network=bird_syrinx freq=820 gain=.22 sustain=.42 decay=.08 + """); + + Assert.Equal(3, patch.AcousticPaths.Count); + Assert.Equal(2, patch.AcousticSourcePorts.Count); + Assert.All(patch.AcousticSourcePorts, port => Assert.Equal(AcousticSourceKind.Labial, port.Kind)); + Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/0/impedance"); + Assert.Contains(patch.ParameterBindings, binding => binding.FieldPath == "/acoustic/sources/1/impedance"); + Assert.Equal(AcousticRadiationKind.Beak, Assert.Single(patch.AcousticRadiationPorts).Kind); + Assert.Equal(AcousticConnectionLaw.AreaScattering, Assert.Single(patch.AcousticConnections).Law); + + var export = FaustEmitter.Emit(patch, new FaustExportOptions("bird_syrinx")); + Assert.Contains("acoustic_graph_radiated", export.Source); + Assert.Contains("graph_connection_reflection_syrinx_merge", export.Source); + Assert.Contains("graph_source_left_labium", export.Source); + Assert.Contains("graph_source_right_labium", export.Source); + Assert.Contains("_load_pressure", export.Source); + Assert.Contains("graph_radiation_flow_beak", export.Source); + Assert.Contains("patch_param_0", export.Source); + Assert.DoesNotContain("Tract", export.Source); + } + [Fact] public void ParserSupportsTypedAcousticPathGraph() { diff --git a/tools/TractGraphRenderer/Program.cs b/tools/TractGraphRenderer/Program.cs index 1aedf13..c872da3 100644 --- a/tools/TractGraphRenderer/Program.cs +++ b/tools/TractGraphRenderer/Program.cs @@ -48,6 +48,12 @@ Console.WriteLine(JsonSerializer.Serialize(new return 0; static string Script(RenderRequest controls) => + controls.Mode.Equals("syrinx", StringComparison.OrdinalIgnoreCase) || + controls.Mode.Equals("bird", StringComparison.OrdinalIgnoreCase) + ? SyrinxScript(controls) + : TractScript(controls); + +static string TractScript(RenderRequest controls) => $$""" patch gain=0.48 soft_clip=true @@ -63,6 +69,35 @@ static string Script(RenderRequest controls) => tract shape=human glottis=modal injection=inj nasal_branch=nose motion=motion propagation=graph loss=.999 freq={{F(controls.Frequency)}} gain={{F(controls.Gain)}} intensity={{F(controls.Intensity)}} tenseness={{F(controls.Tenseness)}} sustain={{F(Math.Max(0.05f, controls.DurationSeconds - 0.12f))}} decay=.12 tongue_index={{F(controls.TongueIndex)}} tongue_diameter={{F(controls.TongueDiameter)}} constriction_index={{F(controls.ConstrictionIndex)}} constriction_diameter={{F(controls.ConstrictionDiameter)}} turbulence={{F(controls.Turbulence)}} velum={{F(controls.Velum)}} lip={{F(controls.LipOpening)}} burst={{F(controls.Burst)}} glottal_reflection={{F(controls.GlottalReflection)}} lip_reflection={{F(controls.LipReflection)}} """; +static string SyrinxScript(RenderRequest controls) => + $$""" + patch gain=0.42 soft_clip=true + + param path=/bird/left/pressure default={{F(Math.Clamp(controls.Intensity, 0, 1))}} min=0 max=1 step=.001 + param path=/bird/right/pressure default={{F(Math.Clamp(controls.Intensity * 0.86f, 0, 1))}} min=0 max=1 step=.001 + param path=/bird/left/opening default={{F(Math.Clamp(controls.LipOpening * 0.24f, 0.05f, 1.0f))}} min=0 max=1 step=.001 + param path=/bird/right/opening default={{F(Math.Clamp(controls.LipOpening * 0.20f, 0.05f, 1.0f))}} min=0 max=1 step=.001 + param path=/bird/load default=.85 min=0 max=2 step=.001 + param path=/bird/beak/opening default=.95 min=0 max=1.5 step=.001 + + path name=left_bronchus length_cm=3.8 diameters=.22,.30,.36,.42 + path name=right_bronchus length_cm=3.6 diameters=.20,.28,.34,.40 + path name=trachea length_cm=8.4 diameters=.38,.48,.56,.46 + + source_port name=left_labium path=left_bronchus kind=syrinx position=0 pressure=@/bird/left/pressure tension={{F(controls.Tenseness)}} opening=@/bird/left/opening noise=.025 impedance=@/bird/load + source_port name=right_labium path=right_bronchus kind=syrinx position=0 pressure=@/bird/right/pressure tension={{F(Math.Clamp(controls.Tenseness + 0.07f, 0, 1))}} opening=@/bird/right/opening noise=.02 balance=.96 impedance=@/bird/load + + terminal name=left_merge path=left_bronchus position=1 kind=junction area_scale=1 + terminal name=right_merge path=right_bronchus position=1 kind=junction area_scale=1 + terminal name=trachea_base path=trachea position=0 kind=junction area_scale=1 + connect name=syrinx_merge terminals=left_merge,right_merge,trachea_base law=area_scatter coupling=1 + + radiation_port name=beak path=trachea kind=beak position=1 opening=@/bird/beak/opening reflection=-.72 + wave_clock name=bird_clock strategy=linear max_delay=1024 smoothing_ms=2 + acoustic_network name=bird_syrinx path=trachea wave_clock=bird_clock sources=left_labium,right_labium radiation=beak terminals=left_merge,right_merge,trachea_base connections=syrinx_merge + acoustic network=bird_syrinx freq={{F(controls.Frequency)}} gain={{F(controls.Gain)}} sustain={{F(Math.Max(0.05f, controls.DurationSeconds - 0.12f))}} decay=.08 + """; + static string F(float value) => value.ToString("0.######", CultureInfo.InvariantCulture); @@ -93,6 +128,7 @@ static void WriteWav(string path, IReadOnlyList samples, int sampleRate, } public sealed record RenderRequest( + string Mode = "tract", string OutputPath = "", int SampleRate = 44100, float DurationSeconds = 0.57f,