@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
package/dist/worklet.d.ts CHANGED
@@ -34,12 +34,11 @@ interface SynthModRouting {
34
34
  *
35
35
  * The patch starts from a BASE — the named `preset` (see
36
36
  * {@link synthPresetNames}; a `"va:"` routing prefix is accepted) or, when
37
- * `preset` is omitted, the default subtractive patch. Every numeric field then
38
- * uses "0 / omit => keep the base value" (non-zero values override, clamped to
39
- * their audible ranges) and the enum fields reserve `'default'` as keep. The
40
- * frozen C ABI has no per-field presence bits, so explicit zero numeric
41
- * overrides (for example `ampSustain: 0`) cannot be represented; they keep the
42
- * base value. A non-empty `modRoutings` REPLACES the base mod matrix.
37
+ * `preset` is omitted, the default subtractive patch. Omitting a numeric field
38
+ * keeps the base value; supplying one overrides it (clamped to its audible
39
+ * range), including an explicit `0` such as `stereoSpread: 0`. The enum fields
40
+ * reserve `'default'` as keep. A `modRoutings` array REPLACES the base mod
41
+ * matrix, and an empty array clears it, while omitting the key keeps it.
43
42
  *
44
43
  * Mode-specific deep parameters (FM operator stacks, modal mode tables,
45
44
  * drawbar registrations, kit pieces, piano strings) travel inside the named
@@ -52,6 +51,8 @@ interface SynthPatch {
52
51
  * `(destination_id, patch)` bindings instead. Defaults to `0`.
53
52
  */
54
53
  destinationId?: number;
54
+ /** Resolve MIDI channels from incoming GM bank/program changes; defaults to false. */
55
+ useGmPrograms?: boolean;
55
56
  /** Base preset name (see {@link synthPresetNames}); omit for the init patch. */
56
57
  preset?: string;
57
58
  engineMode?: SynthEngineMode | number;
@@ -73,12 +74,10 @@ interface SynthPatch {
73
74
  velToCutoffCents?: number;
74
75
  ampAttackMs?: number;
75
76
  ampDecayMs?: number;
76
- /** 0 / omit keeps the base value; explicit zero sustain is not representable. */
77
77
  ampSustain?: number;
78
78
  ampReleaseMs?: number;
79
79
  filterAttackMs?: number;
80
80
  filterDecayMs?: number;
81
- /** 0 / omit keeps the base value; explicit zero sustain is not representable. */
82
81
  filterSustain?: number;
83
82
  filterReleaseMs?: number;
84
83
  lfoRateHz?: number;
@@ -128,6 +127,10 @@ type AutomationCurve = 'linear' | 'exponential' | 'hold' | 's-curve';
128
127
  * remains unity. Maps to the underlying integer code.
129
128
  */
130
129
  type PanLaw = 'const3dB' | 'const4.5dB' | 'const6dB' | 'linear0dB';
130
+ /** Accepted pan-law name aliases for mixer and realtime-engine inputs. */
131
+ type PanLawName = PanLaw | 'const-3db' | '-3db' | 'const-4.5db' | '-4.5db' | 'const-6db' | '-6db' | 'linear-0db' | 'linear' | '0db';
132
+ /** Pan-law name or raw C ABI ordinal. */
133
+ type PanLawInput = PanLawName | number;
131
134
  /** Pre/post-fader send timing (see {@link Mixer.addSend}). */
132
135
  type SendTiming = 'preFader' | 'postFader';
133
136
 
@@ -174,14 +177,21 @@ interface EqBand {
174
177
  sidechainQ?: number;
175
178
  }
176
179
  type VoicePresetId = 'neutral-monitor' | 'bright-idol' | 'soft-whisper' | 'deep-narrator' | 'robot-mascot' | 'dark-villain';
177
- interface RealtimeVoiceChangerPreset {
180
+ type VoicePresetCategory = 'monitor' | 'bright' | 'soft' | 'deep' | 'robot' | 'dark' | 'custom';
181
+ interface RealtimeVoiceChangerPresetMetadata {
178
182
  schemaVersion: 1;
179
- id?: string;
180
- name?: string;
183
+ id: string;
184
+ name: string;
181
185
  description?: string;
182
- macros?: Record<string, number>;
183
- dsp?: Record<string, unknown>;
184
- }
186
+ category: VoicePresetCategory;
187
+ }
188
+ type RealtimeVoiceChangerPreset = (RealtimeVoiceChangerPresetMetadata & {
189
+ dsp: Record<string, unknown>;
190
+ macros?: never;
191
+ }) | (RealtimeVoiceChangerPresetMetadata & {
192
+ macros: Record<string, number>;
193
+ dsp?: never;
194
+ });
185
195
  type RealtimeVoiceChangerConfigInput = VoicePresetId | RealtimeVoiceChangerPreset;
186
196
  /**
187
197
  * Flat (POD) realtime voice-changer configuration. Keys are camelCase to match
@@ -274,6 +284,8 @@ interface EngineTrackLane {
274
284
  */
275
285
  sourceChannelLayout?: number;
276
286
  }
287
+ /** Per-track cue/monitor tap mode: off, pre-fader listen, or after-fader listen. */
288
+ type EngineTrackMonitorMode = 'off' | 'pfl' | 'afl' | 0 | 1 | 2;
277
289
  interface EngineBus {
278
290
  busId: number;
279
291
  gainDb?: number;
@@ -330,6 +342,8 @@ declare class RealtimeEngine {
330
342
  */
331
343
  setParamSmoothingMs(smoothingMs: number): void;
332
344
  setSoloMute(laneIndex: number, solo: boolean, mute: boolean, renderFrame?: number): void;
345
+ /** Queue a per-track PFL/AFL monitor tap mode change. */
346
+ setTrackMonitorMode(laneIndex: number, mode: EngineTrackMonitorMode, renderFrame?: number): void;
333
347
  setMidiClips(clips: readonly EngineMidiClipSchedule[]): void;
334
348
  setBuiltinInstrument(config?: {
335
349
  destinationId?: number;
@@ -536,7 +550,7 @@ declare class RealtimeEngine {
536
550
  /** Sets a track lane strip's pan position in realtime (glitch-free). */
537
551
  setTrackStripPan(trackId: number, pan: number): void;
538
552
  /** Sets a track lane strip's pan law in realtime. */
539
- setTrackStripPanLaw(trackId: number, panLaw: PanLaw | number): void;
553
+ setTrackStripPanLaw(trackId: number, panLaw: PanLawInput): void;
540
554
  /** Sets a track lane strip's pan mode in realtime. */
541
555
  setTrackStripPanMode(trackId: number, panMode: PanMode | number): void;
542
556
  /** Sets a track lane strip's dual-pan left/right positions in realtime. */
@@ -902,7 +916,8 @@ declare enum SonareEngineCommandType {
902
916
  Punch = 14,
903
917
  SetMetronome = 15,
904
918
  SetMarker = 16,
905
- SeekMarker = 17
919
+ SeekMarker = 17,
920
+ SetTrackMonitorMode = 26
906
921
  }
907
922
  declare enum SonareEngineTelemetryType {
908
923
  ProcessBlock = 0,
@@ -928,7 +943,8 @@ declare enum SonareEngineTelemetryError {
928
943
  InsertAutomationOverflow = 16,
929
944
  MidiClockOverflow = 17,
930
945
  MetronomeOverflow = 18,
931
- InvalidCommand = 19
946
+ InvalidCommand = 19,
947
+ MaxChannelsExceeded = 20
932
948
  }
933
949
  interface SonareMeterRingBuffer {
934
950
  sharedBuffer: SharedArrayBuffer;
@@ -1220,7 +1236,7 @@ interface SonareEngineSyncErrorMessage {
1220
1236
  }
1221
1237
  type SonareWorkletTransportMessage = SonareWorkletMeterSnapshot | SonareWorkletSpectrumSnapshot | SonareWorkletExternalMidiMessage | SonareEngineClipPageRequestMessage | SonareEngineTelemetryRecord;
1222
1238
  interface WorkletTransport {
1223
- postMessage?: (message: SonareWorkletTransportMessage | SonareEngineCaptureResponseMessage | SonareEngineTransportResponseMessage | SonareEngineSyncErrorMessage, transfer?: Transferable[]) => void;
1239
+ postMessage?: (message: SonareWorkletTransportMessage | SonareEngineCaptureResponseMessageInternal | SonareEngineTransportResponseMessage | SonareEngineSyncErrorMessage, transfer?: Transferable[]) => void;
1224
1240
  onMeter?: (meter: SonareWorkletMeterSnapshot) => void;
1225
1241
  onSpectrum?: (spectrum: SonareWorkletSpectrumSnapshot) => void;
1226
1242
  }
@@ -1524,6 +1540,12 @@ interface SonareEngineCaptureRequestMessage {
1524
1540
  requestId: number;
1525
1541
  op: 'status' | 'read' | 'reset';
1526
1542
  }
1543
+ /**
1544
+ * Public capture response shape retained for source compatibility.
1545
+ *
1546
+ * The worklet wire protocol is stricter than this legacy structural type; use
1547
+ * `SonareEngineCaptureResponseMessageInternal` inside the implementation.
1548
+ */
1527
1549
  interface SonareEngineCaptureResponseMessage {
1528
1550
  type: 'captureResponse';
1529
1551
  requestId: number;
@@ -1532,6 +1554,31 @@ interface SonareEngineCaptureResponseMessage {
1532
1554
  channels?: Float32Array[] | number[][];
1533
1555
  error?: string;
1534
1556
  }
1557
+ interface SonareEngineCaptureStatusResponseMessageInternal {
1558
+ type: 'captureResponse';
1559
+ requestId: number;
1560
+ ok: true;
1561
+ status: EngineCaptureStatus;
1562
+ }
1563
+ interface SonareEngineCaptureReadResponseMessageInternal {
1564
+ type: 'captureResponse';
1565
+ requestId: number;
1566
+ ok: true;
1567
+ channels: Float32Array[];
1568
+ }
1569
+ interface SonareEngineCaptureResetResponseMessageInternal {
1570
+ type: 'captureResponse';
1571
+ requestId: number;
1572
+ ok: true;
1573
+ }
1574
+ interface SonareEngineCaptureErrorResponseMessageInternal {
1575
+ type: 'captureResponse';
1576
+ requestId: number;
1577
+ ok: false;
1578
+ error: string;
1579
+ }
1580
+ /** Strict wire response type used only by the worklet implementation. */
1581
+ type SonareEngineCaptureResponseMessageInternal = SonareEngineCaptureStatusResponseMessageInternal | SonareEngineCaptureReadResponseMessageInternal | SonareEngineCaptureResetResponseMessageInternal | SonareEngineCaptureErrorResponseMessageInternal;
1535
1582
  interface SonareEngineTransportRequestMessage {
1536
1583
  type: 'transportRequest';
1537
1584
  requestId: number;
@@ -1687,6 +1734,8 @@ declare class SonareEngine {
1687
1734
  /** Clears custom parameters and their automation lanes on both engines. */
1688
1735
  clearParameters(): void;
1689
1736
  setSoloMute(target: string | number, solo: boolean, mute: boolean): boolean;
1737
+ /** Queues a per-track PFL/AFL monitor tap mode change. */
1738
+ setTrackMonitorMode(target: string | number, mode: EngineTrackMonitorMode, renderFrame?: number): boolean;
1690
1739
  setStripGain(target: string | number, db: number): boolean;
1691
1740
  setStripPan(target: string | number, pan: number): boolean;
1692
1741
  /**
package/dist/worklet.js CHANGED
@@ -545,12 +545,25 @@ function getSonareModule() {
545
545
  }
546
546
 
547
547
  // src/codes.ts
548
+ function resolveOrdinalInRange(value, min, max, enumName) {
549
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
550
+ throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
551
+ }
552
+ return value;
553
+ }
548
554
  function resolveEnumOrdinal(value, values, enumName) {
549
555
  if (typeof value === "number") {
550
- if (!Number.isSafeInteger(value) || !Object.values(values).includes(value)) {
556
+ const ordinals = Object.values(values);
557
+ const ordinal = resolveOrdinalInRange(
558
+ value,
559
+ Math.min(...ordinals),
560
+ Math.max(...ordinals),
561
+ enumName
562
+ );
563
+ if (!ordinals.includes(ordinal)) {
551
564
  throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
552
565
  }
553
- return value;
566
+ return ordinal;
554
567
  }
555
568
  if (typeof value === "string") {
556
569
  const ordinal = values[value];
@@ -567,14 +580,23 @@ var AUTOMATION_CURVE_VALUES = {
567
580
  "s-curve": 3
568
581
  };
569
582
  var PAN_LAW_VALUES = {
570
- const3dB: 0,
571
- "const4.5dB": 1,
572
- const6dB: 2,
573
- linear0dB: 3
583
+ const3db: 0,
584
+ "const-3db": 0,
585
+ "-3db": 0,
586
+ "const4.5db": 1,
587
+ "const-4.5db": 1,
588
+ "-4.5db": 1,
589
+ const6db: 2,
590
+ "const-6db": 2,
591
+ "-6db": 2,
592
+ linear0db: 3,
593
+ "linear-0db": 3,
594
+ linear: 3,
595
+ "0db": 3
574
596
  };
575
597
  var PAN_MODE_VALUES = {
576
598
  balance: 0,
577
- pan: 0,
599
+ pan: 1,
578
600
  stereopan: 1,
579
601
  "stereo-pan": 1,
580
602
  dualpan: 2,
@@ -582,11 +604,13 @@ var PAN_MODE_VALUES = {
582
604
  };
583
605
  var METER_TAP_VALUES = { preFader: 0, postFader: 1 };
584
606
  var SEND_TIMING_VALUES = { postFader: 0, preFader: 1 };
607
+ var TRACK_MONITOR_MODE_VALUES = { off: 0, pfl: 1, afl: 2 };
585
608
  function automationCurveCode(curve) {
586
609
  return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, "automation curve");
587
610
  }
588
611
  function panLawCode(panLaw) {
589
- return resolveEnumOrdinal(panLaw, PAN_LAW_VALUES, "pan law");
612
+ const normalized = typeof panLaw === "string" ? panLaw.toLowerCase().replace(/_/g, "-") : panLaw;
613
+ return resolveEnumOrdinal(normalized, PAN_LAW_VALUES, "pan law");
590
614
  }
591
615
  function panModeCode(panMode) {
592
616
  const normalized = typeof panMode === "string" ? panMode.replace(/_/g, "-").toLowerCase() : panMode;
@@ -598,6 +622,9 @@ function meterTapCode(tap) {
598
622
  function sendTimingCode(timing) {
599
623
  return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, "send timing");
600
624
  }
625
+ function trackMonitorModeCode(mode) {
626
+ return resolveEnumOrdinal(mode, TRACK_MONITOR_MODE_VALUES, "track monitor mode");
627
+ }
601
628
 
602
629
  // src/realtime_engine.ts
603
630
  var EXPECTED_ENGINE_ABI_VERSION = 3;
@@ -661,6 +688,10 @@ var RealtimeEngine = class {
661
688
  setSoloMute(laneIndex, solo, mute, renderFrame = -1) {
662
689
  this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
663
690
  }
691
+ /** Queue a per-track PFL/AFL monitor tap mode change. */
692
+ setTrackMonitorMode(laneIndex, mode, renderFrame = -1) {
693
+ this.native.setTrackMonitorMode(laneIndex, trackMonitorModeCode(mode), renderFrame);
694
+ }
664
695
  setMidiClips(clips) {
665
696
  this.native.setMidiClips(clips);
666
697
  }
@@ -1579,7 +1610,7 @@ var Mixer = class _Mixer {
1579
1610
  setPolarityInvert(stripIndex, invertLeft, invertRight) {
1580
1611
  this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
1581
1612
  }
1582
- /** Set the strip's pan law. */
1613
+ /** Set the strip's pan law (a {@link PanLawName} alias or raw C ABI ordinal). */
1583
1614
  setPanLaw(stripIndex, panLaw) {
1584
1615
  this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
1585
1616
  }
@@ -2220,6 +2251,7 @@ var SonareEngineCommandType = /* @__PURE__ */ ((SonareEngineCommandType2) => {
2220
2251
  SonareEngineCommandType2[SonareEngineCommandType2["SetMetronome"] = 15] = "SetMetronome";
2221
2252
  SonareEngineCommandType2[SonareEngineCommandType2["SetMarker"] = 16] = "SetMarker";
2222
2253
  SonareEngineCommandType2[SonareEngineCommandType2["SeekMarker"] = 17] = "SeekMarker";
2254
+ SonareEngineCommandType2[SonareEngineCommandType2["SetTrackMonitorMode"] = 26] = "SetTrackMonitorMode";
2223
2255
  return SonareEngineCommandType2;
2224
2256
  })(SonareEngineCommandType || {});
2225
2257
  var SonareEngineTelemetryType = /* @__PURE__ */ ((SonareEngineTelemetryType2) => {
@@ -2248,6 +2280,7 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
2248
2280
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MidiClockOverflow"] = 17] = "MidiClockOverflow";
2249
2281
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MetronomeOverflow"] = 18] = "MetronomeOverflow";
2250
2282
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["InvalidCommand"] = 19] = "InvalidCommand";
2283
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["MaxChannelsExceeded"] = 20] = "MaxChannelsExceeded";
2251
2284
  return SonareEngineTelemetryError2;
2252
2285
  })(SonareEngineTelemetryError || {});
2253
2286
  function toDb(value) {
@@ -3136,8 +3169,60 @@ function isEngineSyncMessage(value) {
3136
3169
  function isEngineCaptureRequestMessage(value) {
3137
3170
  return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
3138
3171
  }
3172
+ function isCaptureStatus(value) {
3173
+ return isRecord(value) && typeof value.capturedFrames === "number" && Number.isSafeInteger(value.capturedFrames) && value.capturedFrames >= 0 && typeof value.overflowCount === "number" && Number.isSafeInteger(value.overflowCount) && value.overflowCount >= 0 && typeof value.armed === "boolean" && typeof value.punchEnabled === "boolean" && (value.source === "input" || value.source === "output") && typeof value.recordOffsetSamples === "number" && Number.isSafeInteger(value.recordOffsetSamples);
3174
+ }
3175
+ function isCaptureChannel(value) {
3176
+ return value instanceof Float32Array && typeof ArrayBuffer !== "undefined" && value.buffer instanceof ArrayBuffer;
3177
+ }
3178
+ function isCaptureChannels(value) {
3179
+ return Array.isArray(value) && value.every((channel) => isCaptureChannel(channel));
3180
+ }
3181
+ function hasOwn(value, key) {
3182
+ return Object.prototype.hasOwnProperty.call(value, key);
3183
+ }
3184
+ function engineCaptureResponseRequestId(value) {
3185
+ if (!isRecord(value) || value.type !== "captureResponse" || typeof value.requestId !== "number" || !Number.isSafeInteger(value.requestId)) {
3186
+ return void 0;
3187
+ }
3188
+ return value.requestId;
3189
+ }
3139
3190
  function isEngineCaptureResponseMessage(value) {
3140
- return isRecord(value) && value.type === "captureResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
3191
+ const requestId = engineCaptureResponseRequestId(value);
3192
+ if (requestId === void 0 || !isRecord(value)) {
3193
+ return false;
3194
+ }
3195
+ if (value.ok === false) {
3196
+ return typeof value.error === "string" && !hasOwn(value, "status") && !hasOwn(value, "channels");
3197
+ }
3198
+ if (value.ok !== true || hasOwn(value, "error")) {
3199
+ return false;
3200
+ }
3201
+ const hasStatus = hasOwn(value, "status");
3202
+ const hasChannels = hasOwn(value, "channels");
3203
+ if (hasStatus && hasChannels) {
3204
+ return false;
3205
+ }
3206
+ if (hasStatus) {
3207
+ return isCaptureStatus(value.status);
3208
+ }
3209
+ if (hasChannels) {
3210
+ return isCaptureChannels(value.channels);
3211
+ }
3212
+ return true;
3213
+ }
3214
+ function isEngineCaptureResponseForOperation(response, op) {
3215
+ if (!response.ok) {
3216
+ return true;
3217
+ }
3218
+ switch (op) {
3219
+ case "status":
3220
+ return "status" in response;
3221
+ case "read":
3222
+ return "channels" in response;
3223
+ case "reset":
3224
+ return !("status" in response) && !("channels" in response);
3225
+ }
3141
3226
  }
3142
3227
  function isEngineTransportRequestMessage(value) {
3143
3228
  return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
@@ -3173,12 +3258,22 @@ function isFiniteInteger(value) {
3173
3258
  }
3174
3259
  return typeof value === "bigint" || Number.isFinite(value) && Number.isSafeInteger(value);
3175
3260
  }
3261
+ function isTrackMonitorMode(value) {
3262
+ if (value === void 0) {
3263
+ return false;
3264
+ }
3265
+ const mode = typeof value === "bigint" ? Number(value) : value;
3266
+ return typeof mode === "number" && Number.isSafeInteger(mode) && mode >= 0 && mode <= 2;
3267
+ }
3268
+ function isTrackMonitorLaneIndex(value) {
3269
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3270
+ }
3176
3271
  function isValidCommandRecord(command) {
3177
3272
  const type = Number(command.type);
3178
- if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */) {
3273
+ if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */) {
3179
3274
  return false;
3180
3275
  }
3181
- return (command.targetId === void 0 || Number.isSafeInteger(command.targetId)) && (command.argFloat === void 0 || Number.isFinite(command.argFloat)) && isFiniteInteger(command.argInt) && isFiniteInteger(command.sampleTime);
3276
+ return (command.targetId === void 0 || Number.isSafeInteger(command.targetId)) && (command.argFloat === void 0 || Number.isFinite(command.argFloat)) && isFiniteInteger(command.argInt) && isFiniteInteger(command.sampleTime) && (type !== 26 /* SetTrackMonitorMode */ || isTrackMonitorLaneIndex(command.targetId) && isTrackMonitorMode(command.argInt));
3182
3277
  }
3183
3278
  var AUDIO_WORKLET_RENDER_QUANTUM = 128;
3184
3279
  function workletBlockSize(blockSize) {
@@ -3223,14 +3318,19 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3223
3318
  this.resolveReady();
3224
3319
  }
3225
3320
  this.node.port.onmessage = (event) => {
3226
- if (isEngineCaptureResponseMessage(event.data)) {
3227
- const pending = this.captureRequests.get(event.data.requestId);
3321
+ const captureRequestId = engineCaptureResponseRequestId(event.data);
3322
+ if (captureRequestId !== void 0) {
3323
+ const pending = this.captureRequests.get(captureRequestId);
3228
3324
  if (pending) {
3229
- this.captureRequests.delete(event.data.requestId);
3230
- if (event.data.ok) {
3325
+ this.captureRequests.delete(captureRequestId);
3326
+ if (!isEngineCaptureResponseMessage(event.data)) {
3327
+ pending.reject(new Error("Malformed capture response."));
3328
+ } else if (!isEngineCaptureResponseForOperation(event.data, pending.op)) {
3329
+ pending.reject(new Error("Capture response does not match request operation."));
3330
+ } else if (event.data.ok) {
3231
3331
  pending.resolve(event.data);
3232
3332
  } else {
3233
- pending.reject(new Error(event.data.error ?? "Capture request failed"));
3333
+ pending.reject(new Error(event.data.error));
3234
3334
  }
3235
3335
  }
3236
3336
  } else if (isEngineTransportResponseMessage(event.data)) {
@@ -3398,18 +3498,19 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3398
3498
  }
3399
3499
  requestCaptureStatus() {
3400
3500
  return this.sendCaptureRequest("status").then((response) => {
3401
- if (!response.status) {
3501
+ if (!response.ok || !("status" in response)) {
3402
3502
  throw new Error("Capture status response is missing status.");
3403
3503
  }
3404
3504
  return response.status;
3405
3505
  });
3406
3506
  }
3407
3507
  requestCapturedAudio() {
3408
- return this.sendCaptureRequest("read").then(
3409
- (response) => (response.channels ?? []).map(
3410
- (channel) => channel instanceof Float32Array ? channel : new Float32Array(channel)
3411
- )
3412
- );
3508
+ return this.sendCaptureRequest("read").then((response) => {
3509
+ if (!response.ok || !("channels" in response)) {
3510
+ throw new Error("Capture read response is missing channels.");
3511
+ }
3512
+ return response.channels;
3513
+ });
3413
3514
  }
3414
3515
  requestCaptureReset() {
3415
3516
  return this.sendCaptureRequest("reset").then(() => void 0);
@@ -3540,6 +3641,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3540
3641
  this.startRingPolling();
3541
3642
  return () => {
3542
3643
  this.midiOutListeners.delete(callback);
3644
+ this.stopRingPollingIfUnused();
3543
3645
  };
3544
3646
  }
3545
3647
  /**
@@ -3628,10 +3730,17 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3628
3730
  return Promise.reject(new Error("Realtime engine node is destroyed."));
3629
3731
  }
3630
3732
  const requestId = this.captureRequestId++;
3733
+ let rejectRequest;
3631
3734
  const promise = new Promise((resolve, reject) => {
3632
- this.captureRequests.set(requestId, { resolve, reject });
3735
+ rejectRequest = reject;
3736
+ this.captureRequests.set(requestId, { op, resolve, reject });
3633
3737
  });
3634
- this.node.port.postMessage({ type: "captureRequest", requestId, op });
3738
+ try {
3739
+ this.node.port.postMessage({ type: "captureRequest", requestId, op });
3740
+ } catch (error) {
3741
+ this.captureRequests.delete(requestId);
3742
+ rejectRequest(error);
3743
+ }
3635
3744
  return promise;
3636
3745
  }
3637
3746
  sendTransportRequest() {
@@ -3668,6 +3777,21 @@ function setSoloMute(ctx, target, solo, mute) {
3668
3777
  argInt: (mute ? 1 : 0) | (solo ? 2 : 0)
3669
3778
  });
3670
3779
  }
3780
+ function setTrackMonitorMode(ctx, target, mode, renderFrame = -1) {
3781
+ const modeOrdinal = trackMonitorModeCode(mode);
3782
+ const laneIndex = ctx.ensureTrackLane(target);
3783
+ ctx.offlineEngine.setTrackMonitorMode(
3784
+ laneIndex,
3785
+ modeOrdinal,
3786
+ renderFrame
3787
+ );
3788
+ return ctx.sendCommand({
3789
+ type: 26 /* SetTrackMonitorMode */,
3790
+ targetId: laneIndex,
3791
+ sampleTime: renderFrame,
3792
+ argInt: modeOrdinal
3793
+ });
3794
+ }
3671
3795
  function automationParamId(ctx, target, kind) {
3672
3796
  const paramKind = kind === "pan" ? ENGINE_MIXER_PARAM_PAN : ENGINE_MIXER_PARAM_FADER_DB;
3673
3797
  if (target === "master") {
@@ -4181,6 +4305,10 @@ var SonareEngine = class _SonareEngine {
4181
4305
  setSoloMute(target, solo, mute) {
4182
4306
  return setSoloMute(this.parameterContext, target, solo, mute);
4183
4307
  }
4308
+ /** Queues a per-track PFL/AFL monitor tap mode change. */
4309
+ setTrackMonitorMode(target, mode, renderFrame = -1) {
4310
+ return setTrackMonitorMode(this.parameterContext, target, mode, renderFrame);
4311
+ }
4184
4312
  setStripGain(target, db) {
4185
4313
  return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
4186
4314
  }
@@ -4965,6 +5093,21 @@ function resolveMetronomeConfig(config) {
4965
5093
  }
4966
5094
 
4967
5095
  // src/worklet/engine-processor.ts
5096
+ function captureTransferList(channels) {
5097
+ const transfers = [];
5098
+ const seen = /* @__PURE__ */ new Set();
5099
+ for (const channel of channels) {
5100
+ const buffer = channel.buffer;
5101
+ if (!(buffer instanceof ArrayBuffer)) {
5102
+ throw new TypeError("capture response channels must use plain ArrayBuffers");
5103
+ }
5104
+ if (!seen.has(buffer)) {
5105
+ seen.add(buffer);
5106
+ transfers.push(buffer);
5107
+ }
5108
+ }
5109
+ return transfers;
5110
+ }
4968
5111
  var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletProcessor {
4969
5112
  constructor(options = {}, transport) {
4970
5113
  this.closed = false;
@@ -5445,22 +5588,17 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5445
5588
  return;
5446
5589
  }
5447
5590
  if (message.op === "read") {
5448
- const captured = this.engine.capturedAudio();
5449
- const channels = [];
5450
- for (let ch = 0; ch < captured.length; ch++) {
5451
- const source = captured[ch];
5452
- const copy = [];
5453
- for (let i = 0; i < source.length; i++) {
5454
- copy.push(Number(source[i]));
5455
- }
5456
- channels.push(copy);
5457
- }
5458
- this.transport?.postMessage?.({
5459
- type: "captureResponse",
5460
- requestId: message.requestId,
5461
- ok: true,
5462
- channels
5463
- });
5591
+ const channels = Array.from(this.engine.capturedAudio());
5592
+ const transfer = captureTransferList(channels);
5593
+ this.transport?.postMessage?.(
5594
+ {
5595
+ type: "captureResponse",
5596
+ requestId: message.requestId,
5597
+ ok: true,
5598
+ channels
5599
+ },
5600
+ transfer
5601
+ );
5464
5602
  return;
5465
5603
  }
5466
5604
  this.engine.resetCapture();
@@ -5598,6 +5736,19 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5598
5736
  sampleTime
5599
5737
  );
5600
5738
  break;
5739
+ case 26 /* SetTrackMonitorMode */: {
5740
+ const rawMode = command.argInt;
5741
+ const mode = typeof rawMode === "bigint" ? Number(rawMode) : rawMode;
5742
+ if (typeof mode !== "number" || !Number.isSafeInteger(mode) || mode < 0 || mode > 2) {
5743
+ throw new RangeError(`Invalid track monitor mode: ${String(rawMode)}`);
5744
+ }
5745
+ const laneIndex = command.targetId;
5746
+ if (typeof laneIndex !== "number" || !Number.isSafeInteger(laneIndex) || laneIndex < 0 || laneIndex > 4294967295) {
5747
+ throw new RangeError(`Invalid track monitor lane index: ${String(laneIndex)}`);
5748
+ }
5749
+ this.engine.setTrackMonitorMode(laneIndex, mode, sampleTime);
5750
+ break;
5751
+ }
5601
5752
  default:
5602
5753
  this.publishTelemetryRecord({
5603
5754
  type: 1 /* Error */,
@@ -5995,7 +6146,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
5995
6146
  throw new Error("AudioWorklet initialization message buffer overflowed.");
5996
6147
  }
5997
6148
  this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
5998
- postMessage: (message) => port?.postMessage?.(message),
6149
+ postMessage: (message, transfer) => port?.postMessage?.(message, transfer),
5999
6150
  onMeter: (meter) => port?.postMessage?.(meter)
6000
6151
  });
6001
6152
  for (const message of options.initialSyncMessages ?? []) {