@libraz/libsonare 1.0.4 → 1.2.0

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
@@ -2,10 +2,18 @@
2
2
 
3
3
  [![CI](https://img.shields.io/github/actions/workflow/status/libraz/libsonare/ci.yml?branch=main&label=CI)](https://github.com/libraz/libsonare/actions)
4
4
  [![npm](https://img.shields.io/npm/v/@libraz/libsonare)](https://www.npmjs.com/package/@libraz/libsonare)
5
- [![PyPI](https://img.shields.io/pypi/v/libsonare)](https://pypi.org/project/libsonare/)
5
+ [![npm downloads](https://img.shields.io/npm/dm/@libraz/libsonare)](https://www.npmjs.com/package/@libraz/libsonare)
6
+ [![types](https://img.shields.io/npm/types/@libraz/libsonare)](https://www.npmjs.com/package/@libraz/libsonare)
6
7
  [![License](https://img.shields.io/github/license/libraz/libsonare)](https://github.com/libraz/libsonare/blob/main/LICENSE)
8
+ [![PyPI](https://img.shields.io/pypi/v/libsonare?label=PyPI)](https://pypi.org/project/libsonare/)
7
9
 
8
- Fast, dependency-free audio analysis library for browser and Node.js via WebAssembly.
10
+ A dependency-free audio DSP toolkit for browser and Node.js via WebAssembly
11
+ librosa-compatible analysis plus broadcast-grade mastering, mixing, and editing.
12
+ The same C++ processors run client-side in the browser: 77 named mastering DSP
13
+ processors implemented against published references (ITU-R BS.1770-4 true-peak
14
+ limiting, Linkwitz-Riley crossovers, Vicanek matched-Z biquads, ADAA-antialiased
15
+ saturation), with analysis defaults matching librosa — Apache-2.0, no Python,
16
+ no model weights.
9
17
 
10
18
  > **Audio input:** This package expects already-decoded `Float32Array` mono
11
19
  > samples (it does not bundle a file decoder). Use the Web Audio API in the
@@ -32,10 +40,37 @@ const key = detectKey(samples, sampleRate);
32
40
  const result = analyze(samples, sampleRate);
33
41
  console.log(`BPM: ${result.bpm}, Key: ${result.key.name}`);
34
42
 
43
+ // Advanced key options are opt-in; defaults preserve existing behavior.
44
+ const keyWithOptions = detectKey(samples, sampleRate, {
45
+ useHpss: true,
46
+ loudnessWeighted: true,
47
+ highPassHz: 80,
48
+ nFft: 4096,
49
+ hopLength: 512,
50
+ });
51
+
35
52
  // Audio class API
36
53
  const audio = Audio.fromBuffer(samples, sampleRate);
37
54
  console.log(`BPM: ${audio.detectBpm()}`);
38
55
  console.log(`Key: ${audio.detectKey().name}`);
56
+ const audioKeyWithOptions = audio.detectKey({ useHpss: true, highPassHz: 80 });
57
+ ```
58
+
59
+ ### Room acoustics
60
+
61
+ Use `detectAcoustic` for blind RT60/EDT estimation from ordinary audio.
62
+ Use `analyzeImpulseResponse` when you have a measured impulse response and need
63
+ clarity metrics (`c50`, `c80`, `d50`). Blind mode returns `NaN` for clarity
64
+ metrics because they are not reliable without an impulse response.
65
+
66
+ ```typescript
67
+ import { init, analyzeImpulseResponse, detectAcoustic } from '@libraz/libsonare';
68
+
69
+ await init();
70
+
71
+ const blind = detectAcoustic(samples, sampleRate, 6, 24, 30.0, 10.0);
72
+ const room = analyzeImpulseResponse(irSamples, sampleRate);
73
+ console.log(blind.rt60, room.c50);
39
74
  ```
40
75
 
41
76
  ### Decoding files in the browser
@@ -94,10 +129,272 @@ const stats = analyzer.stats();
94
129
  console.log(`BPM: ${stats.estimate.bpm}, Key: ${stats.estimate.key}`);
95
130
  ```
96
131
 
132
+ ### Mastering (WASM)
133
+
134
+ The npm package ships mastering DSP in the default WebAssembly build. Pass
135
+ decoded `Float32Array` samples directly:
136
+
137
+ ```typescript
138
+ import { init, masteringChain, masteringChainStereo } from '@libraz/libsonare';
139
+
140
+ await init();
141
+
142
+ const mastered = masteringChain(samples, sampleRate, {
143
+ eq: { tiltDb: 1.0 },
144
+ dynamics: { compressor: { thresholdDb: -24, ratio: 1.5 } },
145
+ saturation: { tape: { driveDb: 1.0, saturation: 0.2 } },
146
+ loudness: { targetLufs: -14, ceilingDb: -1, truePeakOversample: 4 },
147
+ });
148
+
149
+ const stereo = masteringChainStereo(left, right, sampleRate, {
150
+ stereo: { imager: { width: 1.1 }, monoMaker: { amount: 0.2 } },
151
+ loudness: { targetLufs: -14, ceilingDb: -1, truePeakOversample: 4 },
152
+ });
153
+ ```
154
+
155
+ Named mastering processors use the same names and behavior as the native,
156
+ Python, C, and CLI APIs:
157
+
158
+ ```typescript
159
+ import {
160
+ masteringPairAnalyze,
161
+ masteringPairProcess,
162
+ masteringPairProcessorNames,
163
+ masteringProcess,
164
+ masteringProcessStereo,
165
+ masteringProcessorNames,
166
+ masteringStereoAnalyze,
167
+ } from '@libraz/libsonare';
168
+
169
+ const names = masteringProcessorNames(); // e.g. "dynamics.compressor"
170
+ const compressed = masteringProcess('dynamics.compressor', samples, sampleRate, {
171
+ thresholdDb: -24,
172
+ ratio: 1.5,
173
+ });
174
+
175
+ const widened = masteringProcessStereo('stereo.imager', left, right, sampleRate, {
176
+ width: 1.1,
177
+ });
178
+
179
+ const pairNames = masteringPairProcessorNames(); // e.g. "match.abCrossfade"
180
+ const crossfaded = masteringPairProcess('match.abCrossfade', source, reference, sampleRate, {
181
+ mix: 0.25,
182
+ });
183
+
184
+ const loudnessJson = masteringPairAnalyze(
185
+ 'match.referenceLoudness',
186
+ source,
187
+ reference,
188
+ sampleRate,
189
+ );
190
+ const monoCompatJson = masteringStereoAnalyze(
191
+ 'stereo.monoCompatCheck',
192
+ left,
193
+ right,
194
+ sampleRate,
195
+ );
196
+ ```
197
+
198
+ ### Mastering presets
199
+
200
+ ```typescript
201
+ import { init, masterAudio, masteringPresetNames } from '@libraz/libsonare';
202
+
203
+ await init();
204
+
205
+ 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']
206
+
207
+ const result = masterAudio(samples, sampleRate, 'aiMusic', {
208
+ // optional flat overrides applied on top of the preset (dot notation)
209
+ 'loudness.targetLufs': -13,
210
+ });
211
+ console.log(result.outputLufs, result.appliedGainDb);
212
+ ```
213
+
214
+ ### Mixing
215
+
216
+ ```typescript
217
+ import { init, Mixer, mixStereo, mixingScenePresetJson } from '@libraz/libsonare';
218
+
219
+ await init();
220
+
221
+ const sceneJson = mixingScenePresetJson('vocalReverbSend');
222
+ const offline = mixStereo([vocalL, musicL], [vocalR, musicR], sampleRate, {
223
+ inputTrimDb: [3, 0],
224
+ faderDb: [-3, -12],
225
+ pan: [0, -0.2],
226
+ width: [1, 0.9],
227
+ });
228
+
229
+ const mixer = Mixer.fromSceneJson(sceneJson, sampleRate, 512);
230
+ const block = mixer.processStereo([vocalBlockL, returnBlockL], [vocalBlockR, returnBlockR]);
231
+ console.log(offline.meters[0].maxTruePeakDb, block.left.length);
232
+
233
+ const outL = new Float32Array(512);
234
+ const outR = new Float32Array(512);
235
+ mixer.processStereoInto([vocalBlockL, returnBlockL], [vocalBlockR, returnBlockR], outL, outR);
236
+
237
+ const realtime = mixer.createRealtimeBuffer();
238
+ realtime.leftInputs[0].set(vocalBlockL);
239
+ realtime.rightInputs[0].set(vocalBlockR);
240
+ realtime.leftInputs[1].set(returnBlockL);
241
+ realtime.rightInputs[1].set(returnBlockR);
242
+ realtime.process();
243
+ console.log(realtime.outLeft[0], realtime.outRight[0]);
244
+ mixer.delete();
245
+ ```
246
+
247
+ ### AudioWorklet bridge
248
+
249
+ The package exposes an optional worklet entry that uses the same `sonare.wasm`
250
+ as the offline API. The bridge processes fixed 128-sample render quanta and
251
+ treats each AudioWorklet input as one stereo mixer strip.
252
+
253
+ ```typescript
254
+ // worklet.ts, loaded with audioContext.audioWorklet.addModule(...)
255
+ import { init, mixingScenePresetJson } from '@libraz/libsonare';
256
+ import { registerSonareWorkletProcessor } from '@libraz/libsonare/worklet';
257
+
258
+ await init();
259
+ registerSonareWorkletProcessor();
260
+ ```
261
+
262
+ ```typescript
263
+ // main thread
264
+ import { mixingScenePresetJson } from '@libraz/libsonare';
265
+
266
+ await audioContext.audioWorklet.addModule('/worklet.js');
267
+ const sceneJson = mixingScenePresetJson('vocalReverbSend');
268
+ const node = new AudioWorkletNode(audioContext, 'sonare-worklet-processor', {
269
+ numberOfInputs: 2,
270
+ numberOfOutputs: 1,
271
+ outputChannelCount: [2],
272
+ processorOptions: {
273
+ sceneJson,
274
+ sampleRate: audioContext.sampleRate,
275
+ blockSize: 128,
276
+ spectrumIntervalFrames: 2048,
277
+ spectrumBands: 16,
278
+ },
279
+ });
280
+
281
+ node.port.postMessage({
282
+ type: 'scheduleInsertAutomation',
283
+ stripIndex: 0,
284
+ insertIndex: 0,
285
+ paramId: 0,
286
+ samplePos: 0,
287
+ value: 0,
288
+ curve: 'linear',
289
+ });
290
+
291
+ node.port.onmessage = (event) => {
292
+ if (event.data?.type === 'meter') {
293
+ console.log(event.data.peakDbL, event.data.rmsDbL, event.data.correlation);
294
+ } else if (event.data?.type === 'spectrum') {
295
+ console.log(event.data.frame, event.data.bands);
296
+ }
297
+ };
298
+ ```
299
+
300
+ For cross-origin-isolated pages, meters and spectrum snapshots can use optional
301
+ SharedArrayBuffer rings instead of per-message `postMessage`:
302
+
303
+ ```typescript
304
+ import {
305
+ createSonareMeterRingBuffer,
306
+ createSonareSpectrumRingBuffer,
307
+ readSonareMeterRingBuffer,
308
+ readSonareSpectrumRingBuffer,
309
+ } from '@libraz/libsonare/worklet';
310
+
311
+ const meterRing = createSonareMeterRingBuffer(128);
312
+ const spectrumRing = createSonareSpectrumRingBuffer(64, 16);
313
+ const node = new AudioWorkletNode(audioContext, 'sonare-worklet-processor', {
314
+ numberOfInputs: 2,
315
+ numberOfOutputs: 1,
316
+ outputChannelCount: [2],
317
+ processorOptions: {
318
+ sceneJson,
319
+ sampleRate: audioContext.sampleRate,
320
+ blockSize: 128,
321
+ meterSharedBuffer: meterRing.sharedBuffer,
322
+ spectrumIntervalFrames: 2048,
323
+ spectrumSharedBuffer: spectrumRing.sharedBuffer,
324
+ },
325
+ });
326
+
327
+ let nextMeterRead = 0;
328
+ let nextSpectrumRead = 0;
329
+ function readMeters() {
330
+ const result = readSonareMeterRingBuffer(meterRing, nextMeterRead);
331
+ nextMeterRead = result.nextReadIndex;
332
+ for (const meter of result.meters) {
333
+ console.log(meter.frame, meter.peakDbL, meter.rmsDbL);
334
+ }
335
+ const spectra = readSonareSpectrumRingBuffer(spectrumRing, nextSpectrumRead);
336
+ nextSpectrumRead = spectra.nextReadIndex;
337
+ for (const spectrum of spectra.spectra) {
338
+ console.log(spectrum.frame, spectrum.bands);
339
+ }
340
+ }
341
+ ```
342
+
343
+ ### Progress callback
344
+
345
+ `masteringChainWithProgress` (and its stereo variant) is `masteringChain` with
346
+ an extra `(progress, stage) => void` callback invoked after each enabled stage:
347
+
348
+ ```typescript
349
+ import { init, masteringChainWithProgress } from '@libraz/libsonare';
350
+
351
+ await init();
352
+
353
+ masteringChainWithProgress(
354
+ samples,
355
+ sampleRate,
356
+ { dynamics: { compressor: { thresholdDb: -24 } } },
357
+ (progress, stage) => console.log(`${stage}: ${(progress * 100).toFixed(0)}%`),
358
+ );
359
+ ```
360
+
361
+ ### Streaming mastering chain
362
+
363
+ `StreamingMasteringChain` processes blocks while maintaining per-stage state
364
+ across calls. It only supports modules whose state depends solely on the
365
+ sample rate — it cannot include `repair.denoise` or `loudness` (those require
366
+ offline / look-ahead analysis) and throws at construction if they are enabled.
367
+
368
+ ```typescript
369
+ import { init, StreamingMasteringChain } from '@libraz/libsonare';
370
+
371
+ await init();
372
+
373
+ const chain = new StreamingMasteringChain({
374
+ eq: { tiltDb: 0.5 },
375
+ dynamics: { compressor: { thresholdDb: -20 } },
376
+ });
377
+ chain.prepare(48000, 512, 2);
378
+
379
+ // Mono block
380
+ const monoBlock = new Float32Array(512);
381
+ const processedMono = chain.processMono(monoBlock);
382
+
383
+ // Stereo block (separate L/R Float32Arrays)
384
+ const left = new Float32Array(512);
385
+ const right = new Float32Array(512);
386
+ const { left: outL, right: outR } = chain.processStereo(left, right);
387
+
388
+ chain.reset();
389
+ chain.delete(); // release WASM memory
390
+ ```
391
+
97
392
  ## Features
98
393
 
99
394
  - **Detection**: BPM, key, beats, onsets, chords, sections
100
395
  - **Effects**: HPSS, time stretch, pitch shift, normalize, trim
396
+ - **Mastering**: EQ, compressor, tape/exciter, air band, stereo imaging,
397
+ true-peak limiting, loudness optimization
101
398
  - **Features**: STFT, mel spectrogram, MFCC, chroma, CQT/VQT, spectral features
102
399
  - **Pitch**: YIN, pYIN algorithms
103
400
  - **Streaming**: Real-time analysis with progressive estimates