Map Pink Trombone parity pressure

This commit is contained in:
Meta Vens 2026-05-25 18:36:06 +01:00
commit a0eb78270b
8 changed files with 269 additions and 0 deletions

View file

@ -0,0 +1,108 @@
# Pink Trombone Parity Packet
## Objective
Use Pink Trombone as old-school speech parity pressure, not as a vague mascot
for "more vocal knobs." The current AquaSynth speech-loss surface is a
source/filter proxy. Pink Trombone is a moving vocal-tract waveguide. Those are
not the same machine.
This packet exists so the mismatch stays inspectable.
## Reference Source
Primary source for this packet:
- `chdh/pink-trombone-mod`: MIT TypeScript modularization of Neil Thapen's Pink
Trombone, <https://github.com/chdh/pink-trombone-mod>
- Source files inspected:
- `src/Synthesizer.ts`
- `src/Glottis.ts`
- `src/Tract.ts`
- `src/TractShaper.ts`
The original interactive/programmatic Pink Trombone lineage is also visible in
`zakaton/Pink-Trombone`, but that package is GPL-3.0. Treat it as reference
pressure only unless a later license review says otherwise.
## Current Mechanism
Pink Trombone's source-readable graph:
```text
frequency/tenseness/intensity
-> LF-style glottal source plus aspiration noise
-> 44-section main tract diameter array
-> squared diameters become section areas
-> adjacent section areas become reflection coefficients
-> right/left traveling-wave delay-line state
-> 28-section nasal branch through a velum junction
-> lip output + nose output
```
The synthesizer steps the tract twice per output sample. Tract shape changes are
not static formants: the shaper slews section diameters toward target diameters,
updates reflections at block boundaries, injects turbulence at constrictions,
and emits a transient when a closure opens.
## AquaSynth Capability Map
Current AquaSynth can express:
- runtime parameters with stable paths;
- oscillator/noise sources;
- ADSR and staged rate/level envelopes;
- low/high/band/notch filters;
- static and frame-scanned formant banks;
- patch-level and voice-local modulation;
- compiled Faust control sweeps through `/speech/output/N`.
Current AquaSynth cannot express:
- a 44-cell bidirectional tract waveguide;
- independent right/left traveling-wave state arrays;
- a 28-cell nasal waveguide branch;
- a three-way velum/nose junction;
- per-section diameter targets as synthesis authority;
- diameter-squared area reflection coefficients;
- a subgraph that steps twice per output sample;
- turbulence injected at a tract position into neighboring cells;
- obstruction-state release transients;
- exact Pink Trombone LF glottal coefficient updates.
## First Parity Rung
The first committed rung is structural, not audio:
- `PinkTromboneReference.ToReferencePatch()` records the source, tract features,
and relevant controls.
- `ReferenceRebuildCatalog.PinkTromboneRebuilds` contains the current
AquaSynth source-filter proxy as a deliberately non-passing rebuild.
- The proxy script parses and emits Faust, but its missing features explicitly
name the waveguide authorities AquaSynth does not own.
- `PinkTromboneReferenceDeclaresMissingWaveguideAuthority` prevents the proxy
from being mistaken for real tract parity.
That is the correct failure for now. A failed exactness claim is better than a
successful little lie with sliders.
## Next Cut
Do not add another formant workaround. The next coherent implementation is one
of these:
1. Add a dedicated tract-DSP model to AquaSynth.Faust/Core: typed tract sections,
diameter curves, reflection calculation, glottal source, nasal branch, and a
Faust emitter for that structure.
2. Add a test-only Pink Trombone oracle renderer and compare AquaSynth's first
tract-DSP candidate against fixed vowel/constriction fixtures.
The new owner should be explainable as:
```text
TractPlan owns section diameters and source/injection events so tract acoustics
remain true during rendering.
```
If that sentence becomes awkward, the design is probably drifting back into
proxy mud.

View file

