diff --git a/src/AquaSynth.Core/FaustExport.cs b/src/AquaSynth.Core/FaustExport.cs index 92a329f..f744bb3 100644 --- a/src/AquaSynth.Core/FaustExport.cs +++ b/src/AquaSynth.Core/FaustExport.cs @@ -1515,10 +1515,11 @@ public static class FaustEmitter var leftOutgoing = GraphOutgoing(voiceName, left); var rightOutgoing = GraphOutgoing(voiceName, right); var reflection = $"{voiceName}_graph_area_reflection_{node.Name}"; + var contactRelease = EmitGraphContactClosureRelease(source, voiceName, node, ports, options); source.AppendLine($" {reflection} = (({leftArea}) - ({rightArea})) / max(0.000001, ({leftArea}) + ({rightArea}));"); - source.AppendLine($" {rightOutgoing} = {leftIncoming} - {reflection} * ({leftIncoming} + {rightIncoming}) + ({sourceInjection}) / 2;"); - source.AppendLine($" {leftOutgoing} = {rightIncoming} + {reflection} * ({leftIncoming} + {rightIncoming}) + ({sourceInjection}) / 2;"); + source.AppendLine($" {rightOutgoing} = {leftIncoming} - {reflection} * ({leftIncoming} + {rightIncoming}) + ({sourceInjection}) / 2 + ({contactRelease}) * 0.80;"); + source.AppendLine($" {leftOutgoing} = {rightIncoming} + {reflection} * ({leftIncoming} + {rightIncoming}) + ({sourceInjection}) / 2 + ({contactRelease}) * 0.20;"); var energyIn = $"({leftArea}) * pow({leftIncoming}, 2.0) + ({rightArea}) * pow({rightIncoming}, 2.0)"; var energyOut = $"({leftArea}) * pow({leftOutgoing}, 2.0) + ({rightArea}) * pow({rightOutgoing}, 2.0)"; source.AppendLine($" {voiceName}_graph_area_energy_in_{node.Name} = {ProbeSignal(options, $"/debug/{voiceName}/area/{node.Name}/energy_in", energyIn, 0, 4)};"); @@ -1564,10 +1565,6 @@ public static class FaustEmitter ParameterMap parameters, FaustExportOptions options) { - var contactTerms = node.Terminals - .Where(terminal => terminal.Kind == AcousticTerminalKind.Contact) - .Select(terminal => EmitGraphContactSourceExpression(source, voiceName, node, nodePorts, terminal, options)) - .ToList(); var sourceTerms = node.Terminals .Where(terminal => terminal.Kind == AcousticTerminalKind.Source && sources.ContainsKey(terminal.Port.Length == 0 ? terminal.Name : terminal.Port)) .Select(terminal => @@ -1579,7 +1576,6 @@ public static class FaustEmitter : EmitGraphLoadedSourceExpression(source, patch, voiceName, node, sourceName, port, frequency, parameters, options); }) .ToList(); - sourceTerms.AddRange(contactTerms); return sourceTerms.Count == 0 ? "0.0" : string.Join(" + ", sourceTerms); } @@ -1711,26 +1707,39 @@ public static class FaustEmitter source.AppendLine($" {prefix}_out = {ProbeSignal(options, $"/debug/{voiceName}/source/{sourceName}/{node.Name}/out", $"({prefix}_flow * {prefix}_flow_scale * (0.55 + 2.6 * {prefix}_pressure_drive) + {prefix}_noise) * ({balance})", -2, 2)};"); } - private static string EmitGraphContactSourceExpression( + private static string EmitGraphContactClosureRelease( StringBuilder source, string voiceName, AcousticGraphNode node, IReadOnlyList nodePorts, - AcousticTerminal terminal, FaustExportOptions options) { - var safeTerminal = SafeIdentifier(terminal.Name); - var prefix = $"{voiceName}_graph_contact_{safeTerminal}_{node.Name}"; - var terminalArea = $"{voiceName}_graph_terminal_area_{safeTerminal}"; + var contacts = node.Terminals + .Where(terminal => terminal.Kind == AcousticTerminalKind.Contact) + .ToArray(); + if (contacts.Length == 0) + { + return "0.0"; + } + 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 = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/release", $"(max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.992))", 0, 1)};"); - source.AppendLine($" {prefix}_reservoir_drive = {drive};"); - source.AppendLine($" {prefix}_reservoir = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/reservoir", $"{prefix}_reservoir_drive * {prefix}_closure : + ~ *(0.994)", 0, 2)};"); - source.AppendLine($" {prefix}_release_pressure = 0.12 + 3.20 * clip01({prefix}_reservoir);"); - source.AppendLine($" {prefix}_out = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/out", $"{prefix}_release * {prefix}_release_pressure * (0.35 + 0.65 * clip01(abs({voiceName}_graph_terminal_reflection_{safeTerminal})))", -2, 2)};"); - return $"{prefix}_out"; + var releases = new List(); + foreach (var terminal in contacts) + { + var safeTerminal = SafeIdentifier(terminal.Name); + var prefix = $"{voiceName}_graph_contact_{safeTerminal}_{node.Name}"; + var terminalArea = $"{voiceName}_graph_terminal_area_{safeTerminal}"; + var drive = ContactReservoirDriveExpression(voiceName, node, nodePorts, localPressure); + source.AppendLine($" {prefix}_closure = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/closure", $"clip01((0.0144 - ({terminalArea})) / 0.0144)", 0, 1)};"); + source.AppendLine($" {prefix}_release = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/release", $"(max(0.0, ({prefix}_closure : mem) - {prefix}_closure) : + ~ *(0.994))", 0, 1)};"); + source.AppendLine($" {prefix}_reservoir_drive = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/reservoir_drive", drive, 0, 2)};"); + source.AppendLine($" {prefix}_reservoir = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/reservoir", $"({prefix}_reservoir_drive * {prefix}_closure) : + ~ *(0.998)", 0, 2)};"); + source.AppendLine($" {prefix}_release_pressure = 0.16 + 5.80 * clip01({prefix}_reservoir);"); + source.AppendLine($" {prefix}_out = {ProbeSignal(options, $"/debug/{voiceName}/contact/{terminal.Name}/{node.Name}/out", $"{prefix}_release * {prefix}_release_pressure", -2, 2)};"); + releases.Add($"{prefix}_out"); + } + + return releases.Count == 1 ? releases[0] : $"({string.Join(" + ", releases)})"; } private static string EmitGraphTurbulenceSourceExpression( diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 024ff5a..aa4b209 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -217,3 +217,4 @@ {"evidence":"Demoted graph tissue-valve `drive` from split-brained vocal authority into a parser compatibility alias for `flow_scale`, added explicit `flow_scale`, `tissue_loss`, `aperture_shape`, and `flow_loss` source-port fields, and changed Faust graph lowering so reservoir pressure is pressure-owned while oscillation threshold/Q, velocity decay, aperture law, and pressure-flow resistance come from physical morphology/load controls. Canonical human, bird, and alien graph patches now use the new fields. Focused parser/export/render tests passed, class-sliced non-PT suites passed, and fresh Warbling White-eye audition artifacts were written under `artifacts/listening/warbling-white-eye-20260529T002500-drive-demotion`. The PT accepted-utterance diagnostic renderer is unstable in this run: the mama artifact writes, then the testhost exits with code -1 before reporting a managed assertion.","claim":"Graph vocal source energy, threshold, and harmonic behavior now have separate inspectable owners. Future bird/human tuning should move through pressure, stiffness/tension, damping/tissue loss, aperture/flow resistance, collision, radiation, and graph load instead of reviving a generic vocal drive knob. The next cleanup should inspect the remaining low-frequency flow floor/radiation normalization and the PT diagnostic testhost crash, not use the crash as permission to restore a generic drive knob.","timestamp":"2026-05-29T00:20:00+01:00","topic":"graph-tissue-valve-drive-demotion"} {"evidence":"Rechecked the PT accepted-utterance diagnostic after clearing stale child `dotnet` processes. The focused test passes with enough wall-clock budget; fresh artifact `artifacts/parity/pink-trombone-utterance-logmel/20260529T005420700` records render timings of about 31s for mama, 45s for papa, and 59s for thrombosis. `PinkTromboneAcceptedUtterancesReportGraphLogMelParityWhenFaustIsInstalled` now writes generated DSP/control artifacts before rendering, applies a three-minute per-utterance cancellation budget, and writes render stdout/stderr/timing. `FaustCompiler.RunAsync` now kills the entire child process tree when cancellation fires. Focused PT utterance plus graph Faust validation/render tests passed: 3 tests in 2m17s.","claim":"The previous PT testhost failure is not currently a reproducible managed graph-source crash; the real process smell was unbounded external Faust/dotnet render work with poor pre-crash artifacts. Current acoustic evidence points to underpowered/weakly articulated radiated flow, not a loud low-frequency floor, so next tuning should use graph probes rather than restore the generic drive authority.","timestamp":"2026-05-29T01:00:00+01:00","topic":"pt-utterance-process-observability"} {"evidence":"Added DebugProbeUi source/contact internals and an opt-in thrombosis graph timeline probe gated by `AQUASYNTH_RUN_GRAPH_PROBES=1`. Latest artifact `artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T011547091` shows modal source out 1.335230, modal flow 0.791726, modal pressure drive 0.741528, lip radiation flow 0.337191, lip radiation admittance 0.710362, sibilant injection source area-34 out 0.099288, and lip contact release out 0.032413. The opt-in probe plus graph Faust validation passed in 2m01s; the default focused PT diagnostic still passes with the probe test returning when the env var is absent.","claim":"The graph voice is alive upstream of the final artifact: source and radiation have real signal, while contact release is small/localized and output balance remains weak. Do not restore generic drive or add a blind gain compensator. The next coherent acoustic cut should be either contact/closure participation in scattering or an explicitly owned utterance/radiation output balance, decided from the timeline rather than final log-mel score alone.","timestamp":"2026-05-29T01:20:00+01:00","topic":"thrombosis-graph-timeline-probe"} +{"evidence":"Moved contact release out of the generic node source sum and into ordinary two-port path scattering. The new graph-owned closure primitive emits closure/reservoir/release/out probes and injects release directionally into downstream/upstream traveling waves. Latest opt-in thrombosis probe `artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T021934812`: lip contact out rose from prior 0.032413 to 0.548231, reservoir drive 0.519667, reservoir 6.024680, release 0.485894, lip radiation flow 0.581880, modal source out 1.233802, sibilant area-34 source out 0.099287. Latest accepted-utterance diagnostic `artifacts/parity/pink-trombone-utterance-logmel/20260529T022214016` still fails articulation: thrombosis RMS improves to 0.0761 but cosine stays about 0.331 and silence mismatch worsens to 0.7306. Focused structural graph tests, opt-in thrombosis probe, and accepted-utterance diagnostic passed under normal verbosity.","claim":"Closure pressure now participates in the graph path instead of acting as a decorative source pulse. This is the research-backed ownership cut, but it is not speech parity: contact release reaches radiation, while active-frame duration, output balance, and articulation shape remain weak. Next work should inspect contact filtering/duration or explicit utterance/radiation balance, not restore generic drive or global gain.","timestamp":"2026-05-29T02:30:00+01:00","topic":"graph-owned-closure-reservoir"} diff --git a/state/scratch.md b/state/scratch.md index 82fb620..f2bc9a9 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -2,6 +2,38 @@ Current slice: +- Implemented the first graph-owned closure reservoir cut. Contact terminals no + longer contribute through the generic node source sum. Ordinary two-port path + scatter now emits per-contact closure, reservoir drive, reservoir, release, + and out probes, and injects the release directionally into the same path + scattering law (`0.80` downstream, `0.20` upstream). This is the research + supported move: closure pressure enters the traveling-wave path instead of + acting as an output click or generic source gain. +- Latest opt-in thrombosis probe after the cut: + `artifacts/parity/pink-trombone-graph-thrombosis-probes/20260529T021934812`. + Key peaks: lip contact closure `0.936876`, contact reservoir drive + `0.519667`, contact reservoir `6.024680`, contact release `0.485894`, + contact out `0.548231`, lip radiation flow `0.581880`, lip radiation + admittance `0.710362`, modal source out `1.233802`, modal flow `0.771656`, + sibilant/injection area 34 out `0.099287`. Previous contact out was about + `0.032413`; the closure event now reaches the graph/radiation path. +- Latest utterance diagnostic: + `artifacts/parity/pink-trombone-utterance-logmel/20260529T022214016`. + The speech verdict is still not accepted. `thrombosis` RMS improved from + roughly `0.0673` to `0.0761`, but cosine stayed about `0.331` and silence + mismatch worsened to `0.7306`. This means the closure cut is structurally + correct but not sufficient. Do not claim speech parity. Next likely owner is + either utterance/radiation output balance or more realistic contact + filtering/active-frame duration; do not hide that under a global gain knob. +- Verification: + `dotnet test tests\AquaSynth.Dsl.Tests\AquaSynth.Dsl.Tests.csproj --no-build --disable-build-servers --filter "ParserSupportsPinkTromboneStyleTractVoice|TractVoiceCanLowerThroughAcousticGraph|FaustCompilerValidatesAcousticPathGraphWhenInstalled" -v normal` + passed: 3 tests. `AQUASYNTH_RUN_GRAPH_PROBES=1 dotnet test ... --filter + "PinkTromboneThrombosisGraphDebugProbesWriteTimelineWhenNativeFaustIsInstalled"` + passed: 1 test in 2m09s. `dotnet test ... --filter + "PinkTromboneAcceptedUtterancesReportGraphLogMelParityWhenFaustIsInstalled"` + passed: 1 test in 2m25s. Minimal verbosity sometimes exits silently on the + long native render path; normal verbosity produced passing runs. + - Added graph-native debug probes for vocal source internals when `DebugProbeUi` is enabled: tissue-valve pressure drive, modal tissue, aperture, flow, and output; turbulence-source reservoir/release/output; and diff --git a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs index 53592e8..07e07a5 100644 --- a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs @@ -145,6 +145,8 @@ public sealed class PatchScriptTests 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("* 0.80", faust); + Assert.Contains("* 0.20", faust); Assert.Contains("de.fdelay", faust); Assert.Contains("_reservoir_drive", faust); Assert.Contains("_release_pulse", faust);