Compare commits
No commits in common. "5988c8335cfcdb0de2acfff048158ad17a07d5e0" and "5af6992632657a6219a151faab5c250c8575f2b2" have entirely different histories.
5988c8335c
...
5af6992632
@ -1,6 +1,6 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Web Dev Server" type="ShConfigurationType" focusToolWindowBeforeRun="true">
|
<configuration default="false" name="Run Dev Server" type="ShConfigurationType" focusToolWindowBeforeRun="true">
|
||||||
<option name="SCRIPT_TEXT" value="bun web:dev" />
|
<option name="SCRIPT_TEXT" value="bun dev" />
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||||
<option name="SCRIPT_PATH" value="" />
|
<option name="SCRIPT_PATH" value="" />
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
<option name="SCRIPT_OPTIONS" value="" />
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aquavox",
|
"name": "aquavox",
|
||||||
"version": "2.5.1",
|
"version": "2.3.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -5,15 +5,11 @@
|
|||||||
import type { Spring } from '@core/graphics/spring/spring';
|
import type { Spring } from '@core/graphics/spring/spring';
|
||||||
|
|
||||||
const viewportWidth = document.documentElement.clientWidth;
|
const viewportWidth = document.documentElement.clientWidth;
|
||||||
const blurRatio = viewportWidth > 640 ? 1 : 1.4;
|
|
||||||
|
|
||||||
export let line: ScriptItem;
|
export let line: ScriptItem;
|
||||||
export let index: number;
|
export let index: number;
|
||||||
export let debugMode: Boolean;
|
export let debugMode: Boolean;
|
||||||
export let lyricClick: Function;
|
export let lyricClick: Function;
|
||||||
export let progress: number;
|
|
||||||
export let currentLyricIndex: number;
|
|
||||||
export let scrolling: boolean;
|
|
||||||
|
|
||||||
let ref: HTMLDivElement;
|
let ref: HTMLDivElement;
|
||||||
let clickMask: HTMLSpanElement;
|
let clickMask: HTMLSpanElement;
|
||||||
@ -80,21 +76,9 @@
|
|||||||
opacity = isCurrent ? 1 : 0.36;
|
opacity = isCurrent ? 1 : 0.36;
|
||||||
};
|
};
|
||||||
|
|
||||||
$: {
|
export const setBlur = (blur: number) => {
|
||||||
if (ref && ref.style) {
|
ref.style.filter = `blur(${blur}px)`;
|
||||||
let blurRadius = 0;
|
};
|
||||||
const offset = Math.abs(index - currentLyricIndex);
|
|
||||||
if (progress > line.end) {
|
|
||||||
blurRadius = Math.min(offset * blurRatio, 16);
|
|
||||||
} else if (line.start <= progress && progress <= line.end) {
|
|
||||||
blurRadius = 0;
|
|
||||||
} else {
|
|
||||||
blurRadius = Math.min(offset * blurRatio, 16);
|
|
||||||
}
|
|
||||||
if (scrolling) blurRadius=0;
|
|
||||||
ref.style.filter = `blur(${blurRadius}px)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const update = (pos: LyricPos, delay: number = 0) => {
|
export const update = (pos: LyricPos, delay: number = 0) => {
|
||||||
if (lastPosX === undefined || lastPosY === undefined) {
|
if (lastPosX === undefined || lastPosY === undefined) {
|
||||||
@ -139,25 +123,25 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
style="transform: translate3d({positionX}px, {positionY}px, 0); transition-property: opacity, text-shadow;
|
||||||
|
transition-duration: 0.36s; transition-timing-function: ease-out; opacity: {opacity};
|
||||||
|
transform-origin: center left;"
|
||||||
class="absolute z-50 w-full pr-12 lg:pr-16 cursor-default py-5"
|
class="absolute z-50 w-full pr-12 lg:pr-16 cursor-default py-5"
|
||||||
on:click={() => {
|
bind:this={ref}
|
||||||
lyricClick(index);
|
on:touchstart={() => {
|
||||||
|
clickMask.style.backgroundColor = 'rgba(255,255,255,.3)';
|
||||||
}}
|
}}
|
||||||
on:touchend={() => {
|
on:touchend={() => {
|
||||||
clickMask.style.backgroundColor = 'transparent';
|
clickMask.style.backgroundColor = 'transparent';
|
||||||
}}
|
}}
|
||||||
on:touchstart={() => {
|
on:click={() => {
|
||||||
clickMask.style.backgroundColor = 'rgba(255,255,255,.3)';
|
lyricClick(index);
|
||||||
}}
|
}}
|
||||||
style="transform: translate3d({positionX}px, {positionY}px, 0); transition-property: text-shadow;
|
|
||||||
transition-duration: 0.36s; transition-timing-function: ease-out;
|
|
||||||
transform-origin: center left;"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
bind:this={clickMask}
|
|
||||||
class="absolute w-[calc(100%-2.5rem)] lg:w-[calc(100%-3rem)] h-full
|
class="absolute w-[calc(100%-2.5rem)] lg:w-[calc(100%-3rem)] h-full
|
||||||
-translate-x-2 lg:-translate-x-5 -translate-y-5 rounded-lg duration-300 lg:hover:bg-[rgba(255,255,255,.15)] z-[100] "
|
-translate-x-2 lg:-translate-x-5 -translate-y-5 rounded-lg duration-300 lg:hover:bg-[rgba(255,255,255,.15)]"
|
||||||
|
bind:this={clickMask}
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
{#if debugMode}
|
{#if debugMode}
|
||||||
@ -165,45 +149,26 @@
|
|||||||
{index}: duration: {(line.end - line.start).toFixed(3)}, {line.start.toFixed(3)}~{line.end.toFixed(3)}
|
{index}: duration: {(line.end - line.start).toFixed(3)}, {line.start.toFixed(3)}~{line.end.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if line.words !== undefined && line.words.length > 0}
|
<span
|
||||||
<span
|
class={`text-white text-[2rem] leading-9 lg:text-5xl lg:leading-[4rem] font-semibold text-shadow-lg mr-4
|
||||||
class={`text-white text-[2rem] leading-9 lg:text-5xl lg:leading-[4rem] font-semibold mr-4 `}
|
${isCurrentLyric ? 'text-glow' : ''}`}
|
||||||
>
|
>
|
||||||
{#each line.words as word}
|
{line.text}
|
||||||
{#if word.word}
|
</span>
|
||||||
{#each word.word.split("") as chr, i}
|
|
||||||
<span
|
|
||||||
class={(line.start <= progress && progress <= line.end && progress > (word.endTime - word.startTime) * ((i)/word.word.length) + word.startTime ? "opacity-100 text-glow" : "opacity-35") + " inline-block duration-300"}
|
|
||||||
>
|
|
||||||
{chr}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span
|
|
||||||
class={`text-white text-[2rem] leading-9 lg:text-5xl lg:leading-[4rem] font-semibold mr-4 duration-200 ${line.start <= progress && progress <= line.end ? "opacity-100 text-glow" : "opacity-35"}`}
|
|
||||||
>
|
|
||||||
{line.text}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if line.translation}
|
{#if line.translation}
|
||||||
<br />
|
<br />
|
||||||
<span class={`pl-2 relative text-xl lg:text-2xl top-2 duration-300 text-white`}>
|
<span class={`pl-2 relative text-xl lg:text-2xl top-2 duration-300`}>
|
||||||
{line.translation}
|
{line.translation}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.text-glow {
|
.text-glow {
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 3px #ffffff2c,
|
0 0 3px #ffffff2c,
|
||||||
0 0 6px #ffffff2c,
|
0 0 6px #ffffff2c,
|
||||||
0 15px 30px rgba(0, 0, 0, 0.11),
|
0 15px 30px rgba(0, 0, 0, 0.11),
|
||||||
0 5px 15px rgba(0, 0, 0, 0.08);
|
0 5px 15px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -75,15 +75,17 @@
|
|||||||
const relativeOrigin = lyricTopList[currentLyricIndex] - currentLyricTop;
|
const relativeOrigin = lyricTopList[currentLyricIndex] - currentLyricTop;
|
||||||
for (let i = 0; i < lyricElements.length; i++) {
|
for (let i = 0; i < lyricElements.length; i++) {
|
||||||
const currentLyricComponent = lyricComponents[i];
|
const currentLyricComponent = lyricComponents[i];
|
||||||
const lyric = originalLyrics.scripts[i];
|
|
||||||
let delay = 0;
|
let delay = 0;
|
||||||
if (progress > lyric.end) {
|
if (i < currentLyricIndex) {
|
||||||
delay = 0;
|
delay = 0;
|
||||||
} else if (lyric.start <= progress && progress <= lyric.end) {
|
} else if (i == currentLyricIndex) {
|
||||||
delay = 0.042;
|
delay = 0.042;
|
||||||
} else {
|
} else {
|
||||||
delay = Math.min(Math.min(currentLyricDuration, 0.6), 0.067 * (i - currentLyricIndex + 1.2));
|
delay = Math.min(Math.min(currentLyricDuration, 0.6), 0.067 * (i - currentLyricIndex + 1.2));
|
||||||
}
|
}
|
||||||
|
const offset = Math.abs(i - currentLyricIndex);
|
||||||
|
let blurRadius = Math.min(offset * blurRatio, 16);
|
||||||
|
currentLyricComponent.setBlur(blurRadius);
|
||||||
currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay);
|
currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +107,7 @@
|
|||||||
for (let i = 0; i < lyricElements.length; i++) {
|
for (let i = 0; i < lyricElements.length; i++) {
|
||||||
const currentLyricComponent = lyricComponents[i];
|
const currentLyricComponent = lyricComponents[i];
|
||||||
const currentY = currentLyricComponent.getInfo().y;
|
const currentY = currentLyricComponent.getInfo().y;
|
||||||
scrolling = true;
|
currentLyricComponent.setBlur(0);
|
||||||
currentLyricComponent.stop();
|
currentLyricComponent.stop();
|
||||||
currentLyricComponent.setY(currentY - deltaY);
|
currentLyricComponent.setY(currentY - deltaY);
|
||||||
}
|
}
|
||||||
@ -201,21 +203,21 @@
|
|||||||
lastProgress = progress;
|
lastProgress = progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// $: {
|
$: {
|
||||||
// for (let i = 0; i < lyricElements.length; i++) {
|
for (let i = 0; i < lyricElements.length; i++) {
|
||||||
// const s = originalLyrics.scripts![i].start;
|
const s = originalLyrics.scripts![i].start;
|
||||||
// const t = originalLyrics.scripts![i].end;
|
const t = originalLyrics.scripts![i].end;
|
||||||
// // Explain:
|
// Explain:
|
||||||
// // The `currentLyricIndex` is also used for locating & layout computing,
|
// The `currentLyricIndex` is also used for locating & layout computing,
|
||||||
// // so when the current progress is in the interlude between two lyrics,
|
// so when the current progress is in the interlude between two lyrics,
|
||||||
// // `currentLyricIndex` still needs to have a valid value to ensure that
|
// `currentLyricIndex` still needs to have a valid value to ensure that
|
||||||
// // the style and scrolling position are calculated correctly.
|
// the style and scrolling position are calculated correctly.
|
||||||
// // But in that situation, the “current lyric index” does not exist.
|
// But in that situation, the “current lyric index” does not exist.
|
||||||
// const isCurrent = i == currentLyricIndex && s <= progress && progress <= t;
|
const isCurrent = i == currentLyricIndex && s <= progress && progress <= t;
|
||||||
// const currentLyricComponent = lyricComponents[i];
|
const currentLyricComponent = lyricComponents[i];
|
||||||
// currentLyricComponent.setCurrent(isCurrent);
|
currentLyricComponent.setCurrent(isCurrent);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize
|
// Initialize
|
||||||
@ -261,7 +263,7 @@
|
|||||||
bind:this={lyricsContainer}
|
bind:this={lyricsContainer}
|
||||||
>
|
>
|
||||||
{#each lyricLines as lyric, i}
|
{#each lyricLines as lyric, i}
|
||||||
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} {lyricClick} {progress} {currentLyricIndex} {scrolling}/>
|
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} {lyricClick} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
270
packages/core/lyrics/LRCparser.ts
Normal file
270
packages/core/lyrics/LRCparser.ts
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
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, ScriptWordsItem } from '../type';
|
||||||
|
import type { IDTag } from './type';
|
||||||
|
|
||||||
|
|
||||||
|
interface ParserScriptItem {
|
||||||
|
start: number;
|
||||||
|
text: string;
|
||||||
|
words?: ScriptWordsItem[];
|
||||||
|
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 words = mainPart
|
||||||
|
.filter((s) => joinTokens(s[1]).trim().length > 0)
|
||||||
|
.map((s) => {
|
||||||
|
const wordBegin = s[0];
|
||||||
|
const word = s[1];
|
||||||
|
let ret: Partial<ScriptWordsItem> = { 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>(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, '>'],
|
||||||
|
[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;
|
||||||
|
}
|
@ -16,13 +16,14 @@ import {
|
|||||||
tok,
|
tok,
|
||||||
type Token
|
type Token
|
||||||
} from 'typescript-parsec';
|
} from 'typescript-parsec';
|
||||||
import type { LrcJsonData, ParsedLrc, ScriptItem } from '../type';
|
import type { LrcJsonData, ParsedLrc, ScriptItem, ScriptWordsItem } from '../type';
|
||||||
import type { IDTag } from './type';
|
import type { IDTag } from './type';
|
||||||
|
|
||||||
|
|
||||||
interface ParserScriptItem {
|
interface ParserScriptItem {
|
||||||
start: number;
|
start: number;
|
||||||
text: string;
|
text: string;
|
||||||
|
words?: ScriptWordsItem[];
|
||||||
translation?: string;
|
translation?: string;
|
||||||
singer?: string;
|
singer?: string;
|
||||||
}
|
}
|
||||||
@ -165,10 +166,25 @@ function lrcLine(
|
|||||||
.filter((s) => s.trim().length > 0)
|
.filter((s) => s.trim().length > 0)
|
||||||
.join(wordDiv);
|
.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<ScriptWordsItem> = { 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 singer = singerPart?.text;
|
||||||
const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
|
const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
|
||||||
|
|
||||||
return ['script_item', { start, text, singer, translation } as ParserScriptItem];
|
return ['script_item', { start, text, words, singer, translation } as ParserScriptItem];
|
||||||
}),
|
}),
|
||||||
apply(lrcTag, (r) => ['lrc_tag', r as IDTag]),
|
apply(lrcTag, (r) => ['lrc_tag', r as IDTag]),
|
||||||
apply(seq(spaces(), str('#'), unicodeStr), (cmt) => ['comment', cmt[2].join('')] as const),
|
apply(seq(spaces(), str('#'), unicodeStr), (cmt) => ['comment', cmt[2].join('')] as const),
|
||||||
@ -190,6 +206,8 @@ export function parseLRC(
|
|||||||
const tokenizer = buildLexer([
|
const tokenizer = buildLexer([
|
||||||
[true, /^\[/gu, '['],
|
[true, /^\[/gu, '['],
|
||||||
[true, /^\]/gu, ']'],
|
[true, /^\]/gu, ']'],
|
||||||
|
[true, /^</gu, '<'],
|
||||||
|
[true, /^>/gu, '>'],
|
||||||
[true, /^\|/gu, '|'],
|
[true, /^\|/gu, '|'],
|
||||||
[true, /^./gu, 'char']
|
[true, /^./gu, 'char']
|
||||||
]);
|
]);
|
||||||
|
2
packages/core/lyrics/lrc/type.d.ts
vendored
2
packages/core/lyrics/lrc/type.d.ts
vendored
@ -1,5 +1,3 @@
|
|||||||
import type { ScriptWordsItem } from "../type";
|
|
||||||
|
|
||||||
export interface ParserScriptItem {
|
export interface ParserScriptItem {
|
||||||
start: number;
|
start: number;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { LrcJsonData } from '../type';
|
import type { LrcJsonData } from '../type';
|
||||||
import { parseTTML as ttmlParser } from './parser';
|
import { parseTTML as ttmlParser } from './parser';
|
||||||
import type { LyricLine } from './ttml-types';
|
import type { LyricLine } from './ttml-types';
|
||||||
|
|
||||||
export * from './writer';
|
export * from './writer';
|
||||||
export type * from './ttml-types';
|
export type * from './ttml-types';
|
||||||
|
|
||||||
@ -10,22 +9,11 @@ export function parseTTML(text: string) {
|
|||||||
const lyricLines = ttmlParser(text).lyricLines;
|
const lyricLines = ttmlParser(text).lyricLines;
|
||||||
lyrics = {
|
lyrics = {
|
||||||
scripts: lyricLines.map((value: LyricLine, index: number, array: LyricLine[]) => {
|
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 {
|
return {
|
||||||
text: value.words.map((word) => word.word).join(''),
|
text: value.words.map((word) => word.word).join(''),
|
||||||
start: value.startTime / 1000,
|
start: value.startTime / 1000,
|
||||||
end: value.endTime / 1000,
|
end: value.endTime / 1000,
|
||||||
translation: value.translatedLyric || undefined,
|
translation: value.translatedLyric || undefined
|
||||||
singer: value.singer || undefined,
|
|
||||||
words: words
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
@ -78,7 +78,6 @@ export function parseTTML(ttmlText: string): TTMLLyric {
|
|||||||
lineEl.getAttribute("ttm:agent") !== mainAgentId,
|
lineEl.getAttribute("ttm:agent") !== mainAgentId,
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: 0,
|
endTime: 0,
|
||||||
singer: ""
|
|
||||||
};
|
};
|
||||||
if (isBG) line.isDuet = isDuet;
|
if (isBG) line.isDuet = isDuet;
|
||||||
let haveBg = false;
|
let haveBg = false;
|
||||||
@ -102,8 +101,6 @@ export function parseTTML(ttmlText: string): TTMLLyric {
|
|||||||
line.translatedLyric = wordEl.innerHTML;
|
line.translatedLyric = wordEl.innerHTML;
|
||||||
} else if (role === "x-roman") {
|
} else if (role === "x-roman") {
|
||||||
line.romanLyric = wordEl.innerHTML;
|
line.romanLyric = wordEl.innerHTML;
|
||||||
} else if (role === "x-singer") {
|
|
||||||
line.singer = wordEl.innerHTML;
|
|
||||||
}
|
}
|
||||||
} else if (wordEl.hasAttribute("begin") && wordEl.hasAttribute("end")) {
|
} else if (wordEl.hasAttribute("begin") && wordEl.hasAttribute("end")) {
|
||||||
const word: LyricWord = {
|
const word: LyricWord = {
|
||||||
|
@ -23,5 +23,4 @@ export interface LyricLine {
|
|||||||
isDuet: boolean;
|
isDuet: boolean;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
singer: string;
|
|
||||||
}
|
}
|
||||||
|
10
packages/core/lyrics/type.d.ts
vendored
10
packages/core/lyrics/type.d.ts
vendored
@ -5,11 +5,11 @@ export interface ScriptItem extends ParserScriptItem {
|
|||||||
chorus?: string;
|
chorus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LyricWord {
|
export interface ScriptWordsItem {
|
||||||
startTime: number;
|
start: number;
|
||||||
endTime: number;
|
end: number;
|
||||||
word: string;
|
beginIndex: number;
|
||||||
emptyBeat?: number;
|
endIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LrcMetaData {
|
export interface LrcMetaData {
|
||||||
|
@ -12,4 +12,8 @@
|
|||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user