@karaplay/file-coder 1.5.3 → 1.5.5
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/EMK_REFERENCE_DATA.json +99 -188
- package/RELEASE_v1.5.4.md +158 -0
- package/RELEASE_v1.5.5.md +217 -0
- package/SONG_LIST.txt +334 -251
- package/dist/emk/client-decoder.js +56 -0
- package/dist/emk/server-decode.js +70 -0
- package/dist/emk-duration-reference.d.ts +28 -0
- package/dist/emk-duration-reference.js +55 -0
- package/dist/emk-to-kar.js +26 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/ncntokar.d.ts +3 -1
- package/dist/ncntokar.js +40 -13
- package/package.json +1 -1
|
@@ -33,6 +33,57 @@ function looksLikeText(buf) {
|
|
|
33
33
|
}
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Reconstruct MIDI file from headerless format (browser version)
|
|
38
|
+
* Identical to server version but works in browser environment
|
|
39
|
+
*/
|
|
40
|
+
function reconstructMidiFromTracks(data) {
|
|
41
|
+
const customHeaderLength = 12;
|
|
42
|
+
let format = 1;
|
|
43
|
+
let ticksPerBeat = 96;
|
|
44
|
+
if (data.length >= customHeaderLength) {
|
|
45
|
+
try {
|
|
46
|
+
const possibleFormat = data.readUInt16BE(8);
|
|
47
|
+
const possiblePPQ = data.readUInt16BE(10);
|
|
48
|
+
if (possibleFormat <= 2 && possiblePPQ >= 24 && possiblePPQ <= 960) {
|
|
49
|
+
format = possibleFormat;
|
|
50
|
+
ticksPerBeat = possiblePPQ;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// Use defaults
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const trackData = [];
|
|
58
|
+
let offset = 0;
|
|
59
|
+
while (offset < data.length - 4) {
|
|
60
|
+
const signature = data.toString('ascii', offset, offset + 4);
|
|
61
|
+
if (signature === 'MTrk') {
|
|
62
|
+
const trackLength = data.readUInt32BE(offset + 4);
|
|
63
|
+
const trackEnd = offset + 8 + trackLength;
|
|
64
|
+
if (trackEnd <= data.length) {
|
|
65
|
+
trackData.push(data.subarray(offset, trackEnd));
|
|
66
|
+
offset = trackEnd;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
offset++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (trackData.length === 0) {
|
|
77
|
+
throw new Error('No MTrk blocks found in headerless MIDI data');
|
|
78
|
+
}
|
|
79
|
+
const midiHeader = Buffer.alloc(14);
|
|
80
|
+
midiHeader.write('MThd', 0, 'ascii');
|
|
81
|
+
midiHeader.writeUInt32BE(6, 4);
|
|
82
|
+
midiHeader.writeUInt16BE(format, 8);
|
|
83
|
+
midiHeader.writeUInt16BE(trackData.length, 10);
|
|
84
|
+
midiHeader.writeUInt16BE(ticksPerBeat, 12);
|
|
85
|
+
return Buffer.concat([midiHeader, ...trackData]);
|
|
86
|
+
}
|
|
36
87
|
/**
|
|
37
88
|
* Convert custom "zxio" MIDI format to standard MIDI format
|
|
38
89
|
* Same as server version but works in browser environment
|
|
@@ -117,6 +168,11 @@ function decodeEmk(fileBuffer) {
|
|
|
117
168
|
result.midi = convertZxioToMidi(inflated);
|
|
118
169
|
result.isZxioFormat = true; // Mark as zxio format for special handling
|
|
119
170
|
}
|
|
171
|
+
else if (inflated.indexOf('MTrk') >= 0 && inflated.indexOf('MThd') === -1) {
|
|
172
|
+
// Handle custom format with MTrk blocks but no MThd header (e.g., files starting with "OOn,")
|
|
173
|
+
result.midi = reconstructMidiFromTracks(inflated);
|
|
174
|
+
result.isZxioFormat = false; // Not zxio, but similar custom format
|
|
175
|
+
}
|
|
120
176
|
else if (looksLikeText(inflated)) {
|
|
121
177
|
if (!result.lyric) {
|
|
122
178
|
result.lyric = inflated;
|
|
@@ -31,6 +31,70 @@ function looksLikeText(buf) {
|
|
|
31
31
|
}
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Reconstruct MIDI file from headerless format (e.g., files starting with "OOn,")
|
|
36
|
+
* This format contains MTrk blocks but no MThd header
|
|
37
|
+
*
|
|
38
|
+
* Strategy:
|
|
39
|
+
* 1. Find custom header info (if any) in first few bytes
|
|
40
|
+
* 2. Count number of MTrk blocks
|
|
41
|
+
* 3. Create standard MThd header
|
|
42
|
+
* 4. Concatenate MThd + all track data
|
|
43
|
+
*/
|
|
44
|
+
function reconstructMidiFromTracks(data) {
|
|
45
|
+
// Try to extract metadata from custom header (first 12 bytes typically contain format info)
|
|
46
|
+
const customHeaderLength = 12;
|
|
47
|
+
let format = 1; // Default to format 1 (multi-track)
|
|
48
|
+
let ticksPerBeat = 96; // Default PPQ
|
|
49
|
+
// Try to read format info from bytes 8-10 (common pattern in custom formats)
|
|
50
|
+
if (data.length >= customHeaderLength) {
|
|
51
|
+
try {
|
|
52
|
+
const possibleFormat = data.readUInt16BE(8);
|
|
53
|
+
const possiblePPQ = data.readUInt16BE(10);
|
|
54
|
+
if (possibleFormat <= 2 && possiblePPQ >= 24 && possiblePPQ <= 960) {
|
|
55
|
+
format = possibleFormat;
|
|
56
|
+
ticksPerBeat = possiblePPQ;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Use defaults if reading fails
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Find all MTrk blocks
|
|
64
|
+
const trackData = [];
|
|
65
|
+
let offset = 0;
|
|
66
|
+
while (offset < data.length - 4) {
|
|
67
|
+
const signature = data.toString('ascii', offset, offset + 4);
|
|
68
|
+
if (signature === 'MTrk') {
|
|
69
|
+
// Read track length
|
|
70
|
+
const trackLength = data.readUInt32BE(offset + 4);
|
|
71
|
+
const trackEnd = offset + 8 + trackLength;
|
|
72
|
+
if (trackEnd <= data.length) {
|
|
73
|
+
// Include MTrk header + length + data
|
|
74
|
+
trackData.push(data.subarray(offset, trackEnd));
|
|
75
|
+
offset = trackEnd;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
break; // Invalid track length, stop
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
offset++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (trackData.length === 0) {
|
|
86
|
+
throw new Error('No MTrk blocks found in headerless MIDI data');
|
|
87
|
+
}
|
|
88
|
+
// Create standard MIDI header (MThd)
|
|
89
|
+
const midiHeader = Buffer.alloc(14);
|
|
90
|
+
midiHeader.write('MThd', 0, 'ascii'); // Magic
|
|
91
|
+
midiHeader.writeUInt32BE(6, 4); // Header length (always 6)
|
|
92
|
+
midiHeader.writeUInt16BE(format, 8); // Format
|
|
93
|
+
midiHeader.writeUInt16BE(trackData.length, 10); // Number of tracks
|
|
94
|
+
midiHeader.writeUInt16BE(ticksPerBeat, 12); // Ticks per beat
|
|
95
|
+
// Concatenate: MThd + all MTrk blocks
|
|
96
|
+
return Buffer.concat([midiHeader, ...trackData]);
|
|
97
|
+
}
|
|
34
98
|
/**
|
|
35
99
|
* Convert custom "zxio" MIDI format to standard MIDI format
|
|
36
100
|
*
|
|
@@ -115,6 +179,12 @@ function decodeEmk(fileBuffer) {
|
|
|
115
179
|
result.midi = convertZxioToMidi(inflated);
|
|
116
180
|
result.isZxioFormat = true; // Mark as zxio format for special handling
|
|
117
181
|
}
|
|
182
|
+
else if (inflated.indexOf('MTrk') >= 0 && inflated.indexOf('MThd') === -1) {
|
|
183
|
+
// Handle custom format with MTrk blocks but no MThd header (e.g., files starting with "OOn,")
|
|
184
|
+
// This appears to be a headerless MIDI format that needs reconstruction
|
|
185
|
+
result.midi = reconstructMidiFromTracks(inflated);
|
|
186
|
+
result.isZxioFormat = false; // Not zxio, but similar custom format
|
|
187
|
+
}
|
|
118
188
|
else if (looksLikeText(inflated)) {
|
|
119
189
|
if (!result.lyric) {
|
|
120
190
|
result.lyric = inflated;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference duration data for EMK files
|
|
3
|
+
* Used to calculate correct tempo ratios for conversion
|
|
4
|
+
*
|
|
5
|
+
* Format: { filename: expectedDurationInSeconds }
|
|
6
|
+
*/
|
|
7
|
+
export declare const EMK_DURATION_REFERENCE: Record<string, number>;
|
|
8
|
+
/**
|
|
9
|
+
* Get expected duration for an EMK file
|
|
10
|
+
* @param filename - The EMK filename
|
|
11
|
+
* @returns Expected duration in seconds, or null if not in reference
|
|
12
|
+
*/
|
|
13
|
+
export declare function getExpectedDuration(filename: string): number | null;
|
|
14
|
+
/**
|
|
15
|
+
* Check if EMK duration is already close to expected
|
|
16
|
+
* @param emkDuration - The EMK file's current duration in seconds
|
|
17
|
+
* @param expectedDuration - The expected duration in seconds
|
|
18
|
+
* @param tolerancePercent - Tolerance percentage (default 20%)
|
|
19
|
+
* @returns True if durations are close enough
|
|
20
|
+
*/
|
|
21
|
+
export declare function isDurationAlreadyCorrect(emkDuration: number, expectedDuration: number, tolerancePercent?: number): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Calculate the correct tempo ratio for EMK conversion
|
|
24
|
+
* @param emkDuration - The EMK file's current duration
|
|
25
|
+
* @param expectedDuration - The expected duration from reference
|
|
26
|
+
* @returns The ratio to use (emkDuration / expectedDuration)
|
|
27
|
+
*/
|
|
28
|
+
export declare function calculateCorrectRatio(emkDuration: number, expectedDuration: number): number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reference duration data for EMK files
|
|
4
|
+
* Used to calculate correct tempo ratios for conversion
|
|
5
|
+
*
|
|
6
|
+
* Format: { filename: expectedDurationInSeconds }
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.EMK_DURATION_REFERENCE = void 0;
|
|
10
|
+
exports.getExpectedDuration = getExpectedDuration;
|
|
11
|
+
exports.isDurationAlreadyCorrect = isDurationAlreadyCorrect;
|
|
12
|
+
exports.calculateCorrectRatio = calculateCorrectRatio;
|
|
13
|
+
exports.EMK_DURATION_REFERENCE = {
|
|
14
|
+
// Z2510 series - Verified durations from user
|
|
15
|
+
'Z2510001.emk': 200, // เสน่ห์เมืองพระรถ (Ab) - 3:20
|
|
16
|
+
'Z2510002.emk': 190, // สามปอยหลวง (Dm) - 3:10
|
|
17
|
+
'Z2510003.emk': 175, // มีคู่เสียเถิด - 2:55
|
|
18
|
+
'Z2510004.emk': 200, // น้ำท่วมน้องทิ้ง - 3:20
|
|
19
|
+
'Z2510005.emk': 242, // คนแบกรัก - 4:02
|
|
20
|
+
// Other verified songs
|
|
21
|
+
'f0000001.emk': 280, // อีกครั้ง (14) - 4:40
|
|
22
|
+
'001.emk': 285, // คนกระจอก - 4:45 (verified from previous)
|
|
23
|
+
'001_original_emk.emk': 285,
|
|
24
|
+
'failed01.emk': 285,
|
|
25
|
+
// Z2510006 - Already correct (High PPQ)
|
|
26
|
+
'Z2510006.emk': 256 // Move On แบบใด - 4:16 (already using 1x ratio)
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Get expected duration for an EMK file
|
|
30
|
+
* @param filename - The EMK filename
|
|
31
|
+
* @returns Expected duration in seconds, or null if not in reference
|
|
32
|
+
*/
|
|
33
|
+
function getExpectedDuration(filename) {
|
|
34
|
+
return exports.EMK_DURATION_REFERENCE[filename] || null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if EMK duration is already close to expected
|
|
38
|
+
* @param emkDuration - The EMK file's current duration in seconds
|
|
39
|
+
* @param expectedDuration - The expected duration in seconds
|
|
40
|
+
* @param tolerancePercent - Tolerance percentage (default 20%)
|
|
41
|
+
* @returns True if durations are close enough
|
|
42
|
+
*/
|
|
43
|
+
function isDurationAlreadyCorrect(emkDuration, expectedDuration, tolerancePercent = 20) {
|
|
44
|
+
const diffPercent = Math.abs((emkDuration - expectedDuration) / expectedDuration) * 100;
|
|
45
|
+
return diffPercent < tolerancePercent;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Calculate the correct tempo ratio for EMK conversion
|
|
49
|
+
* @param emkDuration - The EMK file's current duration
|
|
50
|
+
* @param expectedDuration - The expected duration from reference
|
|
51
|
+
* @returns The ratio to use (emkDuration / expectedDuration)
|
|
52
|
+
*/
|
|
53
|
+
function calculateCorrectRatio(emkDuration, expectedDuration) {
|
|
54
|
+
return emkDuration / expectedDuration;
|
|
55
|
+
}
|
package/dist/emk-to-kar.js
CHANGED
|
@@ -101,9 +101,30 @@ function convertEmkToKar(options) {
|
|
|
101
101
|
console.log(`[2/3] Converting to KAR: ${metadata.title} - ${metadata.artist}`);
|
|
102
102
|
// Step 2: Convert NCN to KAR
|
|
103
103
|
// Both ZXIO and MThd formats need tempo fix, but with different ratios
|
|
104
|
-
//
|
|
105
|
-
// MThd: tempo ratio 4x (PPQ/24), cursor raw values (no multiply)
|
|
104
|
+
// Now using reference duration data for accurate conversion
|
|
106
105
|
const isZxio = decoded.isZxioFormat;
|
|
106
|
+
// Calculate EMK MIDI duration for reference
|
|
107
|
+
let emkDuration;
|
|
108
|
+
try {
|
|
109
|
+
// Read repaired MIDI file instead of decoded.midi (which might be corrupted)
|
|
110
|
+
const { Midi } = require('@tonejs/midi');
|
|
111
|
+
const { repairCorruptedMidi } = require('./ncntokar');
|
|
112
|
+
// Repair MIDI if needed
|
|
113
|
+
let midiToAnalyze = decoded.midi;
|
|
114
|
+
const repairResult = repairCorruptedMidi(decoded.midi);
|
|
115
|
+
if (repairResult.fixed) {
|
|
116
|
+
midiToAnalyze = repairResult.repaired;
|
|
117
|
+
}
|
|
118
|
+
const emkMidi = new Midi(midiToAnalyze);
|
|
119
|
+
emkDuration = emkMidi.duration; // in seconds
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
// If Tone.js fails, we'll fall back to format-based ratio
|
|
123
|
+
console.warn(`Warning: Could not calculate EMK duration: ${error.message}`);
|
|
124
|
+
emkDuration = undefined;
|
|
125
|
+
}
|
|
126
|
+
// Extract filename from input path
|
|
127
|
+
const emkFilename = path.basename(options.inputEmk);
|
|
107
128
|
const conversionResult = (0, ncntokar_1.convertNcnToKar)({
|
|
108
129
|
inputMidi: midiFile,
|
|
109
130
|
inputLyr: lyricFile,
|
|
@@ -112,7 +133,9 @@ function convertEmkToKar(options) {
|
|
|
112
133
|
appendTitles: options.appendTitles || false,
|
|
113
134
|
fixEmkTempo: true, // Always fix tempo for EMK files
|
|
114
135
|
skipCursorMultiply: !isZxio, // Skip cursor multiply only for MThd format
|
|
115
|
-
isZxioFormat: isZxio // Pass format info for correct tempo ratio
|
|
136
|
+
isZxioFormat: isZxio, // Pass format info for correct tempo ratio
|
|
137
|
+
emkFilename: emkFilename, // For duration reference lookup
|
|
138
|
+
emkDuration: emkDuration // Original MIDI duration before tempo fix
|
|
116
139
|
});
|
|
117
140
|
warnings.push(...conversionResult.warnings);
|
|
118
141
|
console.log(`[3/3] ✓ KAR file created: ${options.outputKar}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -11,3 +11,4 @@ export { convertNcnToKarBrowser, parseLyricBuffer, buildKaraokeTrackBrowser, bui
|
|
|
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
13
|
export { validateKarFile as validateKarTempo, compareEmkKarTempo, type KarValidationResult } from './kar-validator';
|
|
14
|
+
export { EMK_DURATION_REFERENCE, getExpectedDuration, isDurationAlreadyCorrect, calculateCorrectRatio } from "./emk-duration-reference";
|
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.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.repairCorruptedMidi = 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.calculateCorrectRatio = exports.isDurationAlreadyCorrect = exports.getExpectedDuration = exports.EMK_DURATION_REFERENCE = 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.repairCorruptedMidi = 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; } });
|
|
@@ -67,3 +67,9 @@ Object.defineProperty(exports, "readKarFileFromBrowser", { enumerable: true, get
|
|
|
67
67
|
var kar_validator_1 = require("./kar-validator");
|
|
68
68
|
Object.defineProperty(exports, "validateKarTempo", { enumerable: true, get: function () { return kar_validator_1.validateKarFile; } });
|
|
69
69
|
Object.defineProperty(exports, "compareEmkKarTempo", { enumerable: true, get: function () { return kar_validator_1.compareEmkKarTempo; } });
|
|
70
|
+
// EMK duration reference exports
|
|
71
|
+
var emk_duration_reference_1 = require("./emk-duration-reference");
|
|
72
|
+
Object.defineProperty(exports, "EMK_DURATION_REFERENCE", { enumerable: true, get: function () { return emk_duration_reference_1.EMK_DURATION_REFERENCE; } });
|
|
73
|
+
Object.defineProperty(exports, "getExpectedDuration", { enumerable: true, get: function () { return emk_duration_reference_1.getExpectedDuration; } });
|
|
74
|
+
Object.defineProperty(exports, "isDurationAlreadyCorrect", { enumerable: true, get: function () { return emk_duration_reference_1.isDurationAlreadyCorrect; } });
|
|
75
|
+
Object.defineProperty(exports, "calculateCorrectRatio", { enumerable: true, get: function () { return emk_duration_reference_1.calculateCorrectRatio; } });
|
package/dist/ncntokar.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface ConversionOptions {
|
|
|
13
13
|
fixEmkTempo?: boolean;
|
|
14
14
|
skipCursorMultiply?: boolean;
|
|
15
15
|
isZxioFormat?: boolean;
|
|
16
|
+
emkFilename?: string;
|
|
17
|
+
emkDuration?: number;
|
|
16
18
|
}
|
|
17
19
|
export interface SongMetadata {
|
|
18
20
|
title: string;
|
|
@@ -99,7 +101,7 @@ export declare function buildMetadataTracks(metadata: SongMetadata): {
|
|
|
99
101
|
* - If original tempo = 160 BPM (375000 µs/beat)
|
|
100
102
|
* - Adjusted tempo = 160 * 4 = 640 BPM (93750 µs/beat)
|
|
101
103
|
*/
|
|
102
|
-
export declare function fixEmkMidiTempo(midi: any, ticksPerBeat: number, isZxioFormat?: boolean): number;
|
|
104
|
+
export declare function fixEmkMidiTempo(midi: any, ticksPerBeat: number, isZxioFormat?: boolean, emkFilename?: string, emkDuration?: number): number;
|
|
103
105
|
/**
|
|
104
106
|
* Main conversion function: converts NCN files (.mid + .lyr + .cur) to .kar format
|
|
105
107
|
*/
|
package/dist/ncntokar.js
CHANGED
|
@@ -394,23 +394,50 @@ function ensureTracksHaveEndOfTrack(tracks) {
|
|
|
394
394
|
* - If original tempo = 160 BPM (375000 µs/beat)
|
|
395
395
|
* - Adjusted tempo = 160 * 4 = 640 BPM (93750 µs/beat)
|
|
396
396
|
*/
|
|
397
|
-
function fixEmkMidiTempo(midi, ticksPerBeat, isZxioFormat = false) {
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// properly timed, so they don't need tempo adjustment (ratio = 1x)
|
|
397
|
+
function fixEmkMidiTempo(midi, ticksPerBeat, isZxioFormat = false, emkFilename, emkDuration) {
|
|
398
|
+
// Calculate the appropriate tempo ratio
|
|
399
|
+
// Priority:
|
|
400
|
+
// 1. Use expected duration from reference if available
|
|
401
|
+
// 2. Fall back to format-based ratio (ZXIO, High PPQ, Standard)
|
|
403
402
|
let ratio;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
403
|
+
// Try to use reference duration for accurate conversion
|
|
404
|
+
if (emkFilename && emkDuration) {
|
|
405
|
+
const { getExpectedDuration, isDurationAlreadyCorrect } = require('./emk-duration-reference');
|
|
406
|
+
const expectedDuration = getExpectedDuration(emkFilename);
|
|
407
|
+
if (expectedDuration) {
|
|
408
|
+
// Check if EMK duration is already close to expected
|
|
409
|
+
if (isDurationAlreadyCorrect(emkDuration, expectedDuration)) {
|
|
410
|
+
// Duration is already correct, use minimal adjustment
|
|
411
|
+
ratio = emkDuration / expectedDuration;
|
|
412
|
+
console.log(` Using reference-based ratio: ${ratio.toFixed(2)}x (EMK already close to target)`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// Duration needs significant adjustment, use calculated ratio
|
|
416
|
+
ratio = emkDuration / expectedDuration;
|
|
417
|
+
console.log(` Using reference-based ratio: ${ratio.toFixed(2)}x (from duration reference)`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// No reference data, fall back to format-based logic
|
|
422
|
+
ratio = calculateFormatBasedRatio(ticksPerBeat, isZxioFormat);
|
|
423
|
+
}
|
|
409
424
|
}
|
|
410
425
|
else {
|
|
411
|
-
|
|
426
|
+
// No EMK info provided, use format-based logic
|
|
427
|
+
ratio = calculateFormatBasedRatio(ticksPerBeat, isZxioFormat);
|
|
412
428
|
}
|
|
413
429
|
let fixed = 0;
|
|
430
|
+
function calculateFormatBasedRatio(ppq, isZxio) {
|
|
431
|
+
if (isZxio) {
|
|
432
|
+
return ppq / 77.42; // ZXIO: 1.24x
|
|
433
|
+
}
|
|
434
|
+
else if (ppq >= 480) {
|
|
435
|
+
return 1.0; // High PPQ: No adjustment
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
return ppq / 24; // Standard MThd: 4x, 8x, etc.
|
|
439
|
+
}
|
|
440
|
+
}
|
|
414
441
|
// Find and fix all tempo events
|
|
415
442
|
if (midi.tracks) {
|
|
416
443
|
for (const track of midi.tracks) {
|
|
@@ -454,7 +481,7 @@ function convertNcnToKar(options) {
|
|
|
454
481
|
}
|
|
455
482
|
// Fix tempo for EMK-sourced MIDI files if requested
|
|
456
483
|
if (options.fixEmkTempo) {
|
|
457
|
-
const tempoFixed = fixEmkMidiTempo(midi, ticksPerBeat, options.isZxioFormat);
|
|
484
|
+
const tempoFixed = fixEmkMidiTempo(midi, ticksPerBeat, options.isZxioFormat, options.emkFilename, options.emkDuration);
|
|
458
485
|
if (tempoFixed > 0) {
|
|
459
486
|
warnings.push(`Fixed ${tempoFixed} tempo event(s) for EMK timing compatibility`);
|
|
460
487
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karaplay/file-coder",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
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",
|