@libraz/libsonare 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,27 +5,25 @@
5
5
  [![npm downloads](https://img.shields.io/npm/dm/@libraz/libsonare)](https://www.npmjs.com/package/@libraz/libsonare)
6
6
  [![types](https://img.shields.io/npm/types/@libraz/libsonare)](https://www.npmjs.com/package/@libraz/libsonare)
7
7
  [![License](https://img.shields.io/github/license/libraz/libsonare)](https://github.com/libraz/libsonare/blob/main/LICENSE)
8
+ [![Docs](https://img.shields.io/badge/docs-libsonare.libraz.net-2563eb)](https://libsonare.libraz.net)
8
9
  [![PyPI](https://img.shields.io/pypi/v/libsonare?label=PyPI)](https://pypi.org/project/libsonare/)
9
10
 
10
11
  **Turn audio into data and back — entirely in the browser.** Analyze songs
11
12
  (BPM, key, chords, loudness), master and mix to broadcast loudness, and render
12
13
  MIDI through built-in instruments, all client-side via WebAssembly — the same
13
14
  C++ engine that runs natively, with zero dependencies and no Python or model
14
- weights. 66 named mastering DSP processors implemented against published
15
+ weights. 76 named mastering DSP processors implemented against published
15
16
  references (ITU-R BS.1770-4 true-peak limiting, Linkwitz-Riley crossovers,
16
17
  Vicanek matched-Z biquads, ADAA-antialiased saturation); analysis defaults match
17
18
  librosa where the two overlap.
18
19
 
19
- > **Audio input:** This package expects already-decoded `Float32Array` mono
20
- > samples (it does not bundle a file decoder). Use the Web Audio API in the
21
- > browser or `node:wasi` / a JS audio decoder in Node to obtain samples.
22
- > If you need to read WAV/MP3/M4A files directly in Node, use the native
23
- > N-API package [`@libraz/libsonare-native`](https://github.com/libraz/libsonare/tree/main/bindings/node) instead.
20
+ ## Try it in the browser
24
21
 
25
- > **Platform constraints:** the WebAssembly build is single-threaded (analysis
26
- > runs to completion on the calling thread — there is no non-blocking variant),
27
- > has no host filesystem access, and expects pre-decoded `Float32Array` sample
28
- > buffers. Drive long-running calls from a Web Worker to keep the UI responsive.
22
+ Everything runs client-side — no server, nothing uploaded.
23
+
24
+ - 🎧 **[Live demos](https://libsonare.libraz.net/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.
25
+ - 🎛️ **[sonare studio](https://sonare-studio.libraz.net)** — 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.
26
+ - 📖 **[Documentation & getting started](https://libsonare.libraz.net/docs/getting-started)**
29
27
 
30
28
  ## Installation
31
29
 
@@ -33,781 +31,109 @@ librosa where the two overlap.
33
31
  npm install @libraz/libsonare
34
32
  ```
35
33
 
36
- ## Usage
37
-
38
- ```typescript
39
- import { init, detectBpm, detectKey, analyze, Audio } from '@libraz/libsonare';
40
-
41
- await init();
42
-
43
- // Function API
44
- const bpm = detectBpm(samples, sampleRate);
45
- const key = detectKey(samples, sampleRate);
46
- const result = analyze(samples, sampleRate);
47
- console.log(`BPM: ${result.bpm}, Key: ${result.key.name}`);
48
-
49
- // Advanced key options are opt-in; defaults preserve existing behavior.
50
- const keyWithOptions = detectKey(samples, sampleRate, {
51
- useHpss: true,
52
- loudnessWeighted: true,
53
- highPassHz: 80,
54
- nFft: 4096,
55
- hopLength: 512,
56
- });
57
-
58
- // Audio class API
59
- const audio = Audio.fromBuffer(samples, sampleRate);
60
- console.log(`BPM: ${audio.detectBpm()}`);
61
- console.log(`Key: ${audio.detectKey().name}`);
62
- const audioKeyWithOptions = audio.detectKey({ useHpss: true, highPassHz: 80 });
63
- ```
64
-
65
- ### Request-object calls
66
-
67
- For top-level one-shot analysis, effects, mastering, metering, and feature APIs, pass one request object. This is the documented form because each input has a name and optional settings can grow without changing argument order. Existing positional calls remain supported for compatibility and produce the same results; stateful `Audio` methods and small scalar conversions retain their natural positional forms.
68
-
69
- ```typescript
70
- // Recommended
71
- detectBpm({ samples, sampleRate: 48_000 });
72
-
73
- // Still supported for existing applications
74
- detectBpm(samples, 48_000);
75
- ```
76
-
77
- ### Pitch, timbre, and spectral APIs
78
-
79
- Pitch tracking keeps unvoiced `f0` frames as `NaN` by default. Pass
80
- `fillNa: true` when downstream code needs finite values and should treat
81
- unvoiced frames as `0`. Timbre analysis returns aggregate metrics plus
82
- `timbreOverTime`.
83
-
84
- ```typescript
85
- import {
86
- analyzeTimbre,
87
- decompose,
88
- ebur128LoudnessRange,
89
- estimateTuning,
90
- hpssWithResidual,
91
- init,
92
- lufsInterleaved,
93
- nnFilter,
94
- phaseVocoder,
95
- pitchPyin,
96
- pitchTuning,
97
- pitchYin,
98
- polyFeatures,
99
- remix,
100
- spectralContrast,
101
- zeroCrossings,
102
- } from '@libraz/libsonare';
103
-
104
- await init();
105
-
106
- const yin = pitchYin(samples, sampleRate, 2048, 512, 65, 2093, 0.3, true);
107
- const pyin = pitchPyin(samples, sampleRate, 2048, 512, 65, 2093, 0.3, true);
108
-
109
- const timbre = analyzeTimbre(samples, sampleRate);
110
- console.log(timbre.brightness, timbre.timbreOverTime[0]?.brightness);
111
-
112
- const contrast = spectralContrast(samples, sampleRate); // Matrix2d result
113
- const poly = polyFeatures(samples, sampleRate); // Matrix2d result
114
- const crossings = zeroCrossings(samples); // Int32Array
115
- const tuning = estimateTuning(samples, sampleRate);
116
- const offset = pitchTuning(yin.f0);
117
-
118
- const { w, h } = decompose(spectrogram, nFeatures, nFrames, 8);
119
- const filtered = nnFilter(spectrogram, nFeatures, nFrames);
120
- const remixed = remix(samples, Int32Array.from([0, sampleRate, sampleRate, 2 * sampleRate]));
121
- const stretched = phaseVocoder(samples, 1.5, sampleRate);
122
- const hpss = hpssWithResidual(samples, sampleRate);
123
-
124
- const multi = lufsInterleaved(interleaved, 2, sampleRate);
125
- const lra = ebur128LoudnessRange(samples, sampleRate);
126
- ```
34
+ ## Quick Start
127
35
 
128
- ### Room acoustics
129
-
130
- Use `detectAcoustic` for blind RT60/EDT estimation from ordinary audio.
131
- Use `analyzeImpulseResponse` when you have a measured impulse response and need
132
- clarity metrics (`c50`, `c80`, `d50`). Blind mode returns `NaN` for clarity
133
- metrics because they are not reliable without an impulse response.
134
-
135
- ```typescript
136
- import { init, analyzeImpulseResponse, detectAcoustic } from '@libraz/libsonare';
137
-
138
- await init();
139
-
140
- // Blind estimation from ordinary audio; tuning options (nOctaveBands, …) are optional.
141
- const blind = detectAcoustic(samples, sampleRate);
142
- const room = analyzeImpulseResponse(irSamples, sampleRate);
143
- console.log(blind.rt60, room.c50);
144
- ```
145
-
146
- Acoustic simulation adds `synthesizeRir` (synthesize a shoebox-room impulse
147
- response from geometry), `estimateRoom` (recover an equivalent room from a
148
- recording or IR), and `roomMorph` (creatively re-reverberate audio toward a
149
- target room — not dereverberation).
150
-
151
- ```typescript
152
- import { init, synthesizeRir, estimateRoom, roomMorph } from '@libraz/libsonare';
153
-
154
- await init();
155
-
156
- const { rir, hasError } = synthesizeRir({
157
- lengthM: 6,
158
- widthM: 4,
159
- heightM: 3,
160
- absorption: 0.2,
161
- sampleRate: 48000,
162
- });
163
-
164
- const room = estimateRoom(samples, 48000); // { volume, length, width, height, ... }
165
- const morphed = roomMorph(samples, sampleRate, { lengthM: 12, widthM: 9, wet: 0.5 });
166
- ```
167
-
168
- ### Error handling
169
-
170
- Native (C++) failures are thrown as a `SonareError` carrying a numeric `code`
171
- (an `ErrorCode` value) and its canonical `codeName`, so you can branch on the
172
- cause instead of matching message text. Use the `isSonareError` type guard in a
173
- `catch`:
174
-
175
- ```typescript
176
- import { init, analyze, ErrorCode, isSonareError } from '@libraz/libsonare';
177
-
178
- await init();
179
-
180
- try {
181
- const result = analyze(samples, sampleRate);
182
- } catch (error) {
183
- if (isSonareError(error)) {
184
- console.error(`${error.codeName} (${error.code}): ${error.message}`);
185
- if (error.code === ErrorCode.InvalidParameter) {
186
- // recover...
187
- }
188
- } else {
189
- throw error;
190
- }
191
- }
192
- ```
36
+ `init()` loads the WASM module once; every API is available afterwards. Top-level
37
+ one-shot functions accept a request object (recommended) or positional arguments.
193
38
 
194
- ### Decoding files in the browser
39
+ > **Audio input:** analysis works on decoded `Float32Array` mono samples.
40
+ > `Audio.fromMemory` decodes WAV from an in-memory buffer, but the WebAssembly
41
+ > build bundles no decoder for compressed formats — use the Web Audio API
42
+ > (`decodeAudioData`) for MP3 / M4A / AAC / Opus / FLAC, or the native N-API
43
+ > package [`@libraz/libsonare-native`](https://github.com/libraz/libsonare/tree/main/bindings/node)
44
+ > to read files from disk.
195
45
 
196
- ```typescript
197
- import { init, analyze } from '@libraz/libsonare';
198
-
199
- await init();
200
-
201
- const arrayBuffer = await fetch('song.m4a').then((r) => r.arrayBuffer());
202
- const audioCtx = new AudioContext();
203
- const decoded = await audioCtx.decodeAudioData(arrayBuffer);
204
- // Mono downmix for libsonare:
205
- const samples = decoded.getChannelData(0);
206
- const result = analyze(samples, decoded.sampleRate);
207
- ```
208
-
209
- Web Audio's `decodeAudioData` handles whatever codecs the browser ships with
210
- (WAV/MP3/M4A/AAC/Opus/FLAC on most modern browsers).
211
-
212
- ### Browser (CDN)
213
-
214
- ```html
215
- <script type="module">
216
- import { init, analyze } from 'https://esm.sh/@libraz/libsonare';
217
-
218
- await init();
219
- // ... use with Web Audio API
220
- </script>
221
- ```
222
-
223
- ### Bundlers (Vite, webpack, Next.js, etc.)
224
-
225
- If your bundler doesn't automatically resolve the `.wasm` file, specify its path:
226
-
227
- ```typescript
228
- import wasmUrl from '@libraz/libsonare/wasm?url'; // Vite
229
- import { init } from '@libraz/libsonare';
230
-
231
- // `locateFile(path, prefix)` is called by the Emscripten loader to resolve the
232
- // `.wasm` file; return your bundler-provided URL for it.
233
- await init({ locateFile: (path) => (path.endsWith('.wasm') ? wasmUrl : path) });
234
- ```
235
-
236
- ### Real-time Streaming
237
-
238
- ```typescript
239
- import { init, StreamAnalyzer } from '@libraz/libsonare';
240
-
241
- await init();
242
-
243
- const analyzer = new StreamAnalyzer({ sampleRate: 44100 });
244
-
245
- // In audio processing callback
246
- analyzer.process(audioChunk);
247
-
248
- const stats = analyzer.stats();
249
- console.log(`BPM: ${stats.estimate.bpm}, Key: ${stats.estimate.key}`);
250
- ```
251
-
252
- ### Mastering (WASM)
253
-
254
- The npm package ships mastering DSP in the default WebAssembly build. Pass
255
- decoded `Float32Array` samples directly:
256
-
257
- ```typescript
258
- import { init, masteringChain, masteringChainStereo } from '@libraz/libsonare';
259
-
260
- await init();
261
-
262
- // Config is a tree of processor sections; set only what you want to change.
263
- const mastered = masteringChain(samples, sampleRate, {
264
- dynamics: { compressor: { thresholdDb: -24, ratio: 1.5 } },
265
- loudness: { targetLufs: -14, ceilingDb: -1 },
266
- });
267
-
268
- const stereo = masteringChainStereo(left, right, sampleRate, {
269
- stereo: { imager: { width: 1.1 } },
270
- loudness: { targetLufs: -14, ceilingDb: -1 },
271
- });
272
- ```
273
-
274
- Named mastering processors use the same names and behavior as the native,
275
- Python, C, and CLI APIs:
276
-
277
- ```typescript
278
- import {
279
- masteringPairAnalyze,
280
- masteringPairProcess,
281
- masteringPairProcessorNames,
282
- masteringProcess,
283
- masteringProcessStereo,
284
- masteringProcessorNames,
285
- masteringStereoAnalyze,
286
- } from '@libraz/libsonare';
287
-
288
- const names = masteringProcessorNames(); // e.g. "dynamics.compressor"
289
- const compressed = masteringProcess('dynamics.compressor', samples, sampleRate, {
290
- thresholdDb: -24,
291
- ratio: 1.5,
292
- });
293
-
294
- const widened = masteringProcessStereo('stereo.imager', left, right, sampleRate, {
295
- width: 1.1,
296
- });
297
-
298
- const pairNames = masteringPairProcessorNames(); // e.g. "match.abCrossfade"
299
- const crossfaded = masteringPairProcess('match.abCrossfade', source, reference, sampleRate, {
300
- mix: 0.25,
301
- });
302
-
303
- const loudnessJson = masteringPairAnalyze(
304
- 'match.referenceLoudness',
305
- source,
306
- reference,
307
- sampleRate,
308
- );
309
- const monoCompatJson = masteringStereoAnalyze(
310
- 'stereo.monoCompatCheck',
311
- left,
312
- right,
313
- sampleRate,
314
- );
315
- ```
316
-
317
- ### Mastering presets
46
+ > **Platform constraints:** the WebAssembly build is single-threaded (analysis
47
+ > runs to completion on the calling thread — there is no non-blocking variant)
48
+ > and has no host filesystem access. Drive long-running calls from a Web Worker
49
+ > to keep the UI responsive.
318
50
 
319
51
  ```typescript
320
- import { init, masterAudio, masteringPresetNames } from '@libraz/libsonare';
52
+ import { init, analyze, masterAudio } from '@libraz/libsonare';
321
53
 
322
54
  await init();
323
55
 
324
- masteringPresetNames(); // ['pop', 'edm', 'acoustic', 'hipHop', 'aiMusic', 'speech', 'streaming', 'youtube', 'broadcast', 'podcast', 'audiobook', 'cinema', 'jpop', 'ambient', 'lofi', 'classical', 'drumAndBass', 'techno', 'metal', 'trap', 'rnb', 'jazz', 'kpop', 'trance', 'gameOst']
325
-
326
- const result = masterAudio({
327
- samples,
328
- sampleRate,
329
- preset: 'aiMusic',
330
- // optional overrides applied on top of the preset
331
- overrides: { loudness: { targetLufs: -13 } },
332
- });
333
- console.log(result.outputLufs, result.appliedGainDb);
334
- ```
335
-
336
- ### Mixing
56
+ // Analyze decoded Float32Array mono samples
57
+ const { bpm, key } = analyze({ samples, sampleRate });
58
+ console.log(`BPM: ${bpm} Key: ${key.name}`);
337
59
 
338
- ```typescript
339
- import { init, Mixer, mixStereo, mixingScenePresetJson } from '@libraz/libsonare';
340
-
341
- await init();
342
-
343
- const sceneJson = mixingScenePresetJson('vocalReverbSend');
344
- const offline = mixStereo([vocalL, musicL], [vocalR, musicR], sampleRate, {
345
- inputTrimDb: [3, 0],
346
- faderDb: [-3, -12],
347
- pan: [0, -0.2],
348
- width: [1, 0.9],
349
- });
350
-
351
- const mixer = Mixer.fromSceneJson(sceneJson, sampleRate, 512);
352
- const block = mixer.processStereo([vocalBlockL, returnBlockL], [vocalBlockR, returnBlockR]);
353
- console.log(offline.meters[0].maxTruePeakDb, block.left.length);
354
-
355
- const outL = new Float32Array(512);
356
- const outR = new Float32Array(512);
357
- mixer.processStereoInto([vocalBlockL, returnBlockL], [vocalBlockR, returnBlockR], outL, outR);
358
-
359
- const realtime = mixer.createRealtimeBuffer();
360
- realtime.leftInputs[0].set(vocalBlockL);
361
- realtime.rightInputs[0].set(vocalBlockR);
362
- realtime.leftInputs[1].set(returnBlockL);
363
- realtime.rightInputs[1].set(returnBlockR);
364
- realtime.process();
365
- console.log(realtime.outLeft[0], realtime.outRight[0]);
366
- mixer.delete();
60
+ // Master toward a target loudness with a named preset
61
+ const mastered = masterAudio({ samples, sampleRate, preset: 'streaming' });
62
+ console.log(mastered.outputLufs, mastered.appliedGainDb);
367
63
  ```
368
64
 
369
- ### Headless DAW project
370
-
371
- `Project` is a headless arrangement model: audio & MIDI tracks and clips, MIDI
372
- sequencing, SMF / MIDI 2.0 Clip File I/O, deterministic JSON save/load, and an
373
- offline `bounce`. Every mutation routes through an undoable history, and musical
374
- positions are PPQ (quarter notes). Call `delete()` (or wrap in `try/finally`) to
375
- release the WASM object — the embind handle is not garbage-collected.
65
+ Render a MIDI arrangement through a built-in instrument with the headless
66
+ `Project`. The embind handle is not garbage-collected — call `delete()` when done.
376
67
 
377
68
  ```typescript
378
69
  import { init, Project } from '@libraz/libsonare';
379
70
 
380
71
  await init();
381
72
 
382
- const project = new Project();
383
- try {
384
- project.setSampleRate(48000);
385
-
386
- const { clipId } = project.addMidiClip(0, 4); // { trackId, clipId }
387
- project.setMidiEvents(clipId, [
388
- Project.midiNoteOn(0, 0, 0, 60, 100), // ppq, group, channel, note, velocity
389
- Project.midiNoteOff(1, 0, 0, 60),
390
- ]);
391
-
392
- const json = project.toJson(); // deterministic, byte-stable within a build
393
- const smf = project.exportSmf(); // Uint8Array — Standard MIDI File
394
- const midi2 = project.exportClipFile(); // Uint8Array — MIDI 2.0 Clip File (lossless)
395
-
396
- const { hasTimeline, diagnostics } = project.compile();
397
- const audio = project.bounce({ numChannels: 2 }); // interleaved Float32Array
398
- } finally {
399
- project.delete();
400
- }
401
- ```
402
-
403
- #### Clip warp
404
-
405
- A clip can be time-warped during an offline `bounce`. `setClipWarpMode` selects
406
- the playback mode (`ProjectWarpMode`: `'off'` | `'repitch'` | `'tempo-sync'`),
407
- `setClipWarpRef` binds it to a warp map, and `setWarpMap` registers a first-class
408
- warp map (anchors mapping warp-timeline samples to source samples).
409
-
410
- ```typescript
411
- project.setWarpMap({
412
- id: 1,
413
- name: 'main',
414
- anchors: [
415
- { warpSample: 0, sourceSample: 0 },
416
- { warpSample: 48000, sourceSample: 24000 },
417
- ],
418
- });
419
- project.setClipWarpRef(clipId, 1);
420
- project.setClipWarpMode(clipId, 'tempo-sync');
421
- ```
422
-
423
- > Warp is an offline `Project.bounce` feature only. Realtime warp playback is
424
- > **not** available in `RealtimeEngine`.
425
-
426
- ### Instruments and synthesis
427
-
428
- MIDI tracks bounce silently unless an instrument is bound. `Project` offers
429
- three instrument backends, each as a `bounceWith…` variant that takes a binding
430
- (or array of bindings) plus the usual `ProjectBounceOptions`:
431
-
432
- - `bounceWithBuiltinInstrument(binding?, options?)` — simple built-in oscillator
433
- synth (`BuiltinSynthConfig` / `BuiltinSynthBinding`: waveform + ADSR + gain).
434
- - `bounceWithSynthInstrument(patchOrName?, options?)` — patch-driven NativeSynth
435
- (`SynthPatch`, or a preset-name string like `'saw-lead'`).
436
- - `bounceWithSf2Instrument(config?, options?)` — GS-compatible SoundFont player
437
- (`Sf2InstrumentConfig`), fed by `loadSoundFont()`.
438
-
439
- Discover NativeSynth presets with `synthPresetNames()` and fetch one as an
440
- editable patch with `synthPresetPatch(name)`.
441
-
442
- ```typescript
443
- import { init, Project, synthPresetNames, synthPresetPatch } from '@libraz/libsonare';
444
-
445
- await init();
446
-
447
73
  const project = new Project();
448
74
  try {
449
75
  const { clipId } = project.addMidiClip(0, 4);
450
76
  project.setMidiEvents(clipId, [
451
- Project.midiNoteOn(0, 0, 0, 60, 100),
77
+ Project.midiNoteOn(0, 0, 0, 60, 100), // ppq, group, channel, note, velocity
452
78
  Project.midiNoteOff(1, 0, 0, 60),
453
79
  ]);
454
-
455
- // Built-in oscillator synth.
456
- const a = project.bounceWithBuiltinInstrument({ waveform: 'saw' }, { numChannels: 2 });
457
-
458
- // NativeSynth from a named preset, tweaked.
459
- const patch = synthPresetPatch(synthPresetNames()[0]);
460
- patch.cutoffHz = 4000;
461
- const b = project.bounceWithSynthInstrument(patch, { numChannels: 2 });
462
-
463
- // SoundFont player (requires loadSoundFont first).
464
- project.loadSoundFont(sf2Bytes);
465
- const c = project.bounceWithSf2Instrument({ gain: 0.6 }, { numChannels: 2 });
80
+ const audio = project.bounceWithSynthInstrument('saw-lead', { numChannels: 2 });
466
81
  } finally {
467
82
  project.delete();
468
83
  }
469
84
  ```
470
85
 
471
- ### Real-time engine
472
-
473
- `RealtimeEngine` is a control-thread-driven transport + render engine: it plays
474
- a clip/automation timeline, hosts MIDI instruments, accepts live MIDI, and
475
- renders blocks (or bounces offline). Bind it to an AudioWorklet for browser
476
- playback — see the [AudioWorklet bridge](#audioworklet-bridge) below; the engine
477
- is the offline/headless half, the worklet is the audio-thread half.
478
-
479
- ```typescript
480
- import { init, RealtimeEngine } from '@libraz/libsonare';
481
-
482
- await init();
483
-
484
- const engine = new RealtimeEngine(48000, 128); // sampleRate, maxBlockSize
485
- try {
486
- engine.setSynthInstrument('saw-lead', 0); // patch (or name), destinationId
487
- engine.play();
488
- engine.pushMidiNoteOn(0, 0, 0, 60, 100); // destination, group, channel, note, velocity
489
- engine.pushMidiNoteOff(0, 0, 0, 60);
490
-
491
- const blockL = new Float32Array(128);
492
- const blockR = new Float32Array(128);
493
- const out = engine.process([blockL, blockR]); // Float32Array[] per channel
494
- const telemetry = engine.drainTelemetry();
495
- } finally {
496
- engine.destroy();
497
- }
498
- ```
86
+ ### Decoding audio in the browser
499
87
 
500
- Capabilities:
501
-
502
- - **Transport**: `play` / `stop` / `seekSample` / `seekPpq` / `setTempo` /
503
- `setTimeSignature` / `setLoop`, plus `getTransportState`.
504
- - **Instruments**: `setBuiltinInstrument` / `setSynthInstrument` /
505
- `setSf2Instrument` (+ `loadSoundFont`) per MIDI destination id.
506
- - **Live MIDI**: `pushMidiNoteOn` / `pushMidiNoteOff` / `pushMidiCc` /
507
- `pushMidiPanic`, and `bindMidiCc(channel, controller, paramId, options?)` to
508
- map a CC to an automation parameter.
509
- - **Process / bounce**: `process` (real-time blocks), the allocation-free
510
- `prepareChannels` + `getChannelBuffer` + `processPrepared` worklet path,
511
- `renderOffline`, `bounceOffline`, `freezeOffline`.
512
- - **Clip page providers**: `createClipPageProvider` + `supplyClipPage` for
513
- streaming large clip audio in pages; pair with the OPFS helpers
514
- (`createOpfsClipPageProvider`) and the bounded-memory `ClipPageStreamer`
515
- (see below) so a long multitrack arrangement never holds its full PCM in
516
- WASM memory.
517
- - **Telemetry**: `drainTelemetry` / `drainMeterTelemetry`. Inspect runtime
518
- capabilities (ABI compatibility, SharedArrayBuffer/Atomics) via
519
- `engineCapabilities()`.
520
-
521
- #### Bounded-memory clip streaming
522
-
523
- `ClipPageStreamer` keeps OPFS-paged clips fed within a sliding window around the
524
- playback position, evicting pages that fall outside it. Resident memory per clip
525
- is bounded to `retainBehindPages + readAheadPages + 1` pages regardless of clip
526
- length. `attachOpfsClipStream` wires the provider, primes the leading page, and
527
- registers it in one call; `setClips` schedules the returned `provider`, and
528
- `pump()` (called on your control-thread tick) services page misses.
88
+ The WASM build takes decoded samples, so decode with the Web Audio API first:
529
89
 
530
90
  ```typescript
531
- import { ClipPageStreamer, attachOpfsClipStream, RealtimeEngine } from '@libraz/libsonare';
532
-
533
- const engine = new RealtimeEngine(48000, 128);
534
- const streamer = new ClipPageStreamer(engine, { readAheadPages: 2, retainBehindPages: 1 });
535
-
536
- const { provider } = await attachOpfsClipStream(streamer, engine, {
537
- path: 'clips/vocal.f32',
538
- clipId: 1,
539
- numChannels: 2,
540
- numSamples: 26_460_000, // ~10 min at 44.1 kHz, never fully resident
541
- pageFrames: 65_536,
542
- });
543
- engine.setClips([{ id: 1, pageProvider: provider, startPpq: 0 }]);
544
- engine.play();
545
-
546
- // On each animation frame / control tick:
547
- await streamer.pump(); // fetch upcoming pages, evict old ones
91
+ const buf = await fetch('song.m4a').then((r) => r.arrayBuffer());
92
+ const decoded = await new AudioContext().decodeAudioData(buf);
93
+ const { bpm, key } = analyze({ samples: decoded.getChannelData(0), sampleRate: decoded.sampleRate });
548
94
  ```
549
95
 
550
- ### Real-time voice changer
96
+ ### Loading the `.wasm` file
551
97
 
552
- `RealtimeVoiceChanger` runs a block-by-block voice transformation chain (retune,
553
- formant shaping, EQ, gate, compressor). Construct it from a preset id (see
554
- `realtimeVoiceChangerPresetNames()`) or a full config object, then process blocks.
98
+ Bundlers that don't auto-resolve the `.wasm` asset need its URL. Pass a
99
+ `locateFile` resolver to `init()`:
555
100
 
556
101
  ```typescript
557
- import { init, RealtimeVoiceChanger, voiceChangeRealtime } from '@libraz/libsonare';
102
+ import wasmUrl from '@libraz/libsonare/wasm?url'; // Vite; adapt per bundler
558
103
 
559
- await init();
560
-
561
- const changer = new RealtimeVoiceChanger('bright-idol');
562
- try {
563
- changer.prepare(48000, 128, 1); // sampleRate, maxBlockSize, channels
564
- const out = changer.processMono(block);
565
- } finally {
566
- changer.delete();
567
- }
568
-
569
- // Whole-buffer convenience wrapper (constructs/prepares/disposes internally).
570
- const processed = voiceChangeRealtime(samples, 48000, 'deep-narrator');
571
- ```
572
-
573
- For a simple offline pitch + formant shift without the full chain, use
574
- `voiceChange(samples, sampleRate, { pitchSemitones: -2, formantFactor: 1.1 })`.
575
- Inspect a preset with `realtimeVoiceChangerPresetJson(name)`.
576
-
577
- ### AudioWorklet bridge
578
-
579
- The package exposes an optional worklet entry that uses the same `sonare.wasm`
580
- as the offline API. The bridge processes fixed 128-sample render quanta and
581
- treats each AudioWorklet input as one stereo mixer strip.
582
-
583
- ```typescript
584
- // worklet.ts, loaded with audioContext.audioWorklet.addModule(...)
585
- import { init, mixingScenePresetJson } from '@libraz/libsonare';
586
- import { registerSonareWorkletProcessor } from '@libraz/libsonare/worklet';
587
-
588
- await init();
589
- registerSonareWorkletProcessor();
590
- ```
591
-
592
- ```typescript
593
- // main thread
594
- import { mixingScenePresetJson } from '@libraz/libsonare';
595
-
596
- await audioContext.audioWorklet.addModule('/worklet.js');
597
- const sceneJson = mixingScenePresetJson('vocalReverbSend');
598
- const node = new AudioWorkletNode(audioContext, 'sonare-worklet-processor', {
599
- numberOfInputs: 2,
600
- numberOfOutputs: 1,
601
- outputChannelCount: [2],
602
- processorOptions: {
603
- sceneJson,
604
- sampleRate: audioContext.sampleRate,
605
- blockSize: 128,
606
- spectrumIntervalFrames: 2048,
607
- spectrumBands: 16,
608
- },
609
- });
610
-
611
- node.port.postMessage({
612
- type: 'scheduleInsertAutomation',
613
- stripIndex: 0,
614
- insertIndex: 0,
615
- paramId: 0,
616
- samplePos: 0,
617
- value: 0,
618
- curve: 'linear',
619
- });
620
-
621
- node.port.onmessage = (event) => {
622
- if (event.data?.type === 'meter') {
623
- console.log(event.data.peakDbL, event.data.rmsDbL, event.data.correlation);
624
- } else if (event.data?.type === 'spectrum') {
625
- console.log(event.data.frame, event.data.bands);
626
- }
627
- };
628
- ```
629
-
630
- For cross-origin-isolated pages, meters and spectrum snapshots can use optional
631
- SharedArrayBuffer rings instead of per-message `postMessage`:
632
-
633
- ```typescript
634
- import {
635
- createSonareMeterRingBuffer,
636
- createSonareSpectrumRingBuffer,
637
- readSonareMeterRingBuffer,
638
- readSonareSpectrumRingBuffer,
639
- } from '@libraz/libsonare/worklet';
640
-
641
- const meterRing = createSonareMeterRingBuffer(128);
642
- const spectrumRing = createSonareSpectrumRingBuffer(64, 16);
643
- const node = new AudioWorkletNode(audioContext, 'sonare-worklet-processor', {
644
- numberOfInputs: 2,
645
- numberOfOutputs: 1,
646
- outputChannelCount: [2],
647
- processorOptions: {
648
- sceneJson,
649
- sampleRate: audioContext.sampleRate,
650
- blockSize: 128,
651
- meterSharedBuffer: meterRing.sharedBuffer,
652
- spectrumIntervalFrames: 2048,
653
- spectrumSharedBuffer: spectrumRing.sharedBuffer,
654
- },
655
- });
656
-
657
- let nextMeterRead = 0;
658
- let nextSpectrumRead = 0;
659
- function readMeters() {
660
- const result = readSonareMeterRingBuffer(meterRing, nextMeterRead);
661
- nextMeterRead = result.nextReadIndex;
662
- for (const meter of result.meters) {
663
- console.log(meter.frame, meter.peakDbL, meter.rmsDbL);
664
- }
665
- const spectra = readSonareSpectrumRingBuffer(spectrumRing, nextSpectrumRead);
666
- nextSpectrumRead = spectra.nextReadIndex;
667
- for (const spectrum of spectra.spectra) {
668
- console.log(spectrum.frame, spectrum.bands);
669
- }
670
- }
671
- ```
672
-
673
- #### RealtimeEngine AudioWorklet facade
674
-
675
- For browser DAW-style playback, `SonareEngine` keeps a main-thread
676
- `RealtimeEngine` mirror for offline renders and synchronizes the live worklet
677
- engine through control messages. There is a single runtime: the full-featured
678
- embind engine runs both the main-thread mirror and the AudioWorklet.
679
-
680
- ```typescript
681
- import { init } from '@libraz/libsonare';
682
- import { init as initWorklet, SonareEngine } from '@libraz/libsonare/worklet';
683
-
684
- await init();
685
- await audioContext.audioWorklet.addModule('/worklet.js');
686
- await initWorklet();
687
-
688
- const engine = await SonareEngine.create(audioContext, { mode: 'sab' });
689
- engine.setTrackLanes([{ trackId: 1 }]);
690
- engine.setTrackStripJson(1, trackSceneJson);
691
- engine.addClip(1, [clipL, clipR], 0);
692
- engine.setTempoSegments([{ startPpq: 0, bpm: 120 }]);
693
- engine.setTimeSignatureSegments([{ startPpq: 0, numerator: 4, denominator: 4 }]);
694
- engine.transport.play();
695
- ```
696
-
697
- | Need | Facade/API |
698
- |---|---|
699
- | Track routing, fader, pan, solo/mute | `setTrackLanes`, `setStripGain`, `setStripPan`, `setSoloMute` |
700
- | Track/master inserts and EQ | `setTrackStripJson`, `setMasterStripJson`, `setTrackStripEqBand`, `setMasterStripEqBand`, insert bypass methods |
701
- | Sends and buses | `setSends`, `setBusGain`, `setBusStripJson` |
702
- | MIDI clips and live MIDI | `setMidiClips`, `pushMidiNoteOn`, `pushMidiNoteOff`, `pushMidiCc`, `pushMidiPanic` |
703
- | Instruments | `setBuiltinInstrument`, `setSynthInstrument`, `loadSoundFont`, `setSf2Instrument` |
704
- | Recording and monitoring | `configureCapture` (incl. the `inputMonitor` option), `armRecord`, `punch`, `capturedAudio`, `captureStatus` |
705
- | Transport, tempo, markers | `getTransportState`, `cachedTransportState`, `setTempoSegments`, `setTimeSignatureSegments`, marker methods, `setLoopFromMarkers` |
706
- | Clip updates | `addClip`, `removeClip`; the facade sends clip deltas while the processor keeps full-sync compatibility |
707
- | Meters | `onMeter` messages or the meter SharedArrayBuffer ring; records include master, lane, bus, and input target ids |
708
-
709
- Studio integration notes:
710
-
711
- - If a host worklet entry filters message names before forwarding, add every
712
- `sync*`, `captureRequest`, and `transportRequest` message used above to that
713
- allowlist; otherwise the host may silently drop the new control messages.
714
- - If a host vendors built worklet bundles, regenerate and reimport
715
- `worklet.js`, `worklet.d.ts`, `sonare.js`, and `sonare.wasm` together.
716
-
717
- ### Progress callback
718
-
719
- `masteringChainWithProgress` (and its stereo variant) is `masteringChain` with
720
- an extra `(progress, stage) => void` callback invoked after each enabled stage:
721
-
722
- ```typescript
723
- import { init, masteringChainWithProgress } from '@libraz/libsonare';
724
-
725
- await init();
726
-
727
- masteringChainWithProgress(
728
- samples,
729
- sampleRate,
730
- { dynamics: { compressor: { thresholdDb: -24 } } },
731
- (progress, stage) => console.log(`${stage}: ${(progress * 100).toFixed(0)}%`),
732
- );
733
- ```
734
-
735
- ### Streaming mastering chain
736
-
737
- `StreamingMasteringChain` processes blocks while maintaining per-stage state
738
- across calls. It only supports modules whose state depends solely on the
739
- sample rate — it cannot include `repair.denoise` or `loudness` (those require
740
- offline / look-ahead analysis) and throws at construction if they are enabled.
741
-
742
- ```typescript
743
- import { init, StreamingMasteringChain } from '@libraz/libsonare';
744
-
745
- await init();
746
-
747
- const chain = new StreamingMasteringChain({
748
- eq: { tiltDb: 0.5 },
749
- dynamics: { compressor: { thresholdDb: -20 } },
750
- });
751
- chain.prepare(48000, 512, 2);
752
-
753
- // Mono block
754
- const monoBlock = new Float32Array(512);
755
- const processedMono = chain.processMono(monoBlock);
756
-
757
- // Stereo block (separate L/R Float32Arrays)
758
- const left = new Float32Array(512);
759
- const right = new Float32Array(512);
760
- const { left: outL, right: outR } = chain.processStereo(left, right);
761
-
762
- chain.reset();
763
- chain.delete(); // release WASM memory
104
+ await init({ locateFile: (path) => (path.endsWith('.wasm') ? wasmUrl : path) });
764
105
  ```
765
106
 
766
- ### Streaming equalizer and retune
107
+ From a CDN, `import { init } from 'https://esm.sh/@libraz/libsonare'` resolves the
108
+ `.wasm` automatically. See the
109
+ [getting-started guide](https://libsonare.libraz.net/docs/getting-started) for
110
+ per-bundler setup and the AudioWorklet bridge.
767
111
 
768
- `StreamingEqualizer` wraps the unified `EqualizerProcessor` (up to 24 bands,
769
- RBJ/Vicanek biquads, dynamic EQ, linear-phase FIR, mid/side, auto-gain) with
770
- state maintained across calls. `StreamingRetune` is a block-by-block mono voice
771
- retune / pitch shifter.
112
+ ## Capabilities
772
113
 
773
- ```typescript
774
- import { init, StreamingEqualizer, StreamingRetune } from '@libraz/libsonare';
114
+ Every area below has runnable examples and the full API in the
115
+ [documentation](https://libsonare.libraz.net/docs/wasm).
775
116
 
776
- await init();
117
+ - **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](https://libsonare.libraz.net/docs/wasm)
118
+ - **Mastering** — 76 named DSP processors, the configurable `masteringChain`, 25 named presets via `masterAudio`, and reference-matching. → [Mastering processors](https://libsonare.libraz.net/docs/mastering-processors)
119
+ - **Mixing** — offline `mixStereo` and the block-based `Mixer` with scene presets. → [Mixing](https://libsonare.libraz.net/docs/mixing)
120
+ - **Editing DSP** — time-stretch, pitch-shift, HPSS (+ residual), phase vocoder, normalize, trim, remix. → [Editing DSP](https://libsonare.libraz.net/docs/editing-dsp)
121
+ - **Room acoustics** — blind RT60 / EDT, impulse-response clarity metrics, RIR synthesis, room estimation and morphing. → [Room acoustics](https://libsonare.libraz.net/docs/acoustic-analysis)
122
+ - **Realtime & streaming** — `RealtimeEngine` (transport / MIDI / render, bounded-memory clip streaming), `StreamingMasteringChain` / `StreamingEqualizer` / `StreamingRetune`, `RealtimeVoiceChanger`, and the AudioWorklet bridge. → [Realtime & streaming](https://libsonare.libraz.net/docs/realtime-streaming)
123
+ - **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](https://libsonare.libraz.net/docs/wasm)
124
+ - **Headless DAW** — `Project` arrangement model: audio / MIDI tracks and clips, undo/redo, clip warp, SMF / MIDI 2.0 Clip File I/O, deterministic JSON, offline `bounce`. → [API](https://libsonare.libraz.net/docs/wasm)
125
+ - **Conversions** — Hz / mel / MIDI / note, frames / time, resample.
777
126
 
778
- const eq = new StreamingEqualizer({ sampleRate: 48000, maxBlockSize: 512 });
779
- try {
780
- eq.setBand(0, { type: 'HighShelf', frequencyHz: 8000, gainDb: 6, enabled: true });
781
- const { left: eqL, right: eqR } = eq.processStereo(left, right);
782
- } finally {
783
- eq.delete();
784
- }
127
+ Native failures throw a `SonareError` carrying a numeric `code` (an `ErrorCode`
128
+ value) and its `codeName`; narrow with the `isSonareError` type guard.
785
129
 
786
- const retune = new StreamingRetune({ semitones: 2, mix: 1.0 });
787
- try {
788
- retune.prepare(48000, 512); // sampleRate, maxBlockSize
789
- const shifted = retune.processMono(monoBlock);
790
- } finally {
791
- retune.delete();
792
- }
793
- ```
130
+ ## Documentation
794
131
 
795
- ## Features
796
-
797
- - **Detection**: `detectBeats`, `detectOnsets`, `detectDownbeats`, `detectChords`, `detectKey`, `detectKeyCandidates`, `chordFunctionalAnalysis`, sections
798
- - **Analysis**: `analyze`, `analyzeWithProgress`, `analyzeBpm`, `analyzeRhythm`, `analyzeDynamics`, `analyzeTimbre`; `hasFfmpegSupport` capability check
799
- - **Effects**: HPSS, HPSS with residual, time stretch, phase vocoder, pitch shift, normalize, trim, remix
800
- - **Mastering**: EQ, compressor, tape/exciter, air band, stereo imaging,
801
- true-peak limiting, loudness optimization
802
- - **Features**: STFT, mel spectrogram, MFCC, chroma, CQT/VQT, spectral contrast, poly features, zero crossings
803
- - **Pitch**: YIN, pYIN algorithms with optional `fillNa`
804
- - **Decomposition & loudness**: NMF decomposition, nearest-neighbour filtering, multichannel LUFS, EBU R128 LRA
805
- - **Streaming**: Real-time analysis with progressive estimates; streaming mastering chain, equalizer, and retune
806
- - **Instruments**: built-in synth, patch-driven NativeSynth (12 synthesis engines, incl. physically-modeled piano/strings/winds — being tuned over time), SoundFont (SF2) player — bound to `Project` bounces or the `RealtimeEngine`
807
- - **Real-time**: `RealtimeEngine` transport/MIDI/render, `RealtimeVoiceChanger`, AudioWorklet bridge
808
- - **Room acoustics**: blind RT60/EDT, impulse-response clarity metrics, RIR synthesis, room estimation, room morphing
809
- - **Headless DAW**: `Project` arrangement model — audio/MIDI tracks & clips, undo/redo, MIDI sequencing, clip warp, SMF / MIDI 2.0 Clip File I/O, deterministic JSON, offline `bounce`
810
- - **Conversions**: Hz/mel/MIDI/note, frames/time, resample
132
+ Full API reference, guides, and browser-local demos live at
133
+ **[libsonare.libraz.net](https://libsonare.libraz.net)**
134
+ ([getting started](https://libsonare.libraz.net/docs/getting-started) ·
135
+ [browser / WASM API](https://libsonare.libraz.net/docs/wasm) ·
136
+ [demos](https://libsonare.libraz.net/demos)).
811
137
 
812
138
  ## Also available
813
139
 
@@ -815,6 +141,9 @@ try {
815
141
  pip install libsonare # Python bindings with CLI
816
142
  ```
817
143
 
144
+ The native Node.js N-API binding (reads files from disk) lives at
145
+ [`bindings/node`](https://github.com/libraz/libsonare/tree/main/bindings/node).
146
+
818
147
  ## License
819
148
 
820
149
  [Apache License 2.0](https://github.com/libraz/libsonare/blob/main/LICENSE)