ref: add new AMLL dynamic lyric

This commit is contained in:
alikia2x 2024-08-01 21:40:09 +08:00
parent e8b7a6e3d2
commit d19b230809
11 changed files with 682 additions and 30 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "aquavox",
"version": "1.15.0",
"version": "2.0.0",
"private": false,
"scripts": {
"dev": "vite dev",
@ -34,15 +34,27 @@
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^1.6.0"
},
"type": "module",
"dependencies": {
"@applemusic-like-lyrics/core": "^0.1.3",
"@applemusic-like-lyrics/lyric": "^0.2.2",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@pixi/app": "^7.4.2",
"@pixi/core": "^7.4.2",
"@pixi/display": "^7.4.2",
"@pixi/filter-blur": "^7.4.2",
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.2",
"@pixi/sprite": "^7.4.2",
"@types/bun": "^1.1.6",
"bezier-easing": "^2.1.0",
"jotai": "^2.8.0",
"jotai-svelte": "^0.0.2",
"jss": "^10.10.0",
"jss-preset-default": "^10.10.0",
"localforage": "^1.10.0",
"lrc-parser-ts": "^1.0.3",
"music-metadata-browser": "^2.5.10",

View File

@ -0,0 +1,132 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import {
LyricPlayer as CoreLyricPlayer,
type LyricLineMouseEvent,
type LyricLine,
type spring
} from '@applemusic-like-lyrics/core';
export let disabled: boolean = false;
export let playing: boolean = true;
export let alignAnchor: 'top' | 'bottom' | 'center' = 'center';
export let alignPosition: number = 0.5;
export let enableSpring: boolean = true;
export let enableBlur: boolean = true;
export let enableScale: boolean = true;
export let hidePassedLines: boolean = false;
export let lyricLines: LyricLine[] = [];
export let currentTime: number = 0;
export let linePosXSpringParams: Partial<spring.SpringParams> = {};
export let linePosYSpringParams: Partial<spring.SpringParams> = {};
export let lineScaleSpringParams: Partial<spring.SpringParams> = {};
export let bottomLine: any = null;
export let onLyricLineClick: (line: LyricLineMouseEvent) => void = () => {};
export let onLyricLineContextMenu: (line: LyricLineMouseEvent) => void = () => {};
export let lyricPlayer: CoreLyricPlayer;
let wrapperEl: HTMLDivElement | null;
$: {
if (playing) {
lyricPlayer.resume();
} else {
lyricPlayer.pause();
}
}
let animationCanceled = false;
let animationLastTime = -1;
const onFrame = (time: number) => {
if (animationCanceled) return;
if (animationLastTime === -1) {
animationLastTime = time;
}
lyricPlayer.update(time - animationLastTime);
animationLastTime = time;
requestAnimationFrame(onFrame);
};
const startAnimation = () => {
animationCanceled = false;
animationLastTime = -1;
requestAnimationFrame(onFrame);
};
const stopAnimation = () => {
animationCanceled = true;
};
// Monitor changes to `disabled`
$: {
if (!disabled) {
startAnimation();
} else {
stopAnimation();
}
}
$: {
if (playing) {
lyricPlayer.resume();
} else {
lyricPlayer.pause();
}
}
$: {
if (lyricPlayer && wrapperEl) {
wrapperEl.appendChild(lyricPlayer.getElement());
}
}
$: {
if (alignAnchor !== undefined) {
lyricPlayer.setAlignAnchor(alignAnchor);
}
}
$: {
lyricPlayer.setLyricLines(lyricLines);
lyricPlayer.update();
}
$: {
lyricPlayer.setAlignPosition(alignPosition);
}
$: {
lyricPlayer.setEnableSpring(enableSpring);
}
$: {
lyricPlayer.setEnableScale(enableScale);
}
$: {
lyricPlayer.setEnableBlur(enableBlur);
}
$: {
lyricPlayer.setCurrentTime(currentTime);
}
$: {
const handler = (e: Event) => onLyricLineClick(e as LyricLineMouseEvent);
lyricPlayer.addEventListener('line-click', handler);
}
// Clean up on component unmount
onDestroy(() => {
animationCanceled = true;
lyricPlayer.dispose();
});
</script>
<div
bind:this={wrapperEl}
class="absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12 lg:px-[7.5rem] xl:left-[45vw]
xl:px-[3vw] h-[calc(100vh-17rem)] xl:h-screen font-sans
text-left no-scrollbar overflow-y-auto z-[1] pt-16 font-semibold mix-blend-plus-lighter"
></div>

View File

@ -0,0 +1,18 @@
import type { LyricLine } from "@applemusic-like-lyrics/core";
import {
type LyricLine as RawLyricLine,
parseLrc,
parseYrc,
parseLys,
parseQrc,
} from "@applemusic-like-lyrics/lyric";
export const mapLyric = (line: RawLyricLine, i: number, lines: RawLyricLine[]): LyricLine => ({
words: line.words,
startTime: line.words[0]?.startTime ?? 0,
endTime: line.words[line.words.length - 1]?.endTime ?? Infinity,
translatedLyric: '',
romanLyric: '',
isBG: false,
isDuet: false
});

3
src/lib/ttml/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./parser";
export * from "./writer";
export type * from "./ttml-types";

168
src/lib/ttml/parser.ts Normal file
View File

@ -0,0 +1,168 @@
/**
* @fileoverview
* TTML
* Apple Music
* @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(`时间戳字符串解析失败:${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,
};
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 (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,
};
}

View File

@ -0,0 +1,26 @@
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;
}

260
src/lib/ttml/writer.ts Normal file
View File

@ -0,0 +1,260 @@
/**
* @fileoverview
* TTML
*
*/
import type { LyricLine, LyricWord, TTMLLyric } from "./ttml-types";
function msToTimestamp(timeMS: number): string {
let time = timeMS;
if (!Number.isSafeInteger(time) || time < 0) {
return "00:00.000";
}
if (time === Infinity) {
return "99:99.999";
}
time = time / 1000;
const secs = time % 60;
time = (time - secs) / 60;
const mins = time % 60;
const hrs = (time - mins) / 60;
const h = hrs.toString().padStart(2, "0");
const m = mins.toString().padStart(2, "0");
const s = secs.toFixed(3).padStart(6, "0");
if (hrs > 0) {
return `${h}:${m}:${s}`;
}
return `${m}:${s}`;
}
export function exportTTML(ttmlLyric: TTMLLyric, pretty = false): string {
const params: LyricLine[][] = [];
const lyric = ttmlLyric.lyricLines;
let tmp: LyricLine[] = [];
for (const line of lyric) {
if (line.words.length === 0 && tmp.length > 0) {
params.push(tmp);
tmp = [];
} else {
tmp.push(line);
}
}
if (tmp.length > 0) {
params.push(tmp);
}
const doc = new Document();
function createWordElement(word: LyricWord): Element {
const span = doc.createElement("span");
span.setAttribute("begin", msToTimestamp(word.startTime));
span.setAttribute("end", msToTimestamp(word.endTime));
if (word.emptyBeat) {
span.setAttribute("amll:empty-beat", `${word.emptyBeat}`);
}
span.appendChild(doc.createTextNode(word.word));
return span;
}
const ttRoot = doc.createElement("tt");
ttRoot.setAttribute("xmlns", "http://www.w3.org/ns/ttml");
ttRoot.setAttribute("xmlns:ttm", "http://www.w3.org/ns/ttml#metadata");
ttRoot.setAttribute("xmlns:amll", "http://www.example.com/ns/amll");
ttRoot.setAttribute(
"xmlns:itunes",
"http://music.apple.com/lyric-ttml-internal",
);
doc.appendChild(ttRoot);
const head = doc.createElement("head");
ttRoot.appendChild(head);
const body = doc.createElement("body");
const hasOtherPerson = !!lyric.find((v) => v.isDuet);
const metadataEl = doc.createElement("metadata");
const mainPersonAgent = doc.createElement("ttm:agent");
mainPersonAgent.setAttribute("type", "person");
mainPersonAgent.setAttribute("xml:id", "v1");
metadataEl.appendChild(mainPersonAgent);
if (hasOtherPerson) {
const otherPersonAgent = doc.createElement("ttm:agent");
otherPersonAgent.setAttribute("type", "other");
otherPersonAgent.setAttribute("xml:id", "v2");
metadataEl.appendChild(otherPersonAgent);
}
for (const metadata of ttmlLyric.metadata) {
for (const value of metadata.value) {
const metaEl = doc.createElement("amll:meta");
metaEl.setAttribute("key", metadata.key);
metaEl.setAttribute("value", value);
metadataEl.appendChild(metaEl);
}
}
head.appendChild(metadataEl);
let i = 0;
const guessDuration = lyric[lyric.length - 1]?.endTime ?? 0;
body.setAttribute("dur", msToTimestamp(guessDuration));
for (const param of params) {
const paramDiv = doc.createElement("div");
const beginTime = param[0]?.startTime ?? 0;
const endTime = param[param.length - 1]?.endTime ?? 0;
paramDiv.setAttribute("begin", msToTimestamp(beginTime));
paramDiv.setAttribute("end", msToTimestamp(endTime));
for (let lineIndex = 0; lineIndex < param.length; lineIndex++) {
const line = param[lineIndex];
const lineP = doc.createElement("p");
const beginTime = line.startTime ?? 0;
const endTime = line.endTime;
lineP.setAttribute("begin", msToTimestamp(beginTime));
lineP.setAttribute("end", msToTimestamp(endTime));
lineP.setAttribute("ttm:agent", line.isDuet ? "v2" : "v1");
lineP.setAttribute("itunes:key", `L${++i}`);
if (line.words.length > 1) {
let beginTime = Infinity;
let endTime = 0;
for (const word of line.words) {
if (word.word.trim().length === 0) {
lineP.appendChild(doc.createTextNode(word.word));
} else {
const span = createWordElement(word);
lineP.appendChild(span);
beginTime = Math.min(beginTime, word.startTime);
endTime = Math.max(endTime, word.endTime);
}
}
lineP.setAttribute("begin", msToTimestamp(line.startTime));
lineP.setAttribute("end", msToTimestamp(line.endTime));
} else if (line.words.length === 1) {
const word = line.words[0];
lineP.appendChild(doc.createTextNode(word.word));
lineP.setAttribute("begin", msToTimestamp(word.startTime));
lineP.setAttribute("end", msToTimestamp(word.endTime));
}
const nextLine = param[lineIndex + 1];
if (nextLine?.isBG) {
lineIndex++;
const bgLine = nextLine;
const bgLineSpan = doc.createElement("span");
bgLineSpan.setAttribute("ttm:role", "x-bg");
if (bgLine.words.length > 1) {
let beginTime = Infinity;
let endTime = 0;
for (
let wordIndex = 0;
wordIndex < bgLine.words.length;
wordIndex++
) {
const word = bgLine.words[wordIndex];
if (word.word.trim().length === 0) {
bgLineSpan.appendChild(doc.createTextNode(word.word));
} else {
const span = createWordElement(word);
if (wordIndex === 0) {
span.prepend(doc.createTextNode("("));
} else if (wordIndex === bgLine.words.length - 1) {
span.appendChild(doc.createTextNode(")"));
}
bgLineSpan.appendChild(span);
beginTime = Math.min(beginTime, word.startTime);
endTime = Math.max(endTime, word.endTime);
}
}
bgLineSpan.setAttribute("begin", msToTimestamp(beginTime));
bgLineSpan.setAttribute("end", msToTimestamp(endTime));
} else if (bgLine.words.length === 1) {
const word = bgLine.words[0];
bgLineSpan.appendChild(doc.createTextNode(`(${word.word})`));
bgLineSpan.setAttribute("begin", msToTimestamp(word.startTime));
bgLineSpan.setAttribute("end", msToTimestamp(word.endTime));
}
if (bgLine.translatedLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-translation");
span.setAttribute("xml:lang", "zh-CN");
span.appendChild(doc.createTextNode(bgLine.translatedLyric));
bgLineSpan.appendChild(span);
}
if (bgLine.romanLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-roman");
span.appendChild(doc.createTextNode(bgLine.romanLyric));
bgLineSpan.appendChild(span);
}
lineP.appendChild(bgLineSpan);
}
if (line.translatedLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-translation");
span.setAttribute("xml:lang", "zh-CN");
span.appendChild(doc.createTextNode(line.translatedLyric));
lineP.appendChild(span);
}
if (line.romanLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-roman");
span.appendChild(doc.createTextNode(line.romanLyric));
lineP.appendChild(span);
}
paramDiv.appendChild(lineP);
}
body.appendChild(paramDiv);
}
ttRoot.appendChild(body);
if (pretty) {
const xsltDoc = new DOMParser().parseFromString(
[
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">',
' <xsl:strip-space elements="*"/>',
' <xsl:template match="para[content-style][not(text())]">',
' <xsl:value-of select="normalize-space(.)"/>',
" </xsl:template>",
' <xsl:template match="node()|@*">',
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
" </xsl:template>",
' <xsl:output indent="yes"/>',
"</xsl:stylesheet>",
].join("\n"),
"application/xml",
);
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsltDoc);
const resultDoc = xsltProcessor.transformToDocument(doc);
return new XMLSerializer().serializeToString(resultDoc);
}
return new XMLSerializer().serializeToString(doc);
}

