Native Node.js bindings for PortAudio
  • C 94.2%
  • JavaScript 4.9%
  • Python 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Andrew Gundersen f2e4bf2893 Rewrite README to document architecture, API, and usage
Expand the README with a motivation section, architecture diagram covering
the lock-free ring buffer and N-API threadsafe callback design, full API
reference table, usage examples for recording/playback and device hot-swap,
and an honest accounting of current constraints.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-08 12:04:50 -05:00
portaudio adding linux portaudio bin 2022-05-17 16:57:27 -05:00
src fixed fs, updated README 2022-04-28 10:30:26 -05:00
test cancel playback functionality, on write event 2021-08-26 09:04:25 -05:00
.gitignore moved to yarn, updated README and .gitignore 2022-03-14 09:50:03 -05:00
.npmrc New PortAudio build, refined linking config, updated README.md 2022-03-10 07:20:38 -06:00
binding.gyp Update binding.gyp 2022-05-18 12:30:14 +00:00
index.js read write switch stream test, .npmrc for publishing 2021-08-19 15:16:01 -05:00
package-lock.json minor updates 2022-05-18 07:28:36 -05:00
package.json changing sleep to dev dependency 2022-05-18 06:48:57 -05:00
README.md Rewrite README to document architecture, API, and usage 2026-08-08 12:04:50 -05:00

nodeaudio

Native Node.js bindings for PortAudio, providing real-time audio capture and playback to JavaScript applications.

Built as a C addon using N-API, with lock-free ring buffers and thread-safe callbacks to bridge PortAudio's real-time audio threads to the Node.js event loop without dropping samples or blocking the main thread.

Motivation

Doing real-time audio in Node.js is hard: audio callbacks fire on high-priority OS threads that cannot safely touch JavaScript state, and any blocking work in the callback causes audible glitches. nodeaudio solves this with a lock-free ring buffer between the PortAudio callback and the Node.js main thread, and uses N-API's thread-safe functions to marshal captured audio into JavaScript as Int16 typed arrays.

The library is designed to support live-audio workflows in Node (recording, playback, and hot-swapping the default input/output device without restarting the process).

Architecture

    ┌─────────────────────┐          ┌─────────────────────┐
    │   PortAudio thread  │          │  Node.js main loop  │
    │   (real-time, RT)   │          │                     │
    │                     │          │                     │
    │   input_callback ──▶│ ringbuf ─│──▶ "data" event     │
    │                     │          │                     │
    │   output_callback ◀─│ ringbuf ◀│─── WriteToOutput    │
    │                     │          │                     │
    └─────────────────────┘          └─────────────────────┘
  • Lock-free ring buffers (pa_ringbuffer) sit between the audio callback and the JS thread, so the RT callback never blocks on a mutex.
  • N-API threadsafe functions (napi_create_threadsafe_function) safely dispatch data and write events from the audio thread onto the Node.js event loop.
  • Large writes are backgrounded. WriteToOutputStream returns immediately; if the payload exceeds the ring buffer capacity, a pthread drains it into the buffer as space frees up, so JavaScript stays responsive.
  • Cancellation. CancelPlayback sets a flag that the background thread checks, so a long playback can be interrupted mid-write.
  • Runtime device switching. Depends on a forked PortAudio that adds Pa_RefreshDevices, letting the app detect device changes (e.g. plugging in headphones, swapping a USB mic) without a restart.

Installation

nodeaudio ships a prebuilt PortAudio dylib/so under portaudio/bin. Build the addon with:

node-gyp rebuild

The build copies libportaudio.dylib (macOS) or libportaudio.so (Linux) into build/Release alongside the compiled nodeaudio.node and dynamically links against it via @loader_path.

To verify the addon links against the bundled PortAudio:

otool -L build/Release/nodeAudio.node   # macOS
ldd build/Release/nodeAudio.node        # Linux

If you rebuild PortAudio from source (see the fork), fix up the install name so the dylib resolves relative to the addon:

install_name_tool -id "@loader_path/libportaudio.dylib" libportaudio.dylib

Usage

const nodeAudio = require('@crimata/nodeaudio');
const EventEmitter = require('events');

