Add /t/ to the IPA table: voiceless alveolar plosive

Measured on /ata/: closure -24.9 dB re vowels (/p/ -20.4), burst 0.39 of the vowel peak, burst peak inside
3.5-7 kHz. 'Above /p/' dropped from the criterion (steward ruling): the pinned labial burst enters at the
glottal end.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Aqua 2026-11-12 19:10:15 +00:00
commit a4a5db1fa4
3 changed files with 62 additions and 0 deletions

View file

@ -25,6 +25,7 @@ public static class PatchScript
{
"p" => "voiceless_bilabial_plosive",
"b" => "voiced_bilabial_plosive",
"t" => "voiceless_alveolar_plosive",
"m" => "voiced_bilabial_nasal",
"n" => "voiced_alveolar_nasal",
"s" => "voiceless_alveolar_fricative",

View file

@ -265,4 +265,51 @@ public sealed class StressAndReleaseRenderTests(ITestOutputHelper output)
Assert.True(peak > 0.004, "audible");
Assert.True(peak < vowel / 2, "under half the following vowel's peak, as /b/");
}
// The /b/ test over the alveolar stop, by its table entry (before the entry the phone was unsupported, and a hand script approximated it).
[Fact]
public void AlveolarStopBurstExistsOnlyAroundItsOwnRelease_t()
{
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_t", "t", 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, WithoutBurst);
if (withBurst is null || withoutBurst is null) return;
var burst = withBurst.Zip(withoutBurst, (x, y) => x - y).ToArray();
const double tFrom = 0.275, tTo = 0.37;
var atRelease = Peak(burst, tFrom + 0.095 * 0.6, tTo + 0.02);
var leading = Peak(burst, 0, tFrom);
var trailing = Peak(burst, tTo + 0.12, 0.77);
output.WriteLine($"/t/ burst peak {atRelease:F4} at the release, {leading:F5} before the plosive, {trailing:F5} after it");
Assert.True(atRelease > 0.004, "the burst is a transient of audible size");
Assert.True(leading < atRelease / 20, "no burst before the plosive's own gesture");
Assert.True(trailing < atRelease / 10, "no burst after the plosive");
}
// Chodroff and Wilson 2014: /t/ burst peak 5216 Hz, /k/ 2868. The band 3.5 to 7 kHz is a DESIGN tolerance around the /t/ figure.
// Peak of the isolated burst's spectrum from 1 kHz up (the weak_spots.burst_spectrum definition: 20 ms Hann window from the release, 400 Hz smoothing).
[Fact]
public void StopTBurstSpectrumIsInTheLiteratureBand()
{
var (x, from, to) = VowelConsonantVowel("t");
var (nb, _, _) = VowelConsonantVowel("t", edit: WithoutBurst);
if (x is null || nb is null) return;
var start = (int)((from + 0.65 * (to - from)) * Rate);
const int n = 960;
var seg = Enumerable.Range(0, n).Select(i => (x[start + i] - nb[start + i]) * (0.5 - 0.5 * Math.Cos(2 * Math.PI * i / (n - 1)))).ToArray();
double Power(double hz)
{
double re = 0, im = 0;
for (var i = 0; i < n; i++) { var a = 2 * Math.PI * hz * i / Rate; re += seg[i] * Math.Cos(a); im -= seg[i] * Math.Sin(a); }
return re * re + im * im;
}
var grid = Enumerable.Range(0, 180).Select(k => 1000.0 + 50 * k).ToArray();
var power = grid.Select(Power).ToArray();
var smooth = power.Select((_, k) => Enumerable.Range(Math.Max(0, k - 4), Math.Min(9, power.Length - Math.Max(0, k - 4))).Average(j => power[j])).ToArray();
var peak = grid[Array.IndexOf(smooth, smooth.Max())];
output.WriteLine($"/t/ burst spectral peak {peak:F0} Hz");
Assert.InRange(peak, 3500, 7000);
}
}

View file

@ -68,6 +68,20 @@ public sealed class WeksaPhonesTests
Assert.Empty(plan.Phones);
}
[Fact]
public void WeksaWordPlanAcceptsT()
{
var word = WeksaPhonesReader.Parse(Line($"[{Ph("ɑ", "none")},{Ph("t", "none")},{Ph("ɑ", "none")}]"));
var plan = WeksaWordPlan.From(word, "ata");
Assert.True(plan.Renderable);
Assert.Empty(plan.Unsupported);
// t names a plosive, not the approximant GestureDescriptor falls back to for a phone the table lacks
Assert.True(PatchScript.TryIpaDescriptor("t", out var descriptor));
Assert.Equal("voiceless_alveolar_plosive", descriptor);
var patch = PatchScript.Parse(IpaGestureExperiment.BuildWordScript(plan.Phones.ToArray(), 140));
Assert.Contains(patch.ControlSplines, s => s.SurfacePath == "/vocal/contacts/0/release_flow" && s.Name.StartsWith("ata_1_t", StringComparison.Ordinal));
}
[Fact]
public void NearCloseVowelsRenderAsVowelsWithTheirOwnStoryShape()
{