# @karaplay/kar-player

[![npm version](https://img.shields.io/npm/v/@karaplay/kar-player.svg)](https://www.npmjs.com/package/@karaplay/kar-player)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

🎤 **Professional KAR (Karaoke) file player library with MIDI playback and advanced lyric rendering**

A high-performance, production-ready karaoke player library built for React and Next.js applications. Features accurate timing, beautiful themes, and extensive customization options.

## ✨ Key Features

### 🎯 Core Capabilities
- **KAR & EMK Support** - Play both KAR and EMK (Extreme Karaoke) files with automatic conversion
- **MIDI Playback** - High-quality MIDI synthesis using SpessaSynth
- **Advanced Lyric Rendering** - Professional-grade lyric display with multiple modes
- **Perfect Timing** - Character-based progress calculation for pixel-perfect synchronization
- **Thai Language Optimized** - Proper handling of Thai vowels and diacritics

### 🎨 Rendering Features
- **Line-Level Wipe Effect** - Smooth gradient animation that follows actual word timing
- **Multiple Display Modes** - Single-line, 2-line, 3-line, extreme-karaoke, and full modes
- **Beautiful Themes** - Default, karaoke, minimal, extreme-karaoke, and text-only themes
- **Text-Only Mode** - Export styled text for custom backgrounds
- **GPU Acceleration** - Optimized for smooth 60fps rendering on low-end devices

### ⚙️ Configuration
- **Environment Variables** - Configure via `.env` files (paths, colors, fonts, spacing)
- **Google Fonts** - Auto-load from 5 Thai fonts (Kanit, Prompt, Anuphan, Chakra Petch, Sarabun)
- **Custom Themes** - Create your own themes with CSS variables
- **Flexible Architecture** - Works in browser and Node.js (server-side conversion)

### 🚀 Performance
- **Sliding Window Rendering** - Minimal DOM elements (only visible lines)
- **RequestAnimationFrame** - Smooth 60fps animation
- **Memoization** - Prevents unnecessary re-renders
- **Page Visibility API** - Auto-resume on tab switch

## 📦 Installation

```bash
npm install @karaplay/kar-player@latest buffer@^6.0.3
```

### Next.js Configuration

If using Next.js, add webpack fallbacks to `next.config.js`:

```javascript
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        fs: false,
        net: false,
        tls: false,
        buffer: require.resolve('buffer/'),
      };
    }
    return config;
  },
};
```

## 🎮 Quick Start

### Basic React Usage

```tsx
import { KaraokePlayer } from '@karaplay/kar-player';

function App() {
  const player = new KaraokePlayer({
    processorUrl: process.env.NEXT_PUBLIC_SPESSASYNTH_PROCESSOR_URL || '/spessasynth_processor.min.js',
    soundFont: process.env.NEXT_PUBLIC_SOUNDFONT_URL || '/assets/sounds/GeneralUserGS.sf3',
    soundBankName: 'main',
    lyricConfig: {
      displayMode: 'three-line',
      theme: 'karaoke'
    }
  });

  // Load and play
  await player.loadKarFile('/path/to/song.kar');
  player.play();

  return <div ref={player.lyricContainerRef} />;
}
```

### React Hooks (Recommended)

```tsx
import { useLyricRenderer } from '@karaplay/kar-player';

function LyricDisplay({ lyrics, currentTime }) {
  const { containerRef } = useLyricRenderer(lyrics, currentTime, {
    displayMode: 'two-line',
    theme: 'karaoke',
    highlightMode: 'line'
  });

  return <div ref={containerRef} className="lyric-container" />;
}
```

## 🎵 Rendering Modes

### Line-Level Wipe Effect (v1.2.0+)

All rendering now uses **line-level** approach:
- **ONE element per line** (no word spans)
- **Character-based progress** - Accurate timing based on word positions
- **CSS gradient wipe** - Smooth animation across entire line
- **Preserved spaces** - All whitespace maintained

```tsx
// Renders as:
<div class="kar-lyric-line kar-line-current" style="--line-progress: 0.5">
  สวัสดีครับ ยินดีต้อนรับ
</div>
```

### Progress Calculation (v1.2.4+)

Accurate character-based progress:

```typescript
// Example: 
// "Hello " (6 chars) at 1000ms
// "World" (5 chars) at 2000ms

// At 1500ms (middle of "Hello "):
// 3 chars highlighted (50% of 6)
// Progress: 3/11 = 0.27 (27%)

// At 2500ms (middle of "World"):
// 6 + 2.5 chars highlighted
// Progress: 8.5/11 = 0.77 (77%)
```

## 🎨 Display Modes

### 1. Two-Line Mode (`two-line`)
```tsx
<useLyricRenderer displayMode="two-line" />
```
- Shows 2 lines: current + next
- Perfect for TV displays
- **Creates exactly 2 DOM elements**

### 2. Three-Line Mode (`three-line`)
```tsx
<useLyricRenderer displayMode="three-line" />
```
- Shows 3 lines: prev + current + next
- Best for desktop/mobile
- **Creates exactly 3 DOM elements**

### 3. Extreme Karaoke Mode (`extreme-karaoke`)
```tsx
<useLyricRenderer displayMode="extreme-karaoke" />
```
- Alternating top/bottom display
- Like professional karaoke boxes
- **Creates exactly 2 DOM elements**

### 4. Single-Line Mode (`single-line`)
```tsx
<useLyricRenderer displayMode="single-line" />
```
- Shows only current line
- Minimal UI
- **Creates exactly 1 DOM element**

### 5. Full Mode (`full`)
```tsx
<useLyricRenderer displayMode="full" />
```
- Shows all lyrics
- Scrollable view
- **Creates N DOM elements** (N = visible lines in viewport)

## 🎨 Themes

### Built-in Themes

```tsx
// 1. Default Theme
<useLyricRenderer theme="default" />

// 2. Karaoke Theme (recommended)
<useLyricRenderer theme="karaoke" />

// 3. Minimal Theme
<useLyricRenderer theme="minimal" />

// 4. Extreme Karaoke Theme
<useLyricRenderer theme="extreme-karaoke" />

// 5. Text-Only Theme
<useLyricRenderer theme="text-only" />
```

### Custom Theme

```typescript
const myTheme = {
  name: 'my-theme',
  colors: {
    active: '#ff0000',
    inactive: '#666666',
    background: '#000000'
  },
  fonts: {
    family: 'Kanit, sans-serif',
    size: '2rem',
    weight: 600
  },
  spacing: {
    lineHeight: '1.5',
    wordSpacing: '-0.05em',
    letterSpacing: '0'
  }
};

<useLyricRenderer theme={myTheme} />
```

## 📝 Text-Only Mode (v0.5.1+)

Export styled text without container styling for custom backgrounds:

```tsx
import { useLyricText } from '@karaplay/kar-player';

function CustomLyrics({ lyrics, currentTime }) {
  const { textElements } = useLyricText(lyrics, currentTime, {
    activeColor: '#ff0000',
    inactiveColor: '#666666'
  });

  return (
    <div style={{ background: 'url(my-bg.jpg)' }}>
      {textElements.map((line, i) => (
        <div 
          key={i}
          style={line.style}
          className={line.className}
        >
          {line.text}
        </div>
      ))}
    </div>
  );
}
```

## 🎭 EMK Support (v0.12.0+)

### Client-Side (Browser)

```tsx
import { convertEmkToKarBrowser } from '@karaplay/kar-player';

// Convert EMK to KAR in browser
const result = await convertEmkToKarBrowser('/path/to/song.emk');

if (result.success && result.karBuffer) {
  await player.loadKarBuffer(result.karBuffer);
  player.play();
}
```

### Server-Side (Node.js)

```tsx
// Import from /server subpath
import { convertEmkToKarServer } from '@karaplay/kar-player/server';

// Next.js API Route
export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get('file');
  
  const result = await convertEmkToKarServer(
    '/tmp/song.emk',
    { outputPath: '/tmp/song.kar' }
  );
  
  if (result.success) {
    return Response.json({ karPath: result.karPath });
  }
}
```

## ⚙️ Environment Configuration

### `.env` Configuration

```bash
# SpessaSynth Processor (Required)
NEXT_PUBLIC_SPESSASYNTH_PROCESSOR_URL=/spessasynth_processor.min.js

# SoundFont (Required)
NEXT_PUBLIC_SOUNDFONT_URL=/assets/sounds/GeneralUserGS.sf3

# Legacy paths (deprecated, use above instead)
NEXT_PUBLIC_SPESSASYNTH_LIB_PATH=/spessasynth_lib.min.js
NEXT_PUBLIC_SPESSASYNTH_CORE_PATH=/spessasynth_core.min.js
NEXT_PUBLIC_DEFAULT_SOUNDFONT=/soundfont.sf2

# Lyric Styling
NEXT_PUBLIC_KAR_PLAYER_ACTIVE_COLOR=#ffeb3b
NEXT_PUBLIC_KAR_PLAYER_INACTIVE_COLOR=rgba(255,255,255,0.5)
NEXT_PUBLIC_KAR_PLAYER_LINE_HEIGHT=1.3
NEXT_PUBLIC_KAR_PLAYER_WORD_SPACING=-0.05em
NEXT_PUBLIC_KAR_PLAYER_LETTER_SPACING=-0.02em

# Google Fonts (Kanit, Prompt, Anuphan, Chakra Petch, Sarabun)
NEXT_PUBLIC_KAR_PLAYER_GOOGLE_FONT=Kanit
NEXT_PUBLIC_KAR_PLAYER_FONT_WEIGHT=400
```

**Make sure these files are available in your `public` directory:**
- `public/spessasynth_processor.min.js`
- `public/assets/sounds/GeneralUserGS.sf3`

### Auto-Load Google Fonts

```tsx
import { loadGoogleFont } from '@karaplay/kar-player';

// Auto-load from .env
loadGoogleFont(); // Uses NEXT_PUBLIC_KAR_PLAYER_GOOGLE_FONT

// Or specify manually
loadGoogleFont('Prompt', '600');
```

## 🚀 Performance Optimizations

### 1. Use Optimized Renderer

```tsx
import { useLyricRendererOptimized } from '@karaplay/kar-player';

// Better performance with RAF + diffing
const { containerRef } = useLyricRendererOptimized(lyrics, currentTime, {
  displayMode: 'two-line',
  theme: 'karaoke'
});
```

### 2. Sliding Window Rendering

Automatically removes old DOM elements (only keeps visible lines):

```tsx
// Automatically enabled in all renderers
// 2-line mode = exactly 2 elements in DOM
// 3-line mode = exactly 3 elements in DOM
```

### 3. GPU Acceleration

Built-in CSS optimizations:

```css
.kar-lyric-line {
  will-change: background;
  transform: translateZ(0);
  backface-visibility: hidden;
  contain: layout style;
  content-visibility: auto;
}
```

### 4. Memoization

```tsx
// All hooks use React.useMemo internally
const { textElements } = useLyricText(lyrics, currentTime, options);
```

## 📊 API Reference

### KaraokePlayer Class

```typescript
const player = new KaraokePlayer({
  processorUrl: string,  // Required: SpessaSynth processor URL
  soundFont: string | ArrayBuffer,  // Required: SoundFont URL or buffer
  soundBankName?: string,  // Optional: Default 'main'
  lyricConfig?: {
    displayMode?: 'single-line' | 'two-line' | 'three-line' | 'extreme-karaoke' | 'full',
    theme?: string | LyricTheme,
    highlightMode?: 'line' | 'word' | 'progressive' | 'none',
    autoScroll?: boolean,
    smoothScroll?: boolean
  }
});

// Methods
await player.loadKarFile(path: string): Promise<LoadKarFileResult>
await player.loadKarBuffer(buffer: ArrayBuffer): Promise<LoadKarFileResult>
player.play(): void
player.pause(): void
player.stop(): void
player.seekTo(time: number): void
player.setVolume(volume: number): void
```

**Example with environment variables:**

```typescript
const player = new KaraokePlayer({
  processorUrl: process.env.NEXT_PUBLIC_SPESSASYNTH_PROCESSOR_URL!,
  soundFont: process.env.NEXT_PUBLIC_SOUNDFONT_URL!,
  soundBankName: 'main',
  lyricConfig: {
    displayMode: 'three-line',
    theme: 'karaoke'
  }
});
```

### React Hooks

#### useLyricRenderer

```typescript
const {
  containerRef,
  currentLineRef,
  forceUpdate,
  scrollToLine
} = useLyricRenderer(
  lyrics: LyricLine[],
  currentTime: number,
  options?: {
    displayMode?: 'single-line' | 'two-line' | 'three-line' | 'extreme-karaoke' | 'full',
    theme?: string | LyricTheme,
    highlightMode?: 'line' | 'word' | 'progressive' | 'none',
    textOnly?: boolean,
    disableBackground?: boolean,
    autoScroll?: boolean,
    smoothScroll?: boolean,
    classNames?: {
      container?: string,
      line?: string,
      current?: string,
      prev?: string,
      next?: string
    }
  }
);
```

#### useLyricText (Text-Only Mode)

```typescript
const {
  textElements,
  getLineStyle,
  getLineClass,
  currentLineIndex,
  progress
} = useLyricText(
  lyrics: LyricLine[],
  currentTime: number,
  options?: {
    displayMode?: string,
    inactiveColor?: string,
    activeColor?: string,
    currentLineColor?: string,
    prevLineColor?: string,
    nextLineColor?: string,
    currentFontSize?: string,
    prevFontSize?: string,
    nextFontSize?: string
  }
);

// textElements structure:
interface LyricTextElement {
  lineIndex: number;
  position: 'prev' | 'current' | 'next';
  text: string;           // Full line text
  style: CSSProperties;   // Inline styles with gradient wipe
  className: string;      // kar-lyric-line kar-line-{position}
  progress: number;       // 0-1 for wipe effect
}
```

### Progress Calculator Utilities

```typescript
import { 
  calculateLineProgress,
  calculateLineProgressWordByWord,
  getProgressDebugInfo 
} from '@karaplay/kar-player';

// Character-based progress (recommended)
const progress = calculateLineProgress(
  parts: LyricPart[],
  currentTime: number
): number; // 0-1

// Word-by-word progress (no partial)
const progress = calculateLineProgressWordByWord(
  parts: LyricPart[],
  currentTime: number
): number; // 0-1

// Debug timing
const info = getProgressDebugInfo(
  parts: LyricPart[],
  currentTime: number
): {
  currentTime: number;
  totalChars: number;
  charsHighlighted: number;
  progress: number;
  currentPartIndex: number;
  currentPartText: string;
  currentPartTime: number;
};
```

## 📚 Documentation

- **[CHANGELOG.md](./CHANGELOG.md)** - Version history and breaking changes
- **[ENV_CONFIG.md](./ENV_CONFIG.md)** - Complete .env configuration guide
- **[TEXT_ONLY_MODE.md](./TEXT_ONLY_MODE.md)** - Custom background rendering
- **[PERFORMANCE.md](./PERFORMANCE.md)** - Optimization techniques
- **[EMK_SUPPORT.md](./EMK_SUPPORT.md)** - EMK file handling
- **[GOOGLE_FONTS.md](./GOOGLE_FONTS.md)** - Thai font integration
- **[INSTALL_GUIDE.md](./INSTALL_GUIDE.md)** - Next.js & Firebase setup
- **[VERIFY_EXPORTS.md](./VERIFY_EXPORTS.md)** - Verify package exports

## 🎯 Use Cases

### 1. Professional Karaoke App
```tsx
import { KaraokePlayer } from '@karaplay/kar-player';

const player = new KaraokePlayer({
  processorUrl: '/spessasynth_processor.min.js',
  soundFont: '/assets/sounds/GeneralUserGS.sf3',
  soundBankName: 'main',
  lyricConfig: {
    displayMode: 'extreme-karaoke',
    theme: 'karaoke'
  }
});
```

### 2. Web Karaoke Platform
```tsx
<useLyricRendererOptimized
  displayMode="three-line"
  theme="minimal"
  highlightMode="line"
/>
```

### 3. TV Karaoke Box
```tsx
<useLyricRenderer
  displayMode="two-line"
  theme="extreme-karaoke"
  classNames={{ container: 'tv-display' }}
/>
```

### 4. Mobile Karaoke App
```tsx
<useLyricRenderer
  displayMode="single-line"
  theme="minimal"
  textOnly={true}
/>
```

## 🛠️ Development

```bash
# Install dependencies
npm install

# Build
npm run build

# Test
npm test

# Test with coverage
npm run test:coverage

# Watch mode
npm run test:watch
```

## 🤝 Contributing

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

## 📄 License

MIT © [Your Name]

## 🙏 Acknowledgments

- **SpessaSynth** - MIDI synthesis engine
- **midi-file** - MIDI file parsing
- **pako** - Compression/decompression
- **iconv-lite** - Encoding conversion
- **@karaplay/file-coder** - EMK file support

## 📊 Stats

- **Bundle Size:** ~140 KB (minified)
- **TypeScript:** 100% type coverage
- **Tests:** 147 unit tests
- **Browser Support:** Chrome, Firefox, Safari, Edge (latest 2 versions)
- **Node.js:** 16+ (for server-side conversion)

---

**Built with ❤️ for the karaoke community** 🎤🎵
