ref: using AquaLyrics to drive lyrics processing
This commit is contained in:
parent
e60baa0358
commit
0d60e9a094
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import createSpring from '@core/graphics/spring';
|
||||
import type { LyricWord, ScriptItem } from '@core/lyrics/type';
|
||||
import type { LyricWord, ScriptItem } from '@alikia/aqualyrics';
|
||||
import type { LyricPos } from './type';
|
||||
import type { Spring } from '@core/graphics/spring/spring';
|
||||
import userAdjustingProgress from '@core/state/userAdjustingProgress';
|
||||
|
@ -1,252 +0,0 @@
|
||||
import {
|
||||
alt_sc,
|
||||
apply,
|
||||
buildLexer,
|
||||
expectEOF,
|
||||
fail,
|
||||
kleft,
|
||||
kmid,
|
||||
kright,
|
||||
opt_sc,
|
||||
type Parser,
|
||||
rep,
|
||||
rep_sc,
|
||||
seq,
|
||||
str,
|
||||
tok,
|
||||
type Token
|
||||
} from 'typescript-parsec';
|
||||
import type { LrcJsonData, ParsedLrc, ScriptItem } from '../type';
|
||||
import type { IDTag } from './type';
|
||||
|
||||
|
||||
interface ParserScriptItem {
|
||||
start: number;
|
||||
text: string;
|
||||
translation?: string;
|
||||
singer?: string;
|
||||
}
|
||||
|
||||
function convertTimeToMs({
|
||||
mins,
|
||||
secs,
|
||||
decimals
|
||||
}: {
|
||||
mins?: number | string;
|
||||
secs?: number | string;
|
||||
decimals?: string;
|
||||
}) {
|
||||
let result = 0;
|
||||
if (mins) {
|
||||
result += Number(mins) * 60 * 1000;
|
||||
}
|
||||
if (secs) {
|
||||
result += Number(secs) * 1000;
|
||||
}
|
||||
if (decimals) {
|
||||
const denom = Math.pow(10, decimals.length);
|
||||
result += Number(decimals) / (denom / 1000);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const digit = Array.from({ length: 10 }, (_, i) => apply(str(i.toString()), (_) => i)).reduce(
|
||||
(acc, cur) => alt_sc(cur, acc),
|
||||
fail('no alternatives')
|
||||
);
|
||||
const numStr = apply(rep_sc(digit), (r) => r.join(''));
|
||||
const num = apply(numStr, (r) => parseInt(r));
|
||||
const alpha = alt_sc(
|
||||
Array.from({ length: 26 }, (_, i) =>
|
||||
apply(str(String.fromCharCode('a'.charCodeAt(0) + i)), (_) => String.fromCharCode('a'.charCodeAt(0) + i))
|
||||
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives')),
|
||||
Array.from({ length: 26 }, (_, i) =>
|
||||
apply(str(String.fromCharCode('A'.charCodeAt(0) + i)), (_) => String.fromCharCode('A'.charCodeAt(0) + i))
|
||||
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'))
|
||||
);
|
||||
|
||||
const alphaStr = apply(rep(alpha), (r) => r.join(''));
|
||||
|
||||
function spaces<K>(): Parser<K, Token<K>[]> {
|
||||
return rep_sc(str(' '));
|
||||
}
|
||||
|
||||
const unicodeStr = rep(tok('char'));
|
||||
|
||||
function trimmed<K, T>(p: Parser<K, Token<T>[]>): Parser<K, Token<T>[]> {
|
||||
return apply(p, (r) => {
|
||||
while (r.length > 0 && r[0].text.trim() === '') {
|
||||
r.shift();
|
||||
}
|
||||
while (r.length > 0 && r[r.length - 1].text.trim() === '') {
|
||||
r.pop();
|
||||
}
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
function padded<K, T>(p: Parser<K, T>): Parser<K, T> {
|
||||
return kmid(spaces(), p, spaces());
|
||||
}
|
||||
|
||||
function anythingTyped(types: string[]) {
|
||||
return types.map((t) => tok(t)).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'));
|
||||
}
|
||||
|
||||
function lrcTimestamp<K, T>(delim: [Parser<K, Token<T>>, Parser<K, Token<T>>]) {
|
||||
const innerTS = alt_sc(
|
||||
apply(seq(num, str(':'), num, str('.'), numStr), (r) =>
|
||||
convertTimeToMs({ mins: r[0], secs: r[2], decimals: r[4] })
|
||||
),
|
||||
apply(seq(num, str('.'), numStr), (r) => convertTimeToMs({ secs: r[0], decimals: r[2] })),
|
||||
apply(seq(num, str(':'), num), (r) => convertTimeToMs({ mins: r[0], secs: r[2] })),
|
||||
apply(num, (r) => convertTimeToMs({ secs: r }))
|
||||
);
|
||||
return kmid(delim[0], innerTS, delim[1]);
|
||||
}
|
||||
|
||||
const squareTS = lrcTimestamp([tok('['), tok(']')]);
|
||||
const angleTS = lrcTimestamp([tok('<'), tok('>')]);
|
||||
|
||||
const lrcTag = apply(
|
||||
seq(
|
||||
tok('['),
|
||||
alphaStr,
|
||||
str(':'),
|
||||
tokenParserToText(trimmed(rep(anythingTyped(['char', '[', ']', '<', '>'])))),
|
||||
tok(']')
|
||||
),
|
||||
(r) => ({
|
||||
[r[1]]: r[3]
|
||||
})
|
||||
);
|
||||
|
||||
function joinTokens<T>(tokens: Token<T>[]) {
|
||||
return tokens.map((t) => t.text).join('');
|
||||
}
|
||||
|
||||
function tokenParserToText<K, T>(p: Parser<K, Token<T>> | Parser<K, Token<T>[]>): Parser<K, string> {
|
||||
return apply(p, (r: Token<T> | Token<T>[]) => {
|
||||
if (Array.isArray(r)) {
|
||||
return joinTokens(r);
|
||||
}
|
||||
return r.text;
|
||||
});
|
||||
}
|
||||
|
||||
const singerIndicator = kleft(tok('char'), str(':'));
|
||||
const translateParser = kright(tok('|'), unicodeStr);
|
||||
|
||||
function lrcLine(
|
||||
wordDiv = ' ', legacy = false
|
||||
): Parser<unknown, ['script_item', ParserScriptItem] | ['lrc_tag', IDTag] | ['comment', string] | ['empty', null]> {
|
||||
return alt_sc(
|
||||
legacy ? apply(seq(squareTS, trimmed(rep_sc(anythingTyped(['char', '[', ']', '<', '>'])))), (r) =>
|
||||
['script_item', { start: r[0], text: joinTokens(r[1]) } as ParserScriptItem] // TODO: Complete this
|
||||
) : apply(
|
||||
seq(
|
||||
squareTS,
|
||||
opt_sc(padded(singerIndicator)),
|
||||
rep_sc(
|
||||
seq(
|
||||
opt_sc(angleTS),
|
||||
trimmed(rep_sc(anythingTyped(['char', '[', ']'])))
|
||||
)
|
||||
),
|
||||
opt_sc(trimmed(translateParser))
|
||||
), (r) => {
|
||||
const start = r[0];
|
||||
const singerPart = r[1];
|
||||
const mainPart = r[2];
|
||||
const translatePart = r[3];
|
||||
|
||||
const text = mainPart
|
||||
.map((s) => joinTokens(s[1]))
|
||||
.filter((s) => s.trim().length > 0)
|
||||
.join(wordDiv);
|
||||
|
||||
const singer = singerPart?.text;
|
||||
const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
|
||||
|
||||
return ['script_item', { start, text, singer, translation } as ParserScriptItem];
|
||||
}),
|
||||
apply(lrcTag, (r) => ['lrc_tag', r as IDTag]),
|
||||
apply(seq(spaces(), str('#'), unicodeStr), (cmt) => ['comment', cmt[2].join('')] as const),
|
||||
apply(spaces(), (_) => ['empty', null] as const)
|
||||
);
|
||||
}
|
||||
|
||||
export function dumpToken<T>(t: Token<T> | undefined): string {
|
||||
if (t === undefined) {
|
||||
return '<EOF>';
|
||||
}
|
||||
return '`' + t.text + '` -> ' + dumpToken(t.next);
|
||||
}
|
||||
|
||||
export function parseLRC(
|
||||
input: string,
|
||||
{ wordDiv, strict, legacy }: { wordDiv?: string; strict?: boolean; legacy?: boolean } = {}
|
||||
): ParsedLrc {
|
||||
const tokenizer = buildLexer([
|
||||
[true, /^\[/gu, '['],
|
||||
[true, /^\]/gu, ']'],
|
||||
[true, /^\|/gu, '|'],
|
||||
[true, /^./gu, 'char']
|
||||
]);
|
||||
|
||||
const lines = input
|
||||
.split(/\r\n|\r|\n/gu)
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => tokenizer.parse(line));
|
||||
|
||||
return lines
|
||||
.map((line) => {
|
||||
const res = expectEOF(lrcLine(wordDiv, legacy).parse(line));
|
||||
if (!res.successful) {
|
||||
if (strict) {
|
||||
throw new Error('Failed to parse full line: ' + dumpToken(line));
|
||||
} else {
|
||||
console.error('Failed to parse full line: ' + dumpToken(line));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return res.candidates[0].result;
|
||||
})
|
||||
.filter((r) => r !== null)
|
||||
.reduce((acc, cur) => {
|
||||
switch (cur[0]) {
|
||||
case 'lrc_tag':
|
||||
Object.assign(acc, cur[1]);
|
||||
return acc;
|
||||
case 'script_item':
|
||||
acc.scripts = acc.scripts || [];
|
||||
acc.scripts.push(cur[1]);
|
||||
return acc;
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
}, {} as ParsedLrc);
|
||||
}
|
||||
|
||||
export default function lrcParser(lrc: string): LrcJsonData {
|
||||
const parsedLrc = parseLRC(lrc, { wordDiv: '', strict: false });
|
||||
if (parsedLrc.scripts === undefined) {
|
||||
return parsedLrc as LrcJsonData;
|
||||
}
|
||||
let finalLrc: LrcJsonData = parsedLrc as LrcJsonData;
|
||||
let lyrics: ScriptItem[] = [];
|
||||
let i = 0;
|
||||
while (i < parsedLrc.scripts.length - 1) {
|
||||
let lyricLine = parsedLrc.scripts[i] as ScriptItem;
|
||||
lyricLine.start/=1000;
|
||||
lyricLine.end = parsedLrc.scripts[i+1].start / 1000;
|
||||
if (parsedLrc.scripts[i+1].text.trim() === "") {
|
||||
i+=2;
|
||||
} else i++;
|
||||
if (lyricLine.text.trim() !== "") {
|
||||
lyrics.push(lyricLine);
|
||||
}
|
||||
}
|
||||
finalLrc.scripts = lyrics;
|
||||
return finalLrc;
|
||||
}
|
3
packages/core/lyrics/lrc/type.d.ts
vendored
3
packages/core/lyrics/lrc/type.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
export interface IDTag {
|
||||
[key: string]: string;
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import type { LrcJsonData } from "lrc-parser-ts";
|
||||
import type { LyricData } from "@alikia/aqualyrics";
|
||||
|
||||
export default function createLyricsSearcher(lrc: LrcJsonData): (progress: number) => number {
|
||||
export default function createLyricsSearcher(lrc: LyricData): (progress: number) => number {
|
||||
if (!lrc || !lrc.scripts) return () => 0;
|
||||
const startTimes: number[] = lrc.scripts.map(script => script.start);
|
||||
const endTimes: number[] = lrc.scripts.map(script => script.end);
|
||||
|
||||
return function(progress: number): number {
|
||||
// 使用二分查找定位 progress 对应的歌词索引
|
||||
let left = 0;
|
||||
let right = startTimes.length - 1;
|
||||
|
||||
@ -22,12 +21,10 @@ export default function createLyricsSearcher(lrc: LrcJsonData): (progress: numbe
|
||||
}
|
||||
}
|
||||
|
||||
// 循环结束后,检查 left 索引
|
||||
if (left < startTimes.length && startTimes[left] > progress && (left === 0 || endTimes[left - 1] <= progress)) {
|
||||
return left;
|
||||
}
|
||||
|
||||
// 如果没有找到确切的 progress,返回小于等于 progress 的最大索引
|
||||
return Math.max(0, right);
|
||||
};
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import type { LrcJsonData } from '../type';
|
||||
import { parseTTML as ttmlParser } from './parser';
|
||||
import type { LyricLine } from './ttml-types';
|
||||
|
||||
export type * from './ttml-types';
|
||||
|
||||
export function parseTTML(text: string) {
|
||||
let lyrics: LrcJsonData;
|
||||
const lyricLines = ttmlParser(text).lyricLines;
|
||||
lyrics = {
|
||||
scripts: lyricLines.map((value: LyricLine, _index: number, _array: LyricLine[]) => {
|
||||
let words = value.words.length == 0 ? undefined : value.words;
|
||||
if (words) {
|
||||
words = words.map((word) => {
|
||||
let r = word;
|
||||
r.startTime /= 1000;
|
||||
r.endTime /= 1000;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
return {
|
||||
text: value.words.map((word) => word.word).join(''),
|
||||
start: value.startTime / 1000,
|
||||
end: value.endTime / 1000,
|
||||
translation: value.translatedLyric || undefined,
|
||||
singer: value.singer || undefined,
|
||||
words: words
|
||||
};
|
||||
})
|
||||
};
|
||||
return lyrics;
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* Parser for TTML lyrics.
|
||||
* Used to parse lyrics files from Apple Music,
|
||||
* and extended to support translation and pronounciation of text.
|
||||
* @see https://www.w3.org/TR/2018/REC-ttml1-20181108/
|
||||
*/
|
||||
|
||||
import type {
|
||||
LyricLine,
|
||||
LyricWord,
|
||||
TTMLLyric,
|
||||
TTMLMetadata,
|
||||
} from "./ttml-types";
|
||||
|
||||
const timeRegexp =
|
||||
/^(((?<hour>[0-9]+):)?(?<min>[0-9]+):)?(?<sec>[0-9]+([.:]([0-9]+))?)/;
|
||||
function parseTimespan(timeSpan: string): number {
|
||||
const matches = timeRegexp.exec(timeSpan);
|
||||
if (matches) {
|
||||
const hour = Number(matches.groups?.hour || "0");
|
||||
const min = Number(matches.groups?.min || "0");
|
||||
const sec = Number(matches.groups?.sec.replace(/:/, ".") || "0");
|
||||
return Math.floor((hour * 3600 + min * 60 + sec) * 1000);
|
||||
}
|
||||
throw new TypeError(`Failed to parse time stamp:${timeSpan}`);
|
||||
}
|
||||
|
||||
export function parseTTML(ttmlText: string): TTMLLyric {
|
||||
const domParser = new DOMParser();
|
||||
const ttmlDoc: XMLDocument = domParser.parseFromString(
|
||||
ttmlText,
|
||||
"application/xml",
|
||||
);
|
||||
|
||||
let mainAgentId = "v1";
|
||||
|
||||
const metadata: TTMLMetadata[] = [];
|
||||
for (const meta of ttmlDoc.querySelectorAll("meta")) {
|
||||
if (meta.tagName === "amll:meta") {
|
||||
const key = meta.getAttribute("key");
|
||||
if (key) {
|
||||
const value = meta.getAttribute("value");
|
||||
if (value) {
|
||||
const existing = metadata.find((m) => m.key === key);
|
||||
if (existing) {
|
||||
existing.value.push(value);
|
||||
} else {
|
||||
metadata.push({
|
||||
key,
|
||||
value: [value],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const agent of ttmlDoc.querySelectorAll("ttm\\:agent")) {
|
||||
if (agent.getAttribute("type") === "person") {
|
||||
const id = agent.getAttribute("xml:id");
|
||||
if (id) {
|
||||
mainAgentId = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lyricLines: LyricLine[] = [];
|
||||
|
||||
function parseParseLine(lineEl: Element, isBG = false, isDuet = false) {
|
||||
const line: LyricLine = {
|
||||
words: [],
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG,
|
||||
isDuet:
|
||||
!!lineEl.getAttribute("ttm:agent") &&
|
||||
lineEl.getAttribute("ttm:agent") !== mainAgentId,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
singer: ""
|
||||
};
|
||||
if (isBG) line.isDuet = isDuet;
|
||||
let haveBg = false;
|
||||
|
||||
for (const wordNode of lineEl.childNodes) {
|
||||
if (wordNode.nodeType === Node.TEXT_NODE) {
|
||||
line.words?.push({
|
||||
word: wordNode.textContent ?? "",
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
});
|
||||
} else if (wordNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const wordEl = wordNode as Element;
|
||||
const role = wordEl.getAttribute("ttm:role");
|
||||
|
||||
if (wordEl.nodeName === "span" && role) {
|
||||
if (role === "x-bg") {
|
||||
parseParseLine(wordEl, true, line.isDuet);
|
||||
haveBg = true;
|
||||
} else if (role === "x-translation") {
|
||||
line.translatedLyric = wordEl.innerHTML;
|
||||
} else if (role === "x-roman") {
|
||||
line.romanLyric = wordEl.innerHTML;
|
||||
} else if (role === "x-singer") {
|
||||
line.singer = wordEl.innerHTML;
|
||||
}
|
||||
} else if (wordEl.hasAttribute("begin") && wordEl.hasAttribute("end")) {
|
||||
const word: LyricWord = {
|
||||
word: wordNode.textContent ?? "",
|
||||
startTime: parseTimespan(wordEl.getAttribute("begin") ?? ""),
|
||||
endTime: parseTimespan(wordEl.getAttribute("end") ?? ""),
|
||||
};
|
||||
const emptyBeat = wordEl.getAttribute("amll:empty-beat");
|
||||
if (emptyBeat) {
|
||||
word.emptyBeat = Number(emptyBeat);
|
||||
}
|
||||
line.words.push(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (line.isBG) {
|
||||
const firstWord = line.words?.[0];
|
||||
if (firstWord?.word.startsWith("(")) {
|
||||
firstWord.word = firstWord.word.substring(1);
|
||||
if (firstWord.word.length === 0) {
|
||||
line.words.shift();
|
||||
}
|
||||
}
|
||||
|
||||
const lastWord = line.words?.[line.words.length - 1];
|
||||
if (lastWord?.word.endsWith(")")) {
|
||||
lastWord.word = lastWord.word.substring(0, lastWord.word.length - 1);
|
||||
if (lastWord.word.length === 0) {
|
||||
line.words.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = lineEl.getAttribute("begin");
|
||||
const endTime = lineEl.getAttribute("end");
|
||||
if (startTime && endTime) {
|
||||
line.startTime = parseTimespan(startTime);
|
||||
line.endTime = parseTimespan(endTime);
|
||||
} else {
|
||||
line.startTime = line.words
|
||||
.filter((v) => v.word.trim().length > 0)
|
||||
.reduce((pv, cv) => Math.min(pv, cv.startTime), Infinity);
|
||||
line.endTime = line.words
|
||||
.filter((v) => v.word.trim().length > 0)
|
||||
.reduce((pv, cv) => Math.max(pv, cv.endTime), 0);
|
||||
}
|
||||
|
||||
if (haveBg) {
|
||||
const bgLine = lyricLines.pop();
|
||||
lyricLines.push(line);
|
||||
if (bgLine) lyricLines.push(bgLine);
|
||||
} else {
|
||||
lyricLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
for (const lineEl of ttmlDoc.querySelectorAll("body p[begin][end]")) {
|
||||
parseParseLine(lineEl);
|
||||
}
|
||||
|
||||
return {
|
||||
metadata,
|
||||
lyricLines: lyricLines,
|
||||
};
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
export interface TTMLMetadata {
|
||||
key: string;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface TTMLLyric {
|
||||
metadata: TTMLMetadata[];
|
||||
lyricLines: LyricLine[];
|
||||
}
|
||||
|
||||
export interface LyricWord {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
word: string;
|
||||
emptyBeat?: number;
|
||||
}
|
||||
|
||||
export interface LyricLine {
|
||||
words: LyricWord[];
|
||||
translatedLyric: string;
|
||||
romanLyric: string;
|
||||
isBG: boolean;
|
||||
isDuet: boolean;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
singer: string;
|
||||
}
|
33
packages/core/lyrics/type.d.ts
vendored
33
packages/core/lyrics/type.d.ts
vendored
@ -1,33 +0,0 @@
|
||||
export interface ScriptItem{
|
||||
end: number;
|
||||
chorus?: string;
|
||||
start: number;
|
||||
text: string;
|
||||
words?: LyricWord[];
|
||||
translation?: string;
|
||||
singer?: string;
|
||||
}
|
||||
|
||||
export interface LyricWord {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
word: string;
|
||||
emptyBeat?: number;
|
||||
}
|
||||
|
||||
export interface LyricMetadata {
|
||||
ar?: string;
|
||||
ti?: string;
|
||||
al?: string;
|
||||
au?: string;
|
||||
length?: string;
|
||||
offset?: string;
|
||||
tool?: string;
|
||||
ve?: string;
|
||||
}
|
||||
|
||||
export interface LyricData extends LyricMetadata {
|
||||
scripts?: ScriptItem[];
|
||||
|
||||
[key: string]: any;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import { parseLRC } from '@core/lyrics/lrc/parser';
|
||||
|
||||
describe('LRC parser test', () => {
|
||||
const test01Buffer = fs.readFileSync('./packages/core/test/lyrics/resources/test-01.lrc');
|
||||
const test01Text = test01Buffer.toString('utf-8');
|
||||
const test03Buffer = fs.readFileSync('./packages/core/test/lyrics/resources/test-03.lrc');
|
||||
const test03Text = test03Buffer.toString('utf-8');
|
||||
|
||||
const lf_alternatives = ['\n', '\r\n', '\r'];
|
||||
|
||||
it('Parses test-01.lrc', () => {
|
||||
for (const lf of lf_alternatives) {
|
||||
const text = test01Text.replaceAll('\n', lf);
|
||||
|
||||
const result = parseLRC(text, { wordDiv: '', strict: true });
|
||||
|
||||
expect(result.ar).toBe("洛天依");
|
||||
expect(result.ti).toBe("中华少女·终");
|
||||
expect(result.al).toBe("中华少女");
|
||||
expect(result["tool"]).toBe("歌词滚动姬 https://lrc-maker.github.io");
|
||||
expect(result.scripts!![1].text).toBe("因果与恩怨牵杂等谁来诊断");
|
||||
expect(result.scripts!![1].start).toBe(49000 + 588);
|
||||
}
|
||||
})
|
||||
it('Parses test-03.lrc', () => {
|
||||
const result = parseLRC(test03Text, { wordDiv: ' ', strict: true });
|
||||
expect(result.scripts!![5].text).toBe("བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ།");
|
||||
expect(result.scripts!![5].translation).toBe("在舞池里舞一舞");
|
||||
expect(result.scripts!![6].text).toBe("祝祷转过千年 五色经幡飘飞");
|
||||
expect(result.scripts!![6].singer).toBe("a");
|
||||
expect(result.scripts!![11].singer).toBeUndefined();
|
||||
expect(result.scripts!![11].translation).toBe("我们在此相聚");
|
||||
});
|
||||
it('Accepts some weird but parsable LRCs', () => {
|
||||
const cases = [
|
||||
"[ti: []]",
|
||||
"[ar: [<]]",
|
||||
"[ar: <ar>]",
|
||||
"[ar: a b c]",
|
||||
"[00:00.00] <00:00.04> When the <00:00.16> the",
|
||||
"[00:00.00] [00:00.04] When [00:00.16] the",
|
||||
"[00:00.0000000] <00:00.04> When <00:00.16> the",
|
||||
"[00:00.00] <00:00.04> [When] <00:00.16> the",
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
expect(() => parseLRC(c, { strict: false })).not.toThrow();
|
||||
}
|
||||
})
|
||||
});
|
@ -1,56 +0,0 @@
|
||||
[ti: 中华少女·终]
|
||||
[ar: 洛天依]
|
||||
[al: 中华少女]
|
||||
[tool: 歌词滚动姬 https://lrc-maker.github.io]
|
||||
[00:46.706] 我想要仗剑天涯却陷入纷乱
|
||||
[00:49.588] 因果与恩怨牵杂等谁来诊断
|
||||
[00:52.284] 暗箭在身后是否该回身看
|
||||
[00:55.073] 人心有了鬼心房便要过鬼门关
|
||||
[00:57.875] 早已茫然染了谁的血的这抹长衫
|
||||
[01:00.702] 独木桥上独目瞧的人一夫当关
|
||||
[01:03.581] 复仇或是诅咒缠在身上的宿命
|
||||
[01:06.591] 棋子在棋盘被固定移动不停转
|
||||
[01:09.241] 仇恨与仇恨周而复始往返
|
||||
[01:12.586] 酒楼深胭脂一点分隔光暗
|
||||
[01:15.205] 数求问天涯海角血债偿还
|
||||
[01:18.015] 终是神念迷茫只做旁观
|
||||
[01:21.087] 是非恩怨三生纠葛轮转
|
||||
[01:23.709] 回望从前苦笑将杯酒斟满
|
||||
[01:26.573] 那时明月今日仍旧皎洁
|
||||
[01:29.115] 只叹换拨人看
|
||||
[01:31.024] 你可是这样的少年
|
||||
[01:33.971] 梦想着穿越回从前
|
||||
[01:36.554] 弦月下着青衫抚长剑
|
||||
[01:42.341] 风起时以血绘长卷
|
||||
[01:45.276] 三寸剑只手撼江山
|
||||
[01:47.838] 拂衣去逍遥天地间
|
||||
[01:52.991]
|
||||
[02:16.707] 黄藓绿斑苔痕将岁月扒谱
|
||||
[02:20.077] 望眼欲穿你何时寄来家书
|
||||
[02:22.788] 踱步间院落飞絮聚散化作愁字
|
||||
[02:25.601] 当泪水成河能否凫上一位游子
|
||||
[02:28.050] 当庭间嫣红轻轻闻着雨声
|
||||
[02:30.841] 电闪雷鸣院中青翠摇曳倚风
|
||||
[02:33.362] 青丝落指尖模糊记忆更为清晰
|
||||
[02:36.334] 那一朵英姿遮挡于一抹旌旗飘
|
||||
[02:39.511] 厮杀与厮杀周而复始招摇
|
||||
[02:42.576] 血渍滑落于枪尖映照宵小
|
||||
[02:45.726] 城池下红莲飞溅绽放照耀
|
||||
[02:48.509] 碧落黄泉再无叨扰
|
||||
[02:51.338] 北风呼啸三生等待轮转
|
||||
[02:53.660] 山崖古道思绪被光阴晕染
|
||||
[02:56.895] 那时明月今日仍旧皎洁
|
||||
[02:59.293] 只叹孤身人看
|
||||
[03:01.335] 你可是这样的少年
|
||||
[03:04.377] 梦想着穿越回从前
|
||||
[03:06.924] 北风里铁衣冷槊光寒
|
||||
[03:12.607] 一朝去大小三百战
|
||||
[03:15.623] 岁月欺万里定江山
|
||||
[03:18.126] 再与她同游天地间
|
||||
[03:24.356] 说书人或许会留恋
|
||||
[03:27.057] 但故事毕竟有终点
|
||||
[03:29.590] 最好的惊堂木是时间
|
||||
[03:35.157] 就让我合上这书卷
|
||||
[03:38.242] 愿那些梦中的玩伴
|
||||
[03:40.857] 梦醒后仍然是少年
|
||||
[03:46.139]
|
@ -1,77 +0,0 @@
|
||||
[ti: 雪山之眼]
|
||||
[ar: 洛天依 & 旦增益西]
|
||||
[al: 游四方]
|
||||
[tool: 歌词滚动姬 https://lrc-maker.github.io]
|
||||
[length: 04:17.400]
|
||||
[00:34.280] 浸透了经卷 记忆的呼喊
|
||||
[00:37.800] 雪珠滚落山巅 栽下一个春天
|
||||
[00:47.390] 松石敲响玲珑清脆的银花
|
||||
[00:51.600] 穿过玛瑙的红霞
|
||||
[00:54.430] 在她眼中结编 亘久诗篇
|
||||
[01:05.440] a: བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། | 在舞池里舞一舞
|
||||
[01:08.780] a: 祝祷转过千年 五色经幡飘飞
|
||||
[01:12.040] 奏起悠扬巴叶 任岁月拨弦
|
||||
[01:19.130] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། 我在歌坛献首歌
|
||||
[01:22.330] 宫殿 塔尖 彩绘 日月 同辉
|
||||
[01:25.810] 那层厚重壁垒化身 蝉翼一片
|
||||
[01:29.110] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||
[01:30.790] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[01:32.510] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[01:34.120] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[01:35.920] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[01:37.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[01:39.350] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[01:41.050] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[01:42.740] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[01:44.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[01:46.280] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[01:48.010] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[01:49.600] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[01:51.380] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[01:53.070] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[01:54.820] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[01:58.580] སྔོན་དང་པོ་གྲུབ་ཐོབ་ཐང་སྟོང་རྒྱལ་པོས་མཛད་པའི་མཛད་ཚུལ་དུ། དང་པོ་རྔོན་པའི་ས་སྦྱངས་ས་འདུལ། གཉིས་པ་རྒྱ་ལུའི་བྱིན་འབེབས། གསུམ་པ་ལྷ་མོའི་གླུ་གར་སོགས་རིན་ཆེན་གསུང་མགུར་གཞུང་བཟང་མང་པོ་འདུག་སྟེ། དེ་ཡང་མ་ཉུང་གི་ཚིག་ལ་དུམ་མཚམས་གཅིག་ཞུས་པ་བྱུང་བ་ཡིན་པ་ལགས་སོ། 如祖师唐东杰布所著,一有温巴净地,二有甲鲁祈福,三有仙女歌舞,所著繁多,在此简略献之。
|
||||
[02:24.240] 浸透了经卷 记忆的呼喊
|
||||
[02:27.450] 雪珠滚落山巅 栽下一个春天
|
||||
[02:37.090] 松石敲响玲珑清脆的银花
|
||||
[02:41.280] 穿过玛瑙的红霞
|
||||
[02:44.010] 在她眼中结编 亘久诗篇
|
||||
[02:55.250] བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། 在舞池里舞一舞
|
||||
[02:58.410] 祝祷转过千年 五色经幡飘飞
|
||||
[03:01.750] 奏起悠扬巴叶 任岁月拨弦
|
||||
[03:08.840] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། 我在歌坛献首歌
|
||||
[03:12.050] 宫殿 塔尖 彩绘 日月 同辉
|
||||
[03:15.400] 那层厚重壁垒化身 蝉翼一片
|
||||
[03:18.850] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[03:20.480] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[03:22.210] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[03:23.910] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[03:25.662] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[03:27.391] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[03:29.096] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[03:30.789] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[03:32.496] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[03:34.175] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[03:35.876] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[03:37.606] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[03:39.290] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[03:41.030] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[03:42.679] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[03:44.455] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[03:46.176] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[03:47.910] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[03:49.625] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[03:51.293] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[03:53.005] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[03:54.742] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[03:56.479] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[03:58.159] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[03:59.859] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[04:01.548] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[04:03.312] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[04:05.026] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[04:06.721] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
||||
[04:08.479] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
||||
[04:10.175] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
||||
[04:11.923] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
||||
[04:17.400]
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import createSpring from '@core/graphics/spring';
|
||||
import type { ScriptItem } from '@core/lyrics/type';
|
||||
import type { ScriptItem } from '@alikia/aqualyrics';
|
||||
import type { LyricPos } from './type';
|
||||
import type { Spring } from '@core/graphics/spring/spring';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user