Native Bindings
libsonare ships three bindings: browser WASM, Python, and a Node.js native addon. This page covers what differs between the three (initialization, installation, release semantics, errors, and the Node-native specifics); the full Node native function reference lives on its own page. See the individual API pages for each language:
- Python API — ctypes-based bindings with wheels on PyPI
- Node.js Native API — Native addon for direct C++ performance
For beginners, the choice is usually simple: use Python for scripts and notebooks, use WASM for browser apps, and use Node native only when you specifically need native file decoding or native runtime performance from Node.js.
| You are building | Use | Package |
|---|---|---|
| A browser app | WASM | @libraz/libsonare |
| A Python script or notebook | Python | pip install libsonare |
| A Node.js app needing native decode or performance | Node native | @libraz/libsonare-native (build from source; not on npm) |
What You Will Learn
By the end of this page you should be able to:
- choose between browser WASM, Python, and Node native without treating them as interchangeable packages;
- build and import the Node N-API addon when native decoding or performance is required;
- understand which examples use
@libraz/libsonareand which use@libraz/libsonare-native; - map native addon functions back to the broader JavaScript, Python, mastering, and mixing docs.
Comparison
| WebAssembly | Python | Node.js (N-API) | |
|---|---|---|---|
| Platform | Browser | Desktop | Desktop |
| Initialization | await init() | None | None |
| Distribution | npm (@libraz/libsonare) | PyPI (pip install libsonare) | Source (bindings/node) |
| Build | Emscripten | Pre-built wheels (or CMake + pip) | CMake + cmake-js |
| Performance | Near-native | Native | Native |
| Handle release | delete() | with / close() | destroy(), or using where the runtime supports it |
| File I/O | Sample-based APIs; Audio.fromMemory(...) decodes WAV/MP3 bytes, and the browser decode path can read additional supported formats | WAV/MP3 by default; FFmpeg formats in FFmpeg builds | WAV/MP3 by default; FFmpeg formats in FFmpeg builds |
| Ordinary analysis, effects, features, conversions, mastering, mixing, streaming | Same feature set | Same feature set | Same feature set |
Node.js (N-API)
The Node.js binding is a native addon built with N-API, providing direct C++ performance without WebAssembly overhead.
What are N-API and a "native addon"?
A native addon is a compiled C/C++ module that Node loads like a normal package. It runs real machine code instead of JavaScript or WebAssembly.
N-API (Node-API) is the stable interface Node provides for building these addons. It shields the addon from V8 engine internals, so one compiled binary can keep working across Node versions without recompiling.
The practical upside is native speed and direct file decoding from Node. The cost is that the addon must be built or installed for your platform, instead of running everywhere like the WASM package.
Choosing A Node Package
| Package | Initialization | Use when |
|---|---|---|
@libraz/libsonare | Call await init() before use | You want the browser-compatible WASM package or the exact browser-demo API |
@libraz/libsonare-native | No WASM init; import and call functions directly | You need native file decoding, native runtime performance, or source-tree addon development |
Examples in JavaScript API use the WASM package. Examples that use the native addon carry the @libraz/libsonare-native import path, and the full native reference is on the Node.js Native API page.
Requirements
- Node.js 22+
- CMake 3.16+
- C++17 compiler
- Yarn 4+
Installation
@libraz/libsonare-native is not published to npm. Build it from bindings/node in the source tree; to use it from another project, reference the built local package through your workspace or a file: dependency.
git clone https://github.com/libraz/libsonare.git
cd libsonare/bindings/node
yarn install
yarn buildABI Versions
A binding and the shared library it loads must come from the same tree. The C ABI carries a version per subsystem, each one guarding the exact size and member offsets of that subsystem's flat POD structs, and a binding compares them on load so a layout mismatch is caught before a single byte is exchanged. The practical rule: rebuild the library whenever you rebuild the binding, and never point a binding at a shared library from another checkout.
| ABI | Current | Covers |
|---|---|---|
| Feature | 7 | the flat analysis and feature-result structs plus the shipped general C function contracts — SonareKey, SonareAnalysisResult, SonareChordDetectionOptions, and their neighbors |
| Project | 2 | the headless-DAW project and arrangement structs. Reports 0 in a build without arrangement support |
| Voice changer | 3 | the realtime voice-changer configuration struct |
| Acoustic | 5 | the room-acoustics structs |
| Engine | 3 | the realtime command queue |
sonare_abi_version() packs the first four into one uint32_t (feature in bits 0-7, project in 8-15, voice changer in 16-23, acoustic in 24-31, aggregate 0x05030207), so one comparison covers all of them. The engine ABI is deliberately excluded and keeps its own accessor, sonare_engine_abi_version(), because it versions a SharedArrayBuffer record layout rather than a POD struct. Each subsystem's header owns its macro (SONARE_FEATURE_ABI_VERSION, SONARE_PROJECT_ABI_VERSION, SONARE_VOICE_CHANGER_ABI_VERSION, SONARE_ACOUSTIC_ABI_VERSION). The feature macro also covers shipped general C function contracts, so a signature change such as the note APIs' voiced argument moves it; the other macros move on layout changes to the structs they guard. The project version moves once per release that changes its layout. Equal values therefore mean identical layout and covered contracts, and a field appended to a guarded struct is a bump, not an additive change.
What each binding does with the number differs, and it decides where a stale library shows up (the error contract puts it beside every other failure, and Installation covers the fix):
- Python compares the packed value against the one it was built for when the library first loads, before configuring a single symbol, and raises a
SonareErrorwithErrorCode.ABI_MISMATCH(10) and a message beginninglibsonare ABI mismatch— a shared library from another checkout fails on first library use, never partway through a call. ItsRealtimeEngineandProjectconstructors, andProject.from_json, check the engine and project versions the same way. The Python CLI exits with 13. - Node native and WASM make the same comparison themselves and throw a
SonareErrorwithErrorCode.AbiMismatch(10): Node when the addon is required, WASM ininit(). Both export the query functionsabiVersion(),projectAbiVersion(),engineAbiVersion(),voiceChangerAbiVersion()and the constantsEXPECTED_PROJECT_ABI_VERSION,EXPECTED_ABI_VERSIONandEXPECTED_ENGINE_ABI_VERSION(WASM also exposesEXPECTED_ABI_VERSIONfrom/worklet); the Node addon checks the engine ABI when aRealtimeEngineis constructed.projectAbiVersion()equals the expected constant when arrangement support is compiled in and0when it is not, and WASM'sengineCapabilities()reportsengineAbiVersion,expectedEngineAbiVersionandabiCompatible. The engine ABI is outside the packed value, so eachRealtimeEngineconstructor compares it separately and throwsAbiMismatchbefore creating the engine; Node'sProjectconstructor andProject.fromJsondo the same with the project version. - A C caller compares
sonare_abi_version()with its compile-timeSONARE_ABI_VERSIONonce; the JSON entry points (the voice changer's JSON configuration, the capability catalog) tolerate layout drift and need no gate. See C ABI versions.
Additive struct changes are gated on a per-struct struct_version so existing C callers keep their behavior at both source and call level. SonareNoteSegmenterConfig is the case to know: it carries a trailing voiced_threshold read only when struct_version is 2, so a config filled the way it always was is unaffected, and a zero-initialized one keeps the 0.5 default. What does change is the version the library reports — which is why the library has to be rebuilt alongside the binding even when your own code is untouched.
Mastering API
Node users can choose between the WASM npm package and the native addon:
| Package | Use when |
|---|---|
@libraz/libsonare | You want the same API as the browser demo or need Web-compatible WASM. |
@libraz/libsonare-native | You need native file decoding or native runtime performance in Node.js. |
import {
masterAudioStereo,
masteringChainStereo,
masteringAssistantSuggest,
masteringAssistantSuggestStereo,
masteringAudioProfile,
masteringAudioProfileStereo,
masteringPresetNames,
masteringPairAnalyze,
masteringProcessorNames,
masteringStreamingPreview,
masteringStreamingPreviewStereo,
} from '@libraz/libsonare-native'
console.log(masteringProcessorNames())
console.log(masteringPresetNames())
const mastered = masteringChainStereo(left, right, sampleRate, {
dynamics: {
compressor: {
thresholdDb: -18,
ratio: 2.2,
autoMakeup: true,
},
},
loudness: {
targetLufs: -14,
ceilingDb: -1,
truePeakOversample: 4,
},
})
console.log(mastered.outputLufs, mastered.stages)
const presetMaster = masterAudioStereo(left, right, sampleRate, 'pop', {
loudness: { targetLufs: -14 },
})
console.log(presetMaster.outputLufs, presetMaster.stages)
const matchReport = JSON.parse(
masteringPairAnalyze('match.referenceLoudness', source, reference, sampleRate),
)
const masteredWithProgress = masteringChainStereo(left, right, sampleRate, {
loudness: { targetLufs: -14, ceilingDb: -1, truePeakOversample: 4 },
}, (progress, stage) => {
console.log(`render ${(progress * 100).toFixed(0)}%: ${stage}`)
})
console.log(masteredWithProgress.outputLufs)
const profile = JSON.parse(masteringAudioProfile(samples, sampleRate, {
nFft: 2048,
hopLength: 512,
truePeakOversample: 4,
}))
const suggestions = JSON.parse(masteringAssistantSuggest(samples, sampleRate, {
targetLufs: -14,
ceilingDb: -1,
preferStreamingSafe: true,
}))
const deliveryPreview = JSON.parse(masteringStreamingPreview(samples, sampleRate, [
{ name: 'Streaming', targetLufs: -14, ceilingDb: -1 },
]))
console.log(profile, suggestions, deliveryPreview)
// The stereo entry points are request-object only — there is no positional
// overload, so a positional call throws.
const stereoProfile = JSON.parse(masteringAudioProfileStereo({
left,
right,
sampleRate,
params: { nFft: 2048, hopLength: 512, truePeakOversample: 4 },
}))
const stereoSuggestions = JSON.parse(masteringAssistantSuggestStereo({
left,
right,
sampleRate,
params: { targetLufs: -14, ceilingDb: -1, preferStreamingSafe: true },
}))
// Omitting platforms uses the built-in Spotify / Apple Music / YouTube set.
const stereoPreview = JSON.parse(masteringStreamingPreviewStereo({ left, right, sampleRate }))
console.log(stereoProfile, stereoSuggestions, stereoPreview)Use the stereo entry points whenever you have two channels. The mono ones measure a 0.5 * (left + right) downmix, which on decorrelated stereo material reads about 6 dB low, dragging integrated loudness and every value derived from it down by the same amount; only the loudness block of the stereo profile is measured from both channels. The measured comparison and the demo are under Stereo profiling, assistant, and preview, and the readings are in LUFS (LUFS).
Note the calling-convention difference: these four take a single request object and nothing else, unlike the positional mastering examples above. The metering sibling meteringCrestFactorDbStereo({ left, right, sampleRate }) follows the same request-object shape and returns a number. Node native and WASM also name the request types differently: Node declares one request type per function (MasteringAudioProfileStereoRequest, MasteringAssistantSuggestStereoRequest, MasteringStreamingPreviewStereoRequest), where WASM shares MasteringStereoParamsRequest.
The assistant/profile helpers accept the same option names as the WASM entry points. Profile params are nFft, hopLength (both in samples at 48 kHz, rescaled to the input rate), and truePeakOversample; assistant params are targetLufs, ceilingDb, enableRepair, preferStreamingSafe, and speechMonoAmount. The native parser also accepts snake_case aliases.
For long offline renders, pass the optional progress callback to masteringChain(...), masteringChainStereo(...), masterAudio(...), or masterAudioStereo(...) and update your Node UI from that callback.
The WASM package exposes the same camelCase mastering API names as the browser demo. The main groups are:
| Group | API names |
|---|---|
| Presets and quick entry points | mastering(), masteringPresetNames(), masteringPresetParams(), masteringPlatformNames(), masterAudio(), masterAudioStereo(), masterAudioWithProgress(), masterAudioStereoWithProgress() |
| Full chains | masteringChain(), masteringChainStereo(), masteringChainWithProgress(), masteringChainStereoWithProgress() |
| Offline dynamics (one-shot) | masteringDynamicsCompressor(), masteringDynamicsGate(), masteringDynamicsTransientShaper() |
| Repair in one call (any channel count) | masteringRepairAnalyze(), masteringRepairApply() — see Measure and repair in one call |
| Offline repair — mono | masteringRepairDeclick(), masteringRepairDeclip(), masteringRepairDecrackle(), masteringRepairDehum(), masteringRepairDenoiseClassical(), masteringRepairDereverbClassical(), masteringRepairTrimSilence() |
| Offline repair — stereo pair | masteringRepairDeclickStereo(), masteringRepairDeclipStereo(), masteringRepairDecrackleStereo(), masteringRepairDehumStereo(), masteringRepairDenoiseClassicalStereo(), masteringRepairDereverbClassicalStereo(), masteringRepairTrimSilenceStereo() |
| Offline repair — channel-linked (any channel count) | masteringRepairDenoiseClassicalLinked(), masteringRepairDereverbClassicalLinked() |
| Repair measurement (no audio returned) | masteringRepairDetectClicks(), masteringRepairDetectClipping(), masteringRepairDetectCrackle(), masteringRepairDetectHum(), masteringRepairDetectNoiseFloor(), masteringRepairDetectReverb(), masteringRepairDetectTrimRange(), masteringRepairDetectTrimRangeStereo(), masteringRepairNoiseBandBins(), masteringRepairDereverbConfigForRoom() |
| Assistant and profiling | masteringAudioProfile(), masteringAssistantSuggest(), masteringAssistantSuggestChain(), masteringStreamingPreview(), masteringAudioProfileStereo(), masteringAssistantSuggestStereo(), masteringAssistantSuggestChainStereo(), masteringStreamingPreviewStereo(), masteringAbMatchLoudness(), masteringAbMatchLoudnessStereo() |
| Named processors | masteringProcessorNames(), masteringProcessorCatalog(), masteringAmpPresetCatalog(), masteringInsertNames(), masteringInsertParamNames(name), masteringInsertParamInfo(name, sampleRate?), masteringInsertTiming(name, params, sampleRate), masteringProcess(), masteringProcessStereo() |
| Pair and stereo analysis | masteringPairProcessorNames(), masteringPairProcess(), masteringPairProcessStereo(), masteringPairAnalysisNames(), masteringPairAnalyze(), masteringStereoAnalysisNames(), masteringStereoAnalyze() |
| Streaming render | StreamingMasteringChain |
The pair processors, pair analyses and both A/B loudness matches take referenceSampleRate in their request objects, as on WASM: the reference is resampled onto the source's rate. StreamingEqualizer.match takes it in its options and accepts buffers of different lengths.
Node native uses the same base names but folds progress into an optional final callback argument instead of exporting separate *WithProgress helper functions.
Repair entry points
Every repair function accepts either a request object or the positional form, on both packages. The shape decides the input arity and what comes back:
| Shape | Call | Returns |
|---|---|---|
| Mono | masteringRepairDeclick(samples, sampleRate, options?) | the repaired Float32Array |
| Stereo pair | masteringRepairDeclickStereo(left, right, sampleRate, config?) | the repaired pair plus its report fields |
| Channel-linked | masteringRepairDenoiseClassicalLinked(channels, sampleRate, config?) | { channels, report }, one output per input channel in input order |
| Measurement | masteringRepairDetectClicks(samples, sampleRate, options?) | a detection report; no audio |
A stereo repair is not two mono calls: the decision that would move the stereo image is shared across the pair. Report-field shapes, the measurement helpers, and masteringAbMatchLoudness are in the JavaScript mastering reference; the stages and their fixed order are in Audio Repair.
Mixing API
The native addon and the WASM package both expose the mixing API: mixStereo(...), mixingScenePresetNames(), mixingScenePresetJson(), and the persistent Mixer class.
Use it for channel-strip processing, scene presets, sends, buses, automation, meters, and offline stem rendering. See Mixing Engine for the cross-runtime guide.
For persistent mixers, Node native accepts a StripRef (number | string) for most strip control methods; WASM methods use numeric strip indexes and expose stripById(id) for lookup.
Node stripMeter(strip) reads the post-fader meter; use meterTap(strip, 'preFader' | 'postFader') when you need an explicit tap. After loading scene JSON, mixer.sceneWarnings() lists every key the scene reader did not consume (typically typos) as non-fatal warnings, with its path.
Projects, Instruments & Live MIDI
The Node native addon exposes the same headless-DAW API as WASM and Python: the Project class (tracks, clips, tempo, undo/redo, SMF/MIDI 2.0 interchange), instrument-bound bounces (bounceWithSynthInstrument(s), SoundFont loading), the NativeSynth preset catalog (synthPresetNames() / synthPresetPatch() / SynthPatch), chordFunctionalAnalysis(...), and the RealtimeEngine with live MIDI input.
The engine carries the same lane mixer and MIDI clip schedule as the other bindings — setTrackLanes (at most MAX_TRACK_LANES, 32, lanes) / setTrackBuses, strip JSON and insert controls for tracks, the master, and buses, insert automation-id resolvers (each throws a RangeError for an unknown track, bus, insert, destination or parameter), setParamSmoothingMs, wide/scope telemetry, setMidiClips, and sampleAtPpq — with the same camelCase names as WASM. It also exposes external-device routing through setMidiDestinationExternal, setExternalMidiClockEnabled, drainExternalMidi, and externalMidiDroppedCount (see Realtime Engine). The browser-only helpers (bindWebMidi, bindMicrophoneInput) are WASM-specific and not part of the native addon.
The guides carry the depth: Project Editing, Bouncing Projects, Built-in Synthesizer, SoundFont Player, and MIDI Input.
Error Handling
Like the WASM package, the native addon throws a structured SonareError on every failure the library or an object's state reports: an Error subclass with a numeric code and its canonical codeName, mirroring the C ABI error enum. Both packages export ErrorCode, SonareError, and the isSonareError(value) type guard, and the same failure reports the same numeric code on every binding. A refused argument is a native RangeError (out of range, non-finite, or an unknown id or name) or TypeError (wrong type) on both packages, and isSonareError does not match it. A call on a destroyed object, including a destroyed HrtfSet, ProjectTimeline or SampleBank passed as an argument, and a failure caused by earlier configuration (a full table, a routing change earlier routing forbids) is a SonareError with InvalidState. The error contract lists which failure takes which class and code on every runtime; Error Handling has the ErrorCode enum and a usage example.
SonareError is a runtime class on both packages, so importing the name by value gets you a constructor rather than undefined, and a shared TypeScript module can import it from either package and find the same kind of thing. Its instanceof is brand-based rather than prototype-based — it narrows any Error named SonareError that carries a numeric code:
import { ErrorCode, isSonareError, SonareError } from '@libraz/libsonare-native'
try {
// ...
} catch (err) {
if (err instanceof SonareError && err.code === ErrorCode.InvalidParameter) {
console.error(err.codeName, err.message)
}
}That matters in two cases a prototype check would miss. The addon raises plain Error objects carrying the shape rather than constructing the class, and an error that crossed a worker or structuredClone() boundary has lost its prototype; both still narrow. isSonareError(value) is the same duck-typed check written as a type guard — instanceof delegates to it, so the two never disagree, and either is fine to use. Constructing a SonareError directly is supported for re-raising a native failure across a boundary that does not preserve prototypes.
Audio Method Differences
The WASM Audio class offers method-style access to common one-shot helpers. Focused helpers remain standalone when they are less common or need a different calling shape.
Available as Audio methods | Still standalone in WASM |
|---|---|
| Core BPM/key/beat/chord analysis | analyzeSections(...) |
| HPSS and editing helpers | analyzeMelody(...) |
| Mastering helpers | analyzeDynamics(...) |
| Feature extraction | analyzeTimbre(...) |
| Loudness and resampling | Room-acoustic helpers, section/melody/dynamics/timbre helpers |
Node native's Audio object has more methods because it can call into the native addon directly.
| Capability | Node native | WASM |
|---|---|---|
Extra Audio methods | More focused analysis and room-acoustic methods are available as instance methods | Use standalone focused helpers where available |
| File construction | Audio.fromFile(...), Audio.fromFileChannel(...), Audio.fromBuffer(...), Audio.fromMemory(...) | Audio.fromBuffer(...), Audio.fromMemory(...), Audio.fromMemoryWithBrowserFallback(...) |
| Sample access | audio.getData() returns a copy | audio.data returns a copy |
Node native adds analyzeBpm(...), analyzeImpulseResponse(...), detectAcoustic(...), analyzeRhythm(...), analyzeDynamics(...), and analyzeTimbre(...) to Audio. The room helpers estimateRoom(...), synthesizeRir(...), and roomMorph(...) remain standalone functions. The full method and function reference is on the Node.js Native API page.
StreamingMasteringChain
The native addon (and the WASM package) exposes a StreamingMasteringChain class for incremental rendering, for example when bridging an Electron app, worker, or audio capture pipeline. It accepts the same nested config as masteringChain(), plus loudnessStaticGainDb and the optional loudnessStaticGainPeakDb, and renders one block at a time.
import { StreamingMasteringChain } from '@libraz/libsonare-native';
const chain = new StreamingMasteringChain({
eq: { tilt: { tiltDb: 0.5 } },
dynamics: { compressor: { thresholdDb: -20 } },
maximizer: { truePeakLimiter: { ceilingDb: -1, oversampleFactor: 4 } },
});
chain.prepare(48000, /*maxBlockSize=*/512, /*numChannels=*/2);
// processMono needs a chain prepared with numChannels = 1
const { left, right } = chain.processStereo(leftBlock, rightBlock);
console.log(chain.stageNames(), chain.latencySamples());
chain.setParameter('eq.tilt.tiltDb', 1.0); // realtime-safe update at a block boundary
chain.reset(); // clear state without rebuildingAfter prepare(), setParameter(key, value) changes one realtime-safe full-chain parameter at a serialized block boundary. The native chain keeps its processor instances, audio history, and latency. Use the full dotted key (for example, eq.tilt.tiltDb); structural keys, keys from disabled stages, and unknown keys are rejected. Serialize this call with process*(), flush*(), reset(), and prepare(). Changes take effect at the boundary and are not smoothed automatically.
Stereo-only stages are skipped when numChannels === 1. The streaming chain rejects offline-only repair stages. A loudness stage requires a precomputed static gain in loudnessStaticGainDb; optionally pass the source true-peak in loudnessStaticGainPeakDb so the gain is clamped against the configured ceiling. The WASM build exposes delete() (with destroy() and [Symbol.dispose] as aliases) to release the underlying handle; the native addon exposes an idempotent destroy() and [Symbol.dispose]. using releases it when the code is compiled by TypeScript 5.2+ or runs on a runtime with explicit resource management (Node 22 itself does not parse using). GC eventually reclaims a native handle, but a long-lived process that builds one chain per request must release it explicitly or native memory accumulates.
Related mastering guides: Browser local processing, Reference match, Quality checklist.
StreamingEqualizer
StreamingEqualizer is available in Node native, Python, and WASM. A fractional, NaN or oversized maxBlockSize or slope throws a RangeError on Node native.
It is useful when you need an EQ that keeps state across processMono / processStereo calls. It can also publish a spectrum snapshot and configure bands from a reference match: match(source, reference, { sampleRate?, referenceSampleRate?, maxBands? }) takes buffers of any lengths, and the reference may be at its own rate.
Node native and WASM accept the same phase mode values; Python additionally supports context-manager usage:
| Runtime | Phase mode values |
|---|---|
| Node native | 'zero', 'natural', 'linear', or 1 / 2 / 3 |
| WASM | 'zero', 'natural', 'linear', or 1 / 2 / 3 (same as Node native) |
| Python | String or numeric modes; also supports with StreamingEqualizer(...) as eq: / eq.close() |
import { StreamingEqualizer } from '@libraz/libsonare-native';
const eq = new StreamingEqualizer({ sampleRate: 48000, maxBlockSize: 512 });
eq.setBand(0, { type: 'HighShelf', frequencyHz: 8000, gainDb: 4, enabled: true });
eq.setPhaseMode('natural');
eq.setAutoGain(true);
const { left, right } = eq.processStereo(leftBlock, rightBlock);
console.log(eq.spectrum(), eq.latencySamples(), left, right);The native build auto-detects FFmpeg development libraries via pkg-config. Without FFmpeg it decodes WAV and MP3. To require or disable FFmpeg explicitly:
SONARE_FFMPEG=1 yarn build # require FFmpeg-backed decoding
SONARE_FFMPEG=0 yarn build # force WAV/MP3-only decodingFull Function Reference
The complete function-by-function Node native reference lives on its own page: Node.js Native API. It documents the analysis, effects, feature-extraction, inverse-reconstruction, librosa-compatible, conversion, metering, and scale-quantization functions, the streaming and realtime classes, and the exported TypeScript types.