lrc-audio-player 0.1.1 → 0.1.3

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