diff --git a/src/AquaSynth.Core/WeksaPhones.cs b/src/AquaSynth.Core/WeksaPhones.cs
index 92da8e3..4e21898 100644
--- a/src/AquaSynth.Core/WeksaPhones.cs
+++ b/src/AquaSynth.Core/WeksaPhones.cs
@@ -84,14 +84,16 @@ public sealed record WeksaWordPlan(IReadOnlyList<(IpaGestureExperimentTarget Tar
///
/// 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.
///
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)
};
diff --git a/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs b/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs
index 165be16..6c66326 100644
--- a/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs
+++ b/tests/AquaSynth.Dsl.Tests/StressAndReleaseRenderTests.cs
@@ -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();
+ 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);
+ }
}
diff --git a/tests/AquaSynth.Dsl.Tests/WeksaPhonesTests.cs b/tests/AquaSynth.Dsl.Tests/WeksaPhonesTests.cs
index cb5a3e7..34349af 100644
--- a/tests/AquaSynth.Dsl.Tests/WeksaPhonesTests.cs
+++ b/tests/AquaSynth.Dsl.Tests/WeksaPhonesTests.cs
@@ -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);