@applemusic-like-lyrics/lyric 0.4.1 → 1.0.0
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/README.md +17 -9
- package/dist/amll-lyric.cjs +1984 -0
- package/dist/amll-lyric.cjs.map +1 -0
- package/dist/amll-lyric.d.cts +219 -0
- package/dist/amll-lyric.d.mts +219 -0
- package/dist/amll-lyric.mjs +1963 -0
- package/dist/amll-lyric.mjs.map +1 -0
- package/package.json +49 -40
- package/LICENSE +0 -661
- package/pkg/amll_lyric.d.ts +0 -187
- package/pkg/amll_lyric.js +0 -9
- package/pkg/amll_lyric_bg.js +0 -585
- package/pkg/amll_lyric_bg.wasm +0 -0
- package/pkg/amll_lyric_bg.wasm.d.ts +0 -25
|
@@ -0,0 +1,1984 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let pako = require("pako");
|
|
3
|
+
let _applemusic_like_lyrics_ttml = require("@applemusic-like-lyrics/ttml");
|
|
4
|
+
//#region src/utils.ts
|
|
5
|
+
const createLine = (line) => ({
|
|
6
|
+
words: [],
|
|
7
|
+
translatedLyric: "",
|
|
8
|
+
romanLyric: "",
|
|
9
|
+
isBG: false,
|
|
10
|
+
isDuet: false,
|
|
11
|
+
startTime: 0,
|
|
12
|
+
endTime: 0,
|
|
13
|
+
...line
|
|
14
|
+
});
|
|
15
|
+
const createWord = (word) => ({
|
|
16
|
+
startTime: 0,
|
|
17
|
+
endTime: 0,
|
|
18
|
+
word: "",
|
|
19
|
+
...word
|
|
20
|
+
});
|
|
21
|
+
const parseTime = (time) => Math.round(time.split(":").map(Number).reverse().reduce((acc, cur, idx) => acc + cur * 60 ** idx, 0) * 1e3);
|
|
22
|
+
const formatTime = (ms) => {
|
|
23
|
+
return `${Math.floor(ms / 6e4).toString().padStart(2, "0")}:${Math.floor(ms % 6e4 / 1e3).toString().padStart(2, "0")}.${Math.round(ms % 1e3).toString().padStart(3, "0")}`;
|
|
24
|
+
};
|
|
25
|
+
const normalizeTimestamp = (ms) => {
|
|
26
|
+
if (!Number.isFinite(ms) || ms < 0) return 0;
|
|
27
|
+
return ms;
|
|
28
|
+
};
|
|
29
|
+
const normalizeDuration = (duration) => {
|
|
30
|
+
if (!Number.isFinite(duration) || duration < 0) return 0;
|
|
31
|
+
return duration;
|
|
32
|
+
};
|
|
33
|
+
const MAX_LRC_TIMESTAMP = 60039999;
|
|
34
|
+
const clampTimestamp = (ms, max = MAX_LRC_TIMESTAMP) => Math.min(max, normalizeTimestamp(ms));
|
|
35
|
+
/**
|
|
36
|
+
* Returns consecutive pairs from the given iterable.
|
|
37
|
+
*
|
|
38
|
+
* Example: `0, 1, 2, 3` -> `[0, 1], [1, 2], [2, 3]`
|
|
39
|
+
*/
|
|
40
|
+
function* pairwise(iterable) {
|
|
41
|
+
let prev;
|
|
42
|
+
let hasPrev = false;
|
|
43
|
+
for (const curr of iterable) {
|
|
44
|
+
if (hasPrev) yield [prev, curr];
|
|
45
|
+
prev = curr;
|
|
46
|
+
hasPrev = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/formats/ass.ts
|
|
51
|
+
function writeASSTimestamp(ms) {
|
|
52
|
+
const normalized = normalizeTimestamp(ms);
|
|
53
|
+
const milli = Math.round(normalized) % 1e3;
|
|
54
|
+
const secTotal = Math.floor(Math.round(normalized) / 1e3);
|
|
55
|
+
const sec = secTotal % 60;
|
|
56
|
+
const minTotal = Math.floor(secTotal / 60);
|
|
57
|
+
return `${Math.floor(minTotal / 60)}:${String(minTotal % 60).padStart(2, "0")}:${String(sec).padStart(2, "0")}.${String(Math.floor(milli / 10)).padStart(2, "0")}`;
|
|
58
|
+
}
|
|
59
|
+
function getSpeakerName(line) {
|
|
60
|
+
let name = line.isDuet ? "v2" : "v1";
|
|
61
|
+
if (line.isBG) name += "-bg";
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
function writeLyricDialogue(result, startTime, endTime, name, text) {
|
|
65
|
+
result.push(`Dialogue: 0,${writeASSTimestamp(startTime)}, ${writeASSTimestamp(endTime)}, Default, ${name},0,0,0,,${text}`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 将歌词数组转换为 ASS 字幕格式字符串
|
|
69
|
+
* @param lines 歌词数组
|
|
70
|
+
* @returns ASS 字幕格式字符串
|
|
71
|
+
*/
|
|
72
|
+
function stringifyAss(lines) {
|
|
73
|
+
const result = [
|
|
74
|
+
"[Script Info]",
|
|
75
|
+
"[Events]",
|
|
76
|
+
"Formats: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
|
|
77
|
+
];
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const timedWords = line.words.map((w) => ({
|
|
80
|
+
...w,
|
|
81
|
+
startTime: normalizeTimestamp(w.startTime),
|
|
82
|
+
endTime: normalizeTimestamp(w.endTime)
|
|
83
|
+
})).filter((w) => w.endTime > w.startTime);
|
|
84
|
+
const startTime = Math.min(...timedWords.map((w) => w.startTime));
|
|
85
|
+
const endTime = Math.max(...timedWords.map((w) => w.endTime));
|
|
86
|
+
if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;
|
|
87
|
+
let lyricText = "";
|
|
88
|
+
let previousWordEndTime = startTime;
|
|
89
|
+
for (const word of line.words) {
|
|
90
|
+
const wordStart = normalizeTimestamp(word.startTime);
|
|
91
|
+
const wordEnd = normalizeTimestamp(word.endTime);
|
|
92
|
+
if (wordStart >= wordEnd) {
|
|
93
|
+
lyricText += word.word;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (wordStart > previousWordEndTime) {
|
|
97
|
+
const gapDurationCS = Math.floor((wordStart - previousWordEndTime + 5) / 10);
|
|
98
|
+
if (gapDurationCS > 0) lyricText += `{\\k${gapDurationCS}}`;
|
|
99
|
+
}
|
|
100
|
+
const wordDurationCS = Math.floor((wordEnd - wordStart + 5) / 10);
|
|
101
|
+
if (wordDurationCS > 0) lyricText += `{\\k${wordDurationCS}}`;
|
|
102
|
+
lyricText += word.word;
|
|
103
|
+
previousWordEndTime = wordEnd;
|
|
104
|
+
}
|
|
105
|
+
const speaker = getSpeakerName(line);
|
|
106
|
+
writeLyricDialogue(result, startTime, endTime, speaker, lyricText);
|
|
107
|
+
if (line.translatedLyric) writeLyricDialogue(result, startTime, endTime, `${speaker}-trans`, line.translatedLyric);
|
|
108
|
+
if (line.romanLyric) writeLyricDialogue(result, startTime, endTime, `${speaker}-roman`, line.romanLyric);
|
|
109
|
+
}
|
|
110
|
+
return `${result.join("\n")}\n`;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/formats/eqrc/constants.ts
|
|
114
|
+
/**
|
|
115
|
+
* @internal
|
|
116
|
+
* @module constants
|
|
117
|
+
* @description
|
|
118
|
+
* 包含了所有 `custom_des` 模块所需的常量数据。
|
|
119
|
+
*/
|
|
120
|
+
const KEY_1 = new TextEncoder().encode("!@#)(*$%");
|
|
121
|
+
const KEY_2 = new TextEncoder().encode("123ZXC!@");
|
|
122
|
+
const KEY_3 = new TextEncoder().encode("!@#)(NHL");
|
|
123
|
+
const S_BOXES = [
|
|
124
|
+
[
|
|
125
|
+
14,
|
|
126
|
+
4,
|
|
127
|
+
13,
|
|
128
|
+
1,
|
|
129
|
+
2,
|
|
130
|
+
15,
|
|
131
|
+
11,
|
|
132
|
+
8,
|
|
133
|
+
3,
|
|
134
|
+
10,
|
|
135
|
+
6,
|
|
136
|
+
12,
|
|
137
|
+
5,
|
|
138
|
+
9,
|
|
139
|
+
0,
|
|
140
|
+
7,
|
|
141
|
+
0,
|
|
142
|
+
15,
|
|
143
|
+
7,
|
|
144
|
+
4,
|
|
145
|
+
14,
|
|
146
|
+
2,
|
|
147
|
+
13,
|
|
148
|
+
1,
|
|
149
|
+
10,
|
|
150
|
+
6,
|
|
151
|
+
12,
|
|
152
|
+
11,
|
|
153
|
+
9,
|
|
154
|
+
5,
|
|
155
|
+
3,
|
|
156
|
+
8,
|
|
157
|
+
4,
|
|
158
|
+
1,
|
|
159
|
+
14,
|
|
160
|
+
8,
|
|
161
|
+
13,
|
|
162
|
+
6,
|
|
163
|
+
2,
|
|
164
|
+
11,
|
|
165
|
+
15,
|
|
166
|
+
12,
|
|
167
|
+
9,
|
|
168
|
+
7,
|
|
169
|
+
3,
|
|
170
|
+
10,
|
|
171
|
+
5,
|
|
172
|
+
0,
|
|
173
|
+
15,
|
|
174
|
+
12,
|
|
175
|
+
8,
|
|
176
|
+
2,
|
|
177
|
+
4,
|
|
178
|
+
9,
|
|
179
|
+
1,
|
|
180
|
+
7,
|
|
181
|
+
5,
|
|
182
|
+
11,
|
|
183
|
+
3,
|
|
184
|
+
14,
|
|
185
|
+
10,
|
|
186
|
+
0,
|
|
187
|
+
6,
|
|
188
|
+
13
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
15,
|
|
192
|
+
1,
|
|
193
|
+
8,
|
|
194
|
+
14,
|
|
195
|
+
6,
|
|
196
|
+
11,
|
|
197
|
+
3,
|
|
198
|
+
4,
|
|
199
|
+
9,
|
|
200
|
+
7,
|
|
201
|
+
2,
|
|
202
|
+
13,
|
|
203
|
+
12,
|
|
204
|
+
0,
|
|
205
|
+
5,
|
|
206
|
+
10,
|
|
207
|
+
3,
|
|
208
|
+
13,
|
|
209
|
+
4,
|
|
210
|
+
7,
|
|
211
|
+
15,
|
|
212
|
+
2,
|
|
213
|
+
8,
|
|
214
|
+
15,
|
|
215
|
+
12,
|
|
216
|
+
0,
|
|
217
|
+
1,
|
|
218
|
+
10,
|
|
219
|
+
6,
|
|
220
|
+
9,
|
|
221
|
+
11,
|
|
222
|
+
5,
|
|
223
|
+
0,
|
|
224
|
+
14,
|
|
225
|
+
7,
|
|
226
|
+
11,
|
|
227
|
+
10,
|
|
228
|
+
4,
|
|
229
|
+
13,
|
|
230
|
+
1,
|
|
231
|
+
5,
|
|
232
|
+
8,
|
|
233
|
+
12,
|
|
234
|
+
6,
|
|
235
|
+
9,
|
|
236
|
+
3,
|
|
237
|
+
2,
|
|
238
|
+
15,
|
|
239
|
+
13,
|
|
240
|
+
8,
|
|
241
|
+
10,
|
|
242
|
+
1,
|
|
243
|
+
3,
|
|
244
|
+
15,
|
|
245
|
+
4,
|
|
246
|
+
2,
|
|
247
|
+
11,
|
|
248
|
+
6,
|
|
249
|
+
7,
|
|
250
|
+
12,
|
|
251
|
+
0,
|
|
252
|
+
5,
|
|
253
|
+
14,
|
|
254
|
+
9
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
10,
|
|
258
|
+
0,
|
|
259
|
+
9,
|
|
260
|
+
14,
|
|
261
|
+
6,
|
|
262
|
+
3,
|
|
263
|
+
15,
|
|
264
|
+
5,
|
|
265
|
+
1,
|
|
266
|
+
13,
|
|
267
|
+
12,
|
|
268
|
+
7,
|
|
269
|
+
11,
|
|
270
|
+
4,
|
|
271
|
+
2,
|
|
272
|
+
8,
|
|
273
|
+
13,
|
|
274
|
+
7,
|
|
275
|
+
0,
|
|
276
|
+
9,
|
|
277
|
+
3,
|
|
278
|
+
4,
|
|
279
|
+
6,
|
|
280
|
+
10,
|
|
281
|
+
2,
|
|
282
|
+
8,
|
|
283
|
+
5,
|
|
284
|
+
14,
|
|
285
|
+
12,
|
|
286
|
+
11,
|
|
287
|
+
15,
|
|
288
|
+
1,
|
|
289
|
+
13,
|
|
290
|
+
6,
|
|
291
|
+
4,
|
|
292
|
+
9,
|
|
293
|
+
8,
|
|
294
|
+
15,
|
|
295
|
+
3,
|
|
296
|
+
0,
|
|
297
|
+
11,
|
|
298
|
+
1,
|
|
299
|
+
2,
|
|
300
|
+
12,
|
|
301
|
+
5,
|
|
302
|
+
10,
|
|
303
|
+
14,
|
|
304
|
+
7,
|
|
305
|
+
1,
|
|
306
|
+
10,
|
|
307
|
+
13,
|
|
308
|
+
0,
|
|
309
|
+
6,
|
|
310
|
+
9,
|
|
311
|
+
8,
|
|
312
|
+
7,
|
|
313
|
+
4,
|
|
314
|
+
15,
|
|
315
|
+
14,
|
|
316
|
+
3,
|
|
317
|
+
11,
|
|
318
|
+
5,
|
|
319
|
+
2,
|
|
320
|
+
12
|
|
321
|
+
],
|
|
322
|
+
[
|
|
323
|
+
7,
|
|
324
|
+
13,
|
|
325
|
+
14,
|
|
326
|
+
3,
|
|
327
|
+
0,
|
|
328
|
+
6,
|
|
329
|
+
9,
|
|
330
|
+
10,
|
|
331
|
+
1,
|
|
332
|
+
2,
|
|
333
|
+
8,
|
|
334
|
+
5,
|
|
335
|
+
11,
|
|
336
|
+
12,
|
|
337
|
+
4,
|
|
338
|
+
15,
|
|
339
|
+
13,
|
|
340
|
+
8,
|
|
341
|
+
11,
|
|
342
|
+
5,
|
|
343
|
+
6,
|
|
344
|
+
15,
|
|
345
|
+
0,
|
|
346
|
+
3,
|
|
347
|
+
4,
|
|
348
|
+
7,
|
|
349
|
+
2,
|
|
350
|
+
12,
|
|
351
|
+
1,
|
|
352
|
+
10,
|
|
353
|
+
14,
|
|
354
|
+
9,
|
|
355
|
+
10,
|
|
356
|
+
6,
|
|
357
|
+
9,
|
|
358
|
+
0,
|
|
359
|
+
12,
|
|
360
|
+
11,
|
|
361
|
+
7,
|
|
362
|
+
13,
|
|
363
|
+
15,
|
|
364
|
+
1,
|
|
365
|
+
3,
|
|
366
|
+
14,
|
|
367
|
+
5,
|
|
368
|
+
2,
|
|
369
|
+
8,
|
|
370
|
+
4,
|
|
371
|
+
3,
|
|
372
|
+
15,
|
|
373
|
+
0,
|
|
374
|
+
6,
|
|
375
|
+
10,
|
|
376
|
+
10,
|
|
377
|
+
13,
|
|
378
|
+
8,
|
|
379
|
+
9,
|
|
380
|
+
4,
|
|
381
|
+
5,
|
|
382
|
+
11,
|
|
383
|
+
12,
|
|
384
|
+
7,
|
|
385
|
+
2,
|
|
386
|
+
14
|
|
387
|
+
],
|
|
388
|
+
[
|
|
389
|
+
2,
|
|
390
|
+
12,
|
|
391
|
+
4,
|
|
392
|
+
1,
|
|
393
|
+
7,
|
|
394
|
+
10,
|
|
395
|
+
11,
|
|
396
|
+
6,
|
|
397
|
+
8,
|
|
398
|
+
5,
|
|
399
|
+
3,
|
|
400
|
+
15,
|
|
401
|
+
13,
|
|
402
|
+
0,
|
|
403
|
+
14,
|
|
404
|
+
9,
|
|
405
|
+
14,
|
|
406
|
+
11,
|
|
407
|
+
2,
|
|
408
|
+
12,
|
|
409
|
+
4,
|
|
410
|
+
7,
|
|
411
|
+
13,
|
|
412
|
+
1,
|
|
413
|
+
5,
|
|
414
|
+
0,
|
|
415
|
+
15,
|
|
416
|
+
10,
|
|
417
|
+
3,
|
|
418
|
+
9,
|
|
419
|
+
8,
|
|
420
|
+
6,
|
|
421
|
+
4,
|
|
422
|
+
2,
|
|
423
|
+
1,
|
|
424
|
+
11,
|
|
425
|
+
10,
|
|
426
|
+
13,
|
|
427
|
+
7,
|
|
428
|
+
8,
|
|
429
|
+
15,
|
|
430
|
+
9,
|
|
431
|
+
12,
|
|
432
|
+
5,
|
|
433
|
+
6,
|
|
434
|
+
3,
|
|
435
|
+
0,
|
|
436
|
+
14,
|
|
437
|
+
11,
|
|
438
|
+
8,
|
|
439
|
+
12,
|
|
440
|
+
7,
|
|
441
|
+
1,
|
|
442
|
+
14,
|
|
443
|
+
2,
|
|
444
|
+
13,
|
|
445
|
+
6,
|
|
446
|
+
15,
|
|
447
|
+
0,
|
|
448
|
+
9,
|
|
449
|
+
10,
|
|
450
|
+
4,
|
|
451
|
+
5,
|
|
452
|
+
3
|
|
453
|
+
],
|
|
454
|
+
[
|
|
455
|
+
12,
|
|
456
|
+
1,
|
|
457
|
+
10,
|
|
458
|
+
15,
|
|
459
|
+
9,
|
|
460
|
+
2,
|
|
461
|
+
6,
|
|
462
|
+
8,
|
|
463
|
+
0,
|
|
464
|
+
13,
|
|
465
|
+
3,
|
|
466
|
+
4,
|
|
467
|
+
14,
|
|
468
|
+
7,
|
|
469
|
+
5,
|
|
470
|
+
11,
|
|
471
|
+
10,
|
|
472
|
+
15,
|
|
473
|
+
4,
|
|
474
|
+
2,
|
|
475
|
+
7,
|
|
476
|
+
12,
|
|
477
|
+
9,
|
|
478
|
+
5,
|
|
479
|
+
6,
|
|
480
|
+
1,
|
|
481
|
+
13,
|
|
482
|
+
14,
|
|
483
|
+
0,
|
|
484
|
+
11,
|
|
485
|
+
3,
|
|
486
|
+
8,
|
|
487
|
+
9,
|
|
488
|
+
14,
|
|
489
|
+
15,
|
|
490
|
+
5,
|
|
491
|
+
2,
|
|
492
|
+
8,
|
|
493
|
+
12,
|
|
494
|
+
3,
|
|
495
|
+
7,
|
|
496
|
+
0,
|
|
497
|
+
4,
|
|
498
|
+
10,
|
|
499
|
+
1,
|
|
500
|
+
13,
|
|
501
|
+
11,
|
|
502
|
+
6,
|
|
503
|
+
4,
|
|
504
|
+
3,
|
|
505
|
+
2,
|
|
506
|
+
12,
|
|
507
|
+
9,
|
|
508
|
+
5,
|
|
509
|
+
15,
|
|
510
|
+
10,
|
|
511
|
+
11,
|
|
512
|
+
14,
|
|
513
|
+
1,
|
|
514
|
+
7,
|
|
515
|
+
6,
|
|
516
|
+
0,
|
|
517
|
+
8,
|
|
518
|
+
13
|
|
519
|
+
],
|
|
520
|
+
[
|
|
521
|
+
4,
|
|
522
|
+
11,
|
|
523
|
+
2,
|
|
524
|
+
14,
|
|
525
|
+
15,
|
|
526
|
+
0,
|
|
527
|
+
8,
|
|
528
|
+
13,
|
|
529
|
+
3,
|
|
530
|
+
12,
|
|
531
|
+
9,
|
|
532
|
+
7,
|
|
533
|
+
5,
|
|
534
|
+
10,
|
|
535
|
+
6,
|
|
536
|
+
1,
|
|
537
|
+
13,
|
|
538
|
+
0,
|
|
539
|
+
11,
|
|
540
|
+
7,
|
|
541
|
+
4,
|
|
542
|
+
9,
|
|
543
|
+
1,
|
|
544
|
+
10,
|
|
545
|
+
14,
|
|
546
|
+
3,
|
|
547
|
+
5,
|
|
548
|
+
12,
|
|
549
|
+
2,
|
|
550
|
+
15,
|
|
551
|
+
8,
|
|
552
|
+
6,
|
|
553
|
+
1,
|
|
554
|
+
4,
|
|
555
|
+
11,
|
|
556
|
+
13,
|
|
557
|
+
12,
|
|
558
|
+
3,
|
|
559
|
+
7,
|
|
560
|
+
14,
|
|
561
|
+
10,
|
|
562
|
+
15,
|
|
563
|
+
6,
|
|
564
|
+
8,
|
|
565
|
+
0,
|
|
566
|
+
5,
|
|
567
|
+
9,
|
|
568
|
+
2,
|
|
569
|
+
6,
|
|
570
|
+
11,
|
|
571
|
+
13,
|
|
572
|
+
8,
|
|
573
|
+
1,
|
|
574
|
+
4,
|
|
575
|
+
10,
|
|
576
|
+
7,
|
|
577
|
+
9,
|
|
578
|
+
5,
|
|
579
|
+
0,
|
|
580
|
+
15,
|
|
581
|
+
14,
|
|
582
|
+
2,
|
|
583
|
+
3,
|
|
584
|
+
12
|
|
585
|
+
],
|
|
586
|
+
[
|
|
587
|
+
13,
|
|
588
|
+
2,
|
|
589
|
+
8,
|
|
590
|
+
4,
|
|
591
|
+
6,
|
|
592
|
+
15,
|
|
593
|
+
11,
|
|
594
|
+
1,
|
|
595
|
+
10,
|
|
596
|
+
9,
|
|
597
|
+
3,
|
|
598
|
+
14,
|
|
599
|
+
5,
|
|
600
|
+
0,
|
|
601
|
+
12,
|
|
602
|
+
7,
|
|
603
|
+
1,
|
|
604
|
+
15,
|
|
605
|
+
13,
|
|
606
|
+
8,
|
|
607
|
+
10,
|
|
608
|
+
3,
|
|
609
|
+
7,
|
|
610
|
+
4,
|
|
611
|
+
12,
|
|
612
|
+
5,
|
|
613
|
+
6,
|
|
614
|
+
11,
|
|
615
|
+
0,
|
|
616
|
+
14,
|
|
617
|
+
9,
|
|
618
|
+
2,
|
|
619
|
+
7,
|
|
620
|
+
11,
|
|
621
|
+
4,
|
|
622
|
+
1,
|
|
623
|
+
9,
|
|
624
|
+
12,
|
|
625
|
+
14,
|
|
626
|
+
2,
|
|
627
|
+
0,
|
|
628
|
+
6,
|
|
629
|
+
10,
|
|
630
|
+
13,
|
|
631
|
+
15,
|
|
632
|
+
3,
|
|
633
|
+
5,
|
|
634
|
+
8,
|
|
635
|
+
2,
|
|
636
|
+
1,
|
|
637
|
+
14,
|
|
638
|
+
7,
|
|
639
|
+
4,
|
|
640
|
+
10,
|
|
641
|
+
8,
|
|
642
|
+
13,
|
|
643
|
+
15,
|
|
644
|
+
12,
|
|
645
|
+
9,
|
|
646
|
+
0,
|
|
647
|
+
3,
|
|
648
|
+
5,
|
|
649
|
+
6,
|
|
650
|
+
11
|
|
651
|
+
]
|
|
652
|
+
];
|
|
653
|
+
const P_BOX = [
|
|
654
|
+
16,
|
|
655
|
+
7,
|
|
656
|
+
20,
|
|
657
|
+
21,
|
|
658
|
+
29,
|
|
659
|
+
12,
|
|
660
|
+
28,
|
|
661
|
+
17,
|
|
662
|
+
1,
|
|
663
|
+
15,
|
|
664
|
+
23,
|
|
665
|
+
26,
|
|
666
|
+
5,
|
|
667
|
+
18,
|
|
668
|
+
31,
|
|
669
|
+
10,
|
|
670
|
+
2,
|
|
671
|
+
8,
|
|
672
|
+
24,
|
|
673
|
+
14,
|
|
674
|
+
32,
|
|
675
|
+
27,
|
|
676
|
+
3,
|
|
677
|
+
9,
|
|
678
|
+
19,
|
|
679
|
+
13,
|
|
680
|
+
30,
|
|
681
|
+
6,
|
|
682
|
+
22,
|
|
683
|
+
11,
|
|
684
|
+
4,
|
|
685
|
+
25
|
|
686
|
+
];
|
|
687
|
+
const E_BOX_TABLE = [
|
|
688
|
+
32,
|
|
689
|
+
1,
|
|
690
|
+
2,
|
|
691
|
+
3,
|
|
692
|
+
4,
|
|
693
|
+
5,
|
|
694
|
+
4,
|
|
695
|
+
5,
|
|
696
|
+
6,
|
|
697
|
+
7,
|
|
698
|
+
8,
|
|
699
|
+
9,
|
|
700
|
+
8,
|
|
701
|
+
9,
|
|
702
|
+
10,
|
|
703
|
+
11,
|
|
704
|
+
12,
|
|
705
|
+
13,
|
|
706
|
+
12,
|
|
707
|
+
13,
|
|
708
|
+
14,
|
|
709
|
+
15,
|
|
710
|
+
16,
|
|
711
|
+
17,
|
|
712
|
+
16,
|
|
713
|
+
17,
|
|
714
|
+
18,
|
|
715
|
+
19,
|
|
716
|
+
20,
|
|
717
|
+
21,
|
|
718
|
+
20,
|
|
719
|
+
21,
|
|
720
|
+
22,
|
|
721
|
+
23,
|
|
722
|
+
24,
|
|
723
|
+
25,
|
|
724
|
+
24,
|
|
725
|
+
25,
|
|
726
|
+
26,
|
|
727
|
+
27,
|
|
728
|
+
28,
|
|
729
|
+
29,
|
|
730
|
+
28,
|
|
731
|
+
29,
|
|
732
|
+
30,
|
|
733
|
+
31,
|
|
734
|
+
32,
|
|
735
|
+
1
|
|
736
|
+
];
|
|
737
|
+
const KEY_RND_SHIFT = [
|
|
738
|
+
1,
|
|
739
|
+
1,
|
|
740
|
+
2,
|
|
741
|
+
2,
|
|
742
|
+
2,
|
|
743
|
+
2,
|
|
744
|
+
2,
|
|
745
|
+
2,
|
|
746
|
+
1,
|
|
747
|
+
2,
|
|
748
|
+
2,
|
|
749
|
+
2,
|
|
750
|
+
2,
|
|
751
|
+
2,
|
|
752
|
+
2,
|
|
753
|
+
1
|
|
754
|
+
];
|
|
755
|
+
const KEY_PERM_C = [
|
|
756
|
+
56,
|
|
757
|
+
48,
|
|
758
|
+
40,
|
|
759
|
+
32,
|
|
760
|
+
24,
|
|
761
|
+
16,
|
|
762
|
+
8,
|
|
763
|
+
0,
|
|
764
|
+
57,
|
|
765
|
+
49,
|
|
766
|
+
41,
|
|
767
|
+
33,
|
|
768
|
+
25,
|
|
769
|
+
17,
|
|
770
|
+
9,
|
|
771
|
+
1,
|
|
772
|
+
58,
|
|
773
|
+
50,
|
|
774
|
+
42,
|
|
775
|
+
34,
|
|
776
|
+
26,
|
|
777
|
+
18,
|
|
778
|
+
10,
|
|
779
|
+
2,
|
|
780
|
+
59,
|
|
781
|
+
51,
|
|
782
|
+
43,
|
|
783
|
+
35
|
|
784
|
+
];
|
|
785
|
+
const KEY_PERM_D = [
|
|
786
|
+
62,
|
|
787
|
+
54,
|
|
788
|
+
46,
|
|
789
|
+
38,
|
|
790
|
+
30,
|
|
791
|
+
22,
|
|
792
|
+
14,
|
|
793
|
+
6,
|
|
794
|
+
61,
|
|
795
|
+
53,
|
|
796
|
+
45,
|
|
797
|
+
37,
|
|
798
|
+
29,
|
|
799
|
+
21,
|
|
800
|
+
13,
|
|
801
|
+
5,
|
|
802
|
+
60,
|
|
803
|
+
52,
|
|
804
|
+
44,
|
|
805
|
+
36,
|
|
806
|
+
28,
|
|
807
|
+
20,
|
|
808
|
+
12,
|
|
809
|
+
4,
|
|
810
|
+
27,
|
|
811
|
+
19,
|
|
812
|
+
11,
|
|
813
|
+
3
|
|
814
|
+
];
|
|
815
|
+
const KEY_COMPRESSION = [
|
|
816
|
+
13,
|
|
817
|
+
16,
|
|
818
|
+
10,
|
|
819
|
+
23,
|
|
820
|
+
0,
|
|
821
|
+
4,
|
|
822
|
+
2,
|
|
823
|
+
27,
|
|
824
|
+
14,
|
|
825
|
+
5,
|
|
826
|
+
20,
|
|
827
|
+
9,
|
|
828
|
+
22,
|
|
829
|
+
18,
|
|
830
|
+
11,
|
|
831
|
+
3,
|
|
832
|
+
25,
|
|
833
|
+
7,
|
|
834
|
+
15,
|
|
835
|
+
6,
|
|
836
|
+
26,
|
|
837
|
+
19,
|
|
838
|
+
12,
|
|
839
|
+
1,
|
|
840
|
+
40,
|
|
841
|
+
51,
|
|
842
|
+
30,
|
|
843
|
+
36,
|
|
844
|
+
46,
|
|
845
|
+
54,
|
|
846
|
+
29,
|
|
847
|
+
39,
|
|
848
|
+
50,
|
|
849
|
+
44,
|
|
850
|
+
32,
|
|
851
|
+
47,
|
|
852
|
+
43,
|
|
853
|
+
48,
|
|
854
|
+
38,
|
|
855
|
+
55,
|
|
856
|
+
33,
|
|
857
|
+
52,
|
|
858
|
+
45,
|
|
859
|
+
41,
|
|
860
|
+
49,
|
|
861
|
+
35,
|
|
862
|
+
28,
|
|
863
|
+
31
|
|
864
|
+
];
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/formats/eqrc/custom-des.ts
|
|
867
|
+
/**
|
|
868
|
+
* @internal
|
|
869
|
+
* @module custom_des
|
|
870
|
+
* @description
|
|
871
|
+
* 本模块包含了为解密 QRC 歌词而移植的、非标准的类 DES 算法的底层实现。
|
|
872
|
+
*
|
|
873
|
+
* <h2>
|
|
874
|
+
* <strong>警告:该 DES 实现并非标准实现!</strong>
|
|
875
|
+
* </h2>
|
|
876
|
+
*
|
|
877
|
+
* 它是结构类似DES的、但完全私有的分组密码算法。
|
|
878
|
+
* 本实现仅用于 QRC 歌词解密,不应用于实际安全目的。
|
|
879
|
+
*/
|
|
880
|
+
/**
|
|
881
|
+
* 从8字节密钥中根据置换表提取位,生成一个 BigInt。
|
|
882
|
+
*
|
|
883
|
+
* 这个函数对应原始C代码中的天书BITNUM宏,模拟 QQ 音乐特有的非标准的字节序处理方式。
|
|
884
|
+
* 其将 8 字节密钥视为两个独立的、小端序的32位整数拼接而成。
|
|
885
|
+
*
|
|
886
|
+
* 例如,要读取第0位(MSB),它实际访问的是 `key[3]` 的最高位。
|
|
887
|
+
* 要读取第31位,它访问的是 `key[0]` 的最低位。
|
|
888
|
+
*
|
|
889
|
+
* @param key 8字节的密钥 Uint8Array
|
|
890
|
+
* @param table 0-based 的位索引置换表
|
|
891
|
+
*/
|
|
892
|
+
function permuteFromKeyBytes(key, table) {
|
|
893
|
+
let output = 0n;
|
|
894
|
+
let currentBitMask = 1n << BigInt(table.length - 1);
|
|
895
|
+
for (let i = 0; i < table.length; i++) {
|
|
896
|
+
const pos = table[i];
|
|
897
|
+
const wordIndex = pos >> 5;
|
|
898
|
+
const bitInWord = pos & 31;
|
|
899
|
+
const byteInWord = bitInWord >> 3;
|
|
900
|
+
const bitInByte = bitInWord & 7;
|
|
901
|
+
if (key[wordIndex * 4 + 3 - byteInWord] >> 7 - bitInByte & 1) output |= currentBitMask;
|
|
902
|
+
currentBitMask >>= 1n;
|
|
903
|
+
}
|
|
904
|
+
return output;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* 对一个存储在 BigInt 中的28位密钥部分进行循环左移。
|
|
908
|
+
* @param value 包含28位数据的高位的 BigInt
|
|
909
|
+
* @param amount 左移的位数
|
|
910
|
+
*/
|
|
911
|
+
function rotateLeft28Bit(value, amount) {
|
|
912
|
+
const BITS_28_MASK = 4294967280n;
|
|
913
|
+
const val = value & BITS_28_MASK;
|
|
914
|
+
return (val << BigInt(amount) | val >> BigInt(28 - amount)) & BITS_28_MASK;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* DES 密钥调度算法。
|
|
918
|
+
* 从一个64位的主密钥(实际使用56位,每字节的最低位是奇偶校验位,被忽略)
|
|
919
|
+
* 生成16个48位的轮密钥。
|
|
920
|
+
*
|
|
921
|
+
* @param key 8字节的DES密钥
|
|
922
|
+
* @param mode 加密或解密模式
|
|
923
|
+
*/
|
|
924
|
+
function keySchedule(key, mode) {
|
|
925
|
+
const schedule = new Int32Array(32);
|
|
926
|
+
const c0 = permuteFromKeyBytes(key, KEY_PERM_C);
|
|
927
|
+
const d0 = permuteFromKeyBytes(key, KEY_PERM_D);
|
|
928
|
+
let c = c0 << 4n;
|
|
929
|
+
let d = d0 << 4n;
|
|
930
|
+
for (let i = 0; i < 16; i++) {
|
|
931
|
+
const shift = KEY_RND_SHIFT[i];
|
|
932
|
+
c = rotateLeft28Bit(c, shift);
|
|
933
|
+
d = rotateLeft28Bit(d, shift);
|
|
934
|
+
const toGen = mode === 1 ? 15 - i : i;
|
|
935
|
+
let subkey48bit = 0n;
|
|
936
|
+
for (let k = 0; k < KEY_COMPRESSION.length; k++) {
|
|
937
|
+
const pos = KEY_COMPRESSION[k];
|
|
938
|
+
if ((pos < 28 ? c >> BigInt(31 - pos) & 1n : d >> BigInt(31 - (pos - 27)) & 1n) === 1n) subkey48bit |= 1n << BigInt(47 - k);
|
|
939
|
+
}
|
|
940
|
+
const b5 = Number(subkey48bit >> 40n & 255n);
|
|
941
|
+
const b4 = Number(subkey48bit >> 32n & 255n);
|
|
942
|
+
const b3 = Number(subkey48bit >> 24n & 255n);
|
|
943
|
+
const high24 = b5 << 16 | b4 << 8 | b3;
|
|
944
|
+
const b2 = Number(subkey48bit >> 16n & 255n);
|
|
945
|
+
const b1 = Number(subkey48bit >> 8n & 255n);
|
|
946
|
+
const b0 = Number(subkey48bit & 255n);
|
|
947
|
+
const low24 = b2 << 16 | b1 << 8 | b0;
|
|
948
|
+
schedule[toGen * 2] = high24;
|
|
949
|
+
schedule[toGen * 2 + 1] = low24;
|
|
950
|
+
}
|
|
951
|
+
return schedule;
|
|
952
|
+
}
|
|
953
|
+
const IP_RULE = [
|
|
954
|
+
34,
|
|
955
|
+
42,
|
|
956
|
+
50,
|
|
957
|
+
58,
|
|
958
|
+
2,
|
|
959
|
+
10,
|
|
960
|
+
18,
|
|
961
|
+
26,
|
|
962
|
+
36,
|
|
963
|
+
44,
|
|
964
|
+
52,
|
|
965
|
+
60,
|
|
966
|
+
4,
|
|
967
|
+
12,
|
|
968
|
+
20,
|
|
969
|
+
28,
|
|
970
|
+
38,
|
|
971
|
+
46,
|
|
972
|
+
54,
|
|
973
|
+
62,
|
|
974
|
+
6,
|
|
975
|
+
14,
|
|
976
|
+
22,
|
|
977
|
+
30,
|
|
978
|
+
40,
|
|
979
|
+
48,
|
|
980
|
+
56,
|
|
981
|
+
64,
|
|
982
|
+
8,
|
|
983
|
+
16,
|
|
984
|
+
24,
|
|
985
|
+
32,
|
|
986
|
+
33,
|
|
987
|
+
41,
|
|
988
|
+
49,
|
|
989
|
+
57,
|
|
990
|
+
1,
|
|
991
|
+
9,
|
|
992
|
+
17,
|
|
993
|
+
25,
|
|
994
|
+
35,
|
|
995
|
+
43,
|
|
996
|
+
51,
|
|
997
|
+
59,
|
|
998
|
+
3,
|
|
999
|
+
11,
|
|
1000
|
+
19,
|
|
1001
|
+
27,
|
|
1002
|
+
37,
|
|
1003
|
+
45,
|
|
1004
|
+
53,
|
|
1005
|
+
61,
|
|
1006
|
+
5,
|
|
1007
|
+
13,
|
|
1008
|
+
21,
|
|
1009
|
+
29,
|
|
1010
|
+
39,
|
|
1011
|
+
47,
|
|
1012
|
+
55,
|
|
1013
|
+
63,
|
|
1014
|
+
7,
|
|
1015
|
+
15,
|
|
1016
|
+
23,
|
|
1017
|
+
31
|
|
1018
|
+
];
|
|
1019
|
+
const INV_IP_RULE = [
|
|
1020
|
+
37,
|
|
1021
|
+
5,
|
|
1022
|
+
45,
|
|
1023
|
+
13,
|
|
1024
|
+
53,
|
|
1025
|
+
21,
|
|
1026
|
+
61,
|
|
1027
|
+
29,
|
|
1028
|
+
38,
|
|
1029
|
+
6,
|
|
1030
|
+
46,
|
|
1031
|
+
14,
|
|
1032
|
+
54,
|
|
1033
|
+
22,
|
|
1034
|
+
62,
|
|
1035
|
+
30,
|
|
1036
|
+
39,
|
|
1037
|
+
7,
|
|
1038
|
+
47,
|
|
1039
|
+
15,
|
|
1040
|
+
55,
|
|
1041
|
+
23,
|
|
1042
|
+
63,
|
|
1043
|
+
31,
|
|
1044
|
+
40,
|
|
1045
|
+
8,
|
|
1046
|
+
48,
|
|
1047
|
+
16,
|
|
1048
|
+
56,
|
|
1049
|
+
24,
|
|
1050
|
+
64,
|
|
1051
|
+
32,
|
|
1052
|
+
33,
|
|
1053
|
+
1,
|
|
1054
|
+
41,
|
|
1055
|
+
9,
|
|
1056
|
+
49,
|
|
1057
|
+
17,
|
|
1058
|
+
57,
|
|
1059
|
+
25,
|
|
1060
|
+
34,
|
|
1061
|
+
2,
|
|
1062
|
+
42,
|
|
1063
|
+
10,
|
|
1064
|
+
50,
|
|
1065
|
+
18,
|
|
1066
|
+
58,
|
|
1067
|
+
26,
|
|
1068
|
+
35,
|
|
1069
|
+
3,
|
|
1070
|
+
43,
|
|
1071
|
+
11,
|
|
1072
|
+
51,
|
|
1073
|
+
19,
|
|
1074
|
+
59,
|
|
1075
|
+
27,
|
|
1076
|
+
36,
|
|
1077
|
+
4,
|
|
1078
|
+
44,
|
|
1079
|
+
12,
|
|
1080
|
+
52,
|
|
1081
|
+
20,
|
|
1082
|
+
60,
|
|
1083
|
+
28
|
|
1084
|
+
];
|
|
1085
|
+
const IP_LEFT_TABLE = new Int32Array(2048);
|
|
1086
|
+
const IP_RIGHT_TABLE = new Int32Array(2048);
|
|
1087
|
+
const INV_IP_LEFT_TABLE = new Int32Array(2048);
|
|
1088
|
+
const INV_IP_RIGHT_TABLE = new Int32Array(2048);
|
|
1089
|
+
function generatePermutationTables() {
|
|
1090
|
+
const applyPermutation = (input, rule) => {
|
|
1091
|
+
let output = 0n;
|
|
1092
|
+
for (let i = 0; i < 64; i++) {
|
|
1093
|
+
const srcBit1Based = rule[i];
|
|
1094
|
+
if (input >> BigInt(64 - srcBit1Based) & 1n) output |= 1n << BigInt(63 - i);
|
|
1095
|
+
}
|
|
1096
|
+
return output;
|
|
1097
|
+
};
|
|
1098
|
+
for (let bytePos = 0; bytePos < 8; bytePos++) for (let byteVal = 0; byteVal < 256; byteVal++) {
|
|
1099
|
+
const permuted = applyPermutation(BigInt(byteVal) << BigInt(56 - bytePos * 8), IP_RULE);
|
|
1100
|
+
const idx = bytePos << 8 | byteVal;
|
|
1101
|
+
IP_LEFT_TABLE[idx] = Number(permuted >> 32n & 4294967295n);
|
|
1102
|
+
IP_RIGHT_TABLE[idx] = Number(permuted & 4294967295n);
|
|
1103
|
+
}
|
|
1104
|
+
for (let blockPos = 0; blockPos < 8; blockPos++) for (let blockVal = 0; blockVal < 256; blockVal++) {
|
|
1105
|
+
const permuted = applyPermutation(BigInt(blockVal) << BigInt(56 - blockPos * 8), INV_IP_RULE);
|
|
1106
|
+
const idx = blockPos << 8 | blockVal;
|
|
1107
|
+
INV_IP_LEFT_TABLE[idx] = Number(permuted >> 32n & 4294967295n);
|
|
1108
|
+
INV_IP_RIGHT_TABLE[idx] = Number(permuted & 4294967295n);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
generatePermutationTables();
|
|
1112
|
+
/**
|
|
1113
|
+
* 计算 DES S-盒的查找索引。
|
|
1114
|
+
* @param a 一个包含6位数据的 u8
|
|
1115
|
+
*/
|
|
1116
|
+
function calculateSboxIndex(a) {
|
|
1117
|
+
return a & 32 | (a & 31) >> 1 | (a & 1) << 4;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* 对一个 32 位整数应用非标准的 P 盒置换规则。
|
|
1121
|
+
* @param input S-盒代换后的 32 位中间结果
|
|
1122
|
+
*/
|
|
1123
|
+
function applyQqPboxPermutation(input) {
|
|
1124
|
+
let output = 0;
|
|
1125
|
+
for (let i = 0; i < 32; i++) {
|
|
1126
|
+
const sourceBit1Based = P_BOX[i];
|
|
1127
|
+
const destBitMask = 1 << 31 - i;
|
|
1128
|
+
if ((input & 1 << 32 - sourceBit1Based) !== 0) output |= destBitMask;
|
|
1129
|
+
}
|
|
1130
|
+
return output;
|
|
1131
|
+
}
|
|
1132
|
+
const SP_TABLE = new Int32Array(512);
|
|
1133
|
+
/**
|
|
1134
|
+
* 生成 S-P 盒合并查找表以提高性能。
|
|
1135
|
+
*/
|
|
1136
|
+
function generateSpTables() {
|
|
1137
|
+
for (let sBoxIdx = 0; sBoxIdx < 8; sBoxIdx++) for (let sBoxInput = 0; sBoxInput < 64; sBoxInput++) {
|
|
1138
|
+
const sBoxIndex = calculateSboxIndex(sBoxInput);
|
|
1139
|
+
const prePBoxVal = S_BOXES[sBoxIdx][sBoxIndex] << 28 - sBoxIdx * 4;
|
|
1140
|
+
SP_TABLE[sBoxIdx << 6 | sBoxInput] = applyQqPboxPermutation(prePBoxVal);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
generateSpTables();
|
|
1144
|
+
const EBOX_HIGH_TABLE = new Int32Array(1024);
|
|
1145
|
+
const EBOX_LOW_TABLE = new Int32Array(1024);
|
|
1146
|
+
function generateEBoxTables() {
|
|
1147
|
+
for (let chunkIdx = 0; chunkIdx < 4; chunkIdx++) {
|
|
1148
|
+
const shiftIn32 = (3 - chunkIdx) * 8;
|
|
1149
|
+
for (let byteVal = 0; byteVal < 256; byteVal++) {
|
|
1150
|
+
let high24 = 0;
|
|
1151
|
+
let low24 = 0;
|
|
1152
|
+
const input = byteVal << shiftIn32;
|
|
1153
|
+
for (let i = 0; i < 24; i++) if (input >>> 32 - E_BOX_TABLE[i] & 1) high24 |= 1 << 23 - i;
|
|
1154
|
+
for (let i = 24; i < 48; i++) if (input >>> 32 - E_BOX_TABLE[i] & 1) low24 |= 1 << 47 - i;
|
|
1155
|
+
const tableIdx = chunkIdx << 8 | byteVal;
|
|
1156
|
+
EBOX_HIGH_TABLE[tableIdx] = high24;
|
|
1157
|
+
EBOX_LOW_TABLE[tableIdx] = low24;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
generateEBoxTables();
|
|
1162
|
+
/**
|
|
1163
|
+
* DES 的 F 函数。
|
|
1164
|
+
*/
|
|
1165
|
+
function fFunction(state, keyHigh24, keyLow24) {
|
|
1166
|
+
const b0 = state >>> 24 & 255;
|
|
1167
|
+
const b1 = state >>> 16 & 255;
|
|
1168
|
+
const b2 = state >>> 8 & 255;
|
|
1169
|
+
const b3 = state & 255;
|
|
1170
|
+
const eboxHigh24 = EBOX_HIGH_TABLE[b0] | EBOX_HIGH_TABLE[256 | b1] | EBOX_HIGH_TABLE[512 | b2] | EBOX_HIGH_TABLE[768 | b3];
|
|
1171
|
+
const eboxLow24 = EBOX_LOW_TABLE[b0] | EBOX_LOW_TABLE[256 | b1] | EBOX_LOW_TABLE[512 | b2] | EBOX_LOW_TABLE[768 | b3];
|
|
1172
|
+
const xorHigh24 = eboxHigh24 ^ keyHigh24;
|
|
1173
|
+
const xorLow24 = eboxLow24 ^ keyLow24;
|
|
1174
|
+
return SP_TABLE[xorHigh24 >>> 18 & 63] | SP_TABLE[64 | xorHigh24 >>> 12 & 63] | SP_TABLE[128 | xorHigh24 >>> 6 & 63] | SP_TABLE[192 | xorHigh24 & 63] | SP_TABLE[256 | xorLow24 >>> 18 & 63] | SP_TABLE[320 | xorLow24 >>> 12 & 63] | SP_TABLE[384 | xorLow24 >>> 6 & 63] | SP_TABLE[448 | xorLow24 & 63];
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* DES 加密/解密单个64位数据块。
|
|
1178
|
+
*
|
|
1179
|
+
* @param input 8字节的输入数据块 (明文或密文)。
|
|
1180
|
+
* @param output 8字节的可变切片,用于存储输出数据块 (密文或明文)。
|
|
1181
|
+
* @param keySchedule 一个包含16个轮密钥的向量的引用,每个轮密钥是6字节。
|
|
1182
|
+
*/
|
|
1183
|
+
function desCrypt(input, output, keySchedule) {
|
|
1184
|
+
let left = 0;
|
|
1185
|
+
let right = 0;
|
|
1186
|
+
for (let i = 0; i < 8; i++) {
|
|
1187
|
+
const idx = i << 8 | input[i];
|
|
1188
|
+
left |= IP_LEFT_TABLE[idx];
|
|
1189
|
+
right |= IP_RIGHT_TABLE[idx];
|
|
1190
|
+
}
|
|
1191
|
+
for (let i = 0; i < 15; i++) {
|
|
1192
|
+
const temp = right;
|
|
1193
|
+
right = (left ^ fFunction(right, keySchedule[i * 2], keySchedule[i * 2 + 1])) >>> 0;
|
|
1194
|
+
left = temp;
|
|
1195
|
+
}
|
|
1196
|
+
left = (left ^ fFunction(right, keySchedule[30], keySchedule[31])) >>> 0;
|
|
1197
|
+
let outLeft = 0;
|
|
1198
|
+
let outRight = 0;
|
|
1199
|
+
for (let i = 0; i < 4; i++) {
|
|
1200
|
+
const idxL = i << 8 | left >>> 24 - i * 8 & 255;
|
|
1201
|
+
outLeft |= INV_IP_LEFT_TABLE[idxL];
|
|
1202
|
+
outRight |= INV_IP_RIGHT_TABLE[idxL];
|
|
1203
|
+
const idxR = i + 4 << 8 | right >>> 24 - i * 8 & 255;
|
|
1204
|
+
outLeft |= INV_IP_LEFT_TABLE[idxR];
|
|
1205
|
+
outRight |= INV_IP_RIGHT_TABLE[idxR];
|
|
1206
|
+
}
|
|
1207
|
+
output[0] = outLeft >>> 24 & 255;
|
|
1208
|
+
output[1] = outLeft >>> 16 & 255;
|
|
1209
|
+
output[2] = outLeft >>> 8 & 255;
|
|
1210
|
+
output[3] = outLeft & 255;
|
|
1211
|
+
output[4] = outRight >>> 24 & 255;
|
|
1212
|
+
output[5] = outRight >>> 16 & 255;
|
|
1213
|
+
output[6] = outRight >>> 8 & 255;
|
|
1214
|
+
output[7] = outRight & 255;
|
|
1215
|
+
}
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/formats/eqrc/utils.ts
|
|
1218
|
+
/**
|
|
1219
|
+
* @module utils
|
|
1220
|
+
* @description
|
|
1221
|
+
*
|
|
1222
|
+
* 包含一些工具函数。
|
|
1223
|
+
*/
|
|
1224
|
+
/**
|
|
1225
|
+
* 将十六进制字符串转换为 Uint8Array。
|
|
1226
|
+
*/
|
|
1227
|
+
function hexToUint8Array(hex) {
|
|
1228
|
+
if (typeof Buffer !== "undefined") return Buffer.from(hex, "hex");
|
|
1229
|
+
if (hex.length % 2 !== 0) throw new Error("无效的十六进制字符串: 长度必须是偶数");
|
|
1230
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
1231
|
+
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
1232
|
+
return bytes;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* 将 Uint8Array 转换为十六进制字符串。
|
|
1236
|
+
*/
|
|
1237
|
+
function uint8ArrayToHex(bytes) {
|
|
1238
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("hex");
|
|
1239
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1240
|
+
}
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/formats/eqrc/index.ts
|
|
1243
|
+
/**
|
|
1244
|
+
* @module qrc-codec
|
|
1245
|
+
* @description
|
|
1246
|
+
* 此模块是加密与解密 QRC 歌词的核心。
|
|
1247
|
+
* 提供了两个主要的公共函数:`decryptQrc` 和 `encryptQrc`。
|
|
1248
|
+
*
|
|
1249
|
+
* 非标准 3DES 算法实现由 `custom_des` 模块提供。
|
|
1250
|
+
*
|
|
1251
|
+
* 迁移自 https://github.com/apoint123/qrc-decoder
|
|
1252
|
+
*/
|
|
1253
|
+
const DES_BLOCK_SIZE = 8;
|
|
1254
|
+
/**
|
|
1255
|
+
* 非标准 3DES 编解码器
|
|
1256
|
+
*/
|
|
1257
|
+
var QqMusicCodec = class {
|
|
1258
|
+
encryptSchedule;
|
|
1259
|
+
decryptSchedule;
|
|
1260
|
+
constructor() {
|
|
1261
|
+
this.decryptSchedule = [
|
|
1262
|
+
keySchedule(KEY_3, 1),
|
|
1263
|
+
keySchedule(KEY_2, 0),
|
|
1264
|
+
keySchedule(KEY_1, 1)
|
|
1265
|
+
];
|
|
1266
|
+
this.encryptSchedule = [
|
|
1267
|
+
keySchedule(KEY_1, 0),
|
|
1268
|
+
keySchedule(KEY_2, 1),
|
|
1269
|
+
keySchedule(KEY_3, 0)
|
|
1270
|
+
];
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* 解密一个8字节的数据块。
|
|
1274
|
+
*/
|
|
1275
|
+
decryptBlock(input, output) {
|
|
1276
|
+
const temp1 = new Uint8Array(8);
|
|
1277
|
+
const temp2 = new Uint8Array(8);
|
|
1278
|
+
desCrypt(input, temp1, this.decryptSchedule[0]);
|
|
1279
|
+
desCrypt(temp1, temp2, this.decryptSchedule[1]);
|
|
1280
|
+
desCrypt(temp2, output, this.decryptSchedule[2]);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* 加密一个8字节的数据块。
|
|
1284
|
+
*/
|
|
1285
|
+
encryptBlock(input, output) {
|
|
1286
|
+
const temp1 = new Uint8Array(8);
|
|
1287
|
+
const temp2 = new Uint8Array(8);
|
|
1288
|
+
desCrypt(input, temp1, this.encryptSchedule[0]);
|
|
1289
|
+
desCrypt(temp1, temp2, this.encryptSchedule[1]);
|
|
1290
|
+
desCrypt(temp2, output, this.encryptSchedule[2]);
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
const CODEC = new QqMusicCodec();
|
|
1294
|
+
/**
|
|
1295
|
+
* 使用零字节对数据进行填充。
|
|
1296
|
+
*
|
|
1297
|
+
* QQ音乐使用的填充方案是零填充。
|
|
1298
|
+
* @param data 需要填充的字节数据
|
|
1299
|
+
* @param blockSize 块大小,对于DES来说是8
|
|
1300
|
+
*/
|
|
1301
|
+
function zeroPad(data, blockSize) {
|
|
1302
|
+
const paddingLen = (blockSize - data.length % blockSize) % blockSize;
|
|
1303
|
+
if (paddingLen === 0) return data;
|
|
1304
|
+
const paddedData = new Uint8Array(data.length + paddingLen);
|
|
1305
|
+
paddedData.set(data, 0);
|
|
1306
|
+
return paddedData;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* 使用 Zlib 解压缩字节数据。
|
|
1310
|
+
* 同时会尝试移除头部的 UTF-8 BOM (0xEF 0xBB 0xBF)。
|
|
1311
|
+
*/
|
|
1312
|
+
function decompress(data) {
|
|
1313
|
+
const decompressed = (0, pako.inflate)(data);
|
|
1314
|
+
if (decompressed.length >= 3 && decompressed[0] === 239 && decompressed[1] === 187 && decompressed[2] === 191) return decompressed.slice(3);
|
|
1315
|
+
return decompressed;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* 解密十六进制字符串格式的 Qrc 歌词数据
|
|
1319
|
+
* 解密后可去头尾 XML 数据后通过调用 `parseQrc` 解析歌词行
|
|
1320
|
+
* @param encryptedHexString 十六进制格式的字符串,代表被加密的歌词数据
|
|
1321
|
+
* @returns 被解密出来的歌词字符串,是前后有 XML 混合的 QRC 歌词
|
|
1322
|
+
*/
|
|
1323
|
+
function decryptQrcHex(encryptedHexString) {
|
|
1324
|
+
const encryptedBytes = hexToUint8Array(encryptedHexString);
|
|
1325
|
+
if (encryptedBytes.length % DES_BLOCK_SIZE !== 0) throw new Error(`加密数据长度不是${DES_BLOCK_SIZE}的倍数`);
|
|
1326
|
+
const decryptedData = new Uint8Array(encryptedBytes.length);
|
|
1327
|
+
for (let i = 0; i < encryptedBytes.length; i += DES_BLOCK_SIZE) {
|
|
1328
|
+
const chunk = encryptedBytes.subarray(i, i + DES_BLOCK_SIZE);
|
|
1329
|
+
const outChunk = decryptedData.subarray(i, i + DES_BLOCK_SIZE);
|
|
1330
|
+
CODEC.decryptBlock(chunk, outChunk);
|
|
1331
|
+
}
|
|
1332
|
+
const decompressedBytes = decompress(decryptedData);
|
|
1333
|
+
return new TextDecoder("utf-8").decode(decompressedBytes);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* 对明文执行加密操作。
|
|
1337
|
+
* @param plaintext 明文字符串
|
|
1338
|
+
* @returns 十六进制格式的字符串,代表被加密的歌词数据
|
|
1339
|
+
*/
|
|
1340
|
+
function encryptQrcHex(plaintext) {
|
|
1341
|
+
const paddedData = zeroPad((0, pako.deflate)(new TextEncoder().encode(plaintext)), DES_BLOCK_SIZE);
|
|
1342
|
+
const encryptedData = new Uint8Array(paddedData.length);
|
|
1343
|
+
for (let i = 0; i < paddedData.length; i += DES_BLOCK_SIZE) {
|
|
1344
|
+
const chunk = paddedData.subarray(i, i + DES_BLOCK_SIZE);
|
|
1345
|
+
const outChunk = encryptedData.subarray(i, i + DES_BLOCK_SIZE);
|
|
1346
|
+
CODEC.encryptBlock(chunk, outChunk);
|
|
1347
|
+
}
|
|
1348
|
+
return uint8ArrayToHex(encryptedData);
|
|
1349
|
+
}
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region src/formats/eslrc.ts
|
|
1352
|
+
const TIME_REGEX = /^\[((?:\d+:)*\d+(?:\.\d+)?)\]/;
|
|
1353
|
+
function parseTimestampPrefix(src) {
|
|
1354
|
+
const match = src.match(TIME_REGEX);
|
|
1355
|
+
if (!match) return null;
|
|
1356
|
+
const [raw, timeStr] = match;
|
|
1357
|
+
return {
|
|
1358
|
+
time: parseTime(timeStr),
|
|
1359
|
+
length: raw.length
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function parseEslrcLine(rawLine) {
|
|
1363
|
+
let src = rawLine.trim();
|
|
1364
|
+
const first = parseTimestampPrefix(src);
|
|
1365
|
+
if (!first) return null;
|
|
1366
|
+
src = src.slice(first.length);
|
|
1367
|
+
let startTime = first.time;
|
|
1368
|
+
if (!src.trim()) return null;
|
|
1369
|
+
const words = [];
|
|
1370
|
+
while (src.trim().length > 0) {
|
|
1371
|
+
const nextTimePos = src.indexOf("[");
|
|
1372
|
+
if (nextTimePos <= 0) return null;
|
|
1373
|
+
const word = src.slice(0, nextTimePos);
|
|
1374
|
+
const nextTime = parseTimestampPrefix(src.slice(nextTimePos));
|
|
1375
|
+
if (!nextTime) return null;
|
|
1376
|
+
words.push(createWord({
|
|
1377
|
+
word,
|
|
1378
|
+
startTime,
|
|
1379
|
+
endTime: nextTime.time
|
|
1380
|
+
}));
|
|
1381
|
+
src = src.slice(nextTimePos + nextTime.length);
|
|
1382
|
+
startTime = nextTime.time;
|
|
1383
|
+
}
|
|
1384
|
+
return createLine({ words });
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* 解析 ESLyric 逐词歌词格式字符串
|
|
1388
|
+
* @param eslrc 歌词字符串
|
|
1389
|
+
* @returns 成功解析出来的歌词
|
|
1390
|
+
*/
|
|
1391
|
+
function parseEslrc(eslrc) {
|
|
1392
|
+
const result = [];
|
|
1393
|
+
for (const rawLine of eslrc.split(/\r?\n/)) {
|
|
1394
|
+
const line = parseEslrcLine(rawLine);
|
|
1395
|
+
if (line) result.push(line);
|
|
1396
|
+
}
|
|
1397
|
+
result.sort((a, b) => (a.words[0]?.startTime ?? Number.MAX_SAFE_INTEGER) - (b.words[0]?.startTime ?? Number.MAX_SAFE_INTEGER));
|
|
1398
|
+
for (const line of result) {
|
|
1399
|
+
for (const word of line.words) {
|
|
1400
|
+
word.startTime = clampTimestamp(word.startTime);
|
|
1401
|
+
word.endTime = clampTimestamp(word.endTime);
|
|
1402
|
+
}
|
|
1403
|
+
line.startTime = clampTimestamp(line.words[0]?.startTime ?? 0);
|
|
1404
|
+
line.endTime = clampTimestamp(line.words[line.words.length - 1]?.endTime ?? 0);
|
|
1405
|
+
}
|
|
1406
|
+
return result;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* 将歌词数组转换为 ESLyric 逐词歌词格式字符串
|
|
1410
|
+
* @param lines 歌词数组
|
|
1411
|
+
* @returns ESLyric 逐词歌词格式字符串
|
|
1412
|
+
*/
|
|
1413
|
+
function stringifyEslrc(lines) {
|
|
1414
|
+
return lines.map((line) => {
|
|
1415
|
+
if (!line.words.length) return "";
|
|
1416
|
+
return `[${formatTime(clampTimestamp(line.words[0].startTime))}]${line.words.map((word) => `${word.word}[${formatTime(clampTimestamp(word.endTime))}]`).join("")}`;
|
|
1417
|
+
}).filter(Boolean).join("\n");
|
|
1418
|
+
}
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/formats/lys.ts
|
|
1421
|
+
/**
|
|
1422
|
+
* 解析 LYS 格式中的属性值
|
|
1423
|
+
* @param prop 属性值
|
|
1424
|
+
* @returns 对唱与背景标志位
|
|
1425
|
+
*/
|
|
1426
|
+
function parseProp(prop) {
|
|
1427
|
+
if (prop < 0 || prop > 8) prop = 0;
|
|
1428
|
+
return {
|
|
1429
|
+
isDuet: prop % 3 === 0 ? void 0 : prop % 3 === 2,
|
|
1430
|
+
isBG: prop <= 2 ? void 0 : prop >= 6
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* 解析 LYS 格式的歌词字符串
|
|
1435
|
+
* @param lys 歌词字符串
|
|
1436
|
+
* @returns 成功解析出来的歌词
|
|
1437
|
+
*/
|
|
1438
|
+
function parseLys(lys) {
|
|
1439
|
+
const lines = lys.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1440
|
+
const lyricLines = [];
|
|
1441
|
+
const propRegex = /^\[(\d+)\]/;
|
|
1442
|
+
const wordRegex = /(.*?)\((\d+),(\d+)\)/g;
|
|
1443
|
+
for (const lineStr of lines) {
|
|
1444
|
+
const propMatch = lineStr.match(propRegex);
|
|
1445
|
+
if (!propMatch) continue;
|
|
1446
|
+
const [, propStr] = propMatch;
|
|
1447
|
+
const content = lineStr.slice(propMatch[0].length);
|
|
1448
|
+
const words = [];
|
|
1449
|
+
const props = parseProp(Number(propStr));
|
|
1450
|
+
for (const match of content.matchAll(wordRegex)) {
|
|
1451
|
+
const [, rawWord, startStr, durStr] = match;
|
|
1452
|
+
const startTime = Number(startStr);
|
|
1453
|
+
const endTime = startTime + Number(durStr);
|
|
1454
|
+
const wordText = rawWord;
|
|
1455
|
+
words.push(createWord({
|
|
1456
|
+
word: wordText,
|
|
1457
|
+
startTime,
|
|
1458
|
+
endTime
|
|
1459
|
+
}));
|
|
1460
|
+
}
|
|
1461
|
+
const lineStartTime = words[0]?.startTime ?? 0;
|
|
1462
|
+
const lineEndTime = words[words.length - 1]?.endTime ?? 0;
|
|
1463
|
+
if (!words.length) continue;
|
|
1464
|
+
if (props.isBG === void 0) props.isBG = words.length > 0 && /^[((]/.test(words[0].word) && /[))]$/.test(words[words.length - 1].word);
|
|
1465
|
+
if (props.isBG && words.length) {
|
|
1466
|
+
words[0].word = words[0].word.replace(/^[((]/, "");
|
|
1467
|
+
words[words.length - 1].word = words[words.length - 1].word.replace(/[))]$/, "");
|
|
1468
|
+
}
|
|
1469
|
+
lyricLines.push(createLine({
|
|
1470
|
+
startTime: lineStartTime,
|
|
1471
|
+
endTime: lineEndTime,
|
|
1472
|
+
isDuet: !!props.isDuet,
|
|
1473
|
+
isBG: props.isBG,
|
|
1474
|
+
words
|
|
1475
|
+
}));
|
|
1476
|
+
}
|
|
1477
|
+
return lyricLines;
|
|
1478
|
+
}
|
|
1479
|
+
function makeProp(line) {
|
|
1480
|
+
let prop = 0;
|
|
1481
|
+
prop += line.isDuet ? 2 : 1;
|
|
1482
|
+
prop += line.isBG ? 6 : 3;
|
|
1483
|
+
return prop;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* 将歌词数组转换为 LYS 格式的字符串
|
|
1487
|
+
* @param lines 歌词数组
|
|
1488
|
+
* @returns LYS 格式的字符串
|
|
1489
|
+
*/
|
|
1490
|
+
function stringifyLys(lines) {
|
|
1491
|
+
return lines.map((line) => {
|
|
1492
|
+
const prop = makeProp(line);
|
|
1493
|
+
const printWords = [];
|
|
1494
|
+
line.words.forEach((w) => {
|
|
1495
|
+
if (w.word.trim() || !printWords.length) printWords.push({
|
|
1496
|
+
word: w.word,
|
|
1497
|
+
startTime: normalizeTimestamp(w.startTime),
|
|
1498
|
+
duration: normalizeDuration(normalizeTimestamp(w.endTime) - normalizeTimestamp(w.startTime))
|
|
1499
|
+
});
|
|
1500
|
+
else printWords[printWords.length - 1].word += w.word;
|
|
1501
|
+
});
|
|
1502
|
+
return `[${prop}]${printWords.map((w) => `${w.word}(${w.startTime},${w.duration})`).join("")}`;
|
|
1503
|
+
}).join("\n");
|
|
1504
|
+
}
|
|
1505
|
+
//#endregion
|
|
1506
|
+
//#region src/formats/lqe.ts
|
|
1507
|
+
function parseAttr(attr, headerMatches, rawLines, lines) {
|
|
1508
|
+
const headerIndex = headerMatches.findIndex((item) => {
|
|
1509
|
+
if (attr === "translatedLyric") return item.type === "translation";
|
|
1510
|
+
return item.type === "romanization";
|
|
1511
|
+
});
|
|
1512
|
+
if (headerIndex === -1) return;
|
|
1513
|
+
const timeRegex = /^\[((?:\d+:)*\d+(?:\.\d+)?)\](.*)$/;
|
|
1514
|
+
const attrLines = rawLines.slice(headerMatches[headerIndex].index + 1, headerMatches[headerIndex + 1].index).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
1515
|
+
const match = line.match(timeRegex);
|
|
1516
|
+
if (!match) return null;
|
|
1517
|
+
const [, timeStr, text] = match;
|
|
1518
|
+
const time = parseTime(timeStr);
|
|
1519
|
+
if (Number.isNaN(time)) return null;
|
|
1520
|
+
return {
|
|
1521
|
+
time,
|
|
1522
|
+
text
|
|
1523
|
+
};
|
|
1524
|
+
}).filter((item) => item !== null);
|
|
1525
|
+
let attrLineIndex = 0;
|
|
1526
|
+
for (const line of lines) {
|
|
1527
|
+
if (attrLines[attrLineIndex]?.time !== line.startTime) continue;
|
|
1528
|
+
line[attr] = attrLines[attrLineIndex].text;
|
|
1529
|
+
attrLineIndex++;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* 解析 LQE 格式的歌词字符串
|
|
1534
|
+
* @param lqe 歌词字符串
|
|
1535
|
+
* @returns 成功解析出来的歌词
|
|
1536
|
+
*/
|
|
1537
|
+
function parseLqe(lqe) {
|
|
1538
|
+
const lines = lqe.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1539
|
+
const headerRegex = /^\[([a-zA-Z]+):.+\]$/;
|
|
1540
|
+
const headerMatches = [];
|
|
1541
|
+
lines.forEach((line, index) => {
|
|
1542
|
+
const match = line.match(headerRegex);
|
|
1543
|
+
if (!match) return;
|
|
1544
|
+
const [, type] = match;
|
|
1545
|
+
if (type === "lyrics") headerMatches.push({
|
|
1546
|
+
index,
|
|
1547
|
+
type: "lyric"
|
|
1548
|
+
});
|
|
1549
|
+
else if (type === "translation") headerMatches.push({
|
|
1550
|
+
index,
|
|
1551
|
+
type: "translation"
|
|
1552
|
+
});
|
|
1553
|
+
else if (type === "pronunciation") headerMatches.push({
|
|
1554
|
+
index,
|
|
1555
|
+
type: "romanization"
|
|
1556
|
+
});
|
|
1557
|
+
else headerMatches.push({
|
|
1558
|
+
index,
|
|
1559
|
+
type: "unknown"
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
headerMatches.push({
|
|
1563
|
+
index: lines.length,
|
|
1564
|
+
type: "unknown"
|
|
1565
|
+
});
|
|
1566
|
+
const lyricHeaderIndex = headerMatches.findIndex((item) => item.type === "lyric");
|
|
1567
|
+
if (lyricHeaderIndex === -1) return [];
|
|
1568
|
+
const parsedLines = parseLys(lines.slice(headerMatches[lyricHeaderIndex].index + 1, headerMatches[lyricHeaderIndex + 1].index).join("\n"));
|
|
1569
|
+
parseAttr("translatedLyric", headerMatches, lines, parsedLines);
|
|
1570
|
+
parseAttr("romanLyric", headerMatches, lines, parsedLines);
|
|
1571
|
+
return parsedLines;
|
|
1572
|
+
}
|
|
1573
|
+
function stringifyAttr(lines, attr) {
|
|
1574
|
+
const header = attr === "translatedLyric" ? "[translation: format@LRC]" : "[pronunciation: format@LRC, language@romaji]";
|
|
1575
|
+
const contentLines = lines.map((line) => {
|
|
1576
|
+
const value = line[attr];
|
|
1577
|
+
if (!value) return null;
|
|
1578
|
+
return `[${formatTime(line.startTime)}]${value}`;
|
|
1579
|
+
}).filter((line) => line !== null);
|
|
1580
|
+
if (contentLines.length === 0) return null;
|
|
1581
|
+
return [header, ...contentLines].join("\n");
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* 将歌词数组转换为 LQE 格式的字符串
|
|
1585
|
+
* @param lines 歌词数组
|
|
1586
|
+
* @returns LQE 格式的字符串
|
|
1587
|
+
*/
|
|
1588
|
+
function stringifyLqe(lines) {
|
|
1589
|
+
return ["[Lyricify Quick Export]\n[version:1.0]", [
|
|
1590
|
+
`[lyrics: format@Lyricify Syllable]\n${stringifyLys(lines)}`,
|
|
1591
|
+
stringifyAttr(lines, "translatedLyric"),
|
|
1592
|
+
stringifyAttr(lines, "romanLyric")
|
|
1593
|
+
].filter((section) => section !== null).join("\n\n\n")].join("\n\n");
|
|
1594
|
+
}
|
|
1595
|
+
//#endregion
|
|
1596
|
+
//#region src/formats/lrc.ts
|
|
1597
|
+
/**
|
|
1598
|
+
* 解析 LyRiC 格式的歌词字符串
|
|
1599
|
+
* @param lrc 歌词字符串
|
|
1600
|
+
* @returns 成功解析出来的歌词
|
|
1601
|
+
*/
|
|
1602
|
+
function parseLrc(lrc) {
|
|
1603
|
+
const tagRegex = /^\[([a-z]+):([^\]]+)\]$/;
|
|
1604
|
+
const timeRegex = /^\[((?:\d+:)*\d+(?:\.\d+)?)\](.*)$/;
|
|
1605
|
+
const bgRegex = /^[((](.+)[))]$/;
|
|
1606
|
+
const lines = lrc.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1607
|
+
const lyricLines = [];
|
|
1608
|
+
for (let lineStr of lines) {
|
|
1609
|
+
if (tagRegex.test(lineStr)) continue;
|
|
1610
|
+
const timeStamps = [];
|
|
1611
|
+
while (true) {
|
|
1612
|
+
const match = lineStr.match(timeRegex);
|
|
1613
|
+
if (!match) break;
|
|
1614
|
+
const [, timeStr, text] = match;
|
|
1615
|
+
const timeStamp = parseTime(timeStr);
|
|
1616
|
+
if (Number.isNaN(timeStamp)) break;
|
|
1617
|
+
timeStamps.push(timeStamp);
|
|
1618
|
+
lineStr = text;
|
|
1619
|
+
}
|
|
1620
|
+
if (timeStamps.length === 0) continue;
|
|
1621
|
+
lineStr = lineStr.trim();
|
|
1622
|
+
const backgroundMatch = lineStr.match(bgRegex);
|
|
1623
|
+
const isBG = Boolean(backgroundMatch);
|
|
1624
|
+
if (backgroundMatch) lineStr = backgroundMatch[1];
|
|
1625
|
+
for (const t of timeStamps) lyricLines.push(createLine({
|
|
1626
|
+
startTime: t,
|
|
1627
|
+
endTime: MAX_LRC_TIMESTAMP,
|
|
1628
|
+
words: [createWord({
|
|
1629
|
+
word: lineStr,
|
|
1630
|
+
startTime: t,
|
|
1631
|
+
endTime: t
|
|
1632
|
+
})],
|
|
1633
|
+
isBG
|
|
1634
|
+
}));
|
|
1635
|
+
}
|
|
1636
|
+
lyricLines.sort((a, b) => a.startTime - b.startTime);
|
|
1637
|
+
for (const [prev, curr] of pairwise(lyricLines)) prev.endTime = prev.words[0].endTime = curr.startTime;
|
|
1638
|
+
return lyricLines.filter((line) => line.words[0].word);
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* 将歌词数组转换为 LyRiC 格式的字符串
|
|
1642
|
+
* @param lines 歌词数组
|
|
1643
|
+
* @returns LyRiC 格式的字符串
|
|
1644
|
+
*/
|
|
1645
|
+
function stringifyLrc(lines) {
|
|
1646
|
+
return lines.map((line) => {
|
|
1647
|
+
const text = line.words.map((w) => w.word).join("");
|
|
1648
|
+
const printText = line.isBG ? `(${text})` : text;
|
|
1649
|
+
return `[${formatTime(normalizeTimestamp(line.startTime))}]${printText}`;
|
|
1650
|
+
}).join("\n");
|
|
1651
|
+
}
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region src/formats/lrca2.ts
|
|
1654
|
+
/**
|
|
1655
|
+
* 解析 LRC A2 格式的歌词字符串
|
|
1656
|
+
* @param lrc 歌词字符串
|
|
1657
|
+
* @returns 成功解析出来的歌词
|
|
1658
|
+
*/
|
|
1659
|
+
function parseLrcA2(lrc) {
|
|
1660
|
+
const lines = lrc.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1661
|
+
const lyricLines = [];
|
|
1662
|
+
const lineTimeStampRegex = /^\[((?:\d+:)*\d+(?:\.\d+)?)\]/;
|
|
1663
|
+
const wordTimestampRegex = /<((?:\d+:)*\d+(?:\.\d+)?)>/;
|
|
1664
|
+
const wordTimestampPrefixRegex = /^<((?:\d+:)*\d+(?:\.\d+)?)>/;
|
|
1665
|
+
for (let lineStr of lines) {
|
|
1666
|
+
if (lineStr.match(/^\[([a-z]):(.+)\]$/i)) continue;
|
|
1667
|
+
const lineTimeStampmatch = lineStr.match(lineTimeStampRegex);
|
|
1668
|
+
if (!lineTimeStampmatch) continue;
|
|
1669
|
+
const [lineTimeStamp, lineTimeStr] = lineTimeStampmatch;
|
|
1670
|
+
const lineStartTime = parseTime(lineTimeStr);
|
|
1671
|
+
if (Number.isNaN(lineStartTime)) continue;
|
|
1672
|
+
lineStr = lineStr.slice(lineTimeStamp.length).trim();
|
|
1673
|
+
if (!lineStr) continue;
|
|
1674
|
+
const lineItems = [];
|
|
1675
|
+
while (lineStr.length) {
|
|
1676
|
+
const prefixedTimeStampMatch = lineStr.match(wordTimestampPrefixRegex);
|
|
1677
|
+
if (prefixedTimeStampMatch) {
|
|
1678
|
+
const [wordTimeStamp, wordTimeStr] = prefixedTimeStampMatch;
|
|
1679
|
+
const parsedWordTime = parseTime(wordTimeStr);
|
|
1680
|
+
if (!Number.isNaN(parsedWordTime)) lineItems.push(parsedWordTime);
|
|
1681
|
+
lineStr = lineStr.slice(wordTimeStamp.length);
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
const nextWordTimeStampIndex = lineStr.search(wordTimestampRegex);
|
|
1685
|
+
const text = nextWordTimeStampIndex === -1 ? lineStr : lineStr.slice(0, nextWordTimeStampIndex);
|
|
1686
|
+
lineItems.push(text);
|
|
1687
|
+
lineStr = lineStr.slice(text.length);
|
|
1688
|
+
}
|
|
1689
|
+
const words = [];
|
|
1690
|
+
lineItems.forEach((item, index) => {
|
|
1691
|
+
if (typeof item === "number") return;
|
|
1692
|
+
const startTime = lineItems[index - 1] ?? lineStartTime;
|
|
1693
|
+
const endTime = lineItems[index + 1] ?? startTime;
|
|
1694
|
+
if (typeof startTime !== "number" || typeof endTime !== "number") return;
|
|
1695
|
+
if (item.startsWith(" ") && words[words.length - 1]?.word.trim()) words.push(createWord({ word: " " }));
|
|
1696
|
+
words.push(createWord({
|
|
1697
|
+
word: item.trim(),
|
|
1698
|
+
startTime,
|
|
1699
|
+
endTime
|
|
1700
|
+
}));
|
|
1701
|
+
if (item.endsWith(" ")) words.push(createWord({ word: " " }));
|
|
1702
|
+
});
|
|
1703
|
+
const lineEndTime = words[words.length - 1]?.endTime ?? lineStartTime;
|
|
1704
|
+
lyricLines.push(createLine({
|
|
1705
|
+
startTime: lineStartTime,
|
|
1706
|
+
endTime: lineEndTime,
|
|
1707
|
+
words
|
|
1708
|
+
}));
|
|
1709
|
+
}
|
|
1710
|
+
return lyricLines;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* 将歌词数组转换为 LRC A2 格式的字符串
|
|
1714
|
+
* @param lines 歌词数组
|
|
1715
|
+
* @returns LRC A2 格式的字符串
|
|
1716
|
+
*/
|
|
1717
|
+
function stringifylrcA2(lines) {
|
|
1718
|
+
return lines.map((line) => {
|
|
1719
|
+
const normalizedLineStartTime = normalizeTimestamp(line.startTime);
|
|
1720
|
+
if (line.words.length === 0) return `[${formatTime(normalizedLineStartTime)}]`;
|
|
1721
|
+
const normalizedWords = [];
|
|
1722
|
+
line.words.forEach((w) => {
|
|
1723
|
+
if (!w.word.trim() && normalizedWords.length) {
|
|
1724
|
+
normalizedWords[normalizedWords.length - 1].word += w.word;
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
normalizedWords.push({
|
|
1728
|
+
word: w.word,
|
|
1729
|
+
startTime: normalizeTimestamp(w.startTime),
|
|
1730
|
+
endTime: normalizeTimestamp(w.endTime)
|
|
1731
|
+
});
|
|
1732
|
+
});
|
|
1733
|
+
const lineItems = normalizedWords.flatMap((w) => [w.startTime, w.word]);
|
|
1734
|
+
lineItems.push(normalizedWords[normalizedWords.length - 1].endTime);
|
|
1735
|
+
return `[${formatTime(normalizedLineStartTime)}]` + lineItems.map((item) => typeof item === "number" ? `<${formatTime(item)}>` : item).join("");
|
|
1736
|
+
}).join("\n");
|
|
1737
|
+
}
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region src/formats/lyl.ts
|
|
1740
|
+
/**
|
|
1741
|
+
* 解析 LYL 格式的歌词字符串
|
|
1742
|
+
* @param lyl 歌词字符串
|
|
1743
|
+
* @returns 成功解析出来的歌词
|
|
1744
|
+
*/
|
|
1745
|
+
function parseLyl(lyl) {
|
|
1746
|
+
const lines = lyl.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1747
|
+
const lyricLines = [];
|
|
1748
|
+
const timeRegex = /^\[(\d+),(\d+)\](.*)$/;
|
|
1749
|
+
const bgRegex = /^[((](.+)[))]$/;
|
|
1750
|
+
for (const lineStr of lines) {
|
|
1751
|
+
if (lineStr === "[type:LyricifyLines]") continue;
|
|
1752
|
+
const timeMatch = lineStr.match(timeRegex);
|
|
1753
|
+
if (!timeMatch) continue;
|
|
1754
|
+
const [, startStr, endStr, text] = timeMatch;
|
|
1755
|
+
const startTime = Number(startStr);
|
|
1756
|
+
const endTime = Number(endStr);
|
|
1757
|
+
const backgroundMatch = text.match(bgRegex);
|
|
1758
|
+
const isBG = Boolean(backgroundMatch);
|
|
1759
|
+
const textContent = (backgroundMatch ? backgroundMatch[1] : text).trim();
|
|
1760
|
+
if (!textContent) continue;
|
|
1761
|
+
lyricLines.push(createLine({
|
|
1762
|
+
startTime,
|
|
1763
|
+
endTime,
|
|
1764
|
+
isBG,
|
|
1765
|
+
words: [createWord({
|
|
1766
|
+
word: textContent,
|
|
1767
|
+
startTime,
|
|
1768
|
+
endTime
|
|
1769
|
+
})]
|
|
1770
|
+
}));
|
|
1771
|
+
}
|
|
1772
|
+
return lyricLines;
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* 将歌词数组转换为 LYL 格式的字符串
|
|
1776
|
+
* @param lines 歌词数组
|
|
1777
|
+
* @returns LYL 格式的字符串
|
|
1778
|
+
*/
|
|
1779
|
+
function stringifyLyl(lines) {
|
|
1780
|
+
return ["[type:LyricifyLines]", ...lines.map((line) => {
|
|
1781
|
+
const text = line.words.map((w) => w.word).join("");
|
|
1782
|
+
const printText = line.isBG ? `(${text})` : text;
|
|
1783
|
+
return `[${normalizeTimestamp(line.startTime)},${normalizeTimestamp(line.endTime)}]${printText}`;
|
|
1784
|
+
})].join("\n");
|
|
1785
|
+
}
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/formats/qrc.ts
|
|
1788
|
+
/**
|
|
1789
|
+
* 解析 QRC 格式的歌词字符串
|
|
1790
|
+
* @param qrc 歌词字符串
|
|
1791
|
+
* @returns 成功解析出来的歌词
|
|
1792
|
+
*/
|
|
1793
|
+
function parseQrc(qrc) {
|
|
1794
|
+
const wordPattern = /(.*?)\((\d+),(\d+)\)/g;
|
|
1795
|
+
const linePattern = /^\[(\d+),(\d+)\]/;
|
|
1796
|
+
return qrc.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0).map((lineStr) => {
|
|
1797
|
+
const lineMatch = lineStr.match(linePattern);
|
|
1798
|
+
if (!lineMatch) return null;
|
|
1799
|
+
const [linePrefix, lineStartStr, lineDurStr] = lineMatch;
|
|
1800
|
+
const lineStart = Number(lineStartStr);
|
|
1801
|
+
const lineDuration = Number(lineDurStr);
|
|
1802
|
+
const words = [];
|
|
1803
|
+
const lineContent = lineStr.slice(linePrefix.length).trim();
|
|
1804
|
+
if (!lineContent) return null;
|
|
1805
|
+
for (const wordMatch of lineContent.matchAll(wordPattern)) {
|
|
1806
|
+
const [, wordText, wordStartStr, wordDurStr] = wordMatch;
|
|
1807
|
+
const wordStart = Number(wordStartStr);
|
|
1808
|
+
const wordDur = Number(wordDurStr);
|
|
1809
|
+
words.push(createWord({
|
|
1810
|
+
word: wordText,
|
|
1811
|
+
startTime: wordStart,
|
|
1812
|
+
endTime: wordStart + wordDur
|
|
1813
|
+
}));
|
|
1814
|
+
}
|
|
1815
|
+
const isBG = words.length > 0 && /^[((]/.test(words[0].word) && /[))]$/.test(words[words.length - 1].word);
|
|
1816
|
+
if (isBG) {
|
|
1817
|
+
words[0].word = words[0].word.replace(/^[((]/, "");
|
|
1818
|
+
words[words.length - 1].word = words[words.length - 1].word.replace(/[))]$/, "");
|
|
1819
|
+
}
|
|
1820
|
+
return createLine({
|
|
1821
|
+
startTime: lineStart,
|
|
1822
|
+
endTime: lineStart + lineDuration,
|
|
1823
|
+
words,
|
|
1824
|
+
isBG
|
|
1825
|
+
});
|
|
1826
|
+
}).filter((line) => line !== null);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* 将歌词数组转换为 QRC 格式的字符串
|
|
1830
|
+
* @param lines 歌词数组
|
|
1831
|
+
* @returns QRC 格式的字符串
|
|
1832
|
+
*/
|
|
1833
|
+
function stringifyQrc(lines) {
|
|
1834
|
+
return lines.map((line) => {
|
|
1835
|
+
const lineStart = normalizeTimestamp(line.startTime);
|
|
1836
|
+
const lineDuration = normalizeDuration(normalizeTimestamp(line.endTime) - lineStart);
|
|
1837
|
+
const lineWords = [];
|
|
1838
|
+
for (const [index, { word, startTime, endTime }] of line.words.entries()) {
|
|
1839
|
+
if (!word.trim() && lineWords.length) {
|
|
1840
|
+
lineWords[lineWords.length - 1] += word;
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
let printedWord = word;
|
|
1844
|
+
if (line.isBG) {
|
|
1845
|
+
if (index === 0) printedWord = `(${printedWord}`;
|
|
1846
|
+
if (index === line.words.length - 1) printedWord += ")";
|
|
1847
|
+
}
|
|
1848
|
+
const normalizedWordStart = normalizeTimestamp(startTime);
|
|
1849
|
+
const wordDuration = normalizeDuration(normalizeTimestamp(endTime) - normalizedWordStart);
|
|
1850
|
+
lineWords.push(`${printedWord}(${normalizedWordStart},${wordDuration})`);
|
|
1851
|
+
}
|
|
1852
|
+
return `[${lineStart},${lineDuration}]${lineWords.join("")}`;
|
|
1853
|
+
}).join("\n");
|
|
1854
|
+
}
|
|
1855
|
+
//#endregion
|
|
1856
|
+
//#region src/formats/ttml.ts
|
|
1857
|
+
/**
|
|
1858
|
+
* 解析 TTML 格式(包含 AMLL 特有属性信息)的歌词字符串
|
|
1859
|
+
* @param ttmlText 歌词字符串
|
|
1860
|
+
* @returns 成功解析出来的 TTML 歌词对象
|
|
1861
|
+
*/
|
|
1862
|
+
function parseTTML(ttmlText) {
|
|
1863
|
+
return (0, _applemusic_like_lyrics_ttml.parseTTML)(ttmlText);
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* 将歌词数组转换为 TTML 格式(包含 AMLL 特有属性信息)的歌词字符串
|
|
1867
|
+
* @param ttmlLyric TTML 歌词对象
|
|
1868
|
+
*/
|
|
1869
|
+
function stringifyTTML(ttmlLyric) {
|
|
1870
|
+
return (0, _applemusic_like_lyrics_ttml.exportTTML)(ttmlLyric);
|
|
1871
|
+
}
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/formats/yrc.ts
|
|
1874
|
+
const beginParenPattern = /^[((]/;
|
|
1875
|
+
const endParenPattern = /[))]$/;
|
|
1876
|
+
function checkIsBG(words) {
|
|
1877
|
+
return words.length > 0 && beginParenPattern.test(words[0].word) && endParenPattern.test(words[words.length - 1].word);
|
|
1878
|
+
}
|
|
1879
|
+
function trimBGParentheses(words) {
|
|
1880
|
+
words[0].word = words[0].word.slice(1);
|
|
1881
|
+
words[words.length - 1].word = words[words.length - 1].word.slice(0, -1);
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* 解析 YRC 格式的歌词字符串
|
|
1885
|
+
* @param yrc 歌词字符串
|
|
1886
|
+
* @returns 成功解析出来的歌词
|
|
1887
|
+
*/
|
|
1888
|
+
function parseYrc(yrc) {
|
|
1889
|
+
const wordPattern = /^(.*?)\((\d+),(\d+),0\)/;
|
|
1890
|
+
const linePattern = /^\[(\d+),(\d+)\]/;
|
|
1891
|
+
return yrc.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0).map((lineStr) => {
|
|
1892
|
+
const lineMatch = lineStr.match(linePattern);
|
|
1893
|
+
if (!lineMatch) return null;
|
|
1894
|
+
const [linePrefix, lineStartStr, lineDurStr] = lineMatch;
|
|
1895
|
+
const lineStart = Number(lineStartStr);
|
|
1896
|
+
const lineDuration = Number(lineDurStr);
|
|
1897
|
+
const words = [];
|
|
1898
|
+
let lineContent = lineStr.slice(linePrefix.length).trim();
|
|
1899
|
+
if (!lineContent) return null;
|
|
1900
|
+
let lastStart = -1;
|
|
1901
|
+
let lastEnd = -1;
|
|
1902
|
+
while (true) {
|
|
1903
|
+
const wordMatch = lineContent.match(wordPattern);
|
|
1904
|
+
if (!wordMatch) break;
|
|
1905
|
+
const [fullMatch, lastText, wordStartStr, wordDurStr] = wordMatch;
|
|
1906
|
+
if (lastText && lastStart !== -1) words.push(createWord({
|
|
1907
|
+
word: lastText,
|
|
1908
|
+
startTime: lastStart,
|
|
1909
|
+
endTime: lastEnd
|
|
1910
|
+
}));
|
|
1911
|
+
const wordStart = Number(wordStartStr);
|
|
1912
|
+
const wordEnd = wordStart + Number(wordDurStr);
|
|
1913
|
+
[lastStart, lastEnd] = [wordStart, wordEnd];
|
|
1914
|
+
lineContent = lineContent.slice(fullMatch.length);
|
|
1915
|
+
}
|
|
1916
|
+
if (lastStart !== -1 && lineContent) words.push(createWord({
|
|
1917
|
+
word: lineContent,
|
|
1918
|
+
startTime: lastStart,
|
|
1919
|
+
endTime: lastEnd
|
|
1920
|
+
}));
|
|
1921
|
+
const isBG = checkIsBG(words);
|
|
1922
|
+
if (isBG) trimBGParentheses(words);
|
|
1923
|
+
return createLine({
|
|
1924
|
+
startTime: lineStart,
|
|
1925
|
+
endTime: lineStart + lineDuration,
|
|
1926
|
+
words,
|
|
1927
|
+
isBG
|
|
1928
|
+
});
|
|
1929
|
+
}).filter((line) => line !== null);
|
|
1930
|
+
}
|
|
1931
|
+
function makeParenthesesFull(text) {
|
|
1932
|
+
return text.replace(/\(/g, "(").replace(/\)/g, ")");
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* 将歌词数组转换为 YRC 格式的字符串
|
|
1936
|
+
* @param lines 歌词数组
|
|
1937
|
+
* @returns YRC 格式的字符串
|
|
1938
|
+
*/
|
|
1939
|
+
function stringifyYrc(lines) {
|
|
1940
|
+
return lines.map((line) => {
|
|
1941
|
+
const lineStart = normalizeTimestamp(line.startTime);
|
|
1942
|
+
const lineDuration = normalizeDuration(normalizeTimestamp(line.endTime) - lineStart);
|
|
1943
|
+
const lineWords = [];
|
|
1944
|
+
for (const [index, { word, startTime, endTime }] of line.words.entries()) {
|
|
1945
|
+
if (!word.trim() && lineWords.length) {
|
|
1946
|
+
lineWords[lineWords.length - 1] += word;
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
let printedWord = makeParenthesesFull(word);
|
|
1950
|
+
if (line.isBG) {
|
|
1951
|
+
if (index === 0) printedWord = `(${printedWord}`;
|
|
1952
|
+
if (index === line.words.length - 1) printedWord += ")";
|
|
1953
|
+
}
|
|
1954
|
+
const normalizedWordStart = normalizeTimestamp(startTime);
|
|
1955
|
+
const wordDuration = normalizeDuration(normalizeTimestamp(endTime) - normalizedWordStart);
|
|
1956
|
+
lineWords.push(`(${normalizedWordStart},${wordDuration},0)${printedWord}`);
|
|
1957
|
+
}
|
|
1958
|
+
return `[${lineStart},${lineDuration}]${lineWords.join("")}`;
|
|
1959
|
+
}).join("\n");
|
|
1960
|
+
}
|
|
1961
|
+
//#endregion
|
|
1962
|
+
exports.decryptQrcHex = decryptQrcHex;
|
|
1963
|
+
exports.encryptQrcHex = encryptQrcHex;
|
|
1964
|
+
exports.parseEslrc = parseEslrc;
|
|
1965
|
+
exports.parseLqe = parseLqe;
|
|
1966
|
+
exports.parseLrc = parseLrc;
|
|
1967
|
+
exports.parseLrcA2 = parseLrcA2;
|
|
1968
|
+
exports.parseLyl = parseLyl;
|
|
1969
|
+
exports.parseLys = parseLys;
|
|
1970
|
+
exports.parseQrc = parseQrc;
|
|
1971
|
+
exports.parseTTML = parseTTML;
|
|
1972
|
+
exports.parseYrc = parseYrc;
|
|
1973
|
+
exports.stringifyAss = stringifyAss;
|
|
1974
|
+
exports.stringifyEslrc = stringifyEslrc;
|
|
1975
|
+
exports.stringifyLqe = stringifyLqe;
|
|
1976
|
+
exports.stringifyLrc = stringifyLrc;
|
|
1977
|
+
exports.stringifyLyl = stringifyLyl;
|
|
1978
|
+
exports.stringifyLys = stringifyLys;
|
|
1979
|
+
exports.stringifyQrc = stringifyQrc;
|
|
1980
|
+
exports.stringifyTTML = stringifyTTML;
|
|
1981
|
+
exports.stringifyYrc = stringifyYrc;
|
|
1982
|
+
exports.stringifylrcA2 = stringifylrcA2;
|
|
1983
|
+
|
|
1984
|
+
//# sourceMappingURL=amll-lyric.cjs.map
|