npm package: @karaplay/karaoke-player
Description: Karaoke player library with A/B swapping renderer for MIDI karaoke files
Latest version: 1.0.14

# @karaplay/karaoke-player

Karaoke player library with A/B swapping renderer for MIDI karaoke files.

## 🎯 More Accurate Timing Options

This library now supports **multiple timing methods** for maximum accuracy:

1. **MIDI Note Events** (⭐ Best) - Use note events from vocal track
   - Most accurate - uses native MIDI tick timing
   - Knows when to sing and when to rest (Note On/Off)
   - Word/syllable level mapping
   - See [MIDI_NOTE_APPROACH.md](./MIDI_NOTE_APPROACH.md) for details

2. **MIDI Lyric Events** - Use lyric events from MIDI file
   - Very accurate - absolute timing
   - See [MIDI_LYRIC_APPROACH.md](./MIDI_LYRIC_APPROACH.md) for details

3. **CUR File** - Fallback method
   - Relative timing, requires normalization

### Why MIDI Lyric Events?
- ✅ **Absolute Timing**: No normalization needed - timing is absolute
- ✅ **Native MIDI Integration**: Timing matches MIDI playback 100%
- ✅ **Tempo-Aware**: Automatically handles tempo changes
- ✅ **Standard Format**: Uses MIDI standard (SMF format)
- ✅ **Backward Compatible**: Falls back to CUR file if MIDI doesn't have lyric events

### Quick Start with MIDI Note Events (Recommended)

```javascript
import { 
  parseMidiNotes, 
  notesToCharTimestamps,
  findVocalTrack 
} from '@karaplay/karaoke-player';

// Parse MIDI file for note events
const midiBuffer = await fetchArrayBuffer('/midi');
const result = parseMidiNotes(midiBuffer, {
  minNote: 48,  // C3 - vocal range
  maxNote: 84   // C6 - vocal range
});

// Find vocal track automatically
const vocalTrack = findVocalTrack(result.tracks);

// Convert note events to timestamps
const words = fullLyricText.split(/\s+/);
const timestamps = notesToCharTimestamps(
  vocalTrack.notes,
  result.tempoMap,
  result.division,
  fullLyricText,
  words
);
```

### Alternative: MIDI Lyric Events

```javascript
import { parseMidiLyrics, lyricEventsToTimestamps } from '@karaplay/karaoke-player';

const { lyricEvents, tempoMap, division } = parseMidiLyrics(midiBuffer);
const timestamps = lyricEventsToTimestamps(
  lyricEvents,
  tempoMap,
  division,
  fullLyricText
);
```

## Installation

```bash
npm install @test-midi/karaoke-player
```

## Usage

### Basic Setup

```javascript
import {
  initMidiPlayer,
  parseLyrics,
  parseCurTimestamps,
  normalizeTimestamps,
  getCurrentCharByTime,
  renderABSwap,
  computeBaseMs,
  safeGetStatus,
  playMidi,
  pauseMidi,
  stopMidi,
  seekMidi,
  setSongDuration,
  injectCalibrationUI,
  getLyricSpeed,
  getTimeOffsetMs
} from '@test-midi/karaoke-player';

// Initialize MIDI player
await initMidiPlayer();

// Set song duration
setSongDuration(180000); // 3 minutes

// Parse lyrics
const lyricText = await fetch('/lyrics.lyr').then(r => r.text());
const { fullText, lineTexts } = parseLyrics(lyricText);

// Parse CUR timestamps
const curBuffer = await fetch('/song.cur').then(r => r.arrayBuffer());
const rawTimestamps = parseCurTimestamps(curBuffer);
const timestamps = normalizeTimestamps(rawTimestamps, 180000);

// Render loop
function render() {
  const status = safeGetStatus();
  const baseMs = computeBaseMs(status);
  const lyricSpeed = getLyricSpeed();
  const timeOffsetMs = getTimeOffsetMs();
  const lyricMs = (baseMs + timeOffsetMs) * lyricSpeed;
  
  const currentCharPos = getCurrentCharByTime(timestamps, lyricMs);
  renderABSwap(currentCharPos, lineTexts, lyricMs, timestamps);
  
  requestAnimationFrame(render);
}

// Start rendering
render();
```

### HTML Structure

Your HTML should have two elements for the karaoke lines:

```html
<div id="line1"></div>
<div id="line2"></div>
```

### Calibration UI

To add the calibration UI panel:

```javascript
import { injectCalibrationUI } from '@test-midi/karaoke-player';

injectCalibrationUI();
```

This will add a floating panel for adjusting lyric speed and time offset.

## API Reference

### Utilities

- `$(id)` - Get DOM element by ID with caching
- `esc(s)` - Escape HTML special characters
- `clamp(x, a, b)` - Clamp value between min and max
- `formatTime(ms)` - Format milliseconds to MM:SS
- `fetchArrayBuffer(url)` - Fetch array buffer from URL

### Lyric Parser

- `decodeLyric(buffer)` - Decode lyric buffer to text using multiple encodings
- `parseLyrics(text)` - Parse lyrics text and extract singing lines
- `parseCurTimestamps(buffer)` - Parse CUR timestamps from buffer
- `normalizeTimestamps(timestamps, songDurationMs)` - Normalize timestamps to song duration
- `getCurrentCharByTime(timestamps, currentTimeMs)` - Get current character position by time using binary search
- `findLineByCharPos(lineTexts, charPos)` - Find line by character position

### MIDI Player

- `initMidiPlayer()` - Initialize MIDI player (requires WebAudioTinySynth)
- `setSongDuration(durationMs)` - Set song duration
- `safeGetStatus()` - Get player status safely
- `computeBaseMs(status)` - Compute base time in milliseconds from player status
- `playMidi()` - Play MIDI
- `pauseMidi()` - Pause MIDI
- `stopMidi()` - Stop MIDI and reset position
- `seekMidi(ms)` - Seek MIDI to specific time

### Calibration

- `injectCalibrationUI()` - Inject calibration UI panel
- `getLyricSpeed()` - Get current lyric speed
- `getTimeOffsetMs()` - Get current time offset

### Renderer

- `renderABSwap(currentCharPos, lineTexts, lyricMs, curTimestamps)` - Render karaoke with A/B slot swapping

## Requirements

- WebAudioTinySynth library (must be loaded separately)
- Modern browser with ES6 module support

## License

MIT