WebAssembly Guide
libsonare can be compiled to WebAssembly for audio analysis directly in web browsers. The key rule: its APIs work on decoded audio samples (a mono Float32Array of numbers), not on a raw .mp3/.wav file. You get those samples either by decoding the file yourself with the Web Audio API or another JavaScript decoder, or by handing the encoded bytes to the Audio.fromMemory* helpers, which decode for you. The table below shows the full path.
Use this page when you are building a browser app. If you are writing a Python script, terminal batch job, or native desktop tool, start with Getting Started and choose another runtime.
Browser Mental Model
| Step | What happens |
|---|---|
| 1. Load a file | Use fetch, an <input type="file">, drag-and-drop, or another browser source |
| 2. Decode audio | Use Audio.fromMemory(...), Audio.fromMemoryWithBrowserFallback(...), AudioContext.decodeAudioData(...), or your own decoder |
| 3. Choose samples | Pass one mono channel, downmix stereo yourself, or call stereo APIs where available |
| 4. Call libsonare | Pass samples plus sampleRate to analysis, editing, mastering, or mixing APIs |
The most common beginner mistake is passing an MP3 ArrayBuffer directly to an analysis function. Decode it first; libsonare's browser package works on PCM samples, not compressed file bytes.
Three quick sanity checks catch most setup problems:
await init()has completed before any DSP call.- The value passed as
sampleRateis the rate of the decoded samples, usuallyaudioBuffer.sampleRate. - The sample array is PCM audio (
Float32Array), not encoded file bytes.
What are Float32Array, PCM, mono, and downmixing?
- PCM samples are the raw, uncompressed waveform — a long list of amplitude numbers. An MP3/WAV file is compressed or wrapped bytes; decoding turns it into PCM.
Float32Arrayis the JavaScript typed array the Web Audio API uses to hold those samples as 32-bit floats (normally in the −1…1 range), one number per sample. libsonare's browser API takes this directly.- Mono / downmixing — mono is a single channel. Stereo audio has separate left and right channels; downmixing combines them into one (typically by averaging) so you can pass a single channel to a mono API.
What You Will Learn
By the end of this page you should be able to:
- install and initialize the WASM package correctly;
- decode browser files into PCM and pass the right channel/sample-rate pair to libsonare;
- choose between one-shot functions,
Audio,StreamAnalyzer,StreamingMasteringChain,Mixer, andRealtimeEngine; - understand the bundle-size and Worker/AudioWorklet tradeoffs before shipping a browser app.
Installation
npm/yarn
npm install @libraz/libsonareyarn add @libraz/libsonarepnpm add @libraz/libsonareCDN
<script type="module">
import { init, detectBpm } from 'https://unpkg.com/@libraz/libsonare';
</script>Basic Usage
import { init, detectBpm, detectKey, analyze } from '@libraz/libsonare';
async function analyzeAudio() {
// Initialize WASM module
await init();
// Get audio data from AudioContext
const audioCtx = new AudioContext();
const response = await fetch('music.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// Get one mono channel. Downmix explicitly if you need both stereo channels.
const samples = audioBuffer.getChannelData(0);
const sampleRate = audioBuffer.sampleRate;
// Detect BPM
const bpm = detectBpm(samples, sampleRate);
console.log(`BPM: ${bpm}`);
// Detect key
const key = detectKey(samples, sampleRate);
console.log(`Key: ${key.name}`); // "C major"
// All-in-one analysis
const result = analyze(samples, sampleRate);
console.log(result);
}CLI equivalent for the same one-file checks:
sonare bpm music.mp3
sonare key music.mp3
sonare analyze music.mp3 --jsonThe demo below is the same data flow in visual form: decoded samples go in, a time/frequency view comes out. If your browser page can render this kind of result, the WASM package, initialization, decoding, and sample-rate plumbing are all connected.
The browser build also exposes the full librosa-parity helper set — functions that mirror the popular Python audio library librosa, so existing librosa recipes port over — grouped by intent:
- Waveform pre-processing —
preemphasis/deemphasis,trimSilence/splitSilence - Framing / size alignment —
frameSignal,padCenter,fixLength,fixFrames - 1-D post-processing —
peakPick,vectorNormalize - Features —
pcen(mel dynamic-range compression),tonnetz(harmonic-space projection),tempogram/plp(tempo representations) - Unit conversion —
powerToDb/amplitudeToDb/dbToPower/dbToAmplitude,framesToSamples/samplesToFrames
See the JS API reference for signatures and the librosa Compatibility mapping.
Browser Mixing
The WASM package exposes the mixing engine. Use mixStereo(...) for one-shot stem rendering, or keep a persistent Mixer built from scene JSON when you need buses, sends, insert automation, goniometer data, and strip meters.
import { init, Mixer, mixStereo, mixingScenePresetJson } from '@libraz/libsonare';
await init();
const rendered = mixStereo([vocalL, musicL], [vocalR, musicR], sampleRate, {
faderDb: [-3, -12],
pan: [0, -0.2],
width: [1, 0.9],
});
const mixer = Mixer.fromSceneJson(mixingScenePresetJson('vocalReverbSend'), sampleRate, 512);
mixer.scheduleFaderAutomation(0, sampleRate * 4, -6, 's-curve');
const block = mixer.processStereo([vocalBlockL, musicBlockL], [vocalBlockR, musicBlockR]);
const meter = mixer.stripMeter(0, 'postFader');
mixer.delete();For a full walkthrough, see Mixing Engine.
CLI equivalent for rendering a built-in mixer scene:
sonare mix \
--preset vocalReverbSend \
--input vocal.wav \
--input music.wav \
-o mixed.wavAudio Class
You can use the Audio class as an object-oriented alternative to standalone functions. It wraps the samples and sample rate, so you don't need to pass them every time.
import { init, Audio } from '@libraz/libsonare';
await init();
const audioCtx = new AudioContext();
const response = await fetch('music.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// Create Audio instance
const audio = Audio.fromBuffer(
audioBuffer.getChannelData(0),
audioBuffer.sampleRate
);
// Analysis
const bpm = audio.detectBpm();
const key = audio.detectKey();
const result = audio.analyze();
// Effects
const { harmonic, percussive } = audio.hpss();
const stretched = audio.timeStretch(1.5);
const shifted = audio.pitchShift(2);
// Feature extraction
const mel = audio.melSpectrogram();
const mfcc = audio.mfcc();
const chroma = audio.chroma();
const pitch = audio.pitchPyin();
console.log(`BPM: ${bpm}, Key: ${key.name}`);
console.log(`Median pitch: ${pitch.medianF0.toFixed(1)} Hz`);CLI equivalents for the calls above. All four are available in the Python CLI:
sonare analyze music.mp3 --json
sonare hpss music.mp3 -o separated --json
sonare pitch-shift music.wav --semitones 2 -o shifted.wav
sonare pitch music.mp3 --algorithm pyin --jsonSee the JS API Reference for the full list of instance methods.
Browser Mastering
The /mastering demo uses the same WASM package described here. Audio decoding happens in the browser, mastering work runs in a Web Worker, and the rendered WAV plus JSON report are created locally.
For implementation details, see Mastering Implementation, Browser Local Processing, Mastering, and Stereo, Limiter, and Loudness Controls.
The mastering API also includes masteringAssistantSuggest(...), masteringAudioProfile(...), and masteringStreamingPreview(...) for JSON-driven assistant output, source profiling, and platform preview reporting.
CLI equivalent for a simple loudness-normalized master:
sonare mastering track.wav --target-lufs -14 --ceiling-db -1 -o master.wavFile Input
Most WASM APIs take decoded PCM samples. For encoded bytes, use Audio.fromMemory(...) for WAV/MP3 or Audio.fromMemoryWithBrowserFallback(...) to try the native decoder first and then use AudioContext.decodeAudioData() for browser-supported formats such as AAC, OGG, and FLAC.
async function analyzeFile(file: File) {
await init();
const audioCtx = new AudioContext();
const arrayBuffer = await file.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
const samples = audioBuffer.getChannelData(0);
return analyze(samples, audioBuffer.sampleRate);
}
// Usage with file input
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const result = await analyzeFile(file);
console.log(`BPM: ${result.bpm}`);
});Progress Reporting
import { init, analyzeWithProgress } from '@libraz/libsonare';
await init();
const result = analyzeWithProgress(samples, sampleRate, (progress, stage) => {
const percent = Math.round(progress * 100);
console.log(`${stage}: ${percent}%`);
// Update UI
progressBar.style.width = `${percent}%`;
statusText.textContent = stage;
});Cancellation
Long-running offline analysis and mastering calls take a cancel callback. It is polled at the same progress boundaries onProgress reports, so a user who loads the wrong file does not have to wait out the render.
import {
ErrorCode,
init,
isSonareError,
masteringChainWithProgress,
} from '@libraz/libsonare';
await init();
let abandoned = false;
cancelButton.onclick = () => { abandoned = true; };
try {
const result = masteringChainWithProgress({
samples,
sampleRate,
config,
onProgress: (progress, stage) => updateUi(progress, stage),
cancel: () => abandoned,
});
render(result);
} catch (error) {
if (!(isSonareError(error) && error.code === ErrorCode.Cancelled)) throw error;
// Expected: the user asked to stop.
}Returning true from cancel aborts the call, which throws with SONARE_ERROR_CANCELLED (error code 8). A cancelled call leaves its outputs unallocated — there is no partial result to read, so treat the throw as "nothing happened" rather than "half a master".
Web Worker Usage
The package ships a Worker client, so the common case needs no worker file of your own. Import OfflineWorkerClient from the main entry point @libraz/libsonare (the /worker subpath is the worker-side entry, and exports installOfflineWorkerEndpoint):
import { OfflineWorkerClient } from '@libraz/libsonare';
const client = new OfflineWorkerClient();
const task = client.analyze(
{ samples, sampleRate },
{ onProgress: ({ progress, stage }) => updateUi(progress, stage) },
);
cancelButton.onclick = () => task.cancel();
const result = await task; // the task is thenable
client.dispose();analyze, detectBpm, detectKey, detectChords, masterAudio, and masterAudioStereo are available. Each returns an OfflineWorkerTask, which is awaitable and carries cancel().
Inputs are transferred, not copied
Float32Array inputs are transferred to the Worker by default, so the buffer becomes detached on the calling thread and reading it afterwards yields an empty array. Pass { copy: true } when you still need the samples — for drawing a waveform, say. Prompt cancellation of an already-running native call needs cross-origin isolation (the client uses a SharedArrayBuffer flag); without it, cancellation still lands, at the next task boundary.
Project, Mixer, and the realtime classes are deliberately absent from the client: their native handles belong to one JavaScript realm and cannot be moved across threads.
Writing your own worker
If you need an operation the client does not expose, the ordinary pattern still works — import the main entry inside a worker of your own.
worker.ts:
import { init, analyze, AnalysisResult } from '@libraz/libsonare';
let initialized = false;
self.onmessage = async (e: MessageEvent) => {
const { samples, sampleRate } = e.data;
if (!initialized) {
await init();
initialized = true;
}
try {
const result = analyze(samples, sampleRate);
self.postMessage({ success: true, result });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};main.ts:
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module'
});
function analyzeInWorker(
samples: Float32Array,
sampleRate: number
): Promise<AnalysisResult> {
return new Promise((resolve, reject) => {
worker.onmessage = (e) => {
if (e.data.success) {
resolve(e.data.result);
} else {
reject(new Error(e.data.error));
}
};
worker.postMessage({ samples, sampleRate });
});
}Stereo to Mono Conversion
async function getMonoSamples(audioBuffer: AudioBuffer): Promise<Float32Array> {
if (audioBuffer.numberOfChannels === 1) {
return audioBuffer.getChannelData(0);
}
// Mix stereo to mono
const left = audioBuffer.getChannelData(0);
const right = audioBuffer.getChannelData(1);
const mono = new Float32Array(left.length);
for (let i = 0; i < left.length; i++) {
mono[i] = (left[i] + right[i]) / 2;
}
return mono;
}Performance Tips
Downsampling
For BPM detection, 22050 Hz is sufficient:
import { resample, detectBpm } from '@libraz/libsonare';
// Downsample for faster analysis
const downsampled = resample(samples, 48000, 22050);
const bpm = detectBpm(downsampled, 22050);Analyze Segments
For long files, analyze only relevant sections:
function analyzeSegment(
samples: Float32Array,
sampleRate: number,
startSec: number,
endSec: number
) {
const start = Math.floor(startSec * sampleRate);
const end = Math.floor(endSec * sampleRate);
const segment = samples.slice(start, end);
return analyze(segment, sampleRate);
}
// Analyze only chorus (60-90 seconds)
const result = analyzeSegment(samples, sampleRate, 60, 90);React Example
import { useState } from 'react';
import { init, analyzeWithProgress, AnalysisResult } from '@libraz/libsonare';
function AudioAnalyzer() {
const [progress, setProgress] = useState(0);
const [stage, setStage] = useState('');
const [result, setResult] = useState<AnalysisResult | null>(null);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
await init();
const audioCtx = new AudioContext();
const arrayBuffer = await file.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
const samples = audioBuffer.getChannelData(0);
const analysisResult = analyzeWithProgress(
samples,
audioBuffer.sampleRate,
(p, s) => {
setProgress(p);
setStage(s);
}
);
setResult(analysisResult);
};
return (
<div>
<input type="file" accept="audio/*" onChange={handleFileChange} />
{stage && (
<div>
<div>{stage}: {Math.round(progress * 100)}%</div>
<progress value={progress} max={1} />
</div>
)}
{result && (
<div>
<p>BPM: {result.bpm.toFixed(1)}</p>
<p>Key: {result.key.name}</p>
</div>
)}
</div>
);
}Streaming Analysis
The Streaming API enables real-time audio analysis with low latency. Unlike batch analysis, streaming processes audio chunk by chunk as it arrives.
Batch vs Streaming
| Approach | Use Case | Latency | Features |
|---|---|---|---|
| Batch | Pre-recorded files | High | All-in-one analysis (BPM, chords, sections) |
| Streaming | Live audio, visualization | Low (~10ms) | Mel, chroma, onset, updating BPM/key |
Architecture Overview
Audio capture and analysis run on the AudioWorklet thread so the main thread stays free to paint; only the small, already-computed frame buffer crosses the thread boundary via postMessage, not raw audio.
Basic Example
ScriptProcessorNode is deprecated — use AudioWorklet in production
The first example below uses createScriptProcessor() because it is the shortest way to see frames flowing. ScriptProcessorNode is deprecated: it runs on the main thread and can glitch under load. For anything real, use the AudioWorklet integration shown right after it, which runs the analyzer off the main thread.
import { init, StreamAnalyzer } from '@libraz/libsonare';
async function setupStreaming() {
await init();
const audioCtx = new AudioContext();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioCtx.createMediaStreamSource(stream);
// Create analyzer with output-frame throttling
const analyzer = new StreamAnalyzer({
sampleRate: audioCtx.sampleRate,
nFft: 2048,
hopLength: 512,
nMels: 128,
computeMel: true,
computeChroma: true,
computeOnset: true,
emitEveryNFrames: 4, // emit one frame per 4 hops (~21 frames/s at 44100Hz, hopLength 512)
});
// Use ScriptProcessor for simplicity (AudioWorklet recommended for production)
const processor = audioCtx.createScriptProcessor(512, 1, 1);
processor.onaudioprocess = (e) => {
const input = e.inputBuffer.getChannelData(0);
analyzer.process(input);
const available = analyzer.availableFrames();
if (available > 0) {
const frames = analyzer.readFrames(available);
updateVisualization(frames);
// Check BPM/key estimates that update as audio arrives
const stats = analyzer.stats();
if (stats.estimate.updated) {
const keyNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const mode = stats.estimate.keyMinor ? 'minor' : 'major';
console.log(`BPM: ${stats.estimate.bpm.toFixed(1)}`);
// estimate.key is a PitchClass index (0-11), not a string
console.log(`Key: ${keyNames[stats.estimate.key]} ${mode}`);
}
}
};
source.connect(processor);
processor.connect(audioCtx.destination);
}AudioWorklet Integration
For production use, run StreamAnalyzer in an AudioWorklet — the browser's dedicated audio-rendering thread — so analysis does not block the main thread. The example below shows a self-contained analyzer worklet.
WASM in AudioWorklet
Loading WASM in AudioWorklet requires special handling. The WASM module must be loaded and instantiated within the worklet context.
analyzer-worklet.ts:
import { init, StreamAnalyzer } from '@libraz/libsonare';
class AnalyzerWorklet extends AudioWorkletProcessor {
private analyzer?: StreamAnalyzer;
private frameCounter = 0;
constructor() {
super();
void init().then(() => {
// sampleRate is a global in AudioWorkletGlobalScope
this.analyzer = new StreamAnalyzer({
sampleRate,
nFft: 2048,
hopLength: 512,
nMels: 64, // reduced for bandwidth
computeMel: true,
computeChroma: true,
computeOnset: true,
emitEveryNFrames: 4,
});
});
}
process(inputs: Float32Array[][]): boolean {
const input = inputs[0]?.[0];
if (!input || input.length === 0 || !this.analyzer) return true;
this.analyzer.process(input);
const available = this.analyzer.availableFrames();
if (available >= 4) {
const frames = this.analyzer.readFrames(available);
// Transfer buffers for zero-copy
this.port.postMessage({
type: 'frames',
data: frames
}, [
frames.timestamps.buffer,
frames.mel.buffer,
frames.chroma.buffer
]);
}
// Periodically send stats
if (++this.frameCounter % 100 === 0) {
this.port.postMessage({
type: 'stats',
data: this.analyzer.stats()
});
}
return true;
}
}
registerProcessor('analyzer-worklet', AnalyzerWorklet);main.ts:
const audioCtx = new AudioContext();
await audioCtx.audioWorklet.addModule('analyzer-worklet.js');
const workletNode = new AudioWorkletNode(audioCtx, 'analyzer-worklet');
workletNode.port.onmessage = (e) => {
if (e.data.type === 'frames') {
renderVisualization(e.data.data);
} else if (e.data.type === 'stats') {
updateBpmDisplay(e.data.data.estimate);
}
};
// Connect audio source
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioCtx.createMediaStreamSource(stream);
source.connect(workletNode);Related entry points (realtime engine, MIDI)
The example above builds a custom analyzer worklet. If you instead want to run the whole playback engine in a worklet — track lanes, channel strips, buses, MIDI clips, live MIDI, instruments, and capture — the package ships an AudioWorklet bridge at @libraz/libsonare/worklet. The bridge's SonareEngine API mirrors that engine to the worklet; see Realtime and Streaming.
The main package entry (@libraz/libsonare) also ships two main-thread browser integration helpers: bindMicrophoneInput(...) wires getUserMedia into an AudioWorklet engine node (see Recording and Takes), and bindWebMidi(...) bridges Web MIDI input to the engine (see MIDI Input).
Bandwidth Optimization
The TypeScript StreamAnalyzer has three read methods. Choose them by how much precision your UI needs and how much data you can afford to move between threads.
| Method | Returned type | Use when |
|---|---|---|
readFrames(maxFrames) | FrameBuffer with Float32Array / Int32Array fields | You need full precision for analysis or high-quality visuals |
readFramesI16(maxFrames) | StreamFramesI16 | You want smaller payloads but still enough precision for most visual meters |
readFramesU8(maxFrames) | StreamFramesU8 | You need very small payloads for mobile or dense visual updates |
The analyzer still computes internally in float. Select the transfer precision with the explicit read method: readFrames() for float data, readFramesI16() for 16-bit data, or readFramesU8() for 8-bit data. StreamConfig.outputFormat is retained only for source compatibility and must be omitted or set to 0; the C++/WASM read path quantizes the integer forms, so you do not need to quantize manually before postMessage.
Both quantized read paths accept an optional StreamQuantizeConfig to widen the quantization ranges for unusually loud or quiet streams that would otherwise saturate; see custom quantization ranges.
Plain lists and objects returned from WASM are rooted back into the JavaScript realm that called them. That means arrays from name-list helpers (*Names()), preset-name helpers, section results, key-candidate calls, and the object from synthPresetPatch(...) can be passed through structuredClone() or postMessage() without first rebuilding them by hand. Typed-array payloads still follow the normal transferable-buffer rules below.
What are "Structure-of-Arrays" and transferable objects?
- Structure-of-Arrays (SoA) means each field lives in its own flat typed array — all timestamps in one array, all mel values in another — instead of an array of per-frame objects. It is cheaper to slice and cheaper to hand to another thread.
- Transferable objects are
ArrayBuffers thatpostMessagecan move to a worker instead of copying. Ownership transfers (the sender's view becomes empty afterward), which makes passing audio frames between threads near-instant. List the buffers in the second argument:postMessage(msg, [buffer, ...]). - Quantizing here means packing each float into a smaller 16-bit or 8-bit integer — fewer bytes to send, at the cost of precision (fine for a meter or heatmap, not for further DSP).
| Approach | Approx. size per frame | Best For |
|---|---|---|
readFrames() (Float32 SoA) | ~600 bytes | General use, full precision |
readFramesI16() (quantized SoA) | ~300 bytes | High-quality visualizations |
readFramesU8() (quantized SoA) | ~150 bytes | Mobile, bandwidth-limited |
Updating Estimates
The Streaming API provides BPM and key estimates that improve over time:
const stats = analyzer.stats();
// BPM (available after ~10 seconds — see StreamConfig.bpmUpdateIntervalSec)
if (stats.estimate.bpm > 0) {
const confidence = stats.estimate.bpmConfidence;
console.log(`BPM: ${stats.estimate.bpm.toFixed(1)} (${(confidence * 100).toFixed(0)}%)`);
}
// Key (available after ~5 seconds — see StreamConfig.keyUpdateIntervalSec)
if (stats.estimate.key >= 0) {
const keyNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const keyName = keyNames[stats.estimate.key];
const mode = stats.estimate.keyMinor ? 'minor' : 'major';
console.log(`Key: ${keyName} ${mode}`);
}Visualization Example
import type { StreamAnalyzer } from '@libraz/libsonare';
function renderVisualization(frames: ReturnType<StreamAnalyzer['readFrames']>, nMels: number) {
const { nFrames, mel, chroma, onsetStrength } = frames;
// Render mel spectrogram (scrolling display). Values are linear power; clamp/scale to 0-1.
for (let f = 0; f < nFrames; f++) {
for (let m = 0; m < nMels; m++) {
const value = Math.min(1, mel[f * nMels + m]);
const c = Math.round(value * 255);
const color = `rgb(${c}, ${Math.round(c * 0.5)}, ${255 - c})`;
// Draw pixel at (scrollX + f, nMels - m)
}
}
// Render chroma (12 pitch classes)
for (let f = 0; f < nFrames; f++) {
for (let c = 0; c < 12; c++) {
const value = chroma[f * 12 + c];
// Draw chroma bar
}
}
// Trigger effects on strong onsets (linear units)
for (let f = 0; f < nFrames; f++) {
if (onsetStrength[f] > 1.5) { // tune threshold for your audio
triggerBeatEffect();
}
}
}Inverse Reconstruction
The WASM build ships the inverse reconstruction helpers, so you can go from a mel spectrogram or MFCC matrix back to a spectrum or audio entirely in the browser:
import { melSpectrogram, melToAudio, mfcc, mfccToAudio, init } from '@libraz/libsonare';
await init();
// Mel → audio (Griffin-Lim phase reconstruction)
const mel = melSpectrogram(samples, sampleRate, 2048, 512, 128);
const reconstructed = melToAudio(mel.power, mel.nMels, mel.nFrames, sampleRate);
// MFCC → audio
const m = mfcc(samples, sampleRate, 2048, 512, 128, 20);
const fromMfcc = mfccToAudio(m.coefficients, m.nMfcc, m.nFrames, mel.nMels, sampleRate);Source-built C++ CLI equivalents:
sonare mel-to-audio music.wav -o mel-reconstructed.wav
sonare mfcc-to-audio music.wav -o mfcc-reconstructed.wav| Function | Returns | Notes |
|---|---|---|
melToStft(melPower, nMels, nFrames, sampleRate?, nFft?, fmin?, fmax?, htk?) | StftPowerResult { nBins, nFrames, power } | Pseudo-inverse of the mel filterbank |
melToAudio(melPower, nMels, nFrames, sampleRate?, nFft?, hopLength?, fmin?, fmax?, nIter?, htk?) | Float32Array | Griffin-Lim audio synthesis |
mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels?, lifter?) | MelPowerResult { nMels, nFrames, power } | Inverse DCT back to a mel spectrogram; pass the forward MFCC lifter |
mfccToAudio(mfccCoefficients, nMfcc, nFrames, nMels, sampleRate?, nFft?, hopLength?, fmin?, fmax?, nIter?, htk?, lifter?) | Float32Array | MFCC → mel → audio in one call; pass the forward MFCC lifter |
Lossy round-trip
These reconstruct magnitude and estimate phase with Griffin-Lim, so the output is an approximation — fine for sonification, audition, and visualization, not for bit-exact recovery. See Inverse Features for the processing flow and caveats.
Streaming Retune
StreamingRetune is the WASM block-by-block mono retune object. Use it for live or chunked pitch shifting when you need state to continue across blocks.
import { init, StreamingRetune } from '@libraz/libsonare';
await init();
const retune = new StreamingRetune({ semitones: 3, mix: 1 });
retune.prepare(48000, 512);
try {
const shifted = retune.processMono(inputBlock);
retune.setConfig({ semitones: -2, mix: 0.75 });
const next = retune.processMono(nextInputBlock);
console.log(shifted, next, retune.grainSize());
} finally {
retune.delete();
}For file-based offline processing from the terminal, use the closest CLI commands. Both are available in the Python CLI:
sonare pitch-shift vocal.wav --semitones 3 -o shifted.wav
sonare voice-change vocal.wav --pitch-semitones 3 --formant-factor 1.0 -o voice.wavRealtime Voice Changer
RealtimeVoiceChanger is the WASM object for the preset-based live voice chain. It is separate from the offline voiceChange(...) helper because it keeps DSP state across blocks and exposes heap-backed zero-copy buffers for AudioWorklet-style loops.
import {
init,
RealtimeVoiceChanger,
realtimeVoiceChangerPresetConfig,
realtimeVoiceChangerPresetNames,
voiceCharacterPresetId,
} from '@libraz/libsonare';
await init();
const changer = new RealtimeVoiceChanger('bright-idol');
changer.prepare(48000, 128, 1);
try {
const out = changer.processMono(inputBlock);
const realtime = changer.createRealtimeMonoBuffer(128);
realtime.input.set(inputBlock.subarray(0, 128));
realtime.process();
console.log(
voiceCharacterPresetId(1),
realtimeVoiceChangerPresetNames(),
realtimeVoiceChangerPresetConfig('bright-idol'),
out,
realtime.output,
);
} finally {
changer.delete();
}Use realtimeVoiceChangerPresetJson(name) to inspect a built-in preset and validateRealtimeVoiceChangerPresetJson(json) before accepting user-authored preset JSON. RealtimeVoiceChangerConfigInput accepts one of the six strict VoicePresetId strings or a preset object with either a dsp object or a macros object, never both. voiceCharacterPresetId(...) returns null for an unknown numeric ordinal and throws for an unknown string ID; realtimeVoiceChangerPresetConfig(...) throws when it cannot resolve a preset. If you need the canonical ID or resolved flat POD config, use voiceCharacterPresetId(...) and realtimeVoiceChangerPresetConfig(...).
Browser Compatibility
| Browser | Minimum Version |
|---|---|
| Chrome | 57+ |
| Firefox | 52+ |
| Safari | 11+ |
| Edge | 16+ |
Requirements:
- WebAssembly support
- Web Audio API
- ES2017+ (async/await)
Package Artifacts
The published package ships a few coordinated pieces:
- Main module —
sonare.jsplussonare.wasm, the Emscripten build behind every analysis, mastering, mixing, and editing API. - Main API entry — the package
index(index.js/index.d.ts) is the tsup bundle behindimport ... from '@libraz/libsonare'; it exposes the all-in-one analysis, mastering, mixing, and editing API. - AudioWorklet entry —
worklet.js/worklet.d.ts, a separate, self-contained tsup bundle (no code-splitting, so it is fully portable into anAudioWorkletGlobalScope); it carries theSonareEngineAPI, the worklet processor classes and their registration helpers, and the ring-buffer protocol, and re-exports onlyinit/isInitializedfrom the main entry so the worklet realm can initialize its own WASM instance. - Analysis-only module —
sonare-analysis.js/sonare-analysis.wasmbehind the@libraz/libsonare/analysisentry, compiled without the mastering, mixing, realtime, and project bindings. CI records its size in a report, but size growth alone does not fail the build. - Offline Worker entry —
worker.jsbehind@libraz/libsonare/worker, the Worker side ofOfflineWorkerClient. - Voice-changer JSON Schemas — both preset schemas ship in the package under
schemas/, so a host can validate a preset document without fetching anything.
Bundle Size
The size table covers the main module and the main API entry. The analysis-only module, the realtime runtime, and the worklet bundle are separate artifacts and are not listed here — the analysis module is substantially smaller than sonare.wasm because it leaves the mastering, mixing, realtime, and project surfaces out.
| File | Size | Gzipped |
|---|---|---|
sonare.js | ~58 KB | ~14 KB |
index.js | ~254 KB | ~51 KB |
sonare.wasm | ~4,059 KB | ~1,376 KB |
| Total | ~4,372 KB | ~1,442 KB |
Troubleshooting
AudioContext Not Allowed
Modern browsers require user interaction before creating AudioContext:
document.addEventListener('click', async () => {
const audioCtx = new AudioContext();
await audioCtx.resume();
});Cross-Origin Issues
When loading audio from other domains:
const response = await fetch(url, {
mode: 'cors',
credentials: 'omit'
});Memory Issues
For very long audio files, consider analyzing in chunks:
const CHUNK_DURATION = 60; // seconds
for (let start = 0; start < totalDuration; start += CHUNK_DURATION) {
const chunk = samples.slice(
start * sampleRate,
(start + CHUNK_DURATION) * sampleRate
);
// Analyze chunk
}Native Failures Throw SonareError
When the C++ core rejects an input, the WASM binding throws a structured SonareError carrying a numeric code and codeName — never a raw Emscripten pointer number or an opaque [object Object]. Catch it with the exported isSonareError(...) guard and branch on ErrorCode; see Error Handling.