@karaplay/file-coder 1.5.0 → 1.5.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/DEMO_ENHANCED.md +207 -134
- package/DOCUMENTATION_INDEX.md +317 -0
- package/EMK_REFERENCE_DATA.json +190 -0
- package/EMK_SONGS_INFO.md +336 -0
- package/EMK_TEST_SUITE_README.md +456 -0
- package/EMK_TEST_SUITE_SUMMARY.txt +197 -0
- package/README.md +90 -0
- package/RELEASE_v1.5.1.md +190 -0
- package/RELEASE_v1.5.2.md +238 -0
- package/SONG_LIST.txt +268 -0
- package/TEMPO_TRICKS_SUMMARY.md +240 -0
- package/demo-libs/KarFile.js +391 -0
- package/demo-libs/MIDIEvents.js +325 -0
- package/demo-libs/MIDIFile.js +450 -0
- package/demo-libs/MIDIFileHeader.js +144 -0
- package/demo-libs/MIDIFileTrack.js +111 -0
- package/demo-libs/TextEncoding.js +275 -0
- package/demo-libs/UTF8.js +151 -0
- package/demo-server.js +78 -1
- package/demo-simple.html +287 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -1
- package/dist/kar-validator.d.ts +66 -0
- package/dist/kar-validator.js +152 -0
- package/dist/ncntokar.browser.js +13 -1
- package/dist/ncntokar.js +13 -1
- package/package.json +4 -1
- package/verify-emk-reference.js +230 -0
- package/analyze-emk-cursor.js +0 -169
- package/analyze-emk-simple.js +0 -124
- package/check-real-duration.js +0 -69
- package/temp/test_output.kar +0 -0
- package/test-all-emk-durations.js +0 -109
- package/test-convert-001.js +0 -130
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* KarFile : Read and parse KAR (karaoke) files
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const MIDIFile_1 = __importDefault(require("./MIDIFile"));
|
|
44
|
+
const TextEncoding_1 = __importDefault(require("./TextEncoding"));
|
|
45
|
+
class KarFile {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.fileName = null;
|
|
48
|
+
this.midiFile = null;
|
|
49
|
+
this.midiData = null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decode bytes with fallback: TIS-620 -> Windows-874 -> UTF-8 (validated)
|
|
53
|
+
* TIS-620 is a single-byte Thai encoding used in NCN KAR files
|
|
54
|
+
* This matches the logic from KarFileTIS.js
|
|
55
|
+
*/
|
|
56
|
+
decodeTIS620(bytes) {
|
|
57
|
+
if (!bytes || bytes.length === 0) {
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
// Convert to Buffer if needed
|
|
61
|
+
let buffer;
|
|
62
|
+
if (Buffer.isBuffer(bytes)) {
|
|
63
|
+
buffer = bytes;
|
|
64
|
+
}
|
|
65
|
+
else if (bytes instanceof Uint8Array) {
|
|
66
|
+
buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
67
|
+
}
|
|
68
|
+
else if (Array.isArray(bytes)) {
|
|
69
|
+
buffer = Buffer.from(bytes);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
buffer = Buffer.from(bytes);
|
|
73
|
+
}
|
|
74
|
+
// Strategy 1: Try Windows-874 first (for Thai KAR files)
|
|
75
|
+
// Windows-874 is compatible with TIS-620 for Thai characters
|
|
76
|
+
try {
|
|
77
|
+
const decoded = buffer.toString('win874');
|
|
78
|
+
// Check if result looks valid (has Thai characters or is readable)
|
|
79
|
+
if (decoded && !decoded.includes('\ufffd')) {
|
|
80
|
+
// Check if it contains Thai characters (U+0E00-U+0E7F range)
|
|
81
|
+
const hasThai = /[\u0E00-\u0E7F]/.test(decoded);
|
|
82
|
+
// Check if it's not garbled (no weird control chars except common ones)
|
|
83
|
+
const isReadable = !/[^\x20-\x7E\u0E00-\u0E7F\n\r\t]/.test(decoded) || hasThai;
|
|
84
|
+
if (isReadable) {
|
|
85
|
+
console.log(' ✓ Decoded as Windows-874 (TIS-620)');
|
|
86
|
+
return decoded;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
// Continue to next strategy
|
|
92
|
+
}
|
|
93
|
+
// Strategy 2: Try UTF-8 with validation
|
|
94
|
+
try {
|
|
95
|
+
const utf8Decoded = buffer.toString('utf8');
|
|
96
|
+
// Validate UTF-8: check for replacement characters and valid UTF-8 sequences
|
|
97
|
+
if (utf8Decoded && !utf8Decoded.includes('\ufffd')) {
|
|
98
|
+
// Additional validation: check if it's valid UTF-8
|
|
99
|
+
// Try encoding back to see if it's valid
|
|
100
|
+
try {
|
|
101
|
+
const reencoded = Buffer.from(utf8Decoded, 'utf8');
|
|
102
|
+
// Compare byte-by-byte (allow some tolerance for multi-byte sequences)
|
|
103
|
+
let isValid = true;
|
|
104
|
+
if (reencoded.length === buffer.length) {
|
|
105
|
+
// Same length, check if bytes match
|
|
106
|
+
for (let i = 0; i < Math.min(reencoded.length, buffer.length); i++) {
|
|
107
|
+
if (reencoded[i] !== buffer[i]) {
|
|
108
|
+
isValid = false;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Different length - might be valid UTF-8 but different encoding
|
|
115
|
+
// Check if decoded text is readable
|
|
116
|
+
isValid = /^[\x20-\x7E\u00A0-\uFFFF\n\r\t]*$/.test(utf8Decoded);
|
|
117
|
+
}
|
|
118
|
+
if (isValid && utf8Decoded.length > 0) {
|
|
119
|
+
console.log(' ✓ Decoded as UTF-8 (validated)');
|
|
120
|
+
return utf8Decoded;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
// Validation failed, continue
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
// Continue to next strategy
|
|
130
|
+
}
|
|
131
|
+
// Strategy 3: Manual TIS-620 to Unicode conversion (fallback)
|
|
132
|
+
console.log(' ⚠️ Using manual TIS-620 conversion (fallback)');
|
|
133
|
+
return TextEncoding_1.default.decodeTIS620(buffer);
|
|
134
|
+
}
|
|
135
|
+
readFile(filePath, callback) {
|
|
136
|
+
fs.readFile(filePath, (err, buffer) => {
|
|
137
|
+
if (err) {
|
|
138
|
+
return callback(err);
|
|
139
|
+
}
|
|
140
|
+
this.fileName = path.basename(filePath, path.extname(filePath));
|
|
141
|
+
this.readBuffer(buffer);
|
|
142
|
+
callback(null, this);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
readBuffer(buffer) {
|
|
146
|
+
console.log("=== KarFile.readBuffer() ===");
|
|
147
|
+
console.log("Buffer size:", buffer.byteLength || buffer.length, "bytes");
|
|
148
|
+
// Convert Node.js Buffer to ArrayBuffer if needed
|
|
149
|
+
let arrayBuffer;
|
|
150
|
+
if (Buffer.isBuffer(buffer)) {
|
|
151
|
+
// Convert Node.js Buffer to ArrayBuffer
|
|
152
|
+
arrayBuffer = new Uint8Array(buffer).buffer;
|
|
153
|
+
}
|
|
154
|
+
else if (buffer instanceof ArrayBuffer) {
|
|
155
|
+
arrayBuffer = buffer;
|
|
156
|
+
}
|
|
157
|
+
else if (buffer instanceof Uint8Array) {
|
|
158
|
+
const slice = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
159
|
+
arrayBuffer = slice instanceof ArrayBuffer ? slice : new Uint8Array(slice).buffer;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
throw new Error('Invalid buffer type');
|
|
163
|
+
}
|
|
164
|
+
const midiFile = new MIDIFile_1.default(arrayBuffer);
|
|
165
|
+
this.midiFile = midiFile;
|
|
166
|
+
console.log("MIDI File created:", midiFile);
|
|
167
|
+
this.midiData = {
|
|
168
|
+
format: midiFile.header.getFormat(), // 0, 1 or 2
|
|
169
|
+
trackCount: midiFile.header.getTracksCount(), // n
|
|
170
|
+
timeDivision: midiFile.header.getTimeDivision(),
|
|
171
|
+
};
|
|
172
|
+
console.log("MIDI Data:", this.midiData);
|
|
173
|
+
// Time division
|
|
174
|
+
if (midiFile.header.getTimeDivision() === MIDIFile_1.default.Header.TICKS_PER_BEAT) {
|
|
175
|
+
this.midiData.ticksPerBeat = midiFile.header.getTicksPerBeat();
|
|
176
|
+
console.log("Ticks per beat:", this.midiData.ticksPerBeat);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this.midiData.frames = midiFile.header.getSMPTEFrames();
|
|
180
|
+
this.midiData.ticksPerFrame = midiFile.header.getTicksPerFrame();
|
|
181
|
+
console.log("SMPTE Frames:", this.midiData.frames);
|
|
182
|
+
console.log("Ticks per frame:", this.midiData.ticksPerFrame);
|
|
183
|
+
}
|
|
184
|
+
console.log("=== End readBuffer() ===\n");
|
|
185
|
+
}
|
|
186
|
+
readEvents() {
|
|
187
|
+
this.MIDIEvents = this.midiFile.getMidiEvents();
|
|
188
|
+
this.trackEvents = [];
|
|
189
|
+
for (const idx in this.MIDIEvents) {
|
|
190
|
+
const ev = this.MIDIEvents[idx];
|
|
191
|
+
if (!this.trackEvents[ev.track]) {
|
|
192
|
+
this.trackEvents[ev.track] = [];
|
|
193
|
+
}
|
|
194
|
+
this.trackEvents[ev.track].push(ev);
|
|
195
|
+
}
|
|
196
|
+
return this.trackEvents;
|
|
197
|
+
}
|
|
198
|
+
convertText(txt) {
|
|
199
|
+
return txt.replace(/\\/g, "\n").replace(/\//g, "\n");
|
|
200
|
+
}
|
|
201
|
+
getText() {
|
|
202
|
+
console.log("=== KarFile.getText() ===");
|
|
203
|
+
this.textEvents = this.midiFile.getLyrics();
|
|
204
|
+
console.log("Text Events count:", this.textEvents.length);
|
|
205
|
+
this.text = [];
|
|
206
|
+
for (const idx in this.textEvents) {
|
|
207
|
+
const ev = this.textEvents[idx];
|
|
208
|
+
// ALWAYS decode from raw data as TIS-620 (NCN KAR files use TIS-620)
|
|
209
|
+
let decodedText = '';
|
|
210
|
+
if (ev.data) {
|
|
211
|
+
try {
|
|
212
|
+
let bytes = null;
|
|
213
|
+
if (ev.data instanceof Uint8Array) {
|
|
214
|
+
bytes = ev.data;
|
|
215
|
+
}
|
|
216
|
+
else if (Array.isArray(ev.data)) {
|
|
217
|
+
bytes = new Uint8Array(ev.data);
|
|
218
|
+
}
|
|
219
|
+
else if (Buffer.isBuffer(ev.data)) {
|
|
220
|
+
bytes = ev.data;
|
|
221
|
+
}
|
|
222
|
+
if (bytes && bytes.length > 0) {
|
|
223
|
+
// Always decode as TIS-620
|
|
224
|
+
decodedText = this.decodeTIS620(bytes);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
decodedText = ev.text || '';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
console.warn(` Event ${idx}: Failed to decode TIS-620:`, e.message);
|
|
232
|
+
decodedText = ev.text || '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
decodedText = ev.text || '';
|
|
237
|
+
}
|
|
238
|
+
if (!this.text[ev.track]) {
|
|
239
|
+
this.text[ev.track] = "";
|
|
240
|
+
}
|
|
241
|
+
this.text[ev.track] += this.convertText(decodedText);
|
|
242
|
+
}
|
|
243
|
+
console.log("Text by track:");
|
|
244
|
+
for (const trackIdx in this.text) {
|
|
245
|
+
console.log(` Track ${trackIdx}: ${this.text[trackIdx].substring(0, 100)}${this.text[trackIdx].length > 100 ? '...' : ''}`);
|
|
246
|
+
}
|
|
247
|
+
console.log("=== End getText() ===\n");
|
|
248
|
+
return this.text;
|
|
249
|
+
}
|
|
250
|
+
addLyrics(stime, line, trk, parts) {
|
|
251
|
+
if (!this.trackLines) {
|
|
252
|
+
this.trackLines = [];
|
|
253
|
+
}
|
|
254
|
+
if (this.trackLines.length === 0) {
|
|
255
|
+
const itime = Math.max(1, stime - 5000);
|
|
256
|
+
const diff = Math.floor((stime - itime) / 5);
|
|
257
|
+
const introParts = [
|
|
258
|
+
{ time: itime, text: "*" },
|
|
259
|
+
{ time: itime + diff * 1, text: "*" },
|
|
260
|
+
{ time: itime + diff * 2, text: "*" },
|
|
261
|
+
{ time: itime + diff * 3, text: "*" },
|
|
262
|
+
{ time: itime + diff * 4, text: "*" }
|
|
263
|
+
];
|
|
264
|
+
if (line.trim() === "") {
|
|
265
|
+
stime = itime;
|
|
266
|
+
line = "*****";
|
|
267
|
+
parts = introParts;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
this.trackLines.push({ time: itime, text: "*****", track: trk, parts: introParts });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.trackLines.push({ time: stime, text: line, track: trk, parts: parts });
|
|
274
|
+
}
|
|
275
|
+
getLyrics() {
|
|
276
|
+
console.log("=== KarFile.getLyrics() ===");
|
|
277
|
+
if (!this.textEvents) {
|
|
278
|
+
this.textEvents = this.midiFile.getLyrics();
|
|
279
|
+
}
|
|
280
|
+
console.log("Text Events:", this.textEvents.length);
|
|
281
|
+
const trackLyrics = [];
|
|
282
|
+
for (const idx in this.textEvents) {
|
|
283
|
+
const ev = this.textEvents[idx];
|
|
284
|
+
// ALWAYS decode from raw data as TIS-620 (NCN KAR files use TIS-620)
|
|
285
|
+
// Library's getLyrics() decodes as UTF-8 which is wrong for Thai KAR files
|
|
286
|
+
let decodedText = '';
|
|
287
|
+
// Priority: decode from ev.data (raw bytes) as TIS-620
|
|
288
|
+
if (ev.data) {
|
|
289
|
+
try {
|
|
290
|
+
let bytes = null;
|
|
291
|
+
if (ev.data instanceof Uint8Array) {
|
|
292
|
+
bytes = ev.data;
|
|
293
|
+
}
|
|
294
|
+
else if (Array.isArray(ev.data)) {
|
|
295
|
+
bytes = new Uint8Array(ev.data);
|
|
296
|
+
}
|
|
297
|
+
else if (Buffer.isBuffer(ev.data)) {
|
|
298
|
+
bytes = ev.data;
|
|
299
|
+
}
|
|
300
|
+
if (bytes && bytes.length > 0) {
|
|
301
|
+
// Always decode as TIS-620 for NCN KAR files
|
|
302
|
+
decodedText = this.decodeTIS620(bytes);
|
|
303
|
+
console.log(` Event ${idx}: Decoded from TIS-620, bytes=${bytes.length}, result="${decodedText.substring(0, 30)}${decodedText.length > 30 ? '...' : ''}"`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// Fallback to ev.text if no data
|
|
307
|
+
decodedText = ev.text || '';
|
|
308
|
+
console.log(` Event ${idx}: No raw data, using ev.text="${decodedText.substring(0, 30)}${decodedText.length > 30 ? '...' : ''}"`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
console.warn(` Event ${idx}: Failed to decode TIS-620:`, e.message);
|
|
313
|
+
// Fallback
|
|
314
|
+
decodedText = ev.text || '';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// No ev.data, use ev.text as fallback
|
|
319
|
+
decodedText = ev.text || '';
|
|
320
|
+
console.log(` Event ${idx}: No ev.data, using ev.text="${decodedText.substring(0, 30)}${decodedText.length > 30 ? '...' : ''}"`);
|
|
321
|
+
}
|
|
322
|
+
// Skip base64-like data (header markers)
|
|
323
|
+
if (decodedText && decodedText.length > 50 && /^[A-Za-z0-9+/=]+$/.test(decodedText)) {
|
|
324
|
+
console.log(` Event ${idx}: Skipping base64 data`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (decodedText && decodedText.indexOf('LyrHdr') === 0) {
|
|
328
|
+
console.log(` Event ${idx}: Skipping LyrHdr marker`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
// Skip empty text
|
|
332
|
+
if (!decodedText || decodedText.trim().length === 0) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const text = this.convertText(decodedText);
|
|
336
|
+
const time = Math.round(ev.playTime);
|
|
337
|
+
console.log(` Event ${idx}: track=${ev.track}, time=${time}ms, text="${text.substring(0, 30)}${text.length > 30 ? '...' : ''}"`);
|
|
338
|
+
if (!trackLyrics[ev.track]) {
|
|
339
|
+
trackLyrics[ev.track] = [];
|
|
340
|
+
}
|
|
341
|
+
trackLyrics[ev.track].push({ text: text, time: time });
|
|
342
|
+
}
|
|
343
|
+
console.log("\nProcessing lyrics by track...");
|
|
344
|
+
for (const trk in trackLyrics) {
|
|
345
|
+
console.log(` Track ${trk}: ${trackLyrics[trk].length} lyric events`);
|
|
346
|
+
}
|
|
347
|
+
for (const trk in trackLyrics) {
|
|
348
|
+
let parts = [];
|
|
349
|
+
let line = "";
|
|
350
|
+
let startTime = 0;
|
|
351
|
+
const lyrics = trackLyrics[trk];
|
|
352
|
+
for (const idx in lyrics) {
|
|
353
|
+
const time = lyrics[idx].time;
|
|
354
|
+
let text = lyrics[idx].text;
|
|
355
|
+
if (text.charAt(0) === "\n") {
|
|
356
|
+
const stime = parts.length > 0 ? parts[0].time : time;
|
|
357
|
+
this.addLyrics(stime, line, parseInt(trk), parts);
|
|
358
|
+
parts = [];
|
|
359
|
+
startTime = 0;
|
|
360
|
+
line = "";
|
|
361
|
+
text = text.substring(1);
|
|
362
|
+
}
|
|
363
|
+
if (startTime === 0) {
|
|
364
|
+
startTime = time;
|
|
365
|
+
}
|
|
366
|
+
line += text;
|
|
367
|
+
parts.push({ time: time, text: text });
|
|
368
|
+
if (line.charAt(line.length - 1) === "\n" && parts.length > 0) {
|
|
369
|
+
const finalTime = parts[0].time;
|
|
370
|
+
this.addLyrics(finalTime, line, parseInt(trk), parts);
|
|
371
|
+
startTime = 0;
|
|
372
|
+
parts = [];
|
|
373
|
+
line = "";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
console.log("Track Lines:", this.trackLines ? this.trackLines.length : 0);
|
|
378
|
+
if (this.trackLines) {
|
|
379
|
+
console.log("Sample lines:");
|
|
380
|
+
for (let i = 0; i < Math.min(5, this.trackLines.length); i++) {
|
|
381
|
+
const line = this.trackLines[i];
|
|
382
|
+
console.log(` Line ${i}: time=${line.time}ms, text="${line.text.substring(0, 50)}${line.text.length > 50 ? '...' : ''}", parts=${line.parts ? line.parts.length : 0}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.trackLyrics = trackLyrics;
|
|
386
|
+
console.log("=== End getLyrics() ===\n");
|
|
387
|
+
return this.trackLines || [];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
module.exports = KarFile;
|
|
391
|
+
module.exports = KarFile;
|