@karaplay/file-coder 1.4.7 → 1.4.8

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,115 @@
1
+ # Release v1.4.8 - ZXIO Format Tempo Fix
2
+
3
+ ## šŸ› Critical Bug Fix: ZXIO Format Tempo & Timing Synchronization
4
+
5
+ ### Problem
6
+ After implementing zxio format support in v1.4.7, lyrics were not synchronized with music playback. The tempo fix (introduced in v1.4.5) was incorrectly applied to zxio format files, causing lyrics to play at the wrong speed relative to the music.
7
+
8
+ **Symptoms:**
9
+ - Lyrics faster than music (when tempo was over-adjusted)
10
+ - Lyrics out of sync with melody
11
+
12
+ ### Root Cause
13
+ The zxio format stores MIDI data with **correct tempo already embedded**. Unlike standard MThd format which needs tempo adjustment, zxio format files should NOT have their tempo multiplied.
14
+
15
+ When tempo fix was applied to zxio files:
16
+ - Original tempo: 64 BPM
17
+ - Incorrectly adjusted to: 256 BPM (64 Ɨ 4)
18
+ - Result: **Music played too fast, lyrics out of sync**
19
+
20
+ ### Solution
21
+ Implemented format-specific tempo handling:
22
+
23
+ **ZXIO Format:**
24
+ - āœ… NO tempo adjustment
25
+ - āœ… Normal cursor timing multiplication
26
+ - āœ… Result: Perfect synchronization (-20 ticks = 0.05 seconds lead time)
27
+
28
+ **MThd Format (Standard EMK):**
29
+ - āœ… Apply tempo fix (multiply by ratio)
30
+ - āœ… Skip cursor multiplication (use raw values)
31
+ - āœ… Result: Proper tempo for standard files
32
+
33
+ ### Technical Details
34
+
35
+ **Changes:**
36
+ 1. Added `isZxioFormat` flag to `DecodedEmkParts` interface
37
+ 2. Detection: Set flag when zxio format is encountered during decode
38
+ 3. Conditional logic: Apply tempo fix only for MThd format
39
+
40
+ ```typescript
41
+ // In emk-to-kar.ts
42
+ const needsTempoFix = !decoded.isZxioFormat;
43
+
44
+ convertNcnToKar({
45
+ fixEmkTempo: needsTempoFix, // Only for MThd format
46
+ skipCursorMultiply: needsTempoFix // Only when tempo is fixed
47
+ });
48
+ ```
49
+
50
+ ### Testing Results
51
+
52
+ **ZXIO Format Files (e.g., failed01.emk, 001.emk):**
53
+ ```
54
+ āœ… Tempo: 64 BPM (correct - not multiplied)
55
+ āœ… Timing: -20 ticks (0.05 seconds)
56
+ āœ… Status: Perfect synchronization
57
+ ```
58
+
59
+ **Before Fix:**
60
+ ```
61
+ āŒ Tempo: 256 BPM (incorrect - 4x multiplied)
62
+ āŒ Timing: -365 ticks (0.89 seconds)
63
+ āŒ Status: Lyrics too fast
64
+ ```
65
+
66
+ ### Files Modified
67
+ - āœ… `src/emk/server-decode.ts` - Added `isZxioFormat` flag
68
+ - āœ… `src/emk/client-decoder.ts` - Browser version of flag
69
+ - āœ… `src/emk-to-kar.ts` - Conditional tempo fix logic
70
+ - āœ… `src/emk-to-kar.browser.ts` - Browser version
71
+ - āœ… `src/ncntokar.ts` - `skipCursorMultiply` option
72
+ - āœ… `src/ncntokar.browser.ts` - Browser version
73
+ - āœ… `package.json` - Version bump to 1.4.8
74
+
75
+ ### Usage
76
+
77
+ **No code changes needed!** The library automatically detects file format:
78
+
79
+ ```typescript
80
+ import { convertEmkToKar } from '@karaplay/file-coder';
81
+
82
+ // Works correctly for both formats
83
+ const result = convertEmkToKar({
84
+ inputEmk: 'song.emk',
85
+ outputKar: 'song.kar'
86
+ });
87
+
88
+ // ✨ zxio: No tempo adjustment, perfect sync
89
+ // ✨ MThd: Tempo adjusted, proper timing
90
+ ```
91
+
92
+ ### Compatibility
93
+ - āœ… Backward compatible with v1.4.7
94
+ - āœ… Fixes timing issues for zxio format
95
+ - āœ… Maintains proper handling for MThd format
96
+ - āœ… Works in both Node.js and browser
97
+ - āœ… No breaking changes
98
+
99
+ ### Migration
100
+ āœ… **Zero migration required** - Transparent bug fix
101
+
102
+ ### Known Limitations
103
+ - Standard MThd format files (Z251xxxx.emk) may still have timing variations depending on the source file's cursor data quality
104
+ - This is expected behavior and not related to the zxio format fix
105
+
106
+ ---
107
+
108
+ **Full Changelog:** v1.4.7...v1.4.8
109
+
110
+ **Install:**
111
+ ```bash
112
+ npm install @karaplay/file-coder@1.4.8
113
+ ```
114
+
115
+ **Issue Fixed:** Lyrics timing synchronization for zxio format EMK files
@@ -0,0 +1,156 @@
1
+ const fs = require('fs');
2
+ const { inflateSync } = require('zlib');
3
+
4
+ const XOR_KEY = Buffer.from([0xAF, 0xF2, 0x4C, 0x9C, 0xE9, 0xEA, 0x99, 0x43]);
5
+ const MAGIC_SIGNATURE = '.SFDS';
6
+ const ZLIB_SECOND_BYTES = new Set([0x01, 0x5E, 0x9C, 0xDA, 0x7D, 0x20, 0xBB]);
7
+
8
+ function xorDecrypt(data) {
9
+ const decrypted = Buffer.alloc(data.length);
10
+ for (let i = 0; i < data.length; i++) {
11
+ decrypted[i] = data[i] ^ XOR_KEY[i % XOR_KEY.length];
12
+ }
13
+ return decrypted;
14
+ }
15
+
16
+ function looksLikeText(buf) {
17
+ const sample = buf.subarray(0, Math.min(64, buf.length));
18
+ for (let i = 0; i < sample.length; i++) {
19
+ const c = sample[i];
20
+ if (c === 0x0a || c === 0x0d || c === 0x09 || (c >= 0x20 && c <= 0x7e) || c >= 0x80) {
21
+ continue;
22
+ }
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+
28
+ console.log('='.repeat(80));
29
+ console.log('ANALYZING failed01.emk STRUCTURE');
30
+ console.log('='.repeat(80));
31
+ console.log('');
32
+
33
+ const emkBuffer = fs.readFileSync('./songs/emk/failed01.emk');
34
+ console.log(`File size: ${emkBuffer.length} bytes`);
35
+ console.log('');
36
+
37
+ // Decrypt
38
+ console.log('Step 1: Decrypting...');
39
+ const decryptedBuffer = xorDecrypt(emkBuffer);
40
+
41
+ const magic = decryptedBuffer.subarray(0, MAGIC_SIGNATURE.length).toString('utf-8');
42
+ console.log(`Magic signature: "${magic}" ${magic === MAGIC_SIGNATURE ? 'āœ“' : 'āœ—'}`);
43
+
44
+ if (magic !== MAGIC_SIGNATURE) {
45
+ console.log('āŒ Invalid EMK file signature!');
46
+ process.exit(1);
47
+ }
48
+
49
+ console.log('');
50
+ console.log('Step 2: Finding zlib blocks...');
51
+ console.log('');
52
+
53
+ const inflatedParts = [];
54
+ let blockIndex = 0;
55
+
56
+ for (let i = 0; i < decryptedBuffer.length - 2; i++) {
57
+ const b0 = decryptedBuffer[i];
58
+ const b1 = decryptedBuffer[i + 1];
59
+
60
+ if (b0 !== 0x78 || !ZLIB_SECOND_BYTES.has(b1)) continue;
61
+
62
+ try {
63
+ const inflated = inflateSync(decryptedBuffer.subarray(i));
64
+ blockIndex++;
65
+
66
+ console.log(`Block ${blockIndex} at offset ${i}:`);
67
+ console.log(` Compressed from: ${i}`);
68
+ console.log(` Inflated size: ${inflated.length} bytes`);
69
+
70
+ const asciiPrefix = inflated.subarray(0, Math.min(32, inflated.length)).toString('ascii', 0, 16);
71
+ const hexPrefix = inflated.subarray(0, Math.min(32, inflated.length)).toString('hex').match(/.{1,2}/g).join(' ');
72
+
73
+ console.log(` ASCII prefix: "${asciiPrefix}"`);
74
+ console.log(` HEX prefix: ${hexPrefix}`);
75
+
76
+ let blockType = 'Unknown';
77
+ let isMidi = false;
78
+
79
+ if (asciiPrefix.startsWith('SIGNATURE=')) {
80
+ blockType = 'Header';
81
+ } else if (asciiPrefix.startsWith('CODE=')) {
82
+ blockType = 'SongInfo';
83
+ } else if (inflated.subarray(0, 4).toString('ascii') === 'MThd') {
84
+ blockType = 'MIDI (MThd)';
85
+ isMidi = true;
86
+ } else if (inflated.subarray(0, 4).toString('ascii') === 'RIFF') {
87
+ blockType = 'RIFF (WAV/similar)';
88
+ } else if (looksLikeText(inflated)) {
89
+ blockType = 'Text (Lyric/Cursor)';
90
+ } else {
91
+ blockType = 'Binary (Cursor?)';
92
+ }
93
+
94
+ console.log(` Type: ${blockType} ${isMidi ? '***' : ''}`);
95
+ console.log('');
96
+
97
+ inflatedParts.push({
98
+ type: blockType,
99
+ offset: i,
100
+ size: inflated.length,
101
+ data: inflated,
102
+ index: blockIndex
103
+ });
104
+ } catch (err) {
105
+ // Not a valid zlib block, continue
106
+ continue;
107
+ }
108
+ }
109
+
110
+ console.log('='.repeat(80));
111
+ console.log('SUMMARY');
112
+ console.log('='.repeat(80));
113
+ console.log(`Total blocks found: ${inflatedParts.length}`);
114
+
115
+ inflatedParts.forEach((block, i) => {
116
+ console.log(`${i+1}. ${block.type.padEnd(25)} - ${block.size.toString().padStart(6)} bytes (offset ${block.offset})`);
117
+ });
118
+
119
+ // Check for MIDI
120
+ const midiBlocks = inflatedParts.filter(p => p.type.includes('MIDI'));
121
+ console.log(`\nMIDI blocks: ${midiBlocks.length}`);
122
+
123
+ if (midiBlocks.length === 0) {
124
+ console.log('\nāŒ NO MIDI BLOCK FOUND!');
125
+ console.log('This is why the conversion fails.');
126
+ console.log('\nPossible reasons:');
127
+ console.log(' 1. The file is corrupted');
128
+ console.log(' 2. The MIDI data is not in standard MThd format');
129
+ console.log(' 3. The zlib compression is different');
130
+ console.log(' 4. The file format is different from standard EMK');
131
+
132
+ // Check if there's any block that might be MIDI but not detected
133
+ console.log('\nChecking for non-standard MIDI formats...');
134
+ inflatedParts.forEach((block, i) => {
135
+ if (block.type === 'Binary (Cursor?)' && block.size > 1000) {
136
+ console.log(`\nBlock ${i+1} (${block.size} bytes) might be MIDI:`);
137
+ const first16 = block.data.subarray(0, 16);
138
+ console.log(` First 16 bytes (hex): ${first16.toString('hex').match(/.{1,2}/g).join(' ')}`);
139
+ console.log(` First 16 bytes (ascii): ${first16.toString('ascii').replace(/[^\x20-\x7E]/g, '.')}`);
140
+
141
+ // Try to find MThd anywhere in the block
142
+ const mthdIndex = block.data.indexOf('MThd');
143
+ if (mthdIndex >= 0) {
144
+ console.log(` āš ļø Found 'MThd' at offset ${mthdIndex} inside this block!`);
145
+ console.log(` This block might contain MIDI data starting at offset ${mthdIndex}`);
146
+ }
147
+ }
148
+ });
149
+ } else {
150
+ console.log('\nāœ“ MIDI block found');
151
+ midiBlocks.forEach(m => {
152
+ console.log(` - Block ${m.index}: ${m.size} bytes at offset ${m.offset}`);
153
+ });
154
+ }
155
+
156
+ console.log('');
@@ -4,6 +4,7 @@ export interface DecodedEmkParts {
4
4
  midi?: Buffer;
5
5
  lyric?: Buffer;
6
6
  cursor?: Buffer;
7
+ isZxioFormat?: boolean;
7
8
  }
8
9
  export declare function xorDecrypt(data: Buffer): Buffer;
9
10
  export declare function looksLikeText(buf: Buffer): boolean;
@@ -115,6 +115,7 @@ function decodeEmk(fileBuffer) {
115
115
  else if (inflated.subarray(0, 4).toString('ascii') === 'zxio') {
116
116
  // Handle custom "zxio" MIDI format
117
117
  result.midi = convertZxioToMidi(inflated);
118
+ result.isZxioFormat = true; // Mark as zxio format for special handling
118
119
  }
119
120
  else if (looksLikeText(inflated)) {
120
121
  if (!result.lyric) {
@@ -9,6 +9,7 @@ export interface DecodedEmkParts {
9
9
  midi?: Buffer;
10
10
  lyric?: Buffer;
11
11
  cursor?: Buffer;
12
+ isZxioFormat?: boolean;
12
13
  }
13
14
  export declare function xorDecrypt(data: Buffer): Buffer;
14
15
  export declare function looksLikeText(buf: Buffer): boolean;
@@ -113,6 +113,7 @@ function decodeEmk(fileBuffer) {
113
113
  // Handle custom "zxio" MIDI format (found in some EMK files)
114
114
  // This format has a custom header followed by MIDI tracks
115
115
  result.midi = convertZxioToMidi(inflated);
116
+ result.isZxioFormat = true; // Mark as zxio format for special handling
116
117
  }
117
118
  else if (looksLikeText(inflated)) {
118
119
  if (!result.lyric) {
@@ -43,12 +43,15 @@ function convertEmkToKarBrowser(options) {
43
43
  };
44
44
  console.log(`[Browser] [2/3] Converting to KAR: ${metadata.title} - ${metadata.artist}`);
45
45
  // Step 2: Convert NCN buffers to KAR
46
+ // Note: zxio format has correct tempo already, no need to fix
47
+ const needsTempoFix = !decoded.isZxioFormat;
46
48
  const conversionResult = (0, ncntokar_browser_1.convertNcnToKarBrowser)({
47
49
  midiBuffer: decoded.midi,
48
50
  lyricBuffer: decoded.lyric,
49
51
  cursorBuffer: decoded.cursor,
50
52
  outputFileName: options.outputFileName || 'output.kar',
51
- fixEmkTempo: true // Always fix tempo for EMK-sourced files
53
+ fixEmkTempo: needsTempoFix, // Fix tempo only for standard MThd format
54
+ skipCursorMultiply: needsTempoFix // Skip cursor multiply only when tempo is fixed
52
55
  });
53
56
  warnings.push(...conversionResult.warnings);
54
57
  console.log(`[Browser] [3/3] āœ“ KAR buffer created`);
@@ -100,13 +100,18 @@ function convertEmkToKar(options) {
100
100
  };
101
101
  console.log(`[2/3] Converting to KAR: ${metadata.title} - ${metadata.artist}`);
102
102
  // Step 2: Convert NCN to KAR
103
+ // Note: zxio format has correct tempo already, no need to fix
104
+ // zxio: no tempo fix, use normal cursor multiply
105
+ // MThd: fix tempo, skip cursor multiply (use raw cursor values)
106
+ const needsTempoFix = !decoded.isZxioFormat;
103
107
  const conversionResult = (0, ncntokar_1.convertNcnToKar)({
104
108
  inputMidi: midiFile,
105
109
  inputLyr: lyricFile,
106
110
  inputCur: cursorFile,
107
111
  outputKar: options.outputKar,
108
112
  appendTitles: options.appendTitles || false,
109
- fixEmkTempo: true // Always fix tempo for EMK-sourced files
113
+ fixEmkTempo: needsTempoFix, // Fix tempo only for standard MThd format
114
+ skipCursorMultiply: needsTempoFix // Skip cursor multiply only when tempo is fixed (MThd format)
110
115
  });
111
116
  warnings.push(...conversionResult.warnings);
112
117
  console.log(`[3/3] āœ“ KAR file created: ${options.outputKar}`);
@@ -8,6 +8,7 @@ export interface BrowserConversionOptions {
8
8
  cursorBuffer: Buffer;
9
9
  outputFileName?: string;
10
10
  fixEmkTempo?: boolean;
11
+ skipCursorMultiply?: boolean;
11
12
  }
12
13
  export interface BrowserConversionResult {
13
14
  success: boolean;
@@ -69,7 +70,7 @@ export declare function buildKaraokeTrackBrowser(metadata: {
69
70
  artist: string;
70
71
  fullLyric: string;
71
72
  lines: string[];
72
- }, cursorBuffer: Buffer, ticksPerBeat: number): {
73
+ }, cursorBuffer: Buffer, ticksPerBeat: number, skipCursorMultiply?: boolean): {
73
74
  track: any[];
74
75
  warnings: string[];
75
76
  };
@@ -163,7 +163,7 @@ function parseLyricBuffer(lyricBuffer) {
163
163
  /**
164
164
  * Builds karaoke track with timing information (browser version)
165
165
  */
166
- function buildKaraokeTrackBrowser(metadata, cursorBuffer, ticksPerBeat) {
166
+ function buildKaraokeTrackBrowser(metadata, cursorBuffer, ticksPerBeat, skipCursorMultiply) {
167
167
  const karaokeTrack = [];
168
168
  const warnings = [];
169
169
  karaokeTrack.push(createMetaEvent("trackName", 0, "Words"));
@@ -206,7 +206,12 @@ function buildKaraokeTrackBrowser(metadata, cursorBuffer, ticksPerBeat) {
206
206
  warnings.push(`ran out of timing info at line: "${trimmed.substring(0, 30)}..."`);
207
207
  break;
208
208
  }
209
- absoluteTimestamp = Math.floor(absoluteTimestamp * (ticksPerBeat / 24));
209
+ // Conversion: * (ticksPerBeat / 24)
210
+ // For EMK files with tempo fix applied, skip multiplication to avoid double-adjustment
211
+ if (!skipCursorMultiply) {
212
+ absoluteTimestamp = Math.floor(absoluteTimestamp * (ticksPerBeat / 24));
213
+ }
214
+ // else: use raw cursor values (for EMK with tempo fix)
210
215
  if (absoluteTimestamp < previousAbsoluteTimestamp) {
211
216
  warnings.push("timestamp out of order - clamping");
212
217
  absoluteTimestamp = previousAbsoluteTimestamp;
@@ -410,7 +415,7 @@ function convertNcnToKarBrowser(options) {
410
415
  // Parse lyric buffer
411
416
  const metadata = parseLyricBuffer(options.lyricBuffer);
412
417
  // Build karaoke track
413
- const { track: karaokeTrack, warnings: karaokeWarnings } = buildKaraokeTrackBrowser(metadata, options.cursorBuffer, ticksPerBeat);
418
+ const { track: karaokeTrack, warnings: karaokeWarnings } = buildKaraokeTrackBrowser(metadata, options.cursorBuffer, ticksPerBeat, options.skipCursorMultiply);
414
419
  warnings.push(...karaokeWarnings);
415
420
  // Build metadata tracks
416
421
  const { lyricTrack, artistTrack, titleTrack } = buildMetadataTracksBrowser(metadata);
@@ -11,6 +11,7 @@ export interface ConversionOptions {
11
11
  appendTitles?: boolean;
12
12
  titlesFile?: string;
13
13
  fixEmkTempo?: boolean;
14
+ skipCursorMultiply?: boolean;
14
15
  }
15
16
  export interface SongMetadata {
16
17
  title: string;
@@ -62,7 +63,7 @@ export declare function parseLyricFile(lyricFilePath: string): SongMetadata;
62
63
  /**
63
64
  * Builds karaoke track with timing information
64
65
  */
65
- export declare function buildKaraokeTrack(metadata: SongMetadata, cursorBuffer: Buffer, ticksPerBeat: number): {
66
+ export declare function buildKaraokeTrack(metadata: SongMetadata, cursorBuffer: Buffer, ticksPerBeat: number, skipCursorMultiply?: boolean): {
66
67
  track: any[];
67
68
  warnings: string[];
68
69
  };
package/dist/ncntokar.js CHANGED
@@ -200,7 +200,7 @@ function parseLyricFile(lyricFilePath) {
200
200
  /**
201
201
  * Builds karaoke track with timing information
202
202
  */
203
- function buildKaraokeTrack(metadata, cursorBuffer, ticksPerBeat) {
203
+ function buildKaraokeTrack(metadata, cursorBuffer, ticksPerBeat, skipCursorMultiply) {
204
204
  const karaokeTrack = [];
205
205
  const warnings = [];
206
206
  karaokeTrack.push(metaEvent("trackName", 0, "Words"));
@@ -246,7 +246,11 @@ function buildKaraokeTrack(metadata, cursorBuffer, ticksPerBeat) {
246
246
  break;
247
247
  }
248
248
  // Conversion: * (ticksPerBeat / 24)
249
- absoluteTimestamp = Math.floor(absoluteTimestamp * (ticksPerBeat / 24));
249
+ // For EMK files with tempo fix applied, skip multiplication to avoid double-adjustment
250
+ if (!skipCursorMultiply) {
251
+ absoluteTimestamp = Math.floor(absoluteTimestamp * (ticksPerBeat / 24));
252
+ }
253
+ // else: use raw cursor values (for EMK with tempo fix)
250
254
  if (absoluteTimestamp < previousAbsoluteTimestamp) {
251
255
  warnings.push("timestamp out of order - clamping");
252
256
  absoluteTimestamp = previousAbsoluteTimestamp;
@@ -384,7 +388,7 @@ function convertNcnToKar(options) {
384
388
  const metadata = parseLyricFile(options.inputLyr);
385
389
  // Build karaoke track
386
390
  const cursorBuffer = fs.readFileSync(options.inputCur);
387
- const { track: karaokeTrack, warnings: karaokeWarnings } = buildKaraokeTrack(metadata, cursorBuffer, ticksPerBeat);
391
+ const { track: karaokeTrack, warnings: karaokeWarnings } = buildKaraokeTrack(metadata, cursorBuffer, ticksPerBeat, options.skipCursorMultiply);
388
392
  warnings.push(...karaokeWarnings);
389
393
  // Build metadata tracks
390
394
  const { lyricTrack, artistTrack, titleTrack } = buildMetadataTracks(metadata);
@@ -0,0 +1,96 @@
1
+ const fs = require('fs');
2
+ const { inflateSync } = require('zlib');
3
+ const { parseMidi } = require('midi-file');
4
+
5
+ const XOR_KEY = Buffer.from([0xAF, 0xF2, 0x4C, 0x9C, 0xE9, 0xEA, 0x99, 0x43]);
6
+ const MAGIC_SIGNATURE = '.SFDS';
7
+ const ZLIB_SECOND_BYTES = new Set([0x01, 0x5E, 0x9C, 0xDA, 0x7D, 0x20, 0xBB]);
8
+
9
+ function xorDecrypt(data) {
10
+ const decrypted = Buffer.alloc(data.length);
11
+ for (let i = 0; i < data.length; i++) {
12
+ decrypted[i] = data[i] ^ XOR_KEY[i % XOR_KEY.length];
13
+ }
14
+ return decrypted;
15
+ }
16
+
17
+ console.log('Finding MIDI in Block 3...\n');
18
+
19
+ const emkBuffer = fs.readFileSync('./songs/emk/failed01.emk');
20
+ const decryptedBuffer = xorDecrypt(emkBuffer);
21
+
22
+ // Find block 3 (offset 387)
23
+ const block3Compressed = decryptedBuffer.subarray(387);
24
+ const block3 = inflateSync(block3Compressed);
25
+
26
+ console.log(`Block 3 size: ${block3.length} bytes`);
27
+ console.log(`First 64 bytes (hex):`);
28
+ console.log(block3.subarray(0, 64).toString('hex').match(/.{1,2}/g).join(' '));
29
+ console.log('');
30
+
31
+ // Search for 'MThd'
32
+ const mthdIndex = block3.indexOf('MThd');
33
+ console.log(`Index of 'MThd': ${mthdIndex}`);
34
+
35
+ if (mthdIndex >= 0) {
36
+ console.log(`\nFound 'MThd' at offset ${mthdIndex}!`);
37
+ console.log(`\nBytes before 'MThd':`);
38
+ console.log(` HEX: ${block3.subarray(0, mthdIndex).toString('hex')}`);
39
+ console.log(` ASCII: ${block3.subarray(0, mthdIndex).toString('ascii')}`);
40
+ console.log(` Length: ${mthdIndex} bytes`);
41
+
42
+ // Extract MIDI starting from MThd
43
+ const midiData = block3.subarray(mthdIndex);
44
+ console.log(`\nMIDI data size: ${midiData.length} bytes`);
45
+ console.log(`First 32 bytes of MIDI:`);
46
+ console.log(midiData.subarray(0, 32).toString('hex').match(/.{1,2}/g).join(' '));
47
+
48
+ // Try to parse it
49
+ try {
50
+ const midi = parseMidi(midiData);
51
+ console.log(`\nāœ… MIDI parsed successfully!`);
52
+ console.log(` Format: ${midi.header.format}`);
53
+ console.log(` Tracks: ${midi.tracks.length}`);
54
+ console.log(` Ticks per beat: ${midi.header.ticksPerBeat}`);
55
+
56
+ // Check tempo
57
+ let tempoFound = false;
58
+ midi.tracks.forEach((track, idx) => {
59
+ track.forEach(event => {
60
+ if (event.type === 'setTempo' && !tempoFound) {
61
+ const bpm = (60000000 / event.microsecondsPerBeat).toFixed(2);
62
+ console.log(` Initial tempo: ${bpm} BPM (${event.microsecondsPerBeat} µs/beat) in Track ${idx}`);
63
+ tempoFound = true;
64
+ }
65
+ });
66
+ });
67
+
68
+ if (!tempoFound) {
69
+ console.log(` āš ļø No tempo events found`);
70
+ }
71
+
72
+ // Save the extracted MIDI
73
+ fs.writeFileSync('./songs/emk/failed01-extracted.mid', midiData);
74
+ console.log(`\nāœ“ Extracted MIDI saved to: ./songs/emk/failed01-extracted.mid`);
75
+
76
+ } catch (err) {
77
+ console.log(`\nāŒ Failed to parse MIDI: ${err.message}`);
78
+ }
79
+ } else {
80
+ console.log('\nāŒ No MThd signature found in Block 3');
81
+ }
82
+
83
+ // Also check what "zxio" might be
84
+ console.log('\n' + '='.repeat(60));
85
+ console.log('Analysis of header "zxio":');
86
+ console.log('='.repeat(60));
87
+
88
+ const header = block3.subarray(0, 12);
89
+ console.log('Bytes: ' + header.toString('hex').match(/.{1,2}/g).join(' '));
90
+ console.log('ASCII: ' + header.toString('ascii'));
91
+
92
+ // Could be a custom header or file identification
93
+ const possibleMagic = header.subarray(0, 4).toString('ascii');
94
+ console.log(`\nFirst 4 bytes as string: "${possibleMagic}"`);
95
+ console.log('This might be a custom EMK MIDI format identifier.');
96
+ console.log('The standard EMK decoder needs to be updated to handle this format.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karaplay/file-coder",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "A comprehensive library for encoding/decoding karaoke files (.emk, .kar, MIDI) with Next.js support. Convert EMK to KAR, read/write karaoke files, full browser and server support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/test-final.js DELETED
@@ -1,175 +0,0 @@
1
- const fs = require('fs');
2
- const { parseMidi } = require('midi-file');
3
- const { convertEmkToKar } = require('./dist/index.js');
4
-
5
- console.log('='.repeat(80));
6
- console.log('FINAL TEMPO FIX VERIFICATION');
7
- console.log('='.repeat(80));
8
- console.log('');
9
-
10
- // Convert EMK to KAR with automatic tempo fix
11
- const outputKar = './songs/fix/final-converted.kar';
12
- console.log('Step 1: Converting EMK to KAR with auto tempo fix...\n');
13
-
14
- const result = convertEmkToKar({
15
- inputEmk: './songs/fix/song.emk',
16
- outputKar: outputKar,
17
- keepIntermediateFiles: false
18
- });
19
-
20
- console.log(`\nConversion: ${result.success ? 'āœ… SUCCESS' : 'āŒ FAILED'}`);
21
- console.log(`Warnings: ${result.warnings.length}`);
22
- result.warnings.forEach(w => console.log(` - ${w}`));
23
-
24
- // Compare timing and tempo
25
- console.log('\n' + '='.repeat(80));
26
- console.log('COMPARISON');
27
- console.log('='.repeat(80));
28
-
29
- const originalMidi = parseMidi(fs.readFileSync('./songs/fix/song.kar'));
30
- const convertedMidi = parseMidi(fs.readFileSync(outputKar));
31
-
32
- // Check tempo
33
- console.log('\n1. Tempo:');
34
- let originalTempo = null;
35
- let convertedTempo = null;
36
-
37
- originalMidi.tracks.forEach(track => {
38
- track.forEach(event => {
39
- if (event.type === 'setTempo') {
40
- originalTempo = (60000000 / event.microsecondsPerBeat).toFixed(2);
41
- }
42
- });
43
- });
44
-
45
- convertedMidi.tracks.forEach(track => {
46
- track.forEach(event => {
47
- if (event.type === 'setTempo') {
48
- convertedTempo = (60000000 / event.microsecondsPerBeat).toFixed(2);
49
- }
50
- });
51
- });
52
-
53
- console.log(` Original: ${originalTempo} BPM`);
54
- console.log(` Converted: ${convertedTempo} BPM`);
55
- console.log(` Difference: ${((convertedTempo / originalTempo - 1) * 100).toFixed(2)}%`);
56
-
57
- // Check timing (first 10 lyric events)
58
- console.log('\n2. Timing (first 10 karaoke events):');
59
-
60
- function getKaraokeTrack(midi) {
61
- let maxTextEvents = 0;
62
- let karaokeTrack = null;
63
-
64
- midi.tracks.forEach(track => {
65
- const textEvents = track.filter(e => e.type === 'text');
66
- if (textEvents.length > maxTextEvents) {
67
- maxTextEvents = textEvents.length;
68
- karaokeTrack = track;
69
- }
70
- });
71
-
72
- return karaokeTrack;
73
- }
74
-
75
- const originalKaraokeTrack = getKaraokeTrack(originalMidi);
76
- const convertedKaraokeTrack = getKaraokeTrack(convertedMidi);
77
-
78
- let originalAbsolute = 0;
79
- let convertedAbsolute = 0;
80
- let originalTimes = [];
81
- let convertedTimes = [];
82
-
83
- originalKaraokeTrack.forEach(event => {
84
- originalAbsolute += event.deltaTime;
85
- if (event.type === 'text') {
86
- originalTimes.push(originalAbsolute);
87
- }
88
- });
89
-
90
- convertedKaraokeTrack.forEach(event => {
91
- convertedAbsolute += event.deltaTime;
92
- if (event.type === 'text') {
93
- convertedTimes.push(convertedAbsolute);
94
- }
95
- });
96
-
97
- console.log(' Idx | Original | Converted | Match');
98
- console.log(' ' + '-'.repeat(40));
99
-
100
- for (let i = 0; i < Math.min(10, originalTimes.length, convertedTimes.length); i++) {
101
- const match = originalTimes[i] === convertedTimes[i] ? 'āœ“' : 'āœ—';
102
- console.log(` ${(i+1).toString().padStart(3)} | ${originalTimes[i].toString().padStart(8)} | ${convertedTimes[i].toString().padStart(9)} | ${match}`);
103
- }
104
-
105
- // Calculate playback duration
106
- console.log('\n3. Playback Duration Estimate:');
107
-
108
- function calculateDuration(midi) {
109
- const ticksPerBeat = midi.header.ticksPerBeat;
110
- let microsecondsPerBeat = 500000; // default 120 BPM
111
-
112
- // Get tempo
113
- midi.tracks.forEach(track => {
114
- track.forEach(event => {
115
- if (event.type === 'setTempo') {
116
- microsecondsPerBeat = event.microsecondsPerBeat;
117
- }
118
- });
119
- });
120
-
121
- // Get last event time
122
- let maxTicks = 0;
123
- midi.tracks.forEach(track => {
124
- let currentTicks = 0;
125
- track.forEach(event => {
126
- currentTicks += event.deltaTime;
127
- if (currentTicks > maxTicks) {
128
- maxTicks = currentTicks;
129
- }
130
- });
131
- });
132
-
133
- // Calculate duration
134
- const microsecondsPerTick = microsecondsPerBeat / ticksPerBeat;
135
- const durationMicroseconds = maxTicks * microsecondsPerTick;
136
- const durationSeconds = durationMicroseconds / 1000000;
137
-
138
- return {
139
- ticks: maxTicks,
140
- seconds: durationSeconds,
141
- minutes: Math.floor(durationSeconds / 60),
142
- remainingSeconds: Math.floor(durationSeconds % 60)
143
- };
144
- }
145
-
146
- const originalDuration = calculateDuration(originalMidi);
147
- const convertedDuration = calculateDuration(convertedMidi);
148
-
149
- console.log(` Original: ${originalDuration.minutes}:${originalDuration.remainingSeconds.toString().padStart(2, '0')} (${originalDuration.ticks} ticks)`);
150
- console.log(` Converted: ${convertedDuration.minutes}:${convertedDuration.remainingSeconds.toString().padStart(2, '0')} (${convertedDuration.ticks} ticks)`);
151
-
152
- const durationDiff = Math.abs(originalDuration.seconds - convertedDuration.seconds);
153
- console.log(` Time difference: ${durationDiff.toFixed(1)} seconds`);
154
-
155
- // Summary
156
- console.log('\n' + '='.repeat(80));
157
- console.log('SUMMARY');
158
- console.log('='.repeat(80));
159
-
160
- const tempoOk = Math.abs(convertedTempo / originalTempo - 1) < 0.05; // within 5%
161
- const timingOk = originalTimes[5] === convertedTimes[5]; // check 6th event
162
- const durationOk = durationDiff < 10; // within 10 seconds
163
-
164
- console.log(`\nāœ“ Tempo fix implemented: ${tempoOk ? 'āœ… PASS' : 'āŒ FAIL'} (within 5%)`);
165
- console.log(`āœ“ Timing accuracy: ${timingOk ? 'āœ… PASS' : 'āŒ FAIL'} (exact match)`);
166
- console.log(`āœ“ Duration accuracy: ${durationOk ? 'āœ… PASS' : 'āŒ FAIL'} (within 10s)`);
167
-
168
- if (tempoOk && timingOk && durationOk) {
169
- console.log('\nšŸŽ‰ ALL TESTS PASSED! Tempo fix is working correctly.');
170
- } else {
171
- console.log('\nāš ļø Some tests failed. Review the results above.');
172
- }
173
-
174
- console.log(`\nāœ“ Converted file saved: ${outputKar}`);
175
- console.log('');