lrc-audio-player 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-O4Y7SZ2V.js +274 -0
- package/dist/index.cjs +5 -13
- package/dist/index.d.cts +3 -142
- package/dist/index.d.ts +3 -142
- package/dist/index.js +5 -276
- package/dist/player-C3ZCZJVs.d.cts +151 -0
- package/dist/player-C3ZCZJVs.d.ts +151 -0
- package/dist/react.cjs +335 -0
- package/dist/react.d.cts +51 -0
- package/dist/react.d.ts +51 -0
- package/dist/react.js +43 -0
- package/package.json +21 -6
package/dist/index.js
CHANGED
|
@@ -1,279 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
ti: "title",
|
|
7
|
-
ar: "artist",
|
|
8
|
-
al: "album",
|
|
9
|
-
au: "author",
|
|
10
|
-
by: "author",
|
|
11
|
-
offset: "offset"
|
|
12
|
-
};
|
|
13
|
-
function timeToSeconds(min, sec, frac) {
|
|
14
|
-
const fraction = frac ? Number(`0.${frac}`) : 0;
|
|
15
|
-
return Number(min) * 60 + Number(sec) + fraction;
|
|
16
|
-
}
|
|
17
|
-
function parseTokens(raw, lineTime) {
|
|
18
|
-
if (!WORD_TAG.test(raw)) {
|
|
19
|
-
return { text: raw.trim() };
|
|
20
|
-
}
|
|
21
|
-
WORD_TAG.lastIndex = 0;
|
|
22
|
-
const tokens = [];
|
|
23
|
-
let lastIndex = 0;
|
|
24
|
-
let lastTime = lineTime;
|
|
25
|
-
let match;
|
|
26
|
-
let plain = "";
|
|
27
|
-
while (match = WORD_TAG.exec(raw)) {
|
|
28
|
-
const chunk = raw.slice(lastIndex, match.index);
|
|
29
|
-
if (chunk.length) {
|
|
30
|
-
tokens.push({ time: lastTime, text: chunk });
|
|
31
|
-
plain += chunk;
|
|
32
|
-
}
|
|
33
|
-
lastTime = timeToSeconds(match[1], match[2], match[3]);
|
|
34
|
-
lastIndex = WORD_TAG.lastIndex;
|
|
35
|
-
}
|
|
36
|
-
const tail = raw.slice(lastIndex);
|
|
37
|
-
if (tail.length) {
|
|
38
|
-
tokens.push({ time: lastTime, text: tail });
|
|
39
|
-
plain += tail;
|
|
40
|
-
}
|
|
41
|
-
return { text: plain.trim(), tokens };
|
|
42
|
-
}
|
|
43
|
-
function parseLRC(lrc) {
|
|
44
|
-
const metadata = {};
|
|
45
|
-
const lines = [];
|
|
46
|
-
const rawLines = lrc.split(/\r?\n/);
|
|
47
|
-
for (const rawLine of rawLines) {
|
|
48
|
-
const trimmed = rawLine.trim();
|
|
49
|
-
if (!trimmed) continue;
|
|
50
|
-
const metaMatch = trimmed.match(META_TAG);
|
|
51
|
-
if (metaMatch && !TIME_TAG.test(trimmed)) {
|
|
52
|
-
const key = metaMatch[1].toLowerCase();
|
|
53
|
-
const value = metaMatch[2].trim();
|
|
54
|
-
const knownKey = KNOWN_META_KEYS[key];
|
|
55
|
-
if (knownKey === "offset") {
|
|
56
|
-
metadata.offset = Number(value);
|
|
57
|
-
} else if (knownKey) {
|
|
58
|
-
metadata[knownKey] = value;
|
|
59
|
-
} else {
|
|
60
|
-
metadata[key] = value;
|
|
61
|
-
}
|
|
62
|
-
TIME_TAG.lastIndex = 0;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
TIME_TAG.lastIndex = 0;
|
|
66
|
-
const times = [];
|
|
67
|
-
let rest = trimmed;
|
|
68
|
-
let leadingMatch;
|
|
69
|
-
while (leadingMatch = rest.match(/^\[(\d{1,2}):(\d{2})(?:[.:](\d{1,3}))?\]/)) {
|
|
70
|
-
times.push(timeToSeconds(leadingMatch[1], leadingMatch[2], leadingMatch[3]));
|
|
71
|
-
rest = rest.slice(leadingMatch[0].length);
|
|
72
|
-
}
|
|
73
|
-
if (times.length === 0) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
for (const time of times) {
|
|
77
|
-
const { text, tokens } = parseTokens(rest, time);
|
|
78
|
-
lines.push({ time, text, tokens });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const offsetSeconds = (metadata.offset ?? 0) / 1e3;
|
|
82
|
-
if (offsetSeconds) {
|
|
83
|
-
for (const line of lines) {
|
|
84
|
-
line.time -= offsetSeconds;
|
|
85
|
-
if (line.tokens) {
|
|
86
|
-
for (const token of line.tokens) {
|
|
87
|
-
token.time -= offsetSeconds;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
lines.sort((a, b) => a.time - b.time);
|
|
93
|
-
return { metadata, lines };
|
|
94
|
-
}
|
|
95
|
-
function parseJSONLyrics(json) {
|
|
96
|
-
const lines = typeof json === "string" ? JSON.parse(json) : json;
|
|
97
|
-
const sorted = [...lines].sort((a, b) => a.time - b.time);
|
|
98
|
-
return { metadata: {}, lines: sorted };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/player.ts
|
|
102
|
-
var LyricPlayer = class {
|
|
103
|
-
constructor(options) {
|
|
104
|
-
this.currentIndex = -1;
|
|
105
|
-
this.listeners = {};
|
|
106
|
-
this.handleTimeUpdate = () => {
|
|
107
|
-
const time = this.audio.currentTime;
|
|
108
|
-
const newIndex = this.findLineIndexAtTime(time);
|
|
109
|
-
if (newIndex !== this.currentIndex) {
|
|
110
|
-
this.currentIndex = newIndex;
|
|
111
|
-
this.emit("linechange", this.getCurrentLine(), newIndex);
|
|
112
|
-
}
|
|
113
|
-
this.emit("timeupdate", time);
|
|
114
|
-
};
|
|
115
|
-
this.audio = typeof options.audio === "string" ? new Audio(options.audio) : options.audio;
|
|
116
|
-
const parsed = this.resolveLyrics(options.lyrics);
|
|
117
|
-
this.metadata = parsed.metadata;
|
|
118
|
-
this.lines = parsed.lines;
|
|
119
|
-
this.offsetSeconds = (options.offsetMs ?? 0) / 1e3;
|
|
120
|
-
this.audio.addEventListener("timeupdate", this.handleTimeUpdate);
|
|
121
|
-
this.audio.addEventListener("play", () => this.emit("play"));
|
|
122
|
-
this.audio.addEventListener("pause", () => this.emit("pause"));
|
|
123
|
-
this.audio.addEventListener("ended", () => this.emit("ended"));
|
|
124
|
-
this.audio.addEventListener("error", (e) => this.emit("error", e));
|
|
125
|
-
}
|
|
126
|
-
// ---------------------------------------------------------------------
|
|
127
|
-
// Lyric source resolution
|
|
128
|
-
// ---------------------------------------------------------------------
|
|
129
|
-
resolveLyrics(lyrics) {
|
|
130
|
-
if (!lyrics) return { metadata: {}, lines: [] };
|
|
131
|
-
if (typeof lyrics === "string") {
|
|
132
|
-
return parseLRC(lyrics);
|
|
133
|
-
}
|
|
134
|
-
if (Array.isArray(lyrics)) {
|
|
135
|
-
return parseJSONLyrics(lyrics);
|
|
136
|
-
}
|
|
137
|
-
if ("type" in lyrics) {
|
|
138
|
-
switch (lyrics.type) {
|
|
139
|
-
case "lrc":
|
|
140
|
-
return parseLRC(lyrics.data);
|
|
141
|
-
case "json":
|
|
142
|
-
return parseJSONLyrics(lyrics.data);
|
|
143
|
-
case "parsed":
|
|
144
|
-
return lyrics.data;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return lyrics;
|
|
148
|
-
}
|
|
149
|
-
/** Replace the loaded lyrics at any time (e.g. after fetching a file). */
|
|
150
|
-
setLyrics(lyrics) {
|
|
151
|
-
const parsed = this.resolveLyrics(lyrics);
|
|
152
|
-
this.metadata = parsed.metadata;
|
|
153
|
-
this.lines.length = 0;
|
|
154
|
-
this.lines.push(...parsed.lines);
|
|
155
|
-
this.currentIndex = -1;
|
|
156
|
-
this.handleTimeUpdate();
|
|
157
|
-
}
|
|
158
|
-
/** Adjust the global lyric offset (in milliseconds) at runtime. */
|
|
159
|
-
setOffset(offsetMs) {
|
|
160
|
-
this.offsetSeconds = offsetMs / 1e3;
|
|
161
|
-
this.currentIndex = -1;
|
|
162
|
-
this.handleTimeUpdate();
|
|
163
|
-
}
|
|
164
|
-
// ---------------------------------------------------------------------
|
|
165
|
-
// Playback controls (thin wrappers over the audio element)
|
|
166
|
-
// ---------------------------------------------------------------------
|
|
167
|
-
play() {
|
|
168
|
-
return this.audio.play();
|
|
169
|
-
}
|
|
170
|
-
pause() {
|
|
171
|
-
this.audio.pause();
|
|
172
|
-
}
|
|
173
|
-
toggle() {
|
|
174
|
-
return this.audio.paused ? this.audio.play() : this.audio.pause();
|
|
175
|
-
}
|
|
176
|
-
seek(timeSeconds) {
|
|
177
|
-
this.audio.currentTime = Math.max(0, timeSeconds);
|
|
178
|
-
this.handleTimeUpdate();
|
|
179
|
-
}
|
|
180
|
-
/** Seek directly to the start of a given lyric line. */
|
|
181
|
-
seekToLine(index) {
|
|
182
|
-
const line = this.lines[index];
|
|
183
|
-
if (line) this.seek(line.time + this.offsetSeconds);
|
|
184
|
-
}
|
|
185
|
-
get currentTime() {
|
|
186
|
-
return this.audio.currentTime;
|
|
187
|
-
}
|
|
188
|
-
get duration() {
|
|
189
|
-
return this.audio.duration;
|
|
190
|
-
}
|
|
191
|
-
get paused() {
|
|
192
|
-
return this.audio.paused;
|
|
193
|
-
}
|
|
194
|
-
set volume(value) {
|
|
195
|
-
this.audio.volume = value;
|
|
196
|
-
}
|
|
197
|
-
get volume() {
|
|
198
|
-
return this.audio.volume;
|
|
199
|
-
}
|
|
200
|
-
// ---------------------------------------------------------------------
|
|
201
|
-
// Lyric lookup
|
|
202
|
-
// ---------------------------------------------------------------------
|
|
203
|
-
/** The currently active lyric line, or null if before the first line. */
|
|
204
|
-
getCurrentLine() {
|
|
205
|
-
return this.currentIndex >= 0 ? this.lines[this.currentIndex] : null;
|
|
206
|
-
}
|
|
207
|
-
/** The index of the currently active lyric line (-1 if none yet). */
|
|
208
|
-
getCurrentIndex() {
|
|
209
|
-
return this.currentIndex;
|
|
210
|
-
}
|
|
211
|
-
/** The next lyric line after the current one, if any. */
|
|
212
|
-
getNextLine() {
|
|
213
|
-
const next = this.lines[this.currentIndex + 1];
|
|
214
|
-
return next ?? null;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* For lines with word-level timing, returns the index of the active
|
|
218
|
-
* token within the current line (-1 if no tokens or none active yet).
|
|
219
|
-
*/
|
|
220
|
-
getCurrentTokenIndex() {
|
|
221
|
-
const line = this.getCurrentLine();
|
|
222
|
-
if (!line?.tokens?.length) return -1;
|
|
223
|
-
const t = this.audio.currentTime - this.offsetSeconds;
|
|
224
|
-
let idx = -1;
|
|
225
|
-
for (let i = 0; i < line.tokens.length; i++) {
|
|
226
|
-
if (line.tokens[i].time <= t) idx = i;
|
|
227
|
-
else break;
|
|
228
|
-
}
|
|
229
|
-
return idx;
|
|
230
|
-
}
|
|
231
|
-
getCurrentToken() {
|
|
232
|
-
const line = this.getCurrentLine();
|
|
233
|
-
const idx = this.getCurrentTokenIndex();
|
|
234
|
-
return line?.tokens && idx >= 0 ? line.tokens[idx] : null;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Find the line index active at an arbitrary time, without affecting
|
|
238
|
-
* playback or the cached current index. Uses binary search.
|
|
239
|
-
*/
|
|
240
|
-
findLineIndexAtTime(timeSeconds) {
|
|
241
|
-
const t = timeSeconds - this.offsetSeconds;
|
|
242
|
-
const lines = this.lines;
|
|
243
|
-
if (lines.length === 0 || t < lines[0].time) return -1;
|
|
244
|
-
let lo = 0;
|
|
245
|
-
let hi = lines.length - 1;
|
|
246
|
-
while (lo < hi) {
|
|
247
|
-
const mid = lo + hi + 1 >> 1;
|
|
248
|
-
if (lines[mid].time <= t) lo = mid;
|
|
249
|
-
else hi = mid - 1;
|
|
250
|
-
}
|
|
251
|
-
return lo;
|
|
252
|
-
}
|
|
253
|
-
// ---------------------------------------------------------------------
|
|
254
|
-
// Event handling
|
|
255
|
-
// ---------------------------------------------------------------------
|
|
256
|
-
on(event, listener) {
|
|
257
|
-
let set = this.listeners[event];
|
|
258
|
-
if (!set) {
|
|
259
|
-
set = /* @__PURE__ */ new Set();
|
|
260
|
-
this.listeners[event] = set;
|
|
261
|
-
}
|
|
262
|
-
set.add(listener);
|
|
263
|
-
}
|
|
264
|
-
off(event, listener) {
|
|
265
|
-
this.listeners[event]?.delete(listener);
|
|
266
|
-
}
|
|
267
|
-
emit(event, ...args) {
|
|
268
|
-
this.listeners[event]?.forEach((listener) => listener(...args));
|
|
269
|
-
}
|
|
270
|
-
/** Remove all listeners and detach from the underlying audio element. */
|
|
271
|
-
destroy() {
|
|
272
|
-
this.audio.removeEventListener("timeupdate", this.handleTimeUpdate);
|
|
273
|
-
this.audio.pause();
|
|
274
|
-
this.listeners = {};
|
|
275
|
-
}
|
|
276
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
LyricPlayer,
|
|
3
|
+
parseJSONLyrics,
|
|
4
|
+
parseLRC
|
|
5
|
+
} from "./chunk-O4Y7SZ2V.js";
|
|
277
6
|
export {
|
|
278
7
|
LyricPlayer,
|
|
279
8
|
parseJSONLyrics,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single token within a lyric line (for word/syllable-level timing,
|
|
3
|
+
* "enhanced LRC" style: <mm:ss.xx>word).
|
|
4
|
+
*/
|
|
5
|
+
interface LyricToken {
|
|
6
|
+
/** Time in seconds at which this token starts. */
|
|
7
|
+
time: number;
|
|
8
|
+
/** The text of the token (word, syllable, or chunk). */
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A single line of lyrics with its start time and optional
|
|
13
|
+
* word-level timing tokens.
|
|
14
|
+
*/
|
|
15
|
+
interface LyricLine {
|
|
16
|
+
/** Time in seconds at which this line starts. */
|
|
17
|
+
time: number;
|
|
18
|
+
/** Full text of the line (tokens stripped of timestamps, joined). */
|
|
19
|
+
text: string;
|
|
20
|
+
/** Optional word/syllable-level tokens, if the source had enhanced timing. */
|
|
21
|
+
tokens?: LyricToken[];
|
|
22
|
+
/** Optional metadata tag, e.g. for translation/extension lines (`[tr]` etc). */
|
|
23
|
+
tag?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Options for constructing a LyricPlayer instance.
|
|
27
|
+
*/
|
|
28
|
+
type LyricPlayerOptions$1 = {
|
|
29
|
+
audio: string | HTMLAudioElement;
|
|
30
|
+
lyrics?: string | LyricLine[] | ParsedLyrics | LyricSource;
|
|
31
|
+
offsetMs?: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Metadata commonly found in LRC files ([ti], [ar], [al], [by], [offset], etc).
|
|
35
|
+
*/
|
|
36
|
+
interface LyricMetadata {
|
|
37
|
+
title?: string;
|
|
38
|
+
artist?: string;
|
|
39
|
+
album?: string;
|
|
40
|
+
author?: string;
|
|
41
|
+
/** Offset in milliseconds. Positive values make lyrics appear later. */
|
|
42
|
+
offset?: number;
|
|
43
|
+
[key: string]: string | number | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parsed lyric file: metadata plus a time-sorted array of lines.
|
|
47
|
+
*/
|
|
48
|
+
interface ParsedLyrics {
|
|
49
|
+
metadata: LyricMetadata;
|
|
50
|
+
lines: LyricLine[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Events emitted by LyricPlayer.
|
|
54
|
+
*/
|
|
55
|
+
interface LyricPlayerEvents {
|
|
56
|
+
/** Fired whenever the active lyric line index changes (including to -1). */
|
|
57
|
+
linechange: (line: LyricLine | null, index: number) => void;
|
|
58
|
+
/** Fired on every underlying `timeupdate` from the audio element. */
|
|
59
|
+
timeupdate: (currentTime: number) => void;
|
|
60
|
+
/** Fired when playback starts. */
|
|
61
|
+
play: () => void;
|
|
62
|
+
/** Fired when playback pauses. */
|
|
63
|
+
pause: () => void;
|
|
64
|
+
/** Fired when the track finishes playing. */
|
|
65
|
+
ended: () => void;
|
|
66
|
+
/** Fired if the audio element raises an error. */
|
|
67
|
+
error: (error: Event) => void;
|
|
68
|
+
}
|
|
69
|
+
type LyricPlayerEventName = keyof LyricPlayerEvents;
|
|
70
|
+
|
|
71
|
+
type LyricSource = {
|
|
72
|
+
type: "lrc";
|
|
73
|
+
data: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: "json";
|
|
76
|
+
data: string | LyricLine[];
|
|
77
|
+
} | {
|
|
78
|
+
type: "parsed";
|
|
79
|
+
data: ParsedLyrics;
|
|
80
|
+
};
|
|
81
|
+
interface LyricPlayerOptions {
|
|
82
|
+
/** Audio source: URL/path, or an existing HTMLAudioElement to take over. */
|
|
83
|
+
audio: string | HTMLAudioElement;
|
|
84
|
+
/** Lyrics source. Defaults to LRC text if a plain string is passed. */
|
|
85
|
+
lyrics?: string | LyricLine[] | ParsedLyrics | LyricSource;
|
|
86
|
+
/**
|
|
87
|
+
* Additional offset in milliseconds applied on top of any [offset:] tag
|
|
88
|
+
* in the LRC file. Positive values shift lyrics later.
|
|
89
|
+
*/
|
|
90
|
+
offsetMs?: number;
|
|
91
|
+
}
|
|
92
|
+
type Listener<E extends LyricPlayerEventName> = LyricPlayerEvents[E];
|
|
93
|
+
/**
|
|
94
|
+
* Wraps an HTMLAudioElement together with parsed, time-synced lyrics.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* const player = new LyricPlayer({ audio: 'song.mp3', lyrics: lrcText });
|
|
98
|
+
* player.on('linechange', (line) => console.log(line?.text));
|
|
99
|
+
* player.play();
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare class LyricPlayer {
|
|
103
|
+
readonly audio: HTMLAudioElement;
|
|
104
|
+
readonly metadata: LyricMetadata;
|
|
105
|
+
readonly lines: LyricLine[];
|
|
106
|
+
private offsetSeconds;
|
|
107
|
+
private currentIndex;
|
|
108
|
+
private listeners;
|
|
109
|
+
constructor(options: LyricPlayerOptions);
|
|
110
|
+
private resolveLyrics;
|
|
111
|
+
/** Replace the loaded lyrics at any time (e.g. after fetching a file). */
|
|
112
|
+
setLyrics(lyrics: LyricPlayerOptions["lyrics"]): void;
|
|
113
|
+
/** Adjust the global lyric offset (in milliseconds) at runtime. */
|
|
114
|
+
setOffset(offsetMs: number): void;
|
|
115
|
+
play(): Promise<void>;
|
|
116
|
+
pause(): void;
|
|
117
|
+
toggle(): Promise<void> | void;
|
|
118
|
+
seek(timeSeconds: number): void;
|
|
119
|
+
/** Seek directly to the start of a given lyric line. */
|
|
120
|
+
seekToLine(index: number): void;
|
|
121
|
+
get currentTime(): number;
|
|
122
|
+
get duration(): number;
|
|
123
|
+
get paused(): boolean;
|
|
124
|
+
set volume(value: number);
|
|
125
|
+
get volume(): number;
|
|
126
|
+
/** The currently active lyric line, or null if before the first line. */
|
|
127
|
+
getCurrentLine(): LyricLine | null;
|
|
128
|
+
/** The index of the currently active lyric line (-1 if none yet). */
|
|
129
|
+
getCurrentIndex(): number;
|
|
130
|
+
/** The next lyric line after the current one, if any. */
|
|
131
|
+
getNextLine(): LyricLine | null;
|
|
132
|
+
/**
|
|
133
|
+
* For lines with word-level timing, returns the index of the active
|
|
134
|
+
* token within the current line (-1 if no tokens or none active yet).
|
|
135
|
+
*/
|
|
136
|
+
getCurrentTokenIndex(): number;
|
|
137
|
+
getCurrentToken(): LyricToken | null;
|
|
138
|
+
/**
|
|
139
|
+
* Find the line index active at an arbitrary time, without affecting
|
|
140
|
+
* playback or the cached current index. Uses binary search.
|
|
141
|
+
*/
|
|
142
|
+
findLineIndexAtTime(timeSeconds: number): number;
|
|
143
|
+
on<E extends LyricPlayerEventName>(event: E, listener: Listener<E>): void;
|
|
144
|
+
off<E extends LyricPlayerEventName>(event: E, listener: Listener<E>): void;
|
|
145
|
+
private emit;
|
|
146
|
+
private handleTimeUpdate;
|
|
147
|
+
/** Remove all listeners and detach from the underlying audio element. */
|
|
148
|
+
destroy(): void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { type LyricLine as L, type ParsedLyrics as P, type LyricMetadata as a, LyricPlayer as b, type LyricPlayerEventName as c, type LyricPlayerEvents as d, type LyricPlayerOptions as e, type LyricSource as f, type LyricToken as g, type LyricPlayerOptions$1 as h };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single token within a lyric line (for word/syllable-level timing,
|
|
3
|
+
* "enhanced LRC" style: <mm:ss.xx>word).
|
|
4
|
+
*/
|
|
5
|
+
interface LyricToken {
|
|
6
|
+
/** Time in seconds at which this token starts. */
|
|
7
|
+
time: number;
|
|
8
|
+
/** The text of the token (word, syllable, or chunk). */
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A single line of lyrics with its start time and optional
|
|
13
|
+
* word-level timing tokens.
|
|
14
|
+
*/
|
|
15
|
+
interface LyricLine {
|
|
16
|
+
/** Time in seconds at which this line starts. */
|
|
17
|
+
time: number;
|
|
18
|
+
/** Full text of the line (tokens stripped of timestamps, joined). */
|
|
19
|
+
text: string;
|
|
20
|
+
/** Optional word/syllable-level tokens, if the source had enhanced timing. */
|
|
21
|
+
tokens?: LyricToken[];
|
|
22
|
+
/** Optional metadata tag, e.g. for translation/extension lines (`[tr]` etc). */
|
|
23
|
+
tag?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Options for constructing a LyricPlayer instance.
|
|
27
|
+
*/
|
|
28
|
+
type LyricPlayerOptions$1 = {
|
|
29
|
+
audio: string | HTMLAudioElement;
|
|
30
|
+
lyrics?: string | LyricLine[] | ParsedLyrics | LyricSource;
|
|
31
|
+
offsetMs?: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Metadata commonly found in LRC files ([ti], [ar], [al], [by], [offset], etc).
|
|
35
|
+
*/
|
|
36
|
+
interface LyricMetadata {
|
|
37
|
+
title?: string;
|
|
38
|
+
artist?: string;
|
|
39
|
+
album?: string;
|
|
40
|
+
author?: string;
|
|
41
|
+
/** Offset in milliseconds. Positive values make lyrics appear later. */
|
|
42
|
+
offset?: number;
|
|
43
|
+
[key: string]: string | number | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parsed lyric file: metadata plus a time-sorted array of lines.
|
|
47
|
+
*/
|
|
48
|
+
interface ParsedLyrics {
|
|
49
|
+
metadata: LyricMetadata;
|
|
50
|
+
lines: LyricLine[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Events emitted by LyricPlayer.
|
|
54
|
+
*/
|
|
55
|
+
interface LyricPlayerEvents {
|
|
56
|
+
/** Fired whenever the active lyric line index changes (including to -1). */
|
|
57
|
+
linechange: (line: LyricLine | null, index: number) => void;
|
|
58
|
+
/** Fired on every underlying `timeupdate` from the audio element. */
|
|
59
|
+
timeupdate: (currentTime: number) => void;
|
|
60
|
+
/** Fired when playback starts. */
|
|
61
|
+
play: () => void;
|
|
62
|
+
/** Fired when playback pauses. */
|
|
63
|
+
pause: () => void;
|
|
64
|
+
/** Fired when the track finishes playing. */
|
|
65
|
+
ended: () => void;
|
|
66
|
+
/** Fired if the audio element raises an error. */
|
|
67
|
+
error: (error: Event) => void;
|
|
68
|
+
}
|
|
69
|
+
type LyricPlayerEventName = keyof LyricPlayerEvents;
|
|
70
|
+
|
|
71
|
+
type LyricSource = {
|
|
72
|
+
type: "lrc";
|
|
73
|
+
data: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: "json";
|
|
76
|
+
data: string | LyricLine[];
|
|
77
|
+
} | {
|
|
78
|
+
type: "parsed";
|
|
79
|
+
data: ParsedLyrics;
|
|
80
|
+
};
|
|
81
|
+
interface LyricPlayerOptions {
|
|
82
|
+
/** Audio source: URL/path, or an existing HTMLAudioElement to take over. */
|
|
83
|
+
audio: string | HTMLAudioElement;
|
|
84
|
+
/** Lyrics source. Defaults to LRC text if a plain string is passed. */
|
|
85
|
+
lyrics?: string | LyricLine[] | ParsedLyrics | LyricSource;
|
|
86
|
+
/**
|
|
87
|
+
* Additional offset in milliseconds applied on top of any [offset:] tag
|
|
88
|
+
* in the LRC file. Positive values shift lyrics later.
|
|
89
|
+
*/
|
|
90
|
+
offsetMs?: number;
|
|
91
|
+
}
|
|
92
|
+
type Listener<E extends LyricPlayerEventName> = LyricPlayerEvents[E];
|
|
93
|
+
/**
|
|
94
|
+
* Wraps an HTMLAudioElement together with parsed, time-synced lyrics.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* const player = new LyricPlayer({ audio: 'song.mp3', lyrics: lrcText });
|
|
98
|
+
* player.on('linechange', (line) => console.log(line?.text));
|
|
99
|
+
* player.play();
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare class LyricPlayer {
|
|
103
|
+
readonly audio: HTMLAudioElement;
|
|
104
|
+
readonly metadata: LyricMetadata;
|
|
105
|
+
readonly lines: LyricLine[];
|
|
106
|
+
private offsetSeconds;
|
|
107
|
+
private currentIndex;
|
|
108
|
+
private listeners;
|
|
109
|
+
constructor(options: LyricPlayerOptions);
|
|
110
|
+
private resolveLyrics;
|
|
111
|
+
/** Replace the loaded lyrics at any time (e.g. after fetching a file). */
|
|
112
|
+
setLyrics(lyrics: LyricPlayerOptions["lyrics"]): void;
|
|
113
|
+
/** Adjust the global lyric offset (in milliseconds) at runtime. */
|
|
114
|
+
setOffset(offsetMs: number): void;
|
|
115
|
+
play(): Promise<void>;
|
|
116
|
+
pause(): void;
|
|
117
|
+
toggle(): Promise<void> | void;
|
|
118
|
+
seek(timeSeconds: number): void;
|
|
119
|
+
/** Seek directly to the start of a given lyric line. */
|
|
120
|
+
seekToLine(index: number): void;
|
|
121
|
+
get currentTime(): number;
|
|
122
|
+
get duration(): number;
|
|
123
|
+
get paused(): boolean;
|
|
124
|
+
set volume(value: number);
|
|
125
|
+
get volume(): number;
|
|
126
|
+
/** The currently active lyric line, or null if before the first line. */
|
|
127
|
+
getCurrentLine(): LyricLine | null;
|
|
128
|
+
/** The index of the currently active lyric line (-1 if none yet). */
|
|
129
|
+
getCurrentIndex(): number;
|
|
130
|
+
/** The next lyric line after the current one, if any. */
|
|
131
|
+
getNextLine(): LyricLine | null;
|
|
132
|
+
/**
|
|
133
|
+
* For lines with word-level timing, returns the index of the active
|
|
134
|
+
* token within the current line (-1 if no tokens or none active yet).
|
|
135
|
+
*/
|
|
136
|
+
getCurrentTokenIndex(): number;
|
|
137
|
+
getCurrentToken(): LyricToken | null;
|
|
138
|
+
/**
|
|
139
|
+
* Find the line index active at an arbitrary time, without affecting
|
|
140
|
+
* playback or the cached current index. Uses binary search.
|
|
141
|
+
*/
|
|
142
|
+
findLineIndexAtTime(timeSeconds: number): number;
|
|
143
|
+
on<E extends LyricPlayerEventName>(event: E, listener: Listener<E>): void;
|
|
144
|
+
off<E extends LyricPlayerEventName>(event: E, listener: Listener<E>): void;
|
|
145
|
+
private emit;
|
|
146
|
+
private handleTimeUpdate;
|
|
147
|
+
/** Remove all listeners and detach from the underlying audio element. */
|
|
148
|
+
destroy(): void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { type LyricLine as L, type ParsedLyrics as P, type LyricMetadata as a, LyricPlayer as b, type LyricPlayerEventName as c, type LyricPlayerEvents as d, type LyricPlayerOptions as e, type LyricSource as f, type LyricToken as g, type LyricPlayerOptions$1 as h };
|