Add Pink Trombone parity fixtures

This commit is contained in:
Meta Vens 2026-05-25 21:33:33 +01:00
commit 7a42c70809
8 changed files with 125 additions and 3 deletions

View file

@ -85,6 +85,8 @@ named failure against Pink Trombone, but it does not own the anatomy:
moving tongue/constriction gestures.
- Renders Aqua DSL reconstructions.
- Scores log-mel cosine similarity plus targeted feature probes.
- First fixture catalog exists in `PinkTromboneParityFixtures`; reference
rendering/audio thresholds remain the next pass.
## First Cut
@ -102,7 +104,7 @@ The next cut is timing exactness and proof: the oral/nasal tube now derives movi
per-section diameter/area/reflection fields, distributes frication into section
updates, derives release bursts from obstruction history, and carries substep
clock intent. Exact two recursive tract state updates per output sample and
rendered parity fixtures still need proof.
reference-rendered parity thresholds still need proof.
## Cut Line

View file

@ -107,6 +107,10 @@ Trombone anatomy:
- `substeps` expresses waveguide clock intent; current Faust lowering consumes
it through drive/loss scaling but does not yet prove exact intra-sample state
updates.
- `PinkTromboneParityFixtures` defines fixed Aqua DSL workouts for open,
front, nasal, sibilant, and closure cases using the reusable low-level
primitives. The reference-rendered PT audio still needs to be captured or
generated before thresholds can be honest.
- The proxy script parses and emits Faust with live controls, but its missing
features explicitly name the waveguide authorities AquaSynth does not own.
- `tools/vocal-tract-playground` exposes the same control surface for fast

View file

