@libraz/libsonare 1.1.0 → 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 +164 -2
- package/dist/index.d.ts +1122 -18
- package/dist/index.js +1124 -14
- 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 +14 -4
- package/src/index.ts +1895 -63
- package/src/public_types.ts +451 -0
- package/src/sonare-rt.d.ts +93 -0
- package/src/sonare.js.d.ts +710 -2
- package/src/stream_types.ts +35 -0
- package/src/wasm_types.ts +695 -2
- package/src/worklet.ts +2140 -0
package/README.md
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
[](https://github.com/libraz/libsonare/blob/main/LICENSE)
|
|
8
8
|
[](https://pypi.org/project/libsonare/)
|
|
9
9
|
|
|
10
|
-
|
|
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.
|
|
11
17
|
|
|
12
18
|
> **Audio input:** This package expects already-decoded `Float32Array` mono
|
|
13
19
|
> samples (it does not bundle a file decoder). Use the Web Audio API in the
|
|
@@ -34,10 +40,37 @@ const key = detectKey(samples, sampleRate);
|
|
|
34
40
|
const result = analyze(samples, sampleRate);
|
|
35
41
|
console.log(`BPM: ${result.bpm}, Key: ${result.key.name}`);
|
|
36
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
|
+
|
|
37
52
|
// Audio class API
|
|
38
53
|
const audio = Audio.fromBuffer(samples, sampleRate);
|
|
39
54
|
console.log(`BPM: ${audio.detectBpm()}`);
|
|
40
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);
|
|
41
74
|
```
|
|
42
75
|
|
|
43
76
|
### Decoding files in the browser
|
|
@@ -169,7 +202,7 @@ import { init, masterAudio, masteringPresetNames } from '@libraz/libsonare';
|
|
|
169
202
|
|
|
170
203
|
await init();
|
|
171
204
|
|
|
172
|
-
masteringPresetNames(); // ['pop', 'edm', 'acoustic', 'hipHop', 'aiMusic', 'speech']
|
|
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']
|
|
173
206
|
|
|
174
207
|
const result = masterAudio(samples, sampleRate, 'aiMusic', {
|
|
175
208
|
// optional flat overrides applied on top of the preset (dot notation)
|
|
@@ -178,6 +211,135 @@ const result = masterAudio(samples, sampleRate, 'aiMusic', {
|
|
|
178
211
|
console.log(result.outputLufs, result.appliedGainDb);
|
|
179
212
|
```
|
|
180
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
|
+
|
|
181
343
|
### Progress callback
|
|
182
344
|
|
|
183
345
|
`masteringChainWithProgress` (and its stereo variant) is `masteringChain` with
|