@libraz/libsonare 1.6.0 → 1.7.1

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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis.d.ts +139 -11
  3. package/dist/analysis.js +162 -42
  4. package/dist/analysis.js.map +1 -1
  5. package/dist/index.d.ts +211 -39
  6. package/dist/index.js +501 -142
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/realtime-voice-changer-preset.schema.json +2 -2
  9. package/dist/sonare-analysis.js +1 -1
  10. package/dist/sonare-analysis.wasm +0 -0
  11. package/dist/sonare.js +1 -1
  12. package/dist/sonare.wasm +0 -0
  13. package/dist/worker.js +56 -29
  14. package/dist/worker.js.map +1 -1
  15. package/dist/worklet.d.ts +67 -18
  16. package/dist/worklet.js +193 -42
  17. package/dist/worklet.js.map +1 -1
  18. package/package.json +3 -3
  19. package/src/_chain_config.ts +6 -1
  20. package/src/analysis_helpers.ts +34 -29
  21. package/src/audio.ts +7 -2
  22. package/src/codes.ts +47 -11
  23. package/src/effects_mastering.ts +5 -0
  24. package/src/effects_transform.ts +202 -22
  25. package/src/feature_music.ts +8 -1
  26. package/src/feature_spectral.ts +64 -2
  27. package/src/feature_spectrogram.ts +44 -3
  28. package/src/index.ts +28 -3
  29. package/src/mastering_core.ts +70 -0
  30. package/src/metering.ts +18 -0
  31. package/src/mixer.ts +3 -3
  32. package/src/project.ts +6 -0
  33. package/src/project_class.ts +143 -16
  34. package/src/project_internal.ts +14 -5
  35. package/src/project_types.ts +50 -9
  36. package/src/public_types_mastering.ts +9 -0
  37. package/src/public_types_mixing.ts +16 -0
  38. package/src/public_types_realtime.ts +23 -5
  39. package/src/public_types_spectral.ts +16 -2
  40. package/src/quick_analysis.ts +21 -3
  41. package/src/realtime_engine.ts +14 -3
  42. package/src/sonare.js.d.ts +87 -0
  43. package/src/stream_analyzer.ts +6 -1
  44. package/src/web_midi.ts +10 -7
  45. package/src/worklet/engine-node.ts +54 -19
  46. package/src/worklet/engine-parameter-facade.ts +30 -1
  47. package/src/worklet/engine-processor.ts +52 -20
  48. package/src/worklet/engine-register.ts +1 -1
  49. package/src/worklet/engine-strips.ts +2 -2
  50. package/src/worklet/engine.ts +10 -0
  51. package/src/worklet/guards.ts +99 -7
  52. package/src/worklet/messages.ts +41 -1
  53. package/src/worklet/protocol.ts +4 -0
@@ -699,6 +699,10 @@ export interface WasmProjectSource {
699
699
  storageHandleId: number;
700
700
  sampleRateHint: number;
701
701
  nameOrUri: string;
702
+ /** Owning content hash for audio sources; empty for MIDI sources. */
703
+ contentHash: string;
704
+ /** External source-separation role for audio sources; empty for MIDI sources. */
705
+ externalStemRole: string;
702
706
  }
703
707
 
