Skip to content

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

StepWhat happens
1. Load a fileUse fetch, an <input type="file">, drag-and-drop, or another browser source
2. Decode audioUse Audio.fromMemory(...), Audio.fromMemoryWithBrowserFallback(...), AudioContext.decodeAudioData(...), or your own decoder
3. Choose samplesPass one mono channel, downmix stereo yourself, or call stereo APIs where available
4. Call libsonarePass 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.

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.
  • Float32Array is 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, and RealtimeEngine;
  • understand the bundle-size and Worker/AudioWorklet tradeoffs before shipping a browser app.

Installation

npm/yarn

bash
npm install @libraz/libsonare
bash
yarn add @libraz/libsonare
bash
pnpm add @libraz/libsonare

CDN

html
<script type="module">
  import { init, detectBpm } from 'https://unpkg.com/@libraz/libsonare';
</script>

Basic Usage

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

  // Full analysis
  const result = analyze(samples, sampleRate);
  console.log(result);
}

CLI equivalent for the same one-file checks:

bash
sonare bpm music.mp3
sonare key music.mp3
sonare analyze music.mp3 --json

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-processingpreemphasis / deemphasis, trimSilence / splitSilence
  • Framing / size alignmentframeSignal, padCenter, fixLength, fixFrames
  • 1-D post-processingpeakPick, vectorNormalize
  • Featurespcen (mel dynamic-range compression), tonnetz (harmonic-space projection), tempogram / plp (tempo representations)
  • Unit conversionpowerToDb / 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.

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

bash
sonare mix \
  --preset vocalReverbSend \
  --input vocal.wav \
  --input music.wav \
  -o mixed.wav

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

typescript
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. analyze, hpss, and pitch are available in the Python CLI; pitch-shift is from the source-built C++ CLI:

bash
sonare analyze music.mp3 --json
sonare hpss music.mp3 --json
sonare pitch-shift music.wav --semitones 2 -o shifted.wav
sonare pitch music.mp3 --algorithm pyin --json

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

bash
sonare mastering track.wav --target-lufs -14 --ceiling-db -1 -o master.wav

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

typescript
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

typescript
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;
});

Web Worker Usage

Offload analysis to a Web Worker to avoid blocking the main thread.

worker.ts:

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

typescript
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

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

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

typescript
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

tsx
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

ApproachUse CaseLatencyFeatures
BatchPre-recorded filesHighFull analysis (BPM, chords, sections)
StreamingLive audio, visualizationLow (~10ms)Mel, chroma, onset, progressive BPM/key
METERS · LOUDNESSIDLE
Loudness metering — LUFS, true-peak, and range

The bar tracks momentary loudness as the clip plays; the panel is the loudness over time. Integrated LUFS is the single overall number, true-peak is the real ceiling between samples, and LRA captures how much the loudness moves. Switch the window to compare the fast momentary meter with the smoother short-term one.

Window

Architecture Overview

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.

typescript
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 throttling for 60fps
  const analyzer = new StreamAnalyzer({
    sampleRate: audioCtx.sampleRate,
    nFft: 2048,
    hopLength: 512,
    nMels: 128,
    computeMel: true,
    computeChroma: true,
    computeOnset: true,
    emitEveryNFrames: 4, // emit every 4 frames (~60fps at 44100Hz)
  });

  // 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 progressive BPM/key estimates
      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 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:

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

typescript
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 full 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 and a reduced realtime module at @libraz/libsonare/rt. The bridge's SonareEngine facade mirrors that engine to the worklet; see Realtime and Streaming.

The main package entry (@libraz/libsonare) also ships two main-thread browser-glue 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 wrapper has three read methods. Choose them by how much precision your UI needs and how much data you can afford to move between threads.

MethodReturned typeUse when
readFrames(maxFrames)FrameBuffer with Float32Array / Int32Array fieldsYou need full precision for analysis or high-quality visuals
readFramesI16(maxFrames)StreamFramesI16You want smaller payloads but still enough precision for most visual meters
readFramesU8(maxFrames)StreamFramesU8You need very small payloads for mobile or dense visual updates

