@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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible KAR file reader
|
|
3
|
+
* Reads and validates .kar files from buffers
|
|
4
|
+
*/
|
|
5
|
+
export interface KarFileInfo {
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
header: {
|
|
8
|
+
format?: number;
|
|
9
|
+
numTracks?: number;
|
|
10
|
+
ticksPerBeat?: number;
|
|
11
|
+
};
|
|
12
|
+
tracks: KarTrack[];
|
|
13
|
+
metadata: {
|
|
14
|
+
title?: string;
|
|
15
|
+
artist?: string;
|
|
16
|
+
hasKaraokeTrack: boolean;
|
|
17
|
+
hasLyricTrack: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface KarTrack {
|
|
21
|
+
name?: string;
|
|
22
|
+
events: number;
|
|
23
|
+
hasText: boolean;
|
|
24
|
+
hasLyrics: boolean;
|
|
25
|
+
textEvents: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reads and parses a KAR file from buffer
|
|
29
|
+
*/
|
|
30
|
+
export declare function readKarBuffer(buffer: Buffer): KarFileInfo;
|
|
31
|
+
/**
|
|
32
|
+
* Validates that a KAR buffer has the expected structure
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateKarBuffer(buffer: Buffer): {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
errors: string[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Extracts lyrics from a KAR buffer
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractLyricsFromKarBuffer(buffer: Buffer): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Helper to read File object as KAR
|
|
45
|
+
*/
|
|
46
|
+
export declare function readKarFile(file: File): Promise<KarFileInfo>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Browser-compatible KAR file reader
|
|
4
|
+
* Reads and validates .kar files from buffers
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
/**
|
|
8
|
+
* Browser-compatible KAR file reader
|
|
9
|
+
* Reads and validates .kar files from buffers
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.readKarBuffer = readKarBuffer;
|
|
46
|
+
exports.validateKarBuffer = validateKarBuffer;
|
|
47
|
+
exports.extractLyricsFromKarBuffer = extractLyricsFromKarBuffer;
|
|
48
|
+
exports.readKarFile = readKarFile;
|
|
49
|
+
const midi_file_1 = require("midi-file");
|
|
50
|
+
const iconv = __importStar(require("iconv-lite"));
|
|
51
|
+
/**
|
|
52
|
+
* Reads and parses a KAR file from buffer
|
|
53
|
+
*/
|
|
54
|
+
function readKarBuffer(buffer) {
|
|
55
|
+
const midi = (0, midi_file_1.parseMidi)(buffer);
|
|
56
|
+
const info = {
|
|
57
|
+
isValid: true,
|
|
58
|
+
header: {
|
|
59
|
+
format: midi.header.format,
|
|
60
|
+
numTracks: midi.header.numTracks,
|
|
61
|
+
ticksPerBeat: midi.header.ticksPerBeat
|
|
62
|
+
},
|
|
63
|
+
tracks: [],
|
|
64
|
+
metadata: {
|
|
65
|
+
hasKaraokeTrack: false,
|
|
66
|
+
hasLyricTrack: false
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// Process each track
|
|
70
|
+
for (const track of midi.tracks || []) {
|
|
71
|
+
const trackInfo = {
|
|
72
|
+
events: track.length,
|
|
73
|
+
hasText: false,
|
|
74
|
+
hasLyrics: false,
|
|
75
|
+
textEvents: []
|
|
76
|
+
};
|
|
77
|
+
for (const event of track) {
|
|
78
|
+
// Handle different event types
|
|
79
|
+
if (event.type === 'trackName' && 'text' in event) {
|
|
80
|
+
trackInfo.name = event.text;
|
|
81
|
+
}
|
|
82
|
+
else if (event.type === 'text' && 'text' in event) {
|
|
83
|
+
trackInfo.hasText = true;
|
|
84
|
+
const text = event.text;
|
|
85
|
+
trackInfo.textEvents.push(text);
|
|
86
|
+
// Check for karaoke markers
|
|
87
|
+
if (text.startsWith('@T')) {
|
|
88
|
+
if (!info.metadata.title) {
|
|
89
|
+
info.metadata.title = text.substring(2);
|
|
90
|
+
}
|
|
91
|
+
else if (!info.metadata.artist) {
|
|
92
|
+
info.metadata.artist = text.substring(2);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (event.type === 'lyrics' && 'text' in event) {
|
|
97
|
+
trackInfo.hasLyrics = true;
|
|
98
|
+
trackInfo.textEvents.push(event.text);
|
|
99
|
+
}
|
|
100
|
+
else if (event.type === 'unknownMeta' && 'data' in event) {
|
|
101
|
+
// Handle TIS-620 encoded text events
|
|
102
|
+
const data = event.data;
|
|
103
|
+
if (data && data.length > 0) {
|
|
104
|
+
try {
|
|
105
|
+
const buffer = Buffer.from(data);
|
|
106
|
+
const text = iconv.decode(buffer, 'tis-620');
|
|
107
|
+
trackInfo.hasText = true;
|
|
108
|
+
trackInfo.textEvents.push(text);
|
|
109
|
+
if (text.startsWith('@T')) {
|
|
110
|
+
if (!info.metadata.title) {
|
|
111
|
+
info.metadata.title = text.substring(2);
|
|
112
|
+
}
|
|
113
|
+
else if (!info.metadata.artist) {
|
|
114
|
+
info.metadata.artist = text.substring(2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
// Ignore decode errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Identify special tracks
|
|
125
|
+
if (trackInfo.name === 'Words') {
|
|
126
|
+
info.metadata.hasKaraokeTrack = true;
|
|
127
|
+
}
|
|
128
|
+
else if (trackInfo.name === 'Lyric') {
|
|
129
|
+
info.metadata.hasLyricTrack = true;
|
|
130
|
+
}
|
|
131
|
+
else if (trackInfo.name === 'SongTitle' && trackInfo.textEvents.length > 0) {
|
|
132
|
+
info.metadata.title = trackInfo.textEvents[0];
|
|
133
|
+
}
|
|
134
|
+
else if (trackInfo.name === 'Artist' && trackInfo.textEvents.length > 0) {
|
|
135
|
+
info.metadata.artist = trackInfo.textEvents[0];
|
|
136
|
+
}
|
|
137
|
+
info.tracks.push(trackInfo);
|
|
138
|
+
}
|
|
139
|
+
return info;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Validates that a KAR buffer has the expected structure
|
|
143
|
+
*/
|
|
144
|
+
function validateKarBuffer(buffer) {
|
|
145
|
+
const errors = [];
|
|
146
|
+
const warnings = [];
|
|
147
|
+
try {
|
|
148
|
+
const info = readKarBuffer(buffer);
|
|
149
|
+
// Check basic structure
|
|
150
|
+
if (!info.header.ticksPerBeat) {
|
|
151
|
+
errors.push('Missing ticksPerBeat in header');
|
|
152
|
+
}
|
|
153
|
+
if (info.tracks.length === 0) {
|
|
154
|
+
errors.push('No tracks found in file');
|
|
155
|
+
}
|
|
156
|
+
// Check for karaoke tracks
|
|
157
|
+
if (!info.metadata.hasKaraokeTrack) {
|
|
158
|
+
warnings.push('No "Words" karaoke track found');
|
|
159
|
+
}
|
|
160
|
+
if (!info.metadata.hasLyricTrack) {
|
|
161
|
+
warnings.push('No "Lyric" track found');
|
|
162
|
+
}
|
|
163
|
+
if (!info.metadata.title) {
|
|
164
|
+
warnings.push('No title metadata found');
|
|
165
|
+
}
|
|
166
|
+
if (!info.metadata.artist) {
|
|
167
|
+
warnings.push('No artist metadata found');
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
valid: errors.length === 0,
|
|
171
|
+
errors,
|
|
172
|
+
warnings
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
errors.push(`Failed to read KAR buffer: ${error.message}`);
|
|
177
|
+
return {
|
|
178
|
+
valid: false,
|
|
179
|
+
errors,
|
|
180
|
+
warnings
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Extracts lyrics from a KAR buffer
|
|
186
|
+
*/
|
|
187
|
+
function extractLyricsFromKarBuffer(buffer) {
|
|
188
|
+
const info = readKarBuffer(buffer);
|
|
189
|
+
const lyrics = [];
|
|
190
|
+
for (const track of info.tracks) {
|
|
191
|
+
if (track.name === 'Words' || track.hasText) {
|
|
192
|
+
for (const text of track.textEvents) {
|
|
193
|
+
// Skip metadata markers
|
|
194
|
+
if (!text.startsWith('@')) {
|
|
195
|
+
lyrics.push(text);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return lyrics;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Helper to read File object as KAR
|
|
204
|
+
*/
|
|
205
|
+
async function readKarFile(file) {
|
|
206
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
207
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
208
|
+
return readKarBuffer(buffer);
|
|
209
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KAR file reader utility
|
|
3
|
+
* Reads and validates .kar (MIDI karaoke) files
|
|
4
|
+
*/
|
|
5
|
+
export interface KarFileInfo {
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
header: {
|
|
8
|
+
format?: number;
|
|
9
|
+
numTracks?: number;
|
|
10
|
+
ticksPerBeat?: number;
|
|
11
|
+
};
|
|
12
|
+
tracks: KarTrack[];
|
|
13
|
+
metadata: {
|
|
14
|
+
title?: string;
|
|
15
|
+
artist?: string;
|
|
16
|
+
hasKaraokeTrack: boolean;
|
|
17
|
+
hasLyricTrack: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface KarTrack {
|
|
21
|
+
name?: string;
|
|
22
|
+
events: number;
|
|
23
|
+
hasText: boolean;
|
|
24
|
+
hasLyrics: boolean;
|
|
25
|
+
textEvents: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reads and parses a KAR file
|
|
29
|
+
*/
|
|
30
|
+
export declare function readKarFile(filePath: string): KarFileInfo;
|
|
31
|
+
/**
|
|
32
|
+
* Validates that a KAR file has the expected structure
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateKarFile(filePath: string): {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
errors: string[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Extracts lyrics from a KAR file
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractLyricsFromKar(filePath: string): string[];
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* KAR file reader utility
|
|
4
|
+
* Reads and validates .kar (MIDI karaoke) files
|
|
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.readKarFile = readKarFile;
|
|
41
|
+
exports.validateKarFile = validateKarFile;
|
|
42
|
+
exports.extractLyricsFromKar = extractLyricsFromKar;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const midi_file_1 = require("midi-file");
|
|
45
|
+
const iconv = __importStar(require("iconv-lite"));
|
|
46
|
+
/**
|
|
47
|
+
* Reads and parses a KAR file
|
|
48
|
+
*/
|
|
49
|
+
function readKarFile(filePath) {
|
|
50
|
+
const buffer = fs.readFileSync(filePath);
|
|
51
|
+
const midi = (0, midi_file_1.parseMidi)(buffer);
|
|
52
|
+
const info = {
|
|
53
|
+
isValid: true,
|
|
54
|
+
header: {
|
|
55
|
+
format: midi.header.format,
|
|
56
|
+
numTracks: midi.header.numTracks,
|
|
57
|
+
ticksPerBeat: midi.header.ticksPerBeat
|
|
58
|
+
},
|
|
59
|
+
tracks: [],
|
|
60
|
+
metadata: {
|
|
61
|
+
hasKaraokeTrack: false,
|
|
62
|
+
hasLyricTrack: false
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
// Process each track
|
|
66
|
+
for (const track of midi.tracks || []) {
|
|
67
|
+
const trackInfo = {
|
|
68
|
+
events: track.length,
|
|
69
|
+
hasText: false,
|
|
70
|
+
hasLyrics: false,
|
|
71
|
+
textEvents: []
|
|
72
|
+
};
|
|
73
|
+
for (const event of track) {
|
|
74
|
+
// Handle different event types
|
|
75
|
+
if (event.type === 'trackName' && 'text' in event) {
|
|
76
|
+
trackInfo.name = event.text;
|
|
77
|
+
}
|
|
78
|
+
else if (event.type === 'text' && 'text' in event) {
|
|
79
|
+
trackInfo.hasText = true;
|
|
80
|
+
const text = event.text;
|
|
81
|
+
trackInfo.textEvents.push(text);
|
|
82
|
+
// Check for karaoke markers
|
|
83
|
+
if (text.startsWith('@T')) {
|
|
84
|
+
if (!info.metadata.title) {
|
|
85
|
+
info.metadata.title = text.substring(2);
|
|
86
|
+
}
|
|
87
|
+
else if (!info.metadata.artist) {
|
|
88
|
+
info.metadata.artist = text.substring(2);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (event.type === 'lyrics' && 'text' in event) {
|
|
93
|
+
trackInfo.hasLyrics = true;
|
|
94
|
+
trackInfo.textEvents.push(event.text);
|
|
95
|
+
}
|
|
96
|
+
else if (event.type === 'unknownMeta' && 'data' in event) {
|
|
97
|
+
// Handle TIS-620 encoded text events
|
|
98
|
+
const data = event.data;
|
|
99
|
+
if (data && data.length > 0) {
|
|
100
|
+
try {
|
|
101
|
+
const buffer = Buffer.from(data);
|
|
102
|
+
const text = iconv.decode(buffer, 'tis-620');
|
|
103
|
+
trackInfo.hasText = true;
|
|
104
|
+
trackInfo.textEvents.push(text);
|
|
105
|
+
if (text.startsWith('@T')) {
|
|
106
|
+
if (!info.metadata.title) {
|
|
107
|
+
info.metadata.title = text.substring(2);
|
|
108
|
+
}
|
|
109
|
+
else if (!info.metadata.artist) {
|
|
110
|
+
info.metadata.artist = text.substring(2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// Ignore decode errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Identify special tracks
|
|
121
|
+
if (trackInfo.name === 'Words') {
|
|
122
|
+
info.metadata.hasKaraokeTrack = true;
|
|
123
|
+
}
|
|
124
|
+
else if (trackInfo.name === 'Lyric') {
|
|
125
|
+
info.metadata.hasLyricTrack = true;
|
|
126
|
+
}
|
|
127
|
+
else if (trackInfo.name === 'SongTitle' && trackInfo.textEvents.length > 0) {
|
|
128
|
+
info.metadata.title = trackInfo.textEvents[0];
|
|
129
|
+
}
|
|
130
|
+
else if (trackInfo.name === 'Artist' && trackInfo.textEvents.length > 0) {
|
|
131
|
+
info.metadata.artist = trackInfo.textEvents[0];
|
|
132
|
+
}
|
|
133
|
+
info.tracks.push(trackInfo);
|
|
134
|
+
}
|
|
135
|
+
return info;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Validates that a KAR file has the expected structure
|
|
139
|
+
*/
|
|
140
|
+
function validateKarFile(filePath) {
|
|
141
|
+
const errors = [];
|
|
142
|
+
const warnings = [];
|
|
143
|
+
try {
|
|
144
|
+
const info = readKarFile(filePath);
|
|
145
|
+
// Check basic structure
|
|
146
|
+
if (!info.header.ticksPerBeat) {
|
|
147
|
+
errors.push('Missing ticksPerBeat in header');
|
|
148
|
+
}
|
|
149
|
+
if (info.tracks.length === 0) {
|
|
150
|
+
errors.push('No tracks found in file');
|
|
151
|
+
}
|
|
152
|
+
// Check for karaoke tracks
|
|
153
|
+
if (!info.metadata.hasKaraokeTrack) {
|
|
154
|
+
warnings.push('No "Words" karaoke track found');
|
|
155
|
+
}
|
|
156
|
+
if (!info.metadata.hasLyricTrack) {
|
|
157
|
+
warnings.push('No "Lyric" track found');
|
|
158
|
+
}
|
|
159
|
+
if (!info.metadata.title) {
|
|
160
|
+
warnings.push('No title metadata found');
|
|
161
|
+
}
|
|
162
|
+
if (!info.metadata.artist) {
|
|
163
|
+
warnings.push('No artist metadata found');
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
valid: errors.length === 0,
|
|
167
|
+
errors,
|
|
168
|
+
warnings
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
errors.push(`Failed to read KAR file: ${error.message}`);
|
|
173
|
+
return {
|
|
174
|
+
valid: false,
|
|
175
|
+
errors,
|
|
176
|
+
warnings
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Extracts lyrics from a KAR file
|
|
182
|
+
*/
|
|
183
|
+
function extractLyricsFromKar(filePath) {
|
|
184
|
+
const info = readKarFile(filePath);
|
|
185
|
+
const lyrics = [];
|
|
186
|
+
for (const track of info.tracks) {
|
|
187
|
+
if (track.name === 'Words' || track.hasText) {
|
|
188
|
+
for (const text of track.textEvents) {
|
|
189
|
+
// Skip metadata markers
|
|
190
|
+
if (!text.startsWith('@')) {
|
|
191
|
+
lyrics.push(text);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return lyrics;
|
|
197
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible NCN to KAR converter
|
|
3
|
+
* Works with File API and ArrayBuffers instead of fs
|
|
4
|
+
*/
|
|
5
|
+
export interface BrowserConversionOptions {
|
|
6
|
+
midiBuffer: Buffer;
|
|
7
|
+
lyricBuffer: Buffer;
|
|
8
|
+
cursorBuffer: Buffer;
|
|
9
|
+
outputFileName?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BrowserConversionResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
karBuffer: Buffer;
|
|
14
|
+
fileName: string;
|
|
15
|
+
metadata: {
|
|
16
|
+
title: string;
|
|
17
|
+
artist: string;
|
|
18
|
+
fullLyric: string;
|
|
19
|
+
lines: string[];
|
|
20
|
+
};
|
|
21
|
+
warnings: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads text from buffer as TIS-620 (Thai encoding)
|
|
25
|
+
*/
|
|
26
|
+
export declare function readBufferTextTIS620(buffer: Buffer): string;
|
|
27
|
+
/**
|
|
28
|
+
* Splits text into lines while keeping line endings
|
|
29
|
+
*/
|
|
30
|
+
export declare function splitLinesKeepEndings(text: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Removes line endings from a string
|
|
33
|
+
*/
|
|
34
|
+
export declare function trimLineEndings(s: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a MIDI meta event with TIS-620 encoding for Thai text
|
|
37
|
+
*/
|
|
38
|
+
export declare function createMetaEvent(subtype: string, deltaTime: number, text: string): any;
|
|
39
|
+
/**
|
|
40
|
+
* Creates an end-of-track MIDI event
|
|
41
|
+
*/
|
|
42
|
+
export declare function createEndOfTrack(deltaTime?: number): any;
|
|
43
|
+
/**
|
|
44
|
+
* Simple cursor reader for browser
|
|
45
|
+
*/
|
|
46
|
+
export declare class BrowserCursorReader {
|
|
47
|
+
private buf;
|
|
48
|
+
private off;
|
|
49
|
+
constructor(buf: Buffer);
|
|
50
|
+
readU16LE(): number | null;
|
|
51
|
+
readU8(): number | null;
|
|
52
|
+
remaining(): number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parses lyric buffer to extract metadata (title, artist, lyrics)
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseLyricBuffer(lyricBuffer: Buffer): {
|
|
58
|
+
title: string;
|
|
59
|
+
artist: string;
|
|
60
|
+
fullLyric: string;
|
|
61
|
+
lines: string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Builds karaoke track with timing information (browser version)
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildKaraokeTrackBrowser(metadata: {
|
|
67
|
+
title: string;
|
|
68
|
+
artist: string;
|
|
69
|
+
fullLyric: string;
|
|
70
|
+
lines: string[];
|
|
71
|
+
}, cursorBuffer: Buffer, ticksPerBeat: number): {
|
|
72
|
+
track: any[];
|
|
73
|
+
warnings: string[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Builds additional metadata tracks (Lyric, Artist, SongTitle)
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildMetadataTracksBrowser(metadata: {
|
|
79
|
+
title: string;
|
|
80
|
+
artist: string;
|
|
81
|
+
fullLyric: string;
|
|
82
|
+
}): {
|
|
83
|
+
lyricTrack: any[];
|
|
84
|
+
artistTrack: any[];
|
|
85
|
+
titleTrack: any[];
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Browser-compatible NCN to KAR conversion
|
|
89
|
+
* Works with buffers instead of file paths
|
|
90
|
+
*/
|
|
91
|
+
export declare function convertNcnToKarBrowser(options: BrowserConversionOptions): BrowserConversionResult;
|
|
92
|
+
/**
|
|
93
|
+
* Helper to convert File to Buffer for browser use
|
|
94
|
+
*/
|
|
95
|
+
export declare function fileToBuffer(file: File): Promise<Buffer>;
|
|
96
|
+
/**
|
|
97
|
+
* Helper to download buffer as file in browser
|
|
98
|
+
*/
|
|
99
|
+
export declare function downloadBuffer(buffer: Buffer, fileName: string, mimeType?: string): void;
|