@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.
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * MIDIFileTrack : Read and edit a MIDI track chunk in a given ArrayBuffer
4
+ */
5
+ class MIDIFileTrack {
6
+ constructor(buffer, start, _strictMode) {
7
+ let a;
8
+ let trackLength;
9
+ // no buffer, creating him
10
+ if (!buffer) {
11
+ a = new Uint8Array(12);
12
+ // Adding the empty track header (MTrk)
13
+ a[0] = 0x4D;
14
+ a[1] = 0x54;
15
+ a[2] = 0x72;
16
+ a[3] = 0x6B;
17
+ // Adding the empty track size (4)
18
+ a[4] = 0x00;
19
+ a[5] = 0x00;
20
+ a[6] = 0x00;
21
+ a[7] = 0x04;
22
+ // Adding the track end event
23
+ a[8] = 0x00;
24
+ a[9] = 0xFF;
25
+ a[10] = 0x2F;
26
+ a[11] = 0x00;
27
+ // Saving the buffer
28
+ this.datas = new DataView(a.buffer, 0, MIDIFileTrack.HDR_LENGTH + 4);
29
+ // parsing the given buffer
30
+ }
31
+ else {
32
+ if (!(buffer instanceof ArrayBuffer)) {
33
+ throw new Error('Invalid buffer received.');
34
+ }
35
+ start = start || 0;
36
+ // Buffer length must size at least like an empty track (8+3bytes)
37
+ if (12 > buffer.byteLength - start) {
38
+ throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' +
39
+ ' Buffer length must size at least 12bytes');
40
+ }
41
+ // Creating a temporary view to read the track header
42
+ this.datas = new DataView(buffer, start, MIDIFileTrack.HDR_LENGTH);
43
+ // Reading MIDI track header chunk
44
+ if (!('M' === String.fromCharCode(this.datas.getUint8(0)) &&
45
+ 'T' === String.fromCharCode(this.datas.getUint8(1)) &&
46
+ 'r' === String.fromCharCode(this.datas.getUint8(2)) &&
47
+ 'k' === String.fromCharCode(this.datas.getUint8(3)))) {
48
+ throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' +
49
+ ' MTrk prefix not found');
50
+ }
51
+ // Reading the track length
52
+ trackLength = this.getTrackLength();
53
+ if (buffer.byteLength - start < trackLength + 8) {
54
+ throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' +
55
+ ' The track size exceed the buffer length.');
56
+ }
57
+ // Creating the final DataView
58
+ this.datas = new DataView(buffer, start, MIDIFileTrack.HDR_LENGTH + trackLength);
59
+ // Trying to find the end of track event
60
+ if (!(0xFF === this.datas.getUint8(MIDIFileTrack.HDR_LENGTH + trackLength - 3) &&
61
+ 0x2F === this.datas.getUint8(MIDIFileTrack.HDR_LENGTH + trackLength - 2) &&
62
+ 0x00 === this.datas.getUint8(MIDIFileTrack.HDR_LENGTH + trackLength - 1))) {
63
+ throw new Error('Invalid MIDIFileTrack (0x' + start.toString(16) + ') :' +
64
+ ' No track end event found at the expected index' +
65
+ ' (' + (MIDIFileTrack.HDR_LENGTH + trackLength - 1).toString(16) + ').');
66
+ }
67
+ }
68
+ }
69
+ // Track length
70
+ getTrackLength() {
71
+ return this.datas.getUint32(4);
72
+ }
73
+ setTrackLength(trackLength) {
74
+ this.datas.setUint32(4, trackLength);
75
+ }
76
+ // Read track contents
77
+ getTrackContent() {
78
+ return new DataView(this.datas.buffer, this.datas.byteOffset + MIDIFileTrack.HDR_LENGTH, this.datas.byteLength - MIDIFileTrack.HDR_LENGTH);
79
+ }
80
+ // Set track content
81
+ setTrackContent(dataView) {
82
+ let origin;
83
+ let destination;
84
+ let i;
85
+ let j;
86
+ // Calculating the track length
87
+ const trackLength = dataView.byteLength - dataView.byteOffset;
88
+ // Track length must size at least like an empty track (4bytes)
89
+ if (4 > trackLength) {
90
+ throw new Error('Invalid track length, must size at least 4bytes');
91
+ }
92
+ this.datas = new DataView(new Uint8Array(MIDIFileTrack.HDR_LENGTH + trackLength).buffer);
93
+ // Adding the track header (MTrk)
94
+ this.datas.setUint8(0, 0x4D); // M
95
+ this.datas.setUint8(1, 0x54); // T
96
+ this.datas.setUint8(2, 0x72); // r
97
+ this.datas.setUint8(3, 0x6B); // k
98
+ // Adding the track size
99
+ this.datas.setUint32(4, trackLength);
100
+ // Copying the content
101
+ origin = new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength);
102
+ destination = new Uint8Array(this.datas.buffer, MIDIFileTrack.HDR_LENGTH, trackLength);
103
+ for (i = 0, j = origin.length; i < j; i++) {
104
+ destination[i] = origin[i];
105
+ }
106
+ }
107
+ }
108
+ module.exports = MIDIFileTrack;
109
+ // Static constants
110
+ MIDIFileTrack.HDR_LENGTH = 8;
111
+ module.exports = MIDIFileTrack;
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Text encoding utilities for MIDI/KAR files
5
+ * Supports UTF-8, TIS-620, and Windows-874 (cp874)
6
+ */
7
+
8
+ var UTF8 = require("./UTF8");
9
+ const TextEncoding = {
10
+ /**
11
+ * Detect encoding from bytes
12
+ */
13
+ detectEncoding: function (bytes, byteOffset, byteLength) {
14
+ byteOffset = byteOffset || 0;
15
+ byteLength = byteLength || (bytes.length - byteOffset);
16
+ // Try UTF-8 first
17
+ if (!UTF8.isNotUTF8(bytes, byteOffset, byteLength)) {
18
+ try {
19
+ UTF8.getStringFromBytes(bytes, byteOffset, byteLength, true);
20
+ return 'utf8';
21
+ }
22
+ catch (e) {
23
+ // Not UTF-8
24
+ }
25
+ }
26
+ // Check for TIS-620 or Windows-874 (Thai encodings)
27
+ // Both use similar byte ranges but have some differences
28
+ let hasThaiChars = false;
29
+ let hasWindows874Specific = false;
30
+ for (let i = byteOffset; i < byteOffset + byteLength && i < bytes.length; i++) {
31
+ const byte = bytes[i];
32
+ // TIS-620 range: 0xA1-0xDA, 0xDF-0xFB
33
+ // Windows-874 range: 0xA1-0xDA, 0xDF-0xFE
34
+ // Windows-874 has additional characters at 0x80-0x9F
35
+ if (byte >= 0x80 && byte <= 0x9F) {
36
+ hasWindows874Specific = true;
37
+ }
38
+ if ((byte >= 0xA1 && byte <= 0xDA) || (byte >= 0xDF && byte <= 0xFE)) {
39
+ hasThaiChars = true;
40
+ }
41
+ }
42
+ if (hasThaiChars || hasWindows874Specific) {
43
+ // Try to determine which one
44
+ // Windows-874 is more common and has more characters
45
+ // We'll try Windows-874 first, then TIS-620
46
+ return hasWindows874Specific ? 'windows874' : 'tis620';
47
+ }
48
+ // Default to latin1 (ISO-8859-1) for single-byte encoding
49
+ return 'latin1';
50
+ },
51
+ /**
52
+ * Decode bytes using specified encoding
53
+ */
54
+ decodeString: function (bytes, byteOffset, byteLength, encoding) {
55
+ byteOffset = byteOffset || 0;
56
+ byteLength = byteLength || (bytes.length - byteOffset);
57
+ // Handle empty data
58
+ if (byteLength <= 0) {
59
+ return '';
60
+ }
61
+ // Convert to Node.js Buffer for encoding support
62
+ let buffer;
63
+ try {
64
+ if (Buffer.isBuffer(bytes)) {
65
+ buffer = bytes.slice(byteOffset, byteOffset + byteLength);
66
+ }
67
+ else if (bytes instanceof Uint8Array) {
68
+ buffer = Buffer.from(bytes.buffer, bytes.byteOffset + byteOffset, byteLength);
69
+ }
70
+ else if (Array.isArray(bytes)) {
71
+ // Array
72
+ buffer = Buffer.from(bytes.slice(byteOffset, byteOffset + byteLength));
73
+ }
74
+ else {
75
+ throw new Error('Invalid bytes type');
76
+ }
77
+ }
78
+ catch (e) {
79
+ throw new Error('Failed to convert bytes to buffer: ' + e.message);
80
+ }
81
+ // Use Node.js Buffer's built-in encoding support
82
+ switch (encoding) {
83
+ case 'utf8':
84
+ try {
85
+ return UTF8.getStringFromBytes(bytes, byteOffset, byteLength, true);
86
+ }
87
+ catch (e) {
88
+ try {
89
+ return buffer.toString('utf8');
90
+ }
91
+ catch (e2) {
92
+ throw new Error('UTF-8 decoding failed');
93
+ }
94
+ }
95
+ case 'tis620':
96
+ // TIS-620 is not directly supported by Node.js Buffer
97
+ // Use manual conversion (always succeeds)
98
+ return TextEncoding.decodeTIS620(buffer);
99
+ case 'windows874':
100
+ case 'cp874':
101
+ // Try Node.js built-in win874, fallback to manual decoding
102
+ try {
103
+ return buffer.toString('win874');
104
+ }
105
+ catch (e) {
106
+ // Fallback to manual decoding (always succeeds)
107
+ return TextEncoding.decodeWindows874(buffer);
108
+ }
109
+ case 'latin1':
110
+ case 'binary':
111
+ try {
112
+ return buffer.toString('latin1');
113
+ }
114
+ catch (e) {
115
+ throw new Error('Latin1 decoding failed');
116
+ }
117
+ default:
118
+ // Try auto-detect
119
+ try {
120
+ const detected = TextEncoding.detectEncoding(bytes, byteOffset, byteLength);
121
+ return TextEncoding.decodeString(bytes, byteOffset, byteLength, detected);
122
+ }
123
+ catch (e) {
124
+ throw new Error('Auto-detection failed: ' + e.message);
125
+ }
126
+ }
127
+ },
128
+ /**
129
+ * Decode TIS-620 encoding
130
+ * TIS-620 is Thai Industrial Standard 620-2533
131
+ */
132
+ decodeTIS620: function (bytes) {
133
+ // TIS-620 to Unicode mapping for Thai characters
134
+ // Range: 0xA1-0xDA, 0xDF-0xFB
135
+ // Convert to array if needed
136
+ const byteArray = bytes instanceof Uint8Array ? bytes :
137
+ Buffer.isBuffer(bytes) ? new Uint8Array(bytes) :
138
+ new Uint8Array(bytes);
139
+ let result = '';
140
+ for (let i = 0; i < byteArray.length; i++) {
141
+ const byte = byteArray[i];
142
+ if (byte < 0x80) {
143
+ // ASCII range
144
+ result += String.fromCharCode(byte);
145
+ }
146
+ else if (byte >= 0xA1 && byte <= 0xDA) {
147
+ // Thai characters: 0xA1-0xDA maps to U+0E01-U+0E3A
148
+ result += String.fromCharCode(0x0E01 + (byte - 0xA1));
149
+ }
150
+ else if (byte >= 0xDF && byte <= 0xFB) {
151
+ // Thai characters: 0xDF-0xFB maps to U+0E3F-U+0E5B
152
+ result += String.fromCharCode(0x0E3F + (byte - 0xDF));
153
+ }
154
+ else {
155
+ // Unknown byte, use replacement character or original
156
+ result += String.fromCharCode(byte);
157
+ }
158
+ }
159
+ return result;
160
+ },
161
+ /**
162
+ * Decode Windows-874 (CP874) encoding
163
+ * Windows-874 is similar to TIS-620 but has additional characters
164
+ */
165
+ decodeWindows874: function (bytes) {
166
+ // Windows-874 to Unicode mapping
167
+ // Similar to TIS-620 but with additional characters at 0x80-0x9F
168
+ // Convert to array if needed
169
+ const byteArray = bytes instanceof Uint8Array ? bytes :
170
+ Buffer.isBuffer(bytes) ? new Uint8Array(bytes) :
171
+ new Uint8Array(bytes);
172
+ let result = '';
173
+ for (let i = 0; i < byteArray.length; i++) {
174
+ const byte = byteArray[i];
175
+ if (byte < 0x80) {
176
+ // ASCII range
177
+ result += String.fromCharCode(byte);
178
+ }
179
+ else if (byte >= 0x80 && byte <= 0x9F) {
180
+ // Windows-874 specific characters (currency symbols, etc.)
181
+ // Map to Unicode equivalents
182
+ const win874Map = {
183
+ 0x80: 0x20AC, // Euro sign
184
+ 0x82: 0x201A, // Single low-9 quotation mark
185
+ 0x83: 0x0192, // Latin small letter f with hook
186
+ 0x84: 0x201E, // Double low-9 quotation mark
187
+ 0x85: 0x2026, // Horizontal ellipsis
188
+ 0x86: 0x2020, // Dagger
189
+ 0x87: 0x2021, // Double dagger
190
+ 0x88: 0x02C6, // Modifier letter circumflex accent
191
+ 0x89: 0x2030, // Per mille sign
192
+ 0x8A: 0x2039, // Single left-pointing angle quotation mark
193
+ 0x8B: 0x203A, // Single right-pointing angle quotation mark
194
+ 0x8C: 0x2018, // Left single quotation mark
195
+ 0x8D: 0x2019, // Right single quotation mark
196
+ 0x8E: 0x201C, // Left double quotation mark
197
+ 0x8F: 0x201D, // Right double quotation mark
198
+ 0x90: 0x2013, // En dash
199
+ 0x91: 0x2014, // Em dash
200
+ 0x92: 0x02DC, // Small tilde
201
+ 0x93: 0x2122, // Trade mark sign
202
+ 0x94: 0x0161, // Latin small letter s with caron
203
+ 0x95: 0x203A, // Single right-pointing angle quotation mark
204
+ 0x96: 0x0152, // Latin capital ligature OE
205
+ 0x97: 0x0153, // Latin small ligature oe
206
+ 0x98: 0x0160, // Latin capital letter S with caron
207
+ 0x99: 0x0178, // Latin capital letter Y with diaeresis
208
+ 0x9A: 0x017E, // Latin small letter z with caron
209
+ 0x9B: 0x017D, // Latin capital letter Z with caron
210
+ };
211
+ if (win874Map[byte]) {
212
+ result += String.fromCharCode(win874Map[byte]);
213
+ }
214
+ else {
215
+ result += String.fromCharCode(byte);
216
+ }
217
+ }
218
+ else if (byte >= 0xA1 && byte <= 0xDA) {
219
+ // Thai characters: 0xA1-0xDA maps to U+0E01-U+0E3A
220
+ result += String.fromCharCode(0x0E01 + (byte - 0xA1));
221
+ }
222
+ else if (byte >= 0xDF && byte <= 0xFE) {
223
+ // Thai characters: 0xDF-0xFE maps to U+0E3F-U+0E5E
224
+ result += String.fromCharCode(0x0E3F + (byte - 0xDF));
225
+ }
226
+ else {
227
+ // Unknown byte
228
+ result += String.fromCharCode(byte);
229
+ }
230
+ }
231
+ return result;
232
+ },
233
+ /**
234
+ * Auto-detect and decode string from bytes
235
+ */
236
+ autoDecode: function (bytes, byteOffset, byteLength) {
237
+ const encoding = TextEncoding.detectEncoding(bytes, byteOffset, byteLength);
238
+ return TextEncoding.decodeString(bytes, byteOffset, byteLength, encoding);
239
+ },
240
+ /**
241
+ * Decode with fallback chain: TIS-620 → Windows-874 → UTF-8
242
+ * This is the recommended method for lyrics decoding in MIDI/KAR files
243
+ */
244
+ decodeWithFallback: function (bytes, byteOffset, byteLength) {
245
+ byteOffset = byteOffset || 0;
246
+ byteLength = byteLength || (bytes.length - byteOffset);
247
+ // Try TIS-620 first (common for Thai karaoke files)
248
+ try {
249
+ return TextEncoding.decodeString(bytes, byteOffset, byteLength, 'tis620');
250
+ }
251
+ catch (e) {
252
+ // Continue to next encoding
253
+ }
254
+ // Try Windows-874 if TIS-620 failed
255
+ try {
256
+ return TextEncoding.decodeString(bytes, byteOffset, byteLength, 'windows874');
257
+ }
258
+ catch (e) {
259
+ // Continue to next encoding
260
+ }
261
+ // Try UTF-8 as final fallback
262
+ try {
263
+ return TextEncoding.decodeString(bytes, byteOffset, byteLength, 'utf8');
264
+ }
265
+ catch (e) {
266
+ // Ultimate fallback: simple character mapping
267
+ let result = '';
268
+ for (let i = byteOffset; i < byteOffset + byteLength && i < bytes.length; i++) {
269
+ result += String.fromCharCode(bytes[i]);
270
+ }
271
+ return result;
272
+ }
273
+ }
274
+ };
275
+ module.exports = TextEncoding;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * UTF8 : Manage UTF-8 strings in ArrayBuffers
4
+ */
5
+ const UTF8 = {
6
+ // non UTF8 encoding detection (cf README file for details)
7
+ isNotUTF8: function (bytes, byteOffset, byteLength) {
8
+ try {
9
+ UTF8.getStringFromBytes(bytes, byteOffset, byteLength, true);
10
+ }
11
+ catch (e) {
12
+ return true;
13
+ }
14
+ return false;
15
+ },
16
+ // UTF8 decoding functions
17
+ getCharLength: function (theByte) {
18
+ // 4 bytes encoded char (mask 11110000)
19
+ if (0xF0 === (theByte & 0xF0)) {
20
+ return 4;
21
+ // 3 bytes encoded char (mask 11100000)
22
+ }
23
+ else if (0xE0 === (theByte & 0xE0)) {
24
+ return 3;
25
+ // 2 bytes encoded char (mask 11000000)
26
+ }
27
+ else if (0xC0 === (theByte & 0xC0)) {
28
+ return 2;
29
+ // 1 bytes encoded char
30
+ }
31
+ else if (theByte === (theByte & 0x7F)) {
32
+ return 1;
33
+ }
34
+ return 0;
35
+ },
36
+ getCharCode: function (bytes, byteOffset, charLength) {
37
+ let charCode = 0;
38
+ let mask = '';
39
+ byteOffset = byteOffset || 0;
40
+ // Retrieve charLength if not given
41
+ charLength = charLength || UTF8.getCharLength(bytes[byteOffset]);
42
+ if (charLength === 0) {
43
+ throw new Error(bytes[byteOffset].toString(2) + ' is not a significative' +
44
+ ' byte (offset:' + byteOffset + ').');
45
+ }
46
+ // Return byte value if charlength is 1
47
+ if (1 === charLength) {
48
+ return bytes[byteOffset];
49
+ }
50
+ // Test UTF8 integrity
51
+ mask = '00000000'.slice(0, charLength) + '1' + '00000000'.slice(charLength + 1);
52
+ if (bytes[byteOffset] & (parseInt(mask, 2))) {
53
+ throw new Error('Index ' + byteOffset + ': A ' + charLength + ' bytes' +
54
+ ' encoded char' + ' cannot encode the ' + (charLength + 1) + 'th rank bit to 1.');
55
+ }
56
+ // Reading the first byte
57
+ mask = '0000'.slice(0, charLength + 1) + '11111111'.slice(charLength + 1);
58
+ charCode += (bytes[byteOffset] & parseInt(mask, 2)) << ((--charLength) * 6);
59
+ // Reading the next bytes
60
+ while (charLength) {
61
+ if (0x80 !== (bytes[byteOffset + 1] & 0x80)
62
+ || 0x40 === (bytes[byteOffset + 1] & 0x40)) {
63
+ throw new Error('Index ' + (byteOffset + 1) + ': Next bytes of encoded char' +
64
+ ' must begin with a "10" bit sequence.');
65
+ }
66
+ charCode += ((bytes[++byteOffset] & 0x3F) << ((--charLength) * 6));
67
+ }
68
+ return charCode;
69
+ },
70
+ getStringFromBytes: function (bytes, byteOffset, byteLength, strict) {
71
+ let charLength;
72
+ const chars = [];
73
+ const offset = byteOffset || 0;
74
+ const length = ('number' === typeof byteLength ?
75
+ byteLength :
76
+ (bytes instanceof Uint8Array ? bytes.byteLength : bytes.length));
77
+ for (let i = offset; i < length; i++) {
78
+ charLength = UTF8.getCharLength(bytes[i]);
79
+ if (i + charLength > length) {
80
+ if (strict) {
81
+ throw new Error('Index ' + i + ': Found a ' + charLength +
82
+ ' bytes encoded char declaration but only ' +
83
+ (length - i) + ' bytes are available.');
84
+ }
85
+ }
86
+ else {
87
+ chars.push(String.fromCodePoint(UTF8.getCharCode(bytes, i, charLength)));
88
+ }
89
+ i += charLength - 1;
90
+ }
91
+ return chars.join('');
92
+ },
93
+ // UTF8 encoding functions
94
+ getBytesForCharCode: function (charCode) {
95
+ if (charCode < 128) {
96
+ return 1;
97
+ }
98
+ else if (charCode < 2048) {
99
+ return 2;
100
+ }
101
+ else if (charCode < 65536) {
102
+ return 3;
103
+ }
104
+ else if (charCode < 2097152) {
105
+ return 4;
106
+ }
107
+ throw new Error('CharCode ' + charCode + ' cannot be encoded with UTF8.');
108
+ },
109
+ setBytesFromCharCode: function (charCode, bytes, byteOffset, neededBytes) {
110
+ charCode = charCode | 0;
111
+ bytes = bytes || [];
112
+ const offset = byteOffset || 0;
113
+ const needed = neededBytes || UTF8.getBytesForCharCode(charCode);
114
+ // Setting the charCode as it to bytes if the byte length is 1
115
+ let currentOffset = offset;
116
+ if (1 === needed) {
117
+ bytes[currentOffset] = charCode;
118
+ }
119
+ else {
120
+ let remainingBytes = needed;
121
+ // Computing the first byte
122
+ bytes[currentOffset++] =
123
+ (parseInt('1111'.slice(0, remainingBytes), 2) << (8 - remainingBytes)) +
124
+ (charCode >>> ((--remainingBytes) * 6));
125
+ // Computing next bytes
126
+ for (; remainingBytes > 0;) {
127
+ bytes[currentOffset++] = ((charCode >>> ((--remainingBytes) * 6)) & 0x3F) | 0x80;
128
+ }
129
+ }
130
+ return bytes;
131
+ },
132
+ setBytesFromString: function (string, bytes, byteOffset, byteLength, strict) {
133
+ string = string || '';
134
+ bytes = bytes || [];
135
+ let offset = byteOffset || 0;
136
+ const length = ('number' === typeof byteLength ?
137
+ byteLength :
138
+ Infinity);
139
+ for (let i = 0, j = string.length; i < j; i++) {
140
+ const neededBytes = UTF8.getBytesForCharCode(string[i].codePointAt(0));
141
+ if (strict && offset + neededBytes > length) {
142
+ throw new Error('Not enought bytes to encode the char "' + string[i] +
143
+ '" at the offset "' + offset + '".');
144
+ }
145
+ UTF8.setBytesFromCharCode(string[i].codePointAt(0), bytes, offset, neededBytes);
146
+ offset += neededBytes;
147
+ }
148
+ return bytes;
149
+ }
150
+ };
151
+ module.exports = UTF8;
package/demo-server.js CHANGED
@@ -170,6 +170,83 @@ app.post('/api/convert', express.json(), async (req, res) => {
170
170
  }
171
171
  });
