Stress amplitude cue 1.26 to 1.30 (secondary 1.14): rendered primary stress +1.7 to +2.0 dB mean over five vowels, /i/ in /nibu/ +0.99 to +1.64 dB

The f0 rise alone moves the level -1.3 to +1.8 dB by vowel through the harmonic/F1 interaction, so the amplitude factor is held to the rendered result. Duration and f0 cues unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Aqua 2026-11-09 12:58:59 +00:00
commit b7c1dc0a41
3 changed files with 38 additions and 8 deletions

View file

@ -84,14 +84,16 @@ public sealed record WeksaWordPlan(IReadOnlyList<(IpaGestureExperimentTarget Tar
/// <summary>
/// Primary stress: duration 220/160 = 1.375 (stressed against unstressed syllable in initial-stress pairs, Sluijter and van
/// Heuven 1996), amplitude 2 dB = 1.26 (the 1-3 dB overall-intensity difference they report), f0 +6 percent (their unaccented
/// f0 difference). Secondary stress is placed halfway, in dB and in percent: a DESIGN VALUE, no secondary-stress measurement was
/// retrieved. Unheard.
/// Heuven 1996), amplitude 1.30 = 2.3 dB on the source flow, f0 +6 percent (their unaccented f0 difference). The amplitude is set
/// by the rendered result, not the nominal 2 dB (their 1-3 dB overall-intensity difference): the f0 rise alone moves the harmonics against
/// F1 and changes the rendered level by -1.3 to +1.8 dB by vowel (i -0.7, a -0.3, u -1.3, e -0.8, o +1.8), so 1.26 rendered +1.7 dB on
/// average over those five and 1.30 renders +2.0. Secondary stress is placed halfway, in dB and in percent: a DESIGN VALUE, no
/// secondary-stress measurement was retrieved. Unheard.
/// </summary>
public static StressCue CueFor(WeksaStress stress) => stress switch
{
WeksaStress.Primary => new(220f / 160f, 1.26f, 1.06f),
WeksaStress.Secondary => new(1.19f, 1.12f, 1.03f),
WeksaStress.Primary => new(220f / 160f, 1.30f, 1.06f),
WeksaStress.Secondary => new(1.19f, 1.14f, 1.03f),
_ => new(1f, 1f, 1f)
};

View file

@ -147,4 +147,32 @@ public sealed class StressAndReleaseRenderTests(ITestOutputHelper output)
output.WriteLine($"/{ipa}/ murmur {db:F1} dB re the flanking vowels");
Assert.InRange(db, -16, -8);
}
// The primary-stress cue as the plan applies it (amplitude and f0 ratio, same vowel length) against the unstressed vowel, over five vowels:
// the f0 rise alone moves the harmonics against F1 and the level by -1.3 to +1.8 dB by vowel, so the amplitude factor is held to the mean
// rendered result, 2 dB (Sluijter and van Heuven 1996: 1 to 3 dB overall). Measured 1.7 dB with the amplitude at 1.26 (outside the band), 2.0 dB at 1.30.
[Fact]
public void PrimaryStressRendersAboutTwoDecibelsLouderOnAverageOverVowels()
{
var cue = WeksaWordPlan.CueFor(WeksaStress.Primary);
var diffs = new List<double>();
foreach (var vowel in new[] { "i", "a", "u", "e", "o" })
{
double Level(float ratio, float intensity)
{
var script = IpaGestureExperiment.BuildWordScript([(new IpaGestureExperimentTarget($"w_0_{vowel}", vowel, "", 0.25f, PitchRatio: ratio), intensity)], 140, 0.2f);
var x = Render(script, 0.45f);
return x is null ? double.NaN : Rms(x, 0.12, 0.22);
}
var flat = Level(1f, 1f);
if (double.IsNaN(flat)) return;
var db = 20 * Math.Log10(Level(cue.PitchRatio, cue.Amplitude) / flat);
output.WriteLine($"/{vowel}/ stressed against unstressed: {db:F2} dB");
diffs.Add(db);
}
output.WriteLine($"mean {diffs.Average():F2} dB");
Assert.InRange(diffs.Average(), 1.8, 2.3);
}
}

View file

@ -105,11 +105,11 @@ public sealed class WeksaPhonesTests
Assert.Equal(1f, none[1].Intensity);
Assert.Equal(WeksaWordPlan.VowelSeconds * 220f / 160f, primary[1].Target.DurationSeconds, 5);
Assert.Equal(1.06f, primary[1].Target.PitchRatio);
Assert.Equal(1.26f, primary[1].Intensity);
Assert.Equal(1.30f, primary[1].Intensity);
// Secondary sits strictly between unstressed and primary on every cue.
Assert.InRange(secondary[1].Target.DurationSeconds, none[1].Target.DurationSeconds + 1e-4f, primary[1].Target.DurationSeconds - 1e-4f);
Assert.InRange(secondary[1].Target.PitchRatio, 1.001f, 1.059f);
Assert.InRange(secondary[1].Intensity, 1.001f, 1.259f);
Assert.InRange(secondary[1].Intensity, 1.001f, 1.299f);
}
[Fact]
@ -124,7 +124,7 @@ public sealed class WeksaPhonesTests
// pitch_ratio surface range 0.5..2: normalized (1.06 - .5) / 1.5; unstressed holds 1 -> (1 - .5) / 1.5.
Assert.Equal((1.06f - .5f) / 1.5f, First(primary, "w_1_i_source_pitch", "/vocal/sources/0/pitch_ratio"), 4);
Assert.Equal((1f - .5f) / 1.5f, First(none, "w_1_i_source_pitch", "/vocal/sources/0/pitch_ratio"), 4);
Assert.Equal(First(none, "w_1_i_source_level", "/vocal/sources/0/flow_scale") * 1.26f, First(primary, "w_1_i_source_level", "/vocal/sources/0/flow_scale"), 4);
Assert.Equal(First(none, "w_1_i_source_level", "/vocal/sources/0/flow_scale") * 1.30f, First(primary, "w_1_i_source_level", "/vocal/sources/0/flow_scale"), 4);
// The b after the stressed vowel starts later by the vowel's extra length.
float Start(SynthPatch p) => p.PhonemeGestures.Single(g => g.Ipa == "b").StartSeconds;
Assert.Equal(WeksaWordPlan.VowelSeconds * (220f / 160f - 1f), Start(primary) - Start(none), 4);