.
\ No newline at end of file
diff --git a/bun.lockb b/bun.lockb
index d65fe08..383e354 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 0342a7d..a627f21 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "aquavox",
- "version": "1.12.13",
+ "version": "1.12.15",
"private": false,
"scripts": {
"dev": "vite dev",
@@ -48,6 +48,7 @@
"music-metadata-browser": "^2.5.10",
"node-cache": "^5.1.2",
"rollup-plugin-node-polyfills": "^0.2.1",
+ "typescript-parsec": "^0.3.4",
"uuid": "^9.0.1"
}
}
diff --git a/src/lib/components/interactiveBox.svelte b/src/lib/components/interactiveBox.svelte
index 0bbf0e5..1d55162 100644
--- a/src/lib/components/interactiveBox.svelte
+++ b/src/lib/components/interactiveBox.svelte
@@ -3,6 +3,7 @@
import { onMount } from 'svelte';
import userAdjustingProgress from '$lib/state/userAdjustingProgress';
import progressBarSlideValue from '$lib/state/progressBarSlideValue';
+ import truncate from '$lib/truncate';
export let name: string;
export let singer: string = '';
@@ -49,10 +50,6 @@
progressBarSlideValue.set((e.offsetX / progressBar.getBoundingClientRect().width) * duration);
}
- function truncate(value: number, min: number, max: number) {
- return Math.min(Math.max(value, min), max);
- }
-
function progressBarMouseUp(offsetX: number) {
adjustDisplayProgress(offsetX / progressBar.getBoundingClientRect().width);
}
diff --git a/src/lib/components/lyrics.svelte b/src/lib/components/lyrics.svelte
index 92cd78f..59135ca 100644
--- a/src/lib/components/lyrics.svelte
+++ b/src/lib/components/lyrics.svelte
@@ -5,6 +5,7 @@
import type { LrcJsonData } from 'lrc-parser-ts';
import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import nextUpdate from '$lib/state/nextUpdate';
+ import truncate from '$lib/truncate';
// Component input properties
export let lyrics: string[];
@@ -38,6 +39,22 @@
$: refs = _refs.filter(Boolean);
$: getLyricIndex = createLyricsSearcher(originalLyrics);
+
+ // handle KeyDown event
+ function onKeyDown(e: KeyboardEvent) {
+ if (e.altKey && e.shiftKey && (e.metaKey || e.key === 'OS') && e.key === 'Enter') {
+ debugMode = !debugMode;
+ localStorage.setItem('debugMode', debugMode ? 'true' : 'false');
+ }
+ }
+
+ // using for debug mode
+ function extractTranslateValue(s: string): string | null {
+ const regex = /translateY\((-?\d*px)\)/;
+ let arr = regex.exec(s);
+ return arr==null ? null : arr[1];
+ }
+
// Helper function to get CSS class for a lyric based on its index and progress
function getClass(lyricIndex: number, progress: number) {
if (!originalLyrics.scripts) return 'previous-lyric';
@@ -117,7 +134,110 @@
}, 500);
}
- // Handle user adjusting progress state changes
+ // Handle scroll events in the lyrics container
+ function scrollHandler() {
+ scrolling = !scriptScrolling;
+ if (scrolling && originalLyrics.scripts) {
+ lastScroll = new Date().getTime();
+ for (let i = 0; i < originalLyrics.scripts.length; i++) {
+ if (refs[i]) {
+ refs[i].style.filter = 'blur(0px)';
+ }
+ }
+ }
+ setTimeout(() => {
+ if (new Date().getTime() - lastScroll > 5000) {
+ scrolling = false;
+ }
+ }, 5500);
+ }
+
+ // Utility function to create a sleep/delay
+ function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ // Scroll to corresponding lyric while adjusting progress
+ $: {
+ if ($userAdjustingProgress == true) {
+ const currentLyric = refs[getLyricIndex(progress)];
+ scrollToLyric(currentLyric);
+ }
+ }
+
+ // Update the current lyric and apply blur effect based on the progress
+ // worked in real-time.
+ $: {
+ (() => {
+ if (!lyricsContainer || !originalLyrics.scripts) return;
+
+ const scripts = originalLyrics.scripts;
+ currentPositionIndex = getLyricIndex(progress);
+ const cl = scripts[currentPositionIndex];
+
+ if (cl.start <= progress && progress <= cl.end) {
+ currentLyricIndex = currentPositionIndex;
+ nextUpdate.set(cl.end);
+ } else {
+ currentLyricIndex = -1;
+ nextUpdate.set(cl.start);
+ }
+
+ const currentLyric = refs[currentPositionIndex];
+ if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return;
+
+ for (let i = 0; i < scripts.length; i++) {
+ const offset = Math.abs(i - currentPositionIndex);
+ const blurRadius = Math.min(offset * 0.96, 16);
+ if (refs[i]) {
+ refs[i].style.filter = `blur(${blurRadius}px)`;
+ }
+ }
+ })();
+ }
+
+ // Main function that control's lyrics update during playing
+ // triggered by nextUpdate's update
+ async function lyricsUpdate(){
+ if (
+ currentPositionIndex < 0 ||
+ currentPositionIndex === currentAnimationIndex ||
+ currentPositionIndex === lastAdjustProgress ||
+ $userAdjustingProgress === true ||
+ scrolling
+ ) return;
+
+ const currentLyric = refs[currentPositionIndex];
+ const currentLyricRect = currentLyric.getBoundingClientRect();
+
+ if (originalLyrics.scripts && currentLyricRect.top < 0) return;
+
+ const offsetHeight = truncate(currentLyricRect.top - currentLyricTopMargin, 0, Infinity);
+
+ // prepare current line
+ currentLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
+ opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
+ currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
+
+ for (let i = currentPositionIndex - 1; i >= 0; i--) {
+ refs[i].style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
+ opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
+ refs[i].style.transform = `translateY(${-offsetHeight}px)`;
+ }
+ if (currentPositionIndex + 1 < refs.length) {
+ const nextLyric = refs[currentPositionIndex + 1];
+ nextLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
+ opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
+ nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
+ await moveToNextLine(offsetHeight);
+ }
+ currentAnimationIndex = currentPositionIndex;
+ }
+
+
+ nextUpdate.subscribe(lyricsUpdate)
+
+ // Process while user is adjusting progress
userAdjustingProgress.subscribe((adjusting) => {
if (!originalLyrics) return;
const scripts = originalLyrics.scripts;
@@ -155,117 +275,6 @@
}
});
- // Handle scroll events in the lyrics container
- function scrollHandler() {
- scrolling = !scriptScrolling;
- if (scrolling && originalLyrics.scripts) {
- lastScroll = new Date().getTime();
- for (let i = 0; i < originalLyrics.scripts.length; i++) {
- if (refs[i]) {
- refs[i].style.filter = 'blur(0px)';
- }
- }
- }
- setTimeout(() => {
- if (new Date().getTime() - lastScroll > 5000) {
- scrolling = false;
- }
- }, 5500);
- }
-
- // Utility function to create a sleep/delay
- function sleep(ms: number) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
-
- // Scroll to corresponding lyric while adjusting progress
- $: {
- if ($userAdjustingProgress == true) {
- const currentLyric = refs[getLyricIndex(progress)];
- scrollToLyric(currentLyric);
- }
- }
-
- // Update the current lyric and apply blur effect based on the progress
- $: {
- (() => {
- if (!lyricsContainer || !originalLyrics.scripts) return;
-
- const scripts = originalLyrics.scripts;
- currentPositionIndex = getLyricIndex(progress);
- const cl = scripts[currentPositionIndex];
-
- if (cl.start <= progress && progress <= cl.end) {
- currentLyricIndex = currentPositionIndex;
- nextUpdate.set(cl.end);
- } else {
- currentLyricIndex = -1;
- nextUpdate.set(cl.start);
- }
-
- const currentLyric = refs[currentPositionIndex];
- if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return;
-
- for (let i = 0; i < scripts.length; i++) {
- const offset = Math.abs(i - currentPositionIndex);
- const blurRadius = Math.min(offset * 0.96, 16);
- if (refs[i]) {
- refs[i].style.filter = `blur(${blurRadius}px)`;
- }
- }
- })();
- }
-
- nextUpdate.subscribe(async (nextUpdate) => {
- if (
- currentPositionIndex < 0 ||
- currentPositionIndex === currentAnimationIndex ||
- currentPositionIndex === lastAdjustProgress ||
- $userAdjustingProgress === true ||
- scrolling
- ) return;
-
- const currentLyric = refs[currentPositionIndex];
-
- if (originalLyrics.scripts && currentLyric.getBoundingClientRect().top < 0) return;
-
- const offsetHeight =
- refs[currentPositionIndex].getBoundingClientRect().top -
- currentLyricTopMargin;
-
- // prepare current line
- currentLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
- opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
- currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
-
- for (let i = currentPositionIndex - 1; i >= 0; i--) {
- refs[i].style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
- opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
- refs[i].style.transform = `translateY(${-offsetHeight}px)`;
- }
- if (currentPositionIndex + 1 < refs.length) {
- const nextLyric = refs[currentPositionIndex + 1];
- nextLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
- opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
- nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
- await moveToNextLine(offsetHeight);
- }
- currentAnimationIndex = currentPositionIndex;
- })
-
- function onKeyDown(e: KeyboardEvent) {
- if (e.altKey && e.shiftKey && (e.metaKey || e.key === 'OS') && e.key === 'Enter') {
- debugMode = !debugMode;
- localStorage.setItem('debugMode', debugMode ? 'true' : 'false');
- }
- }
-
- function extractTranslateValue(s: string): string | null {
- const regex = /translateY\((-?\d*px)\)/;
- let arr = regex.exec(s);
- return arr==null ? null : arr[1];
- }
-
@@ -290,6 +299,7 @@
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 lyrics"
bind:this={lyricsContainer}
+ on:scroll={scrollHandler}
>
{#each lyrics as lyric, i}
diff --git a/src/lib/lyrics/parser.ts b/src/lib/lyrics/parser.ts
index 8d83660..68cb401 100644
--- a/src/lib/lyrics/parser.ts
+++ b/src/lib/lyrics/parser.ts
@@ -1,10 +1,33 @@
-export interface ScriptItem {
+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';
+
+
+interface ParserScriptItem {
start: number;
text: string;
- end: number;
- translation?: string;
words?: ScriptWordsItem[];
- singer?: number;
+ translation?: string;
+ singer?: string;
+}
+
+export interface ScriptItem extends ParserScriptItem {
+ end: number;
chorus?: string;
}
@@ -15,10 +38,24 @@ export interface ScriptWordsItem {
endIndex: number;
}
-export interface LrcJsonData {
+export interface LrcMetaData {
ar?: string;
ti?: string;
al?: string;
+ au?: string;
+ length?: string;
+ offset?: string;
+ tool?: string;
+ ve?: string;
+}
+
+export interface ParsedLrc extends LrcMetaData {
+ scripts?: ParserScriptItem[];
+
+ [key: string]: any;
+}
+
+export interface LrcJsonData extends LrcMetaData {
scripts?: ScriptItem[];
[key: string]: any;
@@ -28,22 +65,241 @@ interface IDTag {
[key: string]: string;
}
-export function splitLine(str: string) {
- return str.split('\n').filter((str) => str.trim() !== '');
-}
-
-export function ExtractIDTags(lines: string[]) {
- let result: IDTag = {};
- const IDTagRegex = /^\[(\w*): (.*?)]$/;
- let lastMatch = 0;
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- const matchResult = line.trim().match(IDTagRegex);
- if (matchResult && matchResult.length == 3) {
- const tagName = matchResult[1];
- const tagValue = matchResult[2];
- result[tagName] = tagValue;
- }
+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(): Parser[]> {
+ return rep_sc(str(' '));
+}
+
+const unicodeStr = rep(tok('char'));
+
+function trimmed(p: Parser[]>): Parser[]> {
+ 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(p: Parser): Parser {
+ 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(delim: [Parser>, Parser>]) {
+ 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(tokens: Token[]) {
+ return tokens.map((t) => t.text).join('');
+}
+
+function tokenParserToText(p: Parser> | Parser[]>): Parser {
+ return apply(p, (r: Token | Token[]) => {
+ 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 {
+ 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 words = mainPart
+ .filter((s) => joinTokens(s[1]).trim().length > 0)
+ .map((s) => {
+ const wordBegin = s[0];
+ const word = s[1];
+ let ret: Partial = { start: wordBegin };
+ if (word[0]) {
+ ret.beginIndex = word[0].pos.columnBegin - 1;
+ }
+ if (word[word.length - 1]) {
+ ret.endIndex = word[word.length - 1].pos.columnEnd;
+ }
+ return ret as ScriptWordsItem; // TODO: Complete this
+ });
+
+ const singer = singerPart?.text;
+ const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
+
+ return ['script_item', { start, text, words, 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: Token | undefined): string {
+ if (t === undefined) {
+ return '';
+ }
+ 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, '|'],
+ [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: true });
+ 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++;
+ lyrics.push(lyricLine);
+ }
+ finalLrc.scripts = lyrics;
+ return finalLrc;
}
\ No newline at end of file
diff --git a/src/lib/truncate.ts b/src/lib/truncate.ts
new file mode 100644
index 0000000..c10b4a4
--- /dev/null
+++ b/src/lib/truncate.ts
@@ -0,0 +1,3 @@
+export default function truncate(value: number, min: number, max: number) {
+ return Math.min(Math.max(value, min), max);
+}
\ No newline at end of file
diff --git a/src/routes/play/[id]/+page.svelte b/src/routes/play/[id]/+page.svelte
index 9c2c853..5d6f7f9 100644
--- a/src/routes/play/[id]/+page.svelte
+++ b/src/routes/play/[id]/+page.svelte
@@ -8,12 +8,14 @@
import extractFileName from '$lib/extractFileName';
import localforage from 'localforage';
import { writable } from 'svelte/store';
- import lrcParser, { type LrcJsonData } from 'lrc-parser-ts';
+ 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 progressBarRaw from '$lib/state/progressBarRaw';
const audioId = $page.params.id;
- let audioPlayer: HTMLAudioElement;
+ let audioPlayer: HTMLAudioElement | null = null;
let volume = 1;
let name = '';
let singer = '';
@@ -42,22 +44,26 @@
]
});
ms.setActionHandler('play', function () {
+ if (audioPlayer===null) return;
audioPlayer.play();
paused = false;
});
ms.setActionHandler('pause', function () {
+ if (audioPlayer===null) return;
audioPlayer.pause();
paused = true;
});
ms.setActionHandler('seekbackward', function () {
+ if (audioPlayer===null) return;
if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0;
}
});
ms.setActionHandler('previoustrack', function () {
+ if (audioPlayer===null) return;
if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0;
}
@@ -79,6 +85,7 @@
prepared.push('cover');
});
localforage.getItem(`${audioId}-file`, function (err, file) {
+ if (audioPlayer===null) return;
if (file) {
const f = file as File;
audioFile = f;
@@ -103,6 +110,7 @@
}
function playAudio() {
+ if (audioPlayer===null) return;
if (audioPlayer.duration) {
duration = audioPlayer.duration;
}
@@ -112,7 +120,7 @@
}
$: {
- if (!launched) {
+ if (!launched && audioPlayer) {
const requirements = ['name', 'file', 'cover'];
let flag = true;
for (const r of requirements) {
@@ -150,21 +158,15 @@
$: {
clearInterval(mainInterval);
mainInterval = setInterval(() => {
- if (
- audioPlayer !== null &&
- audioPlayer.currentTime !== undefined
- ) {
- if ($userAdjustingProgress === false)
- currentProgress = audioPlayer.currentTime;
- progressBarRaw.set(audioPlayer.currentTime);
- }
+ if (audioPlayer===null) return;
+ if ($userAdjustingProgress === false)
+ currentProgress = audioPlayer.currentTime;
+ progressBarRaw.set(audioPlayer.currentTime);
}, 50);
}
- import { onMount } from 'svelte';
- import progressBarRaw from '$lib/state/progressBarRaw';
-
onMount(() => {
+ if (audioPlayer===null) return;
audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1;
});
@@ -175,19 +177,13 @@
}
}
- $: {
- if (originalLyrics) {
- hasLyrics = true;
- } else {
- hasLyrics = false;
- }
- }
+ $: hasLyrics = !!originalLyrics;
readDB();
- {name} - Aquavox
+ {name} - AquaVox
diff --git a/src/test/lrcParser.test.ts b/src/test/lrcParser.test.ts
index db31437..f3193a2 100644
--- a/src/test/lrcParser.test.ts
+++ b/src/test/lrcParser.test.ts
@@ -1,19 +1,87 @@
import { describe, expect, it } from 'vitest';
import fs from 'fs';
-import { ExtractIDTags, splitLine } from '$lib/lyrics/parser';
+import { parseLRC } from '$lib/lyrics/parser';
describe('LRC parser test', () => {
const test01Buffer = fs.readFileSync('./src/test/resources/test-01.lrc');
const test01Text = test01Buffer.toString('utf-8');
- it('Line Split', () => {
- const lyrics = test01Text;
- const lines = splitLine(lyrics);
- expect(lines[26]).toBe('[01:52.991]');
+ const test02Buffer = fs.readFileSync('./src/test/resources/test-02.lrc');
+ const test02Text = test02Buffer.toString('utf-8');
+ const test03Buffer = fs.readFileSync('./src/test/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-02.lrc', () => {
+ const result = parseLRC(test02Text, { wordDiv: ' ', strict: true });
+
+ expect(result.ti).toBe("Somebody to Love");
+ expect(result.ar).toBe("Jefferson Airplane");
+ expect(result.scripts!!.length).toBe(3);
+ expect(result.scripts!![0].text).toBe("When the truth is found to be lies");
+ expect(result.scripts!![0].start).toBe(0);
+ expect(result.scripts!![0].words!![1].beginIndex).toBe("[00:00.00] <00:00.04> When <00:00.16> the".indexOf("the"));
+ expect(result.scripts!![0].words!![1].start).toBe(160);
});
- it('IDTag Extract', () => {
- const lyrics = test01Text;
- const lines = splitLine(lyrics);
- const idTags = ExtractIDTags(lines);
- expect(idTags['ar']).toBe('洛天依');
+ 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('Rejects some invalid LRCs', () => {
+ const cases = [
+ "[<00:00.00>] <00:00.04> When <00:00.16> the",
+ "[00:00.00] <00:00.04> <00:00.16> the",
+ "[00:00.00> <00:00.04> When <00:00.16> the",
+ "<00:00.00> <00:00.04> When <00:00.16> the",
+ "<1:00:00.00> <00:00.04> When <00:00.16> the",
+ ]
+ for (const c of cases) {
+ expect(() => parseLRC(c, { strict: true })).toThrow();
+ }
+ })
+ it('Accepts some weird but parsable LRCs', () => {
+ const cases = [
+ "[ti: []]",
+ "[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();
+ }
+ })
+ it('Parses a legacy LRC', () => {
+ const result = parseLRC(test02Text, { wordDiv: ' ', strict: true, legacy: true });
+
+ expect(result.ti).toBe("Somebody to Love");
+ expect(result.ar).toBe("Jefferson Airplane");
+ expect(result.scripts!!.length).toBe(3);
+ expect(result.scripts!![1].text).toBe("<00:07.67> And <00:07.94> all <00:08.36> the <00:08.63> joy <00:10.28> within <00:10.53> you <00:13.09> dies");
+ expect(result.scripts!![1].start).toBe(6000 + 470);
+ result.scripts!!.forEach((s) => expect(s.words).not.toBeDefined());
});
});
\ No newline at end of file
diff --git a/src/test/resources/test-02.lrc b/src/test/resources/test-02.lrc
new file mode 100644
index 0000000..5f67268
--- /dev/null
+++ b/src/test/resources/test-02.lrc
@@ -0,0 +1,9 @@
+[ti: Somebody to Love]
+[ar: Jefferson Airplane]
+[al: Surrealistic Pillow]
+[lr: Lyricists of that song]
+[length: 2:58]
+
+[00:00.00] <00:00.04> When <00:00.16> the <00:00.82> truth <00:01.29> is <00:01.63> found <00:03.09> to <00:03.37> be <00:05.92> lies
+[00:06.47] <00:07.67> And <00:07.94> all <00:08.36> the <00:08.63> joy <00:10.28> within <00:10.53> you <00:13.09> dies
+[00:13.34] <00:14.32> Don't <00:14.73> you <00:15.14> want <00:15.57> somebody <00:16.09> to <00:16.46> love
\ No newline at end of file
diff --git a/src/test/resources/test-03.lrc b/src/test/resources/test-03.lrc
new file mode 100644
index 0000000..20a669c
--- /dev/null
+++ b/src/test/resources/test-03.lrc
@@ -0,0 +1,77 @@
+[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]
\ No newline at end of file