# Bungee Pitch Shift

High-quality, low-latency pitch shifting and time stretching for Web Audio API using WebAssembly.

Built on the [Bungee](https://github.com/Signalsmith-Audio/bungee) phase vocoder library, this package provides both a framework-agnostic class-based API and optional React hooks for easy integration.

## Features

- **High Quality**: Phase vocoder algorithm for natural-sounding pitch shifts
- **Low Latency**: ~40ms latency using AudioWorklet and WebAssembly
- **Independent Control**: Separate pitch shifting (-12 to +12 semitones) and time stretching (0.5x to 2x speed)
- **Wet/Dry Mix**: Blend processed and original signals
- **Web Worker Support**: Offline processing, file loading, and waveform generation in background threads
- **Framework Agnostic**: Works with vanilla JavaScript or any framework
- **React Support**: Optional React hooks included
- **TypeScript**: Full TypeScript support with type definitions

## Installation

```bash
npm install bungee-pitch-shift
```

## Setup

After installation, copy the bundled processor to your public directory:

```bash
# Copy the bundled processor (includes embedded WASM)
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/
```

**That's it!** The bundled processor is completely self-contained:
- ✅ Includes the AudioWorklet processor
- ✅ Includes the Emscripten WASM glue code
- ✅ Includes the embedded WASM binary
- ✅ No additional files needed

**Note**: The path assumes your public directory is at `public/`. Adjust as needed for your project structure (e.g., `public/`, `static/`, `dist/`).

### Build Tool Integration

For automated copying during builds, add a postinstall script to your `package.json`:

```json
{
  "scripts": {
    "postinstall": "cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/"
  }
}
```

### Using a CDN

Load the bundled processor from a CDN or remote server:

```typescript
const pitchShift = await BungeePitchShift.create(audioContext, {
  workletPath: 'https://cdn.example.com/bungee-processor-bundled.js'
});
```

**Benefits:**
- No need to copy files to your public directory
- Better caching and performance via CDN
- Simplified deployment
- Share across multiple projects

**CORS Note:** When loading from a different domain, ensure the server sends appropriate CORS headers.

### Next.js / Vite / Other Build Tools

Copy the file to your static assets directory:

**Next.js:**
```bash
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/
```

**Vite:**
```bash
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/
```

**Create React App:**
```bash
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/
```

## Quick Start

### Vanilla JavaScript / TypeScript

```typescript
import { BungeePitchShift } from 'bungee-pitch-shift';

// Create audio context
const audioContext = new AudioContext();

// Initialize pitch shifter (uses bundled processor by default)
const pitchShift = await BungeePitchShift.create(audioContext);

// Or specify custom path:
// const pitchShift = await BungeePitchShift.create(audioContext, {
//   workletPath: '/bungee-processor-bundled.js'
// });

// Connect to audio graph
sourceNode.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);

// Control parameters
pitchShift.setPitch(5);    // +5 semitones (perfect fourth up)
pitchShift.setSpeed(1.2);  // 20% faster
pitchShift.setMix(0.8);    // 80% wet, 20% dry

// Cleanup when done
pitchShift.dispose();
```

### React

```tsx
import { usePitchShift } from 'bungee-pitch-shift/react';
import { useRef, useEffect } from 'react';

function AudioPlayer() {
  const audioContext = useRef(new AudioContext()).current;
  const {
    pitchShift,
    initialized,
    updatePitch,
    updateSpeed,
    updateMix,
    info
  } = usePitchShift(audioContext);

  useEffect(() => {
    if (!pitchShift || !initialized) return;

    // Connect to audio graph
    sourceNode.connect(pitchShift.node);
    pitchShift.connect(audioContext.destination);

    return () => {
      pitchShift.disconnect();
    };
  }, [pitchShift, initialized]);

  return (
    <div>
      <h3>Bungee v{info.version}</h3>

      <label>
        Pitch:
        <input
          type="range"
          min="-12"
          max="12"
          step="1"
          onChange={(e) => updatePitch(Number(e.target.value))}
        />
      </label>

      <label>
        Speed:
        <input
          type="range"
          min="0.5"
          max="2"
          step="0.1"
          onChange={(e) => updateSpeed(Number(e.target.value))}
        />
      </label>

      <label>
        Mix:
        <input
          type="range"
          min="0"
          max="1"
          step="0.01"
          onChange={(e) => updateMix(Number(e.target.value))}
        />
      </label>
    </div>
  );
}
```

## Web Worker Utilities

The `bungee-pitch-shift/worker` module provides Web Worker-based utilities for CPU-intensive audio operations. These tools keep your main thread responsive during heavy processing tasks.

### Features

- **Offline Processing**: Process entire audio files with pitch shifting
- **Audio File Loading**: Load and decode audio files off the main thread
- **Waveform Generation**: Generate visualization data without blocking UI
- **Fully Typed**: Complete TypeScript support
- **Transferable Objects**: Efficient zero-copy data transfer

### Setup

Copy the bundled worker file to your public directory:

```bash
cp node_modules/bungee-pitch-shift/dist/worker/audio-processor.worker.bundle.js public/
```

**Important**: Use the `.bundle.js` file (not `.worker.js`). The bundle file is self-contained and has all dependencies included.

### OfflineProcessor - Batch Audio Processing

Process entire audio files with pitch shifting in a worker thread:

```typescript
import { OfflineProcessor } from 'bungee-pitch-shift/worker';

const processor = new OfflineProcessor({
  workerPath: '/audio-processor.worker.bundle.js',
  workletPath: '/bungee-processor-bundled.js'
});

// Process audio buffer
const audioContext = new AudioContext();
const processed = await processor.processWithContext(
  audioBuffer,
  audioContext,
  {
    pitch: 5,      // +5 semitones
    speed: 1.2,    // 20% faster
    mix: 1.0,      // 100% wet
    onProgress: (progress) => {
      console.log(`Progress: ${Math.round(progress * 100)}%`);
    }
  }
);

// Cleanup
processor.dispose();
```

**Key Benefits:**
- Non-blocking: UI remains responsive during processing
- Progress tracking: Get real-time updates on processing status
- Faster than real-time: No playback constraints

### AudioFileLoader - Load Audio Files in Workers

Load and decode audio files without blocking the main thread:

```typescript
import { AudioFileLoader } from 'bungee-pitch-shift/worker';

const loader = new AudioFileLoader({
  workerPath: '/audio-processor.worker.bundle.js'
});

// Load from URL
const audioContext = new AudioContext();
const audioBuffer = await loader.loadWithContext(
  'https://example.com/audio.mp3',
  audioContext,
  {
    onProgress: (progress) => {
      console.log(`Loading: ${Math.round(progress * 100)}%`);
    }
  }
);

// Or decode raw data
const response = await fetch('audio.mp3');
const arrayBuffer = await response.arrayBuffer();
const decoded = await loader.decodeWithContext(arrayBuffer, audioContext);

// Cleanup
loader.dispose();
```

**Use Cases:**
- Large file loading without UI freezing
- Batch loading multiple files
- Pre-loading audio for web apps

### WaveformGenerator - Visualization Data

Generate waveform data for audio visualizations:

```typescript
import { WaveformGenerator } from 'bungee-pitch-shift/worker';

const generator = new WaveformGenerator({
  workerPath: '/audio-processor.worker.bundle.js'
});

// Generate waveform with 1000 samples
const waveform = await generator.generate(audioBuffer, {
  samples: 1000,
  channel: 0,      // Left channel (or -1 for average)
  useRMS: false    // Use peak values
});

// Use waveform.data for rendering
for (let i = 0; i < waveform.data.length; i++) {
  const amplitude = waveform.data[i];
  // Draw visualization bar
}

// Generate for multiple channels
const multiChannel = await generator.generateMultiChannel(audioBuffer, {
  samples: 1000
});

// Quick preview (low resolution)
const preview = await generator.generatePreview(audioBuffer, 100);

// High resolution
const hires = await generator.generateHighRes(audioBuffer);

// Cleanup
generator.dispose();
```

**Waveform Options:**
- `samples`: Number of samples in output (default: 1000)
- `channel`: Which channel to use (0 = left, 1 = right, -1 = average all)
- `useRMS`: Use RMS values instead of peaks (default: false)

### Worker Utilities

The worker module also exports useful utility functions:

```typescript
import {
  serializeAudioBuffer,
  deserializeAudioBuffer,
  getTransferables,
  downsampleAudioBuffer,
  validateAudioBuffer,
  formatDuration,
  formatBytes
} from 'bungee-pitch-shift/worker';

// Serialize for efficient transfer
const serialized = serializeAudioBuffer(audioBuffer);
const transferables = getTransferables(serialized);
worker.postMessage({ audioData: serialized }, transferables);

// Downsample for visualization
const waveformData = downsampleAudioBuffer(audioBuffer, 1000);

// Format helpers
console.log(formatDuration(audioBuffer.duration)); // "2:30"
console.log(formatBytes(arrayBuffer.byteLength));  // "1.5 MB"
```

### Worker Configuration

All worker classes accept configuration options:

```typescript
interface WorkerConfig {
  workerPath?: string;      // Path to worker script
  workletPath?: string;     // Path to bundled AudioWorklet processor
  timeout?: number;         // Operation timeout (ms)

  // Deprecated - no longer needed with bundled processor:
  wasmPath?: string;        // @deprecated
  wasmBinaryPath?: string;  // @deprecated
}
```

### Best Practices

1. **Reuse Worker Instances**: Create once, use multiple times
2. **Dispose When Done**: Always call `.dispose()` to clean up
3. **Progress Callbacks**: Use for better UX on long operations
4. **Error Handling**: Wrap worker calls in try-catch blocks

```typescript
const processor = new OfflineProcessor();

try {
  const result = await processor.processWithContext(
    audioBuffer,
    audioContext,
    { pitch: 5, speed: 1.0 }
  );
  // Use result...
} catch (error) {
  console.error('Processing failed:', error);
} finally {
  processor.dispose();
}
```

## API Reference

### `BungeePitchShift` Class

#### Static Methods

##### `create(audioContext, config?)`

Create and initialize a new pitch shifter instance.

```typescript
// Simplest usage (uses defaults)
const pitchShift = await BungeePitchShift.create(audioContext);

// With configuration
const pitchShift = await BungeePitchShift.create(audioContext, {
  workletPath: '/bungee-processor-bundled.js', // Path or URL to bundled processor
  initialPitch: 0,                              // Initial pitch in semitones
  initialSpeed: 1.0,                            // Initial speed multiplier
  initialMix: 1.0                               // Initial wet/dry mix
});
```

**Note:** `wasmPath` and `wasmBinaryPath` options are deprecated. The bundled processor has the WASM binary embedded, so these are no longer needed.

#### Instance Properties

- `node: AudioWorkletNode` - The AudioWorkletNode for connecting to audio graph
- `initialized: boolean` - Whether the processor is initialized
- `params: PitchShiftParams` - Current parameters (read-only)
- `info: BungeeInfo` - Information about the Bungee processor

#### Instance Methods

##### `setPitch(semitones: number)`

Set pitch shift in semitones.

```typescript
pitchShift.setPitch(7);   // Perfect fifth up
pitchShift.setPitch(-12); // One octave down
```

##### `setSpeed(speed: number)`

Set playback speed multiplier.

```typescript
pitchShift.setSpeed(1.5);  // 50% faster
pitchShift.setSpeed(0.75); // 25% slower
```

##### `setMix(mix: number)`

Set wet/dry mix (0 = fully dry, 1 = fully wet).

```typescript
pitchShift.setMix(1.0); // 100% processed
pitchShift.setMix(0.5); // 50/50 blend
pitchShift.setMix(0.0); // 100% dry (bypass)
```

##### `reset()`

Reset internal processor state. Call this when seeking in audio playback.

```typescript
pitchShift.reset();
```

##### `connect(destination: AudioNode)`

Connect to another audio node.

```typescript
pitchShift.connect(audioContext.destination);
```

##### `disconnect(destination?: AudioNode)`

Disconnect from audio nodes.

```typescript
pitchShift.disconnect();
```

##### `addEventListener(listener: EventListener)`

Add an event listener for processor events.

```typescript
pitchShift.addEventListener((event) => {
  if (event.type === 'initialized') {
    console.log('Version:', event.info.version);
  }
});
```

##### `removeEventListener(listener: EventListener)`

Remove an event listener.

##### `dispose()`

Clean up and release resources. Always call this when done.

```typescript
pitchShift.dispose();
```

### React Hook: `usePitchShift`

```typescript
const {
  pitchShift,      // BungeePitchShift instance
  initialized,     // boolean
  params,          // Current parameters
  info,            // Processor info
  updatePitch,     // (semitones: number) => void
  updateSpeed,     // (speed: number) => void
  updateMix,       // (mix: number) => void
  setEnabled,      // (enabled: boolean) => void
  reset            // () => void
} = usePitchShift(audioContext, config);
```

## TypeScript Types

```typescript
interface BungeePitchShiftConfig {
  /** Path to bundled processor (default: '/bungee-processor-bundled.js') */
  workletPath?: string;

  /** @deprecated - No longer needed, WASM is embedded in bundled processor */
  wasmPath?: string;

  /** @deprecated - No longer needed, WASM is embedded in bundled processor */
  wasmBinaryPath?: string;

  /** Initial pitch shift in semitones (default: 0) */
  initialPitch?: number;

  /** Initial playback speed (default: 1.0) */
  initialSpeed?: number;

  /** Initial wet/dry mix (default: 1.0) */
  initialMix?: number;
}

interface PitchShiftParams {
  pitchSemitones: number;
  speed: number;
  mix: number;
}

interface BungeeInfo {
  version?: string;
  edition?: string;
  latency?: number;
}
```

## Examples

### Audio File Player with Pitch Shift

```typescript
import { BungeePitchShift } from 'bungee-pitch-shift';

const audioContext = new AudioContext();
const pitchShift = await BungeePitchShift.create(audioContext);

// Load audio file
const response = await fetch('audio.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

// Create source
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;

// Connect: source → pitch shift → destination
source.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);

// Play with pitch shift
pitchShift.setPitch(3);
source.start();
```

### Real-time Microphone Processing

```typescript
import { BungeePitchShift } from 'bungee-pitch-shift';

const audioContext = new AudioContext();
const pitchShift = await BungeePitchShift.create(audioContext);

// Get microphone input
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioContext.createMediaStreamSource(stream);

// Connect: mic → pitch shift → destination
source.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);

// Apply pitch shift
pitchShift.setPitch(-5);
```

### Integration with Existing Effects Chain

```typescript
const reverb = audioContext.createConvolver();
const delay = audioContext.createDelay();
const gain = audioContext.createGain();

// Complex chain: source → pitch → reverb → delay → gain → destination
source.connect(pitchShift.node);
pitchShift.connect(reverb);
reverb.connect(delay);
delay.connect(gain);
gain.connect(audioContext.destination);
```

## Browser Compatibility

Requires:
- Web Audio API with AudioWorklet support
- WebAssembly support

Supported browsers:
- Chrome/Edge 66+
- Firefox 76+
- Safari 14.1+

## Performance

- **Latency**: ~40ms (depending on buffer size and sample rate)
- **CPU Usage**: Efficient WASM implementation
- **Automatic Bypass**: When pitch=0 and speed=1.0, processing is bypassed for maximum performance

## License

MIT

## Credits

Built on [Bungee](https://github.com/Signalsmith-Audio/bungee) by Signalsmith Audio.