const emitter = new EventEmitter();
const chunks = [];

emitter.on('data', (int16Arr) => {
    chunks.push(int16Arr);         // fires from the audio thread as PCM arrives
});

emitter.on('write', () => {
    // fires while playback is draining the output buffer
});

// Wire the emitter into the addon and open default streams
nodeAudio.core.Initialize(emitter.emit.bind(emitter));
nodeAudio.core.OpenInputStream(nodeAudio.core.GetDefaultInputDevice());
nodeAudio.core.OpenOutputStream(nodeAudio.core.GetDefaultOutputDevice());

// Record for 2 seconds, then play it back
setTimeout(() => {
    const pcm = nodeAudio.utils.mergeChunks(chunks);
    nodeAudio.core.WriteToOutputStream(pcm.buffer);

    setTimeout(() => nodeAudio.core.Terminate(), 5000);
}, 2000);

Hot-swapping devices

Because GetDefaultInputDevice / GetDefaultOutputDevice call the fork's Pa_RefreshDevices under the hood, you can poll for device changes and reopen the streams when the user plugs in new hardware:

setInterval(() => {
    const input = nodeAudio.core.GetDefaultInputDevice();
    if (input !== currentInput) {
        nodeAudio.core.CloseInputStream();
        nodeAudio.core.OpenInputStream(input);
        currentInput = input;
    }
}, 2000);

See test/rwSwitchStream.js for a fuller example (keyboard-toggled record/play with periodic device rescanning).

API

All core methods hang off nodeAudio.core; JS helpers live on nodeAudio.utils.

Method Description
Initialize(emit) Initialize PortAudio and register a Node EventEmitter's emit for data and write events.
Terminate() Abort any active streams, free ring buffers, and shut PortAudio down.
GetDefaultInputDevice() Refresh the device list and return the current default input device index.
GetDefaultOutputDevice() Refresh the device list and return the current default output device index.
OpenInputStream(deviceIndex) Open and start a capture stream. Emits data events with Int16Array chunks.
OpenOutputStream(deviceIndex) Open and start a playback stream fed by WriteToOutputStream.
CloseInputStream() Close the active capture stream.
CloseOutputStream() Close the active playback stream.
WriteToOutputStream(arrayBuffer) Enqueue Int16 PCM for playback. Non-blocking; large writes are drained on a background pthread.
CancelPlayback() Cancel an in-progress background write.
utils.mergeChunks(chunks) Concatenate an array of Int16Arrays into one.

Events

Both events are emitted on the Node.js main thread via N-API threadsafe functions.

  • data(int16Arr) — fired by the input callback whenever a new frame block arrives from the microphone. int16Arr is a mono Int16Array (see current constraints below).
  • write() — fired by the output callback each time playback drains frames from the ring buffer, so JS can track playback progress.

Current constraints

The addon is opinionated for the recording/playback use case it was built for. If you need something more general, these are the knobs to change:

  • Sample rate is hardcoded to 16 kHz (src/na_utils.c).
  • Sample format is paInt16, mono, 1 channel.
  • Ring buffer is fixed at 8192 frames (src/na_buffer.c).
  • Platforms: binding.gyp has conditions for macOS and Linux only.

Layout

src/na_front.c        N-API glue — declares the exported JS methods
src/na_core.c         Stream lifecycle, playback thread, PortAudio calls
src/na_callbacks.c    Audio-thread callbacks + threadsafe JS dispatch
src/na_buffer.c       Ring buffer init/teardown
src/na_utils.c        Stream parameter setup
src/pa_ringbuffer.c   PortAudio's lock-free ring buffer (vendored)
portaudio/            Bundled headers + prebuilt libportaudio for linking
test/                 Example scripts (record/play, device switching)
index.js              JS entrypoint — re-exports the addon and utility helpers
  • portaudio (fork) — PortAudio fork adding Pa_RefreshDevices, required by this package for runtime device detection.

Publishing

The registry URL and credentials are in .npmrc; API_TOKEN is a GitLab token with api scope. Publish via Yarn (yarn publish) — npm returns 400s against the private GitLab registry.