@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-simple.html CHANGED
@@ -300,10 +300,10 @@
300
300
  <h1>🎀 EMK to KAR Conversion Demo</h1>
301
301
  <p>
302
302
  Testing <strong>@karaplay/file-coder</strong>
303
- <span class="version">v1.4.9</span>
303
+ <span class="version">v1.5.0</span>
304
304
  </p>
305
305
  <p style="margin-top: 10px; color: #666;">
306
- ✨ ZXIO Format Tempo Fix (2.78x) | MThd Format (4x) | Simple Audio Playback
306
+ ✨ ZXIO Format Duration Fix (1.24x) | MThd Format (4x) | Simple Audio Playback
307
307
  </p>
308
308
  </div>
309
309
 
@@ -365,9 +365,9 @@
365
365
  <h3 style="color: #667eea; margin-top: 25px; margin-bottom: 10px;">πŸ“Š Comparison: EMK vs KAR</h3>
366
366
  <div class="status info" style="margin-bottom: 15px;">
367
367
  <strong>πŸ’‘ Why Duration Decreases:</strong><br>
368
- When tempo increases (Γ—2.78), the music plays faster, so the total duration decreases proportionally (Γ—0.36).<br>
369
- <strong>Formula:</strong> Duration Ratio Γ— Tempo Ratio = 0.36 Γ— 2.78 β‰ˆ 1.0 βœ…<br>
370
- <strong>This is correct!</strong> The MIDI notes are the same, but they play faster at higher BPM.
368
+ When tempo increases (Γ—1.24), the music plays faster, so the total duration decreases proportionally (Γ—0.81).<br>
369
+ <strong>Formula:</strong> Duration Ratio Γ— Tempo Ratio = 0.81 Γ— 1.24 β‰ˆ 1.0 βœ…<br>
370
+ <strong>This is correct!</strong> The MIDI notes are the same, but they play faster at higher BPM. The converted KAR now matches the original song duration (4:42-4:45).
371
371
  </div>
372
372
  <table class="comparison-table">
373
373
  <thead>
@@ -384,6 +384,35 @@
384
384
  </div>
385
385
 
386
386
  <div id="statusContainer"></div>
387
+
388
+ <!-- MIDI Player -->
389
+ <div id="playerContainer" style="display: none; margin-top: 25px;">
390
+ <h3 style="color: #667eea; margin-bottom: 15px;">🎡 MIDI Player</h3>
391
+ <div class="card" style="padding: 20px;">
392
+ <div style="display: flex; align-items: center; gap: 15px; margin-bottom: 15px;">
393
+ <button id="playBtn" class="convert-btn" style="flex: 0 0 auto; min-width: 100px;">
394
+ ▢️ Play
395
+ </button>
396
+ <button id="pauseBtn" class="convert-btn" style="flex: 0 0 auto; min-width: 100px; display: none;">
397
+ ⏸️ Pause
398
+ </button>
399
+ <button id="stopBtn" class="convert-btn" style="flex: 0 0 auto; min-width: 100px;">
400
+ ⏹️ Stop
401
+ </button>
402
+ <div style="flex: 1;">
403
+ <div style="font-size: 14px; color: #666; margin-bottom: 5px;">
404
+ <span id="currentTime">0:00</span> / <span id="totalTime">0:00</span>
405
+ </div>
406
+ <div style="width: 100%; height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden;">
407
+ <div id="progressBar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.1s;"></div>
408
+ </div>
409
+ </div>
410
+ </div>
411
+ <div id="playerStatus" style="font-size: 14px; color: #666; padding: 10px; background: #f8f9fa; border-radius: 6px;">
412
+ ⏳ Ready to play...
413
+ </div>
414
+ </div>
415
+ </div>
387
416
  </div>
388
417
  </div>
389
418
  </div>
@@ -507,6 +536,10 @@
507
536
  const convertBtn = document.getElementById('convertBtn');
508
537
  const downloadBtn = document.getElementById('downloadBtn');
509
538
 
539
+ // Stop and hide player
540
+ stopMidi();
541
+ document.getElementById('playerContainer').style.display = 'none';
542
+
510
543
  convertBtn.disabled = true;