@ -37,6 +37,7 @@ public sealed record AudioComparison(
float CentroidRatio,
float EnvelopeDistance,
float LogMelDistance,
float LogMelCosineSimilarity,
float Score);
public sealed class AudioAnalyzer
@ -183,9 +184,10 @@ public sealed class AudioAnalyzer
var centroidRatio = SafeRatio(candidate.Features.SpectralCentroidHz, reference.Features.SpectralCentroidHz);
var envelopeDistance = NormalizedDistance(reference.RmsEnvelope, candidate.RmsEnvelope);
var logMelDistance = NormalizedDistance(reference.LogMelSpectrogram.Values, candidate.LogMelSpectrogram.Values);
var logMelCosine = CosineSimilarity(reference.LogMelSpectrogram.Values, candidate.LogMelSpectrogram.Values);
var ratioPenalty = RatioDistance(durationRatio) + RatioDistance(rmsRatio) + RatioDistance(zeroCrossingRatio) * 0.5f + RatioDistance(centroidRatio) * 0.5f;
var score = 1 / (1 + envelopeDistance * 0.7f + logMelDistance + ratioPenalty);
return new AudioComparison(reference, candidate, durationRatio, rmsRatio, zeroCrossingRatio, centroidRatio, envelopeDistance, logMelDistance, score);
return new AudioComparison(reference, candidate, durationRatio, rmsRatio, zeroCrossingRatio, centroidRatio, envelopeDistance, logMelDistance, logMelCosine, score);
}
private static void Fft(Complex[] buffer)
@ -282,6 +284,24 @@ public sealed class AudioAnalyzer
return MathF.Sqrt(error / Math.Max(float.Epsilon, scale));
}
public static float CosineSimilarity(IReadOnlyList<float> reference, IReadOnlyList<float> candidate)
{
var length = Math.Max(1, Math.Max(reference.Count, candidate.Count));
var dot = 0f;
var left = 0f;
var right = 0f;
for (var i = 0; i < length; i++)
{
var a = ResampledAt(reference, i, length);
var b = ResampledAt(candidate, i, length);
dot += a * b;
left += a * a;
right += b * b;
}
return dot / MathF.Max(0.000001f, MathF.Sqrt(left) * MathF.Sqrt(right));
}
private static float ResampledAt(IReadOnlyList<float> values, int index, int targetLength)
{
if (values.Count == 0) return 0;

View file

@ -0,0 +1,65 @@
namespace AquaSynth.Dsl;
public sealed record PinkTromboneParityFixture(
string Id,
string Description,
string AquaScript,
IReadOnlyList<string> ReferenceFeatures);
public static class PinkTromboneParityFixtures
{
public static IReadOnlyList<PinkTromboneParityFixture> All { get; } =
[
Fixture(
"open-vowel",
"Sustained open oral vowel with modal glottis and quiet constriction.",
"tongue_index=13 tongue_diameter=2.7 constriction_index=32 constriction_diameter=1.4 turbulence=0.02 velum=0.01 lip=1.7",
["glottal_source", "main_tract_waveguide_cells", "diameter_to_reflection_coefficients"]),
Fixture(
"front-vowel",
"Front constricted vowel pressure for tongue-body movement.",
"tongue_index=27 tongue_diameter=1.05 constriction_index=34 constriction_diameter=1.2 turbulence=0.03 velum=0.01 lip=1.1",
["tract_shape_motion", "diameter_authority"]),
Fixture(
"nasal-vowel",
"Velum-open nasalized vowel through oral and nasal waveguides.",
"tongue_index=14 tongue_diameter=2.2 constriction_index=18 constriction_diameter=0.8 turbulence=0.08 velum=0.33 lip=1.35",
["nose_waveguide_cells", "nose_junction"]),
Fixture(
"sibilant",
"High-turbulence front constriction with positioned waveguide injection.",
"tongue_index=28 tongue_diameter=0.75 constriction_index=34 constriction_diameter=0.35 turbulence=0.95 velum=0.01 lip=1.2",
["positioned_turbulence_applied_to_waveguide_cells"]),
Fixture(
"closure-release",
"Closed constriction opening pressure for obstruction-history transient tests.",
"tongue_index=18 tongue_diameter=1.2 constriction_index=24 constriction_diameter=0.02 turbulence=0.25 velum=0.01 lip=1.45 burst=0.8",
["closure_transients"])
];
public static PinkTromboneParityFixture ById(string id) =>
All.First(fixture => fixture.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
private static PinkTromboneParityFixture Fixture(
string id,
string description,
string tractFields,
IReadOnlyList<string> referenceFeatures) =>
new(id, description, Script(tractFields), referenceFeatures);
private static string Script(string tractFields) =>
$$"""
patch gain=0.82 soft_clip=true
tract_shape
name=human
diameters=0.6,0.6,0.6,0.6,0.6,0.7,0.8,1.0,1.1,1.1,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.5,1.4,1.3,1.2,1.15,1.5
glottis name=modal intensity=.72 tenseness=.6 aspiration=.12 reflection=.75 skew=.42
tract_injection name=inj position=32 diameter=1 turbulence=.1 burst=.25 width=1
nasal_branch name=nose junction=17 velum=.01 reflection=-.85 loss=.999 diameters=0.01,0.35,0.5,0.65,0.8,0.95,1.1,1.25,1.4,1.55,1.7,1.8,1.9,1.9,1.85,1.75,1.65,1.55,1.45,1.35,1.25,1.15,1.05,0.95,0.85,0.75,0.65,0.55
tract_motion name=motion diameter_slew=18 shape_return=8 constriction_slew=24 velum_slew=16 obstruction_threshold=.05
tract shape=human glottis=modal injection=inj nasal_branch=nose motion=motion propagation=waveguide substeps=2 waveguide_loss=.999 freq=140 gain=.7 sustain=.45 decay=.12 {{tractFields}}
""";
}

View file

@ -137,3 +137,4 @@
{"date":"2026-05-26","topic":"tract-motion-obstruction-history","lesson":"PT-style movement and release bursts are temporal DSP semantics, not preset flavor. AquaSynth now has `tract_motion` to own slew rates and obstruction threshold; waveguide lowering smooths tract controls and derives burst pressure from previous obstruction state opening into current non-obstruction. Remaining PT expression gaps are true two-steps-per-sample execution, dynamic per-section target diameter arrays, and rendered parity fixtures.","evidence":"Added `TractMotion`, parser command `tract_motion`, voice references/inline overrides, Faust `slew(rate,x)`, smoothed tongue/constriction/velum controls, `tract_prev_injection_close`, `tract_obstructed`, and `tract_release_memory` burst generation. Verification: no-hang build/test command with `--disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false`; focused tests passed 3."}
{"date":"2026-05-26","topic":"dynamic-tract-reflection-field","lesson":"A PT-class waveguide cannot keep reflection coefficients as compile-time constants. AquaSynth waveguide lowering now derives live per-section diameter targets from `tract_shape`, tongue, constriction, and lip controls, slews them through `tract_motion`, squares them into section areas, and recomputes reflection coefficients and nasal junction areas as DSP expressions.","evidence":"Replaced static `wg_k_N` constants in `FaustExport.TractWaveguideExpression` with generated `wg_diameter_target_N`, `wg_diameter_N`, `wg_area_N`, and dynamic `wg_k_N` expressions. Focused no-hang build/test command passed 3 tests."}
{"date":"2026-05-26","topic":"waveguide-substep-clock","lesson":"PT's twice-rate tract clock must be represented explicitly. AquaSynth already had `substeps`; lowering now consumes it through `wg_substep_drive` and `wg_substep_loss` so the field is not decorative. Exact intra-sample recursive state updates remain named debt until the Faust lowering or another backend can prove them.","evidence":"Added substep drive/loss scaling in `TractWaveguideExpression`, updated parity docs/reference metadata, and kept `exact_twice_rate_state_update` as missing. Focused no-hang build/test command passed 3 tests."}
{"date":"2026-05-26","topic":"pink-trombone-parity-fixtures","lesson":"Expressive parity needs fixed workouts before audio golf. AquaSynth now has `PinkTromboneParityFixtures` for open vowel, front vowel, nasal vowel, sibilant, and closure-release scripts built from reusable low-level tract primitives, plus log-mel cosine similarity in `AudioComparison`. The remaining proof is to generate/capture PT reference audio and set honest thresholds.","evidence":"Added `PinkTromboneParityFixtures`, `AudioComparison.LogMelCosineSimilarity`, `AudioAnalyzer.CosineSimilarity`, and tests that every PT fixture parses, emits Faust, uses tract_shape/glottis/tract_injection/nasal_branch/tract_motion, and exports waveguide lowering. Focused build/test passed 4 tests."}

View file

@ -46,6 +46,7 @@ modules:
tract_voice_model: src/AquaSynth.Core/Model.cs
tract_shape_model: src/AquaSynth.Core/Model.cs
tract_waveguide_lowering: src/AquaSynth.Core/FaustExport.cs
pink_trombone_parity_fixtures: src/AquaSynth.Core/PinkTromboneParityFixtures.cs
faust_emitter: src/AquaSynth.Core/FaustExport.cs
faust_compiler: src/AquaSynth.Faust/FaustCompiler.cs
native_faust_runtime: src/AquaSynth.Faust/NativeFaustRuntime.cs

View file

@ -112,7 +112,7 @@ live_invariants:
- The speech lane is the active center: IPA parity, utterance embeddings, synth-driver automation, rendered loss, and compiled Faust control sweeps take priority over new DX7/Zyn digestion.
- Every learned speech arrow must stay inspectable: utterance metadata -> embedding, embedding plus phonetic features -> automation, automation -> rendered audio, rendered audio -> loss/gradient.
- Pink Trombone parity is structural waveguide pressure, not permission to add more formant proxy knobs. AquaSynth now has a `tract` DSL command that lowers into `Voice.Tract` with PT-shaped runtime controls and a Faust tract-voice proxy; it is still an ordinary voice treatment, not a separate graph owner and not an exact 44-cell/28-cell bidirectional waveguide.
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns section diameters/areas and derived reflection coefficients; `glottis` owns excitation controls; `tract_injection` owns positioned frication/burst controls and now distributes frication pressure into waveguide section updates; `nasal_branch` owns a second diameter/area tube; `tract_motion` owns control slew and obstruction threshold; `propagation=waveguide` lowers tract voices into generated oral/nasal right-left waveguide equations with live per-section diameter/area/reflection fields, obstruction-history burst pressure, and substep clock intent. Remaining low-level PT gaps are exact intra-sample recursive substep state updates and rendered parity fixtures.
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns section diameters/areas and derived reflection coefficients; `glottis` owns excitation controls; `tract_injection` owns positioned frication/burst controls and now distributes frication pressure into waveguide section updates; `nasal_branch` owns a second diameter/area tube; `tract_motion` owns control slew and obstruction threshold; `propagation=waveguide` lowers tract voices into generated oral/nasal right-left waveguide equations with live per-section diameter/area/reflection fields, obstruction-history burst pressure, and substep clock intent. `PinkTromboneParityFixtures` now defines fixed Aqua DSL workouts and `AudioComparison` includes log-mel cosine similarity. Remaining low-level PT gaps are exact intra-sample recursive substep state updates and rendered reference thresholds.
- If dotnet builds hang in this workspace, stop orphan dotnet workers and retry with `--disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false`; plain parallel/shared builds can stall without useful diagnostics.
- eSpeak NG is an early parity reference for timing, IPA coverage, and log-mel pressure, not an anatomy oracle or final target.
- Do not expand into prosody, emotional context, personality, or broad Weksa expressivity until the tiny IPA utterance harness can show useful loss movement.

View file

@ -396,6 +396,35 @@ public sealed class PatchScriptTests
Assert.Contains("process =", export.Source);
}
[Fact]
public void PinkTromboneParityFixturesUseReusableLowLevelPrimitives()
{
Assert.True(PinkTromboneParityFixtures.All.Count >= 5);
foreach (var fixture in PinkTromboneParityFixtures.All)
{
var patch = PatchScript.Parse(fixture.AquaScript);
var voice = Assert.Single(patch.Voices);
var export = FaustEmitter.Emit(patch, new FaustExportOptions($"pt_{fixture.Id.Replace('-', '_')}"));
Assert.NotNull(voice.Tract);
Assert.NotEmpty(patch.TractShapes);
Assert.NotEmpty(patch.GlottalSources);
Assert.NotEmpty(patch.TractInjections);
Assert.NotEmpty(patch.NasalBranches);
Assert.NotEmpty(patch.TractMotions);
Assert.Equal(TractPropagationMode.Waveguide, voice.Tract.Propagation);
Assert.Contains("wg_diameter_target_", export.Source);
Assert.Contains("wg_substep_drive", export.Source);
Assert.Contains("process =", export.Source);
Assert.NotEmpty(fixture.ReferenceFeatures);
}
var identical = AudioAnalyzer.CosineSimilarity([1, 2, 3], [1, 2, 3]);
var different = AudioAnalyzer.CosineSimilarity([1, 0, 0], [0, 1, 0]);
Assert.True(identical > .999f);
Assert.True(different < .001f);
}
[Fact]
public void ZynReferenceRebuildsTrackFixtureFeaturePressure()
{