172
172
 
173
+ // API: Get MIDI events for playback
174
+ app.post('/api/parse-kar', express.json(), async (req, res) => {
175
+ try {
176
+ const { karData } = req.body;
177
+
178
+ if (!karData) {
179
+ return res.status(400).json({ success: false, error: 'KAR data required' });
180
+ }
181
+
182
+ console.log('Parsing KAR for playback...');
183
+
184
+ // Decode base64 to buffer
185
+ const karBuffer = Buffer.from(karData, 'base64');
186
+
187
+ // Use karaoke-player to get lyrics
188
+ const KarFile = require('./demo-libs/KarFile');
189
+ const karFile = new KarFile();
190
+ karFile.readBuffer(karBuffer);
191
+ const lyrics = karFile.getLyrics();
192
+
193
+ // Use Tone.js for accurate MIDI parsing (handles tempo correctly!)
194
+ const midi = new Midi(karBuffer);
195
+
196
+ // Extract all note events with accurate timing from Tone.js
197
+ const allEvents = [];
198
+
199
+ midi.tracks.forEach((track, trackIdx) => {
200
+ track.notes.forEach(note => {
201
+ // Note ON
202
+ allEvents.push({
203
+ track: trackIdx,
204
+ time: note.time * 1000, // Convert seconds to milliseconds
205
+ type: 9, // NOTE_ON
206
+ channel: 0,
207
+ note: note.midi,
208
+ velocity: Math.round(note.velocity * 127)
209
+ });
210
+
211
+ // Note OFF
212
+ allEvents.push({
213
+ track: trackIdx,
214
+ time: (note.time + note.duration) * 1000, // Convert seconds to milliseconds
215
+ type: 8, // NOTE_OFF
216
+ channel: 0,
217
+ note: note.midi,
218
+ velocity: 0
219
+ });
220
+ });
221
+ });
222
+
223
+ // Sort events by time
224
+ allEvents.sort((a, b) => a.time - b.time);
225
+
226
+ res.json({
227
+ success: true,
228
+ midiData: {
229
+ format: midi.header.format,
230
+ trackCount: midi.tracks.length,
231
+ ticksPerBeat: midi.header.ppq,
232
+ timeDivision: 2
233
+ },
234
+ events: allEvents,
235
+ lyrics: lyrics,
236
+ totalEvents: allEvents.length,
237
+ duration: midi.duration * 1000 // milliseconds
238
+ });
239
+
240
+ } catch (error) {
241
+ console.error('Parse error:', error);
242
+ res.status(500).json({
243
+ success: false,
244
+ error: error.message,
245
+ stack: error.stack
246
+ });
247
+ }
248
+ });
249
+
173
250
  // Start server
174
251
  app.listen(PORT, () => {
175
252
  console.log('='.repeat(80));
@@ -177,7 +254,7 @@ app.listen(PORT, () => {
177
254
  console.log('='.repeat(80));
178
255
  console.log('');
179
256
  console.log(`Server running at: http://localhost:${PORT}`);
180
- console.log(`Demo page: http://localhost:${PORT}/demo-client.html`);
257
+ console.log(`Demo page: http://localhost:${PORT}/demo-simple.html`);
181
258
  console.log('');
182
259
  console.log('Press Ctrl+C to stop');
183
260
  console.log('');