From 1b6d1803060a8cd966fed59e5f6b898e2252c08a Mon Sep 17 00:00:00 2001 From: Aqua Date: Thu, 12 Nov 2026 13:26:20 +0000 Subject: [PATCH] Tract: the plosive burst gain exists only around its own release; weak-spot re-measure and listening kit (unheard) release_flow was held at the burst gain from the plosive's gesture onward, so later contact states made noise: burst-alone peak 0.0302 in the tail of /bin/, 0.0688 in the leading /n/ of /nibu/. BurstGainAroundGesture ramps it to zero around the gesture. Test PlosiveBurstExistsOnlyAroundItsOwnRelease (before the fix: 0.0675 before the plosive and 0.0215 after it; after: 0.0000 and 0.0000). weak_spots.py re-measures stress level, release, nasals and lax vowels from WordRender renders; WordRender gains --no-burst. Kit tract-weak-spots (14 clips) for aqua-listen. Sources doc: Weak spots re-measured 2026-11-12. Co-Authored-By: Claude Sonnet 5.5 --- .../articulatory-vocal-tract-sources.md | 18 ++- src/AquaSynth.Core/PatchScript.cs | 29 ++++- .../StressAndReleaseRenderTests.cs | 24 ++++ tools/WordRender/Program.cs | 12 +- .../kits/tract-weak-spots/kit.json | 117 ++++++++++++++++++ .../kits/tract-weak-spots/phones/ama.json | 1 + .../kits/tract-weak-spots/phones/ana.json | 1 + .../kits/tract-weak-spots/phones/ba.json | 1 + .../kits/tract-weak-spots/phones/bin.json | 1 + .../kits/tract-weak-spots/phones/bun.json | 1 + .../kits/tract-weak-spots/phones/nibu.json | 1 + .../tract-weak-spots/phones/nibu_flat.json | 1 + .../kits/tract-weak-spots/phones/v_ah.json | 1 + .../kits/tract-weak-spots/phones/v_ah_s.json | 1 + .../kits/tract-weak-spots/phones/v_e.json | 1 + .../kits/tract-weak-spots/phones/v_e_s.json | 1 + .../kits/tract-weak-spots/phones/v_i.json | 1 + .../kits/tract-weak-spots/phones/v_i_s.json | 1 + .../kits/tract-weak-spots/phones/v_ih.json | 1 + .../kits/tract-weak-spots/phones/v_o.json | 1 + .../kits/tract-weak-spots/phones/v_o_s.json | 1 + .../tract-weak-spots/phones/v_oo_short.json | 1 + .../kits/tract-weak-spots/phones/v_u.json | 1 + .../kits/tract-weak-spots/phones/v_u_s.json | 1 + .../kits/tract-weak-spots/render.sh | 29 +++++ tools/formant-check/weak_spots.py | 117 ++++++++++++++++++ 26 files changed, 357 insertions(+), 8 deletions(-) create mode 100644 tools/aqua-listen/kits/tract-weak-spots/kit.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/ama.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/ana.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/ba.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/bin.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/bun.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/nibu.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/nibu_flat.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_ah.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_ah_s.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_e.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_e_s.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_i.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_i_s.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_ih.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_o.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_o_s.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_oo_short.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_u.json create mode 100644 tools/aqua-listen/kits/tract-weak-spots/phones/v_u_s.json create mode 100755 tools/aqua-listen/kits/tract-weak-spots/render.sh create mode 100644 tools/formant-check/weak_spots.py diff --git a/docs/research/articulatory-vocal-tract-sources.md b/docs/research/articulatory-vocal-tract-sources.md index 61a0f69..b45fdc8 100644 --- a/docs/research/articulatory-vocal-tract-sources.md +++ b/docs/research/articulatory-vocal-tract-sources.md @@ -127,9 +127,23 @@ resolution), release 60-95 ms peak 0.0695, vowel 0.129. The wall radiation reads level of -30 dB; the primary level was not retrieved, and the 2.4 manual's skin radiation, Birkholz & Zhang 2025, is the reference to read). In a vowel the term moves the f0 line by under 1 dB; the 200 and 300 Hz harmonics move up to +1.5 dB on one open vowel (wall-only measurement, Soul pass). The band is two second-order sections, not a brick wall: with the mouth and the velum shut the bar sits about 25 dB down above 300 Hz. The burst itself (difference of renders with and without `release_flow`) peaks at about 0.006-0.008, -24 dB re the vowel peak; the release peak in the table above is the voicing breaking through as the contact opens. Burst gain 0.06 is a DESIGN VALUE. -Weak spots found in the renders: (1) the vowel level is not steady, it rises for about 100 ms after onset (ɑ: rms 0.028 at 80 ms to 0.079 at 160 ms) so any window early in a vowel -reads low; the cause is not isolated (surface smoothing and the pressure ramp are the suspects). (2) /n/ murmur 2 dB above /m/ (above). (3) The voice bar's level is a design value and its band +Weak spots found in the renders, 2026-11-08 (not re-measured on 2026-11-12 unless named in the next paragraph): (1) the vowel level is not steady, it rises for about 100 ms after onset (ɑ: rms 0.028 at 80 ms to 0.079 at 160 ms) so any window early in a vowel +reads low; the cause is not isolated (surface smoothing and the pressure ramp are the suspects). (3) The voice bar's level is a design value and its band (90-300 Hz) assumes f0 of 90 Hz and up. (4) Only f0 and level of the first vowel are checked per cue; the cues' perceptual strength is unknown. Everything here is a measurement, nothing was heard. +Re-measure 2026-11-12 on main c0e6b98 (`tools/formant-check/weak_spots.py` over renders from `tools/aqua-listen/kits/tract-weak-spots/render.sh`; the renders reproduce the table above: /ɪ/ 375/2024 Hz, /ʊ/ 486/894 Hz, murmurs -11.81 and -9.64 dB, /i/ in /nibu/ +0.21 dB). Unheard. +- Stress level (100-170 ms of each vowel, stressed against unstressed, f0 rise split from the rest by a render of the unstressed vowel at 148.4 Hz): total i +1.22, ɑ +1.67, u +2.07, e +2.99, o +1.69 dB (mean 1.93, sd 0.60); + the f0 rise alone -0.80, +0.03, +0.17, +1.23, -0.07 dB; the rest (the amplitude factor 1.30 = 2.28 dB nominal, with the longer vowel) +2.02, +1.64, +1.90, +1.76, +1.76 dB (sd 0.14). The scatter between vowels is the f0 rise; the amplitude cue itself is even. + No level the f0 rise cannot move was found cheap, so the five-vowel-mean basis stands (`aqua-body:question:stress-amplitude-calibration-basis`); the rest sits 0.46 dB under the nominal 2.28 dB, cause not isolated. +- Plosive release: a defect was found and fixed. `release_flow` was held at the burst gain from the plosive's gesture on (a surface holds its first value before its first point and its last after its last), so every later contact state made noise: + the difference of the renders with and without `release_flow` peaked at 0.0302 in the final /n/ and tail of /bɪn/ (above its 0.0226 at the release, 81 ms), 0.0126 in /bʊn/; in /nibu/ (flat) the leading /n/ carried it backward: peak 0.0688, rms 0.0067 against the /n/'s own 0.0058. + After the fix (`BurstGainAroundGesture`: zero before and after the plosive, the gain through it) the same difference is 0.0 from 120 ms after the release and the burst itself is unchanged (0.0226 at 81 ms in /bɪn/). Burst alone, after the fix: /bɪn/ 0.0226 (-15.1 dB re its vowel peak 0.129), /ba/ 0.0142 (-24.0 dB re 0.225), /bʊn/ 0.0130 (-24.3 dB re 0.213), + each falling to a tenth in 15-17 ms; the earlier "0.006-0.008, -24 dB" figure above does not hold for /bɪn/. The release peak of the full render (voicing breaking through) is -5.4, -6.3, -5.9 dB re the vowel peak. Test `PlosiveBurstExistsOnlyAroundItsOwnRelease`. +- Nasals: /m/ -11.81 dB, /n/ -9.64 dB re the flanking vowels, Burg N1 295 and 332 Hz. The 2 dB is not harmonic sampling: /n/ minus /m/ is 2.01, 2.17, 2.05, 1.61 dB at f0 110, 140, 170, 200 Hz. Band levels of the murmur (0-400, 400-800, 800-1600, 1600-3200 Hz): /m/ 36.3, 23.6, 21.3, 2.7; /n/ 38.4, 28.2, 16.8, 0.9 dB. + The cause (the shorter oral side branch, 7.1 cm against 9.6 cm from the velum junction) is a candidate, not shown. Open. +- Lax vowels: /ɪ/ and /ʊ/ are in the DSL table (`StoryVowelAreas`) and render in `WordRender` (/bɪn/, /bʊn/, isolated). Burg F1/F2: /ɪ/ 375/2024 against the tube chain 346/1987 (+8.5/+1.8 %), Hillenbrand 429/2034 (-12.5/-0.5 %), Peterson & Barney 392/1993 (-4.3/+1.5 %); + /ʊ/ 486/894 against 478/850 (+1.7/+5.2 %), 469/1123 (+3.7/-20.4 %), 437/1023 (+11.3/-12.6 %). No defect found. +- Phones the tract cannot make: `WordRender` refuses /t/, /d/, /k/, /l/ with exit 4 (`UNSUPPORTED PHONES (the tract has no gesture for them; nothing rendered): t`); the table holds p b m n s z ʃ ʒ ʍ w and the Story vowels. A direct `phoneme_gesture` on an unknown IPA symbol still falls back to an alveolar approximant (`GestureDescriptor`); not changed here. + Mutations (hand-applied, restored, each run against the named tests): pitch ratio ignored, burst removed, burst not shaped by opening, burst gain x20 (0.06 to 1.2), stress duration ignored, stress pitch ignored, stress amplitude ignored, secondary equal to primary, probe timeline sample rate re-hardcoded to 48000: nine of nine killed. Script: `work/mut-b3.sh` in the aqua workspace. diff --git a/src/AquaSynth.Core/PatchScript.cs b/src/AquaSynth.Core/PatchScript.cs index ff2f3db..2fdea8d 100644 --- a/src/AquaSynth.Core/PatchScript.cs +++ b/src/AquaSynth.Core/PatchScript.cs @@ -1567,8 +1567,10 @@ public static class PatchScript 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)); // The release burst is turbulence at the opening constriction: its gain is held for the plosive and the exporter shapes - // it by the stored pressure and the opening (see PrimitiveContactExpression). - HoldOnSurface($"{name}_release_flow", "/vocal/contacts/0/release_flow", start, duration, PlosiveBurstGain); + // it by the stored pressure and the opening (see PrimitiveContactExpression). The gain exists only around this plosive: + // a surface holds its last value after its last point (and its first before its first), so a gain left at its level + // made noise at every later contact state, the final nasal's closure and the tail included. + BurstGainAroundGesture($"{name}_release_flow", "/vocal/contacts/0/release_flow", start, duration, PlosiveBurstGain); AddAuthoredSpline($"{name}_velum", "/vocal/branches/0/opening", start, duration, (0, VelumClosed), (1, VelumClosed)); } else if (tokens.Contains("fricative")) @@ -1627,6 +1629,29 @@ public static class PatchScript AddAuthoredSpline(name, surfacePath, start, duration, (0, normalized), (1, normalized)); } + // Zero before and after the gesture, the gain through it; the ramps are 0.15 of the gesture long and run into the neighbouring + // phones (the leading one is cut where the word begins). + private void BurstGainAroundGesture(string name, string surfacePath, float start, float duration, float gain) + { + if (!_controlSurfacesByPath.TryGetValue(surfacePath, out var surface) || surface.MaxValue <= surface.MinValue) + { + return; + } + + var level = Math.Clamp((gain - surface.MinValue) / (surface.MaxValue - surface.MinValue), 0, 1); + var handle = duration * 0.25f; + ControlSplinePoint At(float time, float value) => new(time, value, handle, value, handle, value); + var ramp = duration * 0.15f; + var points = new List(); + if (start > 0) points.Add(At(Math.Max(0, start - ramp), 0)); + points.Add(At(start, level)); + points.Add(At(start + duration, level)); + points.Add(At(start + duration + ramp, 0)); + var spline = new ControlSpline(UniqueSplineName(name), surfacePath, points.ToArray(), ControlSplineInterpolation.Bezier); + _controlSplines.Add(spline); + _controlSplinesByName[spline.Name] = spline; + } + private static string GestureDescriptor(string ipa, IReadOnlyDictionary fields) { var explicitDescriptor = GetAny(fields, ["descriptor", "desc", "features"], ""); diff --git a/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs b/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs index bebfa1c..1ea7b08 100644 --- a/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs @@ -114,6 +114,30 @@ public sealed class StressAndReleaseRenderTests(ITestOutputHelper output) Assert.True(burstPeak < vowelPeak / 2, "the burst stays well under the following vowel's peak (the b level fix)"); } + // The burst is noise whose amplitude is release_flow times the stored pressure through the opening, and release_flow was held at its + // gain from the plosive's gesture on, so it never ended: every later contact state made noise too. In /bin/ the "burst" alone (the + // difference of the renders with and without release_flow) peaked at 0.0302 in the final /n/ and the tail, above its 0.0226 at the + // release itself, and in /nibu/ the leading /n/ and /i/ carried it backward (a spline holds its first value before its first point). + [Fact] + public void PlosiveBurstExistsOnlyAroundItsOwnRelease() + { + IpaGestureExperimentTarget Phone(string label, string ipa, float seconds) => new($"w_{label}", ipa, "", seconds); + var script = IpaGestureExperiment.BuildWordScript( + [(Phone("0_n", "n", 0.095f), 1f), (Phone("1_a", "a", 0.18f), 1f), (Phone("2_b", "b", 0.095f), 1f), (Phone("3_a", "a", 0.18f), 1f), (Phone("4_n", "n", 0.095f), 1f)], 140); + var withBurst = Render(script, 0.77f); + var withoutBurst = Render(script, 0.77f, p => p with { ControlSplines = p.ControlSplines.Where(s => !s.SurfacePath.EndsWith("/release_flow", StringComparison.Ordinal)).ToArray() }); + if (withBurst is null || withoutBurst is null) return; + var burst = withBurst.Zip(withoutBurst, (x, y) => x - y).ToArray(); + const double bFrom = 0.275, bTo = 0.37; + var atRelease = Peak(burst, bFrom + 0.095 * 0.6, bTo + 0.02); + var leading = Peak(burst, 0, bFrom); + var trailing = Peak(burst, bTo + 0.12, 0.77); + output.WriteLine($"burst peak {atRelease:F4} at the release, {leading:F4} before the plosive (n, a), {trailing:F4} after it (a, n, tail)"); + Assert.True(atRelease > 0.004, "the burst is still there at the release"); + Assert.True(leading < atRelease / 20, "no burst noise before the plosive's own gesture"); + Assert.True(trailing < atRelease / 10, "no burst noise in the vowel, the nasal and the tail after the plosive"); + } + // /ɑ C ɑ/ at the scaffold's own durations (vowel 0.18 s, consonant 0.095 s). Returns the render and the consonant's [start, end). private (float[]? Samples, double From, double To) VowelConsonantVowel(string ipa, float frequency = 140) { diff --git a/tools/WordRender/Program.cs b/tools/WordRender/Program.cs index 1b597e2..bfee4d6 100644 --- a/tools/WordRender/Program.cs +++ b/tools/WordRender/Program.cs @@ -8,10 +8,12 @@ using AquaSynth.Faust; // Offline probe: one word, segmented by Weksa (the phones line of `weksa lower --phones`), as a run of IPA gestures on the // IpaGestureExperiment scaffold, rendered through the native libfaust path to a mono 16-bit WAV plus script.aqua, source.dsp // and manifest.json. It measures peak, rms and NaN; it cannot say how the word sounds. -// usage: WordRender