@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.
- package/BROWSER_API.md +318 -0
- package/README.md +216 -0
- package/REFACTORING_SUMMARY.txt +250 -0
- package/WORKFLOW_SUMMARY.txt +78 -0
- package/bin/ncntokar-cli.js +39 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +74 -0
- package/dist/emk/client-decoder.d.ts +22 -0
- package/dist/emk/client-decoder.js +133 -0
- package/dist/emk/server-decode.d.ts +21 -0
- package/dist/emk/server-decode.js +123 -0
- package/dist/emk-to-kar.browser.d.ts +51 -0
- package/dist/emk-to-kar.browser.js +139 -0
- package/dist/emk-to-kar.d.ts +53 -0
- package/dist/emk-to-kar.js +210 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +64 -0
- package/dist/kar-reader.browser.d.ts +46 -0
- package/dist/kar-reader.browser.js +209 -0
- package/dist/kar-reader.d.ts +42 -0
- package/dist/kar-reader.js +197 -0
- package/dist/ncntokar.browser.d.ts +99 -0
- package/dist/ncntokar.browser.js +296 -0
- package/dist/ncntokar.d.ts +88 -0
- package/dist/ncntokar.js +340 -0
- package/examples/NextJSComponent.tsx +175 -0
- package/libs/emk/client-decoder.ts +142 -0
- package/libs/emk/server-decode.ts +133 -0
- package/libs/ncntokar.js +256 -0
- package/package.json +79 -0
- package/songs/.gitkeep +3 -0
- package/songs/cur/BPL3457.cur +0 -0
- package/songs/cur/Z2510006.cur +0 -0
- package/songs/cur/Z2510008.cur +0 -0
- package/songs/cur/Z2510136.cur +0 -0
- package/songs/cur/Z2510137.cur +0 -0
- package/songs/cur/Z2510138.cur +0 -0
- package/songs/cur/Z2510139.cur +0 -0
- package/songs/cur/Z2510140.cur +0 -0
- package/songs/cur/Z2510141.cur +0 -0
- package/songs/cur/song.cur +0 -0
- package/songs/emk/Z2510001.emk +0 -0
- package/songs/emk/Z2510002.emk +0 -0
- package/songs/emk/Z2510003.emk +0 -0
- package/songs/emk/Z2510004.emk +0 -0
- package/songs/emk/Z2510005.emk +0 -0
- package/songs/emk/Z2510006.emk +0 -0
- package/songs/kar/bpl3457.kar +0 -0
- package/songs/kar/z2510006.kar +0 -0
- package/songs/kar/z2510008.kar +0 -0
- package/songs/kar/z2510136.kar +0 -0
- package/songs/kar/z2510137.kar +0 -0
- package/songs/kar/z2510138.kar +0 -0
- package/songs/kar/z2510139.kar +0 -0
- package/songs/kar/z2510140.kar +0 -0
- package/songs/kar/z2510141.kar +0 -0
- package/songs/lyr/BPL3457.lyr +57 -0
- package/songs/lyr/Z2510006.lyr +53 -0
- package/songs/lyr/Z2510008.lyr +57 -0
- package/songs/lyr/Z2510136.lyr +54 -0
- package/songs/lyr/Z2510137.lyr +66 -0
- package/songs/lyr/Z2510138.lyr +62 -0
- package/songs/lyr/Z2510139.lyr +60 -0
- package/songs/lyr/Z2510140.lyr +41 -0
- package/songs/lyr/Z2510141.lyr +48 -0
- package/songs/lyr/song.lyr +37 -0
- package/songs/midi/BPL3457.MID +0 -0
- package/songs/midi/Z2510006.mid +0 -0
- package/songs/midi/Z2510008.mid +0 -0
- package/songs/midi/Z2510136.mid +0 -0
- package/songs/midi/Z2510137.mid +0 -0
- package/songs/midi/Z2510138.mid +0 -0
- package/songs/midi/Z2510139.mid +0 -0
- package/songs/midi/Z2510140.mid +0 -0
- package/songs/midi/Z2510141.mid +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
|
|
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
|
+
|
|
8
|
+
import { inflateSync } from 'zlib';
|
|
9
|
+
|
|
10
|
+
const XOR_KEY = Buffer.from([0xAF, 0xF2, 0x4C, 0x9C, 0xE9, 0xEA, 0x99, 0x43]);
|
|
11
|
+
const MAGIC_SIGNATURE = '.SFDS';
|
|
12
|
+
|
|
13
|
+
export interface DecodedEmkParts {
|
|
14
|
+
header?: Buffer;
|
|
15
|
+
songInfo?: Buffer;
|
|
16
|
+
midi?: Buffer;
|
|
17
|
+
lyric?: Buffer;
|
|
18
|
+
cursor?: Buffer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ZLIB_SECOND_BYTES = new Set<number>([0x01, 0x5E, 0x9C, 0xDA, 0x7D, 0x20, 0xBB]);
|
|
22
|
+
|
|
23
|
+
function xorDecrypt(data: Buffer): Buffer {
|
|
24
|
+
const decrypted = Buffer.alloc(data.length);
|
|
25
|
+
for (let i = 0; i < data.length; i++) {
|
|
26
|
+
decrypted[i] = data[i] ^ XOR_KEY[i % XOR_KEY.length];
|
|
27
|
+
}
|
|
28
|
+
return decrypted;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function looksLikeText(buf: Buffer): boolean {
|
|
32
|
+
const sample = buf.subarray(0, Math.min(64, buf.length));
|
|
33
|
+
for (let i = 0; i < sample.length; i++) {
|
|
34
|
+
const c = sample[i];
|
|
35
|
+
if (c === 0x0a || c === 0x0d || c === 0x09 || (c >= 0x20 && c <= 0x7e) || c >= 0x80) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function decodeEmk(fileBuffer: Buffer): DecodedEmkParts {
|
|
44
|
+
const decryptedBuffer = xorDecrypt(fileBuffer);
|
|
45
|
+
|
|
46
|
+
const magic = decryptedBuffer.subarray(0, MAGIC_SIGNATURE.length).toString('utf-8');
|
|
47
|
+
if (magic !== MAGIC_SIGNATURE) {
|
|
48
|
+
throw new Error(`Invalid EMK file signature. Expected '${MAGIC_SIGNATURE}' but got '${magic}'.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result: DecodedEmkParts = {};
|
|
52
|
+
const inflatedParts: Buffer[] = [];
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < decryptedBuffer.length - 2; i++) {
|
|
55
|
+
const b0 = decryptedBuffer[i];
|
|
56
|
+
const b1 = decryptedBuffer[i + 1];
|
|
57
|
+
|
|
58
|
+
if (b0 !== 0x78 || !ZLIB_SECOND_BYTES.has(b1)) continue;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const inflated = inflateSync(decryptedBuffer.subarray(i));
|
|
62
|
+
inflatedParts.push(inflated);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (inflatedParts.length < 3) {
|
|
69
|
+
throw new Error(`Invalid EMK structure: expected at least 3 zlib blocks, found ${inflatedParts.length}.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const inflated of inflatedParts) {
|
|
73
|
+
const asciiPrefix = inflated.subarray(0, 16).toString('ascii');
|
|
74
|
+
|
|
75
|
+
if (asciiPrefix.startsWith('SIGNATURE=')) {
|
|
76
|
+
result.header = inflated;
|
|
77
|
+
} else if (asciiPrefix.startsWith('CODE=')) {
|
|
78
|
+
result.songInfo = inflated;
|
|
79
|
+
} else if (inflated.subarray(0, 4).toString('ascii') === 'MThd') {
|
|
80
|
+
result.midi = inflated;
|
|
81
|
+
} else if (looksLikeText(inflated)) {
|
|
82
|
+
if (!result.lyric) {
|
|
83
|
+
result.lyric = inflated;
|
|
84
|
+
} else {
|
|
85
|
+
result.cursor = inflated;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if (!result.cursor) {
|
|
89
|
+
result.cursor = inflated;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!result.midi) throw new Error('MIDI data block not found in EMK file.');
|
|
95
|
+
if (!result.lyric) throw new Error('Lyric data block not found in EMK file.');
|
|
96
|
+
if (!result.cursor) throw new Error('Cursor data block not found in EMK file.');
|
|
97
|
+
|
|
98
|
+
if (!result.songInfo) {
|
|
99
|
+
result.songInfo = Buffer.from('CODE=\nTITLE=Unknown Title\nARTIST=Unknown Artist');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parses the "song info" block from a Buffer.
|
|
108
|
+
* @param songInfoBuffer The buffer containing the song info data.
|
|
109
|
+
* @returns A record containing TITLE, ARTIST, and CODE.
|
|
110
|
+
*/
|
|
111
|
+
export function parseSongInfo(songInfoBuffer: Buffer | undefined): Record<string, string> {
|
|
112
|
+
if (!songInfoBuffer) return {};
|
|
113
|
+
try {
|
|
114
|
+
const text = new TextDecoder('windows-874', { fatal: false }).decode(songInfoBuffer);
|
|
115
|
+
const lines = text.split(/\r?\n/);
|
|
116
|
+
const info: Record<string, string> = {};
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const parts = line.split('=');
|
|
119
|
+
if (parts.length >= 2) {
|
|
120
|
+
const key = parts[0].trim().toUpperCase();
|
|
121
|
+
const value = parts.slice(1).join('=').trim();
|
|
122
|
+
// Only store relevant keys to keep it focused
|
|
123
|
+
if (key === 'TITLE' || key === 'ARTIST' || key === 'CODE' || key === 'title' || key === 'artist' || key === 'code') {
|
|
124
|
+
info[key.toUpperCase()] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return info;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error("Failed to parse song info", e);
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
package/libs/ncntokar.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NCN (.mid + .lyr + .cur) -> .kar (MIDI with embedded karaoke/lyric/title/artist tracks)
|
|
4
|
+
* - .lyr decoded as TIS-620 (Thai encoding)
|
|
5
|
+
* - hard-coded input/output paths
|
|
6
|
+
* - meta events use midi-file format: { deltaTime, type: <subtype>, text }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const iconv = require("iconv-lite");
|
|
12
|
+
const GraphemeSplitter = require("grapheme-splitter");
|
|
13
|
+
const { parseMidi, writeMidi } = require("midi-file");
|
|
14
|
+
|
|
15
|
+
// ===============================
|
|
16
|
+
// ✅ HARD-CODED FILE PATHS HERE
|
|
17
|
+
// ===============================
|
|
18
|
+
const INPUT_MIDI = "songs/midi/Z2510006.mid"; // <-- change
|
|
19
|
+
const INPUT_LYR = "songs/lyr/Z2510006.lyr"; // <-- change (TIS-620)
|
|
20
|
+
const INPUT_CUR = "songs/cur/Z2510006.cur"; // <-- change
|
|
21
|
+
const OUTPUT_KAR = "out/Z25100066.kar"; // <-- change (must NOT exist)
|
|
22
|
+
// titles.txt will be created/appended in same folder as OUTPUT_KAR:
|
|
23
|
+
const TITLES_TXT = path.join(path.dirname(OUTPUT_KAR), "titles.txt");
|
|
24
|
+
|
|
25
|
+
// ===============================
|
|
26
|
+
|
|
27
|
+
function die(msg) {
|
|
28
|
+
console.error(`[error] ${msg}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
function warn(msg) {
|
|
32
|
+
console.warn(`[warn] ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureReadableFile(p) {
|
|
36
|
+
try {
|
|
37
|
+
fs.accessSync(p, fs.constants.R_OK);
|
|
38
|
+
if (!fs.statSync(p).isFile()) die(`Not a file: ${p}`);
|
|
39
|
+
} catch {
|
|
40
|
+
die(`Cannot read file: ${p}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureOutputDoesNotExist(p) {
|
|
45
|
+
if (fs.existsSync(p)) die(`Output already exists: ${p}`);
|
|
46
|
+
const dir = path.dirname(p);
|
|
47
|
+
if (dir && dir !== "." && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFileTextTIS620(p) {
|
|
51
|
+
const buf = fs.readFileSync(p);
|
|
52
|
+
return iconv.decode(buf, "tis-620");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitLinesKeepEndings(text) {
|
|
56
|
+
return text.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
57
|
+
}
|
|
58
|
+
function trimLineEndings(s) {
|
|
59
|
+
return s.replace(/[\r\n]+$/g, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class CursorReader {
|
|
63
|
+
constructor(buf) {
|
|
64
|
+
this.buf = buf;
|
|
65
|
+
this.off = 0;
|
|
66
|
+
}
|
|
67
|
+
readU16LE() {
|
|
68
|
+
if (this.off + 2 > this.buf.length) return null;
|
|
69
|
+
const v = this.buf.readUInt16LE(this.off);
|
|
70
|
+
this.off += 2;
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
readU8() {
|
|
74
|
+
if (this.off + 1 > this.buf.length) return null;
|
|
75
|
+
const v = this.buf[this.off];
|
|
76
|
+
this.off += 1;
|
|
77
|
+
return v;
|
|
78
|
+
}
|
|
79
|
+
remaining() {
|
|
80
|
+
return this.buf.length - this.off;
|
|
81
|
+
}
|
|
82
|
+
eof() {
|
|
83
|
+
return this.off >= this.buf.length;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ===== META EVENT BUILDERS (midi-file compatible) =====
|
|
88
|
+
// midi-file expects: { deltaTime, type: <subtype>, text }
|
|
89
|
+
// where subtype is "trackName", "text", "endOfTrack", etc.
|
|
90
|
+
// For Thai text (originally TIS-620), we encode back to TIS-620 bytes
|
|
91
|
+
// because the karaoke software expects TIS-620 encoding
|
|
92
|
+
function metaEvent(subtype, deltaTime, text) {
|
|
93
|
+
// Encode text back to TIS-620 bytes (original encoding) to preserve Thai characters
|
|
94
|
+
// This is necessary because the original .lyr files are in TIS-620
|
|
95
|
+
// and karaoke software may expect TIS-620 encoding in MIDI files
|
|
96
|
+
const tis620Bytes = iconv.encode(text, 'tis-620');
|
|
97
|
+
|
|
98
|
+
// Map subtype to metatypeByte
|
|
99
|
+
const metaTypeMap = {
|
|
100
|
+
'text': 0x01,
|
|
101
|
+
'trackName': 0x03,
|
|
102
|
+
'copyrightNotice': 0x02,
|
|
103
|
+
'instrumentName': 0x04,
|
|
104
|
+
'lyrics': 0x05,
|
|
105
|
+
'marker': 0x06,
|
|
106
|
+
'cuePoint': 0x07,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const metatypeByte = metaTypeMap[subtype];
|
|
110
|
+
if (metatypeByte !== undefined) {
|
|
111
|
+
// Use unknownMeta with proper metatypeByte to write TIS-620 bytes correctly
|
|
112
|
+
// This bypasses the broken writeString function that doesn't handle multi-byte chars
|
|
113
|
+
return {
|
|
114
|
+
deltaTime,
|
|
115
|
+
type: 'unknownMeta',
|
|
116
|
+
metatypeByte: metatypeByte,
|
|
117
|
+
data: Array.from(tis620Bytes)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback to standard format for other types
|
|
122
|
+
return { deltaTime, type: subtype, text };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function endOfTrack(deltaTime = 0) {
|
|
126
|
+
return { deltaTime, type: "endOfTrack" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function main() {
|
|
130
|
+
ensureReadableFile(INPUT_MIDI);
|
|
131
|
+
ensureReadableFile(INPUT_LYR);
|
|
132
|
+
ensureReadableFile(INPUT_CUR);
|
|
133
|
+
ensureOutputDoesNotExist(OUTPUT_KAR);
|
|
134
|
+
|
|
135
|
+
const midi = parseMidi(fs.readFileSync(INPUT_MIDI));
|
|
136
|
+
|
|
137
|
+
const ticksPerBeat = midi.header && midi.header.ticksPerBeat;
|
|
138
|
+
if (!ticksPerBeat) {
|
|
139
|
+
die("Only ticks-per-beat MIDI timing is supported (SMPTE timing not supported).");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const lyricText = readFileTextTIS620(INPUT_LYR);
|
|
143
|
+
const linesWithEndings = splitLinesKeepEndings(lyricText);
|
|
144
|
+
|
|
145
|
+
if (linesWithEndings.length < 2) die("Lyric file has too few lines (need title and artist).");
|
|
146
|
+
|
|
147
|
+
const songTitle = trimLineEndings(linesWithEndings[0] ?? "");
|
|
148
|
+
const artistName = trimLineEndings(linesWithEndings[1] ?? "");
|
|
149
|
+
|
|
150
|
+
// Perl discards line 3 & 4; lyrics start from line 5 (index 4)
|
|
151
|
+
let fullLyric = "";
|
|
152
|
+
for (let i = 4; i < linesWithEndings.length; i++) fullLyric += linesWithEndings[i];
|
|
153
|
+
|
|
154
|
+
const cursor = new CursorReader(fs.readFileSync(INPUT_CUR));
|
|
155
|
+
const splitter = new GraphemeSplitter();
|
|
156
|
+
|
|
157
|
+
// Track: Words
|
|
158
|
+
const karaokeTrack = [];
|
|
159
|
+
karaokeTrack.push(metaEvent("trackName", 0, "Words"));
|
|
160
|
+
karaokeTrack.push(metaEvent("text", 0, "@T" + songTitle));
|
|
161
|
+
karaokeTrack.push(metaEvent("text", 0, "@T" + artistName));
|
|
162
|
+
|
|
163
|
+
let previousAbsoluteTimestamp = 0;
|
|
164
|
+
|
|
165
|
+
for (let i = 4; i < linesWithEndings.length; i++) {
|
|
166
|
+
const trimmed = trimLineEndings(linesWithEndings[i]);
|
|
167
|
+
if (!trimmed || trimmed.length === 0) continue;
|
|
168
|
+
|
|
169
|
+
const lineForTiming = "/" + trimmed;
|
|
170
|
+
const graphemes = splitter.splitGraphemes(lineForTiming);
|
|
171
|
+
|
|
172
|
+
for (const g of graphemes) {
|
|
173
|
+
let absoluteTimestamp = previousAbsoluteTimestamp;
|
|
174
|
+
|
|
175
|
+
// Read 2 bytes per codepoint inside grapheme (matches Perl logic)
|
|
176
|
+
for (const _cp of Array.from(g)) {
|
|
177
|
+
const v = cursor.readU16LE();
|
|
178
|
+
if (v === null) {
|
|
179
|
+
warn("ran out of timing info; reusing previous timestamp");
|
|
180
|
+
absoluteTimestamp = previousAbsoluteTimestamp;
|
|
181
|
+
} else {
|
|
182
|
+
absoluteTimestamp = v;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Same conversion as Perl: * (ticksPerBeat / 24)
|
|
187
|
+
absoluteTimestamp = Math.floor(absoluteTimestamp * (ticksPerBeat / 24));
|
|
188
|
+
|
|
189
|
+
if (absoluteTimestamp < previousAbsoluteTimestamp) {
|
|
190
|
+
warn("timestamp out of order - clamping");
|
|
191
|
+
absoluteTimestamp = previousAbsoluteTimestamp;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const relativeTimestamp = absoluteTimestamp - previousAbsoluteTimestamp;
|
|
195
|
+
karaokeTrack.push(metaEvent("text", relativeTimestamp, g));
|
|
196
|
+
previousAbsoluteTimestamp = absoluteTimestamp;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Terminator check (Perl warns if EOF / no 0xFF)
|
|
201
|
+
const terminator = cursor.readU8();
|
|
202
|
+
if (terminator === null) {
|
|
203
|
+
warn("EOF without terminator while reading timing info");
|
|
204
|
+
} else {
|
|
205
|
+
const leftover = cursor.remaining();
|
|
206
|
+
if (terminator !== 0xff || leftover > 0) {
|
|
207
|
+
warn(
|
|
208
|
+
`${leftover} bytes (${leftover / 2} values) of unused timing info, or timing info ended without 0xFF terminator`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
karaokeTrack.push(endOfTrack(0));
|
|
214
|
+
|
|
215
|
+
// Track: Lyric (embed full lyric blob)
|
|
216
|
+
const lyricTrack = [
|
|
217
|
+
metaEvent("trackName", 0, "Lyric"),
|
|
218
|
+
metaEvent("text", 0, fullLyric),
|
|
219
|
+
endOfTrack(0),
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
// Track: Artist
|
|
223
|
+
const artistTrack = [
|
|
224
|
+
metaEvent("trackName", 0, "Artist"),
|
|
225
|
+
metaEvent("text", 0, artistName),
|
|
226
|
+
endOfTrack(0),
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
// Track: SongTitle
|
|
230
|
+
const titleTrack = [
|
|
231
|
+
metaEvent("trackName", 0, "SongTitle"),
|
|
232
|
+
metaEvent("text", 0, songTitle),
|
|
233
|
+
endOfTrack(0),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
// Insert as second track (index 1) like Perl
|
|
237
|
+
const originalTracks = midi.tracks || [];
|
|
238
|
+
const newTracks = originalTracks.slice();
|
|
239
|
+
newTracks.splice(1, 0, karaokeTrack, lyricTrack, artistTrack, titleTrack);
|
|
240
|
+
midi.tracks = newTracks;
|
|
241
|
+
|
|
242
|
+
const outBytes = writeMidi(midi);
|
|
243
|
+
fs.writeFileSync(OUTPUT_KAR, Buffer.from(outBytes));
|
|
244
|
+
|
|
245
|
+
fs.appendFileSync(
|
|
246
|
+
TITLES_TXT,
|
|
247
|
+
`${path.basename(OUTPUT_KAR)}\t${songTitle}\t${artistName}\n`,
|
|
248
|
+
"utf8"
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
console.log("✅ Done");
|
|
252
|
+
console.log(`- Output: ${OUTPUT_KAR}`);
|
|
253
|
+
console.log(`- Titles: ${TITLES_TXT}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@karaplay/file-coder",
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ncntokar": "./bin/ncntokar-cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"types": "./dist/client.d.ts",
|
|
17
|
+
"default": "./dist/client.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"test": "node_modules/.bin/jest",
|
|
23
|
+
"test:watch": "node_modules/.bin/jest --watch",
|
|
24
|
+
"test:coverage": "node_modules/.bin/jest --coverage",
|
|
25
|
+
"prepare": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"karaoke",
|
|
29
|
+
"midi",
|
|
30
|
+
"emk",
|
|
31
|
+
"kar",
|
|
32
|
+
"nextjs",
|
|
33
|
+
"decoder",
|
|
34
|
+
"encoder",
|
|
35
|
+
"thai",
|
|
36
|
+
"tis-620",
|
|
37
|
+
"karaoke-converter",
|
|
38
|
+
"midi-karaoke",
|
|
39
|
+
"browser",
|
|
40
|
+
"client-side",
|
|
41
|
+
"file-converter"
|
|
42
|
+
],
|
|
43
|
+
"author": "karaplay",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/karaplay/file-coder.git"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/karaplay/file-coder#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/karaplay/file-coder/issues"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"grapheme-splitter": "^1.0.4",
|
|
55
|
+
"iconv-lite": "^0.6.3",
|
|
56
|
+
"midi-file": "^1.2.4",
|
|
57
|
+
"pako": "^2.1.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/jest": "^29.5.11",
|
|
61
|
+
"@types/node": "^20.10.6",
|
|
62
|
+
"@types/pako": "^2.0.3",
|
|
63
|
+
"jest": "^29.7.0",
|
|
64
|
+
"ts-jest": "^29.1.1",
|
|
65
|
+
"typescript": "^5.3.3"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"next": ">=13.0.0",
|
|
69
|
+
"react": ">=18.0.0"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"next": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"react": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/songs/.gitkeep
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
����������ͻ���
|
|
2
|
+
�Դ �Ե���
|
|
3
|
+
E
|
|
4
|
+
|
|
5
|
+
. E .
|
|
6
|
+
�ŧ : ����������ͻ���
|
|
7
|
+
��ŻԹ : �Դ �Ե���
|
|
8
|
+
....�����....
|
|
9
|
+
..�Һ����� ������..�觹�
|
|
10
|
+
�����ջѭ�� �Ծ����� ����
|
|
11
|
+
�����ٻ���� ŧ�͡��
|
|
12
|
+
�����ѹ���� ��ͨ�觼����� ����ͧ
|
|
13
|
+
���ŧ�� �������� �����
|
|
14
|
+
��ҹ..����� ��������¤�
|
|
15
|
+
�ͧ..��ҧ ����ҹ�ѹ ����...
|
|
16
|
+
������� �͡����..���...
|
|
17
|
+
��..ʹ�...
|
|
18
|
+
�Ф��Ժ� ��ʹ�...
|
|
19
|
+
�����.. �ѡ伷ҧ��.. �����...
|
|
20
|
+
�����������ͻ��� ���ҹ�ͧ����
|
|
21
|
+
���ջѭ�� 仫��ͧ͢��ҧ
|
|
22
|
+
���..�ش ����ǡ����� ��ҧ�ҧ
|
|
23
|
+
��� �о����ҧ ��Ҵ�Ѵ�ѹ�����
|
|
24
|
+
���¢��ö..��ҧ �ٹ���������
|
|
25
|
+
�Ѻ���� ����վ���� ������
|
|
26
|
+
��ҡ������ѧ
|
|
27
|
+
��Шѧ�������ҧ ������
|
|
28
|
+
������ҹ��..���
|
|
29
|
+
�Ժ��ѡ �ѡ�����..��ҹ�...
|
|
30
|
+
....�����....
|
|
31
|
+
..��..ʹ�.. .
|
|
32
|
+
�Ф��Ժ� ��ʹ�...
|
|
33
|
+
�����.. �ѡ伷ҧ.!��.!. �����.!.
|
|
34
|
+
......
|
|
35
|
+
..�����������ͻ��� ���ҹ�ͧ����
|
|
36
|
+
���ջѭ�� 仫��ͧ͢��ҧ
|
|
37
|
+
���..�ش ����ǡ����� ��ҧ�ҧ
|
|
38
|
+
��� �о����ҧ ��Ҵ�Ѵ�ѹ�����
|
|
39
|
+
���¢��ö..��ҧ �ٹ���������
|
|
40
|
+
�Ѻ���� ����վ���� ������
|
|
41
|
+
��ҡ������ѧ
|
|
42
|
+
��Шѧ�������ҧ ������
|
|
43
|
+
������ҹ��..��� ����� �����������.. ������
|
|
44
|
+
�����������ͻ��� ���ҹ�ͧ����
|
|
45
|
+
���ջѭ�� 仫��ͧ͢��ҧ
|
|
46
|
+
���..�ش ����ǡ����� ��ҧ�ҧ
|
|
47
|
+
��� �о����ҧ ��Ҵ�Ѵ�ѹ�����
|
|
48
|
+
���¢��ö..��ҧ �ٹ���������
|
|
49
|
+
�Ѻ���.!� ����վ���� ������
|
|
50
|
+
��ҡ������ѧ
|
|
51
|
+
��Шѧ�������ҧ ������
|
|
52
|
+
������ҹ��..���
|
|
53
|
+
�Ժ��ѡ �ѡ�����..��ҹ��ҹ...
|
|
54
|
+
��ҹ..��..���
|
|
55
|
+
�Ժ��ѡ �ѡ�����..��ҹ�...
|
|
56
|
+
...���ŧ...
|
|
57
|
+
.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Move On Ẻ�
|
|
2
|
+
���� �����ɰ�
|
|
3
|
+
E
|
|
4
|
+
|
|
5
|
+
...... Intro ......
|
|
6
|
+
�ͨҡ�ѹ��Ũ��ش��µ�
|
|
7
|
+
��駤����������¡Ѻ�����˧�
|
|
8
|
+
�����Ӿѧ�褹����
|
|
9
|
+
�Ѻ�����ҷ������������
|
|
10
|
+
... ��������ѭ�� �����Ҩ�����駡ѹ
|
|
11
|
+
�����ç�ӧ���� ��ѹ�������ѡ�ѹ
|
|
12
|
+
���ҤԴ���������
|
|
13
|
+
���������������������ѹ
|
|
14
|
+
... �ѧ�ٿ�����ç������
|
|
15
|
+
�����ҫ���Ѻ������ҧ����
|
|
16
|
+
����ѹ���� ������ �����
|
|
17
|
+
�ѧ���� ����� �����
|
|
18
|
+
... ���¹�ѹ��駷ء � ���ҧ
|
|
19
|
+
��ҡ��˹�˹�������ҧ
|
|
20
|
+
���������������ѡ���ҧ
|
|
21
|
+
������ѹź���仨ҡ�
|
|
22
|
+
�ѡ�ѹ���ҹ
|
|
23
|
+
��������Ҩ��µ�ͧ����ѹ���˹
|
|
24
|
+
... ���¹�ѹ��駷ء � ���ҧ
|
|
25
|
+
������վ����ͧ�ըҡ
|
|
26
|
+
��������պҧ���ҧ
|
|
27
|
+
�ѹ������ź����ҡ����
|
|
28
|
+
�ѡ��������ѹ
|
|
29
|
+
���о���������ѹ�ѡ���˹
|
|
30
|
+
����¨з���ѹ������
|
|
31
|
+
...... Solo ......
|
|
32
|
+
�ѧ�ٿ�����ç������
|
|
33
|
+
�����ҫ���Ѻ������ҧ����
|
|
34
|
+
����ѹ���� ������ �����
|
|
35
|
+
�ѧ���� ������ �����
|
|
36
|
+
... ���¹�ѹ��駷ء � ���ҧ
|
|
37
|
+
��ҡ��˹�˹�������ҧ
|
|
38
|
+
���������������ѡ���ҧ
|
|
39
|
+
������ѹź���仨ҡ�
|
|
40
|
+
�ѡ�ѹ���ҹ
|
|
41
|
+
��������Ҩ��µ�ͧ����ѹ���˹
|
|
42
|
+
... ���¹�ѹ��駷ء � ���ҧ
|
|
43
|
+
������վ����ͧ�ըҡ
|
|
44
|
+
��������պҧ���ҧ
|
|
45
|
+
�ѹ������ź����ҡ����
|
|
46
|
+
�ѡ��������ѹ
|
|
47
|
+
���о���������ѹ�ѡ���˹
|
|
48
|
+
����¨з���ѹ������
|
|
49
|
+
...... Music ......
|
|
50
|
+
����ѹ������
|
|
51
|
+
...... Music .......
|
|
52
|
+
�ѧ�ٿ�����ç������
|
|
53
|
+
...... Outtro ......
|