@karaplay/file-coder 1.5.5 ā 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EMK_REFERENCE_DATA.json +11 -0
- package/PLAYBACK_SPEED_FIX.md +223 -0
- package/README.md +25 -0
- package/TEST_DEMO.md +186 -0
- package/VERIFICATION_RESULT.md +116 -0
- package/debug-lyric-timing.js +219 -0
- package/debug-output.txt +2962 -0
- package/demo-client.html +6 -1
- package/demo-server.js +92 -4
- package/demo-simple.html +367 -71
- package/dist/ncntokar.js +15 -17
- package/download-soundfonts.js +108 -0
- package/full-debug-output.txt +2971 -0
- package/full-debug-timing.js +256 -0
- package/package.json +4 -2
- package/restart-demo.js +105 -0
- package/songs/soundfonts/GeneralUserGS.sf3 +0 -0
- package/start-demo.sh +12 -0
- package/stop-demo.js +84 -0
- package/temp/convert-result.json +1 -0
- package/temp/debug-test.kar +0 -0
- package/temp/kar-data.txt +1 -0
- package/temp/kar-data2.txt +1 -0
- package/temp/kar-test.txt +1 -0
- package/test-browser-thai.html +88 -0
- package/test-kar-timing.js +249 -0
- package/test-playback-speed.js +133 -0
- package/verify-all-songs-duration.js +285 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { decodeEmkServer, convertEmkToKar } = require('./dist/index.js');
|
|
6
|
+
const { Midi } = require('@tonejs/midi');
|
|
7
|
+
const iconv = require('iconv-lite');
|
|
8
|
+
|
|
9
|
+
const emkFile = process.argv[2] || 'songs/emk/Z2510006.emk';
|
|
10
|
+
|
|
11
|
+
console.log('='.repeat(80));
|
|
12
|
+
console.log('š FULL DEBUG: EMK ā KAR ā Play Timeline');
|
|
13
|
+
console.log('='.repeat(80));
|
|
14
|
+
console.log('File:', emkFile);
|
|
15
|
+
console.log('');
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// STEP 1: Decode EMK
|
|
19
|
+
// ============================================================================
|
|
20
|
+
console.log('š¦ STEP 1: Decoding EMK file...');
|
|
21
|
+
const emkBuffer = fs.readFileSync(emkFile);
|
|
22
|
+
const decoded = decodeEmkServer(emkBuffer);
|
|
23
|
+
|
|
24
|
+
console.log(' ā
Decoded successfully');
|
|
25
|
+
console.log(' Format:', decoded.isZxioFormat ? 'ZXIO' : 'MThd');
|
|
26
|
+
console.log('');
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// STEP 2: Analyze Original MIDI Timing
|
|
30
|
+
// ============================================================================
|
|
31
|
+
console.log('šµ STEP 2: Analyzing ORIGINAL EMK MIDI timing...');
|
|
32
|
+
const originalMidi = new Midi(decoded.midi);
|
|
33
|
+
const originalTempo = originalMidi.header.tempos[0]?.bpm || 120;
|
|
34
|
+
const ppq = originalMidi.header.ppq;
|
|
35
|
+
|
|
36
|
+
console.log(' Tempo:', originalTempo.toFixed(2), 'BPM');
|
|
37
|
+
console.log(' PPQ:', ppq);
|
|
38
|
+
console.log(' Duration:', originalMidi.duration.toFixed(2), 's');
|
|
39
|
+
|
|
40
|
+
// Find first notes
|
|
41
|
+
const firstNotes = [];
|
|
42
|
+
originalMidi.tracks.forEach((track, idx) => {
|
|
43
|
+
if (track.notes.length > 0 && idx > 0) {
|
|
44
|
+
const first = track.notes[0];
|
|
45
|
+
if (first.time < 10) {
|
|
46
|
+
firstNotes.push({ track: idx, time: first.time, midi: first.midi });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
firstNotes.sort((a, b) => a.time - b.time);
|
|
51
|
+
|
|
52
|
+
console.log(' First 5 notes:');
|
|
53
|
+
for (let i = 0; i < Math.min(5, firstNotes.length); i++) {
|
|
54
|
+
console.log(` Track ${firstNotes[i].track}: ${firstNotes[i].time.toFixed(3)}s (note ${firstNotes[i].midi})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Find first vocal note (skip intro drums)
|
|
58
|
+
let firstVocalTime = Infinity;
|
|
59
|
+
for (let i = 0; i < firstNotes.length; i++) {
|
|
60
|
+
if (firstNotes[i].time > 1 && firstNotes[i].time < 10) {
|
|
61
|
+
firstVocalTime = firstNotes[i].time;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log(` ā First vocal note: ${firstVocalTime.toFixed(3)}s`);
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// STEP 3: Analyze Cursor Raw Data
|
|
70
|
+
// ============================================================================
|
|
71
|
+
console.log('ā±ļø STEP 3: Analyzing CURSOR raw data...');
|
|
72
|
+
const cursorBuffer = decoded.cursor;
|
|
73
|
+
|
|
74
|
+
// Read first 50 cursor values (U16LE)
|
|
75
|
+
const cursorValues = [];
|
|
76
|
+
for (let i = 0; i < Math.min(100, cursorBuffer.length - 1); i += 2) {
|
|
77
|
+
const value = cursorBuffer.readUInt16LE(i);
|
|
78
|
+
if (value !== 0xFFFF) {
|
|
79
|
+
cursorValues.push({ index: i / 2, rawValue: value });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(` Total cursor entries: ${cursorValues.length}`);
|
|
84
|
+
console.log(' First 10 raw cursor values (U16LE):');
|
|
85
|
+
for (let i = 0; i < Math.min(10, cursorValues.length); i++) {
|
|
86
|
+
const entry = cursorValues[i];
|
|
87
|
+
// Calculate what this SHOULD be in seconds based on PPQ
|
|
88
|
+
const ticksWithMultiplier = entry.rawValue * (ppq / 24);
|
|
89
|
+
const secondsWithOriginalTempo = (ticksWithMultiplier / ppq) * (60 / originalTempo);
|
|
90
|
+
console.log(` [${entry.index}] raw=${entry.rawValue}, ticks=${ticksWithMultiplier.toFixed(0)}, seconds=${secondsWithOriginalTempo.toFixed(3)}`);
|
|
91
|
+
}
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// STEP 4: Analyze Lyric Content
|
|
96
|
+
// ============================================================================
|
|
97
|
+
console.log('š STEP 4: Analyzing LYRIC file...');
|
|
98
|
+
const lyricText = iconv.decode(decoded.lyric, 'tis-620');
|
|
99
|
+
const lyricLines = lyricText.split('\n');
|
|
100
|
+
|
|
101
|
+
console.log(` Total lines: ${lyricLines.length}`);
|
|
102
|
+
console.log(' Lines 0-9 (should be: title, artist, key, blank, then lyrics):');
|
|
103
|
+
for (let i = 0; i < Math.min(10, lyricLines.length); i++) {
|
|
104
|
+
const line = lyricLines[i].trim();
|
|
105
|
+
console.log(` [${i}] "${line.substring(0, 50)}${line.length > 50 ? '...' : ''}"`);
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// STEP 5: Convert to KAR
|
|
111
|
+
// ============================================================================
|
|
112
|
+
console.log('š STEP 5: Converting EMK ā KAR...');
|
|
113
|
+
const tempDir = path.join(__dirname, 'temp');
|
|
114
|
+
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const outputKar = path.join(tempDir, 'full-debug.kar');
|
|
117
|
+
const result = convertEmkToKar({
|
|
118
|
+
inputEmk: emkFile,
|
|
119
|
+
outputKar: outputKar,
|
|
120
|
+
keepIntermediateFiles: false
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log(' ā
Converted');
|
|
124
|
+
console.log(' Warnings:', result.warnings.length);
|
|
125
|
+
if (result.warnings.length > 0) {
|
|
126
|
+
console.log(' Warnings:');
|
|
127
|
+
result.warnings.forEach(w => console.log(` - ${w}`));
|
|
128
|
+
}
|
|
129
|
+
console.log('');
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// STEP 6: Analyze KAR MIDI Timing
|
|
133
|
+
// ============================================================================
|
|
134
|
+
console.log('š¼ STEP 6: Analyzing KAR MIDI timing...');
|
|
135
|
+
const karBuffer = fs.readFileSync(outputKar);
|
|
136
|
+
const karMidi = new Midi(karBuffer);
|
|
137
|
+
const karTempo = karMidi.header.tempos[0]?.bpm || 120;
|
|
138
|
+
|
|
139
|
+
console.log(' Tempo:', karTempo.toFixed(2), 'BPM');
|
|
140
|
+
console.log(' PPQ:', karMidi.header.ppq);
|
|
141
|
+
console.log(' Duration:', karMidi.duration.toFixed(2), 's');
|
|
142
|
+
console.log(' Tempo ratio:', (karTempo / originalTempo).toFixed(3), 'x');
|
|
143
|
+
|
|
144
|
+
// Find first notes in KAR
|
|
145
|
+
const karFirstNotes = [];
|
|
146
|
+
karMidi.tracks.forEach((track, idx) => {
|
|
147
|
+
if (track.notes.length > 0 && idx > 0) {
|
|
148
|
+
const first = track.notes[0];
|
|
149
|
+
if (first.time < 10) {
|
|
150
|
+
karFirstNotes.push({ track: idx, time: first.time, midi: first.midi });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
karFirstNotes.sort((a, b) => a.time - b.time);
|
|
155
|
+
|
|
156
|
+
console.log(' First 5 notes in KAR:');
|
|
157
|
+
for (let i = 0; i < Math.min(5, karFirstNotes.length); i++) {
|
|
158
|
+
console.log(` Track ${karFirstNotes[i].track}: ${karFirstNotes[i].time.toFixed(3)}s (note ${karFirstNotes[i].midi})`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let karFirstVocalTime = Infinity;
|
|
162
|
+
for (let i = 0; i < karFirstNotes.length; i++) {
|
|
163
|
+
if (karFirstNotes[i].time > 1 && karFirstNotes[i].time < 10) {
|
|
164
|
+
karFirstVocalTime = karFirstNotes[i].time;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
console.log(` ā First vocal note in KAR: ${karFirstVocalTime.toFixed(3)}s`);
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// STEP 7: Read KAR Lyrics with karaoke-player
|
|
173
|
+
// ============================================================================
|
|
174
|
+
console.log('š STEP 7: Reading KAR lyrics with karaoke-player...');
|
|
175
|
+
const KarFile = require('./demo-libs/KarFile');
|
|
176
|
+
const karFile = new KarFile();
|
|
177
|
+
karFile.readBuffer(karBuffer);
|
|
178
|
+
const lyrics = karFile.getLyrics();
|
|
179
|
+
|
|
180
|
+
console.log(` Total lyrics: ${lyrics.length}`);
|
|
181
|
+
console.log(' First 10 lyrics:');
|
|
182
|
+
for (let i = 0; i < Math.min(10, lyrics.length); i++) {
|
|
183
|
+
const lyric = lyrics[i];
|
|
184
|
+
const text = lyric.text.replace(/\n/g, ' ').substring(0, 40);
|
|
185
|
+
console.log(` [${i}] ${(lyric.time / 1000).toFixed(3)}s - "${text}"`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Find first real lyric
|
|
189
|
+
let firstRealLyricIndex = -1;
|
|
190
|
+
let firstLyricTime = 0;
|
|
191
|
+
for (let i = 0; i < lyrics.length; i++) {
|
|
192
|
+
const text = lyrics[i].text.replace(/[\.\*\s]/g, '');
|
|
193
|
+
if (text.length > 3 && !/^(Intro|intro)/.test(text)) {
|
|
194
|
+
firstRealLyricIndex = i;
|
|
195
|
+
firstLyricTime = lyrics[i].time / 1000;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (firstRealLyricIndex >= 0) {
|
|
201
|
+
console.log(` ā First real lyric: [${firstRealLyricIndex}] ${firstLyricTime.toFixed(3)}s`);
|
|
202
|
+
console.log(` Text: "${lyrics[firstRealLyricIndex].text.substring(0, 50)}"`);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// STEP 8: DIAGNOSIS
|
|
208
|
+
// ============================================================================
|
|
209
|
+
console.log('š¬ STEP 8: DIAGNOSIS');
|
|
210
|
+
console.log(' ' + '='.repeat(76));
|
|
211
|
+
console.log('');
|
|
212
|
+
|
|
213
|
+
console.log(' š Timeline Comparison:');
|
|
214
|
+
console.log(' ' + '-'.repeat(76));
|
|
215
|
+
console.log(' Event | EMK Original | KAR File | Difference');
|
|
216
|
+
console.log(' ' + '-'.repeat(76));
|
|
217
|
+
console.log(` First vocal note | ${firstVocalTime.toFixed(3)}s | ${karFirstVocalTime.toFixed(3)}s | ${(karFirstVocalTime - firstVocalTime).toFixed(3)}s`);
|
|
218
|
+
console.log(` First lyric (karaoke-player) | - | ${firstLyricTime.toFixed(3)}s | -`);
|
|
219
|
+
console.log(` Lyric vs Vocal gap | - | - | ${(firstLyricTime - karFirstVocalTime).toFixed(3)}s`);
|
|
220
|
+
console.log(' ' + '-'.repeat(76));
|
|
221
|
+
console.log('');
|
|
222
|
+
|
|
223
|
+
const lyricVocalGap = firstLyricTime - karFirstVocalTime;
|
|
224
|
+
|
|
225
|
+
if (Math.abs(lyricVocalGap) < 0.3) {
|
|
226
|
+
console.log(' ā
TIMING IS GOOD: Lyrics and vocals are synchronized (< 0.3s gap)');
|
|
227
|
+
} else if (lyricVocalGap < 0) {
|
|
228
|
+
console.log(` ā PROBLEM: LYRICS TOO EARLY by ${Math.abs(lyricVocalGap).toFixed(3)}s`);
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(' š Root Cause Analysis:');
|
|
231
|
+
console.log(' Lyrics appear BEFORE the singing starts.');
|
|
232
|
+
console.log(' This means cursor timing is NOT being scaled correctly!');
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(' š” Expected behavior:');
|
|
235
|
+
console.log(` - Vocal note: ${karFirstVocalTime.toFixed(3)}s`);
|
|
236
|
+
console.log(` - Lyric should appear: ~${karFirstVocalTime.toFixed(3)}s`);
|
|
237
|
+
console.log(` - Actual lyric: ${firstLyricTime.toFixed(3)}s`);
|
|
238
|
+
console.log(` - Gap: ${Math.abs(lyricVocalGap).toFixed(3)}s too early`);
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log(' š§ Solution:');
|
|
241
|
+
console.log(' The cursor timing values need to be multiplied by the TEMPO RATIO!');
|
|
242
|
+
console.log(` Tempo ratio: ${(karTempo / originalTempo).toFixed(3)}x`);
|
|
243
|
+
console.log(` Cursor should be scaled: rawCursor * ${(karTempo / originalTempo).toFixed(3)}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(` ā ļø PROBLEM: LYRICS TOO LATE by ${lyricVocalGap.toFixed(3)}s`);
|
|
246
|
+
console.log(' Lyrics appear AFTER the singing starts.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log(' ' + '='.repeat(76));
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
// Cleanup
|
|
254
|
+
fs.unlinkSync(outputKar);
|
|
255
|
+
|
|
256
|
+
console.log('ā
Debug complete!');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karaplay/file-coder",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.6",
|
|
4
4
|
"description": "A comprehensive library for encoding/decoding karaoke files (.emk, .kar, MIDI) with Next.js support. Convert EMK to KAR, read/write karaoke files, full browser and server support.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"test:emk:verbose": "node verify-emk-reference.js --verbose",
|
|
31
31
|
"analyze:emk": "node analyze-all-emk.js",
|
|
32
32
|
"prepare": "npm run build",
|
|
33
|
-
"demo": "node demo-server.js"
|
|
33
|
+
"demo": "node demo-server.js",
|
|
34
|
+
"stop": "node stop-demo.js",
|
|
35
|
+
"restart": "node restart-demo.js"
|
|
34
36
|
},
|
|
35
37
|
"keywords": [
|
|
36
38
|
"karaoke",
|
package/restart-demo.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const PORT = 3000;
|
|
7
|
+
const platform = os.platform();
|
|
8
|
+
|
|
9
|
+
console.log('š Restarting demo server...\n');
|
|
10
|
+
|
|
11
|
+
// Function to kill process on port
|
|
12
|
+
function killProcessOnPort(port) {
|
|
13
|
+
try {
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
// Windows
|
|
16
|
+
console.log(`š Finding process on port ${port} (Windows)...`);
|
|
17
|
+
const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
|
|
18
|
+
const lines = output.split('\n').filter(line => line.includes('LISTENING'));
|
|
19
|
+
|
|
20
|
+
if (lines.length > 0) {
|
|
21
|
+
const pid = lines[0].trim().split(/\s+/).pop();
|
|
22
|
+
console.log(`ā Killing process ${pid}...`);
|
|
23
|
+
execSync(`taskkill /F /PID ${pid}`);
|
|
24
|
+
console.log('ā
Old process killed\n');
|
|
25
|
+
} else {
|
|
26
|
+
console.log('ā
No process found on port (clean start)\n');
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// macOS/Linux
|
|
30
|
+
console.log(`š Finding process on port ${port} (macOS/Linux)...`);
|
|
31
|
+
|
|
32
|
+
// Try method 1: lsof + kill (works in most cases)
|
|
33
|
+
try {
|
|
34
|
+
const output = execSync(`lsof -ti:${port}`, { encoding: 'utf8' });
|
|
35
|
+
const pids = output.trim().split('\n').filter(pid => pid);
|
|
36
|
+
|
|
37
|
+
if (pids.length > 0) {
|
|
38
|
+
pids.forEach(pid => {
|
|
39
|
+
try {
|
|
40
|
+
console.log(`ā Killing process ${pid}...`);
|
|
41
|
+
execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
|
|
42
|
+
console.log('ā
Process killed\n');
|
|
43
|
+
} catch (killErr) {
|
|
44
|
+
// Try pkill as fallback
|
|
45
|
+
try {
|
|
46
|
+
console.log(`ā ļø kill failed, trying pkill...`);
|
|
47
|
+
execSync(`pkill -9 -f "node.*demo-server"`, { stdio: 'ignore' });
|
|
48
|
+
console.log('ā
Process killed via pkill\n');
|
|
49
|
+
} catch (pkillErr) {
|
|
50
|
+
console.log(`ā ļø Could not kill process (will continue anyway)\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Wait a bit for port to be released
|
|
56
|
+
console.log('ā³ Waiting for port to be released...');
|
|
57
|
+
execSync('sleep 1');
|
|
58
|
+
} else {
|
|
59
|
+
console.log('ā
No process found on port\n');
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// lsof returns error if no process found - try pkill as last resort
|
|
63
|
+
try {
|
|
64
|
+
execSync(`pkill -9 -f "node.*demo-server"`, { stdio: 'ignore' });
|
|
65
|
+
console.log('ā
Killed via pkill\n');
|
|
66
|
+
execSync('sleep 1');
|
|
67
|
+
} catch {
|
|
68
|
+
console.log('ā
No process found on port (clean start)\n');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('ā ļø Error killing process:', error.message);
|
|
74
|
+
console.log('Continuing anyway...\n');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Kill old process
|
|
79
|
+
killProcessOnPort(PORT);
|
|
80
|
+
|
|
81
|
+
// Start new server
|
|
82
|
+
console.log('š Starting demo server...\n');
|
|
83
|
+
console.log('='.repeat(80));
|
|
84
|
+
|
|
85
|
+
const server = spawn('node', ['demo-server.js'], {
|
|
86
|
+
stdio: 'inherit'
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.on('error', (err) => {
|
|
90
|
+
console.error('ā Failed to start server:', err);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Handle Ctrl+C
|
|
95
|
+
process.on('SIGINT', () => {
|
|
96
|
+
console.log('\n\nš Stopping server...');
|
|
97
|
+
server.kill('SIGINT');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
process.on('SIGTERM', () => {
|
|
102
|
+
console.log('\n\nš Stopping server...');
|
|
103
|
+
server.kill('SIGTERM');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
});
|
|
Binary file
|
package/start-demo.sh
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
echo "======================================"
|
|
3
|
+
echo "š¤ Starting EMK to KAR Demo Server"
|
|
4
|
+
echo "======================================"
|
|
5
|
+
echo ""
|
|
6
|
+
echo "Server will start at: http://localhost:3000"
|
|
7
|
+
echo "Demo page: http://localhost:3000/demo-simple.html"
|
|
8
|
+
echo ""
|
|
9
|
+
echo "Press Ctrl+C to stop"
|
|
10
|
+
echo ""
|
|
11
|
+
|
|
12
|
+
node demo-server.js
|
package/stop-demo.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const PORT = 3000;
|
|
7
|
+
const platform = os.platform();
|
|
8
|
+
|
|
9
|
+
console.log('š Stopping demo server...\n');
|
|
10
|
+
|
|
11
|
+
// Function to kill process on port
|
|
12
|
+
function killProcessOnPort(port) {
|
|
13
|
+
let killed = false;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
// Windows
|
|
18
|
+
console.log(`š Finding process on port ${port} (Windows)...`);
|
|
19
|
+
const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
|
|
20
|
+
const lines = output.split('\n').filter(line => line.includes('LISTENING'));
|
|
21
|
+
|
|
22
|
+
if (lines.length > 0) {
|
|
23
|
+
const pid = lines[0].trim().split(/\s+/).pop();
|
|
24
|
+
console.log(`ā Killing process ${pid}...`);
|
|
25
|
+
execSync(`taskkill /F /PID ${pid}`);
|
|
26
|
+
console.log('ā
Server stopped!\n');
|
|
27
|
+
killed = true;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// macOS/Linux
|
|
31
|
+
console.log(`š Finding process on port ${port} (macOS/Linux)...`);
|
|
32
|
+
|
|
33
|
+
// Try method 1: pkill (most reliable for node processes)
|
|
34
|
+
try {
|
|
35
|
+
console.log(`š Trying pkill for demo-server...`);
|
|
36
|
+
execSync(`pkill -9 -f "node.*demo-server"`, { encoding: 'utf8' });
|
|
37
|
+
console.log('ā
Server stopped via pkill!');
|
|
38
|
+
killed = true;
|
|
39
|
+
execSync('sleep 1'); // Wait for port to be released
|
|
40
|
+
} catch (pkillErr) {
|
|
41
|
+
// pkill didn't find process or failed
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Try method 2: lsof + kill (if pkill didn't work)
|
|
45
|
+
if (!killed) {
|
|
46
|
+
try {
|
|
47
|
+
const output = execSync(`lsof -ti:${port}`, { encoding: 'utf8' });
|
|
48
|
+
const pids = output.trim().split('\n').filter(pid => pid);
|
|
49
|
+
|
|
50
|
+
if (pids.length > 0) {
|
|
51
|
+
pids.forEach(pid => {
|
|
52
|
+
try {
|
|
53
|
+
console.log(`š Found process ${pid} on port ${port}`);
|
|
54
|
+
console.log(`ā Killing process ${pid}...`);
|
|
55
|
+
execSync(`kill -9 ${pid}`, { encoding: 'utf8' });
|
|
56
|
+
console.log('ā
Process killed');
|
|
57
|
+
killed = true;
|
|
58
|
+
} catch (killErr) {
|
|
59
|
+
console.log(`ā ļø Could not kill ${pid}: ${killErr.message}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (killed) {
|
|
63
|
+
execSync('sleep 1'); // Wait for port to be released
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// lsof failed, no process found
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (killed) {
|
|
73
|
+
console.log('\nā
Demo server stopped successfully!\n');
|
|
74
|
+
} else {
|
|
75
|
+
console.log('\nā ļø No demo server found running on port', port, '\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.log('\nā ļø No demo server found running\n');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Kill process
|
|
84
|
+
killProcessOnPort(PORT);
|