Generalize tract morphology geometry
This commit is contained in:
parent
d745181059
commit
b928416d63
7 changed files with 178 additions and 51 deletions
|
|
@ -11,16 +11,18 @@ morphologies.
|
|||
## Current Mechanism
|
||||
|
||||
`tract` currently lowers into `Voice.Tract`: a voice-local scalar control bundle
|
||||
with a Faust source/filter proxy. It is useful as a playground surface and as a
|
||||
named failure against Pink Trombone, but it does not own the anatomy:
|
||||
with a Faust source/filter proxy and an early waveguide backend. It is useful as
|
||||
a playground surface and as a named failure against Pink Trombone, but the
|
||||
remaining anatomy ownership is still too discrete:
|
||||
|
||||
- no reusable tract area function;
|
||||
- no derived area/reflection coefficient primitive;
|
||||
- no traveling-wave state owner;
|
||||
- no nasal junction primitive;
|
||||
- no waveguide-applied positioned excitation/injection events;
|
||||
- no substep/rate semantics;
|
||||
- no parity renderer/score loop against PT output.
|
||||
- authored tract shapes now own physical length and continuous interpolation,
|
||||
and a tract voice may resample them to a chosen compiled grid;
|
||||
- `sections`, `nose_sections`, and integer junction indices still leak lowering
|
||||
grid choices into the script surface;
|
||||
- `substeps` still exists as compatibility clock intent, but it should be
|
||||
demoted in favor of acoustic length, propagation speed, and fractional delay;
|
||||
- topology changes can recompile, but morphology motion must remain runtime
|
||||
control data.
|
||||
|
||||
## Invariants
|
||||
|
||||
|
|
@ -28,10 +30,17 @@ named failure against Pink Trombone, but it does not own the anatomy:
|
|||
- A tract voice remains a voice. Tract primitives are treatments, sources,
|
||||
curves, junctions, and event lanes that can be combined with the rest of the
|
||||
patch graph.
|
||||
- Diameter/area functions own static or slowly moving tract shape. Tongue and
|
||||
constriction gestures deform that shape; they do not replace the owner.
|
||||
- Diameter/area functions own continuous tract shape over normalized position,
|
||||
physical acoustic length, derived areas, reflection coefficients, and
|
||||
resampling. Tongue and constriction gestures deform that shape; they do not
|
||||
replace the owner.
|
||||
- PT's 44 oral cells and 28 nasal cells are a reference discretization, not an
|
||||
Aqua morphology model.
|
||||
- Reflection coefficients derive from adjacent areas. Caches and summaries may
|
||||
observe that derivation, but must not become independent truth.
|
||||
- Waveguide clock derives from acoustic length, propagation speed, sample rate,
|
||||
and delay approximation. A user-facing `substeps` count is only legacy
|
||||
pressure until the fractional-delay waveguide owns this cleanly.
|
||||
- Waveguide state owns delay-line propagation once it exists. Formant filters
|
||||
may remain cheap approximations, but they must be named as approximations.
|
||||
- Expressive parity comes before audio golf. Audio parity claims require
|
||||
|
|
@ -40,8 +49,10 @@ named failure against Pink Trombone, but it does not own the anatomy:
|
|||
## Primitive Decomposition
|
||||
|
||||
1. `tract_shape`: reusable section diameter/area function.
|
||||
- Owns sampled diameters or areas over normalized tract position.
|
||||
- Emits diameters, areas, reflection coefficients, and shape summaries.
|
||||
- Owns sampled diameters or areas over normalized continuous tract position.
|
||||
- Owns physical acoustic length through `length_cm`.
|
||||
- Emits interpolated diameters, resampled grids, areas, reflection
|
||||
coefficients, per-cell acoustic delay, and shape summaries.
|
||||
- Feeds `tract` voices and later morphology/gesture planning.
|
||||
|
||||
2. `glottis`: reusable excitation primitive.
|
||||
|
|
@ -64,15 +75,16 @@ named failure against Pink Trombone, but it does not own the anatomy:
|
|||
when a waveguide tract references the branch.
|
||||
|
||||
5. `waveguide_tract`: propagation primitive.
|
||||
- Owns right/left traveling-wave state, substep rate, loss, and radiation.
|
||||
- Owns right/left traveling-wave state, acoustic wave clock, fractional
|
||||
delay/loss approximation, and radiation.
|
||||
- Consumes area/reflection fields and source/injection events.
|
||||
- First oral-tube lowering exists: generated right/left section equations
|
||||
consume `tract_shape` reflection coefficients and boundary reflections.
|
||||
- Waveguide lowering now derives live per-section diameter targets, areas,
|
||||
and reflection coefficients from the tract shape plus tongue, constriction,
|
||||
and lip controls before scattering.
|
||||
Substep clock intent is represented and lowering consumes it for drive/loss
|
||||
scaling, but exact intra-sample recursive state updates remain missing.
|
||||
Legacy substep clock intent is represented and lowering consumes it for
|
||||
drive/loss scaling, but exact fractional-delay propagation remains missing.
|
||||
|
||||
6. `tract_motion`: control-rate slew and obstruction history.
|
||||
- Owns diameter/constriction/velum slew rates and obstruction threshold.
|
||||
|
|
@ -117,10 +129,20 @@ Faust. Current waveguide baseline: open vowel 0.5578, front vowel 0.5368,
|
|||
nasal 0.4299, sibilant 0.2852, closure-release 0.1147. Those are pressure
|
||||
readings, not parity, but closure-release is no longer anti-correlated.
|
||||
|
||||
Exact Pink Trombone timing still needs pressure: the loop currently performs
|
||||
one feedback update per output sample while carrying `substeps` as drive/loss
|
||||
intent. The next backend cut is true twice-per-sample state update inside the
|
||||
Faust-friendly loop shape, not a return to named equation sprawl.
|
||||
The continuous morphology cut has started. `tract_shape length_cm=...` is now
|
||||
physical geometry, not just a list length. `TractAreaFunction` can interpolate
|
||||
diameter over normalized position, resample to an arbitrary grid, and derive
|
||||
per-cell acoustic delay. A `tract` voice may choose a different `sections=...`
|
||||
lowering grid without requiring the named shape's authoring samples to match.
|
||||
For a human-length 17 cm tract sampled at PT's 44 oral cells at 44.1 kHz, that
|
||||
delay is about 0.5 samples per cell. That makes PT's two tract updates per
|
||||
output sample look like a discretization strategy for a half-sample cell grid,
|
||||
not a reusable Aqua language concept.
|
||||
|
||||
Exact Pink Trombone timing still needs pressure, but the next backend cut is no
|
||||
longer "add more substeps." It is a fractional-delay waveguide lowering whose
|
||||
clock comes from physical length, propagation speed, and sample rate while
|
||||
morphology moves through runtime controls without recompiling the patch.
|
||||
|
||||
## Cut Line
|
||||
|
||||
|
|
|
|||
|
|
@ -46,12 +46,21 @@ 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.
|
||||
|
||||
The 44 oral and 28 nasal counts are Pink Trombone's implementation grid, not
|
||||
the AquaSynth abstraction. At 44.1 kHz, a 17 cm tract split into 44 cells has an
|
||||
acoustic travel time of roughly 0.5 samples per cell, so the twice-per-sample
|
||||
update is better understood as a discretization/wave-clock choice. AquaSynth's
|
||||
live target is continuous morphology plus fractional-delay waveguide lowering,
|
||||
with the grid chosen by the backend.
|
||||
|
||||
## AquaSynth Capability Map
|
||||
|
||||
Current AquaSynth can express:
|
||||
|
||||
- runtime parameters with stable paths;
|
||||
- reusable `tract_shape` declarations with authored diameter or area samples;
|
||||
- reusable `tract_shape` declarations with authored diameter or area samples,
|
||||
physical length, normalized interpolation, resampling, and per-cell acoustic
|
||||
delay derivation;
|
||||
- derived tract areas and adjacent-section reflection coefficients;
|
||||
- reusable `glottis` declarations for tract excitation quality;
|
||||
- reusable `tract_injection` declarations for positioned frication/burst
|
||||
|
|
@ -75,7 +84,8 @@ Current AquaSynth can express:
|
|||
|
||||
Current AquaSynth cannot exactly express:
|
||||
|
||||
- exact two recursive tract state updates inside one output sample;
|
||||
- fractional-delay tract propagation whose clock derives from physical length,
|
||||
propagation speed, and sample rate;
|
||||
- exact Pink Trombone LF glottal coefficient updates.
|
||||
|
||||
## Current Parity Rung
|
||||
|
|
@ -89,9 +99,10 @@ Trombone anatomy:
|
|||
AquaSynth tract-voice proxy as a deliberately non-passing rebuild.
|
||||
- The `tract` DSL command parses into an ordinary `Voice` with `Voice.Tract`,
|
||||
so this remains a voice with a very expressive tract treatment.
|
||||
- `tract_shape` owns reusable section diameter/area functions. The current
|
||||
proxy consumes its shape summaries and derived reflection energy; future
|
||||
waveguide lowering should consume the full coefficients.
|
||||
- `tract_shape` owns reusable continuous diameter/area functions. The current
|
||||
proxy consumes its shape summaries and derived reflection energy; waveguide
|
||||
lowering consumes a compiled grid sampled from that continuous owner, so
|
||||
authoring sample count and backend section count can differ.
|
||||
- `glottis` and `tract_injection` own excitation and positioned noise/burst
|
||||
controls consumed by tract voices.
|
||||
- `propagation=waveguide` emits an oral right/left tube from the derived
|
||||
|
|
@ -104,9 +115,9 @@ Trombone anatomy:
|
|||
pressure from obstruction history.
|
||||
- waveguide lowering derives live per-section diameter targets, areas, and
|
||||
reflection coefficients from shape and gesture controls.
|
||||
- `substeps` expresses waveguide clock intent; current Faust lowering consumes
|
||||
it through drive/loss scaling but does not yet prove exact intra-sample state
|
||||
updates.
|
||||
- `substeps` is now legacy waveguide clock pressure. Current Faust lowering
|
||||
consumes it through drive/loss scaling, but the coherent target is
|
||||
fractional-delay propagation from physical tract length and wave speed.
|
||||
- `PinkTromboneParityFixtures` defines fixed Aqua DSL workouts for open,
|
||||
front, nasal, sibilant, and closure cases using the reusable low-level
|
||||
primitives.
|
||||
|
|
@ -140,8 +151,10 @@ of these:
|
|||
DSP model in AquaSynth.Faust/Core: typed tract sections, diameter curves,
|
||||
reflection calculation, glottal source, nasal branch, and a Faust emitter for
|
||||
that structure.
|
||||
2. Add true twice-per-output-sample state update inside the Faust-friendly
|
||||
feedback-loop component.
|
||||
2. Replace `substeps` as a musical surface with fractional-delay waveguide
|
||||
lowering. Physical length, propagation speed, and sample rate decide delay;
|
||||
runtime morphology controls move diameters and junction openings without
|
||||
recompiling unless topology changes.
|
||||
3. Use the log-mel artifact loop to golf fixture-by-fixture now that the real
|
||||
tract path renders.
|
||||
|
||||
|
|
|
|||
|
|
@ -138,10 +138,19 @@ public sealed record Formant(float FrequencyHz, float BandwidthHz, float Gain);
|
|||
|
||||
public sealed record FormantFrame(IReadOnlyList<Formant> Formants);
|
||||
|
||||
public sealed record TractAreaFunction(IReadOnlyList<float> Diameters)
|
||||
public sealed record TractAreaFunction(IReadOnlyList<float> Diameters, float LengthCentimeters = 17)
|
||||
{
|
||||
public int Sections => Diameters.Count;
|
||||
|
||||
public float LengthMeters => MathF.Max(0, LengthCentimeters) / 100;
|
||||
|
||||
public float SectionLengthMeters => Sections == 0 ? 0 : LengthMeters / Sections;
|
||||
|
||||
public float CellDelaySamples(float sampleRate, float propagationSpeedMetersPerSecond = 343) =>
|
||||
propagationSpeedMetersPerSecond <= 0
|
||||
? 0
|
||||
: SectionLengthMeters / propagationSpeedMetersPerSecond * MathF.Max(0, sampleRate);
|
||||
|
||||
public IReadOnlyList<float> Areas => Diameters.Select(diameter => MathF.Max(0, diameter) * MathF.Max(0, diameter)).ToArray();
|
||||
|
||||
public IReadOnlyList<float> ReflectionCoefficients
|
||||
|
|
@ -164,8 +173,8 @@ public sealed record TractAreaFunction(IReadOnlyList<float> Diameters)
|
|||
public float AverageDiameter(float startFraction, float endFraction)
|
||||
{
|
||||
if (Diameters.Count == 0) return 0;
|
||||
var start = Math.Clamp((int)MathF.Floor(startFraction * Diameters.Count), 0, Diameters.Count - 1);
|
||||
var end = Math.Clamp((int)MathF.Ceiling(endFraction * Diameters.Count), start + 1, Diameters.Count);
|
||||
var start = Math.Clamp((int)MathF.Floor(Math.Clamp(startFraction, 0, 1) * Diameters.Count), 0, Diameters.Count - 1);
|
||||
var end = Math.Clamp((int)MathF.Ceiling(Math.Clamp(endFraction, 0, 1) * Diameters.Count), start + 1, Diameters.Count);
|
||||
var sum = 0f;
|
||||
for (var i = start; i < end; i++)
|
||||
{
|
||||
|
|
@ -175,10 +184,31 @@ public sealed record TractAreaFunction(IReadOnlyList<float> Diameters)
|
|||
return sum / Math.Max(1, end - start);
|
||||
}
|
||||
|
||||
public float DiameterAt(float normalizedPosition)
|
||||
{
|
||||
if (Diameters.Count == 0) return 0;
|
||||
if (Diameters.Count == 1) return Diameters[0];
|
||||
var position = Math.Clamp(normalizedPosition, 0, 1) * (Diameters.Count - 1);
|
||||
var left = (int)MathF.Floor(position);
|
||||
var right = Math.Min(Diameters.Count - 1, left + 1);
|
||||
var mix = position - left;
|
||||
return Diameters[left] + (Diameters[right] - Diameters[left]) * mix;
|
||||
}
|
||||
|
||||
public TractAreaFunction Resample(int sections)
|
||||
{
|
||||
if (sections < 1) throw new ArgumentOutOfRangeException(nameof(sections), "section count must be positive");
|
||||
if (sections == 1) return new TractAreaFunction([DiameterAt(0)], LengthCentimeters);
|
||||
var diameters = Enumerable.Range(0, sections)
|
||||
.Select(i => DiameterAt((float)i / (sections - 1)))
|
||||
.ToArray();
|
||||
return new TractAreaFunction(diameters, LengthCentimeters);
|
||||
}
|
||||
|
||||
public float MinimumDiameter => Diameters.Count == 0 ? 0 : Diameters.Min();
|
||||
|
||||
public static TractAreaFunction FromAreas(IReadOnlyList<float> areas) =>
|
||||
new(areas.Select(area => MathF.Sqrt(MathF.Max(0, area))).ToArray());
|
||||
public static TractAreaFunction FromAreas(IReadOnlyList<float> areas, float lengthCentimeters = 17) =>
|
||||
new(areas.Select(area => MathF.Sqrt(MathF.Max(0, area))).ToArray(), lengthCentimeters);
|
||||
}
|
||||
|
||||
public sealed record TractShape(string Name, TractAreaFunction AreaFunction);
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ public static class PatchScript
|
|||
throw new PatchScriptException(line, $"duplicate tract shape `{name}`");
|
||||
}
|
||||
|
||||
var areaFunction = ParseTractAreaFunction(fields, line);
|
||||
var areaFunction = ParseTractAreaFunction(fields, line, 17);
|
||||
var shape = new TractShape(name, areaFunction);
|
||||
_tractShapes.Add(shape);
|
||||
_tractShapesByName[name] = shape;
|
||||
|
|
@ -362,7 +362,7 @@ public static class PatchScript
|
|||
|
||||
var branch = new NasalBranch(
|
||||
name,
|
||||
ParseTractAreaFunction(fields, line),
|
||||
ParseTractAreaFunction(fields, line, 12),
|
||||
GetBoundInt(fields, line, 17, $"/nasal_branches/{_nasalBranches.Count}/junction", "junction", "junction_index", "at"),
|
||||
GetBoundFloat(fields, line, 0.01f, $"/nasal_branches/{_nasalBranches.Count}/velum", "velum", "opening"),
|
||||
GetBoundFloat(fields, line, -0.85f, $"/nasal_branches/{_nasalBranches.Count}/reflection", "reflection", "lip_reflection"),
|
||||
|
|
@ -640,10 +640,9 @@ public static class PatchScript
|
|||
{
|
||||
throw new PatchScriptException(line, "tract sections must be at least 4");
|
||||
}
|
||||
if (areaFunction is not null && areaFunction.Sections != sections)
|
||||
{
|
||||
throw new PatchScriptException(line, "tract shape section count must match tract sections");
|
||||
}
|
||||
areaFunction = areaFunction is not null && areaFunction.Sections != sections
|
||||
? areaFunction.Resample(sections)
|
||||
: areaFunction;
|
||||
var noseSections = GetBoundInt(fields, line, nasal?.AreaFunction?.Sections ?? 28, OwnerField(ownerPath, "tract/nose_sections"), "nose_sections", "nose_cells");
|
||||
if (noseSections < 0)
|
||||
{
|
||||
|
|
@ -1068,7 +1067,7 @@ public static class PatchScript
|
|||
}
|
||||
if (hasInlineShape)
|
||||
{
|
||||
return ParseTractAreaFunction(fields, line);
|
||||
return ParseTractAreaFunction(fields, line, 17);
|
||||
}
|
||||
if (!hasNamedShape)
|
||||
{
|
||||
|
|
@ -1144,15 +1143,16 @@ public static class PatchScript
|
|||
throw new PatchScriptException(line, $"unknown nasal branch `{nasalName}`");
|
||||
}
|
||||
|
||||
var fallback = hasNamedNasal ? namedNasal! : new NasalBranch(AreaFunction: new TractAreaFunction([0.01f, 0.6f, 1.2f, 1.6f]));
|
||||
var fallback = hasNamedNasal ? namedNasal! : new NasalBranch(AreaFunction: new TractAreaFunction([0.01f, 0.6f, 1.2f, 1.6f], 12));
|
||||
var areaFunction = fallback.AreaFunction;
|
||||
var lengthCentimeters = GetFloat(fields, line, areaFunction?.LengthCentimeters ?? 12, "length_cm", "length_centimeters");
|
||||
if (TryGetAny(fields, ["nose_diameters"], out var diameters))
|
||||
{
|
||||
areaFunction = new TractAreaFunction(ParseFloatList(diameters, line, "nose_diameters").ToArray());
|
||||
areaFunction = new TractAreaFunction(ParseFloatList(diameters, line, "nose_diameters").ToArray(), lengthCentimeters);
|
||||
}
|
||||
if (TryGetAny(fields, ["nose_areas"], out var areas))
|
||||
{
|
||||
areaFunction = TractAreaFunction.FromAreas(ParseFloatList(areas, line, "nose_areas").ToArray());
|
||||
areaFunction = TractAreaFunction.FromAreas(ParseFloatList(areas, line, "nose_areas").ToArray(), lengthCentimeters);
|
||||
}
|
||||
|
||||
return new NasalBranch(
|
||||
|
|
@ -1188,7 +1188,7 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, fallback.ObstructionThreshold, OwnerField(ownerPath, "tract/motion/obstruction_threshold"), "obstruction_threshold", "closure"));
|
||||
}
|
||||
|
||||
private static TractAreaFunction ParseTractAreaFunction(IReadOnlyDictionary<string, string> fields, int line)
|
||||
private static TractAreaFunction ParseTractAreaFunction(IReadOnlyDictionary<string, string> fields, int line, float defaultLengthCentimeters)
|
||||
{
|
||||
var hasDiameters = TryGetAny(fields, ["diameters", "diameter"], out var diametersText);
|
||||
var hasAreas = TryGetAny(fields, ["areas", "area"], out var areasText);
|
||||
|
|
@ -1206,10 +1206,15 @@ public static class PatchScript
|
|||
{
|
||||
throw new PatchScriptException(line, "tract shape samples must be zero or greater");
|
||||
}
|
||||
var lengthCentimeters = GetFloat(fields, line, defaultLengthCentimeters, "length_cm", "length_centimeters");
|
||||
if (lengthCentimeters <= 0)
|
||||
{
|
||||
throw new PatchScriptException(line, "tract shape length_cm must be greater than zero");
|
||||
}
|
||||
|
||||
return hasDiameters
|
||||
? new TractAreaFunction(samples.ToArray())
|
||||
: TractAreaFunction.FromAreas(samples.ToArray());
|
||||
? new TractAreaFunction(samples.ToArray(), lengthCentimeters)
|
||||
: TractAreaFunction.FromAreas(samples.ToArray(), lengthCentimeters);
|
||||
}
|
||||
|
||||
private static PadSpectrumProfile ParsePadSpectrumProfile(IReadOnlyDictionary<string, string> fields, int line)
|
||||
|
|
|
|||
|
|
@ -140,3 +140,4 @@
|
|||
{"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."}
|
||||
{"timestamp":"2026-05-25T21:34:00+01:00","topic":"pink-trombone-logmel-parity","claim":"PinkTromboneReferenceRenderer now gives deterministic PT-style fixture audio and PinkTromboneLogMelParityTests writes log-mel/listening artifacts. The compileable Aqua resonator proxy baseline is measurable but weak: cosine 0.5129 open vowel, 0.5405 front vowel, 0.3882 nasal, 0.1156 sibilant, -0.1830 closure-release. Faust waveguide named-state lowering caused compiler churn and remains backend debt rather than a parity claim.","evidence":"dotnet test tests/AquaSynth.Dsl.Tests/AquaSynth.Dsl.Tests.csproj --no-build --filter PinkTromboneFixturesReportLogMelParityWhenFaustIsInstalled -v minimal passed; artifacts/parity/pink-trombone-logmel/20260525T212806301/summary.txt"}
|
||||
{"timestamp":"2026-05-26T09:37:00+01:00","topic":"pink-trombone-waveguide-faust-loop","claim":"The Pink Trombone waveguide lowering now uses one Faust feedback component (`wg_loop ~ si.bus(...)`) instead of scalar named recursive equations. Full 44-section oral and 28-section nasal waveguide candidates compile/render through Faust. Current log-mel cosine baseline: open vowel 0.5578, front vowel 0.5368, nasal 0.4299, sibilant 0.2852, closure-release 0.1147.","evidence":"dotnet test tests/AquaSynth.Dsl.Tests/AquaSynth.Dsl.Tests.csproj --no-build --filter PinkTromboneFixturesReportLogMelParityWhenFaustIsInstalled -v minimal passed; artifacts/parity/pink-trombone-logmel/20260526T093628426/summary.txt"}
|
||||
{"timestamp":"2026-05-26T10:10:00+01:00","topic":"continuous-tract-morphology","claim":"PT's 44 oral cells are a reference grid, not anatomy. A 17 cm tract split into 44 cells at 44.1 kHz has about 0.5 samples of acoustic delay per cell, so PT-style substeps are pressure for fractional-delay waveguide lowering rather than a durable Aqua DSL primitive. `TractAreaFunction` now owns physical length, normalized interpolation, resampling, and per-cell delay derivation; `tract sections=...` can choose a compiled lowering grid without forcing the named morphology's authoring sample count to match.","evidence":"Added `length_cm` parsing for `tract_shape`/`nasal_branch`, `TractAreaFunction.DiameterAt`, `Resample`, and `CellDelaySamples`; focused tests cover continuous interpolation, authoring-grid/lowering-grid separation, and the 44-cell half-sample delay fact."}
|
||||
|
|
|
|||
|
|
@ -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 a Faust-friendly `wg_loop ~ si.bus(...)` feedback component with full 44-section oral and 28-section nasal right/left traveling-wave state, live per-section diameter/area/reflection fields, obstruction-history burst pressure, and substep clock intent. `PinkTromboneParityFixtures` defines fixed Aqua DSL workouts, `PinkTromboneReferenceRenderer` renders deterministic PT-style fixture references, and `AudioComparison` includes log-mel cosine similarity. Current waveguide baseline is weak but measured: cosine about 0.56 open vowel, 0.54 front vowel, 0.43 nasal, 0.29 sibilant, and 0.11 closure-release. Remaining low-level PT gaps are exact twice-per-output-sample state updates and real threshold golf.
|
||||
- PT expressive parity should decompose into reusable Aqua primitives before audio golf: `tract_shape` owns continuous diameter/area geometry over normalized position, physical length (`length_cm`), interpolation/resampling, derived reflection coefficients, and per-cell acoustic delay; `tract` may resample that geometry to a chosen compiled `sections` grid without making authoring sample count anatomy; `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 a Faust-friendly `wg_loop ~ si.bus(...)` feedback component with compiled right/left traveling-wave state, live per-section diameter/area/reflection fields, obstruction-history burst pressure, and legacy substep clock intent. PT's 44 oral and 28 nasal cells are reference discretization, not Aqua morphology authority. `PinkTromboneParityFixtures` defines fixed Aqua DSL workouts, `PinkTromboneReferenceRenderer` renders deterministic PT-style fixture references, and `AudioComparison` includes log-mel cosine similarity. Current waveguide baseline is weak but measured: cosine about 0.56 open vowel, 0.54 front vowel, 0.43 nasal, 0.29 sibilant, and 0.11 closure-release. Remaining low-level gap is fractional-delay waveguide lowering whose clock derives from physical length, propagation speed, and sample rate so runtime morphology motion does not require recompilation.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ public sealed class PatchScriptTests
|
|||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
param path=/pink/tenseness default=.6 min=0 max=1 step=.001
|
||||
tract_shape name=human diameters=.6,.7,.9,1.1,1.3,1.5,1.4,1.2
|
||||
tract_shape name=human length_cm=17 diameters=.6,.7,.9,1.1,1.3,1.5,1.4,1.2
|
||||
glottis name=modal intensity=.72 tenseness=@/pink/tenseness aspiration=.14 reflection=.72 skew=.5
|
||||
tract_injection name=sibilant position=6 diameter=.45 turbulence=.8 burst=.3 width=.8
|
||||
nasal_branch name=nose junction=3 velum=.2 diameters=.01,.6,1.2,1.4
|
||||
|
|
@ -97,6 +97,7 @@ public sealed class PatchScriptTests
|
|||
var shape = Assert.Single(patch.TractShapes);
|
||||
Assert.Equal("human", shape.Name);
|
||||
Assert.Equal(8, shape.AreaFunction.Sections);
|
||||
Assert.Equal(17, shape.AreaFunction.LengthCentimeters);
|
||||
Assert.Equal(7, shape.AreaFunction.ReflectionCoefficients.Count);
|
||||
Assert.Contains(shape.AreaFunction.ReflectionCoefficients, coefficient => MathF.Abs(coefficient) > 0.01f);
|
||||
var glottis = Assert.Single(patch.GlottalSources);
|
||||
|
|
@ -151,6 +152,61 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains("tract_oral_waveguide", faust);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TractAreaFunctionOwnsContinuousMorphology()
|
||||
{
|
||||
var shape = new TractAreaFunction([1, 3, 1], LengthCentimeters: 18);
|
||||
|
||||
Assert.Equal(0.18f, shape.LengthMeters, 5);
|
||||
Assert.Equal(2, shape.DiameterAt(.25f), 5);
|
||||
Assert.Equal(3, shape.DiameterAt(.5f), 5);
|
||||
|
||||
var resampled = shape.Resample(5);
|
||||
Assert.Equal(5, resampled.Sections);
|
||||
Assert.Equal(18, resampled.LengthCentimeters);
|
||||
Assert.Equal([1, 2, 3, 2, 1], resampled.Diameters.Select(value => MathF.Round(value, 5)).ToArray());
|
||||
|
||||
var pinkTromboneCellDelay = new TractAreaFunction(Enumerable.Repeat(1f, 44).ToArray(), LengthCentimeters: 17)
|
||||
.CellDelaySamples(44100);
|
||||
Assert.InRange(pinkTromboneCellDelay, .49f, .51f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParserReadsTractShapeLengthAsPhysicalGeometry()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
tract_shape name=longform length_cm=24 diameters=.5,1,2,1,.5
|
||||
nasal_branch name=side length_cm=9 diameters=.01,.4,.8,.4
|
||||
tract shape=longform nasal_branch=side propagation=waveguide
|
||||
""");
|
||||
|
||||
var shape = Assert.Single(patch.TractShapes);
|
||||
Assert.Equal(24, shape.AreaFunction.LengthCentimeters);
|
||||
Assert.Equal(1.5f, shape.AreaFunction.DiameterAt(.375f), 5);
|
||||
|
||||
var branch = Assert.Single(patch.NasalBranches);
|
||||
Assert.Equal(9, branch.AreaFunction?.LengthCentimeters);
|
||||
Assert.Same(shape.AreaFunction, Assert.Single(patch.Voices).Tract?.AreaFunction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TractVoiceCanChooseLoweringGridWithoutChangingMorphology()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
tract_shape name=curve length_cm=17 diameters=.5,1,2,1,.5
|
||||
tract shape=curve sections=9 propagation=waveguide
|
||||
""");
|
||||
|
||||
var shape = Assert.Single(patch.TractShapes);
|
||||
var tract = Assert.Single(patch.Voices).Tract;
|
||||
|
||||
Assert.NotNull(tract?.AreaFunction);
|
||||
Assert.Equal(5, shape.AreaFunction.Sections);
|
||||
Assert.Equal(9, tract.AreaFunction.Sections);
|
||||
Assert.Equal(17, tract.AreaFunction.LengthCentimeters);
|
||||
Assert.Equal(shape.AreaFunction.DiameterAt(.375f), tract.AreaFunction.DiameterAt(.375f), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParserPreservesDeclaredPatchParameters()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue