npm package: @karaplay/file-coder
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.
Author: karaplay
Latest version: 1.5.6

# @karaplay/file-coder

A comprehensive library for encoding/decoding karaoke files (.emk, .kar, MIDI) with Next.js support.

> **✅ Thai Support**: Thai lyrics are fully supported with proper TIS-620 encoding. All Thai characters are preserved and readable throughout the EMK→KAR conversion process.

## Features

- 🎵 Convert NCN format (.mid + .lyr + .cur) to .kar (MIDI karaoke)
- 🔓 Decode .emk (Extreme Karaoke) files
- 📖 Read and validate .kar files
- ⚡ **NEW**: Complete EMK → KAR workflow (one-step conversion)
- 🌐 Full TypeScript support
- ⚛️ Next.js compatible (both client and server side)
- 🌐 **NEW**: Browser-compatible API for client-side processing
- 🇹🇭 Thai language support (TIS-620 encoding)
- 🎯 **v1.5.0**: Accurate tempo handling for ZXIO format (1.24x ratio)
- ⏱️ **NEW**: Tempo validation and duration accuracy
- ✅ **FIXED v1.3.2**: Client-side EMK decoder now works correctly!
- ✅ 131 tests - 100% pass rate (including integration tests with real files)

## Installation

```bash
npm install @karaplay/file-coder
```

## Quick Start

### Browser/Client-Side (Next.js) 🌐 NEW

For client-side processing in Next.js or browser environments:

```tsx
'use client';

import { convertEmkFileToKar } from '@karaplay/file-coder/client';

function MyComponent() {
  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    
    const result = await convertEmkFileToKar(file, { autoDownload: true });
    console.log('Converted:', result.metadata.title);
  };
  
  return <input type="file" accept=".emk" onChange={handleFileUpload} />;
}
```

**📚 [Full Browser API Documentation →](./BROWSER_API.md)**

### Server-Side (Node.js)

## Usage

### EMK to KAR Workflow (Complete Pipeline) ⭐ NEW

Convert EMK files directly to KAR in one step:

```typescript
import { convertEmkToKar } from '@karaplay/file-coder';

const result = convertEmkToKar({
  inputEmk: 'songs/song.emk',
  outputKar: 'output/song.kar'
});

console.log(`Title: ${result.metadata.title}`);
console.log(`Artist: ${result.metadata.artist}`);
console.log(`Success: ${result.success}`);
// Thai text is readable! ✅
```

**Batch conversion**:
```typescript
import { convertEmkToKarBatch } from '@karaplay/file-coder';

const results = convertEmkToKarBatch(
  ['song1.emk', 'song2.emk', 'song3.emk'],
  'output/kar'
);
```

See [EMK_TO_KAR_WORKFLOW.md](./EMK_TO_KAR_WORKFLOW.md) for complete documentation.

---

## ⏱️ Tempo Handling & Duration Accuracy

### Understanding EMK Tempo Conversion

EMK files use non-standard MIDI timing that requires tempo adjustment during conversion. This library **automatically** handles tempo correction based on the EMK format:

#### ZXIO Format (v1.5.0+)
```
Original EMK:  64 BPM  →  Converted KAR: 79 BPM
Tempo Ratio:   1.24x   (ticksPerBeat / 77.42)
Duration:      4:43    ✅ Accurate!
```

#### MThd Format
```
Tempo Ratio:   4x, 8x, or 20x  (auto-detected)
```

### Validating Conversion Accuracy

Use `@tonejs/midi` to verify tempo and duration:

```typescript
import { Midi } from '@tonejs/midi';
import { convertEmkToKar } from '@karaplay/file-coder';
import * as fs from 'fs';

// Convert EMK to KAR
const result = convertEmkToKar({
  inputEmk: 'song.emk',
  outputKar: 'song.kar'
});

// Validate with Tone.js
const karBuffer = fs.readFileSync('song.kar');
const midi = new Midi(karBuffer);

console.log('✅ Validation Results:');
console.log(`   Tempo: ${midi.header.tempos[0].bpm.toFixed(2)} BPM`);
console.log(`   Duration: ${(midi.duration / 60).toFixed(2)} minutes`);
console.log(`   PPQ: ${midi.header.ppq}`);
console.log(`   Tracks: ${midi.tracks.length}`);
```

### 🔧 Troubleshooting Tempo Issues

**Problem:** Converted KAR plays too fast or too slow  
**Solution:** The library automatically applies correct tempo ratios:

