@libraz/libsonare 1.2.0 → 1.2.2

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/dist/worklet.js CHANGED
@@ -29,6 +29,21 @@ function panLawCode(panLaw) {
29
29
  return 0;
30
30
  }
31
31
  }
32
+ function panModeCode(panMode) {
33
+ if (typeof panMode === "number") {
34
+ return panMode;
35
+ }
36
+ switch (panMode) {
37
+ case "stereoPan":
38
+ case "stereo-pan":
39
+ return 1;
40
+ case "dualPan":
41
+ case "dual-pan":
42
+ return 2;
43
+ default:
44
+ return 0;
45
+ }
46
+ }
32
47
  function meterTapCode(tap) {
33
48
  return tap === "preFader" || tap === 0 ? 0 : 1;
34
49
  }
@@ -236,6 +251,141 @@ var RealtimeEngine = class {
236
251
  this.native.delete();
237
252
  }
238
253
  };
254
+ var RealtimeVoiceChanger = class {
255
+ constructor(config = "neutral-monitor") {
256
+ if (!module) {
257
+ throw new Error("Module not initialized. Call init() first.");
258
+ }
259
+ this.changer = module.createRealtimeVoiceChanger(config);
260
+ }
261
+ prepare(sampleRate, maxBlockSize = 128, channels = 1) {
262
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
263
+ }
264
+ reset() {
265
+ this.changer.reset();
266
+ }
267
+ setConfig(config) {
268
+ this.changer.setConfig(config);
269
+ }
270
+ configJson() {
271
+ return this.changer.configJson();
272
+ }
273
+ latencySamples() {
274
+ return this.changer.latencySamples();
275
+ }
276
+ processMono(samples) {
277
+ return this.changer.processMono(samples);
278
+ }
279
+ processMonoInto(samples, output) {
280
+ this.changer.processMonoInto(samples, output);
281
+ }
282
+ processInterleaved(samples, channels) {
283
+ return this.changer.processInterleaved(samples, channels);
284
+ }
285
+ processInterleavedInto(samples, channels, output) {
286
+ this.changer.processInterleavedInto(samples, channels, output);
287
+ }
288
+ /**
289
+ * Acquire a typed-memory view onto the WASM heap for mono input.
290
+ *
291
+ * Write your input samples into the returned `Float32Array` directly (e.g.
292
+ * via `input.set(source)`); no copy crosses the JS↔C++ bridge until
293
+ * {@link processPreparedMono} is called. The view is owned by this
294
+ * RealtimeVoiceChanger and becomes invalid after {@link delete}; it may
295
+ * also be invalidated if you later call this method with a larger
296
+ * `numSamples` value (the underlying buffer may be reallocated).
297
+ */
298
+ getMonoInputBuffer(numSamples) {
299
+ return this.changer.getMonoInputBuffer(numSamples);
300
+ }
301
+ /** Mono output view counterpart to {@link getMonoInputBuffer}. */
302
+ getMonoOutputBuffer(numSamples) {
303
+ return this.changer.getMonoOutputBuffer(numSamples);
304
+ }
305
+ /**
306
+ * Process the previously-acquired mono input buffer in place. The output
307
+ * appears in the buffer returned by {@link getMonoOutputBuffer}. No JS↔C++
308
+ * sample-level crossings happen on this call — it just hands control to
309
+ * the underlying DSP on already-on-heap data.
310
+ */
311
+ processPreparedMono(numSamples) {
312
+ this.changer.processPreparedMono(numSamples);
313
+ }
314
+ /** Interleaved input view (layout L0,R0,L1,R1,...). */
315
+ getInterleavedInputBuffer(numFrames, numChannels) {
316
+ return this.changer.getInterleavedInputBuffer(numFrames, numChannels);
317
+ }
318
+ /** Interleaved output view counterpart. */
319
+ getInterleavedOutputBuffer(numFrames, numChannels) {
320
+ return this.changer.getInterleavedOutputBuffer(numFrames, numChannels);
321
+ }
322
+ /**
323
+ * Process the previously-acquired interleaved buffer in place. Output
324
+ * appears in the buffer returned by {@link getInterleavedOutputBuffer}.
325
+ */
326
+ processPreparedInterleaved(numFrames, numChannels) {
327
+ this.changer.processPreparedInterleaved(numFrames, numChannels);
328
+ }
329
+ /**
330
+ * Planar-channel input/output view (one Float32Array per channel). Matches
331
+ * AudioWorklet's native layout; processing happens in place.
332
+ */
333
+ getPlanarChannelBuffer(channel, numFrames) {
334
+ return this.changer.getPlanarChannelBuffer(channel, numFrames);
335
+ }
336
+ /**
337
+ * Process the previously-acquired planar channel buffers in place. Each
338
+ * channel must have been obtained from {@link getPlanarChannelBuffer}
339
+ * with the same `numFrames`. Output replaces input in the same buffers.
340
+ */
341
+ processPreparedPlanar(numFrames) {
342
+ this.changer.processPreparedPlanar(numFrames);
343
+ }
344
+ /**
345
+ * Convenience factory for the mono zero-copy path: returns the input/output
346
+ * heap views plus a `process()` thunk wired to the same `numSamples`. The
347
+ * views are reused across calls and become invalid after {@link delete}.
348
+ */
349
+ createRealtimeMonoBuffer(numSamples) {
350
+ const input = this.getMonoInputBuffer(numSamples);
351
+ const output = this.getMonoOutputBuffer(numSamples);
352
+ return {
353
+ input,
354
+ output,
355
+ process: () => this.processPreparedMono(numSamples)
356
+ };
357
+ }
358
+ /** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
359
+ createRealtimeInterleavedBuffer(numFrames, numChannels) {
360
+ const input = this.getInterleavedInputBuffer(numFrames, numChannels);
361
+ const output = this.getInterleavedOutputBuffer(numFrames, numChannels);
362
+ return {
363
+ input,
364
+ output,
365
+ channels: numChannels,
366
+ process: () => this.processPreparedInterleaved(numFrames, numChannels)
367
+ };
368
+ }
369
+ /**
370
+ * Convenience factory for the planar zero-copy path. Acquires one
371
+ * heap-backed Float32Array per channel and returns a `process()` thunk
372
+ * wired to the same `numFrames`. Buffers are reused across calls and
373
+ * become invalid after {@link delete}.
374
+ */
375
+ createRealtimePlanarBuffer(numFrames, numChannels) {
376
+ const channels = [];
377
+ for (let ch = 0; ch < numChannels; ch++) {
378
+ channels.push(this.getPlanarChannelBuffer(ch, numFrames));
379
+ }
380
+ return {
381
+ channels,
382
+ process: () => this.processPreparedPlanar(numFrames)
383
+ };
384
+ }
385
+ delete() {
386
+ this.changer.delete();
387
+ }
388
+ };
239
389
  var Mixer = class _Mixer {
240
390
  constructor(mixer) {
241
391
  this.mixer = mixer;
@@ -379,6 +529,26 @@ var Mixer = class _Mixer {
379
529
  vcaGroupCount() {
380
530
  return this.mixer.vcaGroupCount();
381
531
  }
532
+ /** Set the strip's input trim in dB. */
533
+ setInputTrimDb(stripIndex, db) {
534
+ this.mixer.setInputTrimDb(stripIndex, db);
535
+ }
536
+ /** Set the strip's fader level in dB. */
537
+ setFaderDb(stripIndex, db) {
538
+ this.mixer.setFaderDb(stripIndex, db);
539
+ }
540
+ /** Set the strip's pan position. */
541
+ setPan(stripIndex, pan, panMode = 0) {
542
+ this.mixer.setPan(stripIndex, pan, panModeCode(panMode));
543
+ }
544
+ /** Set the strip's stereo width. */
545
+ setWidth(stripIndex, width) {
546
+ this.mixer.setWidth(stripIndex, width);
547
+ }
548
+ /** Set the strip's mute state. */
549
+ setMuted(stripIndex, muted) {
550
+ this.mixer.setMuted(stripIndex, muted);
551
+ }
382
552
  /**
383
553
  * Set a strip's solo state. Takes effect on the next process without a
384
554
  * graph recompile.
@@ -589,6 +759,12 @@ function isWorkletMessage(value) {
589
759
  function isEngineCommandRecord(value) {
590
760
  return isRecord(value) && typeof value.type === "number";
591
761
  }
762
+ function isRealtimeVoiceChangerMessage(value) {
763
+ if (!isRecord(value) || typeof value.type !== "string") {
764
+ return false;
765
+ }
766
+ return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
767
+ }
592
768
  function isEngineTelemetryRecord(value) {
593
769
  return isRecord(value) && typeof value.type === "number" && typeof value.error === "number" && typeof value.renderFrame === "number" && typeof value.timelineSample === "number" && typeof value.audibleTimelineSample === "number" && typeof value.graphLatencySamplesQ8 === "number" && typeof value.value === "number";
594
770
  }
@@ -1111,7 +1287,7 @@ var SonareWorkletProcessor = class {
1111
1287
  }
1112
1288
  }
1113
1289
  };
1114
- var SonareRealtimeEngineWorkletProcessor = class {
1290
+ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletProcessor {
1115
1291
  constructor(options = {}, transport) {
1116
1292
  this.closed = false;
1117
1293
  this.lastMeterFrame = Number.NEGATIVE_INFINITY;
@@ -1132,6 +1308,12 @@ var SonareRealtimeEngineWorkletProcessor = class {
1132
1308
  options.telemetryRingCapacity
1133
1309
  ) : void 0;
1134
1310
  this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
1311
+ this.channelScratch = new Array(this.channelCount);
1312
+ this.channelScratchViews = new Array(this.channelCount);
1313
+ for (let ch = 0; ch < this.channelCount; ch++) {
1314
+ this.channelScratch[ch] = new Float32Array(this.blockSize);
1315
+ this.channelScratchViews[ch] = this.channelScratch[ch];
1316
+ }
1135
1317
  }
1136
1318
  process(inputs, outputs) {
1137
1319
  if (this.closed) {
@@ -1151,17 +1333,29 @@ var SonareRealtimeEngineWorkletProcessor = class {
1151
1333
  return true;
1152
1334
  }
1153
1335
  this.drainCommands();
1154
- const channels = [];
1336
+ const scratchCapacity = this.channelScratch[0]?.length ?? 0;
1337
+ let usableFrames = frames;
1338
+ if (usableFrames > scratchCapacity) {
1339
+ if (!_SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow) {
1340
+ _SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow = true;
1341
+ console.warn(
1342
+ `SonareRealtimeEngineWorkletProcessor: requested ${usableFrames} frames exceeds pre-allocated capacity ${scratchCapacity}; clamping.`
1343
+ );
1344
+ }
1345
+ usableFrames = scratchCapacity;
1346
+ }
1155
1347
  const input = inputs[0];
1156
1348
  for (let ch = 0; ch < this.channelCount; ch++) {
1349
+ const scratch = this.channelScratch[ch];
1157
1350
  const source = input?.[ch];
1158
- const channel = new Float32Array(frames);
1159
- if (source && source.length === frames) {
1160
- channel.set(source);
1351
+ if (source && source.length === usableFrames) {
1352
+ scratch.set(source, 0);
1353
+ } else {
1354
+ scratch.fill(0, 0, usableFrames);
1161
1355
  }
1162
- channels.push(channel);
1356
+ this.channelScratchViews[ch] = scratch.subarray(0, usableFrames);
1163
1357
  }
1164
- const processed = this.engine.process(channels);
1358
+ const processed = this.engine.process(this.channelScratchViews);
1165
1359
  for (let ch = 0; ch < output.length; ch++) {
1166
1360
  const target = output[ch];
1167
1361
  const source = processed[ch] ?? processed[0];
@@ -1297,6 +1491,8 @@ var SonareRealtimeEngineWorkletProcessor = class {
1297
1491
  return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
1298
1492
  }
1299
1493
  };
1494
+ _SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow = false;
1495
+ var SonareRealtimeEngineWorkletProcessor = _SonareRealtimeEngineWorkletProcessor;
1300
1496
  var SonareRtRealtimeEngineRuntime = class {
1301
1497
  constructor(options) {
1302
1498
  this.closed = false;
@@ -1931,6 +2127,140 @@ var SonareEngine = class _SonareEngine {
1931
2127
  return Math.max(0, Math.round(ppq * 60 / 120 * this.sampleRate));
1932
2128
  }
1933
2129
  };
2130
+ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
2131
+ constructor(options = {}) {
2132
+ this.destroyed = false;
2133
+ this.sampleRate = options.sampleRate ?? 48e3;
2134
+ this.blockSize = options.blockSize ?? 128;
2135
+ this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 1));
2136
+ this.changer = new RealtimeVoiceChanger(options.preset ?? "neutral-monitor");
2137
+ this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
2138
+ this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
2139
+ this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
2140
+ this.planarChannels = [];
2141
+ if (this.channelCount > 1) {
2142
+ for (let ch = 0; ch < this.channelCount; ch++) {
2143
+ this.planarChannels.push(this.changer.getPlanarChannelBuffer(ch, this.blockSize));
2144
+ }
2145
+ }
2146
+ }
2147
+ /**
2148
+ * Handles a control-plane message from the main thread. Runs on the
2149
+ * AudioWorklet global scope but OUTSIDE of `process()` (i.e. outside the
2150
+ * realtime audio callback), so it is safe to perform JSON parsing and
2151
+ * DSP coefficient recomputation here. `setConfig` MUST NOT be deferred
2152
+ * into `process()` because that would block the audio thread for longer
2153
+ * than one render quantum (e.g. 128 samples / 44.1 kHz = ~2.9 ms).
2154
+ */
2155
+ receiveMessage(message) {
2156
+ if (this.destroyed) {
2157
+ return;
2158
+ }
2159
+ if (message.type === "setConfig") {
2160
+ this.changer.setConfig(message.preset);
2161
+ } else if (message.type === "reset") {
2162
+ this.changer.reset();
2163
+ } else if (message.type === "destroy") {
2164
+ this.destroy();
2165
+ }
2166
+ }
2167
+ process(inputs, outputs) {
2168
+ const output = outputs[0];
2169
+ if (this.destroyed || !output || output.length === 0) {
2170
+ return !this.destroyed;
2171
+ }
2172
+ const input = inputs[0];
2173
+ const requestedFrames = output[0]?.length ?? 0;
2174
+ const requestedChannels = Math.min(this.channelCount, output.length);
2175
+ if (requestedFrames === 0 || requestedChannels === 0) {
2176
+ return true;
2177
+ }
2178
+ if (requestedChannels === 1) {
2179
+ const frames2 = this.ensureMonoCapacity(requestedFrames);
2180
+ const source = input?.[0];
2181
+ if (source) {
2182
+ this.monoInput.set(source.subarray(0, frames2));
2183
+ } else {
2184
+ this.monoInput.fill(0, 0, frames2);
2185
+ }
2186
+ this.changer.processMonoInto(
2187
+ this.monoInput.subarray(0, frames2),
2188
+ this.monoOutput.subarray(0, frames2)
2189
+ );
2190
+ output[0].set(this.monoOutput.subarray(0, frames2));
2191
+ return true;
2192
+ }
2193
+ const frames = this.ensureInterleavedCapacity(requestedFrames, requestedChannels);
2194
+ const channels = requestedChannels;
2195
+ for (let ch = 0; ch < channels; ch++) {
2196
+ const src = input?.[ch];
2197
+ const dst = this.planarChannels[ch];
2198
+ if (!dst) {
2199
+ continue;
2200
+ }
2201
+ if (src) {
2202
+ dst.set(src.subarray(0, frames));
2203
+ } else {
2204
+ dst.fill(0, 0, frames);
2205
+ }
2206
+ }
2207
+ this.changer.processPreparedPlanar(frames);
2208
+ for (let ch = 0; ch < channels; ch++) {
2209
+ const src = this.planarChannels[ch];
2210
+ if (src) {
2211
+ output[ch].set(src.subarray(0, frames));
2212
+ }
2213
+ }
2214
+ return true;
2215
+ }
2216
+ destroy() {
2217
+ if (this.destroyed) {
2218
+ return;
2219
+ }
2220
+ this.destroyed = true;
2221
+ this.changer.delete();
2222
+ }
2223
+ /**
2224
+ * Returns the number of frames we can actually process given the
2225
+ * pre-allocated capacity. If the host requests more frames than the
2226
+ * worst-case block size declared at construction time, we clamp to the
2227
+ * available capacity and warn once — we MUST NOT reallocate on the
2228
+ * realtime audio thread.
2229
+ */
2230
+ ensureMonoCapacity(frames) {
2231
+ const capacity = this.monoInput.length;
2232
+ if (frames <= capacity) {
2233
+ return frames;
2234
+ }
2235
+ if (!_SonareRealtimeVoiceChangerWorkletProcessor.warnedMonoOverflow) {
2236
+ _SonareRealtimeVoiceChangerWorkletProcessor.warnedMonoOverflow = true;
2237
+ console.warn(
2238
+ `SonareRealtimeVoiceChangerWorkletProcessor: requested ${frames} mono frames exceeds pre-allocated capacity ${capacity}; clamping. Increase blockSize at construction time to avoid this.`
2239
+ );
2240
+ }
2241
+ return capacity;
2242
+ }
2243
+ /**
2244
+ * Same contract as ensureMonoCapacity but for the planar per-channel
2245
+ * scratch. Returns the number of frames that fit in the available capacity.
2246
+ */
2247
+ ensureInterleavedCapacity(frames, channels) {
2248
+ const capacity = this.planarChannels[0]?.length ?? 0;
2249
+ if (frames <= capacity) {
2250
+ return frames;
2251
+ }
2252
+ if (!_SonareRealtimeVoiceChangerWorkletProcessor.warnedInterleavedOverflow) {
2253
+ _SonareRealtimeVoiceChangerWorkletProcessor.warnedInterleavedOverflow = true;
2254
+ console.warn(
2255
+ `SonareRealtimeVoiceChangerWorkletProcessor: requested ${frames}x${channels} planar frames exceeds pre-allocated capacity ${capacity}; clamping. Increase blockSize or channelCount at construction time to avoid this.`
2256
+ );
2257
+ }
2258
+ return capacity;
2259
+ }
2260
+ };
2261
+ _SonareRealtimeVoiceChangerWorkletProcessor.warnedMonoOverflow = false;
2262
+ _SonareRealtimeVoiceChangerWorkletProcessor.warnedInterleavedOverflow = false;
2263
+ var SonareRealtimeVoiceChangerWorkletProcessor = _SonareRealtimeVoiceChangerWorkletProcessor;
1934
2264
  function registerSonareWorkletProcessor(name = "sonare-worklet-processor") {
1935
2265
  const scope = globalThis;
1936
2266
  if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
@@ -1962,6 +2292,35 @@ function registerSonareWorkletProcessor(name = "sonare-worklet-processor") {
1962
2292
  }
1963
2293
  scope.registerProcessor(name, RegisteredSonareWorkletProcessor);
1964
2294
  }
2295
+ function registerSonareRealtimeVoiceChangerWorkletProcessor(name = "sonare-realtime-voice-changer-processor") {
2296
+ const scope = globalThis;
2297
+ if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
2298
+ throw new Error("AudioWorkletProcessor is not available in this context.");
2299
+ }
2300
+ const Base = scope.AudioWorkletProcessor;
2301
+ class RegisteredSonareRealtimeVoiceChangerWorkletProcessor extends Base {
2302
+ constructor(options) {
2303
+ super();
2304
+ const port = this.port;
2305
+ this.bridge = new SonareRealtimeVoiceChangerWorkletProcessor(options?.processorOptions ?? {});
2306
+ const onMessage = (event) => {
2307
+ if (isRealtimeVoiceChangerMessage(event.data)) {
2308
+ this.bridge.receiveMessage(event.data);
2309
+ }
2310
+ };
2311
+ if (port?.addEventListener) {
2312
+ port.addEventListener("message", onMessage);
2313
+ port.start?.();
2314
+ } else if (port) {
2315
+ port.onmessage = onMessage;
2316
+ }
2317
+ }
2318
+ process(inputs, outputs) {
2319
+ return this.bridge.process(inputs, outputs);
2320
+ }
2321
+ }
2322
+ scope.registerProcessor(name, RegisteredSonareRealtimeVoiceChangerWorkletProcessor);
2323
+ }
1965
2324
  function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-engine-processor") {
1966
2325
  const scope = globalThis;
1967
2326
  if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
@@ -2011,13 +2370,14 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2011
2370
  if (!options.rtModuleUrl) {
2012
2371
  throw new Error("rtModuleUrl is required for sonare-rt AudioWorklet runtime.");
2013
2372
  }
2373
+ const rtModuleUrl = options.rtModuleUrl;
2014
2374
  const memory = new WebAssembly.Memory({ initial: 1024, maximum: 1024, shared: true });
2015
2375
  const globalFactory = globalThis.SonareRtModuleFactory;
2016
- const moduleFactory = globalFactory ? { default: globalFactory } : await import(options.rtModuleUrl);
2376
+ const moduleFactory = globalFactory ? { default: globalFactory } : await import(rtModuleUrl);
2017
2377
  const module2 = await moduleFactory.default({
2018
2378
  wasmMemory: memory,
2019
2379
  wasmBinary: options.rtWasmBinary,
2020
- locateFile: (path) => options.rtModuleUrl.replace(/[^/]*$/, path)
2380
+ locateFile: (path) => rtModuleUrl.replace(/[^/]*$/, path)
2021
2381
  });
2022
2382
  this.rtBridge = new SonareRtRealtimeEngineRuntime({
2023
2383
  module: module2,
@@ -2054,6 +2414,7 @@ export {
2054
2414
  SonareEngineTelemetryType,
2055
2415
  SonareRealtimeEngineNode,
2056
2416
  SonareRealtimeEngineWorkletProcessor,
2417
+ SonareRealtimeVoiceChangerWorkletProcessor,
2057
2418
  SonareRtRealtimeEngineRuntime,
2058
2419
  SonareWorkletProcessor,
2059
2420
  createSonareEngineCommandRingBuffer,
@@ -2068,6 +2429,7 @@ export {
2068
2429
  readSonareMeterRingBuffer,
2069
2430
  readSonareSpectrumRingBuffer,
2070
2431
  registerSonareRealtimeEngineWorkletProcessor,
2432
+ registerSonareRealtimeVoiceChangerWorkletProcessor,
2071
2433
  registerSonareWorkletProcessor,
2072
2434
  sonareEngineCommandRingBufferByteLength,
2073
2435
  sonareEngineTelemetryRingBufferByteLength,