@karaplay/file-coder 1.4.8 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEMO_ENHANCED.md +168 -0
- package/DEMO_FIXED.md +57 -0
- package/DEMO_GUIDE.md +204 -0
- package/DEMO_README.md +193 -0
- package/DEMO_WORKING.md +157 -0
- package/RELEASE_v1.4.9.md +144 -0
- package/RELEASE_v1.5.0.md +91 -0
- package/WHY_DURATION_DECREASES.md +176 -0
- package/analyze-cursor-pattern.js +131 -0
- package/analyze-emk-cursor.js +169 -0
- package/analyze-emk-simple.js +124 -0
- package/analyze-tempo-duration.js +243 -0
- package/calculate-correct-ratio.js +97 -0
- package/check-real-duration.js +69 -0
- package/compare-kar-lyrics-timing.js +110 -0
- package/demo-client.html +722 -0
- package/demo-server.js +184 -0
- package/demo-simple.html +712 -0
- package/dist/emk-to-kar.browser.js +7 -4
- package/dist/emk-to-kar.js +7 -6
- package/dist/ncntokar.browser.d.ts +2 -1
- package/dist/ncntokar.browser.js +5 -3
- package/dist/ncntokar.d.ts +2 -1
- package/dist/ncntokar.js +6 -4
- package/find-zxio-tempo-ratio.js +118 -0
- package/match-cursor-to-lyrics.js +118 -0
- package/package.json +6 -2
- package/songs/emk/001_original_emk.emk +0 -0
- package/songs/fix/001_kar_convert_needtofix_tempo_fast.kar +0 -0
- package/songs/fix/001_original_emk.emk +0 -0
- package/temp/test_output.kar +0 -0
- package/test-all-emk-durations.js +109 -0
- package/test-all-emk-final.js +97 -0
- package/test-convert-001.js +130 -0
- package/analyze-failed-emk.js +0 -156
- package/check-gr.js +0 -19
- package/debug-emk-blocks.js +0 -123
- package/debug-midi-source.js +0 -108
- package/debug-tempo.js +0 -135
- package/debug-timing.js +0 -159
- package/find-midi-in-block.js +0 -96
- package/fix-tempo.js +0 -155
- package/kar-diagnostic.js +0 -158
package/debug-timing.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { parseMidi } = require('midi-file');
|
|
4
|
-
|
|
5
|
-
// Parse KAR file (which is a MIDI file)
|
|
6
|
-
function analyzeKarFile(filePath) {
|
|
7
|
-
console.log(`\n=== Analyzing: ${path.basename(filePath)} ===`);
|
|
8
|
-
|
|
9
|
-
const buffer = fs.readFileSync(filePath);
|
|
10
|
-
const midi = parseMidi(buffer);
|
|
11
|
-
|
|
12
|
-
console.log(`Format: ${midi.header.format}`);
|
|
13
|
-
console.log(`Tracks: ${midi.tracks.length}`);
|
|
14
|
-
console.log(`Ticks per beat: ${midi.header.ticksPerBeat}`);
|
|
15
|
-
|
|
16
|
-
// Find karaoke track (track with lyric text events)
|
|
17
|
-
let karaokeTrackIdx = -1;
|
|
18
|
-
let maxTextEvents = 0;
|
|
19
|
-
|
|
20
|
-
midi.tracks.forEach((track, idx) => {
|
|
21
|
-
const textEvents = track.filter(e => e.type === 'text' && e.text && e.text.trim());
|
|
22
|
-
if (textEvents.length > maxTextEvents) {
|
|
23
|
-
maxTextEvents = textEvents.length;
|
|
24
|
-
karaokeTrackIdx = idx;
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (karaokeTrackIdx === -1) {
|
|
29
|
-
console.log('No karaoke track found!');
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log(`\nKaraoke track index: ${karaokeTrackIdx}`);
|
|
34
|
-
console.log(`Text events: ${maxTextEvents}`);
|
|
35
|
-
|
|
36
|
-
// Extract timing information
|
|
37
|
-
const karaokeTrack = midi.tracks[karaokeTrackIdx];
|
|
38
|
-
let absoluteTime = 0;
|
|
39
|
-
const timings = [];
|
|
40
|
-
|
|
41
|
-
karaokeTrack.forEach((event, idx) => {
|
|
42
|
-
absoluteTime += event.deltaTime;
|
|
43
|
-
|
|
44
|
-
if (event.type === 'text' && event.text && event.text.trim()) {
|
|
45
|
-
timings.push({
|
|
46
|
-
index: idx,
|
|
47
|
-
deltaTime: event.deltaTime,
|
|
48
|
-
absoluteTime: absoluteTime,
|
|
49
|
-
text: event.text
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Show first 20 timing entries
|
|
55
|
-
console.log(`\nFirst 20 timing entries:`);
|
|
56
|
-
timings.slice(0, 20).forEach((t, i) => {
|
|
57
|
-
console.log(`${i+1}. Delta: ${t.deltaTime.toString().padStart(6)}, Absolute: ${t.absoluteTime.toString().padStart(8)}, Text: "${t.text}"`);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Calculate average delta between events
|
|
61
|
-
const deltas = timings.map(t => t.deltaTime).filter(d => d > 0);
|
|
62
|
-
const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
|
|
63
|
-
const minDelta = Math.min(...deltas);
|
|
64
|
-
const maxDelta = Math.max(...deltas);
|
|
65
|
-
|
|
66
|
-
console.log(`\nDelta statistics:`);
|
|
67
|
-
console.log(` Average: ${avgDelta.toFixed(2)}`);
|
|
68
|
-
console.log(` Min: ${minDelta}`);
|
|
69
|
-
console.log(` Max: ${maxDelta}`);
|
|
70
|
-
console.log(` Total events: ${timings.length}`);
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
ticksPerBeat: midi.header.ticksPerBeat,
|
|
74
|
-
timings: timings,
|
|
75
|
-
avgDelta: avgDelta,
|
|
76
|
-
minDelta: minDelta,
|
|
77
|
-
maxDelta: maxDelta
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Compare two KAR files
|
|
82
|
-
function compareKarFiles(originalPath, convertedPath) {
|
|
83
|
-
console.log('\n' + '='.repeat(80));
|
|
84
|
-
console.log('COMPARING TIMING');
|
|
85
|
-
console.log('='.repeat(80));
|
|
86
|
-
|
|
87
|
-
const original = analyzeKarFile(originalPath);
|
|
88
|
-
const converted = analyzeKarFile(convertedPath);
|
|
89
|
-
|
|
90
|
-
if (!original || !converted) {
|
|
91
|
-
console.log('Cannot compare - one of the files has no karaoke track');
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log('\n' + '='.repeat(80));
|
|
96
|
-
console.log('COMPARISON RESULTS');
|
|
97
|
-
console.log('='.repeat(80));
|
|
98
|
-
|
|
99
|
-
console.log(`\nTicks per beat:`);
|
|
100
|
-
console.log(` Original: ${original.ticksPerBeat}`);
|
|
101
|
-
console.log(` Converted: ${converted.ticksPerBeat}`);
|
|
102
|
-
|
|
103
|
-
console.log(`\nAverage delta time:`);
|
|
104
|
-
console.log(` Original: ${original.avgDelta.toFixed(2)}`);
|
|
105
|
-
console.log(` Converted: ${converted.avgDelta.toFixed(2)}`);
|
|
106
|
-
console.log(` Ratio: ${(converted.avgDelta / original.avgDelta).toFixed(4)}x`);
|
|
107
|
-
|
|
108
|
-
// Compare first 20 events side by side
|
|
109
|
-
console.log(`\nFirst 20 events comparison:`);
|
|
110
|
-
console.log(`${'Idx'.padEnd(4)} | ${'Original Delta'.padEnd(14)} | ${'Converted Delta'.padEnd(15)} | ${'Ratio'.padEnd(8)} | Text`);
|
|
111
|
-
console.log('-'.repeat(100));
|
|
112
|
-
|
|
113
|
-
const minLength = Math.min(original.timings.length, converted.timings.length, 20);
|
|
114
|
-
for (let i = 0; i < minLength; i++) {
|
|
115
|
-
const origDelta = original.timings[i].deltaTime;
|
|
116
|
-
const convDelta = converted.timings[i].deltaTime;
|
|
117
|
-
const ratio = origDelta > 0 ? (convDelta / origDelta).toFixed(4) : 'N/A';
|
|
118
|
-
|
|
119
|
-
console.log(
|
|
120
|
-
`${(i+1).toString().padEnd(4)} | ${origDelta.toString().padEnd(14)} | ${convDelta.toString().padEnd(15)} | ${ratio.toString().padEnd(8)} | "${original.timings[i].text}"`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Main
|
|
126
|
-
const originalKar = './songs/fix/song.kar';
|
|
127
|
-
const emkFile = './songs/fix/song.emk';
|
|
128
|
-
|
|
129
|
-
console.log('Step 1: Analyzing original KAR file...');
|
|
130
|
-
analyzeKarFile(originalKar);
|
|
131
|
-
|
|
132
|
-
console.log('\n\nStep 2: Converting EMK to KAR...');
|
|
133
|
-
const { convertEmkToKar } = require('./dist/index.js');
|
|
134
|
-
|
|
135
|
-
const tempKar = './songs/fix/converted-test.kar';
|
|
136
|
-
try {
|
|
137
|
-
const result = convertEmkToKar({
|
|
138
|
-
inputEmk: emkFile,
|
|
139
|
-
outputKar: tempKar,
|
|
140
|
-
keepIntermediateFiles: false
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
console.log(`Conversion ${result.success ? 'SUCCESS' : 'FAILED'}`);
|
|
144
|
-
if (result.warnings.length > 0) {
|
|
145
|
-
console.log('Warnings:', result.warnings);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
console.log('\n\nStep 3: Comparing original vs converted...');
|
|
149
|
-
compareKarFiles(originalKar, tempKar);
|
|
150
|
-
|
|
151
|
-
// Cleanup
|
|
152
|
-
if (fs.existsSync(tempKar)) {
|
|
153
|
-
fs.unlinkSync(tempKar);
|
|
154
|
-
console.log('\nCleaned up temporary file.');
|
|
155
|
-
}
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error('Error during conversion:', error.message);
|
|
158
|
-
console.error(error.stack);
|
|
159
|
-
}
|
package/find-midi-in-block.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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/fix-tempo.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { parseMidi, writeMidi } = require('midi-file');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Copy tempo events from source MIDI to target MIDI
|
|
6
|
-
*/
|
|
7
|
-
function copyTempoEvents(sourceMidiPath, targetMidiPath, outputPath) {
|
|
8
|
-
console.log('Reading source MIDI (original KAR)...');
|
|
9
|
-
const sourceBuffer = fs.readFileSync(sourceMidiPath);
|
|
10
|
-
const sourceMidi = parseMidi(sourceBuffer);
|
|
11
|
-
|
|
12
|
-
console.log('Reading target MIDI (converted from EMK)...');
|
|
13
|
-
const targetBuffer = fs.readFileSync(targetMidiPath);
|
|
14
|
-
const targetMidi = parseMidi(targetBuffer);
|
|
15
|
-
|
|
16
|
-
// Extract all tempo events from source
|
|
17
|
-
const tempoEvents = [];
|
|
18
|
-
sourceMidi.tracks.forEach((track, trackIdx) => {
|
|
19
|
-
let absoluteTime = 0;
|
|
20
|
-
track.forEach((event, eventIdx) => {
|
|
21
|
-
absoluteTime += event.deltaTime;
|
|
22
|
-
if (event.type === 'setTempo') {
|
|
23
|
-
const bpm = 60000000 / event.microsecondsPerBeat;
|
|
24
|
-
tempoEvents.push({
|
|
25
|
-
trackIdx,
|
|
26
|
-
eventIdx,
|
|
27
|
-
absoluteTime,
|
|
28
|
-
deltaTime: event.deltaTime,
|
|
29
|
-
microsecondsPerBeat: event.microsecondsPerBeat,
|
|
30
|
-
bpm: bpm.toFixed(2)
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
console.log(`\nFound ${tempoEvents.length} tempo event(s) in source:`);
|
|
37
|
-
tempoEvents.forEach((t, i) => {
|
|
38
|
-
console.log(` ${i+1}. Track ${t.trackIdx}, Event ${t.eventIdx}, BPM ${t.bpm}, absolute time: ${t.absoluteTime}`);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Find and replace tempo events in target
|
|
42
|
-
let replaced = 0;
|
|
43
|
-
targetMidi.tracks.forEach((track, trackIdx) => {
|
|
44
|
-
for (let i = 0; i < track.length; i++) {
|
|
45
|
-
const event = track[i];
|
|
46
|
-
if (event.type === 'setTempo') {
|
|
47
|
-
const oldBpm = (60000000 / event.microsecondsPerBeat).toFixed(2);
|
|
48
|
-
|
|
49
|
-
// Replace with first tempo from source
|
|
50
|
-
if (tempoEvents.length > 0) {
|
|
51
|
-
const newTempo = tempoEvents[0];
|
|
52
|
-
event.microsecondsPerBeat = newTempo.microsecondsPerBeat;
|
|
53
|
-
|
|
54
|
-
console.log(`\nReplaced tempo in target Track ${trackIdx}, Event ${i}:`);
|
|
55
|
-
console.log(` Old: ${oldBpm} BPM`);
|
|
56
|
-
console.log(` New: ${newTempo.bpm} BPM`);
|
|
57
|
-
replaced++;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (replaced === 0) {
|
|
64
|
-
console.log('\n⚠️ No tempo events found in target to replace');
|
|
65
|
-
console.log('Adding tempo event to first track...');
|
|
66
|
-
|
|
67
|
-
if (tempoEvents.length > 0 && targetMidi.tracks.length > 0) {
|
|
68
|
-
const newTempo = tempoEvents[0];
|
|
69
|
-
|
|
70
|
-
// Insert tempo event at the beginning of first track (after deltaTime 0 events)
|
|
71
|
-
const firstTrack = targetMidi.tracks[0];
|
|
72
|
-
const tempoEvent = {
|
|
73
|
-
type: 'setTempo',
|
|
74
|
-
deltaTime: 0,
|
|
75
|
-
microsecondsPerBeat: newTempo.microsecondsPerBeat
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Insert after any existing deltaTime 0 events
|
|
79
|
-
let insertIndex = 0;
|
|
80
|
-
while (insertIndex < firstTrack.length && firstTrack[insertIndex].deltaTime === 0) {
|
|
81
|
-
insertIndex++;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
firstTrack.splice(insertIndex, 0, tempoEvent);
|
|
85
|
-
console.log(`Added tempo event: ${newTempo.bpm} BPM to Track 0`);
|
|
86
|
-
replaced++;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Write output
|
|
91
|
-
console.log(`\nWriting fixed MIDI to: ${outputPath}`);
|
|
92
|
-
const outputBytes = writeMidi(targetMidi);
|
|
93
|
-
fs.writeFileSync(outputPath, Buffer.from(outputBytes));
|
|
94
|
-
|
|
95
|
-
console.log(`✓ Fixed tempo in ${replaced} location(s)`);
|
|
96
|
-
|
|
97
|
-
return { replaced, tempoEvents };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Test with song.emk -> song.kar
|
|
101
|
-
console.log('='.repeat(80));
|
|
102
|
-
console.log('TEMPO FIX TEST');
|
|
103
|
-
console.log('='.repeat(80));
|
|
104
|
-
console.log('');
|
|
105
|
-
|
|
106
|
-
// First convert EMK to KAR
|
|
107
|
-
const { convertEmkToKar } = require('./dist/index.js');
|
|
108
|
-
const tempKar = './songs/fix/converted-before-fix.kar';
|
|
109
|
-
|
|
110
|
-
console.log('Step 1: Converting EMK to KAR...\n');
|
|
111
|
-
convertEmkToKar({
|
|
112
|
-
inputEmk: './songs/fix/song.emk',
|
|
113
|
-
outputKar: tempKar,
|
|
114
|
-
keepIntermediateFiles: false
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
console.log('\nStep 2: Copying tempo from original KAR...\n');
|
|
118
|
-
const fixedKar = './songs/fix/converted-fixed-tempo.kar';
|
|
119
|
-
copyTempoEvents(
|
|
120
|
-
'./songs/fix/song.kar', // source (original)
|
|
121
|
-
tempKar, // target (converted)
|
|
122
|
-
fixedKar // output
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// Verify
|
|
126
|
-
console.log('\n' + '='.repeat(80));
|
|
127
|
-
console.log('VERIFICATION');
|
|
128
|
-
console.log('='.repeat(80));
|
|
129
|
-
|
|
130
|
-
const verifyOriginal = parseMidi(fs.readFileSync('./songs/fix/song.kar'));
|
|
131
|
-
const verifyFixed = parseMidi(fs.readFileSync(fixedKar));
|
|
132
|
-
|
|
133
|
-
console.log('\nOriginal song.kar tempo:');
|
|
134
|
-
verifyOriginal.tracks.forEach((track, idx) => {
|
|
135
|
-
track.forEach(event => {
|
|
136
|
-
if (event.type === 'setTempo') {
|
|
137
|
-
const bpm = (60000000 / event.microsecondsPerBeat).toFixed(2);
|
|
138
|
-
console.log(` Track ${idx}: ${bpm} BPM`);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
console.log('\nFixed converted KAR tempo:');
|
|
144
|
-
verifyFixed.tracks.forEach((track, idx) => {
|
|
145
|
-
track.forEach(event => {
|
|
146
|
-
if (event.type === 'setTempo') {
|
|
147
|
-
const bpm = (60000000 / event.microsecondsPerBeat).toFixed(2);
|
|
148
|
-
console.log(` Track ${idx}: ${bpm} BPM`);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Cleanup temp file
|
|
154
|
-
fs.unlinkSync(tempKar);
|
|
155
|
-
console.log('\n✓ Test complete! Fixed file saved as: ' + fixedKar);
|
package/kar-diagnostic.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* KAR File Diagnostic Tool
|
|
5
|
-
*
|
|
6
|
-
* This tool checks if a KAR file is valid and provides detailed diagnostics
|
|
7
|
-
* Usage: node kar-diagnostic.js <path-to-kar-file>
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
|
|
13
|
-
function checkKarFile(filePath) {
|
|
14
|
-
console.log('='.repeat(60));
|
|
15
|
-
console.log('KAR File Diagnostic Tool');
|
|
16
|
-
console.log('='.repeat(60));
|
|
17
|
-
console.log('File:', filePath);
|
|
18
|
-
console.log('');
|
|
19
|
-
|
|
20
|
-
// Check if file exists
|
|
21
|
-
if (!fs.existsSync(filePath)) {
|
|
22
|
-
console.error('❌ File not found:', filePath);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Read file
|
|
27
|
-
const data = fs.readFileSync(filePath);
|
|
28
|
-
console.log('✅ File exists');
|
|
29
|
-
console.log(' Size:', data.length, 'bytes');
|
|
30
|
-
console.log('');
|
|
31
|
-
|
|
32
|
-
// Check MThd header
|
|
33
|
-
console.log('--- MIDI Header Check ---');
|
|
34
|
-
const expectedHeader = Buffer.from([0x4D, 0x54, 0x68, 0x64]); // MThd
|
|
35
|
-
const actualHeader = data.subarray(0, 4);
|
|
36
|
-
|
|
37
|
-
console.log('Expected: 4D 54 68 64 (MThd)');
|
|
38
|
-
console.log('Actual: ', actualHeader.toString('hex').match(/.{2}/g).join(' '));
|
|
39
|
-
|
|
40
|
-
if (Buffer.compare(expectedHeader, actualHeader) === 0) {
|
|
41
|
-
console.log('✅ Valid MThd header');
|
|
42
|
-
} else {
|
|
43
|
-
console.log('❌ Invalid header!');
|
|
44
|
-
console.log(' First 20 bytes:', data.subarray(0, 20).toString('hex'));
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
console.log('');
|
|
48
|
-
|
|
49
|
-
// Check MIDI structure
|
|
50
|
-
console.log('--- MIDI Structure ---');
|
|
51
|
-
const headerLength = data.readUInt32BE(4);
|
|
52
|
-
const format = data.readUInt16BE(8);
|
|
53
|
-
const numTracks = data.readUInt16BE(10);
|
|
54
|
-
const division = data.readUInt16BE(12);
|
|
55
|
-
|
|
56
|
-
console.log('Header length:', headerLength, '(expected: 6)');
|
|
57
|
-
console.log('Format:', format, '(expected: 0 or 1)');
|
|
58
|
-
console.log('Number of tracks:', numTracks);
|
|
59
|
-
console.log('Division:', division);
|
|
60
|
-
|
|
61
|
-
if (headerLength !== 6) {
|
|
62
|
-
console.log('⚠️ Unusual header length');
|
|
63
|
-
}
|
|
64
|
-
if (format > 2) {
|
|
65
|
-
console.log('⚠️ Invalid format');
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
if (numTracks === 0) {
|
|
69
|
-
console.log('❌ No tracks found');
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
console.log('✅ Valid structure');
|
|
73
|
-
console.log('');
|
|
74
|
-
|
|
75
|
-
// Check first track
|
|
76
|
-
console.log('--- First Track Check ---');
|
|
77
|
-
const firstTrackPos = 14;
|
|
78
|
-
const firstTrackHeader = data.subarray(firstTrackPos, firstTrackPos + 4).toString('ascii');
|
|
79
|
-
const firstTrackLength = data.readUInt32BE(firstTrackPos + 4);
|
|
80
|
-
|
|
81
|
-
console.log('Track header:', firstTrackHeader, '(expected: MTrk)');
|
|
82
|
-
console.log('Track length:', firstTrackLength);
|
|
83
|
-
|
|
84
|
-
if (firstTrackHeader !== 'MTrk') {
|
|
85
|
-
console.log('❌ Invalid track header');
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
console.log('✅ Valid first track');
|
|
89
|
-
console.log('');
|
|
90
|
-
|
|
91
|
-
// Try parsing with midi-file
|
|
92
|
-
console.log('--- Library Compatibility Test ---');
|
|
93
|
-
try {
|
|
94
|
-
const { parseMidi } = require('midi-file');
|
|
95
|
-
const parsed = parseMidi(data);
|
|
96
|
-
console.log('✅ midi-file library can parse');
|
|
97
|
-
console.log(' Parsed tracks:', parsed.tracks.length);
|
|
98
|
-
console.log(' Format:', parsed.header.format);
|
|
99
|
-
console.log(' Division:', parsed.header.ticksPerBeat || parsed.header.framesPerSecond);
|
|
100
|
-
} catch (e) {
|
|
101
|
-
console.log('❌ midi-file library error:', e.message);
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
console.log('');
|
|
105
|
-
|
|
106
|
-
// Check for karaoke content
|
|
107
|
-
console.log('--- Karaoke Content Check ---');
|
|
108
|
-
const fileContent = data.toString('binary');
|
|
109
|
-
const hasKaraoke = fileContent.includes('@T');
|
|
110
|
-
const hasWords = fileContent.includes('Words');
|
|
111
|
-
|
|
112
|
-
console.log('Contains @T markers:', hasKaraoke ? '✅' : '❌');
|
|
113
|
-
console.log('Contains "Words" track:', hasWords ? '✅' : '❌');
|
|
114
|
-
console.log('');
|
|
115
|
-
|
|
116
|
-
// Summary
|
|
117
|
-
console.log('='.repeat(60));
|
|
118
|
-
console.log('SUMMARY: File is VALID ✅');
|
|
119
|
-
console.log('='.repeat(60));
|
|
120
|
-
console.log('');
|
|
121
|
-
console.log('If you still get errors when using this file:');
|
|
122
|
-
console.log('1. Check file transfer method (use binary mode)');
|
|
123
|
-
console.log('2. Verify file size matches:', data.length, 'bytes');
|
|
124
|
-
console.log('3. Check first 4 bytes: should be 4D 54 68 64');
|
|
125
|
-
console.log('4. Try a different MIDI player/library');
|
|
126
|
-
console.log('5. Check if server is re-encoding the file');
|
|
127
|
-
console.log('');
|
|
128
|
-
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Main
|
|
133
|
-
const args = process.argv.slice(2);
|
|
134
|
-
if (args.length === 0) {
|
|
135
|
-
console.error('Usage: node kar-diagnostic.js <path-to-kar-file>');
|
|
136
|
-
console.error('');
|
|
137
|
-
console.error('Example:');
|
|
138
|
-
console.error(' node kar-diagnostic.js output.kar');
|
|
139
|
-
console.error(' node kar-diagnostic.js /path/to/song.kar');
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const filePath = args[0];
|
|
144
|
-
try {
|
|
145
|
-
checkKarFile(filePath);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
console.error('');
|
|
148
|
-
console.error('❌ Error:', error.message);
|
|
149
|
-
console.error('');
|
|
150
|
-
console.error('Stack trace:');
|
|
151
|
-
console.error(error.stack);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|