| Format | Original BPM | Ratio | Converted BPM | Status |
|--------|--------------|-------|---------------|--------|
| ZXIO   | 64 BPM      | 1.24x | 79 BPM        | ✅ v1.5.0+ |
| MThd   | 160 BPM     | 4x    | 640 BPM       | ✅ Auto |
| MThd   | 142 BPM     | 8x    | 1136 BPM      | ✅ Auto |

**Important Notes:**
1. ✅ **Tempo is automatically corrected** - no manual adjustment needed
2. ✅ **Use @tonejs/midi for accurate duration** - it handles tempo events correctly
3. ⚠️ **Don't use karaoke-player's getTickResolution()** - it can give incorrect timing
4. ✅ **Duration decrease is normal** - faster tempo = shorter playback time

### Best Practices for MIDI Playback

When implementing a player for converted KAR files:

```typescript
import { Midi } from '@tonejs/midi';

// ✅ CORRECT: Use Tone.js for timing
const midi = new Midi(karBuffer);
const duration = midi.duration; // Accurate!

midi.tracks.forEach(track => {
  track.notes.forEach(note => {
    const time = note.time;     // ✅ Correct time in seconds
    const noteNum = note.midi;  // MIDI note number
    const velocity = note.velocity;
  });
});

// ❌ INCORRECT: Don't manually calculate from ticks
// const time = ticks * tickResolution; // May be wrong!
```

---

### NCN to KAR Conversion

```typescript
import { convertNcnToKar } from 'file-coder';

const result = convertNcnToKar({
  inputMidi: 'path/to/song.mid',
  inputLyr: 'path/to/song.lyr',
  inputCur: 'path/to/song.cur',
  outputKar: 'path/to/output.kar',
  appendTitles: true
});

console.log(`Converted: ${result.metadata.title} by ${result.metadata.artist}`);
```

### EMK Decoding (Server-side)

```typescript
import { decodeEmkServer, parseSongInfoServer } from 'file-coder';
import * as fs from 'fs';

const fileBuffer = fs.readFileSync('path/to/song.emk');
const decoded = decodeEmkServer(fileBuffer);

// Access decoded parts
const midiData = decoded.midi;
const lyricData = decoded.lyric;
const cursorData = decoded.cursor;

// Parse song information
const songInfo = parseSongInfoServer(decoded.songInfo);
console.log(`Title: ${songInfo.TITLE}`);
console.log(`Artist: ${songInfo.ARTIST}`);
```

### EMK Decoding (Client-side/Next.js)

```typescript
'use client';

import { decodeEmkClient, parseSongInfoClient } from 'file-coder';

function MyComponent() {
  const handleFileUpload = async (file: File) => {
    const arrayBuffer = await file.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    
    const decoded = decodeEmkClient(buffer);
    const songInfo = parseSongInfoClient(decoded.songInfo);
    
    console.log(`Title: ${songInfo.TITLE}`);
  };
  
  return <input type="file" onChange={(e) => handleFileUpload(e.target.files[0])} />;
}
```

## API Reference

### NCN to KAR Functions

- `convertNcnToKar(options)` - Main conversion function
- `parseLyricFile(filePath)` - Parse .lyr file for metadata
- `buildKaraokeTrack(metadata, cursorBuffer, ticksPerBeat)` - Build karaoke track with timing
- `buildMetadataTracks(metadata)` - Build metadata tracks
- `CursorReader` - Class for reading cursor timing data

### EMK to KAR Workflow Functions ⭐ NEW

- `convertEmkToKar(options)` - Complete pipeline: EMK → KAR (one step!)
- `convertEmkToKarBatch(emkFiles, outputDir, options?)` - Batch convert multiple files
- `validateThaiLyricReadability(lyricBuffer)` - Validate Thai text readability

### KAR File Reader Functions

- `readKarFile(filePath)` - Read and parse .kar file
- `validateKarFile(filePath)` - Validate .kar file structure
- `extractLyricsFromKar(filePath)` - Extract lyrics from .kar file

### EMK Decoder Functions

#### Server-side
- `decodeEmkServer(fileBuffer)` - Decode .emk file (Node.js)
- `parseSongInfoServer(songInfoBuffer)` - Parse song metadata
- `xorDecryptServer(data)` - XOR decryption utility
- `looksLikeTextServer(buffer)` - Text detection utility

