Add IPA phoneme gesture spline sugar
This commit is contained in:
parent
1bf220027d
commit
82bb867e21
8 changed files with 294 additions and 6 deletions
|
|
@ -291,8 +291,27 @@ The separate gesture/timeline owner is now the live direction:
|
|||
A generated contact-opening spline was tested and cut because PT evidence
|
||||
showed it invented an open-then-close transient; contact closure must come
|
||||
from articulator/area control, not a compensating contact-port story.
|
||||
- `phoneme_gesture` is the first IPA/anatomy sugar layer. It accepts known IPA
|
||||
symbols or explicit descriptors such as
|
||||
`descriptor=voiceless_labial-velar_fricative`, identifies the primary organs
|
||||
from place/manner/voicing labels, and emits ordinary `ControlSpline`s onto
|
||||
source, area, branch, contact, and radiation surfaces. It is a deterministic
|
||||
seed, not truth: local reference audio/articulation datasets golf the DSL
|
||||
defaults and the patch's voice code.
|
||||
|
||||
Latest focused evidence: `PatchScriptTests` pass 90/90, and
|
||||
Example:
|
||||
|
||||
```aqua
|
||||
phoneme_gesture name=ma ipa=m start=0 dur=.16 intensity=.8
|
||||
phoneme_gesture name=hw ipa=ʍ descriptor=voiceless_labial-velar_fricative start=.2 dur=.12
|
||||
phoneme_gesture name=vowel ipa=a start=.34 dur=.20
|
||||
```
|
||||
|
||||
Patches may still use normal FM, AM, modulators, envelopes, filters, and
|
||||
post-voice processing around the anatomical control stream. Those tools shape
|
||||
expression; they do not replace the phoneme-to-surface ownership map.
|
||||
|
||||
Latest focused evidence: `PatchScriptTests` pass 92/92, and
|
||||
`PinkTrombonePrimitiveTimelineComparisonWritesArtifacts` writes artifact
|
||||
`artifacts/parity/primitive-vocal-timeline/20260529T185955965`, where
|
||||
`closure-release` returns to `0.155925` mean absolute primitive timeline error
|
||||
|
|
|
|||
|
|
@ -539,6 +539,14 @@ public sealed record ControlSpline(
|
|||
public IReadOnlyList<ControlSplinePoint> Points { get; init; } = Points ?? Array.Empty<ControlSplinePoint>();
|
||||
}
|
||||
|
||||
public sealed record PhonemeGesture(
|
||||
string Name,
|
||||
string Ipa,
|
||||
string Descriptor,
|
||||
float StartSeconds,
|
||||
float DurationSeconds,
|
||||
float Intensity = 1);
|
||||
|
||||
public sealed record VocalNetwork(
|
||||
string Name,
|
||||
IReadOnlyList<string>? Paths = null,
|
||||
|
|
@ -907,6 +915,7 @@ public sealed record SynthPatch
|
|||
public IReadOnlyList<ControlCurve> ControlCurves { get; init; } = Array.Empty<ControlCurve>();
|
||||
public IReadOnlyList<ControlSurface> ControlSurfaces { get; init; } = Array.Empty<ControlSurface>();
|
||||
public IReadOnlyList<ControlSpline> ControlSplines { get; init; } = Array.Empty<ControlSpline>();
|
||||
public IReadOnlyList<PhonemeGesture> PhonemeGestures { get; init; } = Array.Empty<PhonemeGesture>();
|
||||
public IReadOnlyList<GestureGroup> GestureGroups { get; init; } = Array.Empty<GestureGroup>();
|
||||
public IReadOnlyList<ParameterBinding> ParameterBindings { get; init; } = Array.Empty<ParameterBinding>();
|
||||
public Playback Playback { get; init; } = new();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public static class PatchScript
|
|||
private readonly Dictionary<string, ControlSurface> _controlSurfacesByPath = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<ControlSpline> _controlSplines = [];
|
||||
private readonly Dictionary<string, ControlSpline> _controlSplinesByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<PhonemeGesture> _phonemeGestures = [];
|
||||
private readonly List<GestureGroup> _gestureGroups = [];
|
||||
private readonly List<ParameterBinding> _parameterBindings = [];
|
||||
private readonly List<PatchLayer> _layers = [];
|
||||
|
|
@ -136,6 +137,7 @@ public static class PatchScript
|
|||
ControlCurves = _controlCurves,
|
||||
ControlSurfaces = _controlSurfaces,
|
||||
ControlSplines = _controlSplines,
|
||||
PhonemeGestures = _phonemeGestures,
|
||||
GestureGroups = _gestureGroups,
|
||||
ParameterBindings = _parameterBindings,
|
||||
Playback = Playback,
|
||||
|
|
@ -321,6 +323,10 @@ public static class PatchScript
|
|||
FlushPendingOperatorGraph();
|
||||
AddControlSpline(fields, line);
|
||||
break;
|
||||
case "phoneme_gesture":
|
||||
FlushPendingOperatorGraph();
|
||||
AddPhonemeGesture(fields, line);
|
||||
break;
|
||||
case "gesture":
|
||||
FlushPendingOperatorGraph();
|
||||
if (HasAny(fields, "surface", "surface_path", "target"))
|
||||
|
|
@ -1320,6 +1326,190 @@ public static class PatchScript
|
|||
_controlSplinesByName[name] = spline;
|
||||
}
|
||||
|
||||
private void AddAuthoredSpline(
|
||||
string name,
|
||||
string surfacePath,
|
||||
float start,
|
||||
float duration,
|
||||
params (float time, float value)[] points)
|
||||
{
|
||||
if (!_controlSurfacesByPath.TryGetValue(surfacePath, out var surface))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var splineName = UniqueSplineName(name);
|
||||
var clamped = points.Length == 0
|
||||
? [(0f, surface.DefaultNormalized), (1f, surface.DefaultNormalized)]
|
||||
: points;
|
||||
var splinePoints = clamped
|
||||
.Select(point =>
|
||||
new ControlSplinePoint(
|
||||
start + Math.Clamp(point.time, 0, 1) * duration,
|
||||
Math.Clamp(point.value, 0, 1),
|
||||
duration * 0.25f,
|
||||
Math.Clamp(point.value, 0, 1),
|
||||
duration * 0.25f,
|
||||
Math.Clamp(point.value, 0, 1)))
|
||||
.ToArray();
|
||||
var spline = new ControlSpline(splineName, surfacePath, splinePoints, ControlSplineInterpolation.Bezier);
|
||||
_controlSplines.Add(spline);
|
||||
_controlSplinesByName[spline.Name] = spline;
|
||||
}
|
||||
|
||||
private string UniqueSplineName(string name)
|
||||
{
|
||||
var safe = SafeName(name);
|
||||
if (!_controlSplinesByName.ContainsKey(safe))
|
||||
{
|
||||
return safe;
|
||||
}
|
||||
|
||||
for (var i = 1; ; i++)
|
||||
{
|
||||
var candidate = $"{safe}_{i}";
|
||||
if (!_controlSplinesByName.ContainsKey(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitPhonemeGestureSplines(string name, string descriptor, float start, float duration, float intensity)
|
||||
{
|
||||
var tokens = DescriptorTokens(descriptor);
|
||||
var voiced = tokens.Contains("voiced") || tokens.Contains("vowel") || tokens.Contains("nasal") || tokens.Contains("approximant");
|
||||
if (tokens.Contains("voiceless"))
|
||||
{
|
||||
voiced = false;
|
||||
}
|
||||
|
||||
var place = PlaceTarget(tokens);
|
||||
var constriction = ConstrictionTarget(tokens);
|
||||
var pressure = Math.Clamp(0.45f + 0.30f * intensity, 0, 1);
|
||||
AddAuthoredSpline($"{name}_source_pressure", "/vocal/sources/0/pressure", start, duration, (0, 0.4f), (0.2f, pressure), (1, pressure * 0.85f));
|
||||
AddAuthoredSpline($"{name}_source_tension", "/vocal/sources/0/tension", start, duration, (0, voiced ? 0.55f : 0.18f), (1, voiced ? 0.62f : 0.20f));
|
||||
AddAuthoredSpline($"{name}_source_opening", "/vocal/sources/0/opening", start, duration, (0, voiced ? 0.45f : 0.25f), (1, voiced ? 0.48f : 0.28f));
|
||||
AddAuthoredSpline($"{name}_source_noise", "/vocal/sources/0/noise", start, duration, (0, tokens.Contains("fricative") ? 0.45f : 0.05f), (1, tokens.Contains("fricative") ? 0.55f : 0.05f));
|
||||
|
||||
if (tokens.Contains("vowel"))
|
||||
{
|
||||
EmitVowelGesture(name, tokens, start, duration, intensity);
|
||||
return;
|
||||
}
|
||||
|
||||
AddAuthoredSpline($"{name}_constriction_index", "/vocal/areas/0/area/constriction_index", start, duration, (0, place), (1, place));
|
||||
AddAuthoredSpline($"{name}_constriction_diameter", "/vocal/areas/0/area/constriction_diameter", start, duration, (0, constriction), (0.5f, constriction), (1, Math.Min(0.65f, constriction + 0.2f)));
|
||||
|
||||
if (tokens.Contains("bilabial") || tokens.Contains("labiodental") || tokens.Contains("labial") || tokens.Contains("rounded"))
|
||||
{
|
||||
var lipTarget = tokens.Contains("approximant") ? 0.38f : tokens.Contains("fricative") ? 0.18f : 0.04f;
|
||||
AddAuthoredSpline($"{name}_lip_area", "/vocal/areas/0/area/lip_opening", start, duration, (0, lipTarget), (0.7f, lipTarget), (1, 0.45f));
|
||||
AddAuthoredSpline($"{name}_lip_radiation", "/vocal/radiation/0/aperture", start, duration, (0, lipTarget), (0.7f, lipTarget), (1, 0.75f));
|
||||
}
|
||||
|
||||
if (tokens.Contains("nasal"))
|
||||
{
|
||||
AddAuthoredSpline($"{name}_velum", "/vocal/branches/0/opening", start, duration, (0, 0.7f), (0.8f, 0.7f), (1, 0.25f));
|
||||
AddAuthoredSpline($"{name}_contact", "/vocal/contacts/0/opening", start, duration, (0, 0.03f), (0.8f, 0.03f), (1, 0.25f));
|
||||
}
|
||||
else if (tokens.Contains("plosive") || tokens.Contains("stop"))
|
||||
{
|
||||
AddAuthoredSpline($"{name}_contact", "/vocal/contacts/0/opening", start, duration, (0, 0.02f), (0.65f, 0.02f), (1, 0.9f));
|
||||
AddAuthoredSpline($"{name}_stored_pressure", "/vocal/contacts/0/stored_pressure", start, duration, (0, 0.4f), (0.65f, 0.75f), (1, 0.05f));
|
||||
AddAuthoredSpline($"{name}_velum", "/vocal/branches/0/opening", start, duration, (0, 0.01f), (1, 0.01f));
|
||||
}
|
||||
else if (tokens.Contains("fricative"))
|
||||
{
|
||||
AddAuthoredSpline($"{name}_contact", "/vocal/contacts/0/opening", start, duration, (0, 0.18f), (1, 0.22f));
|
||||
AddAuthoredSpline($"{name}_contact_resistance", "/vocal/contacts/0/resistance", start, duration, (0, 0.75f), (1, 0.85f));
|
||||
AddAuthoredSpline($"{name}_velum", "/vocal/branches/0/opening", start, duration, (0, 0.01f), (1, 0.01f));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddAuthoredSpline($"{name}_contact", "/vocal/contacts/0/opening", start, duration, (0, 0.55f), (1, 0.65f));
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitVowelGesture(string name, HashSet<string> tokens, float start, float duration, float intensity)
|
||||
{
|
||||
var frontness = tokens.Contains("front") ? 0.70f : tokens.Contains("back") ? 0.30f : 0.50f;
|
||||
var openness = tokens.Contains("close") ? 0.30f : tokens.Contains("mid") ? 0.50f : tokens.Contains("open") ? 0.78f : 0.55f;
|
||||
var rounding = tokens.Contains("rounded") ? 0.35f : tokens.Contains("unrounded") ? 0.72f : 0.60f;
|
||||
AddAuthoredSpline($"{name}_tongue_index", "/vocal/areas/0/area/tongue_index", start, duration, (0, frontness), (1, frontness));
|
||||
AddAuthoredSpline($"{name}_tongue_diameter", "/vocal/areas/0/area/tongue_diameter", start, duration, (0, openness), (1, openness));
|
||||
AddAuthoredSpline($"{name}_lip_area", "/vocal/areas/0/area/lip_opening", start, duration, (0, rounding), (1, rounding));
|
||||
AddAuthoredSpline($"{name}_lip_radiation", "/vocal/radiation/0/aperture", start, duration, (0, Math.Clamp(rounding + 0.1f * intensity, 0, 1)), (1, Math.Clamp(rounding + 0.1f * intensity, 0, 1)));
|
||||
AddAuthoredSpline($"{name}_velum", "/vocal/branches/0/opening", start, duration, (0, 0.01f), (1, 0.01f));
|
||||
}
|
||||
|
||||
private static string GestureDescriptor(string ipa, IReadOnlyDictionary<string, string> fields)
|
||||
{
|
||||
var explicitDescriptor = GetAny(fields, ["descriptor", "desc", "features"], "");
|
||||
if (explicitDescriptor.Length > 0)
|
||||
{
|
||||
return explicitDescriptor;
|
||||
}
|
||||
|
||||
var place = GetAny(fields, ["place"], "");
|
||||
var manner = GetAny(fields, ["manner"], "");
|
||||
var phonation = GetAny(fields, ["voice", "voicing", "phonation"], "");
|
||||
if (place.Length + manner.Length + phonation.Length > 0)
|
||||
{
|
||||
return $"{phonation}_{place}_{manner}";
|
||||
}
|
||||
|
||||
return ipa switch
|
||||
{
|
||||
"p" => "voiceless_bilabial_plosive",
|
||||
"b" => "voiced_bilabial_plosive",
|
||||
"m" => "voiced_bilabial_nasal",
|
||||
"s" => "voiceless_alveolar_fricative",
|
||||
"z" => "voiced_alveolar_fricative",
|
||||
"ʃ" => "voiceless_postalveolar_fricative",
|
||||
"ʒ" => "voiced_postalveolar_fricative",
|
||||
"ʍ" => "voiceless_labial_velar_fricative",
|
||||
"w" => "voiced_labial_velar_approximant",
|
||||
"i" => "voiced_close_front_unrounded_vowel",
|
||||
"u" => "voiced_close_back_rounded_vowel",
|
||||
"a" or "ɑ" => "voiced_open_back_unrounded_vowel",
|
||||
"æ" => "voiced_open_front_unrounded_vowel",
|
||||
"ə" => "voiced_mid_central_unrounded_vowel",
|
||||
_ => "voiced_alveolar_approximant"
|
||||
};
|
||||
}
|
||||
|
||||
private static HashSet<string> DescriptorTokens(string descriptor) =>
|
||||
descriptor
|
||||
.ToLowerInvariant()
|
||||
.Replace('-', '_')
|
||||
.Split(['_', ',', '+', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static float PlaceTarget(HashSet<string> tokens)
|
||||
{
|
||||
if (tokens.Contains("bilabial") || tokens.Contains("labiodental") || tokens.Contains("labial")) return 0.96f;
|
||||
if (tokens.Contains("dental")) return 0.88f;
|
||||
if (tokens.Contains("alveolar")) return 0.78f;
|
||||
if (tokens.Contains("postalveolar") || tokens.Contains("palatoalveolar")) return 0.70f;
|
||||
if (tokens.Contains("retroflex")) return 0.64f;
|
||||
if (tokens.Contains("palatal")) return 0.56f;
|
||||
if (tokens.Contains("velar")) return 0.44f;
|
||||
if (tokens.Contains("uvular")) return 0.34f;
|
||||
if (tokens.Contains("pharyngeal") || tokens.Contains("epiglottal")) return 0.20f;
|
||||
if (tokens.Contains("glottal")) return 0.05f;
|
||||
return 0.70f;
|
||||
}
|
||||
|
||||
private static float ConstrictionTarget(HashSet<string> tokens)
|
||||
{
|
||||
if (tokens.Contains("plosive") || tokens.Contains("stop") || tokens.Contains("nasal")) return 0.03f;
|
||||
if (tokens.Contains("fricative")) return 0.16f;
|
||||
if (tokens.Contains("approximant")) return 0.48f;
|
||||
if (tokens.Contains("trill") || tokens.Contains("tap") || tokens.Contains("flap")) return 0.24f;
|
||||
return 0.55f;
|
||||
}
|
||||
|
||||
private void AddAcousticConnectionRecord(AcousticConnection connection)
|
||||
{
|
||||
if (_acousticConnectionsByName.ContainsKey(connection.Name)) return;
|
||||
|
|
@ -2293,6 +2483,21 @@ public static class PatchScript
|
|||
_controlSplinesByName[name] = _controlSplines[^1];
|
||||
}
|
||||
|
||||
private void AddPhonemeGesture(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var ipa = RequiredAny(fields, ["ipa", "symbol", "phone", "phoneme"], line);
|
||||
var start = GetFloat(fields, line, 0, "start", "time", "at");
|
||||
var duration = Math.Max(0.001f, GetFloat(fields, line, 0.12f, "dur", "duration", "length"));
|
||||
var intensity = Math.Clamp(GetFloat(fields, line, 1, "intensity", "gain", "strength"), 0, 2);
|
||||
var descriptor = GestureDescriptor(ipa, fields);
|
||||
var name = TryGetAny(fields, ["name", "n"], out var nameText)
|
||||
? nameText
|
||||
: $"ipa_{SafeName(ipa)}_{_phonemeGestures.Count}";
|
||||
|
||||
_phonemeGestures.Add(new PhonemeGesture(name, ipa, descriptor, start, duration, intensity));
|
||||
EmitPhonemeGestureSplines(name, descriptor, start, duration, intensity);
|
||||
}
|
||||
|
||||
private void AddControlCurve(IReadOnlyDictionary<string, string> fields, int line)
|
||||
{
|
||||
var parameterPath = Required(fields, "path", line);
|
||||
|
|
@ -2419,6 +2624,13 @@ public static class PatchScript
|
|||
private static string FieldNameFromFieldPath(string fieldPath) =>
|
||||
fieldPath.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? fieldPath;
|
||||
|
||||
private static string SafeName(string value)
|
||||
{
|
||||
var chars = value.Select(ch => char.IsLetterOrDigit(ch) ? ch : '_').ToArray();
|
||||
var safe = new string(chars).Trim('_');
|
||||
return safe.Length == 0 ? "gesture" : safe;
|
||||
}
|
||||
|
||||
private static List<OperatorNode> ParseOperatorNodes(string value, int line) =>
|
||||
value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part =>
|
||||
|
|
@ -3159,6 +3371,7 @@ public static class PatchScript
|
|||
"control_surface" or "surface" or "controlsurface" => "control_surface",
|
||||
"curve" or "control_curve" or "automation" => "curve",
|
||||
"control_spline" or "spline" or "gesture_spline" or "gesturespline" => "control_spline",
|
||||
"phoneme_gesture" or "phone_gesture" or "ipa_gesture" or "phoneme" or "phone" => "phoneme_gesture",
|
||||
"gesture" or "gesture_group" or "gesturegroup" => "gesture",
|
||||
"s" or "sfxr" => "sfxr",
|
||||
_ => command
|
||||
|
|
|
|||
|
|
@ -233,3 +233,4 @@
|
|||
{"date":"2026-05-29","area":"primitive-vocal-law-correction","summary":"Used primitive timeline artifacts to make the first non-fork physical-law correction. `SourcePort` now owns `FlowScale` defaulting to `.02`; primitive radiation reflection now uses declared `RadiationLoad.Reflection` directly; `ProbeTimelineReport` radiation flow now depends on outgoing path wave like Faust. The focused primitive/artifact tests and PatchScriptTests pass. New artifact `artifacts/parity/primitive-vocal-timeline/20260529T155613895` reduces fixture-suite mean absolute timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining drift points to branch/contact motion ownership.","evidence":"src/AquaSynth.Core/Model.cs; src/AquaSynth.Core/PatchScript.cs; src/AquaSynth.Core/FaustExport.cs; src/AquaSynth.Core/ProbeTimelineReport.cs; artifacts/parity/primitive-vocal-timeline/20260529T155613895; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; tests/AquaSynth.Dsl.Tests/PinkTromboneLogMelParityTests.cs"}
|
||||
{"date":"2026-05-29","area":"primitive-vocal-gesture-surfaces","summary":"Gesture motion now has a separate owner. `ControlSurface` exposes normalized primitive controls, `ControlSpline` owns linear/hold/Bezier future-time motion, Faust emits surface and spline point controls, and `ProbeTimelineReport` evaluates spline-driven primitive fields before audio parity. `tract_motion` adapts velopharynx opening into a branch spline. A generated contact-opening spline was tested and cut because PT timeline evidence showed it invented an open-then-close transient; contact closure must come from articulator/area ownership, not primitive-local smoothing. PatchScriptTests pass 90/90 and primitive timeline artifact `20260529T185955965` keeps closure-release mean absolute timeline error at `0.155925`.","evidence":"src/AquaSynth.Core/Model.cs; src/AquaSynth.Core/ControlSplineTimeline.cs; src/AquaSynth.Core/PatchScript.cs; src/AquaSynth.Core/FaustExport.cs; src/AquaSynth.Core/ProbeTimelineReport.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; artifacts/parity/primitive-vocal-timeline/20260529T185955965; docs/acoustic-graph-proprioception.md"}
|
||||
{"date":"2026-05-29","area":"public-gesture-api","summary":"User-authored gestures now have a public runtime API. `ControlSurfaceCatalog.FromPatch(patch)` exposes normalized surface metadata; `ControlSplineTimeline` lets hosts edit future control points and produce per-time surface control dictionaries; `AquaSynthCompiledPatch.ControlSurfaces` carries the catalog through native Faust compilation; `AquaSynthCompiledPatch.ControlValuesAt(timeline,t)` filters semantic timeline values to the controls actually exported by optimized Faust DSP. This makes custom client gestures first-class without recompiling or relying on PT adapters. PatchScriptTests pass 91/91 and NativeFaustRuntimeTests pass 5/5.","evidence":"src/AquaSynth.Core/ControlSplineTimeline.cs; src/AquaSynth.Faust/NativeFaustRuntime.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; tests/AquaSynth.Dsl.Tests/NativeFaustRuntimeTests.cs; docs/acoustic-graph-proprioception.md"}
|
||||
{"date":"2026-05-29","area":"ipa-phoneme-gesture-dsl","summary":"Added deterministic IPA/anatomy gesture sugar. `phoneme_gesture` records a symbol/descriptor/time/intensity and expands known IPA symbols or explicit place/manner/voicing descriptors into ordinary `ControlSpline`s over public source, area, branch, contact, and radiation surfaces. This gives local IPA-chart/audio datasets a seeded anatomical control path while preserving FM/AM/modulators/envelopes/filters as expressive patch tools around the anatomical stream. PatchScriptTests pass 92/92.","evidence":"src/AquaSynth.Core/Model.cs; src/AquaSynth.Core/PatchScript.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/acoustic-graph-proprioception.md"}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
"generated DSP artifact manifests for engine hosting",
|
||||
"live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof for engine hosts",
|
||||
"phonetic intent, articulatory plan, morphology capability, and constraint-report contracts for vocal tract synthesis",
|
||||
"Pink Trombone fixture path through primitive vocal DSL records: AreaFunction, WaveguidePath, SourcePort, ConstrictionContact, BranchPort, RadiationLoad, ProbeTimeline, ControlSurface, ControlSpline, and VocalNetwork; tract_shape, glottis, tract_injection, nasal_branch, tract_motion, and tract are adapters, not DSP authority",
|
||||
"Pink Trombone fixture path through primitive vocal DSL records: AreaFunction, WaveguidePath, SourcePort, ConstrictionContact, BranchPort, RadiationLoad, ProbeTimeline, ControlSurface, ControlSpline, PhonemeGesture, and VocalNetwork; tract_shape, glottis, tract_injection, nasal_branch, tract_motion, phoneme_gesture, and tract are adapters/sugar, not DSP authority",
|
||||
"utterance parity curriculum research and docs",
|
||||
"patch script parser",
|
||||
"SFXR atoms and parameter mapping",
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
"Surge XT for modern hybrid synth modulation",
|
||||
"Cardinal/VCV-style patches for modular cable/rate semantics"
|
||||
],
|
||||
"rule": "For speech, add schema fields and learned-control lanes only when the parity harness has a loss surface that can exercise them. For reference synths, add DSL abstractions only when rebuilds prove the missing invariant. Parsing alone is inventory, not evidence of expressivity. Pink Trombone should be decomposed into reusable low-level Aqua primitives before audio golf, not copied as a bespoke clone or inflated into PT-shaped modules. After the 2026-05-29 proprioception correction, collapse graph-vocal lowering into a small set of instrumented physical primitives before more utterance golf. Gesture motion belongs in ControlSurface/ControlSpline timeline ownership, not primitive-local smoothing; cut any generated contact/port spline that invents motion not supported by primitive reference timelines. Public clients author gestures through ControlSurfaceCatalog plus ControlSplineTimeline and stream surface control dictionaries; higher-level gesture templates are only spline emitters."
|
||||
"rule": "For speech, add schema fields and learned-control lanes only when the parity harness has a loss surface that can exercise them. For reference synths, add DSL abstractions only when rebuilds prove the missing invariant. Parsing alone is inventory, not evidence of expressivity. Pink Trombone should be decomposed into reusable low-level Aqua primitives before audio golf, not copied as a bespoke clone or inflated into PT-shaped modules. After the 2026-05-29 proprioception correction, collapse graph-vocal lowering into a small set of instrumented physical primitives before more utterance golf. Gesture motion belongs in ControlSurface/ControlSpline timeline ownership, not primitive-local smoothing; cut any generated contact/port spline that invents motion not supported by primitive reference timelines. Public clients author gestures through ControlSurfaceCatalog plus ControlSplineTimeline and stream surface control dictionaries; higher-level gesture templates and phoneme_gesture are only spline emitters. IPA descriptors seed which anatomical surfaces move, while local datasets golf numeric spline defaults and patch voice code."
|
||||
},
|
||||
"ipa_vocal_tract_path": {
|
||||
"status": "This is now the active project center. Roadmap and research notes align the curriculum around structured utterance metadata -> learned utterance embedding -> learned synth automation -> vocal tract/envelope/LFO/filter/spectral controls -> rendered audio loss. The first Core contracts still defend the boundary: PhoneticIntent is the Weksa/language-owned handoff, ArticulatoryPlan is the AquaSynth realization layer, VocalTractMorphology declares anatomy capabilities, ArticulatoryConstraintReport is the host-visible veto surface, and VocalTractPlanResult is the tiny end-to-end proof artifact for host branching. IPA is an authoring input only: tokenization and feature mapping lower into language phonology profiles, timed articulatory gestures, morphology-specific tract/excitation curves, and Faust DSP. The staged support plan starts with tokenization and vowels, then core pulmonic consonants, expanded human IPA, airstream/phonation, and Weksa/alien morphologies. Optional eSpeak NG tests provide the first generated speech workout when ESPEAK_NG or espeak-ng is installed; eSpeak is intelligibility/timing/log-mel pressure, not anatomy truth. AquaSynth owns the learned utterance embedding encoder and synth driver: speech text embeddings plus prosody/emphasis hints plus Ghostlight/Epiphany-shaped character state vectors compress into utterance embeddings, and phonetic features plus that embedding feed a packed-buffer native C# backprop/Adam mapper that predicts tract, AM/FM, LFO, filter, and mel spectral-envelope controls. SpeechBackpropagationPipeline chains the two models: synth-driver target loss returns gradients with respect to its input, slices the utterance-embedding gradient, and updates the utterance encoder from downstream speech loss. The generated ground-truth training fixture renders eSpeak syllables, extracts log-mel evidence, and trains that chained pipeline toward eSpeak-seeded mel/control targets. CultMesh speech training now has payload manifests, peer/lease admission receipts, worker assignment, result collection, and central checkpoint application; workers do not own model updates. Real remote artifact chunk transfer and process supervision remain CultMesh infrastructure surfaces, not AquaSynth workaround territory.",
|
||||
|
|
@ -121,8 +121,8 @@
|
|||
},
|
||||
"last_verified": {
|
||||
"command": "dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false --filter \"FullyQualifiedName~PatchScriptTests\" -v minimal",
|
||||
"result": "Public gesture API cut passes PatchScriptTests 91/91 and NativeFaustRuntimeTests 5/5. ControlSurfaceCatalog.FromPatch exposes normalized surface metadata; ControlSplineTimeline supports future point edits and ControlValuesAt(t) surface dictionaries; AquaSynthCompiledPatch carries ControlSurfaces through native Faust compilation and filters timeline values to exported optimized DSP controls. Earlier full unfiltered runs hit the five-minute wall after leaving orphan dotnet workers; focused suites are the current reliable verification path.",
|
||||
"commit": "pending public gesture API"
|
||||
"result": "IPA phoneme gesture DSL cut passes PatchScriptTests 92/92. phoneme_gesture records symbol/descriptor/start/duration/intensity and emits deterministic anatomical ControlSplines over public surfaces; it is seed material for local IPA/reference-data golf, not exact articulatory truth. Earlier full unfiltered runs hit the five-minute wall after leaving orphan dotnet workers; focused suites are the current reliable verification path.",
|
||||
"commit": "pending ipa phoneme gesture dsl"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
Current slice:
|
||||
|
||||
- Added the first IPA/anatomy gesture DSL layer. `phoneme_gesture` accepts
|
||||
known IPA symbols or explicit descriptors like
|
||||
`descriptor=voiceless_labial-velar_fricative`, maps place/manner/voicing to
|
||||
the primary anatomical surfaces, and emits ordinary `ControlSpline`s. This is
|
||||
deterministic seed material for local dataset golf, not a claim that IPA
|
||||
labels contain exact numeric targets. Patches remain free to use normal
|
||||
FM/AM/modulators/envelopes/filters around the anatomical control stream.
|
||||
Verification: `PatchScriptTests` pass 92/92.
|
||||
|
||||
- Public gesture API pass implemented. `ControlSurfaceCatalog.FromPatch(patch)`
|
||||
is the host-facing discovery surface for normalized controls; hosts can create
|
||||
`ControlSplineTimeline`, edit future points, and call `ControlValuesAt(t)` to
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ live_invariants:
|
|||
- First comparison-driven primitive law correction: `SourcePort` owns `FlowScale` (default `.02`) so pressure/tension/opening no longer emit hidden unit-scale waves; `RadiationLoad.Reflection` owns reflection directly and aperture no longer rewrites that coefficient; `ProbeTimelineReport` radiation flow now uses path outgoing wave like Faust instead of reporting `aperture / impedance`. Artifact `artifacts/parity/primitive-vocal-timeline/20260529T155613895` drops fixture-suite mean absolute timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining large drift is branch/contact dynamics, which needs an owner decision about gesture slew versus primitive-local smoothing.
|
||||
- Gesture motion ownership is now separate from acoustic primitives. `ControlSurface` exposes normalized runtime controls for primitive fields, and `ControlSpline` owns future-time linear/hold/Bezier gesture motion over those surfaces. Faust lowering emits surface and spline point controls, maps effective normalized values back to physical primitive fields, and keeps old `@/param` bindings as compatibility base controls. `tract_motion` adapts velopharynx opening into a branch spline. A generated contact-opening spline was tested and cut because PT timeline evidence showed it invented an open-then-close transient; contact closure must come from articulator/area ownership. Focused verification: `PatchScriptTests` pass 90/90 and primitive timeline artifact `artifacts/parity/primitive-vocal-timeline/20260529T185955965` keeps `closure-release` at mean absolute error `0.155925`.
|
||||
- Public gesture authoring API is now explicit. `ControlSurfaceCatalog.FromPatch(patch)` exposes normalized surface metadata for hosts; `ControlSplineTimeline` lets clients edit future points and emit `ControlValuesAt(t)` as ordinary surface-control dictionaries. Native Faust compiled patches carry `ControlSurfaces` and expose `ControlValuesAt(timeline,t)` to filter semantic timeline values to controls exported by the optimized DSP. This is the runtime path for user-authored gestures without recompilation. Verification: `PatchScriptTests` pass 91/91 and `NativeFaustRuntimeTests` pass 5/5.
|
||||
- First IPA/anatomy sugar is live. `phoneme_gesture` accepts known IPA symbols or explicit descriptors such as `descriptor=voiceless_labial-velar_fricative`, maps place/manner/voicing onto primary anatomical surfaces, and emits ordinary `ControlSpline`s. It is deterministic seed material for local reference-data golf, not exact articulatory truth. Patches may still use normal FM/AM/modulators/envelopes/filters around the anatomical control stream. Verification: `PatchScriptTests` pass 92/92.
|
||||
- TurbulenceJet now includes a release-timed burst-noise component in addition to the deterministic pressure pulse, but a stronger trial barely moved papa; the source is not the main remaining articulation bottleneck. Graph radiation now mixes 30% raw boundary flow with 70% high-passed flow after local admittance weighting, down from the over-bright 15/85 blend. Latest radiation/source artifact under `artifacts/parity/pink-trombone-utterance-logmel/20260527T174236049`: mama/papa/thrombosis cosine 0.6578/0.7692/0.5585, rmsRatio 3.0006/2.6153/2.3488, articulation 0.1437/0.1173/0.1191. Static `closure-release` under `artifacts/parity/pink-trombone-logmel/20260527T174139661` has rmsRatio 0.9900 and speechBandRatio 0.0842. This is still not speaking; next pressure is true tract resonance/passive scattering and pressure storage, not more burst gain.
|
||||
- Same-path two-port area discontinuities now emit `graph_area_energy_in_*` and `graph_area_energy_out_*` probes alongside the three-port branch `graph_connection_energy_*` probes. Use these to inspect passivity and formant-body failures at the scatter sites rather than judging only final output loudness.
|
||||
- `FaustExportOptions.DebugProbeUi` wraps graph probe signals in Faust `vbargraph` UI controls under `/debug/...` without changing audio output arity. Native streaming Faust patches now separate writable controls from read-only probe zones and can read/snapshot probe values after `ProcessBlock`; use this for acoustic graph passivity/radiation inspection, leave it off for normal parity renders.
|
||||
|
|
|
|||
|
|
@ -407,6 +407,42 @@ public sealed class PatchScriptTests
|
|||
timeline.SetFuturePoint("/vocal/nope", timeSeconds: .02f, normalizedValue: .5f, nowSeconds: .0f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhonemeGestureDslEmitsAnatomicalControlSplines()
|
||||
{
|
||||
var patch = PatchScript.Parse("""
|
||||
morphology name=oral length_cm=17 diameters=.6,.8,1.2,1.6,1.3,.9 tongue_index=3 tongue_diameter=1.4 constriction_index=4 constriction_diameter=1 lip_opening=1.5
|
||||
morphology name=nasal length_cm=12 diameters=.05,.35,.6,.8
|
||||
waveguide_path name=oral_path morphology=oral loss=.998
|
||||
waveguide_path name=nasal_path morphology=nasal loss=.997
|
||||
source_port name=folds path=oral_path pressure=.7 tension=.55 opening=.45 noise=.05 impedance=.3
|
||||
branch_port name=velopharynx from=oral_path from_position=.45 to=nasal_path opening=.01 coupling=1
|
||||
constriction_contact name=contact path=oral_path position=.92 opening=.5 resistance=.4 stored_pressure=.1
|
||||
radiation_load name=mouth path=oral_path aperture=.8 reflection=-.82 impedance=.28
|
||||
vocal_network name=voice paths=oral_path,nasal_path sources=folds contacts=contact branches=velopharynx radiation=mouth
|
||||
phoneme_gesture name=ma ipa=m start=0 dur=.16 intensity=.8
|
||||
phoneme_gesture name=hw ipa=ʍ descriptor=voiceless_labial-velar_fricative start=.2 dur=.12 intensity=.9
|
||||
phoneme_gesture name=vowel ipa=a start=.34 dur=.20 intensity=.7
|
||||
vocal network=voice freq=150 gain=.2 sustain=.6
|
||||
""");
|
||||
|
||||
Assert.Equal(3, patch.PhonemeGestures.Count);
|
||||
Assert.Contains(patch.PhonemeGestures, gesture => gesture.Name == "hw" && gesture.Descriptor.Contains("labial-velar", StringComparison.Ordinal));
|
||||
Assert.Contains(patch.ControlSplines, spline => spline.Name.StartsWith("ma_velum", StringComparison.Ordinal) && spline.SurfacePath == "/vocal/branches/0/opening");
|
||||
Assert.Contains(patch.ControlSplines, spline => spline.Name.StartsWith("ma_contact", StringComparison.Ordinal) && spline.SurfacePath == "/vocal/contacts/0/opening");
|
||||
Assert.Contains(patch.ControlSplines, spline => spline.Name.StartsWith("hw_source_noise", StringComparison.Ordinal) && spline.SurfacePath == "/vocal/sources/0/noise");
|
||||
Assert.Contains(patch.ControlSplines, spline => spline.Name.StartsWith("hw_constriction_index", StringComparison.Ordinal) && spline.SurfacePath == "/vocal/areas/0/area/constriction_index");
|
||||
Assert.Contains(patch.ControlSplines, spline => spline.Name.StartsWith("vowel_tongue_index", StringComparison.Ordinal) && spline.SurfacePath == "/vocal/areas/0/area/tongue_index");
|
||||
|
||||
var catalog = ControlSurfaceCatalog.FromPatch(patch);
|
||||
var timeline = catalog.CreateTimeline();
|
||||
var nasalControls = timeline.ControlValuesAt(.08f);
|
||||
var fricativeControls = timeline.ControlValuesAt(.24f);
|
||||
|
||||
Assert.True(nasalControls["/vocal/branches/0/opening"] > fricativeControls["/vocal/branches/0/opening"]);
|
||||
Assert.True(fricativeControls["/vocal/sources/0/noise"] > nasalControls["/vocal/sources/0/noise"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrimitiveProbeTimelineReportsFlowBeforeAudioParity()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue