Nasal stops hold their oral closure to 0.8 of the gesture: /n/ murmur -3.8 to -8.8 dB re the flanking vowels (/m/ -12.8)

The alveolar closure began to open at half the gesture while the velum and the contact held to 0.8, so the oral tract radiated over the last third of the murmur. verify_renders.py now measures the murmur on steady parts against the mean flanking-vowel power; the render test holds both nasals 8 to 16 dB under the vowels.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Aqua 2026-11-09 12:50:59 +00:00
commit 4eab1979bd
3 changed files with 46 additions and 5 deletions

View file

@ -1544,7 +1544,11 @@ public static class PatchScript
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)));
// A nasal stop holds its oral closure for as long as the velum and the contact stay put (to 0.8 of the gesture): the murmur is the
// closed-mouth sound, and an alveolar closure that began to open at 0.5 let the oral tract radiate again over the last third (/n/ read
// -3.8 dB under its vowels against /m/ -13.2 dB, whose lips hold to 0.7).
var closureHold = tokens.Contains("nasal") ? 0.8f : 0.5f;
AddAuthoredSpline($"{name}_constriction_diameter", "/vocal/areas/0/area/constriction_diameter", start, duration, (0, constriction), (closureHold, constriction), (1, Math.Min(0.65f, constriction + 0.2f)));
if (tokens.Contains("bilabial") || tokens.Contains("labiodental") || tokens.Contains("labial") || tokens.Contains("rounded"))
{

View file

@ -113,4 +113,38 @@ public sealed class StressAndReleaseRenderTests(ITestOutputHelper output)
Assert.True(burstAfter < burstPeak / 5, "the burst is over once the vowel is open");
Assert.True(burstPeak < vowelPeak / 2, "the burst stays well under the following vowel's peak (the b level fix)");
}
// /ɑ 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)
{
var script = IpaGestureExperiment.BuildWordScript(
[(new IpaGestureExperimentTarget("w_0_a", "a", "", 0.18f), 1f), (new IpaGestureExperimentTarget("w_1_c", ipa, "", 0.095f), 1f), (new IpaGestureExperimentTarget("w_2_a", "a", "", 0.18f), 1f)], frequency);
return (Render(script, 0.5f), 0.18, 0.275);
}
// Level of the consonant's middle 60 percent against the mean power of the steady parts of the two flanking vowels (the 70 ms before the
// consonant and 90 ms from 30 ms after it), in dB.
private static double ConsonantDbReVowels(float[] x, double from, double to)
{
var v1 = Rms(x, from - 0.08, from - 0.01);
var v2 = Rms(x, to + 0.03, to + 0.12);
var vowels = Math.Sqrt((v1 * v1 + v2 * v2) / 2);
var span = to - from;
return 20 * Math.Log10(Math.Max(1e-9, Rms(x, from + 0.2 * span, to - 0.2 * span)) / vowels);
}
// Fujimura 1962 and later accounts put the nasal murmur well under the flanking vowels; the target band is 8 to 16 dB under (a DESIGN
// value: no primary level was retrieved, docs/research/articulatory-vocal-tract-sources.md). /m/ measured -12.8 dB; /n/ read -3.8 dB
// while its alveolar closure began to open at half the gesture, a third before the velum and the contact did.
[Theory]
[InlineData("m")]
[InlineData("n")]
public void NasalMurmurIsWellBelowTheFlankingVowels(string ipa)
{
var (x, from, to) = VowelConsonantVowel(ipa);
if (x is null) return;
var db = ConsonantDbReVowels(x, from, to);
output.WriteLine($"/{ipa}/ murmur {db:F1} dB re the flanking vowels");
Assert.InRange(db, -16, -8);
}
}

View file

@ -61,10 +61,13 @@ def main():
p = os.path.join(a.dir, w, f"{w}.wav")
if not os.path.exists(p): continue
x, sr = wav(p)
seg = [(0, .18), (.18, .275), (.275, .455)]
r = [rms(x, sr, *s) for s in seg]
res[w] = {"rms": r, "murmur_db_re_vowel": round(20 * np.log10(r[1] / max(r[0], 1e-9)), 1), "burg_murmur": burg(p, .20, .26)}
print(f"{w}: rms V/{c}/V {['%.4f' % q for q in r]} murmur {res[w]['murmur_db_re_vowel']} dB re first vowel; Burg of the murmur {res[w]['burg_murmur']}")
# steady parts only: the consonant's middle 60 percent against the mean power of the 70 ms before it and the 90 ms from 30 ms after it
c0, c1 = .18, .275
v1, v2 = rms(x, sr, c0 - .08, c0 - .01), rms(x, sr, c1 + .03, c1 + .12)
m = rms(x, sr, c0 + .2 * (c1 - c0), c1 - .2 * (c1 - c0))
db = round(20 * np.log10(max(m, 1e-9) / np.sqrt((v1 ** 2 + v2 ** 2) / 2)), 1)
res[w] = {"rms": [v1, m, v2], "murmur_db_re_flanking_vowels": db, "burg_murmur": burg(p, .20, .26)}
print(f"{w}: steady rms V/{c}/V {['%.4f' % q for q in (v1, m, v2)]} murmur {db} dB re the flanking vowels; Burg of the murmur {res[w]['burg_murmur']}")
# stress
p0, p1 = os.path.join(a.dir, "nibu_flat", "nibu_flat.wav"), os.path.join(a.dir, "nibu", "nibu.wav")
if os.path.exists(p0) and os.path.exists(p1):