@karaplay/file-coder 1.4.9 → 1.5.1
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 +241 -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/README.md +90 -0
- package/RELEASE_v1.5.0.md +91 -0
- package/RELEASE_v1.5.1.md +190 -0
- package/WHY_DURATION_DECREASES.md +176 -0
- package/analyze-cursor-pattern.js +131 -0
- package/analyze-tempo-duration.js +243 -0
- package/calculate-correct-ratio.js +97 -0
- package/compare-kar-lyrics-timing.js +110 -0
- package/debug-duration.js +124 -0
- package/demo-client.html +722 -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 +261 -0
- package/demo-simple.html +992 -0
- 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 +3 -3
- package/dist/ncntokar.js +3 -3
- package/find-zxio-tempo-ratio.js +118 -0
- package/manual-test-validator.js +79 -0
- package/match-cursor-to-lyrics.js +118 -0
- package/package.json +5 -2
- package/songs/emk/001_original_emk.emk +0 -0
- package/test-all-emk-final.js +97 -0
- package/test-demo-player.sh +63 -0
- package/test-duration-fix.sh +53 -0
package/demo-client.html
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="th">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>EMK to KAR Conversion Demo - @karaplay/file-coder v1.4.9</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 1400px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.header {
|
|
27
|
+
background: white;
|
|
28
|
+
border-radius: 12px;
|
|
29
|
+
padding: 30px;
|
|
30
|
+
margin-bottom: 20px;
|
|
31
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header h1 {
|
|
35
|
+
color: #667eea;
|
|
36
|
+
margin-bottom: 10px;
|
|
37
|
+
font-size: 32px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.header p {
|
|
41
|
+
color: #666;
|
|
42
|
+
margin-bottom: 5px;
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.version {
|
|
47
|
+
display: inline-block;
|
|
48
|
+
background: #667eea;
|
|
49
|
+
color: white;
|
|
50
|
+
padding: 5px 15px;
|
|
51
|
+
border-radius: 20px;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
margin-left: 10px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.badge {
|
|
58
|
+
display: inline-block;
|
|
59
|
+
padding: 4px 10px;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
margin-left: 8px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.badge-zxio {
|
|
67
|
+
background: #28a745;
|
|
68
|
+
color: white;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.badge-mthd {
|
|
72
|
+
background: #17a2b8;
|
|
73
|
+
color: white;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.main-content {
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: 350px 1fr;
|
|
79
|
+
gap: 20px;
|
|
80
|
+
margin-bottom: 20px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (max-width: 1024px) {
|
|
84
|
+
.main-content {
|
|
85
|
+
grid-template-columns: 1fr;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.card {
|
|
90
|
+
background: white;
|
|
91
|
+
border-radius: 12px;
|
|
92
|
+
padding: 25px;
|
|
93
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.card h2 {
|
|
97
|
+
color: #667eea;
|
|
98
|
+
margin-bottom: 15px;
|
|
99
|
+
font-size: 20px;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 10px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.file-list {
|
|
106
|
+
max-height: 500px;
|
|
107
|
+
overflow-y: auto;
|
|
108
|
+
border: 2px solid #e0e0e0;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
padding: 10px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.file-item {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
padding: 15px;
|
|
118
|
+
margin-bottom: 8px;
|
|
119
|
+
background: #f8f9fa;
|
|
120
|
+
border-radius: 8px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
transition: all 0.3s;
|
|
123
|
+
border: 2px solid transparent;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.file-item:hover {
|
|
127
|
+
background: #e9ecef;
|
|
128
|
+
transform: translateX(5px);
|
|
129
|
+
border-color: #667eea;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.file-item.active {
|
|
133
|
+
background: #667eea;
|
|
134
|
+
color: white;
|
|
135
|
+
border-color: #5568d3;
|
|
136
|
+
transform: translateX(5px);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.file-item.active .file-badge {
|
|
140
|
+
background: white;
|
|
141
|
+
color: #667eea;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.file-name {
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.file-size {
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
color: #999;
|
|
152
|
+
margin-top: 4px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.file-item.active .file-size {
|
|
156
|
+
color: rgba(255,255,255,0.8);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.file-badge {
|
|
160
|
+
background: #667eea;
|
|
161
|
+
color: white;
|
|
162
|
+
padding: 4px 10px;
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
font-size: 11px;
|
|
165
|
+
font-weight: bold;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.btn {
|
|
169
|
+
background: #667eea;
|
|
170
|
+
color: white;
|
|
171
|
+
border: none;
|
|
172
|
+
padding: 12px 24px;
|
|
173
|
+
border-radius: 8px;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
font-size: 15px;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
transition: all 0.3s;
|
|
178
|
+
margin-right: 10px;
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 8px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.btn:hover:not(:disabled) {
|
|
185
|
+
background: #5568d3;
|
|
186
|
+
transform: translateY(-2px);
|
|
187
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.btn:disabled {
|
|
191
|
+
background: #ccc;
|
|
192
|
+
cursor: not-allowed;
|
|
193
|
+
transform: none;
|
|
194
|
+
opacity: 0.6;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.btn-success {
|
|
198
|
+
background: #28a745;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.btn-success:hover:not(:disabled) {
|
|
202
|
+
background: #218838;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.btn-danger {
|
|
206
|
+
background: #dc3545;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.btn-danger:hover:not(:disabled) {
|
|
210
|
+
background: #c82333;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.btn-lg {
|
|
214
|
+
padding: 15px 30px;
|
|
215
|
+
font-size: 16px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.player-container {
|
|
219
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
220
|
+
border-radius: 12px;
|
|
221
|
+
padding: 30px;
|
|
222
|
+
color: white;
|
|
223
|
+
min-height: 250px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.lyrics-display {
|
|
227
|
+
text-align: center;
|
|
228
|
+
font-size: 48px;
|
|
229
|
+
font-weight: bold;
|
|
230
|
+
line-height: 1.4;
|
|
231
|
+
min-height: 150px;
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
justify-content: center;
|
|
235
|
+
color: white;
|
|
236
|
+
text-shadow: 2px 2px 8px rgba(0,0,0,0.3);
|
|
237
|
+
margin-bottom: 20px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.lyrics-highlight {
|
|
241
|
+
color: #ffd700;
|
|
242
|
+
animation: pulse 0.5s ease-in-out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@keyframes pulse {
|
|
246
|
+
0%, 100% { transform: scale(1); }
|
|
247
|
+
50% { transform: scale(1.05); }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.controls {
|
|
251
|
+
display: flex;
|
|
252
|
+
gap: 10px;
|
|
253
|
+
align-items: center;
|
|
254
|
+
margin-top: 20px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.progress-container {
|
|
258
|
+
flex: 1;
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
gap: 5px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.progress-bar {
|
|
265
|
+
height: 10px;
|
|
266
|
+
background: rgba(255,255,255,0.3);
|
|
267
|
+
border-radius: 5px;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.progress-fill {
|
|
273
|
+
height: 100%;
|
|
274
|
+
background: white;
|
|
275
|
+
width: 0%;
|
|
276
|
+
transition: width 0.1s linear;
|
|
277
|
+
box-shadow: 0 0 10px rgba(255,255,255,0.5);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.time-display {
|
|
281
|
+
font-size: 14px;
|
|
282
|
+
color: rgba(255,255,255,0.9);
|
|
283
|
+
text-align: right;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.info-panel {
|
|
287
|
+
display: grid;
|
|
288
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
289
|
+
gap: 15px;
|
|
290
|
+
margin-top: 20px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.info-card {
|
|
294
|
+
background: #f8f9fa;
|
|
295
|
+
padding: 15px;
|
|
296
|
+
border-radius: 8px;
|
|
297
|
+
border-left: 4px solid #667eea;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.info-label {
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
color: #666;
|
|
304
|
+
text-transform: uppercase;
|
|
305
|
+
margin-bottom: 5px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.info-value {
|
|
309
|
+
font-size: 18px;
|
|
310
|
+
font-weight: bold;
|
|
311
|
+
color: #333;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.status {
|
|
315
|
+
padding: 15px 20px;
|
|
316
|
+
border-radius: 8px;
|
|
317
|
+
margin-top: 15px;
|
|
318
|
+
font-size: 14px;
|
|
319
|
+
line-height: 1.6;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.status.success {
|
|
323
|
+
background: #d4edda;
|
|
324
|
+
color: #155724;
|
|
325
|
+
border: 1px solid #c3e6cb;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.status.error {
|
|
329
|
+
background: #f8d7da;
|
|
330
|
+
color: #721c24;
|
|
331
|
+
border: 1px solid #f5c6cb;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.status.info {
|
|
335
|
+
background: #d1ecf1;
|
|
336
|
+
color: #0c5460;
|
|
337
|
+
border: 1px solid #bee5eb;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.loading {
|
|
341
|
+
display: inline-block;
|
|
342
|
+
width: 18px;
|
|
343
|
+
height: 18px;
|
|
344
|
+
border: 3px solid rgba(255,255,255,0.3);
|
|
345
|
+
border-top: 3px solid white;
|
|
346
|
+
border-radius: 50%;
|
|
347
|
+
animation: spin 1s linear infinite;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@keyframes spin {
|
|
351
|
+
0% { transform: rotate(0deg); }
|
|
352
|
+
100% { transform: rotate(360deg); }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.full-width-card {
|
|
356
|
+
grid-column: 1 / -1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.conversion-log {
|
|
360
|
+
background: #f8f9fa;
|
|
361
|
+
padding: 15px;
|
|
362
|
+
border-radius: 8px;
|
|
363
|
+
font-family: 'Courier New', monospace;
|
|
364
|
+
font-size: 13px;
|
|
365
|
+
max-height: 200px;
|
|
366
|
+
overflow-y: auto;
|
|
367
|
+
color: #333;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.log-item {
|
|
371
|
+
padding: 5px 0;
|
|
372
|
+
border-bottom: 1px solid #e0e0e0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.log-item:last-child {
|
|
376
|
+
border-bottom: none;
|
|
377
|
+
}
|
|
378
|
+
</style>
|
|
379
|
+
</head>
|
|
380
|
+
<body>
|
|
381
|
+
<div class="container">
|
|
382
|
+
<div class="header">
|
|
383
|
+
<h1>🎤 EMK to KAR Conversion Demo</h1>
|
|
384
|
+
<p>
|
|
385
|
+
Testing <strong>@karaplay/file-coder</strong>
|
|
386
|
+
<span class="version">v1.4.9</span>
|
|
387
|
+
<span class="badge badge-zxio">ZXIO Support</span>
|
|
388
|
+
<span class="badge badge-mthd">MThd Support</span>
|
|
389
|
+
</p>
|
|
390
|
+
<p style="margin-top: 10px;">
|
|
391
|
+
✨ ZXIO Format Tempo Fix (2.78x) | MThd Format (4x) | Verify tempo and timing accuracy
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div class="main-content">
|
|
396
|
+
<!-- Left Panel: EMK Files -->
|
|
397
|
+
<div class="card">
|
|
398
|
+
<h2>📁 EMK Files</h2>
|
|
399
|
+
<div class="file-list" id="fileList">
|
|
400
|
+
<div class="status info">
|
|
401
|
+
<div class="loading"></div>
|
|
402
|
+
Loading EMK files...
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<!-- Right Panel: Player & Controls -->
|
|
408
|
+
<div class="card">
|
|
409
|
+
<h2>🎵 Karaoke Player</h2>
|
|
410
|
+
|
|
411
|
+
<div class="player-container">
|
|
412
|
+
<div class="lyrics-display" id="lyricsDisplay">
|
|
413
|
+
Select an EMK file to begin
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div class="controls">
|
|
417
|
+
<button class="btn btn-lg" id="convertBtn" disabled>
|
|
418
|
+
🔄 Convert to KAR
|
|
419
|
+
</button>
|
|
420
|
+
<button class="btn btn-success btn-lg" id="playBtn" disabled>
|
|
421
|
+
▶ Play
|
|
422
|
+
</button>
|
|
423
|
+
<button class="btn btn-lg" id="pauseBtn" disabled>
|
|
424
|
+
⏸ Pause
|
|
425
|
+
</button>
|
|
426
|
+
<button class="btn btn-danger btn-lg" id="stopBtn" disabled>
|
|
427
|
+
⏹ Stop
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="progress-container">
|
|
432
|
+
<div class="progress-bar" id="progressBar">
|
|
433
|
+
<div class="progress-fill" id="progressFill"></div>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="time-display" id="timeDisplay">0:00 / 0:00</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div class="info-panel" id="infoPanel" style="display: none;">
|
|
440
|
+
<div class="info-card">
|
|
441
|
+
<div class="info-label">Title</div>
|
|
442
|
+
<div class="info-value" id="infoTitle">-</div>
|
|
443
|
+
</div>
|
|
444
|
+
<div class="info-card">
|
|
445
|
+
<div class="info-label">Artist</div>
|
|
446
|
+
<div class="info-value" id="infoArtist">-</div>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="info-card">
|
|
449
|
+
<div class="info-label">Format</div>
|
|
450
|
+
<div class="info-value" id="infoFormat">-</div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="info-card">
|
|
453
|
+
<div class="info-label">Tempo</div>
|
|
454
|
+
<div class="info-value" id="infoTempo">-</div>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="info-card">
|
|
457
|
+
<div class="info-label">Duration</div>
|
|
458
|
+
<div class="info-value" id="infoDuration">-</div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div id="statusContainer"></div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<script type="module">
|
|
468
|
+
import { Player } from 'https://cdn.jsdelivr.net/npm/karaoke-player@latest/dist/index.min.js';
|
|
469
|
+
|
|
470
|
+
let emkFiles = [];
|
|
471
|
+
let selectedFile = null;
|
|
472
|
+
let convertedKarData = null;
|
|
473
|
+
let player = null;
|
|
474
|
+
let isPlaying = false;
|
|
475
|
+
|
|
476
|
+
// Load EMK files list
|
|
477
|
+
async function loadFileList() {
|
|
478
|
+
try {
|
|
479
|
+
const response = await fetch('/api/emk-files');
|
|
480
|
+
const data = await response.json();
|
|
481
|
+
|
|
482
|
+
if (data.success) {
|
|
483
|
+
emkFiles = data.files;
|
|
484
|
+
renderFileList();
|
|
485
|
+
} else {
|
|
486
|
+
showStatus('error', 'Failed to load EMK files: ' + data.error);
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
showStatus('error', 'Network error: ' + error.message);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Render file list
|
|
494
|
+
function renderFileList() {
|
|
495
|
+
const fileList = document.getElementById('fileList');
|
|
496
|
+
|
|
497
|
+
if (emkFiles.length === 0) {
|
|
498
|
+
fileList.innerHTML = '<div class="status info">No EMK files found</div>';
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fileList.innerHTML = emkFiles.map((file, index) => {
|
|
503
|
+
const formatBadge = file.name.startsWith('Z') || file.name.startsWith('f') ? 'MThd' : 'ZXIO';
|
|
504
|
+
return `
|
|
505
|
+
<div class="file-item" data-index="${index}">
|
|
506
|
+
<div>
|
|
507
|
+
<div class="file-name">${file.name}</div>
|
|
508
|
+
<div class="file-size">${(file.size / 1024).toFixed(2)} KB</div>
|
|
509
|
+
</div>
|
|
510
|
+
<span class="file-badge">${formatBadge}</span>
|
|
511
|
+
</div>
|
|
512
|
+
`;
|
|
513
|
+
}).join('');
|
|
514
|
+
|
|
515
|
+
// Add click handlers
|
|
516
|
+
document.querySelectorAll('.file-item').forEach(item => {
|
|
517
|
+
item.addEventListener('click', () => selectFile(parseInt(item.dataset.index)));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Select file
|
|
522
|
+
function selectFile(index) {
|
|
523
|
+
document.querySelectorAll('.file-item').forEach(item => {
|
|
524
|
+
item.classList.remove('active');
|
|
525
|
+
});
|
|
526
|
+
document.querySelector(`[data-index="${index}"]`).classList.add('active');
|
|
527
|
+
|
|
528
|
+
selectedFile = emkFiles[index];
|
|
529
|
+
convertedKarData = null;
|
|
530
|
+
|
|
531
|
+
document.getElementById('convertBtn').disabled = false;
|
|
532
|
+
document.getElementById('playBtn').disabled = true;
|
|
533
|
+
document.getElementById('pauseBtn').disabled = true;
|
|
534
|
+
document.getElementById('stopBtn').disabled = true;
|
|
535
|
+
|
|
536
|
+
document.getElementById('infoPanel').style.display = 'none';
|
|
537
|
+
document.getElementById('lyricsDisplay').textContent = `Selected: ${selectedFile.name}`;
|
|
538
|
+
|
|
539
|
+
showStatus('info', `Selected: <strong>${selectedFile.name}</strong><br>Click "Convert to KAR" to proceed`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Convert EMK to KAR
|
|
543
|
+
async function convertFile() {
|
|
544
|
+
if (!selectedFile) return;
|
|
545
|
+
|
|
546
|
+
const convertBtn = document.getElementById('convertBtn');
|
|
547
|
+
const playBtn = document.getElementById('playBtn');
|
|
548
|
+
|
|
549
|
+
convertBtn.disabled = true;
|
|
550
|
+
convertBtn.innerHTML = '<div class="loading"></div> Converting...';
|
|
551
|
+
|
|
552
|
+
showStatus('info', `<div class="loading"></div> Converting ${selectedFile.name}...`);
|
|
553
|
+
document.getElementById('lyricsDisplay').innerHTML = '<div class="loading"></div><br>Converting...';
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const response = await fetch('/api/convert', {
|
|
557
|
+
method: 'POST',
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: JSON.stringify({ filename: selectedFile.name })
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const data = await response.json();
|
|
563
|
+
|
|
564
|
+
if (data.success) {
|
|
565
|
+
convertedKarData = data.karData;
|
|
566
|
+
const meta = data.metadata;
|
|
567
|
+
|
|
568
|
+
// Show success status
|
|
569
|
+
showStatus('success', `
|
|
570
|
+
✅ <strong>Conversion Successful!</strong><br>
|
|
571
|
+
<strong>File:</strong> ${selectedFile.name}<br>
|
|
572
|
+
<strong>Title:</strong> ${meta.title}<br>
|
|
573
|
+
<strong>Artist:</strong> ${meta.artist}<br>
|
|
574
|
+
<strong>Format:</strong> ${meta.format}<br>
|
|
575
|
+
<strong>Tempo:</strong> ${meta.tempo} BPM<br>
|
|
576
|
+
<strong>Duration:</strong> ${meta.durationMinutes} minutes<br>
|
|
577
|
+
${meta.warnings.length > 0 ? `<strong>Warnings:</strong> ${meta.warnings.length}` : ''}
|
|
578
|
+
`);
|
|
579
|
+
|
|
580
|
+
// Update info panel
|
|
581
|
+
document.getElementById('infoPanel').style.display = 'grid';
|
|
582
|
+
document.getElementById('infoTitle').textContent = meta.title;
|
|
583
|
+
document.getElementById('infoArtist').textContent = meta.artist;
|
|
584
|
+
document.getElementById('infoFormat').textContent = meta.format;
|
|
585
|
+
document.getElementById('infoTempo').textContent = meta.tempo + ' BPM';
|
|
586
|
+
document.getElementById('infoDuration').textContent = meta.durationMinutes + ' min';
|
|
587
|
+
|
|
588
|
+
document.getElementById('lyricsDisplay').textContent = 'Ready to play!';
|
|
589
|
+
playBtn.disabled = false;
|
|
590
|
+
|
|
591
|
+
} else {
|
|
592
|
+
showStatus('error', `❌ Conversion failed!<br>${data.error}`);
|
|
593
|
+
document.getElementById('lyricsDisplay').textContent = 'Conversion failed';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error('Conversion error:', error);
|
|
598
|
+
showStatus('error', `❌ Network error!<br>${error.message}`);
|
|
599
|
+
document.getElementById('lyricsDisplay').textContent = 'Error';
|
|
600
|
+
} finally {
|
|
601
|
+
convertBtn.innerHTML = '🔄 Convert to KAR';
|
|
602
|
+
convertBtn.disabled = false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Play KAR file
|
|
607
|
+
async function playFile() {
|
|
608
|
+
if (!convertedKarData) return;
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
// Decode base64 to array buffer
|
|
612
|
+
const binaryString = atob(convertedKarData);
|
|
613
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
614
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
615
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Initialize player if needed
|
|
619
|
+
if (!player) {
|
|
620
|
+
player = new Player();
|
|
621
|
+
|
|
622
|
+
// Setup event listeners
|
|
623
|
+
player.on('lyric', (event) => {
|
|
624
|
+
const displayText = event.text || '';
|
|
625
|
+
document.getElementById('lyricsDisplay').innerHTML =
|
|
626
|
+
`<span class="lyrics-highlight">${displayText}</span>`;
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
player.on('timeupdate', (event) => {
|
|
630
|
+
const current = formatTime(event.currentTime);
|
|
631
|
+
const total = formatTime(event.duration);
|
|
632
|
+
document.getElementById('timeDisplay').textContent = `${current} / ${total}`;
|
|
633
|
+
|
|
634
|
+
const progress = (event.currentTime / event.duration) * 100;
|
|
635
|
+
document.getElementById('progressFill').style.width = `${progress}%`;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
player.on('ended', () => {
|
|
639
|
+
isPlaying = false;
|
|
640
|
+
document.getElementById('playBtn').disabled = false;
|
|
641
|
+
document.getElementById('pauseBtn').disabled = true;
|
|
642
|
+
document.getElementById('stopBtn').disabled = true;
|
|
643
|
+
document.getElementById('pauseBtn').textContent = '⏸ Pause';
|
|
644
|
+
document.getElementById('lyricsDisplay').textContent = 'Finished';
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Load and play
|
|
649
|
+
await player.load(bytes.buffer);
|
|
650
|
+
await player.play();
|
|
651
|
+
|
|
652
|
+
isPlaying = true;
|
|
653
|
+
document.getElementById('playBtn').disabled = true;
|
|
654
|
+
document.getElementById('pauseBtn').disabled = false;
|
|
655
|
+
document.getElementById('stopBtn').disabled = false;
|
|
656
|
+
|
|
657
|
+
showStatus('success', '▶ Playing...');
|
|
658
|
+
|
|
659
|
+
} catch (error) {
|
|
660
|
+
console.error('Playback error:', error);
|
|
661
|
+
showStatus('error', `❌ Playback error!<br>${error.message}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Pause/Resume
|
|
666
|
+
function togglePause() {
|
|
667
|
+
if (!player) return;
|
|
668
|
+
|
|
669
|
+
if (isPlaying) {
|
|
670
|
+
player.pause();
|
|
671
|
+
document.getElementById('pauseBtn').textContent = '▶ Resume';
|
|
672
|
+
showStatus('info', '⏸ Paused');
|
|
673
|
+
} else {
|
|
674
|
+
player.play();
|
|
675
|
+
document.getElementById('pauseBtn').textContent = '⏸ Pause';
|
|
676
|
+
showStatus('success', '▶ Resumed');
|
|
677
|
+
}
|
|
678
|
+
isPlaying = !isPlaying;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Stop
|
|
682
|
+
function stopPlayback() {
|
|
683
|
+
if (!player) return;
|
|
684
|
+
|
|
685
|
+
player.stop();
|
|
686
|
+
isPlaying = false;
|
|
687
|
+
|
|
688
|
+
document.getElementById('playBtn').disabled = false;
|
|
689
|
+
document.getElementById('pauseBtn').disabled = true;
|
|
690
|
+
document.getElementById('stopBtn').disabled = true;
|
|
691
|
+
document.getElementById('pauseBtn').textContent = '⏸ Pause';
|
|
692
|
+
document.getElementById('lyricsDisplay').textContent = 'Stopped';
|
|
693
|
+
document.getElementById('progressFill').style.width = '0%';
|
|
694
|
+
document.getElementById('timeDisplay').textContent = '0:00 / 0:00';
|
|
695
|
+
|
|
696
|
+
showStatus('info', '⏹ Stopped');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Show status message
|
|
700
|
+
function showStatus(type, message) {
|
|
701
|
+
const container = document.getElementById('statusContainer');
|
|
702
|
+
container.innerHTML = `<div class="status ${type}">${message}</div>`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Format time helper
|
|
706
|
+
function formatTime(seconds) {
|
|
707
|
+
const mins = Math.floor(seconds / 60);
|
|
708
|
+
const secs = Math.floor(seconds % 60);
|
|
709
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Event listeners
|
|
713
|
+
document.getElementById('convertBtn').addEventListener('click', convertFile);
|
|
714
|
+
document.getElementById('playBtn').addEventListener('click', playFile);
|
|
715
|
+
document.getElementById('pauseBtn').addEventListener('click', togglePause);
|
|
716
|
+
document.getElementById('stopBtn').addEventListener('click', stopPlayback);
|
|
717
|
+
|
|
718
|
+
// Initialize
|
|
719
|
+
loadFileList();
|
|
720
|
+
</script>
|
|
721
|
+
</body>
|
|
722
|
+
</html>
|