511
544
  convertBtn.innerHTML = '<div class="loading"></div> Converting...';
512
545
 
@@ -554,6 +587,10 @@
554
587
  document.getElementById('lyricsDisplay').textContent = 'βœ… Conversion Complete!';
555
588
  downloadBtn.disabled = false;
556
589
 
590
+ // Show player
591
+ document.getElementById('playerContainer').style.display = 'block';
592
+ midiPlayer.parsedData = null; // Reset to force re-parse
593
+
557
594
  } else {
558
595
  showStatus('error', `❌ Conversion failed!<br>${data.error}`);
559
596
  document.getElementById('lyricsDisplay').textContent = 'Conversion failed';
@@ -664,7 +701,7 @@
664
701
  emk: '-',
665
702
  kar: `${tempoRatio.toFixed(2)}x`,
666
703
  change: emkInfo.format === 'ZXIO' ?
667
- (Math.abs(tempoRatio - 2.78) < 0.1 ? 'βœ… Correct (2.78x)' : '⚠️ Unexpected') :
704
+ (Math.abs(tempoRatio - 1.24) < 0.1 ? 'βœ… Correct (1.24x)' : '⚠️ Unexpected') :
668
705
  (Math.abs(tempoRatio - 4) < 0.5 ? 'βœ… Correct (~4x)' : '⚠️ Unexpected')
669
706
  }
670
707
  ];
@@ -701,9 +738,252 @@
701
738
  container.innerHTML = `<div class="status ${type}">${message}</div>`;
702
739
  }
703
740
 
