@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,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify EMK to KAR Conversions Against Reference Data
|
|
4
|
+
*
|
|
5
|
+
* This script:
|
|
6
|
+
* 1. Reads EMK_REFERENCE_DATA.json
|
|
7
|
+
* 2. Converts each EMK file
|
|
8
|
+
* 3. Compares results with reference data
|
|
9
|
+
* 4. Reports any discrepancies
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node verify-emk-reference.js
|
|
13
|
+
* node verify-emk-reference.js --verbose
|
|
14
|
+
* node verify-emk-reference.js --file=001.emk
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { convertEmkToKar, validateKarTempo, compareEmkKarTempo } = require('./dist/index.js');
|
|
20
|
+
|
|
21
|
+
// Parse arguments
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const verbose = args.includes('--verbose');
|
|
24
|
+
const fileArg = args.find(arg => arg.startsWith('--file='));
|
|
25
|
+
const targetFile = fileArg ? fileArg.split('=')[1] : null;
|
|
26
|
+
|
|
27
|
+
// Load reference data
|
|
28
|
+
const referenceFile = path.join(__dirname, 'EMK_REFERENCE_DATA.json');
|
|
29
|
+
if (!fs.existsSync(referenceFile)) {
|
|
30
|
+
console.error('❌ Reference data not found!');
|
|
31
|
+
console.error(' Run: node analyze-all-emk.js');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const referenceData = JSON.parse(fs.readFileSync(referenceFile, 'utf8'));
|
|
36
|
+
|
|
37
|
+
console.log('='.repeat(80));
|
|
38
|
+
console.log('VERIFY EMK CONVERSIONS AGAINST REFERENCE DATA');
|
|
39
|
+
console.log('='.repeat(80));
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(`Reference Data: ${referenceData.generatedAt}`);
|
|
42
|
+
console.log(`Library Version: ${referenceData.version}`);
|
|
43
|
+
console.log(`Total Songs: ${referenceData.totalFiles}`);
|
|
44
|
+
console.log('');
|
|
45
|
+
|
|
46
|
+
// Filter songs to test
|
|
47
|
+
let songsToTest = referenceData.songs.filter(s => s.status === 'success');
|
|
48
|
+
if (targetFile) {
|
|
49
|
+
songsToTest = songsToTest.filter(s => s.filename === targetFile);
|
|
50
|
+
if (songsToTest.length === 0) {
|
|
51
|
+
console.error(`❌ File not found in reference data: ${targetFile}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`Testing ${songsToTest.length} songs...`);
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
const emkDir = path.join(__dirname, 'songs/emk');
|
|
60
|
+
const tempDir = path.join(__dirname, 'temp');
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(tempDir)) {
|
|
63
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const results = {
|
|
67
|
+
passed: 0,
|
|
68
|
+
failed: 0,
|
|
69
|
+
warnings: 0,
|
|
70
|
+
details: []
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
songsToTest.forEach((refSong, idx) => {
|
|
74
|
+
const testNum = idx + 1;
|
|
75
|
+
const totalTests = songsToTest.length;
|
|
76
|
+
|
|
77
|
+
console.log(`[${testNum}/${totalTests}] Testing: ${refSong.filename}`);
|
|
78
|
+
|
|
79
|
+
if (verbose) {
|
|
80
|
+
console.log(` Reference: ${refSong.title} - ${refSong.artist}`);
|
|
81
|
+
console.log(` Expected Duration: ${refSong.durationFormatted} (${refSong.duration.toFixed(2)}s)`);
|
|
82
|
+
console.log(` Expected Tempo: ${refSong.karTempo.toFixed(2)} BPM`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const inputEmk = path.join(emkDir, refSong.filename);
|
|
87
|
+
const outputKar = path.join(tempDir, refSong.filename.replace('.emk', '_verify.kar'));
|
|
88
|
+
|
|
89
|
+
// Convert
|
|
90
|
+
const result = convertEmkToKar({
|
|
91
|
+
inputEmk: inputEmk,
|
|
92
|
+
outputKar: outputKar,
|
|
93
|
+
keepIntermediateFiles: false
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
console.log(` ❌ FAIL: Conversion failed`);
|
|
98
|
+
results.failed++;
|
|
99
|
+
results.details.push({
|
|
100
|
+
file: refSong.filename,
|
|
101
|
+
status: 'failed',
|
|
102
|
+
reason: 'Conversion failed'
|
|
103
|
+
});
|
|
104
|
+
console.log('');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate
|
|
109
|
+
const emkBuffer = fs.readFileSync(inputEmk);
|
|
110
|
+
const karBuffer = fs.readFileSync(outputKar);
|
|
111
|
+
|
|
112
|
+
const validation = validateKarTempo(karBuffer);
|
|
113
|
+
const comparison = compareEmkKarTempo(emkBuffer, karBuffer);
|
|
114
|
+
|
|
115
|
+
// Compare with reference
|
|
116
|
+
const issues = [];
|
|
117
|
+
|
|
118
|
+
// Check tempo
|
|
119
|
+
const tempoDiff = Math.abs(validation.tempo - refSong.karTempo);
|
|
120
|
+
if (tempoDiff > 0.1) {
|
|
121
|
+
issues.push(`Tempo mismatch: expected ${refSong.karTempo.toFixed(2)}, got ${validation.tempo.toFixed(2)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check duration (±2 seconds tolerance)
|
|
125
|
+
const durationDiff = Math.abs(validation.duration - refSong.duration);
|
|
126
|
+
if (durationDiff > 2) {
|
|
127
|
+
issues.push(`Duration mismatch: expected ${refSong.duration.toFixed(2)}s, got ${validation.duration.toFixed(2)}s (diff: ${durationDiff.toFixed(2)}s)`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check tempo ratio (allow 0.1 tolerance)
|
|
131
|
+
const ratioDiff = Math.abs(comparison.tempoRatio - refSong.expectedRatio);
|
|
132
|
+
if (ratioDiff > 0.1) {
|
|
133
|
+
issues.push(`Tempo ratio mismatch: expected ${refSong.expectedRatio}x, got ${comparison.tempoRatio.toFixed(2)}x`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check format
|
|
137
|
+
if (comparison.format !== refSong.format) {
|
|
138
|
+
issues.push(`Format mismatch: expected ${refSong.format}, got ${comparison.format}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Report results
|
|
142
|
+
if (issues.length === 0) {
|
|
143
|
+
console.log(` ✅ PASS: All checks passed`);
|
|
144
|
+
if (verbose) {
|
|
145
|
+
console.log(` Duration: ${validation.duration.toFixed(2)}s (expected ${refSong.duration.toFixed(2)}s)`);
|
|
146
|
+
console.log(` Tempo: ${validation.tempo.toFixed(2)} BPM (expected ${refSong.karTempo.toFixed(2)} BPM)`);
|
|
147
|
+
console.log(` Ratio: ${comparison.tempoRatio.toFixed(2)}x (expected ${refSong.expectedRatio}x)`);
|
|
148
|
+
}
|
|
149
|
+
results.passed++;
|
|
150
|
+
results.details.push({
|
|
151
|
+
file: refSong.filename,
|
|
152
|
+
status: 'passed'
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
// Check if they're warnings or failures
|
|
156
|
+
const hasFailure = issues.some(i => i.includes('mismatch') && !i.includes('Duration'));
|
|
157
|
+
|
|
158
|
+
if (hasFailure) {
|
|
159
|
+
console.log(` ❌ FAIL: ${issues.length} issue(s) found`);
|
|
160
|
+
results.failed++;
|
|
161
|
+
results.details.push({
|
|
162
|
+
file: refSong.filename,
|
|
163
|
+
status: 'failed',
|
|
164
|
+
issues: issues
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
console.log(` ⚠️ WARNING: ${issues.length} minor issue(s)`);
|
|
168
|
+
results.warnings++;
|
|
169
|
+
results.details.push({
|
|
170
|
+
file: refSong.filename,
|
|
171
|
+
status: 'warning',
|
|
172
|
+
issues: issues
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (verbose || hasFailure) {
|
|
177
|
+
issues.forEach(issue => console.log(` - ${issue}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Clean up
|
|
182
|
+
if (fs.existsSync(outputKar)) {
|
|
183
|
+
fs.unlinkSync(outputKar);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.log(` ❌ ERROR: ${error.message}`);
|
|
188
|
+
results.failed++;
|
|
189
|
+
results.details.push({
|
|
190
|
+
file: refSong.filename,
|
|
191
|
+
status: 'error',
|
|
192
|
+
error: error.message
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Print summary
|
|
200
|
+
console.log('='.repeat(80));
|
|
201
|
+
console.log('SUMMARY');
|
|
202
|
+
console.log('='.repeat(80));
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(`✅ Passed: ${results.passed}/${songsToTest.length}`);
|
|
205
|
+
console.log(`⚠️ Warnings: ${results.warnings}/${songsToTest.length}`);
|
|
206
|
+
console.log(`❌ Failed: ${results.failed}/${songsToTest.length}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
|
|
209
|
+
// Exit code
|
|
210
|
+
const exitCode = results.failed > 0 ? 1 : 0;
|
|
211
|
+
|
|
212
|
+
if (exitCode === 0) {
|
|
213
|
+
console.log('🎉 All tests passed!');
|
|
214
|
+
} else {
|
|
215
|
+
console.log('❌ Some tests failed. Review the output above.');
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log('Failed files:');
|
|
218
|
+
results.details.filter(d => d.status === 'failed' || d.status === 'error').forEach(d => {
|
|
219
|
+
console.log(` - ${d.file}`);
|
|
220
|
+
if (d.issues) {
|
|
221
|
+
d.issues.forEach(issue => console.log(` ${issue}`));
|
|
222
|
+
}
|
|
223
|
+
if (d.error) {
|
|
224
|
+
console.log(` Error: ${d.error}`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log('');
|
|
230
|
+
process.exit(exitCode);
|
package/analyze-emk-cursor.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { Midi } = require('@tonejs/midi');
|
|
3
|
-
const { decodeEmkServer } = require('./dist/index.js');
|
|
4
|
-
|
|
5
|
-
console.log('='.repeat(80));
|
|
6
|
-
console.log('ANALYZING EMK CURSOR DATA FOR CORRECT TEMPO');
|
|
7
|
-
console.log('='.repeat(80));
|
|
8
|
-
console.log('');
|
|
9
|
-
|
|
10
|
-
// Decode EMK
|
|
11
|
-
const emkBuffer = fs.readFileSync('./songs/fix/001_original_emk.emk');
|
|
12
|
-
const decoded = decodeEmkServer(emkBuffer);
|
|
13
|
-
|
|
14
|
-
console.log('📁 EMK File Info:');
|
|
15
|
-
console.log(' Format:', decoded.isZxioFormat ? 'ZXIO' : 'MThd');
|
|
16
|
-
console.log(' Has MIDI:', decoded.midi ? 'Yes' : 'No');
|
|
17
|
-
console.log(' Has Lyric:', decoded.lyric ? 'Yes' : 'No');
|
|
18
|
-
console.log(' Has Cursor:', decoded.cursor ? 'Yes' : 'No');
|
|
19
|
-
console.log('');
|
|
20
|
-
|
|
21
|
-
// Parse MIDI
|
|
22
|
-
const midi = new Midi(decoded.midi);
|
|
23
|
-
console.log('📊 MIDI Info:');
|
|
24
|
-
console.log(' PPQ:', midi.header.ppq);
|
|
25
|
-
console.log(' Tempo:', midi.header.tempos[0].bpm.toFixed(2), 'BPM');
|
|
26
|
-
console.log(' Duration:', midi.duration.toFixed(2), 'seconds');
|
|
27
|
-
console.log(' Total Ticks:', midi.header.tempos[0].ticks);
|
|
28
|
-
console.log('');
|
|
29
|
-
|
|
30
|
-
// Find last note and last tempo event
|
|
31
|
-
let maxTicks = 0;
|
|
32
|
-
midi.tracks.forEach(track => {
|
|
33
|
-
track.notes.forEach(note => {
|
|
34
|
-
const noteTicks = note.ticks + (note.durationTicks || 0);
|
|
35
|
-
if (noteTicks > maxTicks) maxTicks = noteTicks;
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Find last tempo/event
|
|
40
|
-
let lastEventTicks = 0;
|
|
41
|
-
midi.tracks.forEach(track => {
|
|
42
|
-
let currentTicks = 0;
|
|
43
|
-
track.forEach(event => {
|
|
44
|
-
currentTicks += event.deltaTime;
|
|
45
|
-
if (currentTicks > lastEventTicks) lastEventTicks = currentTicks;
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
console.log('🎵 MIDI Ticks Info:');
|
|
50
|
-
console.log(' Last Note Ticks:', maxTicks);
|
|
51
|
-
console.log(' Last Event Ticks:', lastEventTicks);
|
|
52
|
-
console.log('');
|
|
53
|
-
|
|
54
|
-
// Analyze cursor data
|
|
55
|
-
const cursorBuffer = decoded.cursor;
|
|
56
|
-
console.log('⏱️ Cursor Data:');
|
|
57
|
-
console.log(' Buffer size:', cursorBuffer.length, 'bytes');
|
|
58
|
-
console.log(' Cursor values:', cursorBuffer.length / 2, 'values (2 bytes each)');
|
|
59
|
-
console.log('');
|
|
60
|
-
|
|
61
|
-
// Read cursor values
|
|
62
|
-
const cursorValues = [];
|
|
63
|
-
for (let i = 0; i < cursorBuffer.length; i += 2) {
|
|
64
|
-
cursorValues.push(cursorBuffer.readUInt16LE(i));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Find min, max, average
|
|
68
|
-
const maxCursor = Math.max(...cursorValues);
|
|
69
|
-
const minCursor = Math.min(...cursorValues.filter(v => v > 0));
|
|
70
|
-
const avgCursor = cursorValues.reduce((a, b) => a + b, 0) / cursorValues.length;
|
|
71
|
-
|
|
72
|
-
console.log(' Min cursor (non-zero):', minCursor);
|
|
73
|
-
console.log(' Max cursor:', maxCursor);
|
|
74
|
-
console.log(' Average cursor:', avgCursor.toFixed(2));
|
|
75
|
-
console.log('');
|
|
76
|
-
|
|
77
|
-
// Cursor represents ticks in the final KAR
|
|
78
|
-
// If cursor max = X and we multiply by ratio Y, final ticks = X * Y
|
|
79
|
-
// Final duration = (X * Y) / (PPQ * final_BPM / 60)
|
|
80
|
-
|
|
81
|
-
console.log('🔍 Cursor to Ticks Analysis:');
|
|
82
|
-
console.log('');
|
|
83
|
-
|
|
84
|
-
// Test different cursor multipliers
|
|
85
|
-
const ppq = midi.header.ppq;
|
|
86
|
-
const targetDuration = 285; // 4:45 in seconds
|
|
87
|
-
|
|
88
|
-
console.log(' Target duration: 4:45 (285 seconds)');
|
|
89
|
-
console.log('');
|
|
90
|
-
|
|
91
|
-
// Cursor multiply ratios to test
|
|
92
|
-
const ratios = [1, 2, 3, 4, 5, 6];
|
|
93
|
-
|
|
94
|
-
console.log(' Testing cursor multiply ratios:');
|
|
95
|
-
console.log('');
|
|
96
|
-
|
|
97
|
-
ratios.forEach(cursorRatio => {
|
|
98
|
-
const finalTicks = maxCursor * cursorRatio;
|
|
99
|
-
|
|
100
|
-
// For each cursor ratio, calculate what tempo would give us 4:45
|
|
101
|
-
const requiredTempo = (finalTicks / targetDuration) * (60 / ppq);
|
|
102
|
-
|
|
103
|
-
// Calculate tempo ratio from original EMK tempo
|
|
104
|
-
const emkTempo = midi.header.tempos[0].bpm;
|
|
105
|
-
const tempoRatio = requiredTempo / emkTempo;
|
|
106
|
-
|
|
107
|
-
console.log(` Cursor ×${cursorRatio}:`);
|
|
108
|
-
console.log(` Final ticks: ${finalTicks}`);
|
|
109
|
-
console.log(` Required tempo: ${requiredTempo.toFixed(2)} BPM`);
|
|
110
|
-
console.log(` Tempo ratio: ${tempoRatio.toFixed(2)}x (from ${emkTempo} BPM)`);
|
|
111
|
-
|
|
112
|
-
// Check if this makes sense
|
|
113
|
-
if (Math.abs(tempoRatio - 1.23) < 0.1) {
|
|
114
|
-
console.log(` ✅ MATCHES! This gives us 4:45 duration`);
|
|
115
|
-
}
|
|
116
|
-
console.log('');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Now let's check: what if we DON'T multiply cursor?
|
|
120
|
-
console.log('📐 Analysis: Cursor as Direct Ticks (no multiply)');
|
|
121
|
-
console.log('');
|
|
122
|
-
|
|
123
|
-
const cursorAsTicks = maxCursor;
|
|
124
|
-
const tempoFor285 = (cursorAsTicks / targetDuration) * (60 / ppq);
|
|
125
|
-
const ratioFor285 = tempoFor285 / midi.header.tempos[0].bpm;
|
|
126
|
-
|
|
127
|
-
console.log(' Max cursor value:', cursorAsTicks);
|
|
128
|
-
console.log(' If cursor = ticks directly:');
|
|
129
|
-
console.log(' Required tempo:', tempoFor285.toFixed(2), 'BPM');
|
|
130
|
-
console.log(' Tempo ratio:', ratioFor285.toFixed(2) + 'x');
|
|
131
|
-
console.log('');
|
|
132
|
-
|
|
133
|
-
// Check with cursor × 4
|
|
134
|
-
const cursor4x = maxCursor * 4;
|
|
135
|
-
const tempoFor285_4x = (cursor4x / targetDuration) * (60 / ppq);
|
|
136
|
-
const ratioFor285_4x = tempoFor285_4x / midi.header.tempos[0].bpm;
|
|
137
|
-
|
|
138
|
-
console.log(' If cursor × 4:');
|
|
139
|
-
console.log(' Final ticks:', cursor4x);
|
|
140
|
-
console.log(' Required tempo:', tempoFor285_4x.toFixed(2), 'BPM');
|
|
141
|
-
console.log(' Tempo ratio:', ratioFor285_4x.toFixed(2) + 'x');
|
|
142
|
-
console.log('');
|
|
143
|
-
|
|
144
|
-
console.log('🎯 CONCLUSION:');
|
|
145
|
-
console.log('');
|
|
146
|
-
console.log(' For song duration 4:45 (285 seconds):');
|
|
147
|
-
console.log('');
|
|
148
|
-
|
|
149
|
-
if (ratioFor285_4x >= 1 && ratioFor285_4x <= 2) {
|
|
150
|
-
console.log(' ✅ Cursor should be multiplied by 4');
|
|
151
|
-
console.log(' ✅ Tempo ratio should be:', ratioFor285_4x.toFixed(2) + 'x');
|
|
152
|
-
console.log(' ✅ Final tempo:', tempoFor285_4x.toFixed(2), 'BPM');
|
|
153
|
-
console.log('');
|
|
154
|
-
console.log(' Current (wrong):');
|
|
155
|
-
console.log(' Tempo ratio: 2.78x (ZXIO formula)');
|
|
156
|
-
console.log(' Final tempo: 178.09 BPM');
|
|
157
|
-
console.log(' Duration: 2:11 ❌');
|
|
158
|
-
console.log('');
|
|
159
|
-
console.log(' Should be:');
|
|
160
|
-
console.log(' Tempo ratio:', ratioFor285_4x.toFixed(2) + 'x');
|
|
161
|
-
console.log(' Final tempo:', tempoFor285_4x.toFixed(2), 'BPM');
|
|
162
|
-
console.log(' Duration: 4:45 ✅');
|
|
163
|
-
} else if (ratioFor285 >= 1 && ratioFor285 <= 2) {
|
|
164
|
-
console.log(' ✅ Cursor should NOT be multiplied (use raw values)');
|
|
165
|
-
console.log(' ✅ Tempo ratio should be:', ratioFor285.toFixed(2) + 'x');
|
|
166
|
-
console.log(' ✅ Final tempo:', tempoFor285.toFixed(2), 'BPM');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
console.log('');
|
package/analyze-emk-simple.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { Midi } = require('@tonejs/midi');
|
|
3
|
-
const { decodeEmkServer } = require('./dist/index.js');
|
|
4
|
-
const { parseMidi } = require('midi-file');
|
|
5
|
-
|
|
6
|
-
console.log('='.repeat(80));
|
|
7
|
-
console.log('ANALYZING EMK FOR CORRECT TEMPO - Simple Version');
|
|
8
|
-
console.log('='.repeat(80));
|
|
9
|
-
console.log('');
|
|
10
|
-
|
|
11
|
-
// Decode EMK
|
|
12
|
-
const emkBuffer = fs.readFileSync('./songs/fix/001_original_emk.emk');
|
|
13
|
-
const decoded = decodeEmkServer(emkBuffer);
|
|
14
|
-
|
|
15
|
-
console.log('📁 EMK File Info:');
|
|
16
|
-
console.log(' Format:', decoded.isZxioFormat ? 'ZXIO' : 'MThd');
|
|
17
|
-
console.log('');
|
|
18
|
-
|
|
19
|
-
// Parse with midi-file to get raw ticks
|
|
20
|
-
const rawMidi = parseMidi(decoded.midi);
|
|
21
|
-
console.log('📊 Raw MIDI Info:');
|
|
22
|
-
console.log(' PPQ:', rawMidi.header.ticksPerBeat);
|
|
23
|
-
console.log('');
|
|
24
|
-
|
|
25
|
-
// Find max ticks
|
|
26
|
-
let maxTicks = 0;
|
|
27
|
-
rawMidi.tracks.forEach((track, idx) => {
|
|
28
|
-
let currentTicks = 0;
|
|
29
|
-
track.forEach(event => {
|
|
30
|
-
currentTicks += event.deltaTime;
|
|
31
|
-
if (currentTicks > maxTicks) maxTicks = currentTicks;
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
console.log(' Max MIDI ticks:', maxTicks);
|
|
36
|
-
console.log('');
|
|
37
|
-
|
|
38
|
-
// Get tempo
|
|
39
|
-
let tempo = null;
|
|
40
|
-
rawMidi.tracks.forEach(track => {
|
|
41
|
-
track.forEach(event => {
|
|
42
|
-
if (event.type === 'setTempo' && !tempo) {
|
|
43
|
-
tempo = (60000000 / event.microsecondsPerBeat).toFixed(2);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
console.log(' Current tempo:', tempo, 'BPM');
|
|
49
|
-
console.log('');
|
|
50
|
-
|
|
51
|
-
// Parse cursor
|
|
52
|
-
const cursorBuffer = decoded.cursor;
|
|
53
|
-
const cursorValues = [];
|
|
54
|
-
for (let i = 0; i < cursorBuffer.length; i += 2) {
|
|
55
|
-
cursorValues.push(cursorBuffer.readUInt16LE(i));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const maxCursor = Math.max(...cursorValues);
|
|
59
|
-
console.log('⏱️ Cursor Data:');
|
|
60
|
-
console.log(' Max cursor value:', maxCursor);
|
|
61
|
-
console.log('');
|
|
62
|
-
|
|
63
|
-
// Target duration
|
|
64
|
-
const targetDuration = 285; // 4 minutes 45 seconds
|
|
65
|
-
const ppq = rawMidi.header.ticksPerBeat;
|
|
66
|
-
|
|
67
|
-
console.log('🎯 Target: 4:45 (285 seconds)');
|
|
68
|
-
console.log('');
|
|
69
|
-
|
|
70
|
-
// Calculate what we need
|
|
71
|
-
console.log('📐 Calculations:');
|
|
72
|
-
console.log('');
|
|
73
|
-
|
|
74
|
-
// Scenario 1: Don't multiply cursor (use raw)
|
|
75
|
-
console.log(' [1] If cursor = ticks (no multiply):');
|
|
76
|
-
const finalTicks1 = maxCursor;
|
|
77
|
-
const reqTempo1 = (finalTicks1 / targetDuration) * (60 / ppq);
|
|
78
|
-
const ratio1 = reqTempo1 / parseFloat(tempo);
|
|
79
|
-
console.log(' Final ticks:', finalTicks1);
|
|
80
|
-
console.log(' Required tempo:', reqTempo1.toFixed(2), 'BPM');
|
|
81
|
-
console.log(' Tempo ratio:', ratio1.toFixed(2) + 'x');
|
|
82
|
-
const dur1 = (finalTicks1 / (ppq * reqTempo1 / 60));
|
|
83
|
-
console.log(' Duration:', dur1.toFixed(2), 'seconds =', (dur1/60).toFixed(2), 'minutes');
|
|
84
|
-
console.log('');
|
|
85
|
-
|
|
86
|
-
// Scenario 2: Multiply cursor by 4
|
|
87
|
-
console.log(' [2] If cursor × 4:');
|
|
88
|
-
const finalTicks2 = maxCursor * 4;
|
|
89
|
-
const reqTempo2 = (finalTicks2 / targetDuration) * (60 / ppq);
|
|
90
|
-
const ratio2 = reqTempo2 / parseFloat(tempo);
|
|
91
|
-
console.log(' Final ticks:', finalTicks2);
|
|
92
|
-
console.log(' Required tempo:', reqTempo2.toFixed(2), 'BPM');
|
|
93
|
-
console.log(' Tempo ratio:', ratio2.toFixed(2) + 'x');
|
|
94
|
-
const dur2 = (finalTicks2 / (ppq * reqTempo2 / 60));
|
|
95
|
-
console.log(' Duration:', dur2.toFixed(2), 'seconds =', (dur2/60).toFixed(2), 'minutes');
|
|
96
|
-
console.log('');
|
|
97
|
-
|
|
98
|
-
// Current implementation (ZXIO)
|
|
99
|
-
console.log(' [3] Current (ZXIO 2.78x, cursor × 4):');
|
|
100
|
-
const currentTempo = parseFloat(tempo) * 2.78;
|
|
101
|
-
const currentTicks = maxCursor * 4;
|
|
102
|
-
const currentDur = (currentTicks / (ppq * currentTempo / 60));
|
|
103
|
-
console.log(' Tempo:', currentTempo.toFixed(2), 'BPM');
|
|
104
|
-
console.log(' Ticks:', currentTicks);
|
|
105
|
-
console.log(' Duration:', currentDur.toFixed(2), 'seconds =', (currentDur/60).toFixed(2), 'minutes ❌');
|
|
106
|
-
console.log('');
|
|
107
|
-
|
|
108
|
-
// Recommendation
|
|
109
|
-
console.log('✅ RECOMMENDATION:');
|
|
110
|
-
console.log('');
|
|
111
|
-
|
|
112
|
-
if (Math.abs(ratio2 - 1.23) < 0.3 && Math.abs(dur2 - 285) < 5) {
|
|
113
|
-
console.log(' Use: Cursor × 4, Tempo ratio', ratio2.toFixed(2) + 'x');
|
|
114
|
-
console.log(' Final tempo:', reqTempo2.toFixed(2), 'BPM');
|
|
115
|
-
console.log(' Duration: 4:45 ✅');
|
|
116
|
-
console.log('');
|
|
117
|
-
console.log(' ZXIO should use ratio:', ratio2.toFixed(2) + 'x (NOT 2.78x)');
|
|
118
|
-
} else if (Math.abs(ratio1 - 1.23) < 0.3 && Math.abs(dur1 - 285) < 5) {
|
|
119
|
-
console.log(' Use: No cursor multiply, Tempo ratio', ratio1.toFixed(2) + 'x');
|
|
120
|
-
console.log(' Final tempo:', reqTempo1.toFixed(2), 'BPM');
|
|
121
|
-
console.log(' Duration: 4:45 ✅');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log('');
|
package/check-real-duration.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { Midi } = require('@tonejs/midi');
|
|
3
|
-
|
|
4
|
-
console.log('='.repeat(80));
|
|
5
|
-
console.log('CHECKING REAL DURATION - 001.emk');
|
|
6
|
-
console.log('='.repeat(80));
|
|
7
|
-
console.log('');
|
|
8
|
-
|
|
9
|
-
// Check original KAR (the reference file)
|
|
10
|
-
const originalKarPath = './songs/fix/001_kar_convert_needtofix_tempo_fast.kar';
|
|
11
|
-
console.log('📁 Original KAR (Reference):');
|
|
12
|
-
console.log(' File:', originalKarPath);
|
|
13
|
-
console.log(' Note: Filename says "needtofix_tempo_fast" - might be WRONG!');
|
|
14
|
-
console.log('');
|
|
15
|
-
|
|
16
|
-
const karBuffer = fs.readFileSync(originalKarPath);
|
|
17
|
-
const karMidi = new Midi(karBuffer);
|
|
18
|
-
|
|
19
|
-
console.log(' Tempo:', karMidi.header.tempos[0].bpm.toFixed(2), 'BPM');
|
|
20
|
-
console.log(' Duration:', karMidi.duration.toFixed(2), 'seconds');
|
|
21
|
-
console.log(' Duration:', (karMidi.duration / 60).toFixed(2), 'minutes');
|
|
22
|
-
console.log('');
|
|
23
|
-
|
|
24
|
-
// Check EMK MIDI
|
|
25
|
-
const { decodeEmkServer } = require('./dist/index.js');
|
|
26
|
-
const emkBuffer = fs.readFileSync('./songs/fix/001_original_emk.emk');
|
|
27
|
-
const decoded = decodeEmkServer(emkBuffer);
|
|
28
|
-
const emkMidi = new Midi(decoded.midi);
|
|
29
|
-
|
|
30
|
-
console.log('📁 EMK MIDI (Original):');
|
|
31
|
-
console.log(' Tempo:', emkMidi.header.tempos[0].bpm.toFixed(2), 'BPM');
|
|
32
|
-
console.log(' Duration:', emkMidi.duration.toFixed(2), 'seconds');
|
|
33
|
-
console.log(' Duration:', (emkMidi.duration / 60).toFixed(2), 'minutes');
|
|
34
|
-
console.log('');
|
|
35
|
-
|
|
36
|
-
// Calculate what tempo should be for 4:45 (285 seconds)
|
|
37
|
-
const targetDuration = 285; // 4 minutes 45 seconds
|
|
38
|
-
const emkDuration = emkMidi.duration;
|
|
39
|
-
const emkTempo = emkMidi.header.tempos[0].bpm;
|
|
40
|
-
|
|
41
|
-
const requiredRatio = emkDuration / targetDuration;
|
|
42
|
-
const requiredTempo = emkTempo * requiredRatio;
|
|
43
|
-
|
|
44
|
-
console.log('🎯 For REAL song duration (4:45):');
|
|
45
|
-
console.log(' Target Duration:', targetDuration, 'seconds (4:45)');
|
|
46
|
-
console.log(' Required Ratio:', requiredRatio.toFixed(2) + 'x');
|
|
47
|
-
console.log(' Required Tempo:', requiredTempo.toFixed(2), 'BPM');
|
|
48
|
-
console.log('');
|
|
49
|
-
|
|
50
|
-
console.log('📊 COMPARISON:');
|
|
51
|
-
console.log('');
|
|
52
|
-
console.log(' Current implementation:');
|
|
53
|
-
console.log(' Ratio: 2.78x (ZXIO formula)');
|
|
54
|
-
console.log(' Tempo: 178.09 BPM');
|
|
55
|
-
console.log(' Duration: 126 seconds (2:06)');
|
|
56
|
-
console.log(' Status: ❌ TOO FAST! (Should be 4:45)');
|
|
57
|
-
console.log('');
|
|
58
|
-
console.log(' Correct implementation:');
|
|
59
|
-
console.log(' Ratio:', requiredRatio.toFixed(2) + 'x');
|
|
60
|
-
console.log(' Tempo:', requiredTempo.toFixed(2), 'BPM');
|
|
61
|
-
console.log(' Duration: 285 seconds (4:45)');
|
|
62
|
-
console.log(' Status: ✅ CORRECT!');
|
|
63
|
-
console.log('');
|
|
64
|
-
|
|
65
|
-
console.log('⚠️ CONCLUSION:');
|
|
66
|
-
console.log(' The reference KAR file (001_kar_convert_needtofix_tempo_fast.kar)');
|
|
67
|
-
console.log(' is WRONG! It has tempo that is TOO FAST (178 BPM).');
|
|
68
|
-
console.log(' The real song should be 4:45, not 2:11!');
|
|
69
|
-
console.log('');
|
package/temp/test_output.kar
DELETED
|
Binary file
|