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);