704
- // Event listeners
741
+ // =================================================================
742
+ // SIMPLE MIDI PLAYER (using karaoke-player libs + Web Audio API)
743
+ // =================================================================
744
+ let midiPlayer = {
745
+ ac: null,
746
+ parsedData: null,
747
+ isPlaying: false,
748
+ isPaused: false,
749
+ startTime: 0,
750
+ pausedTime: 0,
751
+ activeOscillators: {},
752
+ updateInterval: null,
753
+ totalDuration: 0
754
+ };
755
+
756
+ function initMidiPlayer() {
757
+ if (!midiPlayer.ac) {
758
+ midiPlayer.ac = new (window.AudioContext || window.webkitAudioContext)();
759
+ }
760
+ document.getElementById('playerStatus').innerHTML = 'βœ… Ready to play!';
761
+ }
762
+
763
+ async function parseMidiData(base64Data) {
764
+ try {
765
+ document.getElementById('playerStatus').innerHTML = '⏳ Parsing MIDI...';
766
+
767
+ // Call server to parse using karaoke-player libs
768
+ const response = await fetch('/api/parse-kar', {
769
+ method: 'POST',
770
+ headers: { 'Content-Type': 'application/json' },
771
+ body: JSON.stringify({ karData: base64Data })
772
+ });
773
+
774
+ const data = await response.json();
775
+
776
+ if (!data.success) {
777
+ throw new Error(data.error || 'Failed to parse MIDI');
778
+ }
779
+
780
+ document.getElementById('playerStatus').innerHTML = 'βœ… MIDI parsed!';
781
+ return data;
782
+ } catch (error) {
783
+ console.error('MIDI parse error:', error);
784
+ throw error;
785
+ }
786
+ }
787
+
788
+ // MIDI note to frequency conversion
789
+ function midiNoteToFrequency(note) {
790
+ return 440 * Math.pow(2, (note - 69) / 12);
791
+ }
792
+
793
+ function formatTime(seconds) {
794
+ const mins = Math.floor(seconds / 60);
795
+ const secs = Math.floor(seconds % 60);
796
+ return `${mins}:${String(secs).padStart(2, '0')}`;
797
+ }
798
+
799
+ async function playMidi() {
800
+ if (!convertedKarData) {
801
+ alert('Please convert a file first!');
802
+ return;
803
+ }
804
+
805
+ try {
806
+ initMidiPlayer();
807
+
808
+ if (!midiPlayer.parsedData) {
809
+ midiPlayer.parsedData = await parseMidiData(convertedKarData);
810
+ }
811
+
812
+ if (midiPlayer.isPaused) {
813
+ resumeMidi();
814
+ return;
815
+ }
816
+
817
+ // Stop any playing notes
818
+ stopMidi();
819
+
820
+ const events = midiPlayer.parsedData.events;
821
+
822
+ // Use duration from server (Tone.js gives accurate duration)
823
+ midiPlayer.totalDuration = midiPlayer.parsedData.duration || 0;
824
+
825
+ // Convert from milliseconds to seconds
826
+ const totalSeconds = midiPlayer.totalDuration / 1000;
827
+ document.getElementById('totalTime').textContent = formatTime(totalSeconds);
828
+
829
+ console.log('🎡 Player Info:');
830
+ console.log(' Events:', events.length);
831
+ console.log(' Duration:', totalSeconds.toFixed(2), 'seconds');
832
+ console.log(' Duration:', formatTime(totalSeconds));
833
+
834
+ // Start playback
835
+ midiPlayer.isPlaying = true;
836
+ midiPlayer.isPaused = false;
837
+ midiPlayer.startTime = midiPlayer.ac.currentTime;
838
+ midiPlayer.activeOscillators = {};
839
+
840
+ document.getElementById('playBtn').style.display = 'none';
841
+ document.getElementById('pauseBtn').style.display = 'block';
842
+ document.getElementById('playerStatus').innerHTML = `▢️ Playing... (${events.length} events)`;
843
+
844
+ // Schedule all notes
845
+ events.forEach(event => {
846
+ const scheduleTime = midiPlayer.startTime + (event.time / 1000); // Convert ms to seconds
847
+ const noteKey = `${event.track}_${event.note}`;
848
+
849
+ // Note On (type 9 = NOTE_ON)
850
+ if (event.type === 9 && event.velocity > 0) {
851
+ // Schedule note start
852
+ setTimeout(() => {
853
+ if (!midiPlayer.isPlaying) return;
854
+
855
+ try {
856
+ // Create oscillator
857
+ const oscillator = midiPlayer.ac.createOscillator();
858
+ const gainNode = midiPlayer.ac.createGain();
859
+
860
+ oscillator.connect(gainNode);
861
+ gainNode.connect(midiPlayer.ac.destination);
862
+
863
+ // Set frequency from MIDI note
864
+ oscillator.frequency.value = midiNoteToFrequency(event.note);
865
+ oscillator.type = 'sine';
866
+
867
+ // Set volume from velocity
868
+ gainNode.gain.value = (event.velocity / 127) * 0.3; // Scale down volume
869
+
870
+ // Start oscillator
871
+ oscillator.start(scheduleTime);
872
+
873
+ // Store for note off
874
+ midiPlayer.activeOscillators[noteKey] = { oscillator, gainNode, startTime: scheduleTime };
875
+ } catch (e) {
876
+ console.error('Note start error:', e);
877
+ }
878
+ }, Math.max(0, (event.time / 1000) * 1000));
879
+ }
880
+
881
+ // Note Off (type 8 = NOTE_OFF or type 9 with velocity 0)
882
+ if (event.type === 8 || (event.type === 9 && event.velocity === 0)) {
883
+ // Schedule note stop
884
+ setTimeout(() => {
885
+ if (!midiPlayer.isPlaying) return;
886
+
887
+ try {
888
+ const osc = midiPlayer.activeOscillators[noteKey];
889
+ if (osc) {
890
+ // Fade out
891
+ osc.gainNode.gain.exponentialRampToValueAtTime(0.01, scheduleTime + 0.1);
892
+ osc.oscillator.stop(scheduleTime + 0.1);
893
+ delete midiPlayer.activeOscillators[noteKey];
894
+ }
895
+ } catch (e) {
896
+ console.error('Note stop error:', e);
897
+ }
898
+ }, Math.max(0, (event.time / 1000) * 1000));
899
+ }
900
+ });
901
+
902
+ // Update progress
903
+ midiPlayer.updateInterval = setInterval(() => {
904
+ if (!midiPlayer.isPlaying) {
905
+ clearInterval(midiPlayer.updateInterval);
906
+ return;
907
+ }
908
+
909
+ const currentTime = midiPlayer.ac.currentTime - midiPlayer.startTime;
910
+ document.getElementById('currentTime').textContent = formatTime(currentTime);
911
+
912
+ const progress = (currentTime / totalSeconds) * 100;
913
+ document.getElementById('progressBar').style.width = Math.min(progress, 100) + '%';
914
+
915
+ if (currentTime >= totalSeconds + 1) {
916
+ stopMidi();
917
+ }
918
+ }, 100);
919
+
920
+ } catch (error) {
921
+ console.error('Play error:', error);
922
+ document.getElementById('playerStatus').innerHTML = '❌ Play error: ' + error.message;
923
+ }
924
+ }
925
+
926
+ function pauseMidi() {
927
+ if (!midiPlayer.isPlaying) return;
928
+
929
+ midiPlayer.isPaused = true;
930
+ midiPlayer.isPlaying = false;
931
+ midiPlayer.pausedTime = midiPlayer.ac.currentTime - midiPlayer.startTime;
932
+
933
+ // Stop all active oscillators
934
+ Object.values(midiPlayer.activeOscillators).forEach(osc => {
935
+ try {
936
+ osc.oscillator.stop();
937
+ } catch (e) {}
938
+ });
939
+ midiPlayer.activeOscillators = {};
940
+
941
+ document.getElementById('playBtn').style.display = 'block';
942
+ document.getElementById('pauseBtn').style.display = 'none';
943
+ document.getElementById('playerStatus').innerHTML = '⏸️ Paused';
944
+
945
+ if (midiPlayer.updateInterval) {
946
+ clearInterval(midiPlayer.updateInterval);
947
+ }
948
+ }
949
+
950
+ function resumeMidi() {
951
+ // For simplicity, restart from beginning
952
+ playMidi();
953
+ }
954
+
955
+ function stopMidi() {
956
+ midiPlayer.isPlaying = false;
957
+ midiPlayer.isPaused = false;
958
+ midiPlayer.pausedTime = 0;
959
+
960
+ // Stop all active oscillators
961
+ Object.values(midiPlayer.activeOscillators).forEach(osc => {
962
+ try {
963
+ osc.oscillator.stop();
964
+ } catch (e) {}
965
+ });
966
+ midiPlayer.activeOscillators = {};
967
+
968
+ document.getElementById('playBtn').style.display = 'block';
969
+ document.getElementById('pauseBtn').style.display = 'none';
970
+ document.getElementById('currentTime').textContent = '0:00';
971
+ document.getElementById('progressBar').style.width = '0%';
972
+ document.getElementById('playerStatus').innerHTML = '⏹️ Stopped';
973
+
974
+ if (midiPlayer.updateInterval) {
975
+ clearInterval(midiPlayer.updateInterval);
976
+ }
977
+ }
978
+
979
+ // =================================================================
980
+ // EVENT LISTENERS
981
+ // =================================================================
705
982
  document.getElementById('convertBtn').addEventListener('click', convertFile);
