# nodeaudio Native Node.js bindings for [PortAudio](http://www.portaudio.com/), 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](https://andrewgundersen.net/repos/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](https://andrewgundersen.net/repos/portaudio)), fix up the install name so the dylib resolves relative to the addon: install_name_tool -id "@loader_path/libportaudio.dylib" libportaudio.dylib ## Usage ```js 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: ```js 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 `Int16Array`s 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 ## Related - [portaudio (fork)](https://andrewgundersen.net/repos/portaudio) — 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.