Skip to content

gozmanyoni/pcm-ringbuf-player

Repository files navigation

pcm-ringbuf-player

A TypeScript library for playing PCM audio in the browser with support for multiple formats (Int16, Int24, Int32, Float32). Uses a ring buffer for efficient, low-latency transport of data between the main thread and the audio thread.

Features

  • Multiple PCM formats: Int16Array, Int32Array, Float32Array
  • 24-bit PCM support with conversion utilities
  • TypeScript generics for type-safe audio data handling
  • Configurable buffer size for different latency/stability requirements
  • Low-latency playback using AudioWorklet and SharedArrayBuffer
  • Volume control with optional ramping
  • Type-aware conversion automatically normalizes to Float32 for Web Audio API
  • Official Vite and Webpack plugins for seamless integration

Installation

npm install pcm-ringbuf-player

Build Tool Integration

Since pcm-ringbuf-player uses AudioWorklet and SharedArrayBuffer, it requires:

  1. The audio.worklet.js file to be copied to your output directory
  2. Proper HTTP headers for SharedArrayBuffer support

The library provides official plugins for Vite and Webpack to automate both of these requirements.

Vite Plugin

npm install pcm-ringbuf-player

vite.config.ts:

import { defineConfig } from 'vite'
import { pcmPlayerPlugin } from 'pcm-ringbuf-player/vite'

export default defineConfig({
  plugins: [
    pcmPlayerPlugin(), // Automatically copies worklet and sets headers
  ],
})

Options:

pcmPlayerPlugin({
  setHeaders: true  // Set to false to disable automatic CORS headers (default: true)
})

What the plugin does:

  • ✅ Copies audio.worklet.js to your dist/ folder during build
  • ✅ Serves the worklet file at /audio.worklet.js in dev mode
  • ✅ Automatically sets required SharedArrayBuffer headers:
    • Cross-Origin-Opener-Policy: same-origin
    • Cross-Origin-Embedder-Policy: require-corp

Webpack Plugin

npm install pcm-ringbuf-player

webpack.config.js:

const { PcmPlayerWebpackPlugin } = require('pcm-ringbuf-player/webpack')

module.exports = {
  plugins: [
    new PcmPlayerWebpackPlugin(), // Automatically copies worklet and sets headers
  ],
}

Options:

new PcmPlayerWebpackPlugin({
  outputDir: 'dist',     // Output directory (default: 'dist')
  setHeaders: true       // Auto-configure webpack-dev-server headers (default: true)
})

What the plugin does:

  • ✅ Copies audio.worklet.js to your output directory after build
  • ✅ Automatically configures webpack-dev-server headers for SharedArrayBuffer
  • ✅ Works with both build and development modes

Manual header configuration (if setHeaders: false):

const { PcmPlayerWebpackPlugin } = require('pcm-ringbuf-player/webpack')

module.exports = {
  plugins: [
    new PcmPlayerWebpackPlugin({ setHeaders: false }),
  ],
  devServer: {
    ...PcmPlayerWebpackPlugin.getDevServerConfig(), // Manual config
  },
}

Without a Build Tool Plugin

If you're not using Vite or Webpack, you need to:

  1. Copy the worklet file manually from node_modules/pcm-ringbuf-player/dist/audio.worklet.js to your public/static directory

  2. Set the required headers on your development server:

    Cross-Origin-Opener-Policy: same-origin
    Cross-Origin-Embedder-Policy: require-corp
    
  3. Load the worklet with the correct path in your code:

    const player = new PcmPlayer(48000, 2)
    await player.start() // Loads from /audio.worklet.js by default

Quick Start

Basic Usage (Int16)

import { PcmPlayer } from 'pcm-ringbuf-player'

// Create a player with 48kHz sample rate and stereo (2 channels)
const player = new PcmPlayer(48000, 2)

// Start the audio worklet
await player.start()

// Feed PCM audio data (Int16Array)
const pcmData = new Int16Array([/* your audio samples */])
player.feed(pcmData)

// Control volume (0.0 to 1.0)
player.volume(0.5)

// Stop playback and cleanup
await player.stop()

Using Different PCM Formats

import { PcmPlayer } from 'pcm-ringbuf-player'

// Int32 PCM (32-bit signed integer)
const int32Player = new PcmPlayer<Int32Array>(48000, 2, Int32Array)
await int32Player.start()
const int32Data = new Int32Array([/* samples in range -2147483648 to 2147483647 */])
int32Player.feed(int32Data)

// Float32 PCM (normalized -1.0 to 1.0)
const float32Player = new PcmPlayer<Float32Array>(48000, 2, Float32Array)
await float32Player.start()
const float32Data = new Float32Array([/* samples in range -1.0 to 1.0 */])
float32Player.feed(float32Data)

Custom Buffer Size

// Default buffer: 1000 blocks (~2.7 seconds at 48kHz stereo)
const defaultPlayer = new PcmPlayer<Int16Array>(48000, 2, Int16Array)

// Larger buffer for unstable networks or background tabs (5 seconds)
const largeBufferPlayer = new PcmPlayer<Int16Array>(48000, 2, Int16Array, 2000)

// Smaller buffer to reduce memory usage (1.3 seconds)
const smallBufferPlayer = new PcmPlayer<Int16Array>(48000, 2, Int16Array, 500)

Example

See the example folder for a complete React + Vite + TypeScript application demonstrating:

  • Tone Player: Programmatic PCM tone generation with selectable formats (Int16/Int32/Float32)
  • WAV File Player: Upload and play WAV files (supports 8-bit, 16-bit, 24-bit, 32-bit PCM and 32-bit Float)
  • Real-time audio playback with play/stop/restart controls
  • Proper setup with required SharedArrayBuffer headers

To run the example:

cd example
npm install
npm run dev

The example automatically detects WAV file formats including:

  • PCM (8-bit, 16-bit, 24-bit, 32-bit integer)
  • IEEE Float (32-bit)
  • WAVE_FORMAT_EXTENSIBLE (common in 24-bit and 32-bit files)

API

new PcmPlayer<T>(sampleRate, channels, pcmType?, maxBlocks?)

Creates a new PCM player instance.

Parameters:

  • sampleRate: number - Sample rate in Hz (e.g., 48000, 44100)
  • channels: number - Number of audio channels (1 for mono, 2 for stereo)
  • pcmType?: PcmArrayConstructor<T> - TypedArray constructor (Int16Array, Int32Array, or Float32Array). Default: Int16Array
  • maxBlocks?: number - Buffer size in blocks (128 samples per block). Default: 1000 (~2.7 seconds)

Returns: PcmPlayer<T> instance

Examples:

// Default: Int16Array with 1000 blocks
const player1 = new PcmPlayer(48000, 2)

// Explicit Int32Array with default buffer
const player2 = new PcmPlayer<Int32Array>(48000, 2, Int32Array)

// Float32Array with custom buffer size
const player3 = new PcmPlayer<Float32Array>(48000, 2, Float32Array, 2000)

async start(): Promise<void>

Initializes the AudioWorklet and starts the player. Must be called before feeding data.

feed(source: T): void

Feeds PCM audio data to the player. The type T matches the type specified in the constructor.

Parameters:

  • source: T - PCM audio data (Int16Array, Int32Array, or Float32Array)

Notes:

  • For stereo, data should be interleaved: [L, R, L, R, ...]
  • Data is automatically converted to Float32 for Web Audio API:
    • Int16: divided by 32768
    • Int32: divided by 2147483648
    • Float32: passed through (already normalized -1.0 to 1.0)

volume(volume: number, duration?: number): void

Sets the playback volume.

Parameters:

  • volume: number - Volume level (0.0 to 1.0)
  • duration?: number - Optional ramp duration in seconds. Default: 0

async stop(): Promise<void>

Stops playback and cleans up resources. Closes the AudioContext and disconnects all nodes.

getRawBuffer(): SharedArrayBuffer

Returns the underlying SharedArrayBuffer used for the ring buffer. Useful for advanced use cases or debugging.

Returns: SharedArrayBuffer

Type Exports

The library exports the following types for TypeScript users:

import type { PcmArrayType, PcmArrayConstructor } from 'pcm-ringbuf-player'

// PcmArrayType = Int16Array | Int32Array | Float32Array
// PcmArrayConstructor<T> - Conditional type for array constructors

24-bit PCM Support

Since JavaScript doesn't have a native Int24Array type, 24-bit PCM data must be converted to Int32Array. The library provides utility functions for this:

pcm24ToInt32(data, littleEndian?): Int32Array

Converts 24-bit PCM data (stored as Uint8Array) to Int32Array.

Parameters:

  • data: Uint8Array - Raw 24-bit PCM data (3 bytes per sample)
  • littleEndian?: boolean - Byte order (default: true)

Returns: Int32Array with converted samples

Example:

import { pcm24ToInt32 } from 'pcm-ringbuf-player'

// 24-bit PCM data from file or network
const pcm24Data = new Uint8Array([/* 3 bytes per sample */])

// Convert to Int32Array for playback
const int32Data = pcm24ToInt32(pcm24Data, true) // little-endian

