Add Pink Trombone parity fixtures
This commit is contained in:
parent
da4be187a2
commit
7a42c70809
8 changed files with 125 additions and 3 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
65
src/AquaSynth.Core/PinkTromboneParityFixtures.cs
Normal file
65
src/AquaSynth.Core/PinkTromboneParityFixtures.cs
Normal 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}}
|
||||
""";
|
||||
}
|
||||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue