From 073c4873e0a892d88c5e1df3c7e4ed404c3dbe5e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 29 May 2026 16:58:13 +0100 Subject: [PATCH] Correct primitive source and radiation timeline laws --- docs/acoustic-graph-proprioception.md | 8 ++++++-- src/AquaSynth.Core/FaustExport.cs | 5 +++-- src/AquaSynth.Core/Model.cs | 3 ++- src/AquaSynth.Core/PatchScript.cs | 6 ++++-- src/AquaSynth.Core/ProbeTimelineReport.cs | 15 +++++++++++---- state/evidence.jsonl | 1 + state/memory.json | 2 +- state/scratch.md | 9 +++++++++ state/spine.yaml | 1 + tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs | 4 +++- 10 files changed, 41 insertions(+), 13 deletions(-) diff --git a/docs/acoustic-graph-proprioception.md b/docs/acoustic-graph-proprioception.md index d907a51..7abda6c 100644 --- a/docs/acoustic-graph-proprioception.md +++ b/docs/acoustic-graph-proprioception.md @@ -243,11 +243,15 @@ PT/SndKit comparison report: - `PinkTrombonePrimitiveTimelineComparisonWritesArtifacts` writes per-fixture Aqua timelines, PT internal timelines, comparison CSVs, and a summary under `artifacts/parity/primitive-vocal-timeline`. +- The first comparison-driven correction demoted hidden source unit gain into + explicit `SourcePort.FlowScale`, stopped aperture from rewriting + `RadiationLoad.Reflection`, and fixed `ProbeTimelineReport` radiation flow to + observe path wave flow rather than `aperture / impedance` alone. The next implementation pass should harden the primitives further: -- use the artifact reports to choose the first physical-law correction in the - primitive machine; +- decide where gesture slew lives: inside primitive ports or in a separate + gesture/timeline owner; - add optional VTL/ArtiSynth adapters only as artifact comparators, not default CI dependencies; - keep `mama`, `papa`, and `thrombosis` as downstream witnesses until primitive diff --git a/src/AquaSynth.Core/FaustExport.cs b/src/AquaSynth.Core/FaustExport.cs index 0db5bcf..6bbb87c 100644 --- a/src/AquaSynth.Core/FaustExport.cs +++ b/src/AquaSynth.Core/FaustExport.cs @@ -598,10 +598,11 @@ public static class FaustEmitter var tension = parameters.Expression(OwnerField(owner, "tension"), port.Tension); var opening = parameters.Expression(OwnerField(owner, "opening"), port.Opening); var impedance = parameters.Expression(OwnerField(owner, "impedance"), port.Impedance); + var flowScale = parameters.Expression(OwnerField(owner, "flow_scale"), port.FlowScale); var load = $"{voiceName}_primitive_source_{safe}_load_pressure"; var flow = $"{voiceName}_primitive_source_{safe}_flow"; source.AppendLine($" {load} = {ProbeSignal(options, $"/debug/{voiceName}/source/{port.Name}/load_pressure", $"({impedance}) * ({pressure})", 0, 2)};"); - source.AppendLine($" {flow} = {ProbeSignal(options, $"/debug/{voiceName}/source/{port.Name}/flow", $"ma.tanh((({pressure}) * max(0.0, {opening}) * (0.5 + {tension}) + 0.08 * no.noise * {parameters.Expression(OwnerField(owner, "noise"), port.Noise)}) / max(0.05, {impedance}))", -2, 2)};"); + source.AppendLine($" {flow} = {ProbeSignal(options, $"/debug/{voiceName}/source/{port.Name}/flow", $"({flowScale}) * ma.tanh((({pressure}) * max(0.0, {opening}) * (0.5 + {tension}) + 0.08 * no.noise * {parameters.Expression(OwnerField(owner, "noise"), port.Noise)}) / max(0.05, {impedance}))", -2, 2)};"); keepAlive.Add(load); keepAlive.Add(flow); return flow; @@ -671,7 +672,7 @@ public static class FaustEmitter var boundary = $"{voiceName}_primitive_radiation_{safe}_boundary_flow"; var flow = $"{voiceName}_primitive_radiation_{safe}_flow"; var output = $"{voiceName}_primitive_radiation_{safe}_output"; - source.AppendLine($" {voiceName}_primitive_radiation_{safe}_reflection = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/reflection", $"({reflection}) * (1.0 - 0.65 * {aperture})", -1, 1)};"); + source.AppendLine($" {voiceName}_primitive_radiation_{safe}_reflection = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/reflection", reflection, -1, 1)};"); source.AppendLine($" {boundary} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/boundary_flow", $"({wave}) * {aperture} / max(0.05, {impedance})", -4, 4)};"); source.AppendLine($" {flow} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/flow", boundary, -4, 4)};"); source.AppendLine($" {output} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/output", $"{flow} : fi.highpass(1, 40.0 + 500.0 * {aperture})", -4, 4)};"); diff --git a/src/AquaSynth.Core/Model.cs b/src/AquaSynth.Core/Model.cs index 2262282..2a376b1 100644 --- a/src/AquaSynth.Core/Model.cs +++ b/src/AquaSynth.Core/Model.cs @@ -466,7 +466,8 @@ public sealed record SourcePort( float Tension = 0.6f, float Opening = 0.5f, float Noise = 0.08f, - float Impedance = 0.35f); + float Impedance = 0.35f, + float FlowScale = 0.02f); public sealed record ConstrictionContact( string Name, diff --git a/src/AquaSynth.Core/PatchScript.cs b/src/AquaSynth.Core/PatchScript.cs index b3d4df7..b47baa9 100644 --- a/src/AquaSynth.Core/PatchScript.cs +++ b/src/AquaSynth.Core/PatchScript.cs @@ -781,7 +781,8 @@ public static class PatchScript GetBoundFloat(fields, line, 0.6f, $"/vocal/sources/{_sourcePorts.Count}/tension", "tension", "tenseness", "tense"), GetBoundFloat(fields, line, 0.5f, $"/vocal/sources/{_sourcePorts.Count}/opening", "opening", "open"), GetBoundFloat(fields, line, 0.08f, $"/vocal/sources/{_sourcePorts.Count}/noise", "noise", "aspiration"), - GetBoundFloat(fields, line, 0.35f, $"/vocal/sources/{_sourcePorts.Count}/impedance", "impedance", "load", "source_impedance")); + GetBoundFloat(fields, line, 0.35f, $"/vocal/sources/{_sourcePorts.Count}/impedance", "impedance", "load", "source_impedance"), + GetBoundFloat(fields, line, 0.02f, $"/vocal/sources/{_sourcePorts.Count}/flow_scale", "flow_scale", "scale")); _sourcePorts.Add(port); _sourcePortsByName[name] = port; } @@ -1692,7 +1693,8 @@ public static class PatchScript tract.Tenseness, tract.Glottis?.Skew ?? 0.42f, tract.Glottis?.Aspiration ?? 0.08f, - 0.10f); + 0.10f, + 0.02f); if (!_sourcePortsByName.ContainsKey(source.Name)) { _sourcePorts.Add(source); diff --git a/src/AquaSynth.Core/ProbeTimelineReport.cs b/src/AquaSynth.Core/ProbeTimelineReport.cs index 06daf23..7563de2 100644 --- a/src/AquaSynth.Core/ProbeTimelineReport.cs +++ b/src/AquaSynth.Core/ProbeTimelineReport.cs @@ -20,6 +20,7 @@ public static class ProbeTimelineReport for (var block = 0; block < Math.Max(1, blocks); block++) { + var outgoingByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var pathName in network.Paths) { var path = paths[pathName]; @@ -28,6 +29,7 @@ public static class ProbeTimelineReport var delay = area.Shape.LengthMeters / Math.Max(1, path.PropagationSpeedMetersPerSecond) * 48000; var incoming = PathIncomingFlow(network, sources, contacts, branches, path.Name); var outgoing = incoming * Math.Clamp(path.Loss, 0, 1); + outgoingByPath[path.Name] = outgoing; var energyIn = pathArea * incoming * incoming; var energyOut = pathArea * outgoing * outgoing; Add(samples, block, $"path:{path.Name}", "area", pathArea); @@ -43,8 +45,9 @@ public static class ProbeTimelineReport foreach (var sourceName in network.Sources) { var source = sources[sourceName]; - var flow = MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance)); + var flow = SourceFlow(source); Add(samples, block, $"source:{source.Name}", "load_pressure", source.Pressure * source.Impedance); + Add(samples, block, $"source:{source.Name}", "flow_scale", source.FlowScale); Add(samples, block, $"source:{source.Name}", "flow", flow); } @@ -71,8 +74,9 @@ public static class ProbeTimelineReport { var load = radiation[loadName]; var aperture = Math.Clamp(load.Aperture, 0, 1); - var flow = aperture / Math.Max(0.05f, load.Impedance); - Add(samples, block, $"radiation:{load.Name}", "reflection", load.Reflection * (1 - 0.65f * aperture)); + var pathWave = outgoingByPath.GetValueOrDefault(load.Path, 0); + var flow = pathWave * aperture / Math.Max(0.05f, load.Impedance); + Add(samples, block, $"radiation:{load.Name}", "reflection", load.Reflection); Add(samples, block, $"radiation:{load.Name}", "boundary_flow", flow); Add(samples, block, $"radiation:{load.Name}", "flow", flow); Add(samples, block, $"radiation:{load.Name}", "output", flow); @@ -113,7 +117,7 @@ public static class ProbeTimelineReport var sourceFlow = network.Sources .Select(name => sources[name]) .Where(source => source.Path.Equals(pathName, StringComparison.OrdinalIgnoreCase)) - .Sum(source => MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance))); + .Sum(SourceFlow); var contactFlow = network.Contacts .Select(name => contacts[name]) .Where(contact => contact.Path.Equals(pathName, StringComparison.OrdinalIgnoreCase)) @@ -130,6 +134,9 @@ public static class ProbeTimelineReport return sourceFlow + contactFlow + branchFlow; } + private static float SourceFlow(SourcePort source) => + source.FlowScale * MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance)); + private static string Escape(string value) => value.Contains(',') || value.Contains('"') ? $"\"{value.Replace("\"", "\"\"", StringComparison.Ordinal)}\"" : value; } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 88c5aac..603f06f 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -230,3 +230,4 @@ {"date":"2026-05-29","area":"primitive-vocal-telemetry","summary":"Primitive timelines now expose path incoming/outgoing wave variables, area-weighted energy in/out, passivity ratio, and contact resistance. Faust debug probes expose matching path wave/passivity fields. `PrimitiveReferenceReport.ComparePinkTrombone` adds a PT/SndKit-facing static comparison report for area sections/length/reflection count, delay estimate, branch admittance, contact state, and radiation fields. PatchScriptTests pass 86/86.","evidence":"src/AquaSynth.Core/FaustExport.cs; src/AquaSynth.Core/ProbeTimelineReport.cs; src/AquaSynth.Core/PrimitiveReferenceReport.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/acoustic-graph-proprioception.md"} {"date":"2026-05-29","area":"primitive-vocal-reference-timeline","summary":"PT/SndKit local reference internals are now observable through `PinkTromboneReferenceRenderer.RenderTimeline`, exposing oral/nasal path waves and energy, velopharynx branch reflections/admittance, obstruction contact state, and lip radiation flow/output from the renderer's own arrays. `PrimitiveReferenceReport.ComparePinkTromboneTimeline` role-normalizes Aqua primitive timeline samples against PT timeline samples. PatchScriptTests pass 88/88.","evidence":"src/AquaSynth.Core/PinkTromboneReferenceRenderer.cs; src/AquaSynth.Core/PrimitiveReferenceReport.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/acoustic-graph-proprioception.md"} {"date":"2026-05-29","area":"primitive-vocal-artifacts","summary":"Added a PT fixture-suite primitive timeline artifact test. Each fixture writes Aqua primitive timeline CSV, PT internal timeline CSV, role-normalized comparison CSV, and a summary under `artifacts/parity/primitive-vocal-timeline/`. The focused artifact test passes without native Faust or external reference binaries.","evidence":"tests/AquaSynth.Dsl.Tests/PinkTromboneLogMelParityTests.cs; src/AquaSynth.Core/PinkTromboneReferenceRenderer.cs; src/AquaSynth.Core/PrimitiveReferenceReport.cs; docs/acoustic-graph-proprioception.md"} +{"date":"2026-05-29","area":"primitive-vocal-law-correction","summary":"Used primitive timeline artifacts to make the first non-fork physical-law correction. `SourcePort` now owns `FlowScale` defaulting to `.02`; primitive radiation reflection now uses declared `RadiationLoad.Reflection` directly; `ProbeTimelineReport` radiation flow now depends on outgoing path wave like Faust. The focused primitive/artifact tests and PatchScriptTests pass. New artifact `artifacts/parity/primitive-vocal-timeline/20260529T155613895` reduces fixture-suite mean absolute timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining drift points to branch/contact motion ownership.","evidence":"src/AquaSynth.Core/Model.cs; src/AquaSynth.Core/PatchScript.cs; src/AquaSynth.Core/FaustExport.cs; src/AquaSynth.Core/ProbeTimelineReport.cs; artifacts/parity/primitive-vocal-timeline/20260529T155613895; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; tests/AquaSynth.Dsl.Tests/PinkTromboneLogMelParityTests.cs"} diff --git a/state/memory.json b/state/memory.json index 207bd86..7d9a90e 100644 --- a/state/memory.json +++ b/state/memory.json @@ -121,7 +121,7 @@ }, "last_verified": { "command": "dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false --filter \"FullyQualifiedName~PatchScriptTests\" -v minimal", - "result": "Primitive vocal telemetry/reference cut passes PatchScriptTests 88/88 and the focused PT primitive timeline artifact test. Path timelines now include wave/passivity fields, PT/SndKit static primitive comparison is covered, the local PT renderer exposes internal primitive timeline samples, and per-fixture comparison artifacts are written under artifacts/parity/primitive-vocal-timeline. Earlier full unfiltered run hit the five-minute wall after leaving orphan dotnet workers; focused suites are the current reliable verification path.", + "result": "Primitive vocal telemetry/reference cut passes PatchScriptTests 88/88 and the focused PT primitive timeline artifact test. Path timelines now include wave/passivity fields, PT/SndKit static primitive comparison is covered, the local PT renderer exposes internal primitive timeline samples, and per-fixture comparison artifacts are written under artifacts/parity/primitive-vocal-timeline. First comparison-driven law correction added SourcePort.FlowScale, direct RadiationLoad.Reflection ownership, and wave-dependent timeline radiation flow. Earlier full unfiltered run hit the five-minute wall after leaving orphan dotnet workers; focused suites are the current reliable verification path.", "commit": "pending primitive vocal teardown" } } diff --git a/state/scratch.md b/state/scratch.md index 3e0d55d..43e6762 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -45,6 +45,15 @@ Current slice: `comparison.csv`, and `summary.txt` under `artifacts/parity/primitive-vocal-timeline/`. Focused artifact test passes. +- First comparison-driven law correction: `SourcePort` now owns `FlowScale` + (default `.02`) instead of primitive source flow silently emitting unit-scale + waves; primitive radiation reflection now reports/uses declared + `RadiationLoad.Reflection` instead of aperture-modulating the coefficient; + `ProbeTimelineReport` radiation flow now uses the path outgoing wave, matching + the Faust law. New artifact `20260529T155613895` drops suite mean absolute + timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining obvious drift + is branch/contact dynamics, which raises a real owner question: gesture slew + inside ports or separate gesture/timeline primitive. - Proprioception correction after user rejected baby-word golf as the rebuild owner. Diagnosis is now durable in diff --git a/state/spine.yaml b/state/spine.yaml index 6744993..331664c 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -138,6 +138,7 @@ live_invariants: - Primitive path telemetry now reports `incoming_wave`, `outgoing_wave`, `energy_in`, `energy_out`, and `passivity_ratio` in both Faust debug probes and `ProbeTimelineReport`; contact telemetry includes resistance. `PrimitiveReferenceReport.ComparePinkTrombone` compares static primitive area sections, length, reflection count, delay estimate, branch admittance, contact state, and radiation fields against local PT/SndKit fixture controls without adding a native reference dependency. Next pressure is per-block comparison against PT/SndKit internals, not static control matching alone. - `PinkTromboneReferenceRenderer.RenderTimeline` now exposes PT internal oral/nasal path, branch, contact, and radiation samples from the renderer's own diameter/wave/reflection/transient arrays. `PrimitiveReferenceReport.ComparePinkTromboneTimeline` role-normalizes Aqua primitive samples against PT timeline samples for path/branch/contact/radiation signals. This is the first local reference-internal comparison surface for the primitive rebuild; next pressure is fixture-suite artifact reporting and then tightening laws where the timeline drift points. - `PinkTrombonePrimitiveTimelineComparisonWritesArtifacts` writes per-fixture Aqua primitive timelines, PT internal timelines, comparison CSVs, and a summary under `artifacts/parity/primitive-vocal-timeline/`. This makes primitive drift inspectable across the PT fixture suite without native Faust or external VTL/ArtiSynth dependencies. Use these artifacts to choose the next physical-law correction. + - First comparison-driven primitive law correction: `SourcePort` owns `FlowScale` (default `.02`) so pressure/tension/opening no longer emit hidden unit-scale waves; `RadiationLoad.Reflection` owns reflection directly and aperture no longer rewrites that coefficient; `ProbeTimelineReport` radiation flow now uses path outgoing wave like Faust instead of reporting `aperture / impedance`. Artifact `artifacts/parity/primitive-vocal-timeline/20260529T155613895` drops fixture-suite mean absolute timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining large drift is branch/contact dynamics, which needs an owner decision about gesture slew versus primitive-local smoothing. - 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 4646f1c..97f65d2 100644 --- a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs @@ -254,7 +254,7 @@ public sealed class PatchScriptTests param path=/mouth/open default=.9 min=0 max=1 step=.001 morphology name=human length_cm=17 diameters=.6,.8,1.2,1.6,1.3,.9 emit_sections=6 tongue_diameter=1.4 constriction_diameter=.8 lip_opening=@/mouth/open waveguide_path name=oral morphology=human strategy=thiran order=1 max_delay=4096 loss=.998 - source_port name=modal path=oral kind=glottal position=0 pressure=.72 tension=.58 opening=.45 noise=.04 impedance=.32 + source_port name=modal path=oral kind=glottal position=0 pressure=.72 tension=.58 opening=.45 noise=.04 impedance=.32 flow_scale=.02 constriction_contact name=lip_stop path=oral position=.94 opening=@/mouth/open resistance=.45 stored_pressure=.8 radiation_load name=mouth path=oral kind=lip position=1 aperture=@/mouth/open reflection=-.82 impedance=.28 probe_timeline name=flow network=human blocks=2 block_size=32 @@ -271,6 +271,7 @@ public sealed class PatchScriptTests Assert.Equal("human", path.AreaFunction); Assert.Equal(WaveClockDelayStrategy.FractionalThiran, path.DelayStrategy); Assert.Single(patch.SourcePorts); + Assert.Equal(.02f, Assert.Single(patch.SourcePorts).FlowScale, 5); Assert.Single(patch.ConstrictionContacts); Assert.Single(patch.RadiationLoads); Assert.Single(patch.ProbeTimelines); @@ -327,6 +328,7 @@ public sealed class PatchScriptTests Assert.Contains(samples, sample => sample.Primitive == "path:oral" && sample.Signal == "energy_out"); Assert.Contains(samples, sample => sample.Primitive == "path:oral" && sample.Signal == "passivity_ratio"); Assert.Contains(samples, sample => sample.Primitive == "source:modal" && sample.Signal == "flow"); + Assert.Contains(samples, sample => sample.Primitive == "source:modal" && sample.Signal == "flow_scale"); Assert.Contains(samples, sample => sample.Primitive == "contact:lip_stop" && sample.Signal == "resistance"); Assert.Contains(samples, sample => sample.Primitive == "contact:lip_stop" && sample.Signal == "released_flow"); Assert.Contains(samples, sample => sample.Primitive == "radiation:mouth" && sample.Signal == "output");