From 3c75fcd1c301fb1a81066531262ad84f9b6b7081 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sun, 21 Jul 2024 17:55:33 +0800 Subject: [PATCH 1/7] Lyric should add the start time. --- LrcParser/Model/Lyric.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LrcParser/Model/Lyric.cs b/LrcParser/Model/Lyric.cs index 6be44bb..b733f94 100644 --- a/LrcParser/Model/Lyric.cs +++ b/LrcParser/Model/Lyric.cs @@ -10,6 +10,11 @@ public class Lyric /// public string Text { get; set; } = string.Empty; + /// + /// Start time of the lyric. + /// + public int StartTime { get; set; } + /// /// Time tags /// From 1847c6ed27f2013596463dbe1820de8c2e4314a0 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sun, 21 Jul 2024 18:49:13 +0800 Subject: [PATCH 2/7] todo: refactor all test cases. --- LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs b/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs index 333bf3e..61dd7d5 100644 --- a/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs +++ b/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs @@ -13,7 +13,8 @@ namespace LrcParser.Tests.Parser.Lrc.Lines; public class LrcLyricParserTest : BaseSingleLineParserTest { [TestCase("[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", true)] - [TestCase("karaoke", true)] + [TestCase("[00:17:97]<00:00.00>帰<00:00.00>り<00:00.00>道<00:00.00>は<00:00.00>", true)] + [TestCase("karaoke", true)] // depends on the config, might be parsed but no time, or being ignored. [TestCase("", false)] [TestCase(null, false)] public void TestCanDecode(string text, bool expected) From 7dee0c69e3947f158a3c7f1e55284cc992a448d8 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Thu, 25 Jul 2024 21:39:38 +0800 Subject: [PATCH 3/7] Re-write the lrc lyric. --- LrcParser/Parser/Lrc/Metadata/LrcLyric.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/LrcParser/Parser/Lrc/Metadata/LrcLyric.cs b/LrcParser/Parser/Lrc/Metadata/LrcLyric.cs index 3d12e20..911d27a 100644 --- a/LrcParser/Parser/Lrc/Metadata/LrcLyric.cs +++ b/LrcParser/Parser/Lrc/Metadata/LrcLyric.cs @@ -17,12 +17,21 @@ public LrcLyric() public string Text { get; set; } = string.Empty; /// - /// Time tags + /// Start times for the lyrics. + /// Because lrc format allows multiple start times for a single line, so it is an array. + /// + public int[] StartTimes { get; set; } = []; + + /// + /// Time tags. + /// It's the relative time from the start time. /// public SortedDictionary TimeTags { get; set; } = new(); public bool Equals(LrcLyric other) { - return Text == other.Text && TimeTags.SequenceEqual(other.TimeTags); + return Text == other.Text + && StartTimes.SequenceEqual(other.StartTimes) + && TimeTags.SequenceEqual(other.TimeTags); } } From 822fc6007b24d0ab3fa18fb3c4446a185b287680 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sun, 28 Jul 2024 14:13:59 +0800 Subject: [PATCH 4/7] Implement the utility for able to convert the prefix start time-tag. --- .../Parser/Lrc/Utils/LrcStartTimeUtilsTest.cs | 105 ++++++++++++++++++ .../Parser/Lrc/Utils/LrcStartTimeUtils.cs | 97 ++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 LrcParser.Tests/Parser/Lrc/Utils/LrcStartTimeUtilsTest.cs create mode 100644 LrcParser/Parser/Lrc/Utils/LrcStartTimeUtils.cs diff --git a/LrcParser.Tests/Parser/Lrc/Utils/LrcStartTimeUtilsTest.cs b/LrcParser.Tests/Parser/Lrc/Utils/LrcStartTimeUtilsTest.cs new file mode 100644 index 0000000..8a97058 --- /dev/null +++ b/LrcParser.Tests/Parser/Lrc/Utils/LrcStartTimeUtilsTest.cs @@ -0,0 +1,105 @@ +// Copyright (c) karaoke.dev . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using LrcParser.Parser.Lrc.Utils; +using NUnit.Framework; + +namespace LrcParser.Tests.Parser.Lrc.Utils; + +public class LrcStartTimeUtilsTest +{ + #region Decode + + [TestCase("[1:00.00] ", new[] { 60000 }, "")] + [TestCase("[1:00.00][1:02.00] Lyric", new[] { 60000, 62000 }, "Lyric")] + [TestCase("[1:00.00]Lyric", new[] { 60000 }, "Lyric")] // With no spacing. + [TestCase("[1:00.00] Lyric", new[] { 60000 }, "Lyric")] // With lots of spacing. + [TestCase("[1:00.00] <00:00.04> Lyric <00:00.16>", new[] { 60000 }, "<00:00.04> Lyric <00:00.16>")] // With time-tag. + [TestCase("[1:00.00] <00:00.04> Lyric", new[] { 60000 }, "<00:00.04> Lyric")] // With time-tag. + [TestCase("[1:00.00] <00:00.04> Lyric ", new[] { 60000 }, "<00:00.04> Lyric")] // Remove the end spacing. + public void TestDecodeWithValidLine(string line, int[] expectedStartTimes, string lyric) + { + var actual = LrcStartTimeUtils.SplitLyricAndTimeTag(line); + + Assert.That(actual.Item1, Is.EqualTo(expectedStartTimes)); + Assert.That(actual.Item2, Is.EqualTo(lyric)); + } + + [TestCase("Lyric", new int[] { }, "Lyric")] // With no start time. + [TestCase(" Lyric", new int[] { }, "Lyric")] // With no start time. + [TestCase("<00:00.04> Lyric <00:00.16>", new int[] { }, "<00:00.04> Lyric <00:00.16>")] // With no start time but with time-tag. + public void TestDecodeWithInvalidLine(string line, int[] expectedStartTimes, string lyric) + { + var actual = LrcStartTimeUtils.SplitLyricAndTimeTag(line); + + // still return the value, but let outside handle the invalid value. + Assert.That(actual.Item1, Is.EqualTo(expectedStartTimes)); + Assert.That(actual.Item2, Is.EqualTo(lyric)); + } + + [TestCase("[00:00.00]", 0)] + [TestCase("[00:06.00]", 6000)] + [TestCase("[01:00.00]", 60000)] + [TestCase("[10:00.00]", 600000)] + [TestCase("[100:00.00]", 6000000)] + [TestCase("[0:00.00]", 0)] // prevent throw error in some invalid format. + [TestCase("[0:0.0]", 0)] // prevent throw error in some invalid format. + [TestCase("[1:00.00][1:02.00]", 60000)] // rarely to get this case, so return the first one. + public void TestConvertTimeTagToMillionSecond(string timeTag, int expectedMillionSecond) + { + var actual = LrcStartTimeUtils.ConvertTimeTagToMillionSecond(timeTag); + + Assert.That(actual, Is.EqualTo(expectedMillionSecond)); + } + + [TestCase("[--:--.--]")] + [TestCase("[]")] + [TestCase("<1:00.00>")] // should not contains embedded time-tag. + public void TestConvertTimeTagToMillionSecondWithInvalidValue(string timeTag) + { + Assert.Throws(() => LrcStartTimeUtils.ConvertTimeTagToMillionSecond(timeTag)); + } + + #endregion + + #region Encode + + [TestCase(new[] { 60000 }, "Lyric", "[01:00.00] Lyric")] + [TestCase(new[] { 60000, 62000 }, "Lyric", "[01:00.00][01:02.00] Lyric")] + [TestCase(new[] { 60000 }, "<00:00.04> Lyric <00:00.16>", "[01:00.00] <00:00.04> Lyric <00:00.16>")] // With time-tag. + [TestCase(new[] { 60000 }, " Lyric", "[01:00.00] Lyric")] // Start spacing will be removed automatically. + public void TestEncodeWithValidValue(int[] startTimes, string lyric, string expectedLine) + { + var actual = LrcStartTimeUtils.JoinLyricAndTimeTag(startTimes, lyric); + + Assert.That(actual, Is.EqualTo(expectedLine)); + } + + [TestCase(new int[] { }, "Lyric")] // With no start time. + [TestCase(new int[] { }, "[00:00.00] Lyric")] // Lyric should not contains any start time-tag info. + public void TestEncodeWithInvalidValue(int[] startTimes, string expectedLine) + { + Assert.Throws(() => LrcStartTimeUtils.JoinLyricAndTimeTag(startTimes, expectedLine)); + } + + [TestCase(0, "[00:00.00]")] + [TestCase(6000, "[00:06.00]")] + [TestCase(60000, "[01:00.00]")] + [TestCase(600000, "[10:00.00]")] + [TestCase(6000000, "[100:00.00]")] + public void TestConvertMillionSecondToTimeTag(int millionSecond, string expectedTimeTag) + { + var actual = LrcStartTimeUtils.ConvertMillionSecondToTimeTag(millionSecond); + + Assert.That(actual, Is.EqualTo(expectedTimeTag)); + } + + [TestCase(-1)] + public void TestConvertMillionSecondToTimeTagWithInvalidValue(int millionSecond) + { + Assert.Throws(() => LrcStartTimeUtils.ConvertMillionSecondToTimeTag(millionSecond)); + } + + #endregion +} diff --git a/LrcParser/Parser/Lrc/Utils/LrcStartTimeUtils.cs b/LrcParser/Parser/Lrc/Utils/LrcStartTimeUtils.cs new file mode 100644 index 0000000..3039b93 --- /dev/null +++ b/LrcParser/Parser/Lrc/Utils/LrcStartTimeUtils.cs @@ -0,0 +1,97 @@ +// Copyright (c) karaoke.dev . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Text.RegularExpressions; + +namespace LrcParser.Parser.Lrc.Utils; + +public class LrcStartTimeUtils +{ + // technically should be @"\[(\d{2,}):(\d{2})\.(\d{2})\]", but might be small case that start time format is invalid. + private static readonly Regex start_time_regex = new(@"\[(\d{1,}):(\d{1,})\.(\d{1,})\]"); + + /// + /// Separate the lyric format from [100:00.00][100:02.00] When the truth is found to be lies. + /// to: + /// [100:00.00][100:02.00] + /// When the truth is found to be lies + /// + /// + /// + internal static Tuple SplitLyricAndTimeTag(string line) + { + if (string.IsNullOrWhiteSpace(line)) + return new Tuple([], string.Empty); + + // get all matched startTime + MatchCollection matches = start_time_regex.Matches(line); + + var startTimes = matches.Select(x => ConvertTimeTagToMillionSecond(x.Value)).ToArray(); + var lyric = start_time_regex.Replace(line, "").Trim(); + + return new Tuple(startTimes, lyric); + } + + /// + /// Convert the [1:00.00] to 60000 + /// + /// + /// + internal static int ConvertTimeTagToMillionSecond(string timeTag) + { + Match match = start_time_regex.Match(timeTag); + + if (!match.Success) + throw new InvalidOperationException("Time tag format is invalid."); + + int minutes = int.Parse(match.Groups[1].Value); + int seconds = int.Parse(match.Groups[2].Value); + int hundredths = int.Parse(match.Groups[3].Value); + + return minutes * 60 * 1000 + seconds * 1000 + hundredths * 10; + } + + /// + /// Combine the lyric format from: + /// [60000, 66000] + /// When the truth is found to be lies + /// to: + /// [01:00.00][01:06.00] When the truth is found to be lies + /// + /// + /// + /// + internal static string JoinLyricAndTimeTag(int[] startTimes, string lyric) + { + if (startTimes.Any() == false) + throw new InvalidOperationException("Should contains at least one start time."); + + if (start_time_regex.Matches(lyric).Any()) + throw new InvalidOperationException("lyric should not contains any start time-tag info."); + + if (startTimes.Length == 0) + return lyric; + + var result = startTimes.Aggregate(string.Empty, (current, t) => current + ConvertMillionSecondToTimeTag(t)); + + return result + " " + lyric.Trim(); + } + + /// + /// Convert the 60000 to [1:00.00] + /// + /// + /// + internal static string ConvertMillionSecondToTimeTag(int milliseconds) + { + if (milliseconds < 0) + throw new InvalidOperationException($"{nameof(milliseconds)} should be greater than 0."); + + int totalSeconds = milliseconds / 1000; + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + int hundredths = (milliseconds % 1000) / 10; + + return $"[{minutes:D2}:{seconds:D2}.{hundredths:D2}]"; + } +} From e95977f8c997898b78573385f10964ede3ac8b07 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sun, 28 Jul 2024 14:33:36 +0800 Subject: [PATCH 5/7] Lyric time-tag format should be "" not, "[mm:ss:xx]" --- .../Parser/Lrc/Utils/LrcTimedTextUtilsTest.cs | 79 ++++++++++++++++--- .../Parser/Lrc/Utils/TimeTagUtilsTest.cs | 26 ------ .../Parser/Lrc/Utils/LrcTimedTextUtils.cs | 52 +++++++++++- LrcParser/Parser/Lrc/Utils/TimeTagUtils.cs | 46 ----------- 4 files changed, 116 insertions(+), 87 deletions(-) delete mode 100644 LrcParser.Tests/Parser/Lrc/Utils/TimeTagUtilsTest.cs delete mode 100644 LrcParser/Parser/Lrc/Utils/TimeTagUtils.cs diff --git a/LrcParser.Tests/Parser/Lrc/Utils/LrcTimedTextUtilsTest.cs b/LrcParser.Tests/Parser/Lrc/Utils/LrcTimedTextUtilsTest.cs index 97af4c2..464f71b 100644 --- a/LrcParser.Tests/Parser/Lrc/Utils/LrcTimedTextUtilsTest.cs +++ b/LrcParser.Tests/Parser/Lrc/Utils/LrcTimedTextUtilsTest.cs @@ -10,12 +10,13 @@ namespace LrcParser.Tests.Parser.Lrc.Utils; public class LrcTimedTextUtilsTest { - [TestCase("[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", "帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" })] - [TestCase(" [00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", " 帰り道は", new[] { "[1,start]:17970", "[2,start]:18370", "[3,start]:18550", "[4,start]:18940", "[4,end]:19220" })] - [TestCase("[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22] ", "帰り道は ", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" })] - [TestCase("帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", "帰り道は", new[] { "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" })] - [TestCase("[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は", "帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940" })] - [TestCase("[00:51.00][01:29.99][01:48.29][02:31.00][02:41.99]You gotta fight !", "You gotta fight !", new[] { "[0,start]:51000" })] // decode with invalid format. + #region Decode + + [TestCase("<00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22>", "帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" })] + [TestCase(" <00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22>", " 帰り道は", new[] { "[1,start]:17970", "[2,start]:18370", "[3,start]:18550", "[4,start]:18940", "[4,end]:19220" })] + [TestCase("<00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22> ", "帰り道は ", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" })] + [TestCase("帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22>", "帰り道は", new[] { "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" })] + [TestCase("<00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は", "帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940" })] [TestCase("帰り道は", "帰り道は", new string[] { })] [TestCase("", "", new string[] { })] [TestCase(null, "", new string[] { })] @@ -27,11 +28,47 @@ public void TestDecode(string text, string expectedText, string[] expectedTimeTa Assert.That(actualTimeTags, Is.EqualTo(TestCaseTagHelper.ParseTimeTags(expectedTimeTags))); } - [TestCase("帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" }, "[00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22]")] - [TestCase(" 帰り道は", new[] { "[1,start]:17970", "[2,start]:18370", "[3,start]:18550", "[4,start]:18940", "[4,end]:19220" }, " [00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22]")] - [TestCase("帰り道は ", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" }, "[00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22] ")] - [TestCase("帰り道は", new[] { "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" }, "帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22]")] - [TestCase("帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940" }, "[00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は")] + [TestCase("<00:51.00><01:29.99><01:48.29><02:31.00><02:41.99>You gotta fight !", "You gotta fight !", new[] { "[0,start]:51000" })] // decode with invalid format. + public void TestDecodeWithInvalidFormat(string text, string expectedText, string[] expectedTimeTags) + { + var (actualText, actualTimeTags) = LrcTimedTextUtils.TimedTextToObject(text); + + Assert.That(actualText, Is.EqualTo(expectedText)); + Assert.That(actualTimeTags, Is.EqualTo(TestCaseTagHelper.ParseTimeTags(expectedTimeTags))); + } + + [TestCase("<00:00.00>", 0)] + [TestCase("<00:06.00>", 6000)] + [TestCase("<01:00.00>", 60000)] + [TestCase("<10:00.00>", 600000)] + [TestCase("<100:00.00>", 6000000)] + [TestCase("<0:00.00>", 0)] // prevent throw error in some invalid format. + [TestCase("<0:0.0>", 0)] // prevent throw error in some invalid format. + [TestCase("<1:00.00><1:02.00>", 60000)] // rarely to get this case, so return the first one. + public void TestConvertTimeTagToMillionSecond(string timeTag, int expectedMillionSecond) + { + var actual = LrcTimedTextUtils.ConvertTimeTagToMillionSecond(timeTag); + + Assert.That(actual, Is.EqualTo(expectedMillionSecond)); + } + + [TestCase("<--:--.-->")] + [TestCase("<>")] + [TestCase("[1:00.00]")] // should not contains start time-tag. + public void TestConvertTimeTagToMillionSecondWithInvalidValue(string timeTag) + { + Assert.Throws(() => LrcTimedTextUtils.ConvertTimeTagToMillionSecond(timeTag)); + } + + #endregion + + #region Encode + + [TestCase("帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" }, "<00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22>")] + [TestCase(" 帰り道は", new[] { "[1,start]:17970", "[2,start]:18370", "[3,start]:18550", "[4,start]:18940", "[4,end]:19220" }, " <00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22>")] + [TestCase("帰り道は ", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" }, "<00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22> ")] + [TestCase("帰り道は", new[] { "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220" }, "帰<00:18.37>り<00:18.55>道<00:18.94>は<00:19.22>")] + [TestCase("帰り道は", new[] { "[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940" }, "<00:17.97>帰<00:18.37>り<00:18.55>道<00:18.94>は")] [TestCase("帰り道は", new string[] { }, "帰り道は")] [TestCase("", new string[] { }, "")] public void TestEncode(string text, string[] timeTags, string expected) @@ -40,4 +77,24 @@ public void TestEncode(string text, string[] timeTags, string expected) Assert.That(actual, Is.EqualTo(expected)); } + + [TestCase(0, "<00:00.00>")] + [TestCase(6000, "<00:06.00>")] + [TestCase(60000, "<01:00.00>")] + [TestCase(600000, "<10:00.00>")] + [TestCase(6000000, "<100:00.00>")] + public void TestConvertMillionSecondToTimeTag(int millionSecond, string expectedTimeTag) + { + var actual = LrcTimedTextUtils.ConvertMillionSecondToTimeTag(millionSecond); + + Assert.That(actual, Is.EqualTo(expectedTimeTag)); + } + + [TestCase(-1)] + public void TestConvertMillionSecondToTimeTagWithInvalidValue(int millionSecond) + { + Assert.Throws(() => LrcTimedTextUtils.ConvertMillionSecondToTimeTag(millionSecond)); + } + + #endregion } diff --git a/LrcParser.Tests/Parser/Lrc/Utils/TimeTagUtilsTest.cs b/LrcParser.Tests/Parser/Lrc/Utils/TimeTagUtilsTest.cs deleted file mode 100644 index 38d53fc..0000000 --- a/LrcParser.Tests/Parser/Lrc/Utils/TimeTagUtilsTest.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) karaoke.dev . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using LrcParser.Parser.Lrc.Utils; -using NUnit.Framework; - -namespace LrcParser.Tests.Parser.Lrc.Utils; - -public class TimeTagUtilsTest -{ - [TestCase("[00:01:00]", 1000)] - public void TestTimeTagToMillionSecond(string timeTag, int millionSecond) - { - var actual = TimeTagUtils.TimeTagToMillionSecond(timeTag); - - Assert.That(actual, Is.EqualTo(millionSecond)); - } - - [TestCase(1000, "[00:01.00]")] - public void TestTimeTagToMillionSecond(int millionSecond, string timeTag) - { - var actual = TimeTagUtils.MillionSecondToTimeTag(millionSecond); - - Assert.That(actual, Is.EqualTo(timeTag)); - } -} diff --git a/LrcParser/Parser/Lrc/Utils/LrcTimedTextUtils.cs b/LrcParser/Parser/Lrc/Utils/LrcTimedTextUtils.cs index 6ce012b..1f418ec 100644 --- a/LrcParser/Parser/Lrc/Utils/LrcTimedTextUtils.cs +++ b/LrcParser/Parser/Lrc/Utils/LrcTimedTextUtils.cs @@ -9,13 +9,20 @@ namespace LrcParser.Parser.Lrc.Utils; internal static class LrcTimedTextUtils { + // technically should be @"\<(\d{2,}):(\d{2})\.(\d{2})\>", but might be small case that start time format is invalid. + private static readonly Regex start_time_regex = new(@"\<(\d{1,}):(\d{1,})\.(\d{1,})\>"); + + /// + /// + /// + /// + /// internal static Tuple> TimedTextToObject(string timedText) { if (string.IsNullOrEmpty(timedText)) return new Tuple>("", new SortedDictionary()); - var timeTagRegex = new Regex(@"\[\d\d:\d\d[:.]\d\d\]"); - var matchTimeTags = timeTagRegex.Matches(timedText); + var matchTimeTags = start_time_regex.Matches(timedText); var endTextIndex = timedText.Length; @@ -43,7 +50,7 @@ internal static Tuple> TimedTextToObjec var state = hasText && !isEmptyStringNext ? IndexState.Start : IndexState.End; var textIndex = text.Length - (state == IndexState.Start ? 0 : 1); - var time = TimeTagUtils.TimeTagToMillionSecond(match.Value); + var time = ConvertTimeTagToMillionSecond(match.Value); // using try add because it might be possible with duplicated time-tag position in the lyric. timeTags.TryAdd(new TextIndex(textIndex, state), time); @@ -55,6 +62,25 @@ internal static Tuple> TimedTextToObjec return new Tuple>(text, timeTags); } + /// + /// Convert the <1:00.00> to 60000 + /// + /// + /// + internal static int ConvertTimeTagToMillionSecond(string timeTag) + { + Match match = start_time_regex.Match(timeTag); + + if (!match.Success) + throw new InvalidOperationException("Time tag format is invalid."); + + int minutes = int.Parse(match.Groups[1].Value); + int seconds = int.Parse(match.Groups[2].Value); + int hundredths = int.Parse(match.Groups[3].Value); + + return minutes * 60 * 1000 + seconds * 1000 + hundredths * 10; + } + internal static string ToTimedText(string text, SortedDictionary timeTags) { var insertIndex = 0; @@ -63,7 +89,7 @@ internal static string ToTimedText(string text, SortedDictionary foreach (var (textIndex, time) in timeTags) { - var timeTagString = TimeTagUtils.MillionSecondToTimeTag(time); + var timeTagString = ConvertMillionSecondToTimeTag(time); var stringIndex = TextIndexUtils.ToGapIndex(textIndex); timedText = timedText.Insert(insertIndex + stringIndex, timeTagString); @@ -72,4 +98,22 @@ internal static string ToTimedText(string text, SortedDictionary return timedText; } + + /// + /// Convert the 60000 to <1:00.00> + /// + /// + /// + internal static string ConvertMillionSecondToTimeTag(int milliseconds) + { + if (milliseconds < 0) + throw new InvalidOperationException($"{nameof(milliseconds)} should be greater than 0."); + + int totalSeconds = milliseconds / 1000; + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + int hundredths = (milliseconds % 1000) / 10; + + return $"<{minutes:D2}:{seconds:D2}.{hundredths:D2}>"; + } } diff --git a/LrcParser/Parser/Lrc/Utils/TimeTagUtils.cs b/LrcParser/Parser/Lrc/Utils/TimeTagUtils.cs deleted file mode 100644 index e9d801b..0000000 --- a/LrcParser/Parser/Lrc/Utils/TimeTagUtils.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) karaoke.dev . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace LrcParser.Parser.Lrc.Utils; - -internal static class TimeTagUtils -{ - private const char decimal_point = '.'; - - /// - /// Convert milliseconds to format [mm:ss.ss]. - /// - /// - /// Input : 17970 - /// Output : [00:17:97] - /// - /// - /// - internal static string MillionSecondToTimeTag(int millionSecond) - { - return millionSecond < 0 - ? "" - : string.Format("[{0:D2}:{1:D2}" + decimal_point + "{2:D2}]", millionSecond / 1000 / 60, millionSecond / 1000 % 60, millionSecond / 10 % 100); - } - - /// - /// Convert format [mm:ss.ss] to milliseconds. - /// - /// - /// Input : [00:17:97] - /// Output : 17970 - /// - /// - /// - internal static int TimeTagToMillionSecond(string timeTag) - { - if (timeTag.Length < 10 || timeTag[0] != '[' || !char.IsDigit(timeTag[1])) - return -1; - - int minute = int.Parse(timeTag.Substring(1, 2)); - int second = int.Parse(timeTag.Substring(4, 2)); - int millionSecond = int.Parse(timeTag.Substring(7, 2)); - - return (minute * 60 + second) * 1000 + millionSecond * 10; - } -} From 2f8bcce0acc62bae5f8b6886c41c3b0e6734f6c1 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sun, 28 Jul 2024 16:24:44 +0800 Subject: [PATCH 6/7] Fix the lyric line parser. --- .../Parser/Lrc/Lines/LrcLyricParserTest.cs | 65 ++++++++++++++----- LrcParser/Parser/Lrc/Lines/LrcLyricParser.cs | 8 ++- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs b/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs index 61dd7d5..faaa461 100644 --- a/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs +++ b/LrcParser.Tests/Parser/Lrc/Lines/LrcLyricParserTest.cs @@ -34,27 +34,48 @@ public void TestDecode(string lyric, LrcLyric expected) private static IEnumerable testDecodeSource => new object[][] { [ - "[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", + "[00:17.00] <00:00.00>帰<00:01.00>り<00:02.00>道<00:03.00>は<00:04.00>", new LrcLyric { Text = "帰り道は", - TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220"]), + StartTimes = [17000], + TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:0", "[1,start]:1000", "[2,start]:2000", "[3,start]:3000", "[3,end]:4000"]), }, ], [ - "帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", + "[00:17.00] 帰<00:01.00>り<00:02.00>道<00:03.00>は<00:04.00>", new LrcLyric { Text = "帰り道は", - TimeTags = TestCaseTagHelper.ParseTimeTags(["[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220"]), + StartTimes = [17000], + TimeTags = TestCaseTagHelper.ParseTimeTags(["[1,start]:1000", "[2,start]:2000", "[3,start]:3000", "[3,end]:4000"]), }, ], [ - "[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は", + "[00:17.00] <00:00.00>帰<00:01.00>り<00:02.00>道<00:03.00>は", new LrcLyric { Text = "帰り道は", - TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940"]), + StartTimes = [17000], + TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:0", "[1,start]:1000", "[2,start]:2000", "[3,start]:3000"]), + }, + ], + [ + "[00:17.00] 帰り道は", + new LrcLyric + { + Text = "帰り道は", + StartTimes = [17000], + TimeTags = [], + }, + ], + [ + "[00:17.00][00:18.00] 帰り道は", + new LrcLyric + { + Text = "帰り道は", + StartTimes = [17000, 18000], + TimeTags = [], }, ], [ @@ -62,6 +83,7 @@ public void TestDecode(string lyric, LrcLyric expected) new LrcLyric { Text = "帰り道は", + StartTimes = [], TimeTags = [], }, ], @@ -86,9 +108,16 @@ public void TestDecode(string lyric, LrcLyric expected) [TestCaseSource(nameof(testEncodeSource))] public void TestEncode(LrcLyric lyric, string expected) { - var actual = Encode(lyric); + if (string.IsNullOrEmpty(expected)) + { + Assert.That(() => Encode(lyric), Throws.InvalidOperationException); + } + else + { + var actual = Encode(lyric); - Assert.That(actual, Is.EqualTo(expected)); + Assert.That(actual, Is.EqualTo(expected)); + } } private static IEnumerable testEncodeSource => new object[][] @@ -97,33 +126,37 @@ public void TestEncode(LrcLyric lyric, string expected) new LrcLyric { Text = "帰り道は", - TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220"]), + StartTimes = [17000], + TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:0", "[1,start]:1000", "[2,start]:2000", "[3,start]:3000", "[3,end]:4000"]), }, - "[00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22]", + "[00:17.00] <00:00.00>帰<00:01.00>り<00:02.00>道<00:03.00>は<00:04.00>", ], [ new LrcLyric { Text = "帰り道は", - TimeTags = TestCaseTagHelper.ParseTimeTags(["[1,start]:18370", "[2,start]:18550", "[3,start]:18940", "[3,end]:19220"]), + StartTimes = [17000], + TimeTags = TestCaseTagHelper.ParseTimeTags(["[1,start]:1000", "[2,start]:2000", "[3,start]:3000", "[3,end]:4000"]), }, - "帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22]", + "[00:17.00] 帰<00:01.00>り<00:02.00>道<00:03.00>は<00:04.00>", ], [ new LrcLyric { Text = "帰り道は", - TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:17970", "[1,start]:18370", "[2,start]:18550", "[3,start]:18940"]), + StartTimes = [17000], + TimeTags = TestCaseTagHelper.ParseTimeTags(["[0,start]:0", "[1,start]:1000", "[2,start]:2000", "[3,start]:3000"]), }, - "[00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は", + "[00:17.00] <00:00.00>帰<00:01.00>り<00:02.00>道<00:03.00>は", ], [ new LrcLyric { Text = "帰り道は", + StartTimes = [17000], TimeTags = [], }, - "帰り道は", + "[00:17.00] 帰り道は", ], [ new LrcLyric @@ -131,7 +164,7 @@ public void TestEncode(LrcLyric lyric, string expected) Text = "", TimeTags = [], }, - "", + null!, ], }; } diff --git a/LrcParser/Parser/Lrc/Lines/LrcLyricParser.cs b/LrcParser/Parser/Lrc/Lines/LrcLyricParser.cs index 62ded31..69c6ff2 100644 --- a/LrcParser/Parser/Lrc/Lines/LrcLyricParser.cs +++ b/LrcParser/Parser/Lrc/Lines/LrcLyricParser.cs @@ -14,16 +14,20 @@ public override bool CanDecode(string text) public override LrcLyric Decode(string text) { - var (lyric, timeTags) = LrcTimedTextUtils.TimedTextToObject(text); + var (startTimes, lyricText) = LrcStartTimeUtils.SplitLyricAndTimeTag(text); + var (lyric, timeTags) = LrcTimedTextUtils.TimedTextToObject(lyricText); + return new LrcLyric { Text = lyric, + StartTimes = startTimes, TimeTags = timeTags, }; } public override string Encode(LrcLyric component, int index) { - return LrcTimedTextUtils.ToTimedText(component.Text, component.TimeTags); + var lyricWithTimeTag = LrcTimedTextUtils.ToTimedText(component.Text, component.TimeTags); + return LrcStartTimeUtils.JoinLyricAndTimeTag(component.StartTimes, lyricWithTimeTag); } } From e1a66b8063f5db382903744b595243d444202646 Mon Sep 17 00:00:00 2001 From: andy840119 Date: Sun, 28 Jul 2024 16:28:29 +0800 Subject: [PATCH 7/7] Fix the lrc parser. --- LrcParser.Tests/Parser/Lrc/LrcParserTest.cs | 26 +++++++++++---------- LrcParser/Parser/Lrc/LrcParser.cs | 25 ++++++++++++++------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/LrcParser.Tests/Parser/Lrc/LrcParserTest.cs b/LrcParser.Tests/Parser/Lrc/LrcParserTest.cs index d0c5f22..d1ed788 100644 --- a/LrcParser.Tests/Parser/Lrc/LrcParserTest.cs +++ b/LrcParser.Tests/Parser/Lrc/LrcParserTest.cs @@ -14,7 +14,7 @@ public void TestDecode() { var lrcText = new[] { - "[00:17:97]帰[00:18:37]り[00:18:55]道[00:18:94]は[00:19:22]", + "[00:17.00] <00:00.00>帰<00:01.00>り<00:02.00>道<00:03.00>は<00:04.00>", }; var song = new Song @@ -24,13 +24,14 @@ public void TestDecode() new Lyric { Text = "帰り道は", + StartTime = 17000, TimeTags = new SortedDictionary { - { new TextIndex(0), 17970 }, - { new TextIndex(1), 18370 }, - { new TextIndex(2), 18550 }, - { new TextIndex(3), 18940 }, - { new TextIndex(3, IndexState.End), 19220 }, + { new TextIndex(0), 17000 }, + { new TextIndex(1), 18000 }, + { new TextIndex(2), 19000 }, + { new TextIndex(3), 20000 }, + { new TextIndex(3, IndexState.End), 21000 }, }, }, ], @@ -49,13 +50,14 @@ public void TestEncode() new Lyric { Text = "帰り道は", + StartTime = 17000, TimeTags = new SortedDictionary { - { new TextIndex(0), 17970 }, - { new TextIndex(1), 18370 }, - { new TextIndex(2), 18550 }, - { new TextIndex(3), 18940 }, - { new TextIndex(3, IndexState.End), 19220 }, + { new TextIndex(0), 17000 }, + { new TextIndex(1), 18000 }, + { new TextIndex(2), 19000 }, + { new TextIndex(3), 20000 }, + { new TextIndex(3, IndexState.End), 21000 }, }, }, ], @@ -63,7 +65,7 @@ public void TestEncode() var lrcText = new[] { - "[00:17.97]帰[00:18.37]り[00:18.55]道[00:18.94]は[00:19.22]", + "[00:17.00] <00:00.00>帰<00:01.00>り<00:02.00>道<00:03.00>は<00:04.00>", }; checkEncode(song, lrcText); diff --git a/LrcParser/Parser/Lrc/LrcParser.cs b/LrcParser/Parser/Lrc/LrcParser.cs index 7378511..c4aa108 100644 --- a/LrcParser/Parser/Lrc/LrcParser.cs +++ b/LrcParser/Parser/Lrc/LrcParser.cs @@ -26,14 +26,23 @@ protected override Song PostProcess(List values) return new Song { - Lyrics = lyrics.Select(l => new Lyric - { - Text = l.Text, - TimeTags = getTimeTags(l.TimeTags), - }).ToList(), + Lyrics = lyrics.SelectMany(convertLyric).ToList(), }; - static SortedDictionary getTimeTags(SortedDictionary timeTags, int offsetTime = 0) + static IEnumerable convertLyric(LrcLyric lrcLyric) + { + foreach (var startTime in lrcLyric.StartTimes) + { + yield return new Lyric + { + Text = lrcLyric.Text, + StartTime = startTime, + TimeTags = getTimeTags(lrcLyric.TimeTags, startTime), + }; + } + } + + static SortedDictionary getTimeTags(SortedDictionary timeTags, int offsetTime) => new(timeTags.ToDictionary(k => k.Key, v => v.Value + offsetTime as int?)); } @@ -42,12 +51,14 @@ protected override IEnumerable PreProcess(Song song) var lyrics = song.Lyrics; // first, should return the time-tag first. + // todo: implement the algorithm to combine the lyric with different start time but same time-tag. foreach (var lyric in lyrics) { yield return new LrcLyric { Text = lyric.Text, - TimeTags = getTimeTags(lyric.TimeTags), + StartTimes = [lyric.StartTime], + TimeTags = getTimeTags(lyric.TimeTags, -lyric.StartTime), }; }