@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 +299 -2
- package/dist/index.d.ts +1649 -202
- package/dist/index.js +2101 -994
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +2 -0
- package/dist/sonare-rt.js +2 -0
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +447 -0
- package/dist/worklet.js +2078 -0
- package/dist/worklet.js.map +1 -0
- package/package.json +24 -27
- package/src/index.ts +3670 -0
- package/src/public_types.ts +852 -0
- package/src/sonare-rt.d.ts +93 -0
- package/src/sonare.js.d.ts +1332 -0
- package/src/stream_types.ts +133 -0
- package/src/wasm_types.ts +1248 -0
- package/src/worklet.ts +2140 -0
- package/README.npm.md +0 -114
- package/dist/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/libraz/libsonare/actions)
|
|
4
4
|
[](https://www.npmjs.com/package/@libraz/libsonare)
|
|
5
|
-
[](https://www.npmjs.com/package/@libraz/libsonare)
|
|
6
|
+
[](https://www.npmjs.com/package/@libraz/libsonare)
|
|
6
7
|
[](https://github.com/libraz/libsonare/blob/main/LICENSE)
|
|
8
|
+
[](https://pypi.org/project/libsonare/)
|
|
7
9
|
|
|
8
|
-
|
|
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
|