Add tract motion and obstruction history
This commit is contained in:
parent
8386dc4ad1
commit
ae57fa96be
10 changed files with 130 additions and 19 deletions
|
|
@ -70,7 +70,13 @@ named failure against Pink Trombone, but it does not own the anatomy:
|
|||
consume `tract_shape` reflection coefficients and boundary reflections.
|
||||
True substep execution remains missing.
|
||||
|
||||
6. PT parity harness.
|
||||
6. `tract_motion`: control-rate slew and obstruction history.
|
||||
- Owns diameter/constriction/velum slew rates and obstruction threshold.
|
||||
- Feeds smoothed target controls and release-transient detection.
|
||||
- Implemented as `tract_motion`; waveguide lowering uses it for control
|
||||
slew and stateful obstruction-opening burst pressure.
|
||||
|
||||
7. PT parity harness.
|
||||
- Renders fixed PT references for vowels, nasals, fricatives, closures, and
|
||||
moving tongue/constriction gestures.
|
||||
- Renders Aqua DSL reconstructions.
|
||||
|
|
@ -79,7 +85,7 @@ named failure against Pink Trombone, but it does not own the anatomy:
|
|||
## First Cut
|
||||
|
||||
Implemented owners: `tract_shape`, `glottis`, `tract_injection`,
|
||||
`nasal_branch`, and first-pass `propagation=waveguide`.
|
||||
`nasal_branch`, `tract_motion`, and first-pass `propagation=waveguide`.
|
||||
`tract_shape` owns section diameter/area fields and reflection derivation.
|
||||
`glottis` owns excitation quality. `tract_injection` owns positioned
|
||||
frication/burst pressure. The current Faust proxy consumes these primitives
|
||||
|
|
@ -88,10 +94,10 @@ pressure instead of silently inventing all of them inside one helper. The
|
|||
waveguide lowering consumes the derived oral reflection field as right/left
|
||||
section state equations.
|
||||
|
||||
The next cut is cell-local events and timing: the oral/nasal tube now consumes
|
||||
the full reflection coefficient fields and distributes frication into section
|
||||
updates, but PT's two tract steps per output sample, target diameter slew, and
|
||||
obstruction-state transients still need low-level owners.
|
||||
The next cut is timing and proof: the oral/nasal tube now consumes the full
|
||||
reflection coefficient fields, distributes frication into section updates, and
|
||||
derives release bursts from obstruction history. PT's two tract steps per
|
||||
output sample and rendered parity fixtures still need low-level owners.
|
||||
|
||||
## Cut Line
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ Current AquaSynth can express:
|
|||
- reusable `tract_injection` declarations for positioned frication/burst
|
||||
pressure;
|
||||
- reusable `nasal_branch` declarations with velum-controlled junctions;
|
||||
- reusable `tract_motion` declarations for slew and obstruction thresholds;
|
||||
- a `tract` voice command owned by `Voice.Tract`;
|
||||
- Pink Trombone-shaped controls for intensity, tenseness, tongue body,
|
||||
constriction, velum/nasal opening, turbulence, lip opening, and end
|
||||
|
|
@ -74,9 +75,8 @@ Current AquaSynth can express:
|
|||
|
||||
Current AquaSynth cannot exactly express:
|
||||
|
||||
- per-section diameter targets as synthesis authority;
|
||||
- per-section dynamic diameter target arrays;
|
||||
- a subgraph that steps twice per output sample;
|
||||
- obstruction-state release transients derived from prior closure state;
|
||||
- exact Pink Trombone LF glottal coefficient updates.
|
||||
|
||||
## Current Parity Rung
|
||||
|
|
@ -101,6 +101,8 @@ Trombone anatomy:
|
|||
oral/nasal scattering junction when used by a waveguide tract.
|
||||
- `tract_injection` pressure is distributed into waveguide section updates by
|
||||
position and width when waveguide propagation is active.
|
||||
- `tract_motion` smooths tract controls and lets the waveguide derive burst
|
||||
pressure from obstruction history.
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -654,11 +654,20 @@ public static class BuiltInScripts
|
|||
loss=0.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=pt_motion
|
||||
diameter_slew=18
|
||||
shape_return=8
|
||||
constriction_slew=24
|
||||
velum_slew=16
|
||||
obstruction_threshold=0.05
|
||||
|
||||
tract
|
||||
shape=pt_human
|
||||
glottis=pt_glottis
|
||||
injection=pt_constriction
|
||||
nasal_branch=pt_nose
|
||||
motion=pt_motion
|
||||
propagation=waveguide
|
||||
waveguide_loss=0.999
|
||||
substeps=2
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public static class FaustEmitter
|
|||
}
|
||||
source.AppendLine("clip01(x) = min(1.0, max(0.0, x));");
|
||||
source.AppendLine("wrap01(x) = x - floor(x);");
|
||||
source.AppendLine("slew(rate,x) = x : *(1.0 - exp(-max(0.0001, rate) / ma.SR)) : + ~ *(exp(-max(0.0001, rate) / ma.SR));");
|
||||
source.AppendLine("softclip(x) = ma.tanh(x * 1.35);");
|
||||
source.AppendLine("fold(x) = 2.0 * abs(2.0 * (x / 4.0 - floor(x / 4.0)) - 1.0) - 1.0;");
|
||||
source.AppendLine("release_start(a,d,g) = max(g, a + d);");
|
||||
|
|
@ -395,11 +396,21 @@ public static class FaustEmitter
|
|||
var hasNose = tract.NoseSections > 0;
|
||||
var intensity = $"clip01({parameters.Expression(OwnerField(tractPath, "intensity"), tract.Intensity)})";
|
||||
var tenseness = $"clip01({parameters.Expression(OwnerField(tractPath, "tenseness"), tract.Tenseness)})";
|
||||
var tongueIndex = parameters.Expression(OwnerField(tractPath, "tongue_index"), tract.TongueIndex);
|
||||
var tongueDiameter = parameters.Expression(OwnerField(tractPath, "tongue_diameter"), tract.TongueDiameter);
|
||||
var velum = $"clip01(({parameters.Expression(OwnerField(tractPath, "velum"), tract.Velum)} - 0.01) / 0.39)";
|
||||
var constrictionIndex = parameters.Expression(OwnerField(tractPath, "constriction_index"), tract.ConstrictionIndex);
|
||||
var constrictionDiameter = parameters.Expression(OwnerField(tractPath, "constriction_diameter"), tract.ConstrictionDiameter);
|
||||
var motion = tract.Motion;
|
||||
var diameterSlew = parameters.Expression(OwnerField(tractPath, "motion/diameter_slew"), motion?.DiameterSlewPerSecond ?? 18);
|
||||
var constrictionSlew = parameters.Expression(OwnerField(tractPath, "motion/constriction_slew"), motion?.ConstrictionSlewPerSecond ?? 24);
|
||||
var velumSlew = parameters.Expression(OwnerField(tractPath, "motion/velum_slew"), motion?.VelumSlewPerSecond ?? 16);
|
||||
var obstructionThreshold = parameters.Expression(OwnerField(tractPath, "motion/obstruction_threshold"), motion?.ObstructionThreshold ?? 0.05f);
|
||||
var tongueIndexRaw = parameters.Expression(OwnerField(tractPath, "tongue_index"), tract.TongueIndex);
|
||||
var tongueDiameterRaw = parameters.Expression(OwnerField(tractPath, "tongue_diameter"), tract.TongueDiameter);
|
||||
var velumRaw = $"clip01(({parameters.Expression(OwnerField(tractPath, "velum"), tract.Velum)} - 0.01) / 0.39)";
|
||||
var constrictionIndexRaw = parameters.Expression(OwnerField(tractPath, "constriction_index"), tract.ConstrictionIndex);
|
||||
var constrictionDiameterRaw = parameters.Expression(OwnerField(tractPath, "constriction_diameter"), tract.ConstrictionDiameter);
|
||||
var tongueIndex = motion is null ? tongueIndexRaw : $"slew({diameterSlew}, {tongueIndexRaw})";
|
||||
var tongueDiameter = motion is null ? tongueDiameterRaw : $"slew({diameterSlew}, {tongueDiameterRaw})";
|
||||
var velum = motion is null ? velumRaw : $"slew({velumSlew}, {velumRaw})";
|
||||
var constrictionIndex = motion is null ? constrictionIndexRaw : $"slew({constrictionSlew}, {constrictionIndexRaw})";
|
||||
var constrictionDiameter = motion is null ? constrictionDiameterRaw : $"slew({constrictionSlew}, {constrictionDiameterRaw})";
|
||||
var turbulence = $"clip01({parameters.Expression(OwnerField(tractPath, "turbulence"), tract.Turbulence)})";
|
||||
var lipOpening = parameters.Expression(OwnerField(tractPath, "lip_opening"), tract.LipOpening);
|
||||
var glottalReflection = parameters.Expression(OwnerField(tractPath, "glottal_reflection"), tract.GlottalReflection);
|
||||
|
|
@ -414,8 +425,10 @@ public static class FaustEmitter
|
|||
var reflectionEnergy = F(areaFunction is null ? 0.12f : MathF.Min(1, areaFunction.ReflectionCoefficients.Sum(reflection => MathF.Abs(reflection)) / Math.Max(1, areaFunction.ReflectionCoefficients.Count)));
|
||||
var aspiration = $"clip01({parameters.Expression(OwnerField(tractPath, "glottis/aspiration"), glottis?.Aspiration ?? 0.08f)})";
|
||||
var glottalSkew = $"clip01({parameters.Expression(OwnerField(tractPath, "glottis/skew"), glottis?.Skew ?? 0.42f)})";
|
||||
var injectionPosition = parameters.Expression(OwnerField(tractPath, "injection/position"), injection?.Position ?? tract.ConstrictionIndex);
|
||||
var injectionDiameter = parameters.Expression(OwnerField(tractPath, "injection/diameter"), injection?.Diameter ?? tract.ConstrictionDiameter);
|
||||
var injectionPositionRaw = parameters.Expression(OwnerField(tractPath, "injection/position"), injection?.Position ?? tract.ConstrictionIndex);
|
||||
var injectionDiameterRaw = parameters.Expression(OwnerField(tractPath, "injection/diameter"), injection?.Diameter ?? tract.ConstrictionDiameter);
|
||||
var injectionPosition = motion is null ? injectionPositionRaw : $"slew({constrictionSlew}, {injectionPositionRaw})";
|
||||
var injectionDiameter = motion is null ? injectionDiameterRaw : $"slew({constrictionSlew}, {injectionDiameterRaw})";
|
||||
var injectionTurbulence = $"clip01({parameters.Expression(OwnerField(tractPath, "injection/turbulence"), injection?.Turbulence ?? tract.Turbulence)})";
|
||||
var injectionBurst = $"clip01({parameters.Expression(OwnerField(tractPath, "injection/burst"), injection?.Burst ?? 0)})";
|
||||
var injectionWidth = $"max(0.05, {parameters.Expression(OwnerField(tractPath, "injection/width"), injection?.Width ?? 1)})";
|
||||
|
|
@ -441,7 +454,12 @@ public static class FaustEmitter
|
|||
source.AppendLine($"{name}_tract_lf = ({name}_tract_lf_open - (0.12 + {tenseness} * 0.62) * sin(4.0 * ma.PI * {name}_tract_phase)) * {intensity} * (0.45 + 0.75 * pow(max(0.0, {tenseness}), 0.35));");
|
||||
source.AppendLine($"{name}_tract_aspiration = no.noise * {intensity} * {aspiration} * (1.0 - sqrt(max(0.0, {tenseness}))) * (0.08 + 0.22 * {name}_tract_constriction_close);");
|
||||
source.AppendLine($"{name}_tract_frication = no.noise * max({turbulence} * {name}_tract_constriction_close, {injectionTurbulence} * {name}_tract_injection_close) * {intensity} * (0.25 + 0.75 * {name}_tract_injection_pos) * (0.2 + 0.8 * {tenseness});");
|
||||
source.AppendLine($"{name}_tract_burst = no.noise * max({name}_tract_constriction_close * 0.18, {name}_tract_injection_close * {injectionBurst}) * exp(-age * (45.0 + max({name}_tract_constriction_close, {name}_tract_injection_close) * 120.0));");
|
||||
source.AppendLine($"{name}_tract_prev_injection_close = {name}_tract_injection_close : mem;");
|
||||
source.AppendLine($"{name}_tract_obstructed = select2({injectionDiameter} < {obstructionThreshold}, 0.0, 1.0);");
|
||||
source.AppendLine($"{name}_tract_prev_obstructed = {name}_tract_obstructed : mem;");
|
||||
source.AppendLine($"{name}_tract_release_opening = max(0.0, {name}_tract_prev_injection_close - {name}_tract_injection_close) * {name}_tract_prev_obstructed;");
|
||||
source.AppendLine($"{name}_tract_release_memory = {name}_tract_release_opening : + ~ *(0.935);");
|
||||
source.AppendLine($"{name}_tract_burst = no.noise * {name}_tract_release_memory * {injectionBurst} * {intensity};");
|
||||
source.AppendLine($"{name}_tract_excitation = ({name}_tract_lf + {name}_tract_aspiration + {name}_tract_burst) * (0.55 + 0.45 * abs({glottalReflection}));");
|
||||
source.AppendLine($"{name}_tract_injection_pressure = {name}_tract_frication * (0.55 + 0.45 * abs({glottalReflection}));");
|
||||
source.AppendLine($"{name}_tract_raw = {name}_tract_excitation + {name}_tract_injection_pressure;");
|
||||
|
|
|
|||
|
|
@ -213,6 +213,14 @@ public sealed record NasalBranch(
|
|||
float Reflection = -0.85f,
|
||||
float Loss = 0.999f);
|
||||
|
||||
public sealed record TractMotion(
|
||||
string Name = "",
|
||||
float DiameterSlewPerSecond = 18,
|
||||
float ShapeReturnPerSecond = 8,
|
||||
float ConstrictionSlewPerSecond = 24,
|
||||
float VelumSlewPerSecond = 16,
|
||||
float ObstructionThreshold = 0.05f);
|
||||
|
||||
public sealed record Modulator(
|
||||
ModTarget Target,
|
||||
ModWaveform Waveform = ModWaveform.Sine,
|
||||
|
|
@ -271,6 +279,7 @@ public sealed record VocalTract(
|
|||
GlottalSource? Glottis = null,
|
||||
TractInjection? Injection = null,
|
||||
NasalBranch? Nasal = null,
|
||||
TractMotion? Motion = null,
|
||||
TractPropagationMode Propagation = TractPropagationMode.Resonator,
|
||||
float WaveguideLoss = 0.999f,
|
||||
int Substeps = 1);
|
||||
|
|
@ -450,6 +459,7 @@ public sealed record SynthPatch
|
|||
public IReadOnlyList<GlottalSource> GlottalSources { get; init; } = Array.Empty<GlottalSource>();
|
||||
public IReadOnlyList<TractInjection> TractInjections { get; init; } = Array.Empty<TractInjection>();
|
||||
public IReadOnlyList<NasalBranch> NasalBranches { get; init; } = Array.Empty<NasalBranch>();
|
||||
public IReadOnlyList<TractMotion> TractMotions { get; init; } = Array.Empty<TractMotion>();
|
||||
public IReadOnlyList<OperatorGraph> OperatorGraphs { get; init; } = Array.Empty<OperatorGraph>();
|
||||
public IReadOnlyList<ControlLane> Controls { get; init; } = Array.Empty<ControlLane>();
|
||||
public IReadOnlyList<PatchParameter> Parameters { get; init; } = Array.Empty<PatchParameter>();
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ public static class PatchScript
|
|||
private readonly Dictionary<string, TractInjection> _tractInjectionsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<NasalBranch> _nasalBranches = [];
|
||||
private readonly Dictionary<string, NasalBranch> _nasalBranchesByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<TractMotion> _tractMotions = [];
|
||||
private readonly Dictionary<string, TractMotion> _tractMotionsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _defaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _layerDefaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
private PendingOperatorGraph? _pendingOperatorGraph;
|
||||
|
|
@ -69,6 +71,7 @@ public static class PatchScript
|
|||
GlottalSources = _glottalSources,
|
||||
TractInjections = _tractInjections,
|
||||
NasalBranches = _nasalBranches,
|
||||
TractMotions = _tractMotions,
|
||||
OperatorGraphs = _operatorGraphs,
|
||||
Controls = _controls,
|
||||
Parameters = _parameters,
|
||||
|
|
@ -136,6 +139,10 @@ public static class PatchScript
|
|||
FlushPendingOperatorGraph();
|
||||
AddNasalBranch(fields, line);
|
||||
break;
|
||||
case "tract_motion":
|
||||
FlushPendingOperatorGraph();
|
||||
AddTractMotion(fields, line);
|
||||
break;
|
||||
case "voice":
|
||||
FlushPendingOperatorGraph();
|
||||
Voices.Add(ParseVoice(ExpandVoiceFields(fields, line), VoicePath(Voices.Count), line));
|
||||
|
|
@ -364,6 +371,25 @@ public static class PatchScript
|
|||
_nasalBranchesByName[name] = branch;
|
||||
}
|
||||
|
||||
private void AddTractMotion(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var name = Required(fields, "name", line);
|
||||
if (_tractMotionsByName.ContainsKey(name))
|
||||
{
|
||||
throw new PatchScriptException(line, $"duplicate tract motion `{name}`");
|
||||
}
|
||||
|
||||
var motion = new TractMotion(
|
||||
name,
|
||||
GetBoundFloat(fields, line, 18, $"/tract_motions/{_tractMotions.Count}/diameter_slew", "diameter_slew", "slew"),
|
||||
GetBoundFloat(fields, line, 8, $"/tract_motions/{_tractMotions.Count}/shape_return", "shape_return", "return_slew"),
|
||||
GetBoundFloat(fields, line, 24, $"/tract_motions/{_tractMotions.Count}/constriction_slew", "constriction_slew", "constriction"),
|
||||
GetBoundFloat(fields, line, 16, $"/tract_motions/{_tractMotions.Count}/velum_slew", "velum_slew", "velum"),
|
||||
GetBoundFloat(fields, line, 0.05f, $"/tract_motions/{_tractMotions.Count}/obstruction_threshold", "obstruction_threshold", "closure"));
|
||||
_tractMotions.Add(motion);
|
||||
_tractMotionsByName[name] = motion;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ExpandVoiceFields(Dictionary<string, string> fields, int line)
|
||||
{
|
||||
var expanded = new Dictionary<string, string>(_defaults, StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -608,6 +634,7 @@ public static class PatchScript
|
|||
var glottis = ParseGlottalSourceReference(fields, ownerPath, line);
|
||||
var injection = ParseTractInjectionReference(fields, ownerPath, line);
|
||||
var nasal = ParseNasalBranchReference(fields, ownerPath, line);
|
||||
var motion = ParseTractMotionReference(fields, ownerPath, line);
|
||||
var sections = GetBoundInt(fields, line, areaFunction?.Sections ?? 44, OwnerField(ownerPath, "tract/sections"), "sections", "cells");
|
||||
if (sections < 4)
|
||||
{
|
||||
|
|
@ -643,6 +670,7 @@ public static class PatchScript
|
|||
glottis,
|
||||
injection,
|
||||
nasal,
|
||||
motion,
|
||||
ParseTractPropagationMode(GetAny(fields, ["propagation", "model"], "resonator"), line),
|
||||
GetBoundFloat(fields, line, 0.999f, OwnerField(ownerPath, "tract/waveguide_loss"), "waveguide_loss", "loss"),
|
||||
GetBoundInt(fields, line, 1, OwnerField(ownerPath, "tract/substeps"), "substeps", "steps"))
|
||||
|
|
@ -1136,6 +1164,30 @@ public static class PatchScript
|
|||
GetBoundFloat(fields, line, fallback.Loss, OwnerField(ownerPath, "tract/nasal/loss"), "nose_loss"));
|
||||
}
|
||||
|
||||
private TractMotion? ParseTractMotionReference(IReadOnlyDictionary<string, string> fields, string ownerPath, int line)
|
||||
{
|
||||
var hasInlineMotion = HasAny(fields, "diameter_slew", "shape_return", "constriction_slew", "velum_slew", "obstruction_threshold");
|
||||
var hasNamedMotion = TryGetAny(fields, ["motion", "tract_motion"], out var motionName);
|
||||
if (!hasNamedMotion && !hasInlineMotion)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TractMotion? namedMotion = null;
|
||||
if (hasNamedMotion && !_tractMotionsByName.TryGetValue(motionName, out namedMotion))
|
||||
{
|
||||
throw new PatchScriptException(line, $"unknown tract motion `{motionName}`");
|
||||
}
|
||||
|
||||
var fallback = hasNamedMotion ? namedMotion! : new TractMotion();
|
||||
return new TractMotion(
|
||||
hasNamedMotion ? motionName : "",
|
||||
GetBoundFloat(fields, line, fallback.DiameterSlewPerSecond, OwnerField(ownerPath, "tract/motion/diameter_slew"), "diameter_slew", "slew"),
|
||||
GetBoundFloat(fields, line, fallback.ShapeReturnPerSecond, OwnerField(ownerPath, "tract/motion/shape_return"), "shape_return", "return_slew"),
|
||||
GetBoundFloat(fields, line, fallback.ConstrictionSlewPerSecond, OwnerField(ownerPath, "tract/motion/constriction_slew"), "constriction_slew"),
|
||||
GetBoundFloat(fields, line, fallback.VelumSlewPerSecond, OwnerField(ownerPath, "tract/motion/velum_slew"), "velum_slew"),
|
||||
GetBoundFloat(fields, line, fallback.ObstructionThreshold, OwnerField(ownerPath, "tract/motion/obstruction_threshold"), "obstruction_threshold", "closure"));
|
||||
}
|
||||
|
||||
private static TractAreaFunction ParseTractAreaFunction(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var hasDiameters = TryGetAny(fields, ["diameters", "diameter"], out var diametersText);
|
||||
|
|
@ -1576,6 +1628,7 @@ public static class PatchScript
|
|||
"glottis" or "glottal" or "excitation" => "glottis",
|
||||
"tract_injection" or "injection" or "frication" or "burst" => "tract_injection",
|
||||
"nasal_branch" or "nose_branch" or "nasal" => "nasal_branch",
|
||||
"tract_motion" or "shape_motion" or "slew" => "tract_motion",
|
||||
"v" or "voice" => "voice",
|
||||
"tract" or "vt" or "tractvoice" or "tract_voice" => "tract",
|
||||
"opgraph" or "ops" or "operators" => "opgraph",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ public static class PinkTromboneReference
|
|||
new("nose_waveguide_cells", "nasal_branch", "AquaSynth can author a nasal branch as its own diameter/area tube."),
|
||||
new("nose_junction", "generated three-way junction", "Waveguide lowering can emit a velum-controlled oral/nasal junction from local areas."),
|
||||
new("positioned_turbulence_applied_to_waveguide_cells", "position-weighted waveguide injection", "Frication pressure can be distributed into oral waveguide section updates by injection position and width."),
|
||||
new("tract_shape_motion", "tract_motion slew semantics", "AquaSynth can author tract control slew and obstruction thresholds for moving diameter targets."),
|
||||
new("closure_transients", "obstruction-history burst", "Waveguide lowering derives burst pressure from prior obstruction state and current opening."),
|
||||
new("runtime_controls", "/pink/frequency,/pink/intensity,/pink/tenseness,/pink/tongue/index,/pink/tongue/diameter,/pink/velum,/pink/turbulence", "AquaSynth can expose stable runtime parameter paths."),
|
||||
new("glottal_pitch_pressure_proxy", "tract LF-style source", "AquaSynth can drive pitch/loudness-like controls without recompiling."),
|
||||
new("turbulence_proxy", "position-aware frication controls", "The tract voice has constriction index/diameter and turbulence controls."),
|
||||
|
|
@ -77,7 +79,6 @@ public static class PinkTromboneReference
|
|||
],
|
||||
[
|
||||
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("stateful_closure_release_transients", "obstruction release impulse", "AquaSynth can author burst pressure, but it does not yet observe prior obstruction state and emit PT-style 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.");
|
||||
|
|
|
|||
|
|
@ -134,3 +134,4 @@
|
|||
{"date":"2026-05-25","topic":"pink-trombone-expressive-parity","lesson":"The Pink Trombone path is expressive-parity first, audio-parity second. Do not half-consume PT into one helper. Decompose its pipeline into reusable Aqua DSL primitives, then golf rendered PT fixtures with log-mel cosine similarity once the language can express the pipeline honestly.","evidence":"Added `docs/pink-trombone-dsl-parity-plan.md`, oriented parity docs/spine/map, and implemented the first primitive: `tract_shape` declarations parse into reusable `TractAreaFunction` objects that derive areas/reflection coefficients and feed `Voice.Tract` lowering. Focused tests assert shape ownership, coefficient derivation, and reference matched/missing features."}
|
||||
{"date":"2026-05-26","topic":"pink-trombone-low-level-dsp-primitives","lesson":"PT parity must be targeted at the low-level DSP loop: right/left traveling-wave state, reflection coefficients, scattering junctions, nasal branch junctions, and injected excitation. AquaSynth now has reusable primitives for tract shape, glottal source, tract injection, nasal branch, and generated oral/nasal waveguide lowering; remaining gaps are true twice-rate stepping, cell-local turbulence/transient injection, target diameter slew, and rendered PT fixture scoring.","evidence":"Added `GlottalSource`, `TractInjection`, `NasalBranch`, `TractPropagationMode.Waveguide`, parser commands `glottis`, `tract_injection`, `nasal_branch`, and Faust generation for oral right/left waveguide sections plus a velum-controlled three-way nose junction. Focused verification: `dotnet build tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -v minimal` and focused `dotnet test --no-build` passed."}
|
||||
{"date":"2026-05-26","topic":"waveguide-cell-injection","lesson":"A positioned injection primitive is not enough; frication has to enter the traveling-wave section updates. AquaSynth waveguide lowering now computes a section-space injection cell and distributes injection pressure across oral waveguide right/left updates by position and width. Remaining PT DSP gaps are true substep execution, obstruction-history transients, diameter target slew, and rendered parity fixtures.","evidence":"Changed Faust waveguide lowering to split glottal excitation from tract injection pressure, emit `wg_injection_cell` and `wg_inject_N` terms, and add those terms to oral waveguide section and nasal-junction updates. Verification required disabling build servers/shared compilation/parallel build after normal dotnet builds hung: `dotnet build tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false -v minimal`; focused `dotnet test --no-build` passed 3 tests."}
|
||||
{"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."}
|
||||
|
|
|
|||
|
|
@ -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; `propagation=waveguide` lowers tract voices into generated oral/nasal right-left waveguide equations. Remaining low-level PT gaps are true two-steps-per-sample execution, obstruction-history transients, target diameter slew, and 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 obstruction-history burst pressure. Remaining low-level PT gaps are true two-steps-per-sample execution, dynamic per-section target diameter arrays, and parity fixtures.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ public sealed class PatchScriptTests
|
|||
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
|
||||
tract shape=human glottis=modal injection=sibilant nasal_branch=nose propagation=waveguide waveguide_loss=.998 substeps=2 freq=140 tongue_index=4 tongue_diameter=1.4 velum=.2
|
||||
tract_motion name=quick diameter_slew=20 constriction_slew=30 velum_slew=12 obstruction_threshold=.08
|
||||
tract shape=human glottis=modal injection=sibilant nasal_branch=nose motion=quick propagation=waveguide waveguide_loss=.998 substeps=2 freq=140 tongue_index=4 tongue_diameter=1.4 velum=.2
|
||||
""");
|
||||
|
||||
var shape = Assert.Single(patch.TractShapes);
|
||||
|
|
@ -107,6 +108,9 @@ public sealed class PatchScriptTests
|
|||
var nasal = Assert.Single(patch.NasalBranches);
|
||||
Assert.Equal("nose", nasal.Name);
|
||||
Assert.Equal(4, nasal.AreaFunction?.Sections);
|
||||
var motion = Assert.Single(patch.TractMotions);
|
||||
Assert.Equal("quick", motion.Name);
|
||||
Assert.Equal(.08f, motion.ObstructionThreshold, 5);
|
||||
|
||||
var voice = Assert.Single(patch.Voices);
|
||||
Assert.NotNull(voice.Tract);
|
||||
|
|
@ -118,6 +122,7 @@ public sealed class PatchScriptTests
|
|||
Assert.Equal(glottis, voice.Tract.Glottis);
|
||||
Assert.Equal(injection, voice.Tract.Injection);
|
||||
Assert.Equal(nasal, voice.Tract.Nasal);
|
||||
Assert.Equal(motion, voice.Tract.Motion);
|
||||
Assert.Equal(TractPropagationMode.Waveguide, voice.Tract.Propagation);
|
||||
Assert.Equal(.998f, voice.Tract.WaveguideLoss, 5);
|
||||
Assert.Equal(2, voice.Tract.Substeps);
|
||||
|
|
@ -132,6 +137,9 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains("tract_injection_close", faust);
|
||||
Assert.Contains("wg_injection_cell", faust);
|
||||
Assert.Contains("wg_inject_6", faust);
|
||||
Assert.Contains("tract_release_memory", faust);
|
||||
Assert.Contains("tract_obstructed", faust);
|
||||
Assert.Contains("slew(", faust);
|
||||
Assert.Contains("wg_scatter_1", faust);
|
||||
Assert.Contains("nose_reflect_nose", faust);
|
||||
Assert.Contains("tract_nose_waveguide", faust);
|
||||
|
|
@ -370,10 +378,13 @@ public sealed class PatchScriptTests
|
|||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "nose_waveguide_cells");
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "nose_junction");
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "positioned_turbulence_applied_to_waveguide_cells");
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "tract_shape_motion");
|
||||
Assert.Contains(rebuild.MatchedFeatures, feature => feature.Name == "closure_transients");
|
||||
Assert.NotEmpty(patch.TractShapes);
|
||||
Assert.NotEmpty(patch.GlottalSources);
|
||||
Assert.NotEmpty(patch.TractInjections);
|
||||
Assert.NotEmpty(patch.NasalBranches);
|
||||
Assert.NotEmpty(patch.TractMotions);
|
||||
Assert.NotEmpty(patch.Parameters);
|
||||
Assert.Contains("process =", export.Source);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue