Plays PCM Audio in the browser leveraging a ring buffer for efficient transport of data
npm install pcm-ringbuf-playerA 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.
- ✅ 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
``bash`
npm install pcm-ringbuf-player
Since pcm-ringbuf-player uses AudioWorklet and SharedArrayBuffer, it requires:audio.worklet.js
1. The 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.
`bash`
npm install pcm-ringbuf-player
vite.config.ts:
`typescript
import { defineConfig } from 'vite'
import { pcmPlayerPlugin } from 'pcm-ringbuf-player/vite'
export default defineConfig({
plugins: [
pcmPlayerPlugin(), // Automatically copies worklet and sets headers
],
})
`
Options:
`typescript`
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/audio.worklet.js
- ✅ Serves the worklet file at in dev modeCross-Origin-Opener-Policy: same-origin
- ✅ Automatically sets required SharedArrayBuffer headers:
- Cross-Origin-Embedder-Policy: require-corp
-
`bash`
npm install pcm-ringbuf-player
webpack.config.js:
`javascript
const { PcmPlayerWebpackPlugin } = require('pcm-ringbuf-player/webpack')
module.exports = {
plugins: [
new PcmPlayerWebpackPlugin(), // Automatically copies worklet and sets headers
],
}
`
Options:
`javascript`
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):
`javascript
const { PcmPlayerWebpackPlugin } = require('pcm-ringbuf-player/webpack')
module.exports = {
plugins: [
new PcmPlayerWebpackPlugin({ setHeaders: false }),
],
devServer: {
...PcmPlayerWebpackPlugin.getDevServerConfig(), // Manual config
},
}
`
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:
`typescript`
const player = new PcmPlayer(48000, 2)
await player.start() // Loads from /audio.worklet.js by default
`typescript
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()
`
`typescript
import { PcmPlayer } from 'pcm-ringbuf-player'
// Int32 PCM (32-bit signed integer)
const int32Player = new PcmPlayer
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
await float32Player.start()
const float32Data = new Float32Array([/ samples in range -1.0 to 1.0 /])
float32Player.feed(float32Data)
`
`typescript
// Default buffer: 1000 blocks (~2.7 seconds at 48kHz stereo)
const defaultPlayer = new PcmPlayer
// Larger buffer for unstable networks or background tabs (5 seconds)
const largeBufferPlayer = new PcmPlayer
// Smaller buffer to reduce memory usage (1.3 seconds)
const smallBufferPlayer = new PcmPlayer
`
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:
`bash`
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)
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
- - TypedArray constructor (Int16Array, Int32Array, or Float32Array). Default: Int16ArraymaxBlocks?: number
- - Buffer size in blocks (128 samples per block). Default: 1000 (~2.7 seconds)
Returns: PcmPlayer instance
Examples:
`typescript
// Default: Int16Array with 1000 blocks
const player1 = new PcmPlayer(48000, 2)
// Explicit Int32Array with default buffer
const player2 = new PcmPlayer
// Float32Array with custom buffer size
const player3 = new PcmPlayer
`
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)$3
Sets the playback volume.Parameters:
-
volume: number - Volume level (0.0 to 1.0)
- duration?: number - Optional ramp duration in seconds. Default: 0$3
Stops playback and cleans up resources. Closes the AudioContext and disconnects all nodes.$3
Returns the underlying SharedArrayBuffer used for the ring buffer. Useful for advanced use cases or debugging.Returns:
SharedArrayBufferType Exports
The library exports the following types for TypeScript users:
`typescript
import type { PcmArrayType, PcmArrayConstructor } from 'pcm-ringbuf-player'// PcmArrayType = Int16Array | Int32Array | Float32Array
// PcmArrayConstructor - 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:
$3
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 samplesExample:
`typescript
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(48000, 2, Int32Array)
await player.start()
player.feed(int32Data)
`$3
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 samplesExample:
`typescript
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)
`$3
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
- [x] Tests
- [x] Support multiple TypedArrays (Int16, Int32, Float32)
- [x] Documentation
- [x] Configurable buffer size
- [x] 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.
`typescript
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.
`typescript
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
$3
This library uses
SharedArrayBuffer and AudioWorklet, which require:
- Chrome/Edge 68+
- Firefox 79+
- Safari 14.1+$3
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.
MIT