Skip to content

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:

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 buildingUsePackage
A browser appWASM@libraz/libsonare
A Python script or notebookPythonpip install libsonare
A Node.js app needing native decode or performanceNode 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/libsonare and which use @libraz/libsonare-native;
  • map native addon functions back to the broader JavaScript, Python, mastering, and mixing docs.

Comparison ​

WebAssemblyPythonNode.js (N-API)
PlatformBrowserDesktopDesktop
Initializationawait init()NoneNone
Distributionnpm (@libraz/libsonare)PyPI (pip install libsonare)Source (bindings/node)
BuildEmscriptenPre-built wheels (or CMake + pip)CMake + cmake-js
PerformanceNear-nativeNativeNative
Handle releasedelete()with / close()destroy(), or using where the runtime supports it
File I/OSample-based APIs; Audio.fromMemory(...) decodes WAV/MP3 bytes, and the browser decode path can read additional supported formatsWAV/MP3 by default; FFmpeg formats in FFmpeg buildsWAV/MP3 by default; FFmpeg formats in FFmpeg builds
Ordinary analysis, effects, features, conversions, mastering, mixing, streamingSame feature setSame feature setSame 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 ​

PackageInitializationUse when
@libraz/libsonareCall await init() before useYou want the browser-compatible WASM package or the exact browser-demo API
@libraz/libsonare-nativeNo WASM init; import and call functions directlyYou 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.

bash
git clone https://github.com/libraz/libsonare.git
cd libsonare/bindings/node
yarn install
yarn build

ABI 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.

ABICurrentCovers
Feature7the flat analysis and feature-result structs plus the shipped general C function contracts — SonareKey, SonareAnalysisResult, SonareChordDetectionOptions, and their neighbors
Project2the headless-DAW project and arrangement structs. Reports 0 in a build without arrangement support
Voice changer3the realtime voice-changer configuration struct
Acoustic5the room-acoustics structs
Engine3the 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 SonareError with ErrorCode.ABI_MISMATCH (10) and a message beginning libsonare ABI mismatch — a shared library from another checkout fails on first library use, never partway through a call. Its RealtimeEngine and Project constructors, and Project.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 SonareError with ErrorCode.AbiMismatch (10): Node when the addon is required, WASM in init(). Both export the query functions abiVersion(), projectAbiVersion(), engineAbiVersion(), voiceChangerAbiVersion() and the constants EXPECTED_PROJECT_ABI_VERSION, EXPECTED_ABI_VERSION and EXPECTED_ENGINE_ABI_VERSION (WASM also exposes EXPECTED_ABI_VERSION from /worklet); the Node addon checks the engine ABI when a RealtimeEngine is constructed. projectAbiVersion() equals the expected constant when arrangement support is compiled in and 0 when it is not, and WASM's engineCapabilities() reports engineAbiVersion, expectedEngineAbiVersion and abiCompatible. The engine ABI is outside the packed value, so each RealtimeEngine constructor compares it separately and throws AbiMismatch before creating the engine; Node's Project constructor and Project.fromJson do the same with the project version.
  • A C caller compares sonare_abi_version() with its compile-time SONARE_ABI_VERSION once; 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:

PackageUse when
@libraz/libsonareYou want the same API as the browser demo or need Web-compatible WASM.
@libraz/libsonare-nativeYou need native file decoding or native runtime performance in Node.js.
typescript
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:

GroupAPI names
Presets and quick entry pointsmastering(), masteringPresetNames(), masteringPresetParams(), masteringPlatformNames(), masterAudio(), masterAudioStereo(), masterAudioWithProgress(), masterAudioStereoWithProgress()
Full chainsmasteringChain(), 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 — monomasteringRepairDeclick(), masteringRepairDeclip(), masteringRepairDecrackle(), masteringRepairDehum(), masteringRepairDenoiseClassical(), masteringRepairDereverbClassical(), masteringRepairTrimSilence()
Offline repair — stereo pairmasteringRepairDeclickStereo(), 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 profilingmasteringAudioProfile(), masteringAssistantSuggest(), masteringAssistantSuggestChain(), masteringStreamingPreview(), masteringAudioProfileStereo(), masteringAssistantSuggestStereo(), masteringAssistantSuggestChainStereo(), masteringStreamingPreviewStereo(), masteringAbMatchLoudness(), masteringAbMatchLoudnessStereo()
Named processorsmasteringProcessorNames(), masteringProcessorCatalog(), masteringAmpPresetCatalog(), masteringInsertNames(), masteringInsertParamNames(name), masteringInsertParamInfo(name, sampleRate?), masteringInsertTiming(name, params, sampleRate), masteringProcess(), masteringProcessStereo()
Pair and stereo analysismasteringPairProcessorNames(), masteringPairProcess(), masteringPairProcessStereo(), masteringPairAnalysisNames(), masteringPairAnalyze(), masteringStereoAnalysisNames(), masteringStereoAnalyze()
Streaming renderStreamingMasteringChain

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:

ShapeCallReturns
MonomasteringRepairDeclick(samples, sampleRate, options?)the repaired Float32Array
Stereo pairmasteringRepairDeclickStereo(left, right, sampleRate, config?)the repaired pair plus its report fields
Channel-linkedmasteringRepairDenoiseClassicalLinked(channels, sampleRate, config?){ channels, report }, one output per input channel in input order
MeasurementmasteringRepairDetectClicks(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:

typescript
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 methodsStill standalone in WASM
Core BPM/key/beat/chord analysisanalyzeSections(...)
HPSS and editing helpersanalyzeMelody(...)
Mastering helpersanalyzeDynamics(...)
Feature extractionanalyzeTimbre(...)
Loudness and resamplingRoom-acoustic helpers, section/melody/dynamics/timbre helpers

Node native's Audio object has more methods because it can call into the native addon directly.

CapabilityNode nativeWASM
Extra Audio methodsMore focused analysis and room-acoustic methods are available as instance methodsUse standalone focused helpers where available
File constructionAudio.fromFile(...), Audio.fromFileChannel(...), Audio.fromBuffer(...), Audio.fromMemory(...)Audio.fromBuffer(...), Audio.fromMemory(...), Audio.fromMemoryWithBrowserFallback(...)
Sample accessaudio.getData() returns a copyaudio.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.

typescript
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 rebuilding

After 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:

RuntimePhase mode values
Node native'zero', 'natural', 'linear', or 1 / 2 / 3
WASM'zero', 'natural', 'linear', or 1 / 2 / 3 (same as Node native)
PythonString or numeric modes; also supports with StreamingEqualizer(...) as eq: / eq.close()
typescript
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:

bash
SONARE_FFMPEG=1 yarn build  # require FFmpeg-backed decoding
SONARE_FFMPEG=0 yarn build  # force WAV/MP3-only decoding

Full 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.