View File

@ -16,7 +16,7 @@
<div class="w-full flex my-3">
<h2>歌词文件</h2>
<FileSelector accept=".lrc" class="ml-auto top-2 relative" />
<FileSelector accept=".lrc,.ttml" class="ml-auto top-2 relative" />
</div>
<FileList />

View File

@ -4,15 +4,19 @@
import Background from '$lib/components/background.svelte';
import Cover from '$lib/components/cover.svelte';
import InteractiveBox from '$lib/components/interactiveBox.svelte';
import Lyrics from '$lib/components/lyrics.svelte';
import extractFileName from '$lib/extractFileName';
import localforage from 'localforage';
import { writable } from 'svelte/store';
import lrcParser, { type LrcJsonData } from '$lib/lyrics/parser';
import userAdjustingProgress from '$lib/state/userAdjustingProgress';
import type { IAudioMetadata } from 'music-metadata-browser';
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import progressBarRaw from '$lib/state/progressBarRaw';
import { parseTTML, type TTMLLyric } from '$lib/ttml';
import type { LyricLine, LyricLineMouseEvent, LyricPlayer } from '@applemusic-like-lyrics/core';
import NewLyrics from '$lib/components/newLyrics.svelte';
import { LyricPlayer as CoreLyricPlayer } from '@applemusic-like-lyrics/core';
import { parseLrc } from '@applemusic-like-lyrics/lyric';
import { mapLyric } from '$lib/lyrics/mapLyric';
const audioId = $page.params.id;
let audioPlayer: HTMLAudioElement | null = null;
@ -25,9 +29,9 @@
let paused: boolean = true;
let launched = false;
let prepared: string[] = [];
let originalLyrics: LrcJsonData;
let lyricsText: string[] = [];
let lyricLines: LyricLine[];
let hasLyrics: boolean;
let lyricPlayer: LyricPlayer = new CoreLyricPlayer();
const coverPath = writable('');
let mainInterval: ReturnType<typeof setInterval>;
@ -44,26 +48,26 @@
]
});
ms.setActionHandler('play', function () {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
audioPlayer.play();
paused = false;
});
ms.setActionHandler('pause', function () {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
audioPlayer.pause();
paused = true;
});
ms.setActionHandler('seekbackward', function () {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0;
}
});
ms.setActionHandler('previoustrack', function () {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0;
}
@ -85,7 +89,7 @@
prepared.push('cover');
});
localforage.getItem(`${audioId}-file`, function (err, file) {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
if (file) {
const f = file as File;
audioFile = f;
@ -99,10 +103,26 @@
if (file) {
const f = file as File;
f.text().then((lr) => {
originalLyrics = lrcParser(lr);
if (!originalLyrics.scripts) return;
for (const line of originalLyrics.scripts) {
lyricsText.push(line.text);
if (f.name.endsWith('.ttml')) {
lyricLines = parseTTML(lr).lyricLines;
hasLyrics = true;
} else if (f.name.endsWith('.lrc')) {
lyricLines = parseLrc(lr).map((line, i, lines) => ({
words: [
{
word: line.words[0]?.word ?? '',
startTime: line.words[0]?.startTime ?? 0,
endTime: lines[i + 1]?.words?.[0]?.startTime ?? Infinity
}
],
startTime: line.words[0]?.startTime ?? 0,
endTime: lines[i + 1]?.words?.[0]?.startTime ?? Infinity,
translatedLyric: '',
romanLyric: '',
isBG: false,
isDuet: false
}));
hasLyrics = true;
}
});
}
@ -110,7 +130,7 @@
}
function playAudio() {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
if (audioPlayer.duration) {
duration = audioPlayer.duration;
}
@ -140,6 +160,7 @@
if (audioPlayer) {
audioPlayer.currentTime = duration * progress;
currentProgress = duration * progress;
lyricPlayer.calcLayout(false, true);
}
}
@ -158,17 +179,19 @@
$: {
clearInterval(mainInterval);
mainInterval = setInterval(() => {
if (audioPlayer===null) return;
if ($userAdjustingProgress === false)
currentProgress = audioPlayer.currentTime;
if (audioPlayer === null) return;
if ($userAdjustingProgress === false) currentProgress = audioPlayer.currentTime;
progressBarRaw.set(audioPlayer.currentTime);
}, 50);
}
onMount(() => {
if (audioPlayer===null) return;
audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1;
});
onMount(() => {
if (audioPlayer === null) return;
audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1;
});
onDestroy(() => {
if (audioPlayer === null) return;
});
$: {
if (audioPlayer) {
@ -177,7 +200,10 @@
}
}
$: hasLyrics = !!originalLyrics;
function onLyricLineClick(e: LyricLineMouseEvent) {
lyricPlayer.resetScroll();
adjustProgress(lyricLines[e.lineIndex].startTime / 1000 / duration);
}
readDB();
</script>
@ -202,18 +228,24 @@
{hasLyrics}
/>
<Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer}/>
<NewLyrics
{lyricPlayer}
{lyricLines}
currentTime={Math.round(currentProgress * 1000)}
playing={!paused}
{onLyricLineClick}
/>
<audio
bind:this={audioPlayer}
controls
style="display: none"
on:play={() => {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
paused = audioPlayer.paused;
}}
on:pause={() => {
if (audioPlayer===null) return;
if (audioPlayer === null) return;
paused = audioPlayer.paused;
}}
on:ended={() => {

View File

@ -2,9 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';
import wasm from 'vite-plugin-wasm';
export default defineConfig({
plugins: [sveltekit()],
plugins: [sveltekit(), wasm()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},
@ -27,7 +28,7 @@ export default defineConfig({
},
server: {
fs: {
allow: ["./package.json"]
allow: ['./package.json']
}
}
});