@libraz/libsonare 1.7.0 → 1.7.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.
@@ -301,10 +301,12 @@ export class SonareRealtimeEngineNode {
301
301
  ? createSonareExternalMidiRingBuffer(options.externalMidiRingCapacity ?? 256)
302
302
  : undefined;
303
303
  const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
304
+ const cueOutput = options.cueOutput === true;
304
305
  const processorOptions: SonareRealtimeEngineWorkletProcessorOptions = {
305
306
  sampleRate: options.sampleRate ?? context.sampleRate,
306
307
  blockSize,
307
308
  channelCount,
309
+ cueOutput,
308
310
  commandSharedBuffer: commandRing?.sharedBuffer,
309
311
  commandRingCapacity: commandRing?.capacity,
310
312
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
@@ -329,8 +331,10 @@ export class SonareRealtimeEngineNode {
329
331
  new AudioWorkletNode(ctx, name, nodeOptions));
330
332
  const node = factory(context, processorName, {
331
333
  numberOfInputs: 1,
332
- numberOfOutputs: 1,
333
- outputChannelCount: [channelCount],
334
+ // The cue bus needs its own output; a single-output node keeps the
335
+ // historical mix where process() folds the cue into the program.
336
+ numberOfOutputs: cueOutput ? 2 : 1,
337
+ outputChannelCount: cueOutput ? [channelCount, channelCount] : [channelCount],
334
338
  processorOptions,
335
339
  });
336
340
  return new SonareRealtimeEngineNode(
@@ -343,6 +347,7 @@ export class SonareRealtimeEngineNode {
343
347
  audioWorklet,
344
348
  clipPageRequestsRealtimeSafe: mode === 'sab',
345
349
  externalMidiRealtimeSafe: mode === 'sab',
350
+ cueOutput,
346
351
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
347
352
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
348
353
  abiCompatible: detectedCapabilities?.abiCompatible,
@@ -46,6 +46,30 @@ import {
46
46
  writeSonareEngineTelemetryRingBuffer,
47
47
  } from './protocol';
48
48
 
49
+ /**
50
+ * Copies one plane per output channel, zero-filling the tail past `frames` and
51
+ * any channel the source does not cover. Shared by the program and cue outputs
52
+ * so the two cannot drift in their padding behaviour. Allocation-free.
53
+ */
54
+ function copyPlanesToOutput(
55
+ output: Float32Array[],
56
+ planes: readonly Float32Array[],
57
+ frames: number,
58
+ ): void {
59
+ for (let ch = 0; ch < output.length; ch++) {
60
+ const target = output[ch];
61
+ const source = planes[ch] ?? planes[0];
62
+ if (source) {
63
+ target.set(source.subarray(0, Math.min(target.length, frames)));
64
+ if (target.length > frames) {
65
+ target.fill(0, frames);
66
+ }
67
+ } else {
68
+ target.fill(0);
69
+ }
70
+ }
71
+ }
72
+
49
73
  function captureTransferList(channels: readonly Float32Array[]): Transferable[] {
50
74
  const transfers: ArrayBuffer[] = [];
51
75
  const seen = new Set<ArrayBuffer>();
@@ -94,6 +118,11 @@ export class SonareRealtimeEngineWorkletProcessor {
94
118
  // allocated per render quantum (the old engine.process() round-tripped fresh
95
119
  // arrays on both heaps every block, an RT-safety hazard).
96
120
  private channelBuffers: Float32Array[];
121
+ // Cue-bus plane, allocated only when the host asked for a separate PFL/AFL
122
+ // output. Empty otherwise, so a single-output host pays no heap and keeps the
123
+ // historical behaviour where process() folds the cue into the program mix.
124
+ private monitorBuffers: Float32Array[] = [];
125
+ private readonly cueOutput: boolean;
97
126
  private readonly liveClips = new Map<number, EngineClip>();
98
127
  private readonly pagedClipProviders = new Map<number, number>();
99
128
  private readonly pagedClipPageFrames = new Map<number, number>();
@@ -160,6 +189,14 @@ export class SonareRealtimeEngineWorkletProcessor {
160
189
  for (let ch = 0; ch < this.channelCount; ch++) {
161
190
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
162
191
  }
192
+ this.cueOutput = options.cueOutput === true;
193
+ if (this.cueOutput) {
194
+ this.engine.prepareMonitorChannels(this.channelCount, this.blockSize);
195
+ this.monitorBuffers = new Array(this.channelCount);
196
+ for (let ch = 0; ch < this.channelCount; ch++) {
197
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
198
+ }
199
+ }
163
200
  // Arm the engine's scope producer only when a scope ring was provided. The
164
201
  // band count follows the ring's record layout so writeScopeRing never
165
202
  // overruns its slot.
@@ -213,6 +250,9 @@ export class SonareRealtimeEngineWorkletProcessor {
213
250
  if ((this.channelBuffers[0]?.byteLength ?? 0) === 0) {
214
251
  this.reacquireChannelBuffers();
215
252
  }
253
+ if (this.cueOutput && (this.monitorBuffers[0]?.byteLength ?? 0) === 0) {
254
+ this.reacquireMonitorBuffers();
255
+ }
216
256
 
217
257
  const input = inputs[0];
218
258
  // Write the AudioWorklet input straight into the engine's WASM-heap views;
@@ -227,19 +267,20 @@ export class SonareRealtimeEngineWorkletProcessor {
227
267
  }
228
268
  }
229
269
 
230
- // Run the engine in place over the prepared scratch (allocation-free).
231
- this.engine.processPrepared(usableFrames);
270
+ // Run the engine in place over the prepared scratch (allocation-free). The
271
+ // monitor variant keeps the cue bus out of the program planes so it can go
272
+ // to its own output; the plain call folds it in, as it always has.
273
+ if (this.cueOutput) {
274
+ this.engine.processPreparedWithMonitor(usableFrames);
275
+ } else {
276
+ this.engine.processPrepared(usableFrames);
277
+ }
232
278
 
233
- for (let ch = 0; ch < output.length; ch++) {
234
- const target = output[ch];
235
- const source = this.channelBuffers[ch] ?? this.channelBuffers[0];
236
- if (source) {
237
- target.set(source.subarray(0, Math.min(target.length, usableFrames)));
238
- if (target.length > usableFrames) {
239
- target.fill(0, usableFrames);
240
- }
241
- } else {
242
- target.fill(0);
279
+ copyPlanesToOutput(output, this.channelBuffers, usableFrames);
280
+ if (this.cueOutput) {
281
+ const cue = outputs[1];
282
+ if (cue) {
283
+ copyPlanesToOutput(cue, this.monitorBuffers, usableFrames);
243
284
  }
244
285
  }
245
286
  this.publishClipPageRequests();
@@ -256,6 +297,12 @@ export class SonareRealtimeEngineWorkletProcessor {
256
297
  }
257
298
  }
258
299
 
300
+ private reacquireMonitorBuffers(): void {
301
+ for (let ch = 0; ch < this.channelCount; ch++) {
302
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
303
+ }
304
+ }
305
+
259
306
  receiveCommand(command: SonareEngineCommandRecord): void {
260
307
  if (!this.closed) {
261
308
  this.safeApplyCommand(command);
@@ -67,6 +67,13 @@ export interface SonareRealtimeEngineWorkletProcessorOptions {
67
67
  /** Lock-free worklet-to-main-thread MIDI-1 output ring. */
68
68
  externalMidiSharedBuffer?: SharedArrayBuffer;
69
69
  externalMidiRingCapacity?: number;
70
+ /**
71
+ * Route the PFL/AFL cue bus to the processor's SECOND output instead of
72
+ * folding it into the program output. Off by default, so an existing
73
+ * single-output host keeps its current mix sample for sample. The node must
74
+ * be constructed with two outputs for the cue to be audible.
75
+ */
76
+ cueOutput?: boolean;
70
77
  }
71
78
 
72
79
  export interface SonareRealtimeVoiceChangerWorkletProcessorOptions {
@@ -105,6 +112,11 @@ export interface SonareRealtimeEngineNodeCapabilities {
105
112
  clipPageRequestsRealtimeSafe: boolean;
106
113
  /** True when external MIDI uses the SAB output ring rather than postMessage. */
107
114
  externalMidiRealtimeSafe: boolean;
115
+ /**
116
+ * True when the node carries a second output fed by the PFL/AFL cue bus. When
117
+ * false the cue is folded into the program output, as it always was.
118
+ */
119
+ cueOutput: boolean;
108
120
  engineAbiVersion?: number;
109
121
  expectedEngineAbiVersion?: number;
110
122
  abiCompatible?: boolean;
package/src/worklet.ts CHANGED
@@ -10,6 +10,11 @@ export { attachOpfsClipStream } from './clip_page_streamer';
10
10
  // module singleton. Re-export the lifecycle so that realm can initialize its
11
11
  // own wasm instance, independent of the main-thread `index` module.
12
12
  export { init, isInitialized } from './index';
13
+ // Host-side mastering preview inside the worklet realm. Kept here rather than
14
+ // left main-thread-only so a live preview does not have to round-trip audio to
15
+ // the main thread; see the class doc for the prepare/loudness/latency contract.
16
+ export type { StreamingMasteringChainConfig } from './public_types';
17
+ export { StreamingMasteringChain } from './streaming_processors';
13
18
  export { SonareEngine } from './worklet/engine';
14
19
  export { SonareRealtimeEngineNode } from './worklet/engine-node';
15
20
  export type { SonareEngineOptions } from './worklet/engine-options';