706
983
  document.getElementById('downloadBtn').addEventListener('click', downloadKar);
984
+ document.getElementById('playBtn').addEventListener('click', playMidi);
985
+ document.getElementById('pauseBtn').addEventListener('click', pauseMidi);
986
+ document.getElementById('stopBtn').addEventListener('click', stopMidi);
707
987
 
708
988
  // Initialize
709
989
  loadFileList();
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export { decodeEmk as decodeEmkClient, parseSongInfo as parseSongInfoClient, xor
10
10
  export { convertNcnToKarBrowser, parseLyricBuffer, buildKaraokeTrackBrowser, buildMetadataTracksBrowser, fileToBuffer, downloadBuffer, type BrowserConversionOptions, type BrowserConversionResult, } from './ncntokar.browser';
11
11
  export { convertEmkToKarBrowser, convertEmkFileToKar, convertEmkFilesBatch, validateThaiLyricReadabilityBrowser, type BrowserEmkToKarOptions, type BrowserEmkToKarResult, } from './emk-to-kar.browser';
12
12
  export { readKarBuffer, validateKarBuffer, extractLyricsFromKarBuffer, readKarFile as readKarFileFromBrowser, type KarFileInfo as BrowserKarFileInfo, type KarTrack as BrowserKarTrack, } from './kar-reader.browser';
13
+ export { validateKarFile as validateKarTempo, compareEmkKarTempo, type KarValidationResult } from './kar-validator';
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Supports .emk (Extreme Karaoke), .kar (MIDI karaoke), and NCN format conversions
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.readKarFileFromBrowser = exports.extractLyricsFromKarBuffer = exports.validateKarBuffer = exports.readKarBuffer = exports.validateThaiLyricReadabilityBrowser = exports.convertEmkFilesBatch = exports.convertEmkFileToKar = exports.convertEmkToKarBrowser = exports.downloadBuffer = exports.fileToBuffer = exports.buildMetadataTracksBrowser = exports.buildKaraokeTrackBrowser = exports.parseLyricBuffer = exports.convertNcnToKarBrowser = exports.looksLikeTextClient = exports.xorDecryptClient = exports.parseSongInfoClient = exports.decodeEmkClient = exports.looksLikeTextServer = exports.xorDecryptServer = exports.parseSongInfoServer = exports.decodeEmkServer = exports.validateThaiLyricReadability = exports.convertEmkToKarBatch = exports.convertEmkToKar = exports.extractLyricsFromKar = exports.validateKarFile = exports.readKarFile = exports.CursorReader = exports.ensureOutputDoesNotExist = exports.ensureReadableFile = exports.endOfTrack = exports.metaEvent = exports.trimLineEndings = exports.splitLinesKeepEndings = exports.readFileTextTIS620 = exports.buildMetadataTracks = exports.buildKaraokeTrack = exports.parseLyricFile = exports.convertWithDefaults = exports.convertNcnToKar = void 0;
7
+ exports.compareEmkKarTempo = exports.validateKarTempo = exports.readKarFileFromBrowser = exports.extractLyricsFromKarBuffer = exports.validateKarBuffer = exports.readKarBuffer = exports.validateThaiLyricReadabilityBrowser = exports.convertEmkFilesBatch = exports.convertEmkFileToKar = exports.convertEmkToKarBrowser = exports.downloadBuffer = exports.fileToBuffer = exports.buildMetadataTracksBrowser = exports.buildKaraokeTrackBrowser = exports.parseLyricBuffer = exports.convertNcnToKarBrowser = exports.looksLikeTextClient = exports.xorDecryptClient = exports.parseSongInfoClient = exports.decodeEmkClient = exports.looksLikeTextServer = exports.xorDecryptServer = exports.parseSongInfoServer = exports.decodeEmkServer = exports.validateThaiLyricReadability = exports.convertEmkToKarBatch = exports.convertEmkToKar = exports.extractLyricsFromKar = exports.validateKarFile = exports.readKarFile = exports.CursorReader = exports.ensureOutputDoesNotExist = exports.ensureReadableFile = exports.endOfTrack = exports.metaEvent = exports.trimLineEndings = exports.splitLinesKeepEndings = exports.readFileTextTIS620 = exports.buildMetadataTracks = exports.buildKaraokeTrack = exports.parseLyricFile = exports.convertWithDefaults = exports.convertNcnToKar = void 0;
8
8
  // NCN to KAR converter exports
9
9
  var ncntokar_1 = require("./ncntokar");
10
10
  Object.defineProperty(exports, "convertNcnToKar", { enumerable: true, get: function () { return ncntokar_1.convertNcnToKar; } });
@@ -62,3 +62,7 @@ Object.defineProperty(exports, "readKarBuffer", { enumerable: true, get: functio
62
62
  Object.defineProperty(exports, "validateKarBuffer", { enumerable: true, get: function () { return kar_reader_browser_1.validateKarBuffer; } });
63
63
  Object.defineProperty(exports, "extractLyricsFromKarBuffer", { enumerable: true, get: function () { return kar_reader_browser_1.extractLyricsFromKarBuffer; } });
64
64
  Object.defineProperty(exports, "readKarFileFromBrowser", { enumerable: true, get: function () { return kar_reader_browser_1.readKarFile; } });
65
+ // KAR tempo/duration validator exports (NEW v1.5.0+)
66
+ var kar_validator_1 = require("./kar-validator");
67
+ Object.defineProperty(exports, "validateKarTempo", { enumerable: true, get: function () { return kar_validator_1.validateKarFile; } });
68
+ Object.defineProperty(exports, "compareEmkKarTempo", { enumerable: true, get: function () { return kar_validator_1.compareEmkKarTempo; } });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * KAR File Validation Utilities
3
+ * Validates tempo, duration, and format accuracy
4
+ */
5
+ export interface KarValidationResult {
6
+ valid: boolean;
7
+ tempo: number;
8
+ duration: number;
9
+ durationMinutes: string;
10
+ ppq: number;
11
+ trackCount: number;
12
+ noteCount: number;
13
+ format: number;
14
+ warnings: string[];
15
+ errors: string[];
16
+ }
17
+ /**
18
+ * Validate KAR file accuracy using Tone.js
19
+ *
20
+ * This function uses @tonejs/midi which handles tempo events correctly,
21
+ * providing accurate duration calculations.
22
+ *
23
+ * @param karBuffer - KAR file buffer
24
+ * @returns Validation results with tempo and duration info
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { validateKarFile } from '@karaplay/file-coder';
29
+ * import * as fs from 'fs';
30
+ *
31
+ * const karBuffer = fs.readFileSync('song.kar');
32
+ * const validation = validateKarFile(karBuffer);
33
+ *
34
+ * console.log(`Valid: ${validation.valid}`);
35
+ * console.log(`Tempo: ${validation.tempo} BPM`);
36
+ * console.log(`Duration: ${validation.durationMinutes}`);
37
+ * ```
38
+ */
39
+ export declare function validateKarFile(karBuffer: Buffer): KarValidationResult;
40
+ /**
41
+ * Compare EMK and converted KAR tempo/duration
42
+ *
43
+ * @param emkBuffer - Original EMK file buffer
44
+ * @param karBuffer - Converted KAR file buffer
45
+ * @returns Comparison results
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { compareEmkKarTempo } from '@karaplay/file-coder';
50
+ *
51
+ * const comparison = compareEmkKarTempo(emkBuffer, karBuffer);
52
+ * console.log(`Tempo ratio: ${comparison.tempoRatio.toFixed(2)}x`);
53
+ * console.log(`Format: ${comparison.format}`);
54
+ * ```
55
+ */
56
+ export declare function compareEmkKarTempo(emkBuffer: Buffer, karBuffer: Buffer): {
57
+ emkTempo: number;
58
+ karTempo: number;
59
+ tempoRatio: number;
60
+ emkDuration: number;
61
+ karDuration: number;
62
+ durationRatio: number;
63
+ format: string;
64
+ expectedRatio: number;
65
+ ratioMatch: boolean;
66
+ };
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /**
3
+ * KAR File Validation Utilities
4
+ * Validates tempo, duration, and format accuracy
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateKarFile = validateKarFile;
8
+ exports.compareEmkKarTempo = compareEmkKarTempo;
9
+ /**
10
+ * Validate KAR file accuracy using Tone.js
11
+ *
12
+ * This function uses @tonejs/midi which handles tempo events correctly,
13
+ * providing accurate duration calculations.
14
+ *
15
+ * @param karBuffer - KAR file buffer
16
+ * @returns Validation results with tempo and duration info
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { validateKarFile } from '@karaplay/file-coder';
21
+ * import * as fs from 'fs';
22
+ *
23
+ * const karBuffer = fs.readFileSync('song.kar');
24
+ * const validation = validateKarFile(karBuffer);
25
+ *
26
+ * console.log(`Valid: ${validation.valid}`);
27
+ * console.log(`Tempo: ${validation.tempo} BPM`);
28
+ * console.log(`Duration: ${validation.durationMinutes}`);
29
+ * ```
30
+ */
31
+ function validateKarFile(karBuffer) {
32
+ const warnings = [];
33
+ const errors = [];
34
+ try {
35
+ // Dynamic import to avoid bundling issues
36
+ const { Midi } = require('@tonejs/midi');
37
+ const midi = new Midi(karBuffer);
38
+ // Get basic info
39
+ const tempo = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 0;
40
+ const duration = midi.duration;
41
+ const ppq = midi.header.ppq;
42
+ const trackCount = midi.tracks.length;
43
+ // Count notes
44
+ let noteCount = 0;
45
+ midi.tracks.forEach((track) => {
46
+ noteCount += track.notes.length;
47
+ });
48
+ // Format duration
49
+ const minutes = Math.floor(duration / 60);
50
+ const seconds = Math.floor(duration % 60);
51
+ const durationMinutes = `${minutes}:${String(seconds).padStart(2, '0')}`;
52
+ // Validation checks
53
+ if (tempo === 0) {
54
+ warnings.push('No tempo event found - may use default 120 BPM');
55
+ }
56
+ if (duration === 0) {
57
+ errors.push('Duration is zero - file may be corrupt or empty');
58
+ }
59
+ if (noteCount === 0) {
60
+ warnings.push('No notes found - file may be empty');
61
+ }
62
+ if (trackCount === 0) {
63
+ errors.push('No tracks found - file may be corrupt');
64
+ }
65
+ // Check for unusual values
66
+ if (tempo > 300) {
67
+ warnings.push(`High tempo detected (${tempo.toFixed(0)} BPM) - verify playback speed`);
68
+ }
69
+ if (duration < 10) {
70
+ warnings.push(`Very short duration (${duration.toFixed(1)}s) - verify timing`);
71
+ }
72
+ return {
73
+ valid: errors.length === 0,
74
+ tempo,
75
+ duration,
76
+ durationMinutes,
77
+ ppq,
78
+ trackCount,
79
+ noteCount,
80
+ format: midi.header.format,
81
+ warnings,
82
+ errors
83
+ };
84
+ }
85
+ catch (error) {
86
+ errors.push(`Failed to parse KAR file: ${error?.message || String(error)}`);
87
+ return {
88
+ valid: false,
89
+ tempo: 0,
90
+ duration: 0,
91
+ durationMinutes: '0:00',
92
+ ppq: 0,
93
+ trackCount: 0,
94
+ noteCount: 0,
95
+ format: 0,
96
+ warnings,
97
+ errors
98
+ };
99
+ }
100
+ }
101
+ /**
102
+ * Compare EMK and converted KAR tempo/duration
103
+ *
104
+ * @param emkBuffer - Original EMK file buffer
105
+ * @param karBuffer - Converted KAR file buffer
106
+ * @returns Comparison results
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * import { compareEmkKarTempo } from '@karaplay/file-coder';
111
+ *
112
+ * const comparison = compareEmkKarTempo(emkBuffer, karBuffer);
113
+ * console.log(`Tempo ratio: ${comparison.tempoRatio.toFixed(2)}x`);
114
+ * console.log(`Format: ${comparison.format}`);
115
+ * ```
116
+ */
117
+ function compareEmkKarTempo(emkBuffer, karBuffer) {
118
+ try {
119
+ const { Midi } = require('@tonejs/midi');
120
+ // Import from index to get proper exports
121
+ const index = require('./index');
122
+ const decodeEmkServer = index.decodeEmkServer || require('./emk/server-decode').decodeEmk;
123
+ // Decode EMK
124
+ const decoded = decodeEmkServer(emkBuffer);
125
+ const emkMidi = new Midi(decoded.midi);
126
+ // Parse KAR
127
+ const karMidi = new Midi(karBuffer);
128
+ const emkTempo = emkMidi.header.tempos[0]?.bpm || 0;
129
+ const karTempo = karMidi.header.tempos[0]?.bpm || 0;
130
+ const tempoRatio = karTempo / emkTempo;
131
+ const emkDuration = emkMidi.duration;
132
+ const karDuration = karMidi.duration;
133
+ const durationRatio = karDuration / emkDuration;
134
+ const format = decoded.isZxioFormat ? 'ZXIO' : 'MThd';
135
+ const expectedRatio = decoded.isZxioFormat ? 1.24 : 4.0;
136
+ const ratioMatch = Math.abs(tempoRatio - expectedRatio) < 0.1;
137
+ return {
138
+ emkTempo,
139
+ karTempo,
140
+ tempoRatio,
141
+ emkDuration,
142
+ karDuration,
143
+ durationRatio,
144
+ format,
145
+ expectedRatio,
146
+ ratioMatch
147
+ };
148
+ }
149
+ catch (error) {
150
+ throw new Error(`Failed to compare EMK/KAR: ${error?.message || String(error)}`);
151
+ }
152
+ }
@@ -363,7 +363,19 @@ function ensureTracksHaveEndOfTrack(tracks) {
363
363
  function fixEmkMidiTempoBrowser(midi, ticksPerBeat, isZxioFormat = false) {
364
364
  // ZXIO format uses a different time base (PPQ / 77.42 instead of PPQ / 24)
365
365
  // This gives a ratio of approximately 1.24x instead of 4x
366
- const ratio = isZxioFormat ? (ticksPerBeat / 77.42) : (ticksPerBeat / 24);
366
+ //
367
+ // Special case: Some EMK files (e.g., Z2510006.emk) have PPQ >= 480 and are already
368
+ // properly timed, so they don't need tempo adjustment (ratio = 1x)
369
+ let ratio;
370
+ if (isZxioFormat) {
371
+ ratio = ticksPerBeat / 77.42;
372
+ }
373
+ else if (ticksPerBeat >= 480) {
374
+ ratio = 1.0; // No adjustment needed for high PPQ files
375
+ }
376
+ else {
377
+ ratio = ticksPerBeat / 24; // Standard MThd conversion
378
+ }
367
379
  let fixed = 0;
368
380
  // Find and fix all tempo events
369
381
  if (midi.tracks) {
package/dist/ncntokar.js CHANGED
@@ -336,7 +336,19 @@ function ensureTracksHaveEndOfTrack(tracks) {
336
336
  function fixEmkMidiTempo(midi, ticksPerBeat, isZxioFormat = false) {
337
337
  // ZXIO format uses a different time base (PPQ / 77.42 instead of PPQ / 24)
338
338
  // This gives a ratio of approximately 1.24x instead of 4x
339
- const ratio = isZxioFormat ? (ticksPerBeat / 77.42) : (ticksPerBeat / 24);
339
+ //
340
+ // Special case: Some EMK files (e.g., Z2510006.emk) have PPQ >= 480 and are already
341
+ // properly timed, so they don't need tempo adjustment (ratio = 1x)
342
+ let ratio;
343
+ if (isZxioFormat) {
344
+ ratio = ticksPerBeat / 77.42;
345
+ }
346
+ else if (ticksPerBeat >= 480) {
347
+ ratio = 1.0; // No adjustment needed for high PPQ files
348
+ }
349
+ else {
350
+ ratio = ticksPerBeat / 24; // Standard MThd conversion
351
+ }
340
352
  let fixed = 0;
341
353
  // Find and fix all tempo events
342
354
  if (midi.tracks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karaplay/file-coder",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "A comprehensive library for encoding/decoding karaoke files (.emk, .kar, MIDI) with Next.js support. Convert EMK to KAR, read/write karaoke files, full browser and server support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,9 @@
26
26
  "test": "node_modules/.bin/jest",
27
27
  "test:watch": "node_modules/.bin/jest --watch",
28
28
  "test:coverage": "node_modules/.bin/jest --coverage",
29
+ "test:emk": "node verify-emk-reference.js",
30
+ "test:emk:verbose": "node verify-emk-reference.js --verbose",
31
+ "analyze:emk": "node analyze-all-emk.js",
29
32
  "prepare": "npm run build",
30
33
  "demo": "node demo-server.js"
31
34
  },