@karaplay/file-coder 1.4.7 → 1.4.9
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/RELEASE_v1.4.8.md +115 -0
- package/RELEASE_v1.4.9.md +144 -0
- package/dist/emk/client-decoder.d.ts +1 -0
- package/dist/emk/client-decoder.js +1 -0
- package/dist/emk/server-decode.d.ts +1 -0
- package/dist/emk/server-decode.js +1 -0
- package/dist/emk-to-kar.browser.js +7 -1
- package/dist/emk-to-kar.js +7 -1
- package/dist/ncntokar.browser.d.ts +4 -2
- package/dist/ncntokar.browser.js +13 -6
- package/dist/ncntokar.d.ts +4 -2
- package/dist/ncntokar.js +13 -7
- package/package.json +2 -1
- package/songs/fix/001_kar_convert_needtofix_tempo_fast.kar +0 -0
- package/songs/fix/001_original_emk.emk +0 -0
- 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/fix-tempo.js +0 -155
- package/kar-diagnostic.js +0 -158
- package/test-final.js +0 -175
package/debug-midi-source.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { parseMidi } = require('midi-file');
|
|
3
|
-
const { decodeEmkServer } = require('./dist/index.js');
|
|
4
|
-
|
|
5
|
-
// Check MIDI tempo from EMK
|
|
6
|
-
console.log('Step 1: Decode EMK and check MIDI tempo...\n');
|
|
7
|
-
|
|
8
|
-
const emkBuffer = fs.readFileSync('./songs/fix/song.emk');
|
|
9
|
-
const decoded = decodeEmkServer(emkBuffer);
|
|
10
|
-
|
|
11
|
-
console.log('EMK decoded successfully');
|
|
12
|
-
console.log('Has MIDI:', !!decoded.midi);
|
|
13
|
-
console.log('Has Lyric:', !!decoded.lyric);
|
|
14
|
-
console.log('Has Cursor:', !!decoded.cursor);
|
|
15
|
-
|
|
16
|
-
if (decoded.midi) {
|
|
17
|
-
// Write the decoded MIDI to temp file
|
|
18
|
-
fs.writeFileSync('./songs/fix/decoded-midi-temp.mid', decoded.midi);
|
|
19
|
-
|
|
20
|
-
// Parse the decoded MIDI
|
|
21
|
-
const midi = parseMidi(decoded.midi);
|
|
22
|
-
|
|
23
|
-
console.log('\nDecoded MIDI file:');
|
|
24
|
-
console.log(' Format:', midi.header.format);
|
|
25
|
-
console.log(' Tracks:', midi.tracks.length);
|
|
26
|
-
console.log(' Ticks per beat:', midi.header.ticksPerBeat);
|
|
27
|
-
|
|
28
|
-
// Find tempo events
|
|
29
|
-
const tempoEvents = [];
|
|
30
|
-
|
|
31
|
-
midi.tracks.forEach((track, trackIdx) => {
|
|
32
|
-
let absoluteTime = 0;
|
|
33
|
-
|
|
34
|
-
track.forEach((event) => {
|
|
35
|
-
absoluteTime += event.deltaTime;
|
|
36
|
-
|
|
37
|
-
if (event.type === 'setTempo') {
|
|
38
|
-
const bpm = 60000000 / event.microsecondsPerBeat;
|
|
39
|
-
tempoEvents.push({
|
|
40
|
-
trackIdx: trackIdx,
|
|
41
|
-
absoluteTime: absoluteTime,
|
|
42
|
-
microsecondsPerBeat: event.microsecondsPerBeat,
|
|
43
|
-
bpm: bpm.toFixed(2)
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (tempoEvents.length === 0) {
|
|
50
|
-
console.log('\n⚠️ NO TEMPO EVENTS in decoded MIDI!');
|
|
51
|
-
} else {
|
|
52
|
-
console.log(`\n Tempo events: ${tempoEvents.length}`);
|
|
53
|
-
tempoEvents.forEach((t, i) => {
|
|
54
|
-
console.log(` ${i+1}. Track ${t.trackIdx}, BPM: ${t.bpm}, µs/beat: ${t.microsecondsPerBeat}`);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Cleanup
|
|
59
|
-
fs.unlinkSync('./songs/fix/decoded-midi-temp.mid');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log('\n' + '='.repeat(80));
|
|
63
|
-
console.log('Step 2: Check ncntokar conversion process...\n');
|
|
64
|
-
|
|
65
|
-
// Check if ncntokar.ts adds any tempo
|
|
66
|
-
const ncntokarCode = fs.readFileSync('./src/ncntokar.ts', 'utf8');
|
|
67
|
-
|
|
68
|
-
if (ncntokarCode.includes('setTempo')) {
|
|
69
|
-
console.log('⚠️ ncntokar.ts contains "setTempo" - it might be modifying tempo!');
|
|
70
|
-
|
|
71
|
-
// Find the lines
|
|
72
|
-
const lines = ncntokarCode.split('\n');
|
|
73
|
-
lines.forEach((line, idx) => {
|
|
74
|
-
if (line.includes('setTempo')) {
|
|
75
|
-
console.log(` Line ${idx+1}: ${line.trim()}`);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
} else {
|
|
79
|
-
console.log('✓ ncntokar.ts does not modify tempo');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log('\n' + '='.repeat(80));
|
|
83
|
-
console.log('Step 3: Check original song.kar tempo location...\n');
|
|
84
|
-
|
|
85
|
-
const originalBuffer = fs.readFileSync('./songs/fix/song.kar');
|
|
86
|
-
const originalMidi = parseMidi(originalBuffer);
|
|
87
|
-
|
|
88
|
-
console.log('Original song.kar:');
|
|
89
|
-
console.log(' Tracks:', originalMidi.tracks.length);
|
|
90
|
-
|
|
91
|
-
// Check which tracks have tempo
|
|
92
|
-
originalMidi.tracks.forEach((track, trackIdx) => {
|
|
93
|
-
const tempoCount = track.filter(e => e.type === 'setTempo').length;
|
|
94
|
-
if (tempoCount > 0) {
|
|
95
|
-
console.log(` Track ${trackIdx} has ${tempoCount} tempo event(s)`);
|
|
96
|
-
|
|
97
|
-
track.forEach((event, eventIdx) => {
|
|
98
|
-
if (event.type === 'setTempo') {
|
|
99
|
-
const bpm = 60000000 / event.microsecondsPerBeat;
|
|
100
|
-
console.log(` Event ${eventIdx}: BPM ${bpm.toFixed(2)}, µs/beat: ${event.microsecondsPerBeat}, deltaTime: ${event.deltaTime}`);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
console.log('\n' + '='.repeat(80));
|
|
107
|
-
console.log('ANALYSIS COMPLETE');
|
|
108
|
-
console.log('='.repeat(80));
|
package/debug-tempo.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { parseMidi } = require('midi-file');
|
|
4
|
-
|
|
5
|
-
// Parse and compare tempo events
|
|
6
|
-
function analyzeTempo(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 all tempo events in all tracks
|
|
17
|
-
const tempoEvents = [];
|
|
18
|
-
|
|
19
|
-
midi.tracks.forEach((track, trackIdx) => {
|
|
20
|
-
let absoluteTime = 0;
|
|
21
|
-
|
|
22
|
-
track.forEach((event, eventIdx) => {
|
|
23
|
-
absoluteTime += event.deltaTime;
|
|
24
|
-
|
|
25
|
-
if (event.type === 'setTempo') {
|
|
26
|
-
// microsecondsPerBeat = event.microsecondsPerBeat
|
|
27
|
-
// BPM = 60000000 / microsecondsPerBeat
|
|
28
|
-
const bpm = 60000000 / event.microsecondsPerBeat;
|
|
29
|
-
|
|
30
|
-
tempoEvents.push({
|
|
31
|
-
trackIdx: trackIdx,
|
|
32
|
-
eventIdx: eventIdx,
|
|
33
|
-
absoluteTime: absoluteTime,
|
|
34
|
-
deltaTime: event.deltaTime,
|
|
35
|
-
microsecondsPerBeat: event.microsecondsPerBeat,
|
|
36
|
-
bpm: bpm.toFixed(2)
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (tempoEvents.length === 0) {
|
|
43
|
-
console.log('\n⚠️ NO TEMPO EVENTS FOUND - USING DEFAULT 120 BPM');
|
|
44
|
-
return {
|
|
45
|
-
hasTempoEvents: false,
|
|
46
|
-
defaultBpm: 120,
|
|
47
|
-
tempoEvents: []
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
console.log(`\nTempo events found: ${tempoEvents.length}`);
|
|
52
|
-
tempoEvents.forEach((t, i) => {
|
|
53
|
-
console.log(`${i+1}. Track ${t.trackIdx}, Delta: ${t.deltaTime}, Absolute: ${t.absoluteTime}, BPM: ${t.bpm}, µs/beat: ${t.microsecondsPerBeat}`);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
hasTempoEvents: true,
|
|
58
|
-
tempoEvents: tempoEvents
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Main
|
|
63
|
-
const originalKar = './songs/fix/song.kar';
|
|
64
|
-
const emkFile = './songs/fix/song.emk';
|
|
65
|
-
const tempKar = './songs/fix/converted-test.kar';
|
|
66
|
-
|
|
67
|
-
console.log('Converting EMK to KAR...');
|
|
68
|
-
const { convertEmkToKar } = require('./dist/index.js');
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const result = convertEmkToKar({
|
|
72
|
-
inputEmk: emkFile,
|
|
73
|
-
outputKar: tempKar,
|
|
74
|
-
keepIntermediateFiles: false
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
console.log(`Conversion ${result.success ? 'SUCCESS' : 'FAILED'}`);
|
|
78
|
-
|
|
79
|
-
console.log('\n' + '='.repeat(80));
|
|
80
|
-
console.log('TEMPO COMPARISON');
|
|
81
|
-
console.log('='.repeat(80));
|
|
82
|
-
|
|
83
|
-
const originalTempo = analyzeTempo(originalKar);
|
|
84
|
-
const convertedTempo = analyzeTempo(tempKar);
|
|
85
|
-
|
|
86
|
-
console.log('\n' + '='.repeat(80));
|
|
87
|
-
console.log('COMPARISON RESULTS');
|
|
88
|
-
console.log('='.repeat(80));
|
|
89
|
-
|
|
90
|
-
console.log(`\nOriginal has tempo events: ${originalTempo.hasTempoEvents}`);
|
|
91
|
-
console.log(`Converted has tempo events: ${convertedTempo.hasTempoEvents}`);
|
|
92
|
-
|
|
93
|
-
if (originalTempo.hasTempoEvents && !convertedTempo.hasTempoEvents) {
|
|
94
|
-
console.log('\n🔴 PROBLEM FOUND: Original has tempo events but converted file does NOT!');
|
|
95
|
-
console.log('This will cause the converted file to play at default 120 BPM instead of the intended tempo.');
|
|
96
|
-
console.log('\nOriginal tempo events:');
|
|
97
|
-
originalTempo.tempoEvents.forEach((t, i) => {
|
|
98
|
-
console.log(` ${i+1}. Track ${t.trackIdx}, BPM: ${t.bpm} (${t.microsecondsPerBeat} µs/beat)`);
|
|
99
|
-
});
|
|
100
|
-
} else if (!originalTempo.hasTempoEvents && !convertedTempo.hasTempoEvents) {
|
|
101
|
-
console.log('\n✓ Both files use default tempo (120 BPM)');
|
|
102
|
-
} else if (originalTempo.hasTempoEvents && convertedTempo.hasTempoEvents) {
|
|
103
|
-
console.log('\n✓ Both files have tempo events');
|
|
104
|
-
|
|
105
|
-
// Compare tempos
|
|
106
|
-
if (originalTempo.tempoEvents.length !== convertedTempo.tempoEvents.length) {
|
|
107
|
-
console.log(`\n⚠️ Warning: Different number of tempo events (${originalTempo.tempoEvents.length} vs ${convertedTempo.tempoEvents.length})`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Compare first tempo
|
|
111
|
-
const origFirst = originalTempo.tempoEvents[0];
|
|
112
|
-
const convFirst = convertedTempo.tempoEvents[0];
|
|
113
|
-
|
|
114
|
-
if (origFirst && convFirst) {
|
|
115
|
-
console.log(`\nFirst tempo comparison:`);
|
|
116
|
-
console.log(` Original: ${origFirst.bpm} BPM`);
|
|
117
|
-
console.log(` Converted: ${convFirst.bpm} BPM`);
|
|
118
|
-
|
|
119
|
-
if (origFirst.bpm !== convFirst.bpm) {
|
|
120
|
-
console.log(` 🔴 TEMPO MISMATCH!`);
|
|
121
|
-
} else {
|
|
122
|
-
console.log(` ✓ Tempo matches`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Cleanup
|
|
128
|
-
if (fs.existsSync(tempKar)) {
|
|
129
|
-
fs.unlinkSync(tempKar);
|
|
130
|
-
console.log('\nCleaned up temporary file.');
|
|
131
|
-
}
|
|
132
|
-
} catch (error) {
|
|
133
|
-
console.error('Error:', error.message);
|
|
134
|
-
console.error(error.stack);
|
|
135
|
-
}
|
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/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);
|