improve: better lyric effect & performance

add: support for TTML
This commit is contained in:
alikia2x 2024-08-03 21:34:39 +08:00
parent e8b7a6e3d2
commit ba31bc4b98
7 changed files with 528 additions and 31 deletions

View File

@ -6,6 +6,7 @@
import progressBarSlideValue from '$lib/state/progressBarSlideValue'; import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import nextUpdate from '$lib/state/nextUpdate'; import nextUpdate from '$lib/state/nextUpdate';
import truncate from '$lib/truncate'; import truncate from '$lib/truncate';
import { blur } from 'svelte/transition';
// Component input properties // Component input properties
export let lyrics: string[]; export let lyrics: string[];
@ -88,7 +89,7 @@
for (let i = processingLineIndex; i < refs.length; i++) { for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i]; const lyric = refs[i];
lyric.style.transition = lyric.style.transition =
`transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, `transform .5s cubic-bezier(.16,.02,.38,.98), filter 200ms ease, opacity 200ms ease,
font-size 200ms ease, scale 250ms ease`; font-size 200ms ease, scale 250ms ease`;
lyric.style.transform = `translateY(${-h}px)`; lyric.style.transform = `translateY(${-h}px)`;
processingLineIndex = i; processingLineIndex = i;
@ -101,7 +102,7 @@
for (let i = processingLineIndex; i < refs.length; i++) { for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i]; const lyric = refs[i];
lyric.style.transition = lyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease'; 'transform .5s cubic-bezier(.16,.02,.38,.98), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
lyric.style.transform = `translateY(${-h}px)`; lyric.style.transform = `translateY(${-h}px)`;
processingLineIndex = i; processingLineIndex = i;
await sleep(75); await sleep(75);
@ -200,9 +201,13 @@
const currentLyric = refs[currentPositionIndex]; const currentLyric = refs[currentPositionIndex];
if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return; if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return;
for (let i = 0; i < scripts.length; i++) { for (let i = 0; i < refs.length; i++) {
const offset = Math.abs(i - currentPositionIndex); const offset = Math.abs(i - currentPositionIndex);
const blurRadius = Math.min(offset * 0.96, 16); let blurRadius = Math.min(offset * 1.25, 16);
const rect = refs[i].getBoundingClientRect();
if (rect.top + rect.height < 0 || rect.top > lyricsContainer.getBoundingClientRect().height) {
blurRadius = 0;
}
if (refs[i]) { if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`; refs[i].style.filter = `blur(${blurRadius}px)`;
} }
@ -210,6 +215,21 @@
})(); })();
} }
function getViewportRange() {
let min = 0;
let max = 0;
for (let i = 0; i < refs.length; i++) {
const element = refs[i];
if (element.getBoundingClientRect().top < 0) {
min = i;
}
else if (element.getBoundingClientRect().bottom < 0) {
max = i;
return [min, max];
}
}
}
// Main function that control's lyrics update during playing // Main function that control's lyrics update during playing
// triggered by nextUpdate's update // triggered by nextUpdate's update
async function lyricsUpdate(){ async function lyricsUpdate(){
@ -228,18 +248,20 @@
const offsetHeight = truncate(currentLyricRect.top - currentLyricTopMargin, 0, Infinity); const offsetHeight = truncate(currentLyricRect.top - currentLyricTopMargin, 0, Infinity);
// prepare current line // prepare current line
currentLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, currentLyric.style.transition = `transform .5s cubic-bezier(.16,.02,.38,.98), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`; opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
currentLyric.style.transform = `translateY(${-offsetHeight}px)`; currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
// prepare past lines
for (let i = currentPositionIndex - 1; i >= 0; i--) { for (let i = currentPositionIndex - 1; i >= 0; i--) {
refs[i].style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, refs[i].style.transition = `transform .5s cubic-bezier(.16,.02,.38,.98), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`; opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
refs[i].style.transform = `translateY(${-offsetHeight}px)`; refs[i].style.transform = `translateY(${-offsetHeight}px)`;
} }
await sleep(75);
if (currentPositionIndex + 1 < refs.length) { if (currentPositionIndex + 1 < refs.length) {
const nextLyric = refs[currentPositionIndex + 1]; const nextLyric = refs[currentPositionIndex + 1];
nextLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, nextLyric.style.transition = `transform .5s cubic-bezier(.16,.02,.38,.98), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`; opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
nextLyric.style.transform = `translateY(${-offsetHeight}px)`; nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
await moveToNextLine(offsetHeight); await moveToNextLine(offsetHeight);
@ -347,9 +369,9 @@
--lyric-mobile-line-height: 2.4rem; --lyric-mobile-line-height: 2.4rem;
--lyric-mobile-margin: 1.5rem 0; --lyric-mobile-margin: 1.5rem 0;
--lyric-mobile-font-weight: 600; --lyric-mobile-font-weight: 600;
--lyric-desktop-font-size: 3.5rem; --lyric-desktop-font-size: 3rem;
--lyric-desktop-line-height: 4.5rem; --lyric-desktop-line-height: 4.5rem;
--lyric-desktop-margin: 1.75rem 0; --lyric-desktop-margin: 2.75rem 0;
} }
.lyrics { .lyrics {

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

View File

@ -13,6 +13,7 @@
import type { IAudioMetadata } from 'music-metadata-browser'; import type { IAudioMetadata } from 'music-metadata-browser';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import progressBarRaw from '$lib/state/progressBarRaw'; import progressBarRaw from '$lib/state/progressBarRaw';
import { parseTTML, type LyricLine, type LyricWord } from '$lib/ttml';
const audioId = $page.params.id; const audioId = $page.params.id;
let audioPlayer: HTMLAudioElement | null = null; let audioPlayer: HTMLAudioElement | null = null;
@ -44,26 +45,26 @@
] ]
}); });
ms.setActionHandler('play', function () { ms.setActionHandler('play', function () {
if (audioPlayer===null) return; if (audioPlayer === null) return;
audioPlayer.play(); audioPlayer.play();
paused = false; paused = false;
}); });
ms.setActionHandler('pause', function () { ms.setActionHandler('pause', function () {
if (audioPlayer===null) return; if (audioPlayer === null) return;
audioPlayer.pause(); audioPlayer.pause();
paused = true; paused = true;
}); });
ms.setActionHandler('seekbackward', function () { ms.setActionHandler('seekbackward', function () {
if (audioPlayer===null) return; if (audioPlayer === null) return;
if (audioPlayer.currentTime > 4) { if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0; audioPlayer.currentTime = 0;
} }
}); });
ms.setActionHandler('previoustrack', function () { ms.setActionHandler('previoustrack', function () {
if (audioPlayer===null) return; if (audioPlayer === null) return;
if (audioPlayer.currentTime > 4) { if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0; audioPlayer.currentTime = 0;
} }
@ -85,7 +86,7 @@
prepared.push('cover'); prepared.push('cover');
}); });
localforage.getItem(`${audioId}-file`, function (err, file) { localforage.getItem(`${audioId}-file`, function (err, file) {
if (audioPlayer===null) return; if (audioPlayer === null) return;
if (file) { if (file) {
const f = file as File; const f = file as File;
audioFile = f; audioFile = f;
@ -99,18 +100,36 @@
if (file) { if (file) {
const f = file as File; const f = file as File;
f.text().then((lr) => { f.text().then((lr) => {
if (f.name.endsWith('.ttml')) {
const lyricLines = parseTTML(lr).lyricLines;
originalLyrics = {
scripts: lyricLines.map((value: LyricLine, index: number, array: LyricLine[]) => {
return {
text: value.words.map(word => word.word).join(''),
start: value.startTime / 1000,
end: value.endTime / 1000,
translation: value.translatedLyric || undefined
};
})
};
for (const line of originalLyrics.scripts!) {
lyricsText.push(line.text);
}
hasLyrics = true;
} else if (f.name.endsWith('.lrc')) {
originalLyrics = lrcParser(lr); originalLyrics = lrcParser(lr);
if (!originalLyrics.scripts) return; if (!originalLyrics.scripts) return;
for (const line of originalLyrics.scripts) { for (const line of originalLyrics.scripts) {
lyricsText.push(line.text); lyricsText.push(line.text);
} }
}
}); });
} }
}); });
} }
function playAudio() { function playAudio() {
if (audioPlayer===null) return; if (audioPlayer === null) return;
if (audioPlayer.duration) { if (audioPlayer.duration) {
duration = audioPlayer.duration; duration = audioPlayer.duration;
} }
@ -158,15 +177,14 @@
$: { $: {
clearInterval(mainInterval); clearInterval(mainInterval);
mainInterval = setInterval(() => { mainInterval = setInterval(() => {
if (audioPlayer===null) return; if (audioPlayer === null) return;
if ($userAdjustingProgress === false) if ($userAdjustingProgress === false) currentProgress = audioPlayer.currentTime;
currentProgress = audioPlayer.currentTime;
progressBarRaw.set(audioPlayer.currentTime); progressBarRaw.set(audioPlayer.currentTime);
}, 50); }, 50);
} }
onMount(() => { onMount(() => {
if (audioPlayer===null) return; if (audioPlayer === null) return;
audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1; audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1;
}); });
@ -202,18 +220,18 @@
{hasLyrics} {hasLyrics}
/> />
<Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer}/> <Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer} />
<audio <audio
bind:this={audioPlayer} bind:this={audioPlayer}
controls controls
style="display: none" style="display: none"
on:play={() => { on:play={() => {
if (audioPlayer===null) return; if (audioPlayer === null) return;
paused = audioPlayer.paused; paused = audioPlayer.paused;
}} }}
on:pause={() => { on:pause={() => {
if (audioPlayer===null) return; if (audioPlayer === null) return;
paused = audioPlayer.paused; paused = audioPlayer.paused;
}} }}
on:ended={() => { on:ended={() => {