@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,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-
|
|
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('');
|