From 264bb77298568a6f180e64939ea22e097c7f5b84 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sat, 7 May 2022 10:49:45 +0800 Subject: [PATCH 1/4] Let the test project able to access the internal method. --- osu.Framework.Font/osu.Framework.Font.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Framework.Font/osu.Framework.Font.csproj b/osu.Framework.Font/osu.Framework.Font.csproj index 20eb360..d681bc6 100644 --- a/osu.Framework.Font/osu.Framework.Font.csproj +++ b/osu.Framework.Font/osu.Framework.Font.csproj @@ -32,4 +32,9 @@ + + + <_Parameter1>osu.Framework.Font.Tests + + From 90f0e55bd421e6f7dfb73e4a79cbca5f2cacc557 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sat, 7 May 2022 12:41:52 +0800 Subject: [PATCH 2/4] Add all the utils that needed in this PR. --- .../Utils/TextIndexUtilsTest.cs | 46 +++++++++++++++++++ osu.Framework.Font/Utils/TextIndexUtils.cs | 27 +++++++++++ 2 files changed, 73 insertions(+) diff --git a/osu.Framework.Font.Tests/Utils/TextIndexUtilsTest.cs b/osu.Framework.Font.Tests/Utils/TextIndexUtilsTest.cs index 859fc68..d402be1 100644 --- a/osu.Framework.Font.Tests/Utils/TextIndexUtilsTest.cs +++ b/osu.Framework.Font.Tests/Utils/TextIndexUtilsTest.cs @@ -29,5 +29,51 @@ public void TestClamp(int index, TextIndex.IndexState state, int minIndex, int m Assert.Throws(() => TextIndexUtils.Clamp(textIndex, minIndex, maxIndex)); } } + + [TestCase(0, TextIndex.IndexState.Start, 0)] + [TestCase(0, TextIndex.IndexState.End, 1)] + [TestCase(-1, TextIndex.IndexState.Start, -1)] // In utils not checking is index out of range + [TestCase(-1, TextIndex.IndexState.End, 0)] + public void TestToStringIndex(int index, TextIndex.IndexState state, int expected) + { + var textIndex = new TextIndex(index, state); + + int actual = TextIndexUtils.ToStringIndex(textIndex); + Assert.AreEqual(expected, actual); + } + + [TestCase(TextIndex.IndexState.Start, TextIndex.IndexState.End)] + [TestCase(TextIndex.IndexState.End, TextIndex.IndexState.Start)] + public void TestReverseState(TextIndex.IndexState state, TextIndex.IndexState expected) + { + var actual = TextIndexUtils.ReverseState(state); + Assert.AreEqual(expected, actual); + } + + [TestCase(1, TextIndex.IndexState.End, 1, TextIndex.IndexState.Start)] + [TestCase(1, TextIndex.IndexState.Start, 0, TextIndex.IndexState.End)] + [TestCase(0, TextIndex.IndexState.Start, -1, TextIndex.IndexState.End)] // didn't care about negative value. + [TestCase(-1, TextIndex.IndexState.End, -1, TextIndex.IndexState.Start)] // didn't care about negative value. + public void TestGetPreviousIndex(int index, TextIndex.IndexState state, int expectedIndex, TextIndex.IndexState expectedState) + { + var textIndex = new TextIndex(index, state); + + var expected = new TextIndex(expectedIndex, expectedState); + var actual = TextIndexUtils.GetPreviousIndex(textIndex); + Assert.AreEqual(expected, actual); + } + + [TestCase(0, TextIndex.IndexState.Start, 0, TextIndex.IndexState.End)] + [TestCase(0, TextIndex.IndexState.End, 1, TextIndex.IndexState.Start)] + [TestCase(-1, TextIndex.IndexState.Start, -1, TextIndex.IndexState.End)] // didn't care about negative value. + [TestCase(-1, TextIndex.IndexState.End, 0, TextIndex.IndexState.Start)] // didn't care about negative value. + public void TestGetNextIndex(int index, TextIndex.IndexState state, int expectedIndex, TextIndex.IndexState expectedState) + { + var textIndex = new TextIndex(index, state); + + var expected = new TextIndex(expectedIndex, expectedState); + var actual = TextIndexUtils.GetNextIndex(textIndex); + Assert.AreEqual(expected, actual); + } } } diff --git a/osu.Framework.Font/Utils/TextIndexUtils.cs b/osu.Framework.Font/Utils/TextIndexUtils.cs index 6fa03d2..3240a8f 100644 --- a/osu.Framework.Font/Utils/TextIndexUtils.cs +++ b/osu.Framework.Font/Utils/TextIndexUtils.cs @@ -12,5 +12,32 @@ public static TextIndex Clamp(TextIndex value, int minValue, int maxValue) { return new TextIndex(Math.Clamp(value.Index, minValue, maxValue), value.State); } + + public static int ToStringIndex(TextIndex index) + { + if (index.State == TextIndex.IndexState.Start) + return index.Index; + + return index.Index + 1; + } + + public static TextIndex.IndexState ReverseState(TextIndex.IndexState state) + { + return state == TextIndex.IndexState.Start ? TextIndex.IndexState.End : TextIndex.IndexState.Start; + } + + public static TextIndex GetPreviousIndex(TextIndex originIndex) + { + int previousIndex = ToStringIndex(originIndex) - 1; + var previousState = ReverseState(originIndex.State); + return new TextIndex(previousIndex, previousState); + } + + public static TextIndex GetNextIndex(TextIndex originIndex) + { + int nextIndex = ToStringIndex(originIndex); + var nextState = ReverseState(originIndex.State); + return new TextIndex(nextIndex, nextState); + } } } From 67bd8b404b74a9dfc6c300bf853fdeb7b3d57b72 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sat, 7 May 2022 10:53:08 +0800 Subject: [PATCH 3/4] Implement the basic test case and adjust the function for testing. --- .../Graphics/Sprites/KaraokeSpriteTextTest.cs | 27 +++++++++++++++++++ .../Graphics/Sprites/KaraokeSpriteText.cs | 15 ++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs diff --git a/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs b/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs new file mode 100644 index 0000000..41a603b --- /dev/null +++ b/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs @@ -0,0 +1,27 @@ +// Copyright (c) karaoke.dev . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Font.Tests.Helper; +using osu.Framework.Graphics.Sprites; + +namespace osu.Framework.Font.Tests.Graphics.Sprites +{ + public class KaraokeSpriteTextTest + { + [TestCase(new[] { "[0,start]:500", "[1,start]:600", "[2,start]:1000", "[3,start]:1500" }, new[] { "[0,start]:500", "[1,start]:600", "[2,start]:1000", "[3,start]:1500" })] // there's no need to add the interpolation time-tags. + public void TestGetInterpolatedTimeTags(string[] timeTags, string[] expectedTimeTags) + { + var karaokeSpriteText = new KaraokeSpriteText + { + Text = "カラオケ", + TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags) + }; + + var expectedInterpolatedTimeTags = TestCaseTagHelper.ParseTimeTags(expectedTimeTags); + var actualInterpolatedTimeTags = karaokeSpriteText.GetInterpolatedTimeTags(); + + Assert.AreEqual(expectedInterpolatedTimeTags, actualInterpolatedTimeTags); + } + } +} diff --git a/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs b/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs index 14559db..cbcdbdf 100644 --- a/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs +++ b/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs @@ -447,9 +447,7 @@ public virtual void RefreshStateTransforms() rightLyricTextContainer.ClearTransforms(); // filter valid time-tag with order. - var validTimeTag = TimeTags - .Where(x => x.Value.Index >= 0 && x.Value.Index < Text.Length) - .OrderBy(x => x.Key).ToArray(); + var validTimeTag = GetInterpolatedTimeTags(); // not initialize if no time-tag or text. var hasTimeTag = validTimeTag.Any(); @@ -489,6 +487,17 @@ public virtual void RefreshStateTransforms() } } + internal IReadOnlyDictionary GetInterpolatedTimeTags() + { + var orderedTimeTags = TimeTags + .Where(x => x.Value.Index >= 0 && x.Value.Index < Text.Length) + .OrderBy(x => x.Key).ToArray(); + + // todo: do the algorithm. + + return orderedTimeTags.ToDictionary(k => k.Key, v => v.Value); + } + private float getTextIndexPosition(TextIndex index) { var leftTextIndexPosition = leftLyricText.GetTextIndexPosition(index); From f09e527bdcbe4617bc0795cd96573b6ed1bfbd20 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sat, 7 May 2022 11:43:08 +0800 Subject: [PATCH 4/4] Implement the interpolation algorithm and add the test case. --- .../Graphics/Sprites/KaraokeSpriteTextTest.cs | 23 +++++++++- .../Graphics/Sprites/KaraokeSpriteText.cs | 45 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs b/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs index 41a603b..5baad15 100644 --- a/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs +++ b/osu.Framework.Font.Tests/Graphics/Sprites/KaraokeSpriteTextTest.cs @@ -9,7 +9,28 @@ namespace osu.Framework.Font.Tests.Graphics.Sprites { public class KaraokeSpriteTextTest { - [TestCase(new[] { "[0,start]:500", "[1,start]:600", "[2,start]:1000", "[3,start]:1500" }, new[] { "[0,start]:500", "[1,start]:600", "[2,start]:1000", "[3,start]:1500" })] // there's no need to add the interpolation time-tags. + [TestCase(new[] { "[0,start]:500", "[0,end]:600" }, + new[] { "[0,start]:500", "[0,end]:600" })] // there's no need to add the interpolation time-tags. + [TestCase(new[] { "[-1,start]:500", "[4,start]:600" }, + new string[] { })] // will filter those out-of-range time-tags. + [TestCase(new[] { "[0,start]:500", "[1,start]:501" }, + new[] { "[0,start]:500", "[1,start]:501" })] // there's no need to add the interpolation time-tags because timing is too small. + [TestCase(new[] { "[0,start]:500", "[2,start]:600" }, + new[] { "[0,start]:500", "[1,end]:599", "[2,start]:600" })] // It's time to add the interpolation time-tags. + [TestCase(new[] { "[0,end]:500", "[1,end]:600" }, + new[] { "[0,end]:500", "[1,start]:501", "[1,end]:600" })] + [TestCase(new[] { "[0,end]:500", "[2,start]:600" }, + new[] { "[0,end]:500", "[1,start]:501", "[1,end]:599", "[2,start]:600" })] + [TestCase(new[] { "[0,end]:500", "[3,start]:600" }, + new[] { "[0,end]:500", "[1,start]:501", "[2,end]:599", "[3,start]:600" })] + [TestCase(new[] { "[2,start]:500", "[0,start]:600" }, + new[] { "[2,start]:500", "[1,end]:501", "[0,start]:600" })] // let's test some reverse state + [TestCase(new[] { "[1,end]:500", "[0,end]:600" }, + new[] { "[1,end]:500", "[1,start]:599", "[0,end]:600" })] + [TestCase(new[] { "[2,start]:500", "[0,end]:600" }, + new[] { "[2,start]:500", "[1,end]:501", "[1,start]:599", "[0,end]:600" })] + [TestCase(new[] { "[3,start]:500", "[0,end]:600" }, + new[] { "[3,start]:500", "[2,end]:501", "[1,start]:599", "[0,end]:600" })] public void TestGetInterpolatedTimeTags(string[] timeTags, string[] expectedTimeTags) { var karaokeSpriteText = new KaraokeSpriteText diff --git a/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs b/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs index cbcdbdf..f48f1c4 100644 --- a/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs +++ b/osu.Framework.Font/Graphics/Sprites/KaraokeSpriteText.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shaders; using osu.Framework.Layout; +using osu.Framework.Utils; using osuTK; using osuTK.Graphics; @@ -20,6 +21,8 @@ public class KaraokeSpriteText : KaraokeSpriteText public partial class KaraokeSpriteText : CompositeDrawable, ISingleShaderBufferedDrawable, IHasRuby, IHasRomaji where T : LyricSpriteText, new() { + internal const double INTERPOLATION_TIMING = 1; + private readonly MaskingContainer leftLyricTextContainer; private readonly T leftLyricText; @@ -493,9 +496,47 @@ internal IReadOnlyDictionary GetInterpolatedTimeTags() .Where(x => x.Value.Index >= 0 && x.Value.Index < Text.Length) .OrderBy(x => x.Key).ToArray(); - // todo: do the algorithm. + return orderedTimeTags.Aggregate(new Dictionary(), (collections, lastTimeTag) => + { + if (collections.Count == 0) + { + collections.Add(lastTimeTag.Key, lastTimeTag.Value); + return collections; + } + + foreach (var (time, textIndex) in getInterpolatedTimeTagBetweenTwoTimeTag(collections.LastOrDefault(), lastTimeTag)) + { + collections.Add(time, textIndex); + } + + collections.Add(lastTimeTag.Key, lastTimeTag.Value); + return collections; + }); - return orderedTimeTags.ToDictionary(k => k.Key, v => v.Value); + IEnumerable> getInterpolatedTimeTagBetweenTwoTimeTag(KeyValuePair firstTimeTag, KeyValuePair secondTimeTag) + { + // we should not add the interpolation if timing is too small between two time-tags. + var firstTimeTagTime = firstTimeTag.Key; + var secondTimeTagTime = secondTimeTag.Key; + if (Math.Abs(firstTimeTagTime - secondTimeTagTime) <= INTERPOLATION_TIMING * 2) + yield break; + + // there's no need to add the interpolation if index are the same. + if (firstTimeTag.Value.Index == secondTimeTag.Value.Index) + yield break; + + var firstTimeTagIndex = firstTimeTag.Value; + var secondTimeTagIndex = secondTimeTag.Value; + var isLarger = firstTimeTag.Value < secondTimeTag.Value; + + var firstInterpolatedTimeTagIndex = isLarger ? TextIndexUtils.GetNextIndex(firstTimeTagIndex) : TextIndexUtils.GetPreviousIndex(firstTimeTagIndex); + if (firstInterpolatedTimeTagIndex.Index != firstTimeTagIndex.Index) + yield return new KeyValuePair(firstTimeTagTime + INTERPOLATION_TIMING, firstInterpolatedTimeTagIndex); + + var secondInterpolatedTimeTag = isLarger ? TextIndexUtils.GetPreviousIndex(secondTimeTagIndex) : TextIndexUtils.GetNextIndex(secondTimeTagIndex); + if (secondInterpolatedTimeTag.Index != secondTimeTagIndex.Index) + yield return new KeyValuePair(secondTimeTagTime - INTERPOLATION_TIMING, secondInterpolatedTimeTag); + } } private float getTextIndexPosition(TextIndex index)