@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.
@@ -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>