704
708
  export interface WasmEngineMetronomeConfig {
@@ -914,6 +918,7 @@ export interface WasmRealtimeEngine {
914
918
  setParameterSmoothed: (paramId: number, value: number, renderFrame: number) => void;
915
919
  setParamSmoothingMs: (smoothingMs: number) => void;
916
920
  setSoloMute: (laneIndex: number, solo: boolean, mute: boolean, renderFrame: number) => void;
921
+ setTrackMonitorMode: (laneIndex: number, mode: number, renderFrame: number) => void;
917
922
  getTransportState: () => WasmEngineTransportState;
918
923
  play: (renderFrame: number) => void;
919
924
  stop: (renderFrame: number) => void;
@@ -1234,6 +1239,12 @@ export interface SonareModule {
1234
1239
  sampleRate: number,
1235
1240
  nOctaveBands: number,
1236
1241
  ) => WasmAcousticResult;
1242
+ analyzeImpulseResponseEx: (
1243
+ samples: Float32Array,
1244
+ sampleRate: number,
1245
+ nOctaveBands: number,
1246
+ minDecayDb: number,
1247
+ ) => WasmAcousticResult;
1237
1248
  detectAcoustic: (
1238
1249
  samples: Float32Array,
1239
1250
  sampleRate: number,
@@ -1317,6 +1328,11 @@ export interface SonareModule {
1317
1328
  hopLength: number,
1318
1329
  ) => number;
1319
1330
  meteringCrestFactorDb: (samples: Float32Array, sampleRate: number) => number;
1331
+ meteringCrestFactorDbStereo: (
1332
+ left: Float32Array,
1333
+ right: Float32Array,
1334
+ sampleRate: number,
1335
+ ) => number;
1320
1336
  meteringDcOffset: (samples: Float32Array, sampleRate: number) => number;
1321
1337
  meteringTruePeakDb: (
1322
1338
  samples: Float32Array,
@@ -1473,12 +1489,30 @@ export interface SonareModule {
1473
1489
  kernelHarmonic: number,
1474
1490
  kernelPercussive: number,
1475
1491
  ) => WasmHpssResult;
1492
+ hpssEx: (
1493
+ samples: Float32Array,
1494
+ sampleRate: number,
1495
+ kernelHarmonic: number,
1496
+ kernelPercussive: number,
1497
+ nFft: number,
1498
+ hopLength: number,
1499
+ hardMask: boolean,
1500
+ ) => WasmHpssResult;
1476
1501
  hpssWithResidual: (
1477
1502
  samples: Float32Array,
1478
1503
  sampleRate: number,
1479
1504
  kernelHarmonic: number,
1480
1505
  kernelPercussive: number,
1481
1506
  ) => WasmHpssWithResidualResult;
1507
+ hpssWithResidualEx: (
1508
+ samples: Float32Array,
1509
+ sampleRate: number,
1510
+ kernelHarmonic: number,
1511
+ kernelPercussive: number,
1512
+ nFft: number,
1513
+ hopLength: number,
1514
+ hardMask: boolean,
1515
+ ) => WasmHpssWithResidualResult;
1482
1516
  decompose: (
1483
1517
  s: Float32Array,
1484
1518
  nFeatures: number,
@@ -1520,7 +1554,21 @@ export interface SonareModule {
1520
1554
  harmonic: (samples: Float32Array, sampleRate: number) => Float32Array;
1521
1555
  percussive: (samples: Float32Array, sampleRate: number) => Float32Array;
1522
1556
  timeStretch: (samples: Float32Array, sampleRate: number, rate: number) => Float32Array;
1557
+ timeStretchEx: (
1558
+ samples: Float32Array,
1559
+ sampleRate: number,
1560
+ rate: number,
1561
+ nFft: number,
1562
+ hopLength: number,
1563
+ ) => Float32Array;
1523
1564
  pitchShift: (samples: Float32Array, sampleRate: number, semitones: number) => Float32Array;
1565
+ pitchShiftEx: (
1566
+ samples: Float32Array,
1567
+ sampleRate: number,
1568
+ semitones: number,
1569
+ nFft: number,
1570
+ hopLength: number,
1571
+ ) => Float32Array;
1524
1572
  pitchCorrectToMidi: (
1525
1573
  samples: Float32Array,
1526
1574
  sampleRate: number,
@@ -1582,6 +1630,12 @@ export interface SonareModule {
1582
1630
  channels: number,
1583
1631
  ) => Float32Array;
1584
1632
  normalize: (samples: Float32Array, sampleRate: number, targetDb: number) => Float32Array;
1633
+ normalizeEx: (
1634
+ samples: Float32Array,
1635
+ sampleRate: number,
1636
+ targetDb: number,
1637
+ mode: string,
1638
+ ) => Float32Array;
1585
1639
  mastering: (
1586
1640
  samples: Float32Array,
1587
1641
  sampleRate: number,
@@ -1644,6 +1698,24 @@ export interface SonareModule {
1644
1698
  sampleRate: number,
1645
1699
  platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
1646
1700
  ) => string;
1701
+ masteringAssistantSuggestStereo: (
1702
+ left: Float32Array,
1703
+ right: Float32Array,
1704
+ sampleRate: number,
1705
+ params: Record<string, number | boolean>,
1706
+ ) => string;
1707
+ masteringAudioProfileStereo: (
1708
+ left: Float32Array,
1709
+ right: Float32Array,
1710
+ sampleRate: number,
1711
+ params: Record<string, number | boolean>,
1712
+ ) => string;
1713
+ masteringStreamingPreviewStereo: (
1714
+ left: Float32Array,
1715
+ right: Float32Array,
1716
+ sampleRate: number,
1717
+ platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
1718
+ ) => string;
1647
1719
  masteringRepairDeclick: (
1648
1720
  samples: Float32Array,
1649
1721
  sampleRate: number,
@@ -1760,6 +1832,13 @@ export interface SonareModule {
1760
1832
  options: Record<string, unknown>,
1761
1833
  ) => WasmMixResult;
1762
1834
  trim: (samples: Float32Array, sampleRate: number, thresholdDb: number) => Float32Array;
1835
+ trimEx: (
1836
+ samples: Float32Array,
1837
+ sampleRate: number,
1838
+ thresholdDb: number,
1839
+ frameLength: number,
1840
+ hopLength: number,
1841
+ ) => Float32Array;
1763
1842
  spectralEdit: (
1764
1843
  samples: Float32Array,
1765
1844
  sampleRate: number,
@@ -2190,6 +2269,14 @@ export interface SonareModule {
2190
2269
  stftBlendWeight: number,
2191
2270
  stftBlendNFft: number,
2192
2271
  ) => WasmNnlsChromaResult;
2272
+ nnlsChromaEx: (
2273
+ samples: Float32Array,
2274
+ sampleRate: number,
2275
+ enableStftBlend: boolean,
2276
+ stftBlendWeight: number,
2277
+ stftBlendNFft: number,
2278
+ hopLength: number,
2279
+ ) => WasmNnlsChromaResult;
2193
2280
  cqt: (
2194
2281
  samples: Float32Array,
2195
2282
  sampleRate: number,
@@ -1,3 +1,4 @@
1
+ import { resolveOrdinalInRange } from './codes';
1
2
  import { getSonareModule } from './module_state';
2
3
  import type { ChordQuality, PitchClass } from './public_types';
3
4
  import type { WasmStreamAnalyzer } from './sonare.js';
@@ -73,6 +74,10 @@ export class StreamAnalyzer {
73
74
  ) {
74
75
  throw new TypeError('outputFormat must be the integer 0 (Float32)');
75
76
  }
77
+ const window =
78
+ config.window === undefined
79
+ ? undefined
80
+ : resolveOrdinalInRange(config.window, 0, 3, 'stream analyzer window');
76
81
  const module = getSonareModule();
77
82
  const defaults = streamAnalyzerConfigDefaults();
78
83
  this.analyzer = new module.StreamAnalyzer(
@@ -94,7 +99,7 @@ export class StreamAnalyzer {
94
99
  config.maxProgressionEntries ?? defaults.maxProgressionEntries,
95
100
  config.keyUpdateIntervalSec ?? defaults.keyUpdateIntervalSec,
96
101
  config.bpmUpdateIntervalSec ?? defaults.bpmUpdateIntervalSec,
97
- config.window ?? defaults.window,
102
+ window ?? defaults.window,
98
103
  config.outputFormat ?? defaults.outputFormat,
99
104
  );
100
105
  }
package/src/web_midi.ts CHANGED
@@ -214,6 +214,10 @@ export async function bindWebMidi(
214
214
  bound.delete(input.id);
215
215
  };
216
216
 
217
+ // Does not call notify() itself: both call sites below (the initial sync
218
+ // and stateListener's fallback branch) already notify exactly once after
219
+ // calling this, so notifying here too would fire onInputsChanged twice per
220
+ // hotplug event that goes through the fallback branch.
217
221
  const refreshInputs = () => {
218
222
  for (const [, entry] of bound) {
219
223
  if (!shouldBind(entry.input)) {
@@ -223,7 +227,6 @@ export async function bindWebMidi(
223
227
  for (const [, input] of iterInputs(access)) {
224
228
  bindInput(input);
225
229
  }
226
- notify();
227
230
  };
228
231
 
229
232
  const stateListener = (event: MidiConnectionEventLike) => {
@@ -244,6 +247,7 @@ export async function bindWebMidi(
244
247
  };
245
248
 
246
249
  refreshInputs();
250
+ notify();
247
251
  if (access.addEventListener) {
248
252
  access.addEventListener('statechange', stateListener);
249
253
  } else {
@@ -368,12 +372,11 @@ function dispatchUmpMessage(
368
372
  if (status === 0x8) {
369
373
  engine.pushMidiInputNoteOff(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
370
374
  } else if (status === 0x9) {
371
- const velocity = (word1 >>> 25) & 0x7f;
372
- if (velocity === 0) {
373
- engine.pushMidiInputNoteOff(group, channel, data1, 0, portTimeSamples);
374
- } else {
375
- engine.pushMidiInputNoteOn(group, channel, data1, velocity, portTimeSamples);
376
- }
375
+ // MIDI 2.0 note-on is distinct from note-off (status 0x8). Downsample
376
+ // its 16-bit velocity to the engine's 7-bit API, retaining a nonzero
377
+ // note-on even when the top seven bits are all zero.
378
+ const velocity = Math.max(1, (word1 >>> 25) & 0x7f);
379
+ engine.pushMidiInputNoteOn(group, channel, data1, velocity, portTimeSamples);
377
380
  } else if (status === 0xb) {
378
381
  engine.pushMidiInputCc(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
379
382
  }
@@ -1,7 +1,9 @@
1
1
  import type { EngineCaptureStatus, EngineTransportState } from '../index';
2
2
  import { engineCapabilities } from '../index';
3
3
  import {
4
+ engineCaptureResponseRequestId,
4
5
  isClipPageRequestMessage,
6
+ isEngineCaptureResponseForOperation,
5
7
  isEngineCaptureResponseMessage,
6
8
  isEngineTelemetryRecord,
7
9
  isEngineTransportResponseMessage,
@@ -10,7 +12,7 @@ import {
10
12
  } from './guards';
11
13
  import type {
12
14
  SonareEngineCaptureRequestMessage,
13
- SonareEngineCaptureResponseMessage,
15
+ SonareEngineCaptureResponseMessageInternal,
14
16
  SonareEngineClipPageRequestMessage,
15
17
  SonareEngineSyncErrorMessage,
16
18
  SonareEngineTransportResponseMessage,
@@ -53,12 +55,28 @@ function isFiniteInteger(value: number | bigint | undefined): boolean {
53
55
  return typeof value === 'bigint' || (Number.isFinite(value) && Number.isSafeInteger(value));
54
56
  }
55
57
 
58
+ function isTrackMonitorMode(value: number | bigint | undefined): boolean {
59
+ if (value === undefined) {
60
+ return false;
61
+ }
62
+ const mode = typeof value === 'bigint' ? Number(value) : value;
63
+ return typeof mode === 'number' && Number.isSafeInteger(mode) && mode >= 0 && mode <= 2;
64
+ }
65
+
66
+ /** A track-monitor command cannot infer a lane: its target is a uint32 index. */
67
+ function isTrackMonitorLaneIndex(value: number | undefined): boolean {
68
+ return (
69
+ typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 0xffff_ffff
70
+ );
71
+ }
72
+
56
73
  function isValidCommandRecord(command: SonareEngineCommandRecord): boolean {
57
74
  const type = Number(command.type);
58
75
  if (
59
76
  !Number.isSafeInteger(type) ||
60
77
  type < SonareEngineCommandType.SetParam ||
61
- type > SonareEngineCommandType.SeekMarker
78
+ (type > SonareEngineCommandType.SeekMarker &&
79
+ type !== SonareEngineCommandType.SetTrackMonitorMode)
62
80
  ) {
63
81
  return false;
64
82
  }
@@ -66,7 +84,9 @@ function isValidCommandRecord(command: SonareEngineCommandRecord): boolean {
66
84
  (command.targetId === undefined || Number.isSafeInteger(command.targetId)) &&
67
85
  (command.argFloat === undefined || Number.isFinite(command.argFloat)) &&
68
86
  isFiniteInteger(command.argInt) &&
69
- isFiniteInteger(command.sampleTime)
87
+ isFiniteInteger(command.sampleTime) &&
88
+ (type !== SonareEngineCommandType.SetTrackMonitorMode ||
89
+ (isTrackMonitorLaneIndex(command.targetId) && isTrackMonitorMode(command.argInt)))
70
90
  );
71
91
  }
72
92
 
@@ -109,7 +129,8 @@ export class SonareRealtimeEngineNode {
109
129
  private readonly captureRequests = new Map<
110
130
  number,
111
131
  {
112
- resolve: (response: SonareEngineCaptureResponseMessage) => void;
132
+ op: SonareEngineCaptureRequestMessage['op'];
133
+ resolve: (response: SonareEngineCaptureResponseMessageInternal) => void;
113
134
  reject: (reason?: unknown) => void;
114
135
  }
115
136
  >();
@@ -151,14 +172,19 @@ export class SonareRealtimeEngineNode {
151
172
  this.resolveReady();
152
173
  }
153
174
  this.node.port.onmessage = (event: MessageEvent<unknown>) => {
154
- if (isEngineCaptureResponseMessage(event.data)) {
155
- const pending = this.captureRequests.get(event.data.requestId);
175
+ const captureRequestId = engineCaptureResponseRequestId(event.data);
176
+ if (captureRequestId !== undefined) {
177
+ const pending = this.captureRequests.get(captureRequestId);
156
178
  if (pending) {
157
- this.captureRequests.delete(event.data.requestId);
158
- if (event.data.ok) {
179
+ this.captureRequests.delete(captureRequestId);
180
+ if (!isEngineCaptureResponseMessage(event.data)) {
181
+ pending.reject(new Error('Malformed capture response.'));
182
+ } else if (!isEngineCaptureResponseForOperation(event.data, pending.op)) {
183
+ pending.reject(new Error('Capture response does not match request operation.'));
184
+ } else if (event.data.ok) {
159
185
  pending.resolve(event.data);
160
186
  } else {
161
- pending.reject(new Error(event.data.error ?? 'Capture request failed'));
187
+ pending.reject(new Error(event.data.error));
162
188
  }
163
189
  }
164
190
  } else if (isEngineTransportResponseMessage(event.data)) {
@@ -375,7 +401,7 @@ export class SonareRealtimeEngineNode {
375
401
 
376
402
  requestCaptureStatus(): Promise<EngineCaptureStatus> {
377
403
  return this.sendCaptureRequest('status').then((response) => {
378
- if (!response.status) {
404
+ if (!response.ok || !('status' in response)) {
379
405
  throw new Error('Capture status response is missing status.');
380
406
  }
381
407
  return response.status;
@@ -383,11 +409,12 @@ export class SonareRealtimeEngineNode {
383
409
  }
384
410
 
385
411
  requestCapturedAudio(): Promise<Float32Array[]> {
386
- return this.sendCaptureRequest('read').then((response) =>
387
- (response.channels ?? []).map((channel) =>
388
- channel instanceof Float32Array ? channel : new Float32Array(channel),
389
- ),
390
- );
412
+ return this.sendCaptureRequest('read').then((response) => {
413
+ if (!response.ok || !('channels' in response)) {
414
+ throw new Error('Capture read response is missing channels.');
415
+ }
416
+ return response.channels;
417
+ });
391
418
  }
392
419
 
393
420
  requestCaptureReset(): Promise<void> {
@@ -529,6 +556,7 @@ export class SonareRealtimeEngineNode {
529
556
  this.startRingPolling();
530
557
  return () => {
531
558
  this.midiOutListeners.delete(callback);
559
+ this.stopRingPollingIfUnused();
532
560
  };
533
561
  }
534
562
 
@@ -633,15 +661,22 @@ export class SonareRealtimeEngineNode {
633
661
 
634
662
  private sendCaptureRequest(
635
663
  op: SonareEngineCaptureRequestMessage['op'],
636
- ): Promise<SonareEngineCaptureResponseMessage> {
664
+ ): Promise<SonareEngineCaptureResponseMessageInternal> {
637
665
  if (this.destroyed) {
638
666
  return Promise.reject(new Error('Realtime engine node is destroyed.'));
639
667
  }
640
668
  const requestId = this.captureRequestId++;
641
- const promise = new Promise<SonareEngineCaptureResponseMessage>((resolve, reject) => {
642
- this.captureRequests.set(requestId, { resolve, reject });
669
+ let rejectRequest!: (reason?: unknown) => void;
670
+ const promise = new Promise<SonareEngineCaptureResponseMessageInternal>((resolve, reject) => {
671
+ rejectRequest = reject;
672
+ this.captureRequests.set(requestId, { op, resolve, reject });
643
673
  });
644
- this.node.port.postMessage({ type: 'captureRequest', requestId, op });
674
+ try {
675
+ this.node.port.postMessage({ type: 'captureRequest', requestId, op });
676
+ } catch (error) {
677
+ this.captureRequests.delete(requestId);
678
+ rejectRequest(error);
679
+ }
645
680
  return promise;
646
681
  }
647
682
 
@@ -1,4 +1,10 @@
1
- import type { EngineAutomationPoint, EngineParameterInfo, RealtimeEngine } from '../index';
1
+ import { trackMonitorModeCode } from '../codes';
2
+ import type {
3
+ EngineAutomationPoint,
4
+ EngineParameterInfo,
5
+ EngineTrackMonitorMode,
6
+ RealtimeEngine,
7
+ } from '../index';
2
8
  import type { SonareEngineSyncMessage } from './messages';
3
9
  import {
4
10
  ENGINE_MIXER_PARAM_FADER_DB,
@@ -67,6 +73,29 @@ export function setSoloMute(
67
73
  });
68
74
  }
69
75
 
76
+ export function setTrackMonitorMode(
77
+ ctx: EngineParameterContext,
78
+ target: string | number,
79
+ mode: EngineTrackMonitorMode,
80
+ renderFrame = -1,
81
+ ): boolean {
82
+ // Resolve the mode before declaring a lane or touching either engine. This
83
+ // keeps booleans, fractions, and unknown spellings from reaching the queue.
84
+ const modeOrdinal = trackMonitorModeCode(mode);
85
+ const laneIndex = ctx.ensureTrackLane(target);
86
+ ctx.offlineEngine.setTrackMonitorMode(
87
+ laneIndex,
88
+ modeOrdinal as EngineTrackMonitorMode,
89
+ renderFrame,
90
+ );
91
+ return ctx.sendCommand({
92
+ type: SonareEngineCommandType.SetTrackMonitorMode,
93
+ targetId: laneIndex,
94
+ sampleTime: renderFrame,
95
+ argInt: modeOrdinal,
96
+ });
97
+ }
98
+
70
99
  /**
71
100
  * Returns the automation target id for a mixer strip parameter.
72
101
  *
@@ -6,7 +6,7 @@ import {
6
6
  type ResolvedMetronomeConfig,
7
7
  resolveMetronomeConfig,
8
8
  type SonareEngineCaptureRequestMessage,
9
- type SonareEngineCaptureResponseMessage,
9
+ type SonareEngineCaptureResponseMessageInternal,
10
10
  type SonareEngineSyncMessage,
11
11
  type SonareEngineTransportRequestMessage,
12
12
  type SonareEngineTransportResponseMessage,
@@ -46,6 +46,22 @@ import {
46
46
  writeSonareEngineTelemetryRingBuffer,
47
47
  } from './protocol';
48
48
 
49
+ function captureTransferList(channels: readonly Float32Array[]): Transferable[] {
50
+ const transfers: ArrayBuffer[] = [];
51
+ const seen = new Set<ArrayBuffer>();
52
+ for (const channel of channels) {
53
+ const buffer = channel.buffer;
54
+ if (!(buffer instanceof ArrayBuffer)) {
55
+ throw new TypeError('capture response channels must use plain ArrayBuffers');
56
+ }
57
+ if (!seen.has(buffer)) {
58
+ seen.add(buffer);
59
+ transfers.push(buffer);
60
+ }
61
+ }
62
+ return transfers;
63
+ }
64
+
49
65
  /**
50
66
  * AudioWorklet-style bridge for the DAW realtime engine facade.
51
67
  *
@@ -599,26 +615,24 @@ export class SonareRealtimeEngineWorkletProcessor {
599
615
  source: status.source,
600
616
  recordOffsetSamples: status.recordOffsetSamples,
601
617
  },
602
- } satisfies SonareEngineCaptureResponseMessage);
618
+ } satisfies SonareEngineCaptureResponseMessageInternal);
603
619
  return;
604
620
  }
605
621
  if (message.op === 'read') {
606
- const captured = this.engine.capturedAudio();
607
- const channels: number[][] = [];
608
- for (let ch = 0; ch < captured.length; ch++) {
609
- const source = captured[ch];
610
- const copy: number[] = [];
611
- for (let i = 0; i < source.length; i++) {
612
- copy.push(Number(source[i]));
613
- }
614
- channels.push(copy);
615
- }
616
- this.transport?.postMessage?.({
617
- type: 'captureResponse',
618
- requestId: message.requestId,
619
- ok: true,
620
- channels,
621
- } satisfies SonareEngineCaptureResponseMessage);
622
+ // Embind's outer val::array is array-like but not structured-cloneable
623
+ // in every host. Copy only that tiny container; keep each native
624
+ // Float32Array intact so the channel payload remains transferable.
625
+ const channels = Array.from(this.engine.capturedAudio());
626
+ const transfer = captureTransferList(channels);
627
+ this.transport?.postMessage?.(
628
+ {
629
+ type: 'captureResponse',
630
+ requestId: message.requestId,
631
+ ok: true,
632
+ channels,
633
+ } satisfies SonareEngineCaptureResponseMessageInternal,
634
+ transfer,
635
+ );
622
636
  return;
623
637
  }
624
638
  this.engine.resetCapture();
@@ -626,14 +640,14 @@ export class SonareRealtimeEngineWorkletProcessor {
626
640
  type: 'captureResponse',
627
641
  requestId: message.requestId,
628
642
  ok: true,
629
- } satisfies SonareEngineCaptureResponseMessage);
643
+ } satisfies SonareEngineCaptureResponseMessageInternal);
630
644
  } catch (error) {
631
645
  this.transport?.postMessage?.({
632
646
  type: 'captureResponse',
633
647
  requestId: message.requestId,
634
648
  ok: false,
635
649
  error: error instanceof Error ? error.message : String(error),
636
- } satisfies SonareEngineCaptureResponseMessage);
650
+ } satisfies SonareEngineCaptureResponseMessageInternal);
637
651
  }
638
652
  }
639
653
 
@@ -773,6 +787,24 @@ export class SonareRealtimeEngineWorkletProcessor {
773
787
  sampleTime,
774
788
  );
775
789
  break;
790
+ case SonareEngineCommandType.SetTrackMonitorMode: {
791
+ const rawMode = command.argInt;
792
+ const mode = typeof rawMode === 'bigint' ? Number(rawMode) : rawMode;
793
+ if (typeof mode !== 'number' || !Number.isSafeInteger(mode) || mode < 0 || mode > 2) {
794
+ throw new RangeError(`Invalid track monitor mode: ${String(rawMode)}`);
795
+ }
796
+ const laneIndex = command.targetId;
797
+ if (
798
+ typeof laneIndex !== 'number' ||
799
+ !Number.isSafeInteger(laneIndex) ||
800
+ laneIndex < 0 ||
801
+ laneIndex > 0xffff_ffff
802
+ ) {
803
+ throw new RangeError(`Invalid track monitor lane index: ${String(laneIndex)}`);
804
+ }
805
+ this.engine.setTrackMonitorMode(laneIndex, mode as 0 | 1 | 2, sampleTime);
806
+ break;
807
+ }
776
808
  default:
777
809
  this.publishTelemetryRecord({
778
810
  type: SonareEngineTelemetryType.Error,
@@ -121,7 +121,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
121
121
  throw new Error('AudioWorklet initialization message buffer overflowed.');
122
122
  }
123
123
  this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
124
- postMessage: (message) => port?.postMessage?.(message),
124
+ postMessage: (message, transfer) => port?.postMessage?.(message, transfer),
125
125
  onMeter: (meter) => port?.postMessage?.(meter),
126
126
  });
127
127
  for (const message of options.initialSyncMessages ?? []) {
@@ -1,5 +1,5 @@
1
1
  import { panLawCode, panModeCode } from '../codes';
2
- import type { EqBand, PanLaw, PanMode, RealtimeEngine } from '../index';
2
+ import type { EqBand, PanLawInput, PanMode, RealtimeEngine } from '../index';
3
3
  import type { SonareEngineInstrumentSyncMessage, SonareEngineSyncMessage } from './messages';
4
4
 
5
5
  /**
@@ -85,7 +85,7 @@ export function setTrackStripPan(
85
85
  export function setTrackStripPanLaw(
86
86
  ctx: EngineStripContext,
87
87
  target: string | number,
88
- panLaw: PanLaw | number,
88
+ panLaw: PanLawInput,
89
89
  ): void {
90
90
  const trackId = trackIdFor(ctx, target);
91
91
  const code = panLawCode(panLaw);
@@ -15,6 +15,7 @@ import type {
15
15
  EngineTempoSegment,
16
16
  EngineTimeSignatureSegment,
17
17
  EngineTrackLane,
18
+ EngineTrackMonitorMode,
18
19
  EngineTrackSend,
19
20
  EngineTransportState,
20
21
  EqBand,
@@ -386,6 +387,15 @@ export class SonareEngine {
386
387
  return parameter.setSoloMute(this.parameterContext, target, solo, mute);
387
388
  }
388
389
 
390
+ /** Queues a per-track PFL/AFL monitor tap mode change. */
391
+ setTrackMonitorMode(
392
+ target: string | number,
393
+ mode: EngineTrackMonitorMode,
394
+ renderFrame = -1,
395
+ ): boolean {
396
+ return parameter.setTrackMonitorMode(this.parameterContext, target, mode, renderFrame);
397
+ }
398
+
389
399
  setStripGain(target: string | number, db: number): boolean {
390
400
  return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
391
401
  }