@libraz/libsonare
v1.7.2
Published
Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly
Maintainers
Readme
libsonare
Turn audio into data and back — entirely in the browser. Analyze songs (BPM, key, chords, loudness), master and mix to broadcast loudness, and render MIDI through built-in instruments, all client-side via WebAssembly — the same C++ engine that runs natively, with zero dependencies and no Python or model weights. 88 named mastering DSP processors implemented against published references (ITU-R BS.1770-4 true-peak limiting, Linkwitz-Riley crossovers, Vicanek matched-Z biquads, ADAA-antialiased saturation); analysis defaults match librosa where the two overlap.
Try it in the browser
Everything runs client-side — no server, nothing uploaded.
- 🎧 Live demos — analyze a song (BPM / key / chords), master to a target loudness, mix, and render MIDI through the built-in instruments, all in the page.
- 🎛️ sonare studio — a full browser DAW (multi-track sequencing, piano roll, mixer, mastering, WAV / MP3 / MIDI / MusicXML export) built entirely on this WASM engine. It shows how far one Apache-2.0 engine reaches, from analysis to a playable, exportable arrangement.
- 📖 Documentation & getting started
Installation
npm install @libraz/libsonareFor BPM/key/chord detection, feature extraction, and metering without the mastering, mixing, or realtime-engine APIs, import the smaller analysis entry:
import { detectBpm, init } from '@libraz/libsonare/analysis';With emsdk 5.0.2, the analysis binary is 0.91 MiB raw / 368 KiB gzip; the full
entry is 3.88 MiB raw / 1.31 MiB gzip. The analysis entry deliberately has no
masterAudio, mixStereo, Project, Mixer, or RealtimeEngine export.
Quick Start
init() loads the WASM module once; every API is available afterwards. Top-level
one-shot functions accept a request object (recommended) or positional arguments.
Audio input: start with
Audio.fromMemoryWithBrowserFallback(bytes). It decodes WAV/MP3 in WASM, then uses the browser'sdecodeAudioDatafor other browser-supported formats. Pass a decoded monoFloat32Arrayonly when one is already available.
Platform constraints: the WebAssembly build is single-threaded (analysis runs to completion on the calling thread — there is no non-blocking variant) and has no host filesystem access. Drive long-running calls from a Web Worker to keep the UI responsive.
import { Audio, init } from '@libraz/libsonare';
await init();
const bytes = new Uint8Array(await file.arrayBuffer());
const audio = await Audio.fromMemoryWithBrowserFallback(bytes);
const { bpm, key } = audio.analyze();
console.log(`BPM: ${bpm} Key: ${key.name}`);Render a MIDI arrangement through a built-in instrument with the headless
Project. The embind handle is not garbage-collected — call delete() when done.
import { init, Project } from '@libraz/libsonare';
await init();
const project = new Project();
try {
const { clipId } = project.addMidiClip(0, 4);
project.setMidiEvents(clipId, [
Project.midiNoteOn(0, 0, 0, 60, 100), // ppq, group, channel, note, velocity
Project.midiNoteOff(1, 0, 0, 60),
]);
const audio = project.bounceWithSynthInstrument('saw-lead', { numChannels: 2 });
} finally {
project.delete();
}Using already-decoded audio
Use Float32Array directly when another API already decoded the audio:
const audio = Audio.fromBuffer(decoded.getChannelData(0), decoded.sampleRate);
const { bpm, key } = audio.analyze();Offline Worker for longer audio
For audio longer than roughly 30 seconds, use OfflineWorkerClient to keep
analysis or preset mastering off the UI thread. The published ./worker
subpath is resolved automatically. It intentionally exposes only one-shot
value APIs (analyze, BPM/key/chord detection, and masterAudio): native
handles such as Project, Mixer, and realtime engines stay in their owning
JavaScript realm.
import { OfflineWorkerClient } from '@libraz/libsonare';
const offline = new OfflineWorkerClient();
const task = offline.analyze(
{ samples, sampleRate },
{
onProgress: ({ progress, stage }) => updateProgress(progress, stage),
// copy: true, // retain `samples`; the default transfers and detaches it
},
);
cancelButton.onclick = () => task.cancel();
try {
const result = await task;
console.log(result.bpm, result.key.name);
} finally {
offline.dispose();
}By default the input Float32Array is transferred, so its buffer is detached
on the calling thread. Pass { copy: true } when it must remain usable. Prompt
cancellation of a running synchronous WASM call uses SharedArrayBuffer; serve
the page with cross-origin isolation (COOP/COEP) when a cancel button must take
effect immediately. workerUrl lets a host point the client at a separately
hosted copy of @libraz/libsonare/worker.
Loading the .wasm file
Bundlers that don't auto-resolve the .wasm asset need its URL. Pass a
locateFile resolver to init():
import wasmUrl from '@libraz/libsonare/wasm?url'; // Vite; adapt per bundler
await init({ locateFile: (path) => (path.endsWith('.wasm') ? wasmUrl : path) });From a CDN, import { init } from 'https://esm.sh/@libraz/libsonare' resolves the
.wasm automatically. See the
getting-started guide for
per-bundler setup and the AudioWorklet bridge.
Realtime voice changer preset schemas
The published package includes the JSON Schema documents for third-party voice changer presets. Resolve them through the package exports rather than copying a schema from the repository:
@libraz/libsonare/schemas/realtime-voice-changer-preset.schema.json
@libraz/libsonare/schemas/realtime-voice-changer-preset-pack.schema.jsonValidate data against the schema before saving it, then pass the JSON text to
validateRealtimeVoiceChangerPresetJson() before applying it. The runtime check
is authoritative and also rejects malformed JSON such as duplicate keys.
Bounded-memory OPFS clip streaming
For long raw float32 clips stored in OPFS, attachOpfsClipStream supplies only
the current playback window to WASM. It primes the first page, then fetches page
misses on the main thread and evicts pages outside the configured read-ahead /
retain-behind window. The AudioWorklet path uses the same helper: the worklet
posts a bounded batch of misses, and it outputs silence until a page arrives.
import { attachOpfsClipStream } from '@libraz/libsonare';
import { SonareEngine } from '@libraz/libsonare/worklet';
const engine = await SonareEngine.create(audioContext);
const stream = await attachOpfsClipStream(engine, {
path: 'takes/lead.f32',
clipId: 42,
numChannels: 2,
numSamples: 48_000 * 600,
pageFrames: 16_384,
});
// `clipId` must equal the explicit id supplied here.
engine.addClip(trackId, stream.provider, 0, { id: 42 });
// Close the returned binding after removing the clip (or when the host closes).
stream.binding.close();The bounded-memory guarantee applies only to an OPFS/page-provider source.
Passing a Float32Array[] to addClip keeps that full array in the JavaScript
heap, so it is appropriate for short clips but does not make long clips bounded.
Cue bus on a second AudioWorklet output
Per-track PFL/AFL monitoring normally folds the cue into the program output.
Pass cueOutput and the node gains a second output carrying the cue alone, so
it can be routed to headphones or a separate device while the program mix stays
untouched.
import { SonareEngine } from '@libraz/libsonare/worklet';
const engine = await SonareEngine.create(audioContext, { cueOutput: true });
engine.setTrackMonitorMode(trackId, 'pfl');
engine.node.connect(audioContext.destination, 0); // program
engine.node.connect(cueDestination, 1); // cueWithout cueOutput the node keeps a single output and the folded mix, sample
for sample. Off the worklet, the same split is available on the zero-copy path
as prepareMonitorChannels / getMonitorChannelBuffer /
processPreparedWithMonitor, and as processWithMonitor for a copy-in call.
Mastering preview inside the worklet
StreamingMasteringChain is exported from @libraz/libsonare/worklet, so a live
preview can run in the render realm instead of round-tripping audio to the main
thread. Build and prepare() it from a message handler — prepare() allocates
and must not run inside process(). An enabled loudness stage needs the
offline-measured loudnessStaticGainDb, since whole-signal integrated LUFS
cannot be measured block by block. The chain is a host-side stage: it is outside
the engine's own delay compensation, so aligning it against other engine outputs
is the caller's job.
Capabilities
Every area below has runnable examples and the full API in the documentation.
- Analysis — BPM, key (+ candidates), chords, downbeats, sections, melody, tuning; pitch (YIN / pYIN), timbre, and the full spectral feature set (STFT, mel, MFCC, chroma, CQT/VQT, spectral contrast); metering (true-peak, LUFS, correlation, vectorscope, waveform peaks). → API
- Mastering — 88 named DSP processors, the configurable
masteringChain, 25 named presets viamasterAudio, and reference-matching. → Mastering processors - Mixing — offline
mixStereoand the block-basedMixerwith scene presets. → Mixing - Editing DSP — time-stretch, pitch-shift, HPSS (+ residual), phase vocoder, normalize, trim, remix. → Editing DSP
- Room acoustics — blind RT60 / EDT, impulse-response clarity metrics, RIR synthesis, room estimation and morphing. → Room acoustics
- Realtime & streaming —
RealtimeEngine(transport / MIDI / render, bounded-memory clip streaming),StreamingMasteringChain/StreamingEqualizer/StreamingRetune,RealtimeVoiceChanger, and the AudioWorklet bridge. → Realtime & streaming - Instruments & synthesis — built-in oscillator synth, patch-driven NativeSynth (15 synthesis engines, incl. physically-modeled piano / strings / winds — being tuned over time), and a GS-compatible SoundFont (SF2) player. → API
- Headless DAW —
Projectarrangement model: audio / MIDI tracks and clips, undo/redo, clip warp, SMF / MIDI 2.0 Clip File I/O, deterministic JSON, offlinebounce. → API - Conversions — Hz / mel / MIDI / note, frames / time, resample.
Native failures throw a SonareError carrying a numeric code (an ErrorCode
value) and its codeName; narrow with the isSonareError type guard.
Documentation
Full API reference, guides, and browser-local demos live at libsonare.libraz.net (getting started · browser / WASM API · demos).
Also available
pip install libsonare # Python bindings with CLIThe native Node.js N-API binding (reads files from disk) lives at
bindings/node.