// Use with PcmPlayer
const player = new PcmPlayer<Int32Array>(48000, 2, Int32Array)
await player.start()
player.feed(int32Data)

pcm24BufferToInt32(buffer, offset?, length?, littleEndian?): Int32Array

Converts 24-bit PCM data from ArrayBuffer to Int32Array.

Parameters:

  • buffer: ArrayBuffer - Buffer containing 24-bit PCM data
  • offset?: number - Byte offset (default: 0)
  • length?: number - Length in bytes (default: entire buffer from offset)
  • littleEndian?: boolean - Byte order (default: true)

Returns: Int32Array with converted samples

Example:

import { pcm24BufferToInt32 } from 'pcm-ringbuf-player'

// From WAV file or other source
const arrayBuffer = await file.arrayBuffer()

// Convert starting at byte 44 (typical WAV data offset), 1000 bytes
const int32Data = pcm24BufferToInt32(arrayBuffer, 44, 1000)

getPcm24Range(): { min, max, bits }

Returns the valid range for 24-bit PCM values.

Returns: { min: -8388608, max: 8388607, bits: 24 }

Buffer Size Guidelines

The maxBlocks parameter controls the ring buffer size. Each block is 128 samples (RENDER_QUANTUM_FRAMES).

Formula: Buffer duration (seconds) ≈ (maxBlocks * 128) / (sampleRate * channels)

maxBlocks 48kHz Stereo 48kHz Mono 44.1kHz Stereo Use Case
250 ~0.67s ~1.3s ~0.72s Low latency, stable networks
500 ~1.3s ~2.7s ~1.45s Balanced, memory constrained
1000 (default) ~2.7s ~5.3s ~2.9s Recommended for most cases
2000 ~5.3s ~10.7s ~5.8s Unstable networks, background tabs

Recommendations:

  • Increase for: network streams, background tab performance, slower devices
  • Decrease for: real-time applications, memory-constrained environments
  • Monitor console for "UNDERFLOW" messages to tune appropriately

Supported PCM Formats

Format TypedArray Range Bytes/Sample Common Usage
16-bit PCM Int16Array -32768 to 32767 2 CD quality, most common
24-bit PCM Int32Array* -8388608 to 8388607 3 Professional audio, studio recordings
32-bit PCM Int32Array -2147483648 to 2147483647 4 High precision integer
32-bit Float Float32Array -1.0 to 1.0 4 Professional audio, DAWs

Note: *24-bit PCM is automatically converted to Int32Array (JavaScript has no Int24Array type). The conversion preserves full 24-bit precision.

Contributing

Contributions are welcome! Please open a PR with:

  • Clear description of changes
  • Tests for new features
  • Updated documentation

To Do

  • Tests
  • Support multiple TypedArrays (Int16, Int32, Float32)
  • Documentation
  • Configurable buffer size
  • Support for 24-bit PCM
  • Real-time resampling
  • Support for other formats (8-bit signed, 64-bit)

Performance Tips

  1. Feed in chunks: Don't feed the entire audio file at once. Feed in small chunks (50-100ms) to avoid blocking the main thread.

    const chunkSize = sampleRate * channels * 0.05 // 50ms chunks
    for (let i = 0; i < pcmData.length; i += chunkSize) {
      player.feed(pcmData.slice(i, i + chunkSize))
      await new Promise(resolve => setTimeout(resolve, 40)) // Feed faster than playback
    }
  2. Pre-buffer audio: Feed several chunks before playback starts to build up a buffer.

    await player.start()
    // Feed 1 second of audio before considering playback "started"
    for (let i = 0; i < 20; i++) {
      player.feed(chunk)
    }
  3. Monitor underflows: Check the browser console for "UNDERFLOW" messages. If you see these frequently, increase maxBlocks.

  4. Choose appropriate format:

    • Use Int16Array for most cases (2 bytes/sample, good quality)
    • Use 24-bit PCM → Int32Array for professional audio (3 bytes/sample source, 4 bytes/sample converted)
    • Use Int32Array for high-precision requirements (4 bytes/sample)
    • Use Float32Array when audio is already normalized (4 bytes/sample, common in DSP)

Compatibility

Browser Support

This library uses SharedArrayBuffer and AudioWorklet, which require:

  • Chrome/Edge 68+
  • Firefox 79+
  • Safari 14.1+

Required Headers

To enable SharedArrayBuffer, the server must set these HTTP headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Using build plugins: The official Vite and Webpack plugins automatically configure these headers for you in both development and production.

Manual configuration: If not using the plugins, you'll need to configure your server to send these headers. See the Build Tool Integration section for manual setup instructions.

For more information see the SharedArrayBuffer MDN docs.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages