@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,296 @@
1
+ "use strict";
2
+ /**
3
+ * Browser-compatible NCN to KAR converter
4
+ * Works with File API and ArrayBuffers instead of fs
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.BrowserCursorReader = void 0;
44
+ exports.readBufferTextTIS620 = readBufferTextTIS620;
45
+ exports.splitLinesKeepEndings = splitLinesKeepEndings;
46
+ exports.trimLineEndings = trimLineEndings;
47
+ exports.createMetaEvent = createMetaEvent;
48
+ exports.createEndOfTrack = createEndOfTrack;
49
+ exports.parseLyricBuffer = parseLyricBuffer;
50
+ exports.buildKaraokeTrackBrowser = buildKaraokeTrackBrowser;
51
+ exports.buildMetadataTracksBrowser = buildMetadataTracksBrowser;
52
+ exports.convertNcnToKarBrowser = convertNcnToKarBrowser;
53
+ exports.fileToBuffer = fileToBuffer;
54
+ exports.downloadBuffer = downloadBuffer;
55
+ const iconv = __importStar(require("iconv-lite"));
56
+ const grapheme_splitter_1 = __importDefault(require("grapheme-splitter"));
57
+ const midi_file_1 = require("midi-file");
58
+ /**
59
+ * Reads text from buffer as TIS-620 (Thai encoding)
60
+ */
61
+ function readBufferTextTIS620(buffer) {
62
+ return iconv.decode(buffer, "tis-620");
63
+ }
64
+ /**
65
+ * Splits text into lines while keeping line endings
66
+ */
67
+ function splitLinesKeepEndings(text) {
68
+ return text.match(/[^\n]*\n|[^\n]+$/g) || [];
69
+ }
70
+ /**
71
+ * Removes line endings from a string
72
+ */
73
+ function trimLineEndings(s) {
74
+ return s.replace(/[\r\n]+$/g, "");
75
+ }
76
+ /**
77
+ * Creates a MIDI meta event with TIS-620 encoding for Thai text
78
+ */
79
+ function createMetaEvent(subtype, deltaTime, text) {
80
+ const tis620Bytes = iconv.encode(text, 'tis-620');
81
+ const metaTypeMap = {
82
+ 'text': 0x01,
83
+ 'trackName': 0x03,
84
+ 'copyrightNotice': 0x02,
85
+ 'instrumentName': 0x04,
86
+ 'lyrics': 0x05,
87
+ 'marker': 0x06,
88
+ 'cuePoint': 0x07,
89
+ };
90
+ const metatypeByte = metaTypeMap[subtype];
91
+ if (metatypeByte !== undefined) {
92
+ return {
93
+ deltaTime,
94
+ type: 'unknownMeta',
95
+ metatypeByte: metatypeByte,
96
+ data: Array.from(tis620Bytes)
97
+ };
98
+ }
99
+ return { deltaTime, type: subtype, text };
100
+ }
101
+ /**
102
+ * Creates an end-of-track MIDI event
103
+ */
104
+ function createEndOfTrack(deltaTime = 0) {
105
+ return { deltaTime, type: "endOfTrack" };
106
+ }
107
+ /**
108
+ * Simple cursor reader for browser
109
+ */
110
+ class BrowserCursorReader {
111
+ constructor(buf) {
112
+ this.buf = buf;
113
+ this.off = 0;
114
+ }
115
+ readU16LE() {
116
+ if (this.off + 2 > this.buf.length)
117
+ return null;
118
+ const v = this.buf.readUInt16LE(this.off);
119
+ this.off += 2;
120
+ return v;
121
+ }
122
+ readU8() {
123
+ if (this.off + 1 > this.buf.length)
124
+ return null;
125
+ const v = this.buf[this.off];
126
+ this.off += 1;
127
+ return v;
128
+ }
129
+ remaining() {
130
+ return this.buf.length - this.off;
131
+ }
132
+ }
133
+ exports.BrowserCursorReader = BrowserCursorReader;
134
+ /**
135
+ * Parses lyric buffer to extract metadata (title, artist, lyrics)
136
+ */
137
+ function parseLyricBuffer(lyricBuffer) {
138
+ const lyricText = readBufferTextTIS620(lyricBuffer);
139
+ const linesWithEndings = splitLinesKeepEndings(lyricText);
140
+ if (linesWithEndings.length < 2) {
141
+ throw new Error("Lyric file has too few lines (need title and artist).");
142
+ }
143
+ const songTitle = trimLineEndings(linesWithEndings[0] ?? "");
144
+ const artistName = trimLineEndings(linesWithEndings[1] ?? "");
145
+ let fullLyric = "";
146
+ const lyricLines = [];
147
+ for (let i = 4; i < linesWithEndings.length; i++) {
148
+ fullLyric += linesWithEndings[i];
149
+ const trimmed = trimLineEndings(linesWithEndings[i]);
150
+ if (trimmed && trimmed.length > 0) {
151
+ lyricLines.push(trimmed);
152
+ }
153
+ }
154
+ return {
155
+ title: songTitle,
156
+ artist: artistName,
157
+ fullLyric,
158
+ lines: lyricLines
159
+ };
160
+ }
161
+ /**
162
+ * Builds karaoke track with timing information (browser version)
163
+ */
164
+ function buildKaraokeTrackBrowser(metadata, cursorBuffer, ticksPerBeat) {
165
+ const karaokeTrack = [];
166
+ const warnings = [];
167
+ karaokeTrack.push(createMetaEvent("trackName", 0, "Words"));
168
+ karaokeTrack.push(createMetaEvent("text", 0, "@T" + metadata.title));
169
+ karaokeTrack.push(createMetaEvent("text", 0, "@T" + metadata.artist));
170
+ const cursor = new BrowserCursorReader(cursorBuffer);
171
+ const splitter = new grapheme_splitter_1.default();
172
+ let previousAbsoluteTimestamp = 0;
173
+ const lyricsWithEndings = splitLinesKeepEndings(metadata.fullLyric);
174
+ for (let i = 4; i < lyricsWithEndings.length; i++) {
175
+ const trimmed = trimLineEndings(lyricsWithEndings[i]);
176
+ if (!trimmed || trimmed.length === 0)
177
+ continue;
178
+ const lineForTiming = "/" + trimmed;
179
+ const graphemes = splitter.splitGraphemes(lineForTiming);
180
+ for (const g of graphemes) {
181
+ let absoluteTimestamp = previousAbsoluteTimestamp;
182
+ for (const _cp of Array.from(g)) {
183
+ const v = cursor.readU16LE();
184
+ if (v === null) {
185
+ warnings.push("ran out of timing info; reusing previous timestamp");
186
+ absoluteTimestamp = previousAbsoluteTimestamp;
187
+ }
188
+ else {
189
+ absoluteTimestamp = v;
190
+ }
191
+ }
192
+ absoluteTimestamp = Math.floor(absoluteTimestamp * (ticksPerBeat / 24));
193
+ if (absoluteTimestamp < previousAbsoluteTimestamp) {
194
+ warnings.push("timestamp out of order - clamping");
195
+ absoluteTimestamp = previousAbsoluteTimestamp;
196
+ }
197
+ const relativeTimestamp = absoluteTimestamp - previousAbsoluteTimestamp;
198
+ karaokeTrack.push(createMetaEvent("text", relativeTimestamp, g));
199
+ previousAbsoluteTimestamp = absoluteTimestamp;
200
+ }
201
+ }
202
+ const terminator = cursor.readU8();
203
+ if (terminator === null) {
204
+ warnings.push("EOF without terminator while reading timing info");
205
+ }
206
+ else {
207
+ const leftover = cursor.remaining();
208
+ if (terminator !== 0xff || leftover > 0) {
209
+ warnings.push(`${leftover} bytes (${leftover / 2} values) of unused timing info, or timing info ended without 0xFF terminator`);
210
+ }
211
+ }
212
+ karaokeTrack.push(createEndOfTrack(0));
213
+ return { track: karaokeTrack, warnings };
214
+ }
215
+ /**
216
+ * Builds additional metadata tracks (Lyric, Artist, SongTitle)
217
+ */
218
+ function buildMetadataTracksBrowser(metadata) {
219
+ const lyricTrack = [
220
+ createMetaEvent("trackName", 0, "Lyric"),
221
+ createMetaEvent("text", 0, metadata.fullLyric),
222
+ createEndOfTrack(0),
223
+ ];
224
+ const artistTrack = [
225
+ createMetaEvent("trackName", 0, "Artist"),
226
+ createMetaEvent("text", 0, metadata.artist),
227
+ createEndOfTrack(0),
228
+ ];
229
+ const titleTrack = [
230
+ createMetaEvent("trackName", 0, "SongTitle"),
231
+ createMetaEvent("text", 0, metadata.title),
232
+ createEndOfTrack(0),
233
+ ];
234
+ return { lyricTrack, artistTrack, titleTrack };
235
+ }
236
+ /**
237
+ * Browser-compatible NCN to KAR conversion
238
+ * Works with buffers instead of file paths
239
+ */
240
+ function convertNcnToKarBrowser(options) {
241
+ const warnings = [];
242
+ try {
243
+ // Parse MIDI
244
+ const midi = (0, midi_file_1.parseMidi)(options.midiBuffer);
245
+ const ticksPerBeat = midi.header && midi.header.ticksPerBeat;
246
+ if (!ticksPerBeat) {
247
+ throw new Error("Only ticks-per-beat MIDI timing is supported (SMPTE timing not supported).");
248
+ }
249
+ // Parse lyric buffer
250
+ const metadata = parseLyricBuffer(options.lyricBuffer);
251
+ // Build karaoke track
252
+ const { track: karaokeTrack, warnings: karaokeWarnings } = buildKaraokeTrackBrowser(metadata, options.cursorBuffer, ticksPerBeat);
253
+ warnings.push(...karaokeWarnings);
254
+ // Build metadata tracks
255
+ const { lyricTrack, artistTrack, titleTrack } = buildMetadataTracksBrowser(metadata);
256
+ // Insert tracks as second track (index 1)
257
+ const originalTracks = midi.tracks || [];
258
+ const newTracks = originalTracks.slice();
259
+ newTracks.splice(1, 0, karaokeTrack, lyricTrack, artistTrack, titleTrack);
260
+ midi.tracks = newTracks;
261
+ // Write output
262
+ const outBytes = (0, midi_file_1.writeMidi)(midi);
263
+ const karBuffer = Buffer.from(outBytes);
264
+ return {
265
+ success: true,
266
+ karBuffer,
267
+ fileName: options.outputFileName || 'output.kar',
268
+ metadata,
269
+ warnings
270
+ };
271
+ }
272
+ catch (error) {
273
+ throw new Error(`Browser NCN to KAR conversion failed: ${error.message}`);
274
+ }
275
+ }
276
+ /**
277
+ * Helper to convert File to Buffer for browser use
278
+ */
279
+ async function fileToBuffer(file) {
280
+ const arrayBuffer = await file.arrayBuffer();
281
+ return Buffer.from(arrayBuffer);
282
+ }
283
+ /**
284
+ * Helper to download buffer as file in browser
285
+ */
286
+ function downloadBuffer(buffer, fileName, mimeType = 'audio/midi') {
287
+ const blob = new Blob([new Uint8Array(buffer)], { type: mimeType });
288
+ const url = URL.createObjectURL(blob);
289
+ const a = document.createElement('a');
290
+ a.href = url;
291
+ a.download = fileName;
292
+ document.body.appendChild(a);
293
+ a.click();
294
+ document.body.removeChild(a);
295
+ URL.revokeObjectURL(url);
296
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * NCN (.mid + .lyr + .cur) -> .kar (MIDI with embedded karaoke/lyric/title/artist tracks)
3
+ * - .lyr decoded as TIS-620 (Thai encoding)
4
+ * - meta events use midi-file format: { deltaTime, type: <subtype>, text }
5
+ */
6
+ export interface ConversionOptions {
7
+ inputMidi: string;
8
+ inputLyr: string;
9
+ inputCur: string;
10
+ outputKar: string;
11
+ appendTitles?: boolean;
12
+ titlesFile?: string;
13
+ }
14
+ export interface SongMetadata {
15
+ title: string;
16
+ artist: string;
17
+ fullLyric: string;
18
+ lines: string[];
19
+ }
20
+ export declare class CursorReader {
21
+ private buf;
22
+ private off;
23
+ constructor(buf: Buffer);
24
+ readU16LE(): number | null;
25
+ readU8(): number | null;
26
+ remaining(): number;
27
+ eof(): boolean;
28
+ }
29
+ /**
30
+ * Reads a file and decodes it as TIS-620 (Thai encoding)
31
+ */
32
+ export declare function readFileTextTIS620(filePath: string): string;
33
+ /**
34
+ * Splits text into lines while keeping line endings
35
+ */
36
+ export declare function splitLinesKeepEndings(text: string): string[];
37
+ /**
38
+ * Removes line endings from a string
39
+ */
40
+ export declare function trimLineEndings(s: string): string;
41
+ /**
42
+ * Creates a MIDI meta event with TIS-620 encoding for Thai text
43
+ */
44
+ export declare function metaEvent(subtype: string, deltaTime: number, text: string): any;
45
+ /**
46
+ * Creates an end-of-track MIDI event
47
+ */
48
+ export declare function endOfTrack(deltaTime?: number): any;
49
+ /**
50
+ * Validates that a file exists and is readable
51
+ */
52
+ export declare function ensureReadableFile(filePath: string): void;
53
+ /**
54
+ * Ensures output file doesn't exist and creates directory if needed
55
+ */
56
+ export declare function ensureOutputDoesNotExist(filePath: string, createDir?: boolean): void;
57
+ /**
58
+ * Parses lyric file and extracts metadata (title, artist, lyrics)
59
+ */
60
+ export declare function parseLyricFile(lyricFilePath: string): SongMetadata;
61
+ /**
62
+ * Builds karaoke track with timing information
63
+ */
64
+ export declare function buildKaraokeTrack(metadata: SongMetadata, cursorBuffer: Buffer, ticksPerBeat: number): {
65
+ track: any[];
66
+ warnings: string[];
67
+ };
68
+ /**
69
+ * Builds additional metadata tracks (Lyric, Artist, SongTitle)
70
+ */
71
+ export declare function buildMetadataTracks(metadata: SongMetadata): {
72
+ lyricTrack: any[];
73
+ artistTrack: any[];
74
+ titleTrack: any[];
75
+ };
76
+ /**
77
+ * Main conversion function: converts NCN files (.mid + .lyr + .cur) to .kar format
78
+ */
79
+ export declare function convertNcnToKar(options: ConversionOptions): {
80
+ success: boolean;
81
+ outputFile: string;
82
+ warnings: string[];
83
+ metadata: SongMetadata;
84
+ };
85
+ /**
86
+ * CLI-compatible function that uses hard-coded paths (for backward compatibility)
87
+ */
88
+ export declare function convertWithDefaults(inputMidi: string, inputLyr: string, inputCur: string, outputKar: string): void;