@libraz/libsonare 1.6.0 → 1.7.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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis.d.ts +88 -9
  3. package/dist/analysis.js +152 -42
  4. package/dist/analysis.js.map +1 -1
  5. package/dist/index.d.ts +129 -24
  6. package/dist/index.js +455 -140
  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 +62 -10
  16. package/dist/worklet.js +193 -42
  17. package/dist/worklet.js.map +1 -1
  18. package/package.json +3 -3
  19. package/src/analysis_helpers.ts +34 -29
  20. package/src/audio.ts +7 -2
  21. package/src/codes.ts +47 -11
  22. package/src/effects_transform.ts +179 -10
  23. package/src/feature_music.ts +8 -1
  24. package/src/feature_spectral.ts +64 -2
  25. package/src/feature_spectrogram.ts +44 -3
  26. package/src/index.ts +21 -3
  27. package/src/mixer.ts +3 -3
  28. package/src/project.ts +6 -0
  29. package/src/project_class.ts +143 -16
  30. package/src/project_internal.ts +14 -5
  31. package/src/project_types.ts +45 -1
  32. package/src/public_types_mixing.ts +16 -0
  33. package/src/public_types_realtime.ts +23 -5
  34. package/src/quick_analysis.ts +21 -3
  35. package/src/realtime_engine.ts +14 -3
  36. package/src/sonare.js.d.ts +64 -0
  37. package/src/stream_analyzer.ts +6 -1
  38. package/src/web_midi.ts +10 -7
  39. package/src/worklet/engine-node.ts +54 -19
  40. package/src/worklet/engine-parameter-facade.ts +30 -1
  41. package/src/worklet/engine-processor.ts +52 -20
  42. package/src/worklet/engine-register.ts +1 -1
  43. package/src/worklet/engine-strips.ts +2 -2
  44. package/src/worklet/engine.ts +10 -0
  45. package/src/worklet/guards.ts +99 -7
  46. package/src/worklet/messages.ts +41 -1
  47. package/src/worklet/protocol.ts +4 -0
