Add graph source load and syrinx patch
This commit is contained in:
parent
2c38704626
commit
2ee882c612
13 changed files with 246 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
27
patches/advanced/bird-syrinx.aqua
Normal file
27
patches/advanced/bird-syrinx.aqua
Normal file
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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."),
|
||||
|
|
|
|||
|
|
@ -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<AcousticGraphPort> 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<AcousticGraphPort> 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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<float> samples, int sampleRate,
|
|||
}
|
||||
|
||||
public sealed record RenderRequest(
|
||||
string Mode = "tract",
|
||||
string OutputPath = "",
|
||||
int SampleRate = 44100,
|
||||
float DurationSeconds = 0.57f,
|
||||
|
|
|
|||
Loading…
Reference in a new issue