@karaplay/file-coder 1.1.0

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.
Files changed (75) hide show
  1. package/BROWSER_API.md +318 -0
  2. package/README.md +216 -0
  3. package/REFACTORING_SUMMARY.txt +250 -0
  4. package/WORKFLOW_SUMMARY.txt +78 -0
  5. package/bin/ncntokar-cli.js +39 -0
  6. package/dist/client.d.ts +26 -0
  7. package/dist/client.js +74 -0
  8. package/dist/emk/client-decoder.d.ts +22 -0
  9. package/dist/emk/client-decoder.js +133 -0
  10. package/dist/emk/server-decode.d.ts +21 -0
  11. package/dist/emk/server-decode.js +123 -0
  12. package/dist/emk-to-kar.browser.d.ts +51 -0
  13. package/dist/emk-to-kar.browser.js +139 -0
  14. package/dist/emk-to-kar.d.ts +53 -0
  15. package/dist/emk-to-kar.js +210 -0
  16. package/dist/index.d.ts +12 -0
  17. package/dist/index.js +64 -0
  18. package/dist/kar-reader.browser.d.ts +46 -0
  19. package/dist/kar-reader.browser.js +209 -0
  20. package/dist/kar-reader.d.ts +42 -0
  21. package/dist/kar-reader.js +197 -0
  22. package/dist/ncntokar.browser.d.ts +99 -0
  23. package/dist/ncntokar.browser.js +296 -0
  24. package/dist/ncntokar.d.ts +88 -0
  25. package/dist/ncntokar.js +340 -0
  26. package/examples/NextJSComponent.tsx +175 -0
  27. package/libs/emk/client-decoder.ts +142 -0
  28. package/libs/emk/server-decode.ts +133 -0
  29. package/libs/ncntokar.js +256 -0
  30. package/package.json +79 -0
  31. package/songs/.gitkeep +3 -0
  32. package/songs/cur/BPL3457.cur +0 -0
  33. package/songs/cur/Z2510006.cur +0 -0
  34. package/songs/cur/Z2510008.cur +0 -0
  35. package/songs/cur/Z2510136.cur +0 -0
  36. package/songs/cur/Z2510137.cur +0 -0
  37. package/songs/cur/Z2510138.cur +0 -0
  38. package/songs/cur/Z2510139.cur +0 -0
  39. package/songs/cur/Z2510140.cur +0 -0
  40. package/songs/cur/Z2510141.cur +0 -0
  41. package/songs/cur/song.cur +0 -0
  42. package/songs/emk/Z2510001.emk +0 -0
  43. package/songs/emk/Z2510002.emk +0 -0
  44. package/songs/emk/Z2510003.emk +0 -0
  45. package/songs/emk/Z2510004.emk +0 -0
  46. package/songs/emk/Z2510005.emk +0 -0
  47. package/songs/emk/Z2510006.emk +0 -0
  48. package/songs/kar/bpl3457.kar +0 -0
  49. package/songs/kar/z2510006.kar +0 -0
  50. package/songs/kar/z2510008.kar +0 -0
  51. package/songs/kar/z2510136.kar +0 -0
  52. package/songs/kar/z2510137.kar +0 -0
  53. package/songs/kar/z2510138.kar +0 -0
  54. package/songs/kar/z2510139.kar +0 -0
  55. package/songs/kar/z2510140.kar +0 -0
  56. package/songs/kar/z2510141.kar +0 -0
  57. package/songs/lyr/BPL3457.lyr +57 -0
  58. package/songs/lyr/Z2510006.lyr +53 -0
  59. package/songs/lyr/Z2510008.lyr +57 -0
  60. package/songs/lyr/Z2510136.lyr +54 -0
  61. package/songs/lyr/Z2510137.lyr +66 -0
  62. package/songs/lyr/Z2510138.lyr +62 -0
  63. package/songs/lyr/Z2510139.lyr +60 -0
  64. package/songs/lyr/Z2510140.lyr +41 -0
  65. package/songs/lyr/Z2510141.lyr +48 -0
  66. package/songs/lyr/song.lyr +37 -0
  67. package/songs/midi/BPL3457.MID +0 -0
  68. package/songs/midi/Z2510006.mid +0 -0
  69. package/songs/midi/Z2510008.mid +0 -0
  70. package/songs/midi/Z2510136.mid +0 -0
  71. package/songs/midi/Z2510137.mid +0 -0
  72. package/songs/midi/Z2510138.mid +0 -0
  73. package/songs/midi/Z2510139.mid +0 -0
  74. package/songs/midi/Z2510140.mid +0 -0
  75. package/songs/midi/Z2510141.mid +0 -0
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Main decoding logic for .emk (Extreme Karaoke) files.
4
+ * This is the SERVER-SIDE implementation using Node.js's 'zlib'.
5
+ * It is used by the GET /api/emk/import endpoint for playback.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.xorDecrypt = xorDecrypt;
9
+ exports.looksLikeText = looksLikeText;
10
+ exports.decodeEmk = decodeEmk;
11
+ exports.parseSongInfo = parseSongInfo;
12
+ const zlib_1 = require("zlib");
13
+ const XOR_KEY = Buffer.from([0xAF, 0xF2, 0x4C, 0x9C, 0xE9, 0xEA, 0x99, 0x43]);
14
+ const MAGIC_SIGNATURE = '.SFDS';
15
+ const ZLIB_SECOND_BYTES = new Set([0x01, 0x5E, 0x9C, 0xDA, 0x7D, 0x20, 0xBB]);
16
+ function xorDecrypt(data) {
17
+ const decrypted = Buffer.alloc(data.length);
18
+ for (let i = 0; i < data.length; i++) {
19
+ decrypted[i] = data[i] ^ XOR_KEY[i % XOR_KEY.length];
20
+ }
21
+ return decrypted;
22
+ }
23
+ function looksLikeText(buf) {
24
+ const sample = buf.subarray(0, Math.min(64, buf.length));
25
+ for (let i = 0; i < sample.length; i++) {
26
+ const c = sample[i];
27
+ if (c === 0x0a || c === 0x0d || c === 0x09 || (c >= 0x20 && c <= 0x7e) || c >= 0x80) {
28
+ continue;
29
+ }
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ function decodeEmk(fileBuffer) {
35
+ const decryptedBuffer = xorDecrypt(fileBuffer);
36
+ const magic = decryptedBuffer.subarray(0, MAGIC_SIGNATURE.length).toString('utf-8');
37
+ if (magic !== MAGIC_SIGNATURE) {
38
+ throw new Error(`Invalid EMK file signature. Expected '${MAGIC_SIGNATURE}' but got '${magic}'.`);
39
+ }
40
+ const result = {};
41
+ const inflatedParts = [];
42
+ for (let i = 0; i < decryptedBuffer.length - 2; i++) {
43
+ const b0 = decryptedBuffer[i];
44
+ const b1 = decryptedBuffer[i + 1];
45
+ if (b0 !== 0x78 || !ZLIB_SECOND_BYTES.has(b1))
46
+ continue;
47
+ try {
48
+ const inflated = (0, zlib_1.inflateSync)(decryptedBuffer.subarray(i));
49
+ inflatedParts.push(inflated);
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ }
55
+ if (inflatedParts.length < 3) {
56
+ throw new Error(`Invalid EMK structure: expected at least 3 zlib blocks, found ${inflatedParts.length}.`);
57
+ }
58
+ for (const inflated of inflatedParts) {
59
+ const asciiPrefix = inflated.subarray(0, 16).toString('ascii');
60
+ if (asciiPrefix.startsWith('SIGNATURE=')) {
61
+ result.header = inflated;
62
+ }
63
+ else if (asciiPrefix.startsWith('CODE=')) {
64
+ result.songInfo = inflated;
65
+ }
66
+ else if (inflated.subarray(0, 4).toString('ascii') === 'MThd') {
67
+ result.midi = inflated;
68
+ }
69
+ else if (looksLikeText(inflated)) {
70
+ if (!result.lyric) {
71
+ result.lyric = inflated;
72
+ }
73
+ else {
74
+ result.cursor = inflated;
75
+ }
76
+ }
77
+ else {
78
+ if (!result.cursor) {
79
+ result.cursor = inflated;
80
+ }
81
+ }
82
+ }
83
+ if (!result.midi)
84
+ throw new Error('MIDI data block not found in EMK file.');
85
+ if (!result.lyric)
86
+ throw new Error('Lyric data block not found in EMK file.');
87
+ if (!result.cursor)
88
+ throw new Error('Cursor data block not found in EMK file.');
89
+ if (!result.songInfo) {
90
+ result.songInfo = Buffer.from('CODE=\nTITLE=Unknown Title\nARTIST=Unknown Artist');
91
+ }
92
+ return result;
93
+ }
94
+ /**
95
+ * Parses the "song info" block from a Buffer.
96
+ * @param songInfoBuffer The buffer containing the song info data.
97
+ * @returns A record containing TITLE, ARTIST, and CODE.
98
+ */
99
+ function parseSongInfo(songInfoBuffer) {
100
+ if (!songInfoBuffer)
101
+ return {};
102
+ try {
103
+ const text = new TextDecoder('windows-874', { fatal: false }).decode(songInfoBuffer);
104
+ const lines = text.split(/\r?\n/);
105
+ const info = {};
106
+ for (const line of lines) {
107
+ const parts = line.split('=');
108
+ if (parts.length >= 2) {
109
+ const key = parts[0].trim().toUpperCase();
110
+ const value = parts.slice(1).join('=').trim();
111
+ // Only store relevant keys to keep it focused
112
+ if (key === 'TITLE' || key === 'ARTIST' || key === 'CODE' || key === 'title' || key === 'artist' || key === 'code') {
113
+ info[key.toUpperCase()] = value;
114
+ }
115
+ }
116
+ }
117
+ return info;
118
+ }
119
+ catch (e) {
120
+ console.error("Failed to parse song info", e);
121
+ return {};
122
+ }
123
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Browser-compatible EMK to KAR workflow
3
+ * Complete pipeline for client-side processing
4
+ */
5
+ export interface BrowserEmkToKarOptions {
6
+ emkBuffer: Buffer;
7
+ outputFileName?: string;
8
+ autoDownload?: boolean;
9
+ }
10
+ export interface BrowserEmkToKarResult {
11
+ success: boolean;
12
+ karBuffer: Buffer;
13
+ fileName: string;
14
+ intermediateBuffers?: {
15
+ midi: Buffer;
16
+ lyric: Buffer;
17
+ cursor: Buffer;
18
+ };
19
+ metadata: {
20
+ title: string;
21
+ artist: string;
22
+ code?: string;
23
+ };
24
+ warnings: string[];
25
+ }
26
+ /**
27
+ * Browser-compatible EMK to KAR conversion
28
+ * Complete pipeline: EMK buffer → decode → convert → KAR buffer
29
+ */
30
+ export declare function convertEmkToKarBrowser(options: BrowserEmkToKarOptions): BrowserEmkToKarResult;
31
+ /**
32
+ * Process EMK File object directly (for file input in browser)
33
+ */
34
+ export declare function convertEmkFileToKar(emkFile: File, options?: {
35
+ autoDownload?: boolean;
36
+ }): Promise<BrowserEmkToKarResult>;
37
+ /**
38
+ * Process multiple EMK files in the browser
39
+ */
40
+ export declare function convertEmkFilesBatch(emkFiles: File[], options?: {
41
+ autoDownload?: boolean;
42
+ }): Promise<BrowserEmkToKarResult[]>;
43
+ /**
44
+ * Validate Thai text readability from buffer (browser version)
45
+ */
46
+ export declare function validateThaiLyricReadabilityBrowser(lyricBuffer: Buffer): {
47
+ readable: boolean;
48
+ encoding: string;
49
+ preview: string[];
50
+ charCount: number;
51
+ };
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ /**
3
+ * Browser-compatible EMK to KAR workflow
4
+ * Complete pipeline for client-side processing
5
+ */
6
+ 'use client';
7
+ /**
8
+ * Browser-compatible EMK to KAR workflow
9
+ * Complete pipeline for client-side processing
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.convertEmkToKarBrowser = convertEmkToKarBrowser;
13
+ exports.convertEmkFileToKar = convertEmkFileToKar;
14
+ exports.convertEmkFilesBatch = convertEmkFilesBatch;
15
+ exports.validateThaiLyricReadabilityBrowser = validateThaiLyricReadabilityBrowser;
16
+ const client_decoder_1 = require("./emk/client-decoder");
17
+ const ncntokar_browser_1 = require("./ncntokar.browser");
18
+ /**
19
+ * Browser-compatible EMK to KAR conversion
20
+ * Complete pipeline: EMK buffer → decode → convert → KAR buffer
21
+ */
22
+ function convertEmkToKarBrowser(options) {
23
+ const warnings = [];
24
+ try {
25
+ // Step 1: Decode EMK buffer
26
+ console.log('[Browser] [1/3] Decoding EMK buffer...');
27
+ const decoded = (0, client_decoder_1.decodeEmk)(options.emkBuffer);
28
+ // Verify all parts are present
29
+ if (!decoded.midi)
30
+ throw new Error('MIDI data not found in EMK file');
31
+ if (!decoded.lyric)
32
+ throw new Error('Lyric data not found in EMK file');
33
+ if (!decoded.cursor)
34
+ throw new Error('Cursor data not found in EMK file');
35
+ console.log('[Browser] [1/3] ✓ Decoded: MIDI, Lyric, Cursor');
36
+ // Extract metadata from EMK
37
+ const songInfo = (0, client_decoder_1.parseSongInfo)(decoded.songInfo);
38
+ const lyricMetadata = (0, ncntokar_browser_1.parseLyricBuffer)(decoded.lyric);
39
+ const metadata = {
40
+ title: lyricMetadata.title || songInfo.TITLE || 'Unknown Title',
41
+ artist: lyricMetadata.artist || songInfo.ARTIST || 'Unknown Artist',
42
+ code: songInfo.CODE
43
+ };
44
+ console.log(`[Browser] [2/3] Converting to KAR: ${metadata.title} - ${metadata.artist}`);
45
+ // Step 2: Convert NCN buffers to KAR
46
+ const conversionResult = (0, ncntokar_browser_1.convertNcnToKarBrowser)({
47
+ midiBuffer: decoded.midi,
48
+ lyricBuffer: decoded.lyric,
49
+ cursorBuffer: decoded.cursor,
50
+ outputFileName: options.outputFileName || 'output.kar'
51
+ });
52
+ warnings.push(...conversionResult.warnings);
53
+ console.log(`[Browser] [3/3] ✓ KAR buffer created`);
54
+ // Optionally auto-download
55
+ if (options.autoDownload) {
56
+ (0, ncntokar_browser_1.downloadBuffer)(conversionResult.karBuffer, conversionResult.fileName);
57
+ }
58
+ return {
59
+ success: true,
60
+ karBuffer: conversionResult.karBuffer,
61
+ fileName: conversionResult.fileName,
62
+ intermediateBuffers: {
63
+ midi: decoded.midi,
64
+ lyric: decoded.lyric,
65
+ cursor: decoded.cursor
66
+ },
67
+ metadata,
68
+ warnings
69
+ };
70
+ }
71
+ catch (error) {
72
+ throw new Error(`Browser EMK to KAR conversion failed: ${error.message}`);
73
+ }
74
+ }
75
+ /**
76
+ * Process EMK File object directly (for file input in browser)
77
+ */
78
+ async function convertEmkFileToKar(emkFile, options) {
79
+ const emkBuffer = await (0, ncntokar_browser_1.fileToBuffer)(emkFile);
80
+ const fileName = emkFile.name.replace('.emk', '.kar');
81
+ return convertEmkToKarBrowser({
82
+ emkBuffer,
83
+ outputFileName: fileName,
84
+ autoDownload: options?.autoDownload ?? true
85
+ });
86
+ }
87
+ /**
88
+ * Process multiple EMK files in the browser
89
+ */
90
+ async function convertEmkFilesBatch(emkFiles, options) {
91
+ const results = [];
92
+ for (const emkFile of emkFiles) {
93
+ try {
94
+ const result = await convertEmkFileToKar(emkFile, {
95
+ autoDownload: options?.autoDownload ?? false
96
+ });
97
+ results.push(result);
98
+ console.log(`✓ ${emkFile.name}: ${result.metadata.title}`);
99
+ }
100
+ catch (error) {
101
+ console.error(`✗ ${emkFile.name}: ${error.message}`);
102
+ results.push({
103
+ success: false,
104
+ karBuffer: Buffer.alloc(0),
105
+ fileName: emkFile.name.replace('.emk', '.kar'),
106
+ metadata: {
107
+ title: 'Error',
108
+ artist: 'Error'
109
+ },
110
+ warnings: [error.message]
111
+ });
112
+ }
113
+ }
114
+ // Download all if requested
115
+ if (options?.autoDownload) {
116
+ results
117
+ .filter(r => r.success)
118
+ .forEach(r => (0, ncntokar_browser_1.downloadBuffer)(r.karBuffer, r.fileName));
119
+ }
120
+ return results;
121
+ }
122
+ /**
123
+ * Validate Thai text readability from buffer (browser version)
124
+ */
125
+ function validateThaiLyricReadabilityBrowser(lyricBuffer) {
126
+ const iconv = require('iconv-lite');
127
+ const text = iconv.decode(lyricBuffer, 'tis-620');
128
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
129
+ const thaiCharRegex = /[\u0E00-\u0E7F]/;
130
+ const hasThaiChars = thaiCharRegex.test(text);
131
+ const controlCharCount = (text.match(/[\x00-\x08\x0B-\x0C\x0E-\x1F]/g) || []).length;
132
+ const isReadable = controlCharCount < (text.length * 0.1);
133
+ return {
134
+ readable: isReadable,
135
+ encoding: 'TIS-620',
136
+ preview: lines.slice(0, 5),
137
+ charCount: text.length
138
+ };
139
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * EMK to KAR Workflow
3
+ * Complete pipeline: EMK file → decode → MIDI + LYR + CUR → convert → KAR file
4
+ */
5
+ export interface EmkToKarOptions {
6
+ inputEmk: string;
7
+ outputKar: string;
8
+ outputDir?: string;
9
+ keepIntermediateFiles?: boolean;
10
+ appendTitles?: boolean;
11
+ }
12
+ export interface EmkToKarResult {
13
+ success: boolean;
14
+ outputKar: string;
15
+ intermediateFiles?: {
16
+ midi: string;
17
+ lyric: string;
18
+ cursor: string;
19
+ };
20
+ metadata: {
21
+ title: string;
22
+ artist: string;
23
+ code?: string;
24
+ };
25
+ warnings: string[];
26
+ }
27
+ /**
28
+ * Complete workflow: Converts EMK file directly to KAR file
29
+ *
30
+ * Pipeline:
31
+ * 1. Decode EMK → MIDI + Lyric + Cursor files
32
+ * 2. Convert MIDI + Lyric + Cursor → KAR file
33
+ *
34
+ * @param options Configuration options
35
+ * @returns Result with metadata and warnings
36
+ */
37
+ export declare function convertEmkToKar(options: EmkToKarOptions): EmkToKarResult;
38
+ /**
39
+ * Batch convert multiple EMK files to KAR
40
+ */
41
+ export declare function convertEmkToKarBatch(emkFiles: string[], outputDirectory: string, options?: {
42
+ keepIntermediateFiles?: boolean;
43
+ appendTitles?: boolean;
44
+ }): EmkToKarResult[];
45
+ /**
46
+ * Validate Thai text readability from lyric buffer
47
+ */
48
+ export declare function validateThaiLyricReadability(lyricBuffer: Buffer): {
49
+ readable: boolean;
50
+ encoding: string;
51
+ preview: string[];
52
+ charCount: number;
53
+ };
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ /**
3
+ * EMK to KAR Workflow
4
+ * Complete pipeline: EMK file → decode → MIDI + LYR + CUR → convert → KAR file
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.convertEmkToKar = convertEmkToKar;
41
+ exports.convertEmkToKarBatch = convertEmkToKarBatch;
42
+ exports.validateThaiLyricReadability = validateThaiLyricReadability;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const server_decode_1 = require("./emk/server-decode");
46
+ const ncntokar_1 = require("./ncntokar");
47
+ const iconv = __importStar(require("iconv-lite"));
48
+ /**
49
+ * Complete workflow: Converts EMK file directly to KAR file
50
+ *
51
+ * Pipeline:
52
+ * 1. Decode EMK → MIDI + Lyric + Cursor files
53
+ * 2. Convert MIDI + Lyric + Cursor → KAR file
54
+ *
55
+ * @param options Configuration options
56
+ * @returns Result with metadata and warnings
57
+ */
58
+ function convertEmkToKar(options) {
59
+ const warnings = [];
60
+ // Validate input
61
+ if (!fs.existsSync(options.inputEmk)) {
62
+ throw new Error(`Input EMK file not found: ${options.inputEmk}`);
63
+ }
64
+ // Determine output directory for intermediate files
65
+ const outputDir = options.outputDir || path.join(path.dirname(options.outputKar), 'temp');
66
+ // Create temp directory if needed
67
+ if (!fs.existsSync(outputDir)) {
68
+ fs.mkdirSync(outputDir, { recursive: true });
69
+ }
70
+ const baseFileName = path.basename(options.inputEmk, '.emk');
71
+ const midiFile = path.join(outputDir, `${baseFileName}.mid`);
72
+ const lyricFile = path.join(outputDir, `${baseFileName}.lyr`);
73
+ const cursorFile = path.join(outputDir, `${baseFileName}.cur`);
74
+ try {
75
+ // Step 1: Decode EMK file
76
+ console.log(`[1/3] Decoding EMK file: ${options.inputEmk}`);
77
+ const emkBuffer = fs.readFileSync(options.inputEmk);
78
+ const decoded = (0, server_decode_1.decodeEmk)(emkBuffer);
79
+ // Verify all parts are present
80
+ if (!decoded.midi)
81
+ throw new Error('MIDI data not found in EMK file');
82
+ if (!decoded.lyric)
83
+ throw new Error('Lyric data not found in EMK file');
84
+ if (!decoded.cursor)
85
+ throw new Error('Cursor data not found in EMK file');
86
+ // Write intermediate files
87
+ fs.writeFileSync(midiFile, decoded.midi);
88
+ fs.writeFileSync(lyricFile, decoded.lyric);
89
+ fs.writeFileSync(cursorFile, decoded.cursor);
90
+ console.log(`[1/3] ✓ Decoded to: MIDI, Lyric, Cursor`);
91
+ // Extract metadata from EMK
92
+ const songInfo = (0, server_decode_1.parseSongInfo)(decoded.songInfo);
93
+ // Parse lyric file to get title and artist (more reliable)
94
+ const lyricMetadata = (0, ncntokar_1.parseLyricFile)(lyricFile);
95
+ const metadata = {
96
+ title: lyricMetadata.title || songInfo.TITLE || 'Unknown Title',
97
+ artist: lyricMetadata.artist || songInfo.ARTIST || 'Unknown Artist',
98
+ code: songInfo.CODE
99
+ };
100
+ console.log(`[2/3] Converting to KAR: ${metadata.title} - ${metadata.artist}`);
101
+ // Step 2: Convert NCN to KAR
102
+ const conversionResult = (0, ncntokar_1.convertNcnToKar)({
103
+ inputMidi: midiFile,
104
+ inputLyr: lyricFile,
105
+ inputCur: cursorFile,
106
+ outputKar: options.outputKar,
107
+ appendTitles: options.appendTitles || false
108
+ });
109
+ warnings.push(...conversionResult.warnings);
110
+ console.log(`[3/3] ✓ KAR file created: ${options.outputKar}`);
111
+ // Clean up intermediate files if requested
112
+ if (!options.keepIntermediateFiles) {
113
+ fs.unlinkSync(midiFile);
114
+ fs.unlinkSync(lyricFile);
115
+ fs.unlinkSync(cursorFile);
116
+ // Remove temp directory if empty
117
+ try {
118
+ fs.rmdirSync(outputDir);
119
+ }
120
+ catch {
121
+ // Directory not empty or doesn't exist, ignore
122
+ }
123
+ }
124
+ return {
125
+ success: true,
126
+ outputKar: options.outputKar,
127
+ intermediateFiles: options.keepIntermediateFiles ? {
128
+ midi: midiFile,
129
+ lyric: lyricFile,
130
+ cursor: cursorFile
131
+ } : undefined,
132
+ metadata,
133
+ warnings
134
+ };
135
+ }
136
+ catch (error) {
137
+ // Clean up on error
138
+ try {
139
+ if (fs.existsSync(midiFile))
140
+ fs.unlinkSync(midiFile);
141
+ if (fs.existsSync(lyricFile))
142
+ fs.unlinkSync(lyricFile);
143
+ if (fs.existsSync(cursorFile))
144
+ fs.unlinkSync(cursorFile);
145
+ if (fs.existsSync(outputDir))
146
+ fs.rmdirSync(outputDir);
147
+ }
148
+ catch {
149
+ // Ignore cleanup errors
150
+ }
151
+ throw new Error(`EMK to KAR conversion failed: ${error.message}`);
152
+ }
153
+ }
154
+ /**
155
+ * Batch convert multiple EMK files to KAR
156
+ */
157
+ function convertEmkToKarBatch(emkFiles, outputDirectory, options) {
158
+ const results = [];
159
+ // Create output directory
160
+ if (!fs.existsSync(outputDirectory)) {
161
+ fs.mkdirSync(outputDirectory, { recursive: true });
162
+ }
163
+ for (const emkFile of emkFiles) {
164
+ const baseFileName = path.basename(emkFile, '.emk');
165
+ const outputKar = path.join(outputDirectory, `${baseFileName}.kar`);
166
+ try {
167
+ const result = convertEmkToKar({
168
+ inputEmk: emkFile,
169
+ outputKar,
170
+ keepIntermediateFiles: options?.keepIntermediateFiles || false,
171
+ appendTitles: options?.appendTitles || false
172
+ });
173
+ results.push(result);
174
+ console.log(`✓ ${baseFileName}: ${result.metadata.title}`);
175
+ }
176
+ catch (error) {
177
+ console.error(`✗ ${baseFileName}: ${error.message}`);
178
+ results.push({
179
+ success: false,
180
+ outputKar,
181
+ metadata: {
182
+ title: 'Error',
183
+ artist: 'Error'
184
+ },
185
+ warnings: [error.message]
186
+ });
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+ /**
192
+ * Validate Thai text readability from lyric buffer
193
+ */
194
+ function validateThaiLyricReadability(lyricBuffer) {
195
+ // Try TIS-620 decoding
196
+ const text = iconv.decode(lyricBuffer, 'tis-620');
197
+ const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
198
+ // Check if text contains Thai characters
199
+ const thaiCharRegex = /[\u0E00-\u0E7F]/;
200
+ const hasThaiChars = thaiCharRegex.test(text);
201
+ // Check if text is readable (not too many control characters)
202
+ const controlCharCount = (text.match(/[\x00-\x08\x0B-\x0C\x0E-\x1F]/g) || []).length;
203
+ const isReadable = controlCharCount < (text.length * 0.1); // Less than 10% control chars
204
+ return {
205
+ readable: isReadable,
206
+ encoding: 'TIS-620',
207
+ preview: lines.slice(0, 5),
208
+ charCount: text.length
209
+ };
210
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * file-coder - A comprehensive library for encoding/decoding karaoke files
3
+ * Supports .emk (Extreme Karaoke), .kar (MIDI karaoke), and NCN format conversions
4
+ */
5
+ export { convertNcnToKar, convertWithDefaults, parseLyricFile, buildKaraokeTrack, buildMetadataTracks, readFileTextTIS620, splitLinesKeepEndings, trimLineEndings, metaEvent, endOfTrack, ensureReadableFile, ensureOutputDoesNotExist, CursorReader, type ConversionOptions, type SongMetadata } from './ncntokar';
6
+ export { readKarFile, validateKarFile, extractLyricsFromKar, type KarFileInfo, type KarTrack } from './kar-reader';
7
+ export { convertEmkToKar, convertEmkToKarBatch, validateThaiLyricReadability, type EmkToKarOptions, type EmkToKarResult } from './emk-to-kar';
8
+ export { decodeEmk as decodeEmkServer, parseSongInfo as parseSongInfoServer, xorDecrypt as xorDecryptServer, looksLikeText as looksLikeTextServer, type DecodedEmkParts as DecodedEmkPartsServer } from './emk/server-decode';
9
+ export { decodeEmk as decodeEmkClient, parseSongInfo as parseSongInfoClient, xorDecrypt as xorDecryptClient, looksLikeText as looksLikeTextClient, type DecodedEmkParts as DecodedEmkPartsClient } from './emk/client-decoder';
10
+ export { convertNcnToKarBrowser, parseLyricBuffer, buildKaraokeTrackBrowser, buildMetadataTracksBrowser, fileToBuffer, downloadBuffer, type BrowserConversionOptions, type BrowserConversionResult, } from './ncntokar.browser';
11
+ export { convertEmkToKarBrowser, convertEmkFileToKar, convertEmkFilesBatch, validateThaiLyricReadabilityBrowser, type BrowserEmkToKarOptions, type BrowserEmkToKarResult, } from './emk-to-kar.browser';
12
+ export { readKarBuffer, validateKarBuffer, extractLyricsFromKarBuffer, readKarFile as readKarFileFromBrowser, type KarFileInfo as BrowserKarFileInfo, type KarTrack as BrowserKarTrack, } from './kar-reader.browser';
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * file-coder - A comprehensive library for encoding/decoding karaoke files
4
+ * Supports .emk (Extreme Karaoke), .kar (MIDI karaoke), and NCN format conversions
5
+ */
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;
8
+ // NCN to KAR converter exports
9
+ var ncntokar_1 = require("./ncntokar");
10
+ Object.defineProperty(exports, "convertNcnToKar", { enumerable: true, get: function () { return ncntokar_1.convertNcnToKar; } });
11
+ Object.defineProperty(exports, "convertWithDefaults", { enumerable: true, get: function () { return ncntokar_1.convertWithDefaults; } });
12
+ Object.defineProperty(exports, "parseLyricFile", { enumerable: true, get: function () { return ncntokar_1.parseLyricFile; } });
13
+ Object.defineProperty(exports, "buildKaraokeTrack", { enumerable: true, get: function () { return ncntokar_1.buildKaraokeTrack; } });
14
+ Object.defineProperty(exports, "buildMetadataTracks", { enumerable: true, get: function () { return ncntokar_1.buildMetadataTracks; } });
15
+ Object.defineProperty(exports, "readFileTextTIS620", { enumerable: true, get: function () { return ncntokar_1.readFileTextTIS620; } });
16
+ Object.defineProperty(exports, "splitLinesKeepEndings", { enumerable: true, get: function () { return ncntokar_1.splitLinesKeepEndings; } });
17
+ Object.defineProperty(exports, "trimLineEndings", { enumerable: true, get: function () { return ncntokar_1.trimLineEndings; } });
18
+ Object.defineProperty(exports, "metaEvent", { enumerable: true, get: function () { return ncntokar_1.metaEvent; } });
19
+ Object.defineProperty(exports, "endOfTrack", { enumerable: true, get: function () { return ncntokar_1.endOfTrack; } });
20
+ Object.defineProperty(exports, "ensureReadableFile", { enumerable: true, get: function () { return ncntokar_1.ensureReadableFile; } });
21
+ Object.defineProperty(exports, "ensureOutputDoesNotExist", { enumerable: true, get: function () { return ncntokar_1.ensureOutputDoesNotExist; } });
22
+ Object.defineProperty(exports, "CursorReader", { enumerable: true, get: function () { return ncntokar_1.CursorReader; } });
23
+ // KAR file reader exports
24
+ var kar_reader_1 = require("./kar-reader");
25
+ Object.defineProperty(exports, "readKarFile", { enumerable: true, get: function () { return kar_reader_1.readKarFile; } });
26
+ Object.defineProperty(exports, "validateKarFile", { enumerable: true, get: function () { return kar_reader_1.validateKarFile; } });
27
+ Object.defineProperty(exports, "extractLyricsFromKar", { enumerable: true, get: function () { return kar_reader_1.extractLyricsFromKar; } });
28
+ // EMK to KAR workflow exports (complete pipeline)
29
+ var emk_to_kar_1 = require("./emk-to-kar");
30
+ Object.defineProperty(exports, "convertEmkToKar", { enumerable: true, get: function () { return emk_to_kar_1.convertEmkToKar; } });
31
+ Object.defineProperty(exports, "convertEmkToKarBatch", { enumerable: true, get: function () { return emk_to_kar_1.convertEmkToKarBatch; } });
32
+ Object.defineProperty(exports, "validateThaiLyricReadability", { enumerable: true, get: function () { return emk_to_kar_1.validateThaiLyricReadability; } });
33
+ // EMK server-side decoder exports
34
+ var server_decode_1 = require("./emk/server-decode");
35
+ Object.defineProperty(exports, "decodeEmkServer", { enumerable: true, get: function () { return server_decode_1.decodeEmk; } });
36
+ Object.defineProperty(exports, "parseSongInfoServer", { enumerable: true, get: function () { return server_decode_1.parseSongInfo; } });
37
+ Object.defineProperty(exports, "xorDecryptServer", { enumerable: true, get: function () { return server_decode_1.xorDecrypt; } });
38
+ Object.defineProperty(exports, "looksLikeTextServer", { enumerable: true, get: function () { return server_decode_1.looksLikeText; } });
39
+ // EMK client-side decoder exports
40
+ var client_decoder_1 = require("./emk/client-decoder");
41
+ Object.defineProperty(exports, "decodeEmkClient", { enumerable: true, get: function () { return client_decoder_1.decodeEmk; } });
42
+ Object.defineProperty(exports, "parseSongInfoClient", { enumerable: true, get: function () { return client_decoder_1.parseSongInfo; } });
43
+ Object.defineProperty(exports, "xorDecryptClient", { enumerable: true, get: function () { return client_decoder_1.xorDecrypt; } });
44
+ Object.defineProperty(exports, "looksLikeTextClient", { enumerable: true, get: function () { return client_decoder_1.looksLikeText; } });
45
+ // Browser-compatible NCN to KAR conversion
46
+ var ncntokar_browser_1 = require("./ncntokar.browser");
47
+ Object.defineProperty(exports, "convertNcnToKarBrowser", { enumerable: true, get: function () { return ncntokar_browser_1.convertNcnToKarBrowser; } });
48
+ Object.defineProperty(exports, "parseLyricBuffer", { enumerable: true, get: function () { return ncntokar_browser_1.parseLyricBuffer; } });
49
+ Object.defineProperty(exports, "buildKaraokeTrackBrowser", { enumerable: true, get: function () { return ncntokar_browser_1.buildKaraokeTrackBrowser; } });
50
+ Object.defineProperty(exports, "buildMetadataTracksBrowser", { enumerable: true, get: function () { return ncntokar_browser_1.buildMetadataTracksBrowser; } });
51
+ Object.defineProperty(exports, "fileToBuffer", { enumerable: true, get: function () { return ncntokar_browser_1.fileToBuffer; } });
52
+ Object.defineProperty(exports, "downloadBuffer", { enumerable: true, get: function () { return ncntokar_browser_1.downloadBuffer; } });
53
+ // Browser-compatible EMK to KAR workflow
54
+ var emk_to_kar_browser_1 = require("./emk-to-kar.browser");
55
+ Object.defineProperty(exports, "convertEmkToKarBrowser", { enumerable: true, get: function () { return emk_to_kar_browser_1.convertEmkToKarBrowser; } });
56
+ Object.defineProperty(exports, "convertEmkFileToKar", { enumerable: true, get: function () { return emk_to_kar_browser_1.convertEmkFileToKar; } });
57
+ Object.defineProperty(exports, "convertEmkFilesBatch", { enumerable: true, get: function () { return emk_to_kar_browser_1.convertEmkFilesBatch; } });
58
+ Object.defineProperty(exports, "validateThaiLyricReadabilityBrowser", { enumerable: true, get: function () { return emk_to_kar_browser_1.validateThaiLyricReadabilityBrowser; } });
59
+ // Browser-compatible KAR reader
60
+ var kar_reader_browser_1 = require("./kar-reader.browser");
61
+ Object.defineProperty(exports, "readKarBuffer", { enumerable: true, get: function () { return kar_reader_browser_1.readKarBuffer; } });
62
+ Object.defineProperty(exports, "validateKarBuffer", { enumerable: true, get: function () { return kar_reader_browser_1.validateKarBuffer; } });
63
+ Object.defineProperty(exports, "extractLyricsFromKarBuffer", { enumerable: true, get: function () { return kar_reader_browser_1.extractLyricsFromKarBuffer; } });
64
+ Object.defineProperty(exports, "readKarFileFromBrowser", { enumerable: true, get: function () { return kar_reader_browser_1.readKarFile; } });