#### Client-side
- `decodeEmkClient(fileBuffer)` - Decode .emk file (Browser/Next.js)
- `parseSongInfoClient(songInfoBuffer)` - Parse song metadata
- `xorDecryptClient(data)` - XOR decryption utility
- `looksLikeTextClient(buffer)` - Text detection utility

## Development

### Install Dependencies

```bash
npm install
```

### Build

```bash
npm run build
```

### Run Tests

```bash
npm test
```

### Test Coverage

```bash
npm run test:coverage
```

### Demo Server 🎤

A full-featured web demo with MIDI player and synchronized karaoke lyrics:

```bash
# Start demo server
npm run demo

# Stop demo server
npm run stop

# Restart demo server (kills old instance and starts new)
npm run restart
```

Open `http://localhost:3000/demo-simple.html` to access the demo.

**Features:**
- 🎵 High-quality soundfont audio (GeneralUserGS)
- 📝 Real-time synchronized karaoke lyrics
- 🎨 Beautiful UI with lyric highlighting
- 🇹🇭 Full Thai language support
- 📊 EMK vs KAR comparison view
- ⏱️ Accurate tempo and duration conversion

## Requirements

- Node.js >= 16
- TypeScript >= 5.0
- Next.js >= 13.0 (optional, for client-side features)

## License

MIT

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


## Changelog

### v1.3.2 (Latest)
**🔧 Critical Fix: Client-Side EMK Decoder**

- **Fixed**: Client-side EMK decoder was failing with "Invalid EMK structure: expected at least 3 zlib blocks, found 0"
- **Root Cause**: Missing `ZLIB_SECOND_BYTES` validation check in client-decoder (was only checking first byte 0x78)
- **Solution**: Added proper zlib header validation (both bytes) and fallback to Node.js zlib when available
- **Added**: 12 comprehensive client-side decoder tests
- **Result**: Client-side EMK decoding now works in both Node.js (tests/SSR) and browser environments

**Test results:**
```typescript
// All EMK files decode successfully on client-side ✅
// 131/131 tests passing (was 119 in v1.3.1) ✅
// Client and server decoders produce identical results ✅
```

### v1.3.1
**🔧 Critical Fix: Marker Lines Preservation**

- **Fixed**: Marker lines (e.g., "...... Intro ......", "....ดนตรี....") were being incorrectly filtered out
- **Improved**: Now includes ALL lines that have timing data in cursor file
- **Smart Handling**: Stops processing when cursor runs out of timing data
- **Result**: Complete preservation of songs with intro/solo/outro markers

**What was fixed:**
v1.3.0 introduced marker filtering that removed lines like "...... Intro ......" even when the cursor file had timing data for them. This caused songs like "Move On แบบใด" to be missing marker lines. v1.3.1 removes the filtering and trusts the cursor file - if timing exists, the line is included.

**Test results:**
```typescript
// Z2510001: ✅ Has "....ดนตรี...." + complete lyrics
// Z2510006: ✅ Has "...... Intro ......" + complete lyrics  
// 119/119 tests passing ✅
```

### v1.3.0
**🔧 Critical Fix: Beginning Lyrics Preservation**

- **Fixed**: Beginning lyrics were being cut off during EMK to KAR and NCN to KAR conversion
- **Improved**: Smart detection and skipping of instrumental intro markers (e.g., "....ดนตรี....")
- **Added**: 6 comprehensive tests to verify beginning lyrics preservation
- **Verified**: All 119 tests passing, ensuring complete lyric integrity from EMK decode to KAR output

**What was fixed:**
Previously, the conversion process was double-skipping the first 4 lines of lyrics, causing the actual beginning of songs to be missing from KAR files. This version correctly preserves all lyrics from the beginning while intelligently filtering out non-lyrical markers.

**Comparison test:**
```typescript
// EMK decoded lyrics vs KAR output: 100% match
// First lyric: "ท่องเที่ยวมาแล้วแทบทั่วเมืองไทย" ✅
// Last lyric: "กับยุพิน ที่เมืองพระรถ..." ✅
```

### v1.2.0
- Added Thai encoding tests (6 new tests)
- Verified TIS-620 encoding preservation
- Updated README with encoding clarification

### v1.1.1
- Documentation update for Thai encoding in KAR files
- No code changes

### v1.0.0
- Initial release
- Full browser and server support
- EMK decoding and NCN to KAR conversion
- Thai language support (TIS-620)