Set StreamConfig.outputFormat to document the transfer format you plan to read, then call the matching method:

outputFormatRead method
0readFrames()
1readFramesI16()
2readFramesU8()

The analyzer still computes internally in float. readFramesI16() and readFramesU8() quantize (pack each float into a smaller 16-bit or 8-bit integer) in the C++/WASM read path, 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.

WASM wrapper returns that contain plain lists or objects 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 that postMessage can 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).
ApproachApprox. size per frameBest For
readFrames() (Float32 SoA)~600 bytesGeneral use, full precision
readFramesI16() (quantized SoA)~300 bytesHigh-quality visualizations
readFramesU8() (quantized SoA)~150 bytesMobile, bandwidth-limited

Progressive Estimation

The Streaming API provides progressive BPM and key estimates that improve over time:

typescript
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

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

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

bash
sonare mel-to-audio music.wav -o mel-reconstructed.wav
sonare mfcc-to-audio music.wav -o mfcc-reconstructed.wav
FunctionReturnsNotes
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?)Float32ArrayGriffin-Lim audio synthesis
mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels?)MelPowerResult { nMels, nFrames, power }Inverse DCT back to a mel spectrogram
mfccToAudio(mfccCoefficients, nMfcc, nFrames, nMels, sampleRate?, nFft?, hopLength?, fmin?, fmax?, nIter?, htk?)Float32ArrayMFCC → mel → audio in one call

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 full pipeline and caveats.

Streaming Retune

StreamingRetune is the WASM block-by-block mono retune wrapper. Use it for live or chunked pitch shifting when you need state to continue across blocks.

typescript
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. pitch-shift is from the source-built C++ CLI; voice-change is available in the Python CLI:

bash
sonare pitch-shift vocal.wav --semitones 3 -o shifted.wav
sonare voice-change vocal.wav --pitch-semitones 3 --formant-factor 1.0 -o voice.wav

Realtime Voice Changer

RealtimeVoiceChanger is the WASM wrapper for the preset-driven 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.

typescript
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. Current factory presets use schema version 1. If you need the canonical ID or resolved flat POD config, use voiceCharacterPresetId(...) and realtimeVoiceChangerPresetConfig(...).

Browser Compatibility

BrowserMinimum Version
Chrome57+
Firefox52+
Safari11+
Edge16+

Requirements:

  • WebAssembly support
  • Web Audio API
  • ES2017+ (async/await)

Package Artifacts

The published package ships a few coordinated pieces:

  • Main modulesonare.js plus sonare.wasm, the full 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 behind import ... from '@libraz/libsonare'; it exposes the full analysis, mastering, mixing, and editing API.
  • AudioWorklet entryworklet.js / worklet.d.ts, a separate, self-contained tsup bundle (no code-splitting, so it is fully portable into an AudioWorkletGlobalScope); it carries SonareEngine, the worklet processor classes, and the ring-buffer protocol, and re-exports only init / isInitialized from the main entry so the worklet realm can initialize its own WASM instance.
  • Realtime runtime — a dedicated lightweight AudioWorklet runtime (sonare-rt.wasm plus loaders, C-ABI only) you can select as a runtime target for engine playback when you do not need the full module. It is reachable via @libraz/libsonare/rt; see Realtime and Streaming.

Bundle Size

The size table covers the main module and the main API entry. The realtime runtime and worklet bundle are separate artifacts and are not listed here.

FileSizeGzipped
sonare.js~57 KB~14 KB
index.js~166 KB~35 KB
sonare.wasm~2,986 KB~1,070 KB
Total~3,210 KB~1,121 KB

Troubleshooting

AudioContext Not Allowed

Modern browsers require user interaction before creating AudioContext:

typescript
document.addEventListener('click', async () => {
  const audioCtx = new AudioContext();
  await audioCtx.resume();
});

Cross-Origin Issues

When loading audio from other domains:

typescript
const response = await fetch(url, {
  mode: 'cors',
  credentials: 'omit'
});

Memory Issues

For very long audio files, consider analyzing in chunks:

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