@ -608,6 +608,54 @@ public static class BuiltInScripts
public static readonly IReadOnlyList<string> ZynStyleNames = ["additive-lead", "pad-texture", "vocal-layer"];
public const string PinkTromboneProxy = """
patch
gain=0.82
soft_clip=true
param name=frequency path=/pink/frequency default=140 min=10 max=600 step=0.01 unit=Hz rate=audio
param name=intensity path=/pink/intensity default=0.7 min=0 max=1 step=0.001
param name=tenseness path=/pink/tenseness default=0.6 min=0 max=1 step=0.001
param name=tongue_index path=/pink/tongue/index default=12.9 min=0 max=44 step=0.001 unit=cell
param name=tongue_diameter path=/pink/tongue/diameter default=2.43 min=0 max=4 step=0.001 unit=diameter
param name=velum path=/pink/velum default=0.01 min=0.01 max=0.4 step=0.001 unit=diameter
param name=constriction_index path=/pink/constriction/index default=32 min=0 max=44 step=0.001 unit=cell
param name=constriction_diameter path=/pink/constriction/diameter default=1 min=-1 max=4 step=0.001 unit=diameter
defaults
wave=saw
freq=@/pink/frequency
gain=@/pink/intensity
attack=0.004
sustain=0.18
decay=0.08
lpf=0.64
lpf_q=@/pink/tenseness
tremolo=0.02
tremolo_hz=6
formants=520:90:0.8,1250:170:1,2600:320:0.45
formant_mix=0.42
voice
voice
wave=noise
freq=2500
gain=0.04
noise=0.8
hpf=0.45
lpf=0.7
formants=1800:220:0.8,3600:480:0.55
formant_mix=0.35
voice
wave=sine
freq=360
gain=@/pink/velum
formants=300:80:1,900:180:0.45
formant_mix=0.55
""";
public const string Dx7StyleAlgorithm32AdditiveOrgan = """
patch
gain=0.58
@ -1075,6 +1123,7 @@ public static class BuiltInScripts
foreach (var item in Classic808PrimitiveGolfScripts) yield return ("808", item.Name, item.Script);
foreach (var item in FmBellPrimitiveGolfScripts) yield return ("fm-bell", item.Name, item.Script);
foreach (var item in WobbleBassPrimitiveGolfScripts) yield return ("wobble-bass", item.Name, item.Script);
yield return ("pink-trombone", "current-aquasynth-proxy", PinkTromboneProxy);
foreach (var item in ReferenceRebuildCatalog.Dx7Rebuilds) yield return ("dx7", item.Name, item.Script);
foreach (var item in ReferenceRebuildCatalog.ZynRebuilds) yield return ("zynaddsubfx", item.Name, item.Script);
foreach (var item in AdvancedReferenceScripts) yield return ("advanced", item.Name, item.Script);

View file

@ -0,0 +1,74 @@
namespace AquaSynth.Dsl;
public static class PinkTromboneReference
{
public const string ReferenceId = "pink-trombone/modular-waveguide";
public static ReferencePatch ToReferencePatch() =>
new(
ReferenceId,
"pink-trombone",
"Pink Trombone modular waveguide",
Source,
Features,
Parameters);
public static ReferenceSource Source { get; } = new(
"source",
"https://github.com/chdh/pink-trombone-mod",
"MIT",
"",
"MIT TypeScript modularization of Neil Thapen's Pink Trombone, used as source-readable parity pressure.");
public static IReadOnlyList<ReferenceFeature> Features { get; } =
[
new("main_tract_waveguide_cells", "44", "Tract.n owns the main vocal tract section count."),
new("nose_waveguide_cells", "28", "Tract.noseLength owns the nasal branch section count."),
new("tract_sample_rate", "2x-audio-sample-rate", "Synthesizer steps the tract twice for each output sample."),
new("glottal_source", "LF-style-normalized-waveform", "Glottis derives an LF-style waveform from frequency and tenseness."),
new("glottal_reflection", "0.75", "The source end reflects the left-going tract wave while adding glottal output."),
new("lip_reflection", "-0.85", "The lip end reflects the right-going tract wave and radiates output."),
new("diameter_authority", "section-diameter-array", "A 44-element diameter array owns tract shape before reflection calculation."),
new("reflection_formula", "(area[i-1]-area[i])/(area[i-1]+area[i])", "Reflection coefficients derive from adjacent squared diameters."),
new("nose_junction", "three-way-velum-reflection", "The tract/nose junction uses left, right, and nose reflection coefficients from local areas."),
new("turbulence_injection", "positioned-constriction-noise", "Frication noise is injected into neighboring tract cells based on constriction position and diameter."),
new("closure_transients", "obstruction-release-impulse", "Opening a closure can add a decaying transient at the obstruction position."),
new("tract_shape_motion", "diameter-target-slew", "TractShaper moves diameters toward target shape with position-dependent return speeds.")
];
public static IReadOnlyList<PatchParameter> Parameters { get; } =
[
new("/pink/frequency", "Frequency", 140, 10, 600, 0.01f, "Hz", "audio", "Glottal target frequency."),
new("/pink/intensity", "Intensity", 0.7f, 0, 1, 0.001f, "normalized", "control", "Glottal excitation intensity."),
new("/pink/tenseness", "Tenseness", 0.6f, 0, 1, 0.001f, "normalized", "control", "LF waveform/turbulence voice-quality control."),
new("/pink/tongue/index", "Tongue index", 12.9f, 0, 44, 0.001f, "cell", "control", "Tongue-body constriction center in tract-cell coordinates."),
new("/pink/tongue/diameter", "Tongue diameter", 2.43f, 0, 4, 0.001f, "diameter", "control", "Tongue-body diameter target."),
new("/pink/velum", "Velum", 0.01f, 0.01f, 0.4f, 0.001f, "diameter", "control", "Nasal branch opening diameter."),
new("/pink/constriction/index", "Constriction index", 32, 0, 44, 0.001f, "cell", "control", "Fricative or closure constriction position."),
new("/pink/constriction/diameter", "Constriction diameter", 1, -1, 4, 0.001f, "diameter", "control", "Fricative or closure constriction diameter.")
];
public static ReferenceRebuild CurrentAquaSynthProxy() =>
new(
"pink-trombone/current-aquasynth-proxy",
"pink-trombone",
"Current AquaSynth source-filter proxy",
ReferenceId,
BuiltInScripts.PinkTromboneProxy,
[
new("runtime_controls", "/pink/frequency,/pink/intensity,/pink/tenseness,/pink/tongue/index,/pink/tongue/diameter,/pink/velum", "AquaSynth can expose stable runtime parameter paths."),
new("glottal_pitch_pressure_proxy", "oscillator-frequency-plus-gain", "AquaSynth can drive pitch/loudness-like controls without recompiling."),
new("turbulence_proxy", "noise-voice", "AquaSynth can emit a separate noise source for frication pressure."),
new("formant_proxy", "static-resonant-bandpass-bank", "AquaSynth can sketch vowel/body color with formant filters.")
],
[
new("main_tract_waveguide_cells", "44 bidirectional sections", "No AquaSynth DSL authority owns arrays of right/left traveling wave state per tract section."),
new("nose_waveguide_cells", "28 bidirectional sections", "The current DSL has no nasal branch delay-line graph or velum-controlled three-way junction."),
new("diameter_to_reflection_coefficients", "per-block area reflection update", "Formant filters do not preserve the section-area ownership or reflection math."),
new("twice_rate_tract_stepping", "two tract steps per audio sample", "AquaSynth voice graphs do not express a subgraph with its own stepping rate."),
new("positioned_turbulence_injection", "noise injected into neighboring cells", "The current noise field is source/color pressure, not a positioned tract injection event."),
new("closure_release_transients", "obstruction release impulse", "AquaSynth envelopes can shape amplitude, but they do not observe obstruction state and emit release transients."),
new("lf_glottal_exactness", "Pink Trombone LF-style coefficients", "The proxy uses ordinary oscillators/noise, not the LF coefficient path from tenseness.")
],
"This is a deliberately non-passing rebuild: it keeps the current source-filter proxy parseable while naming the tract-DSP authority AquaSynth lacks.");
}

View file

@ -131,8 +131,18 @@ public static class ReferenceRebuildCatalog
"This is the first thresholded audio parity rung. It proves the render/compare loop on a lawful fixture without pretending the DX7 internals are solved.")
];
public static readonly IReadOnlyList<ReferenceRebuild> PinkTromboneRebuilds =
[
PinkTromboneReference.CurrentAquaSynthProxy()
];
public static IEnumerable<ReferenceRebuild> All()
{
foreach (var rebuild in PinkTromboneRebuilds)
{
yield return rebuild;
}
foreach (var rebuild in ZynRebuilds)
{
yield return rebuild;

View file

@ -129,3 +129,4 @@
{"date":"2026-05-24","topic":"cultmesh-speech-training-step","lesson":"AquaSynth can now run the first honest CultMesh-distributed speech training step: an authority trainer emits SpeechRenderRequest documents and a SpeechWorkerPayloadManifest, admits only CultMesh peers with valid speech-render-worker leases, records SpeechWorkerAdmissionReceipt documents for admitted/rejected peers, lets admitted workers produce SpeechRenderResult gradients, and applies those gradients centrally through SpeechBackpropagationPipeline. Workers still do not write checkpoints.","evidence":"Added GameCult.Mesh to AquaSynth.Core, added payload/admission/assignment/training-step types and store helpers in SpeechDistributedTraining.cs, added RunCultMeshTrainingStep, and added a test with two leased workers plus rejected unleased/read-replica peers. Focused verification: `dotnet test tests/AquaSynth.Dsl.Tests/AquaSynth.Dsl.Tests.csproj --filter \"SpeechDistributedTrainingTests\" -v minimal`: 3 passed. Remaining infrastructure gap: real remote artifact chunk transfer and process supervision belong to CultMesh, not an AquaSynth-local workaround."}
{"date":"2026-05-25","topic":"vocal-tract-control-playground","lesson":"The current AquaSynth vocal-tract lane is touchable as a voice-patch target surface before it is a Pink Trombone-class tract simulation. A useful playground should expose the real VocalTractControlTarget knobs directly: these are the controls the learned speech driver predicts from phonetic and utterance input, not learned controls themselves. Keep the tract-ish visualization honest that the WebAudio renderer is a lab proxy rather than full waveguide anatomy.","evidence":"Added tools/vocal-tract-playground with static HTML/CSS/JS, a Node static server, WebAudio source/filter synthesis, tract canvas visualization, presets, and sliders for the 14 base voice-patch target controls plus six mel bands. Verified with `node --check` on app.js/serve.mjs, curl against http://localhost:5125/, and browser interaction: Play toggled audio, Ee preset updated controls and canvas, console errors were empty."}
{"date":"2026-05-25","topic":"speech-loss-control-authority","lesson":"Exporting `/speech/output/N` controls is not proof that the speech training surface is usable. Every base VocalTractControlTarget output must route into audible DSP before gradient descent is trusted; epsilon keep-alive references are not control authority.","evidence":"The compiled Faust speech-loss surface previously used tongue body, tongue tip, lip aperture, lip rounding, and velum only in the final keep-controls epsilon. Routed tract controls into small resonant source-filter proxies, velum into a nasal tone, LFO depth into tremolo depth, and added tests that fail when base controls are only declared/kept alive. Focused verification: `dotnet test AquaSynth.sln --no-restore --filter FaustRenderedSpeechLossSurfaceTests` passed. Full `dotnet test AquaSynth.sln --no-restore` timed out twice at 120s and 300s without a result; orphaned dotnet workers were stopped."}
{"date":"2026-05-25","topic":"pink-trombone-parity-boundary","lesson":"Pink Trombone parity must pressure AquaSynth at the tract-DSP authority, not through more source/filter proxy controls. The current DSL can expose parameters and formant/noise proxies, but it cannot exactly represent Pink Trombone's moving bidirectional waveguide graph: 44 main tract cells, 28 nasal cells, diameter-to-area reflection coefficients, velum junction, twice-rate tract stepping, positioned turbulence injection, closure transients, or exact LF glottal coefficient updates.","evidence":"Inspected `chdh/pink-trombone-mod` source (`Synthesizer.ts`, `Glottis.ts`, `Tract.ts`, `TractShaper.ts`) and added `docs/pink-trombone-parity.md`, `PinkTromboneReference`, a current non-passing AquaSynth proxy rebuild, and `PinkTromboneReferenceDeclaresMissingWaveguideAuthority` so the proxy cannot be mistaken for actual tract parity."}

View file

@ -18,6 +18,7 @@ surfaces:
reference_roadmap: docs/reference-synth-roadmap.md
ipa_vocal_tract_roadmap: docs/ipa-vocal-tract-roadmap.md
utterance_parity_research: docs/utterance-parity-research.md
pink_trombone_parity: docs/pink-trombone-parity.md
wet_voice_witness_fixture: tests/AquaSynth.Dsl.Tests/Fixtures/Speech/WetVoice01
faust_toolchain_boundary: docs/faust-toolchain-boundary.md
dx7_public_domain_fixtures: tests/AquaSynth.Dsl.Tests/Fixtures/Dx7/PublicDomain
@ -35,6 +36,7 @@ modules:
model: src/AquaSynth.Core/Model.cs
parser: src/AquaSynth.Core/PatchScript.cs
vocal_tract_model: src/AquaSynth.Core/VocalTractModel.cs
pink_trombone_reference: src/AquaSynth.Core/PinkTromboneReference.cs
packed_neural_network: src/AquaSynth.Core/PackedNeuralNetwork.cs
utterance_embedding_learning: src/AquaSynth.Core/UtteranceEmbeddingLearning.cs
speech_backpropagation_pipeline: src/AquaSynth.Core/SpeechBackpropagationPipeline.cs

View file

@ -42,6 +42,7 @@ current_mechanism:
zyn_pressure_survey: docs/zynaddsubfx-pressure-survey.md
roadmap: docs/reference-synth-roadmap.md
utterance_parity_research: docs/utterance-parity-research.md
pink_trombone_parity: docs/pink-trombone-parity.md
parameter_contracts: src/AquaSynth.Core/Model.cs
live_invariants:
@ -109,6 +110,7 @@ live_invariants:
- Do not hide missing graph structure behind unrelated voice fields.
- 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. Current AquaSynth cannot exactly express the 44-cell main tract, 28-cell nasal branch, diameter-to-reflection updates, twice-rate tract stepping, positioned turbulence injection, or closure transients through the ordinary `voice` DSL.
- 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.
- Speech training receipts must keep time as a witness: every request, render result, and checkpoint should record stage decision time, latency spent, latency budget, confidence, and artifact/model identifiers.

View file

@ -288,6 +288,29 @@ public sealed class PatchScriptTests
}
}
[Fact]
public void PinkTromboneReferenceDeclaresMissingWaveguideAuthority()
{
var reference = PinkTromboneReference.ToReferencePatch();
var rebuild = Assert.Single(ReferenceRebuildCatalog.PinkTromboneRebuilds);
var patch = PatchScript.Parse(rebuild.Script);
var export = FaustEmitter.Emit(patch, new FaustExportOptions("pink_trombone_proxy"));
Assert.Equal("pink-trombone", rebuild.Family);
Assert.Contains(reference.Features, feature => feature.Name == "main_tract_waveguide_cells" && feature.Value == "44");
Assert.Contains(reference.Features, feature => feature.Name == "nose_waveguide_cells" && feature.Value == "28");
Assert.Contains(reference.Features, feature => feature.Name == "tract_sample_rate" && feature.Value == "2x-audio-sample-rate");
Assert.Contains(reference.Features, feature => feature.Name == "reflection_formula");
Assert.Contains(reference.Parameters, parameter => parameter.Path == "/pink/tongue/index");
Assert.Contains(reference.Parameters, parameter => parameter.Path == "/pink/constriction/diameter");
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "formant_proxy");
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "main_tract_waveguide_cells");
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "diameter_to_reflection_coefficients");
Assert.Contains(rebuild.MissingFeatures, feature => feature.Name == "nose_waveguide_cells");
Assert.NotEmpty(patch.Parameters);
Assert.Contains("process =", export.Source);
}
[Fact]
public void ZynReferenceRebuildsTrackFixtureFeaturePressure()
{