add support for ttml format

This commit is contained in:
Zhe Fang
2025-06-22 22:38:03 -04:00
parent 827602766d
commit 0befdf48dd
10 changed files with 119 additions and 24 deletions

View File

@@ -47,6 +47,7 @@
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">

View File

@@ -28,6 +28,9 @@ namespace BetterLyrics.WinUI3.Converter
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderEslrcFile"
),
LyricsSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderTtmlFile"
),
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null),
};
}

View File

@@ -6,5 +6,6 @@
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
}
}

View File

@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
@@ -142,29 +141,63 @@ namespace BetterLyrics.WinUI3.Helper
private void ParseTtml(string raw, int durationMs)
{
// 简单 TTML 解析
try
{
var xdoc = XDocument.Parse(raw);
XNamespace ns = xdoc.Root?.Name.Namespace ?? "";
var body = xdoc.Descendants(ns + "body").FirstOrDefault();
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null)
return;
var ps = body.Descendants(ns + "p");
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
string text = p.Value.Trim();
string? begin = p.Attribute("begin")?.Value;
string? end = p.Attribute("end")?.Value;
int startMs = ParseTtmlTime(begin);
int endMs = ParseTtmlTime(end);
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 处理分词分时
var spans = p.Elements()
.Where(s =>
s.Name.LocalName == "span"
&& s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))
== null
)
.ToList();
string text = string.Concat(spans.Select(s => s.Value));
var charTimings = new List<CharTiming>();
for (int i = 0; i < spans.Count; i++)
{
var span = spans[i];
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
if (sStartMs == 0 && sEndMs == 0)
continue;
if (sEndMs == 0)
sEndMs =
(i + 1 < spans.Count)
? ParseTtmlTime(spans[i + 1].Attribute("begin")?.Value)
: pEndMs;
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = sEndMs });
}
if (spans.Count == 0)
text = p.Value.Trim();
_lyricsLines.Add(
new LyricsLine
{
StartMs = startMs,
EndMs = endMs,
StartMs = pStartMs,
EndMs = pEndMs,
Texts = [text],
CharTimings = [],
CharTimings = charTimings,
}
);
}
@@ -177,12 +210,22 @@ namespace BetterLyrics.WinUI3.Helper
private int ParseTtmlTime(string? t)
{
if (string.IsNullOrEmpty(t))
if (string.IsNullOrWhiteSpace(t))
return 0;
// 支持 "00:00:01.000" 或 "1.000s"
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (double.TryParse(t.TrimEnd('s'), out double seconds))
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
@@ -190,11 +233,38 @@ namespace BetterLyrics.WinUI3.Helper
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(parts[2]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}

View File

@@ -44,14 +44,10 @@ namespace BetterLyrics.WinUI3.Services
if (file.Contains(title) && file.Contains(artist))
{
Track track = new(file);
if (track.Lyrics.SynchronizedLyrics.Count > 0)
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
// Get synchronized lyrics from the track (metadata)
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
return bytes;
}
}
}
@@ -111,6 +107,13 @@ namespace BetterLyrics.WinUI3.Services
LyricsFormat.Eslrc
);
break;
case LyricsSearchProvider.LocalTtmlFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Ttml
);
break;
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLib(
title,
@@ -137,6 +140,8 @@ namespace BetterLyrics.WinUI3.Services
return (searchedLyrics, LyricsFormat.Lrc);
case LyricsSearchProvider.LocalEslrcFile:
return (searchedLyrics, LyricsFormat.Eslrc);
case LyricsSearchProvider.LocalTtmlFile:
return (searchedLyrics, LyricsFormat.Ttml);
default:
break;
}

View File

@@ -480,4 +480,7 @@
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>Local .ESLRC files</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Local .TTML files</value>
</data>
</root>

View File

@@ -480,4 +480,7 @@
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>ローカル.ESLRCファイル</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>ローカル.TTMLファイル</value>
</data>
</root>

View File

@@ -480,4 +480,7 @@
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>로컬 .ESLRC 파일</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>로컬 .TTML 파일</value>
</data>
</root>

View File

@@ -480,4 +480,7 @@
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
</root>

View File

@@ -480,4 +480,7 @@
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
</root>