@@ -29,7 +29,12 @@ import type {
29
29
  import { Mode, PitchClass } from './public_types';
30
30
  import type { ProgressCallback, WasmAcousticResult } from './sonare.js';
31
31
  import type { ValidateOptions } from './validation';
32
- import { assertNonNegativeInteger, assertSampleRate, assertSamples } from './validation';
32
+ import {
33
+ assertFiniteScalar,
34
+ assertNonNegativeInteger,
35
+ assertSampleRate,
36
+ assertSamples,
37
+ } from './validation';
33
38
 
34
39
  function requireModule() {
35
40
  return getSonareModule();
@@ -81,6 +86,7 @@ export interface ChordFunctionalAnalysisRequest extends DetectChordsRequest {
81
86
  /** Canonical request form for impulse-response analysis. */
82
87
  export interface AnalyzeImpulseResponseRequest extends SamplesRequest {
83
88
  nOctaveBands?: number;
89
+ minDecayDb?: number;
84
90
  }
85
91
 
86
92
  /** Canonical request form for acoustic analysis. */
@@ -435,23 +441,35 @@ export function analyzeImpulseResponse(
435
441
  samples: Float32Array,
436
442
  sampleRate?: number,
437
443
  nOctaveBands?: number,
444
+ minDecayDb?: number,
438
445
  ): AcousticResult;
439
446
  export function analyzeImpulseResponse(
440
447
  samples: Float32Array | AnalyzeImpulseResponseRequest,
441
448
  sampleRate = 48000,
442
449
  nOctaveBands = 6,
450
+ minDecayDb?: number,
443
451
  ): AcousticResult {
444
- const request = samples instanceof Float32Array ? { samples, sampleRate, nOctaveBands } : samples;
452
+ const request =
453
+ samples instanceof Float32Array ? { samples, sampleRate, nOctaveBands, minDecayDb } : samples;
454
+ if (request.minDecayDb === null) {
455
+ throw new TypeError('analyzeImpulseResponse: minDecayDb must be a finite number');
456
+ }
457
+ const resolvedMinDecayDb = request.minDecayDb === undefined ? 30.0 : request.minDecayDb;
458
+ assertFiniteScalar('analyzeImpulseResponse', resolvedMinDecayDb, 'minDecayDb');
459
+ if (resolvedMinDecayDb <= 0) {
460
+ throw new RangeError('analyzeImpulseResponse: minDecayDb must be greater than zero');
461
+ }
445
462
  validateAnalysisInput(
446
463
  'analyzeImpulseResponse',
447
464
  request.samples,
448
465
  request.sampleRate ?? 48000,
449
466
  request,
450
467
  );
451
- const result: WasmAcousticResult = requireModule().analyzeImpulseResponse(
468
+ const result: WasmAcousticResult = requireModule().analyzeImpulseResponseEx(
452
469
  request.samples,
453
470
  request.sampleRate ?? 48000,
454
471
  request.nOctaveBands ?? 6,
472
+ resolvedMinDecayDb,
455
473
  );
456
474
  return result;
457
475
  }
@@ -1,8 +1,8 @@
1
- import { panLawCode, panModeCode, sendTimingCode } from './codes';
1
+ import { panLawCode, panModeCode, sendTimingCode, trackMonitorModeCode } from './codes';
2
2
  import { ErrorCode, SonareError } from './errors';
3
3
  import { getSonareModule } from './module_state';
4
4
  import type { ProjectMidiCcBinding, SynthPatch } from './project';
5
- import type { EqBand, PanLaw, PanMode, SendTiming } from './public_types';
5
+ import type { EqBand, PanLawInput, PanMode, SendTiming } from './public_types';
6
6
  import type {
7
7
  WasmClipPageRequest,
8
8
  WasmEngineAutomationPoint,
@@ -78,6 +78,12 @@ export interface EngineTrackLane {
78
78
  sourceChannelLayout?: number;
79
79
  }
80
80
 
81
+ /** Per-track cue/monitor tap mode: off, pre-fader listen, or after-fader listen. */
82
+ export type EngineTrackMonitorMode = 'off' | 'pfl' | 'afl' | 0 | 1 | 2;
83
+
84
+ /** Short alias for {@link EngineTrackMonitorMode}. */
85
+ export type TrackMonitorMode = EngineTrackMonitorMode;
86
+
81
87
  export interface EngineBus {
82
88
  busId: number;
83
89
  gainDb?: number;
@@ -219,6 +225,11 @@ export class RealtimeEngine {
219
225
  this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
220
226
  }
221
227
 
228
+ /** Queue a per-track PFL/AFL monitor tap mode change. */
229
+ setTrackMonitorMode(laneIndex: number, mode: EngineTrackMonitorMode, renderFrame = -1): void {
230
+ this.native.setTrackMonitorMode(laneIndex, trackMonitorModeCode(mode), renderFrame);
231
+ }
232
+
222
233
  setMidiClips(clips: readonly EngineMidiClipSchedule[]): void {
223
234
  this.native.setMidiClips(clips);
224
235
  }
@@ -831,7 +842,7 @@ export class RealtimeEngine {
831
842
  }
832
843
 
833
844
  /** Sets a track lane strip's pan law in realtime. */
834
- setTrackStripPanLaw(trackId: number, panLaw: PanLaw | number): void {
845
+ setTrackStripPanLaw(trackId: number, panLaw: PanLawInput): void {
835
846
  this.native.setTrackStripPanLaw(trackId, panLawCode(panLaw));
836
847
  }
837
848
 
@@ -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,
@@ -1473,12 +1484,30 @@ export interface SonareModule {
1473
1484
  kernelHarmonic: number,
1474
1485
  kernelPercussive: number,
1475
1486
  ) => WasmHpssResult;
1487
+ hpssEx: (
1488
+ samples: Float32Array,
1489
+ sampleRate: number,
1490
+ kernelHarmonic: number,
1491
+ kernelPercussive: number,
1492
+ nFft: number,
1493
+ hopLength: number,
1494
+ hardMask: boolean,
1495
+ ) => WasmHpssResult;
1476
1496
  hpssWithResidual: (
1477
1497
  samples: Float32Array,
1478
1498
  sampleRate: number,
1479
1499
  kernelHarmonic: number,
1480
1500
  kernelPercussive: number,
1481
1501
  ) => WasmHpssWithResidualResult;
1502
+ hpssWithResidualEx: (
1503
+ samples: Float32Array,
1504
+ sampleRate: number,
1505
+ kernelHarmonic: number,
1506
+ kernelPercussive: number,
1507
+ nFft: number,
1508
+ hopLength: number,
1509
+ hardMask: boolean,
1510
+ ) => WasmHpssWithResidualResult;
1482
1511
  decompose: (
1483
1512
  s: Float32Array,
1484
1513
  nFeatures: number,
@@ -1520,7 +1549,21 @@ export interface SonareModule {
1520
1549
  harmonic: (samples: Float32Array, sampleRate: number) => Float32Array;
1521
1550
  percussive: (samples: Float32Array, sampleRate: number) => Float32Array;
1522
1551
  timeStretch: (samples: Float32Array, sampleRate: number, rate: number) => Float32Array;
1552
+ timeStretchEx: (
1553
+ samples: Float32Array,
1554
+ sampleRate: number,
1555
+ rate: number,
1556
+ nFft: number,
1557
+ hopLength: number,
1558
+ ) => Float32Array;
1523
1559
  pitchShift: (samples: Float32Array, sampleRate: number, semitones: number) => Float32Array;
1560
+ pitchShiftEx: (
1561
+ samples: Float32Array,
1562
+ sampleRate: number,
1563
+ semitones: number,
1564
+ nFft: number,
1565
+ hopLength: number,
1566
+ ) => Float32Array;
1524
1567
  pitchCorrectToMidi: (
1525
1568
  samples: Float32Array,
1526
1569
  sampleRate: number,
@@ -1582,6 +1625,12 @@ export interface SonareModule {
1582
1625
  channels: number,
1583
1626
  ) => Float32Array;
1584
1627
  normalize: (samples: Float32Array, sampleRate: number, targetDb: number) => Float32Array;
1628
+ normalizeEx: (
1629
+ samples: Float32Array,
1630
+ sampleRate: number,
1631
+ targetDb: number,
1632
+ mode: string,
1633
+ ) => Float32Array;
1585
1634
  mastering: (
1586
1635
  samples: Float32Array,
1587
1636
  sampleRate: number,
@@ -1760,6 +1809,13 @@ export interface SonareModule {
1760
1809
  options: Record<string, unknown>,
1761
1810
  ) => WasmMixResult;
1762
1811
  trim: (samples: Float32Array, sampleRate: number, thresholdDb: number) => Float32Array;
1812
+ trimEx: (
1813
+ samples: Float32Array,
1814
+ sampleRate: number,
1815
+ thresholdDb: number,
1816
+ frameLength: number,
1817
+ hopLength: number,
1818
+ ) => Float32Array;
1763
1819
  spectralEdit: (
1764
1820
  samples: Float32Array,
1765
1821
  sampleRate: number,
@@ -2190,6 +2246,14 @@ export interface SonareModule {
2190
2246
  stftBlendWeight: number,
2191
2247
  stftBlendNFft: number,
2192
2248
  ) => WasmNnlsChromaResult;
2249
+ nnlsChromaEx: (
2250
+ samples: Float32Array,
2251
+ sampleRate: number,
2252
+ enableStftBlend: boolean,
2253
+ stftBlendWeight: number,
2254
+ stftBlendNFft: number,
2255
+ hopLength: number,
2256
+ ) => WasmNnlsChromaResult;
2193
2257
  cqt: (
2194
2258
  samples: Float32Array,
2195
2259
  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
  }