@libraz/libsonare 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worklet.js CHANGED
@@ -39,6 +39,7 @@ function makeSonareError(raw, thrown) {
39
39
  }
40
40
  function wrapModuleErrors(raw) {
41
41
  const cache = /* @__PURE__ */ new Map();
42
+ const objectCache = /* @__PURE__ */ new WeakMap();
42
43
  const convert = (error) => {
43
44
  const ptr = nativeExceptionPtr(error);
44
45
  if (ptr !== null) {
@@ -46,6 +47,83 @@ function wrapModuleErrors(raw) {
46
47
  }
47
48
  throw error;
48
49
  };
50
+ const wrapNativeObject = (value) => {
51
+ if (value === null || typeof value !== "object") {
52
+ return value;
53
+ }
54
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof Promise) {
55
+ return value;
56
+ }
57
+ const objectValue = value;
58
+ const cached = objectCache.get(objectValue);
59
+ if (cached) {
60
+ return cached;
61
+ }
62
+ const methodCache = /* @__PURE__ */ new Map();
63
+ const wrapped = new Proxy(objectValue, {
64
+ get(target, prop, receiver) {
65
+ const member = Reflect.get(target, prop, receiver);
66
+ if (typeof member !== "function") {
67
+ return member;
68
+ }
69
+ const cachedMethod = methodCache.get(prop);
70
+ if (cachedMethod) {
71
+ return cachedMethod;
72
+ }
73
+ const method = member;
74
+ const wrappedMethod = (...args) => {
75
+ try {
76
+ return wrapNativeObject(Reflect.apply(method, target, args));
77
+ } catch (error) {
78
+ return convert(error);
79
+ }
80
+ };
81
+ methodCache.set(prop, wrappedMethod);
82
+ return wrappedMethod;
83
+ }
84
+ });
85
+ objectCache.set(objectValue, wrapped);
86
+ return wrapped;
87
+ };
88
+ const wrapFunction = (value) => {
89
+ const fnCache = /* @__PURE__ */ new Map();
90
+ return new Proxy(value, {
91
+ get(target, prop, receiver) {
92
+ const member = Reflect.get(target, prop, receiver);
93
+ if (typeof member !== "function") {
94
+ return member;
95
+ }
96
+ const cachedMember = fnCache.get(prop);
97
+ if (cachedMember) {
98
+ return cachedMember;
99
+ }
100
+ const fn = member;
101
+ const wrappedMember = (...args) => {
102
+ try {
103
+ return wrapNativeObject(Reflect.apply(fn, target, args));
104
+ } catch (error) {
105
+ return convert(error);
106
+ }
107
+ };
108
+ fnCache.set(prop, wrappedMember);
109
+ return wrappedMember;
110
+ },
111
+ apply(t, thisArg, args) {
112
+ try {
113
+ return wrapNativeObject(Reflect.apply(t, thisArg, args));
114
+ } catch (error) {
115
+ return convert(error);
116
+ }
117
+ },
118
+ construct(t, args, newTarget) {
119
+ try {
120
+ return wrapNativeObject(Reflect.construct(t, args, newTarget));
121
+ } catch (error) {
122
+ return convert(error);
123
+ }
124
+ }
125
+ });
126
+ };
49
127
  return new Proxy(raw, {
50
128
  get(target, prop, receiver) {
51
129
  const value = Reflect.get(target, prop, receiver);
@@ -56,23 +134,7 @@ function wrapModuleErrors(raw) {
56
134
  if (cached) {
57
135
  return cached;
58
136
  }
59
- const fn = value;
60
- const wrapped = new Proxy(fn, {
61
- apply(t, thisArg, args) {
62
- try {
63
- return Reflect.apply(t, thisArg, args);
64
- } catch (error) {
65
- return convert(error);
66
- }
67
- },
68
- construct(t, args, newTarget) {
69
- try {
70
- return Reflect.construct(t, args, newTarget);
71
- } catch (error) {
72
- return convert(error);
73
- }
74
- }
75
- });
137
+ const wrapped = wrapFunction(value);
76
138
  cache.set(prop, wrapped);
77
139
  return wrapped;
78
140
  }
@@ -732,9 +794,6 @@ function engineCapabilities() {
732
794
  };
733
795
  }
734
796
  var RealtimeEngine = class {
735
- nativeExt() {
736
- return this.native;
737
- }
738
797
  constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
739
798
  const module2 = getSonareModule();
740
799
  const capabilities = engineCapabilities();
@@ -761,8 +820,14 @@ var RealtimeEngine = class {
761
820
  setParameterSmoothed(paramId, value, renderFrame = -1) {
762
821
  this.native.setParameterSmoothed(paramId, value, renderFrame);
763
822
  }
823
+ setSoloMute(laneIndex, solo, mute, renderFrame = -1) {
824
+ this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
825
+ }
826
+ setMidiClips(clips) {
827
+ this.native.setMidiClips(clips);
828
+ }
764
829
  setBuiltinInstrument(config = {}, destinationId = config.destinationId ?? 0) {
765
- this.nativeExt().setBuiltinInstrument(destinationId, config);
830
+ this.native.setBuiltinInstrument(destinationId, config);
766
831
  }
767
832
  /**
768
833
  * Bind the patch-driven NativeSynth to a realtime MIDI destination. `patch`
@@ -774,7 +839,7 @@ var RealtimeEngine = class {
774
839
  * binding convenience, not part of the NativeSynth patch itself.
775
840
  */
776
841
  setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
777
- this.nativeExt().setSynthInstrument(destinationId, patch);
842
+ this.native.setSynthInstrument(destinationId, patch);
778
843
  }
779
844
  /**
780
845
  * Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
@@ -783,7 +848,7 @@ var RealtimeEngine = class {
783
848
  * not referenced afterwards. Replaces any previously loaded SoundFont.
784
849
  */
785
850
  loadSoundFont(data) {
786
- this.nativeExt().loadSoundFont(data);
851
+ this.native.loadSoundFont(data);
787
852
  }
788
853
  /**
789
854
  * Bind a GS-compatible SoundFont player to a realtime MIDI destination, fed
@@ -795,13 +860,13 @@ var RealtimeEngine = class {
795
860
  * synthesizer GM fallback bank (the data-free floor).
796
861
  */
797
862
  setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
798
- this.nativeExt().setSf2Instrument(destinationId, config);
863
+ this.native.setSf2Instrument(destinationId, config);
799
864
  }
800
865
  clearMidiInstrument(destinationId = 0) {
801
- this.nativeExt().clearMidiInstrument(destinationId);
866
+ this.native.clearMidiInstrument(destinationId);
802
867
  }
803
868
  midiInstrumentCount() {
804
- return this.nativeExt().midiInstrumentCount();
869
+ return this.native.midiInstrumentCount();
805
870
  }
806
871
  /**
807
872
  * Bind a live MIDI CC to an engine automation parameter. The MIDI event still
@@ -809,7 +874,7 @@ var RealtimeEngine = class {
809
874
  * mapped into [minValue, maxValue] for `paramId`.
810
875
  */
811
876
  bindMidiCc(channel, controller, paramId, options = {}) {
812
- this.nativeExt().bindMidiCc(
877
+ this.native.bindMidiCc(
813
878
  channel,
814
879
  controller,
815
880
  paramId,
@@ -818,42 +883,42 @@ var RealtimeEngine = class {
818
883
  );
819
884
  }
820
885
  clearMidiCcBindings() {
821
- this.nativeExt().clearMidiCcBindings();
886
+ this.native.clearMidiCcBindings();
822
887
  }
823
888
  midiCcBindingCount() {
824
- return this.nativeExt().midiCcBindingCount();
889
+ return this.native.midiCcBindingCount();
825
890
  }
826
891
  /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
827
892
  setMidiFx(destinationId, configJson) {
828
- this.nativeExt().setMidiFx(destinationId, configJson);
893
+ this.native.setMidiFx(destinationId, configJson);
829
894
  }
830
895
  clearMidiFx(destinationId = 0) {
831
- this.nativeExt().clearMidiFx(destinationId);
896
+ this.native.clearMidiFx(destinationId);
832
897
  }
833
898
  /** Enable the engine-owned live MIDI input source for a destination. */
834
899
  setMidiInputSource(destinationId = 0) {
835
- this.nativeExt().setMidiInputSource(destinationId);
900
+ this.native.setMidiInputSource(destinationId);
836
901
  }
837
902
  clearMidiInputSource() {
838
- this.nativeExt().clearMidiInputSource();
903
+ this.native.clearMidiInputSource();
839
904
  }
840
905
  midiInputPendingCount() {
841
- return this.nativeExt().midiInputPendingCount();
906
+ return this.native.midiInputPendingCount();
842
907
  }
843
908
  pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
844
- this.nativeExt().pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
909
+ this.native.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
845
910
  }
846
911
  pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
847
- this.nativeExt().pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
912
+ this.native.pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
848
913
  }
849
914
  pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
850
- this.nativeExt().pushMidiInputCc(group, channel, controller, value, portTimeSamples);
915
+ this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
851
916
  }
852
917
  pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
853
- this.nativeExt().pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
918
+ this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
854
919
  }
855
920
  pushMidiNoteOff(destinationId, group, channel, note, velocity = 0, renderFrame = -1) {
856
- this.nativeExt().pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
921
+ this.native.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
857
922
  }
858
923
  /**
859
924
  * Queue an immediate (live) MIDI control change to a MIDI destination
@@ -862,21 +927,21 @@ var RealtimeEngine = class {
862
927
  * immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
863
928
  */
864
929
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
865
- this.nativeExt().pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
930
+ this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
866
931
  }
867
932
  /**
868
933
  * Queue a MIDI panic (all-notes-off) releasing every sounding note at
869
934
  * `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
870
935
  */
871
936
  pushMidiPanic(renderFrame = -1) {
872
- this.nativeExt().pushMidiPanic(renderFrame);
937
+ this.native.pushMidiPanic(renderFrame);
873
938
  }
874
939
  /**
875
940
  * Remove all registered parameters (and their automation lanes). Control-thread
876
941
  * only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
877
942
  */
878
943
  clearParameters() {
879
- this.nativeExt().clearParameters();
944
+ this.native.clearParameters();
880
945
  }
881
946
  /** Read back the current transport state snapshot. */
882
947
  getTransportState() {
@@ -897,9 +962,18 @@ var RealtimeEngine = class {
897
962
  setTempo(bpm) {
898
963
  this.native.setTempo(bpm);
899
964
  }
965
+ setTempoSegments(segments) {
966
+ this.native.setTempoSegments([...segments]);
967
+ }
900
968
  setTimeSignature(numerator, denominator) {
901
969
  this.native.setTimeSignature(numerator, denominator);
902
970
  }
971
+ setTimeSignatureSegments(segments) {
972
+ this.native.setTimeSignatureSegments([...segments]);
973
+ }
974
+ sampleAtPpq(ppq) {
975
+ return Number(this.native.sampleAtPpq(ppq));
976
+ }
903
977
  setLoop(startPpq, endPpq, enabled = true) {
904
978
  this.native.setLoop(startPpq, endPpq, enabled);
905
979
  }
@@ -968,21 +1042,81 @@ var RealtimeEngine = class {
968
1042
  clipCount() {
969
1043
  return this.native.clipCount();
970
1044
  }
1045
+ setTrackLanes(lanes) {
1046
+ this.native.setTrackLanes(
1047
+ lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane)
1048
+ );
1049
+ }
1050
+ setTrackBuses(buses) {
1051
+ this.native.setTrackBuses(buses);
1052
+ }
1053
+ setBusStripJson(busId, sceneJson) {
1054
+ try {
1055
+ JSON.parse(sceneJson);
1056
+ } catch (error) {
1057
+ const message = error instanceof Error ? error.message : "invalid bus strip JSON";
1058
+ throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
1059
+ }
1060
+ this.native.setBusStripJson(busId, sceneJson);
1061
+ }
1062
+ setTrackStripJson(trackId, sceneJson) {
1063
+ try {
1064
+ JSON.parse(sceneJson);
1065
+ } catch (error) {
1066
+ const message = error instanceof Error ? error.message : "invalid track strip JSON";
1067
+ throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
1068
+ }
1069
+ this.native.setTrackStripJson(trackId, sceneJson);
1070
+ }
1071
+ setTrackStripEqBand(trackId, bandIndex, band) {
1072
+ this.native.setTrackStripEqBandJson(
1073
+ trackId,
1074
+ bandIndex,
1075
+ typeof band === "string" ? band : JSON.stringify(band)
1076
+ );
1077
+ }
1078
+ setTrackStripEqBandJson(trackId, bandIndex, bandJson) {
1079
+ this.native.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
1080
+ }
1081
+ setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass = false) {
1082
+ this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
1083
+ }
1084
+ setMasterStripJson(sceneJson) {
1085
+ try {
1086
+ JSON.parse(sceneJson);
1087
+ } catch (error) {
1088
+ const message = error instanceof Error ? error.message : "invalid master strip JSON";
1089
+ throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
1090
+ }
1091
+ this.native.setMasterStripJson(sceneJson);
1092
+ }
1093
+ setMasterStripEqBand(bandIndex, band) {
1094
+ this.native.setMasterStripEqBandJson(
1095
+ bandIndex,
1096
+ typeof band === "string" ? band : JSON.stringify(band)
1097
+ );
1098
+ }
1099
+ setMasterStripEqBandJson(bandIndex, bandJson) {
1100
+ this.native.setMasterStripEqBandJson(bandIndex, bandJson);
1101
+ }
1102
+ setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass = false) {
1103
+ this.native.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
1104
+ }
971
1105
  createClipPageProvider(numChannels, numSamples, pageFrames) {
972
- const id = this.nativeExt().createClipPageProvider(numChannels, numSamples, pageFrames);
1106
+ const id = this.native.createClipPageProvider(numChannels, numSamples, pageFrames);
973
1107
  return new ClipPageProvider(this, id);
974
1108
  }
975
1109
  supplyClipPage(providerId, pageIndex, channels) {
976
- this.nativeExt().supplyClipPage(providerId, pageIndex, channels);
1110
+ this.native.supplyClipPage(providerId, pageIndex, channels);
977
1111
  }
978
1112
  clearClipPage(providerId, pageIndex) {
979
- this.nativeExt().clearClipPage(providerId, pageIndex);
1113
+ this.native.clearClipPage(providerId, pageIndex);
980
1114
  }
981
1115
  destroyClipPageProvider(providerId) {
982
- this.nativeExt().destroyClipPageProvider(providerId);
1116
+ this.native.destroyClipPageProvider(providerId);
983
1117
  }
984
1118
  popClipPageRequest() {
985
- return this.nativeExt().popClipPageRequest();
1119
+ return this.native.popClipPageRequest();
986
1120
  }
987
1121
  setCaptureBuffer(numChannels, capacityFrames) {
988
1122
  this.native.setCaptureBuffer(numChannels, capacityFrames);
@@ -1100,7 +1234,7 @@ async function init(options) {
1100
1234
  }
1101
1235
  initPromise = (async () => {
1102
1236
  try {
1103
- const createModule = (await import("./sonare.js")).default;
1237
+ const createModule = options?.moduleFactory ?? (await import("./sonare.js")).default;
1104
1238
  module = await createModule(options);
1105
1239
  setSonareModule(module);
1106
1240
  } catch (error) {
@@ -1115,8 +1249,20 @@ function isInitialized() {
1115
1249
  }
1116
1250
 
1117
1251
  // src/worklet.ts
1252
+ var ENGINE_MIXER_TARGET_BASE = 1297612800;
1253
+ var ENGINE_MIXER_PARAM_FADER_DB = 1;
1254
+ var ENGINE_MIXER_PARAM_PAN = 2;
1255
+ function engineMixerLaneTarget(laneIndex, paramKind) {
1256
+ return ENGINE_MIXER_TARGET_BASE | (laneIndex & 255) << 8 | paramKind & 255;
1257
+ }
1258
+ function engineMixerBusTarget(busIndex, paramKind) {
1259
+ return ENGINE_MIXER_TARGET_BASE | (254 - busIndex & 255) << 8 | paramKind & 255;
1260
+ }
1261
+ function engineMixerMasterTarget(paramKind) {
1262
+ return ENGINE_MIXER_TARGET_BASE | 255 << 8 | paramKind & 255;
1263
+ }
1118
1264
  var SONARE_METER_RING_HEADER_INTS = 4;
1119
- var SONARE_METER_RING_RECORD_FLOATS = 7;
1265
+ var SONARE_METER_RING_RECORD_FLOATS = 14;
1120
1266
  var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
1121
1267
  var SONARE_FRAME_LANE_BASE = 16777216;
1122
1268
  function encodeFrameLo(frame) {
@@ -1207,7 +1353,19 @@ function isEngineSyncMessage(value) {
1207
1353
  if (!isRecord(value) || typeof value.type !== "string") {
1208
1354
  return false;
1209
1355
  }
1210
- return value.type === "syncClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation";
1356
+ return value.type === "syncClips" || value.type === "syncClipsDelta" || value.type === "syncMidiClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation" || value.type === "syncTempo" || value.type === "syncMixer" || value.type === "syncCapture" || value.type === "syncTrackStripEqBand" || value.type === "syncMasterStripEqBand" || value.type === "syncTrackStripInsertBypassed" || value.type === "syncMasterStripInsertBypassed" || value.type === "syncBuiltinInstrument" || value.type === "syncSynthInstrument" || value.type === "syncSf2Instrument" || value.type === "syncLoadSoundFont" || value.type === "syncMidiNoteOn" || value.type === "syncMidiNoteOff" || value.type === "syncMidiCc" || value.type === "syncMidiPanic";
1357
+ }
1358
+ function isEngineCaptureRequestMessage(value) {
1359
+ return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
1360
+ }
1361
+ function isEngineCaptureResponseMessage(value) {
1362
+ return isRecord(value) && value.type === "captureResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
1363
+ }
1364
+ function isEngineTransportRequestMessage(value) {
1365
+ return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
1366
+ }
1367
+ function isEngineTransportResponseMessage(value) {
1368
+ return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
1211
1369
  }
1212
1370
  function isRealtimeVoiceChangerMessage(value) {
1213
1371
  if (!isRecord(value) || typeof value.type !== "string") {
@@ -1219,7 +1377,7 @@ function isEngineTelemetryRecord(value) {
1219
1377
  return isRecord(value) && typeof value.type === "number" && typeof value.error === "number" && typeof value.renderFrame === "number" && typeof value.timelineSample === "number" && typeof value.audibleTimelineSample === "number" && typeof value.graphLatencySamplesQ8 === "number" && typeof value.value === "number";
1220
1378
  }
1221
1379
  function isMeterSnapshot(value) {
1222
- return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number";
1380
+ return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && (typeof value.targetId === "number" || value.targetId === void 0);
1223
1381
  }
1224
1382
  function sonareMeterRingBufferByteLength(capacity) {
1225
1383
  const clampedCapacity = Math.max(1, Math.floor(capacity));
@@ -1237,19 +1395,27 @@ function createSonareMeterRingBuffer(capacity = 128) {
1237
1395
  }
1238
1396
  function readSonareMeterRingBuffer(ring, readIndex = 0) {
1239
1397
  const writeIndex = Atomics.load(ring.header, 0);
1398
+ const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
1240
1399
  const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
1241
1400
  const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
1242
1401
  const meters = [];
1243
1402
  for (let index = firstReadable; index < writeIndex; index++) {
1244
- const offset = index % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1403
+ const offset = index % ring.capacity * recordFloats;
1245
1404
  meters.push({
1246
1405
  type: "meter",
1247
- frame: decodeFrame(ring.records[offset], ring.records[offset + 6]),
1248
- peakDbL: ring.records[offset + 1],
1249
- peakDbR: ring.records[offset + 2],
1250
- rmsDbL: ring.records[offset + 3],
1251
- rmsDbR: ring.records[offset + 4],
1252
- correlation: ring.records[offset + 5]
1406
+ frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
1407
+ targetId: ring.records[offset + 2],
1408
+ peakDbL: ring.records[offset + 3],
1409
+ peakDbR: ring.records[offset + 4],
1410
+ rmsDbL: ring.records[offset + 5],
1411
+ rmsDbR: ring.records[offset + 6],
1412
+ correlation: ring.records[offset + 7],
1413
+ truePeakDbL: ring.records[offset + 8],
1414
+ truePeakDbR: ring.records[offset + 9],
1415
+ momentaryLufs: ring.records[offset + 10],
1416
+ shortTermLufs: ring.records[offset + 11],
1417
+ integratedLufs: ring.records[offset + 12],
1418
+ gainReductionDb: ring.records[offset + 13]
1253
1419
  });
1254
1420
  }
1255
1421
  return { nextReadIndex: writeIndex, meters };
@@ -1509,12 +1675,19 @@ function telemetryFromEngine(telemetry) {
1509
1675
  function meterFromEngine(meter) {
1510
1676
  return {
1511
1677
  type: "meter",
1678
+ targetId: meter.targetId,
1512
1679
  frame: meter.renderFrame,
1513
1680
  peakDbL: meter.peakDbL,
1514
1681
  peakDbR: meter.peakDbR,
1515
1682
  rmsDbL: meter.rmsDbL,
1516
1683
  rmsDbR: meter.rmsDbR,
1517
- correlation: meter.correlation
1684
+ correlation: meter.correlation,
1685
+ truePeakDbL: meter.truePeakDbL,
1686
+ truePeakDbR: meter.truePeakDbR,
1687
+ momentaryLufs: meter.momentaryLufs,
1688
+ shortTermLufs: meter.shortTermLufs,
1689
+ integratedLufs: meter.integratedLufs,
1690
+ gainReductionDb: meter.gainReductionDb
1518
1691
  };
1519
1692
  }
1520
1693
  function magnitudeToDb(value) {
@@ -1669,12 +1842,19 @@ var SonareWorkletProcessor = class {
1669
1842
  const denominator = Math.sqrt(sumL * sumR);
1670
1843
  const meter = {
1671
1844
  type: "meter",
1845
+ targetId: 0,
1672
1846
  frame: this.processedFrames,
1673
1847
  peakDbL: toDb(peakL),
1674
1848
  peakDbR: toDb(peakR),
1675
1849
  rmsDbL: toDb(rmsL),
1676
1850
  rmsDbR: toDb(rmsR),
1677
- correlation: denominator > 0 ? sumLR / denominator : 0
1851
+ correlation: denominator > 0 ? sumLR / denominator : 0,
1852
+ truePeakDbL: toDb(peakL),
1853
+ truePeakDbR: toDb(peakR),
1854
+ momentaryLufs: Number.NaN,
1855
+ shortTermLufs: Number.NaN,
1856
+ integratedLufs: Number.NaN,
1857
+ gainReductionDb: Number.NaN
1678
1858
  };
1679
1859
  this.transport.onMeter?.(meter);
1680
1860
  if (this.meterRing) {
@@ -1691,12 +1871,19 @@ var SonareWorkletProcessor = class {
1691
1871
  const writeIndex = Atomics.load(ring.header, 0);
1692
1872
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1693
1873
  ring.records[offset] = encodeFrameLo(meter.frame);
1694
- ring.records[offset + 1] = meter.peakDbL;
1695
- ring.records[offset + 2] = meter.peakDbR;
1696
- ring.records[offset + 3] = meter.rmsDbL;
1697
- ring.records[offset + 4] = meter.rmsDbR;
1698
- ring.records[offset + 5] = meter.correlation;
1699
- ring.records[offset + 6] = encodeFrameHi(meter.frame);
1874
+ ring.records[offset + 1] = encodeFrameHi(meter.frame);
1875
+ ring.records[offset + 2] = meter.targetId;
1876
+ ring.records[offset + 3] = meter.peakDbL;
1877
+ ring.records[offset + 4] = meter.peakDbR;
1878
+ ring.records[offset + 5] = meter.rmsDbL;
1879
+ ring.records[offset + 6] = meter.rmsDbR;
1880
+ ring.records[offset + 7] = meter.correlation;
1881
+ ring.records[offset + 8] = meter.truePeakDbL;
1882
+ ring.records[offset + 9] = meter.truePeakDbR;
1883
+ ring.records[offset + 10] = meter.momentaryLufs;
1884
+ ring.records[offset + 11] = meter.shortTermLufs;
1885
+ ring.records[offset + 12] = meter.integratedLufs;
1886
+ ring.records[offset + 13] = meter.gainReductionDb;
1700
1887
  Atomics.store(ring.header, 0, writeIndex + 1);
1701
1888
  }
1702
1889
  publishSpectrum(left, right) {
@@ -1761,6 +1948,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1761
1948
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
1762
1949
  // SetMetronome command only toggles enabled state; the config arrives here.
1763
1950
  this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
1951
+ this.liveClips = /* @__PURE__ */ new Map();
1764
1952
  this.sampleRate = options.sampleRate ?? 48e3;
1765
1953
  this.blockSize = options.blockSize ?? 128;
1766
1954
  this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
@@ -1864,8 +2052,28 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1864
2052
  }
1865
2053
  switch (message.type) {
1866
2054
  case "syncClips":
2055
+ this.liveClips.clear();
2056
+ for (const clip of message.clips) {
2057
+ if (clip.id !== void 0) {
2058
+ this.liveClips.set(clip.id, clip);
2059
+ }
2060
+ }
1867
2061
  this.engine.setClips(message.clips);
1868
2062
  break;
2063
+ case "syncClipsDelta":
2064
+ for (const clipId of message.removeIds) {
2065
+ this.liveClips.delete(clipId);
2066
+ }
2067
+ for (const clip of message.upserts) {
2068
+ if (clip.id !== void 0) {
2069
+ this.liveClips.set(clip.id, clip);
2070
+ }
2071
+ }
2072
+ this.engine.setClips(Array.from(this.liveClips.values()));
2073
+ break;
2074
+ case "syncMidiClips":
2075
+ this.engine.setMidiClips(message.clips);
2076
+ break;
1869
2077
  case "syncMarkers":
1870
2078
  this.engine.setMarkers(message.markers);
1871
2079
  break;
@@ -1876,6 +2084,184 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1876
2084
  case "syncAutomation":
1877
2085
  this.engine.setAutomationLane(message.paramId, message.points);
1878
2086
  break;
2087
+ case "syncTempo":
2088
+ if (message.tempoSegments) {
2089
+ this.engine.setTempoSegments(message.tempoSegments);
2090
+ } else {
2091
+ this.engine.setTempo(message.bpm);
2092
+ }
2093
+ if (message.timeSignatureSegments) {
2094
+ this.engine.setTimeSignatureSegments(message.timeSignatureSegments);
2095
+ } else {
2096
+ this.engine.setTimeSignature(
2097
+ message.timeSignature.numerator,
2098
+ message.timeSignature.denominator
2099
+ );
2100
+ }
2101
+ break;
2102
+ case "syncMixer":
2103
+ if (message.buses) {
2104
+ this.engine.setTrackBuses(message.buses);
2105
+ }
2106
+ this.engine.setTrackLanes(message.lanes);
2107
+ for (const strip of message.trackStrips ?? []) {
2108
+ this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
2109
+ }
2110
+ for (const strip of message.busStrips ?? []) {
2111
+ this.engine.setBusStripJson(strip.busId, strip.sceneJson);
2112
+ }
2113
+ if (message.masterStripJson) {
2114
+ this.engine.setMasterStripJson(message.masterStripJson);
2115
+ }
2116
+ break;
2117
+ case "syncCapture":
2118
+ this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
2119
+ this.engine.setCaptureSource(message.source);
2120
+ this.engine.setRecordOffsetSamples(message.recordOffsetSamples);
2121
+ this.engine.setInputMonitor(message.inputMonitor.enabled, message.inputMonitor.gain);
2122
+ break;
2123
+ case "syncTrackStripEqBand":
2124
+ this.engine.setTrackStripEqBandJson(message.trackId, message.bandIndex, message.bandJson);
2125
+ break;
2126
+ case "syncMasterStripEqBand":
2127
+ this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
2128
+ break;
2129
+ case "syncTrackStripInsertBypassed":
2130
+ this.engine.setTrackStripInsertBypassed(
2131
+ message.trackId,
2132
+ message.insertIndex,
2133
+ message.bypassed,
2134
+ message.resetOnBypass
2135
+ );
2136
+ break;
2137
+ case "syncMasterStripInsertBypassed":
2138
+ this.engine.setMasterStripInsertBypassed(
2139
+ message.insertIndex,
2140
+ message.bypassed,
2141
+ message.resetOnBypass
2142
+ );
2143
+ break;
2144
+ case "syncBuiltinInstrument":
2145
+ this.engine.setBuiltinInstrument(message.config, message.destinationId);
2146
+ break;
2147
+ case "syncSynthInstrument":
2148
+ this.engine.setSynthInstrument(message.patch, message.destinationId);
2149
+ break;
2150
+ case "syncLoadSoundFont":
2151
+ this.engine.loadSoundFont(message.data);
2152
+ break;
2153
+ case "syncSf2Instrument":
2154
+ this.engine.setSf2Instrument(message.config, message.destinationId);
2155
+ break;
2156
+ case "syncMidiNoteOn":
2157
+ this.engine.pushMidiNoteOn(
2158
+ message.destinationId,
2159
+ message.group,
2160
+ message.channel,
2161
+ message.note,
2162
+ message.velocity,
2163
+ message.renderFrame
2164
+ );
2165
+ break;
2166
+ case "syncMidiNoteOff":
2167
+ this.engine.pushMidiNoteOff(
2168
+ message.destinationId,
2169
+ message.group,
2170
+ message.channel,
2171
+ message.note,
2172
+ message.velocity,
2173
+ message.renderFrame
2174
+ );
2175
+ break;
2176
+ case "syncMidiCc":
2177
+ this.engine.pushMidiCc(
2178
+ message.destinationId,
2179
+ message.group,
2180
+ message.channel,
2181
+ message.controller,
2182
+ message.value,
2183
+ message.renderFrame
2184
+ );
2185
+ break;
2186
+ case "syncMidiPanic":
2187
+ this.engine.pushMidiPanic(message.renderFrame);
2188
+ break;
2189
+ }
2190
+ }
2191
+ receiveCaptureRequest(message) {
2192
+ if (this.closed) {
2193
+ return;
2194
+ }
2195
+ try {
2196
+ if (message.op === "status") {
2197
+ const status = this.engine.captureStatus();
2198
+ this.transport?.postMessage?.({
2199
+ type: "captureResponse",
2200
+ requestId: message.requestId,
2201
+ ok: true,
2202
+ status: {
2203
+ capturedFrames: status.capturedFrames,
2204
+ overflowCount: status.overflowCount,
2205
+ armed: status.armed,
2206
+ punchEnabled: status.punchEnabled,
2207
+ source: status.source,
2208
+ recordOffsetSamples: status.recordOffsetSamples
2209
+ }
2210
+ });
2211
+ return;
2212
+ }
2213
+ if (message.op === "read") {
2214
+ const captured = this.engine.capturedAudio();
2215
+ const channels = [];
2216
+ for (let ch = 0; ch < captured.length; ch++) {
2217
+ const source = captured[ch];
2218
+ const copy = [];
2219
+ for (let i = 0; i < source.length; i++) {
2220
+ copy.push(Number(source[i]));
2221
+ }
2222
+ channels.push(copy);
2223
+ }
2224
+ this.transport?.postMessage?.({
2225
+ type: "captureResponse",
2226
+ requestId: message.requestId,
2227
+ ok: true,
2228
+ channels
2229
+ });
2230
+ return;
2231
+ }
2232
+ this.engine.resetCapture();
2233
+ this.transport?.postMessage?.({
2234
+ type: "captureResponse",
2235
+ requestId: message.requestId,
2236
+ ok: true
2237
+ });
2238
+ } catch (error) {
2239
+ this.transport?.postMessage?.({
2240
+ type: "captureResponse",
2241
+ requestId: message.requestId,
2242
+ ok: false,
2243
+ error: error instanceof Error ? error.message : String(error)
2244
+ });
2245
+ }
2246
+ }
2247
+ receiveTransportRequest(message) {
2248
+ if (this.closed) {
2249
+ return;
2250
+ }
2251
+ try {
2252
+ this.transport?.postMessage?.({
2253
+ type: "transportResponse",
2254
+ requestId: message.requestId,
2255
+ ok: true,
2256
+ state: this.engine.getTransportState()
2257
+ });
2258
+ } catch (error) {
2259
+ this.transport?.postMessage?.({
2260
+ type: "transportResponse",
2261
+ requestId: message.requestId,
2262
+ ok: false,
2263
+ error: error instanceof Error ? error.message : String(error)
2264
+ });
1879
2265
  }
1880
2266
  }
1881
2267
  destroy() {
@@ -1956,6 +2342,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1956
2342
  case 17 /* SeekMarker */:
1957
2343
  this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
1958
2344
  break;
2345
+ case 10 /* SetSoloMute */:
2346
+ this.engine.setSoloMute(
2347
+ Math.trunc(Number(command.targetId ?? 0)),
2348
+ Boolean((Number(command.argInt ?? 0) & 2) !== 0),
2349
+ Boolean((Number(command.argInt ?? 0) & 1) !== 0),
2350
+ sampleTime
2351
+ );
2352
+ break;
1959
2353
  default:
1960
2354
  this.publishTelemetryRecord({
1961
2355
  type: 1 /* Error */,
@@ -1987,10 +2381,12 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1987
2381
  }
1988
2382
  for (const item of this.engine.drainMeterTelemetry(64)) {
1989
2383
  const meter = meterFromEngine(item);
1990
- if (meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
2384
+ if (meter.frame !== this.lastMeterFrame && meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
1991
2385
  continue;
1992
2386
  }
1993
- this.lastMeterFrame = meter.frame;
2387
+ if (meter.frame !== this.lastMeterFrame) {
2388
+ this.lastMeterFrame = meter.frame;
2389
+ }
1994
2390
  if (this.meterRing) {
1995
2391
  this.writeMeterRing(meter);
1996
2392
  } else {
@@ -2007,12 +2403,19 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
2007
2403
  const writeIndex = Atomics.load(ring.header, 0);
2008
2404
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
2009
2405
  ring.records[offset] = encodeFrameLo(meter.frame);
2010
- ring.records[offset + 1] = meter.peakDbL;
2011
- ring.records[offset + 2] = meter.peakDbR;
2012
- ring.records[offset + 3] = meter.rmsDbL;
2013
- ring.records[offset + 4] = meter.rmsDbR;
2014
- ring.records[offset + 5] = meter.correlation;
2015
- ring.records[offset + 6] = encodeFrameHi(meter.frame);
2406
+ ring.records[offset + 1] = encodeFrameHi(meter.frame);
2407
+ ring.records[offset + 2] = meter.targetId;
2408
+ ring.records[offset + 3] = meter.peakDbL;
2409
+ ring.records[offset + 4] = meter.peakDbR;
2410
+ ring.records[offset + 5] = meter.rmsDbL;
2411
+ ring.records[offset + 6] = meter.rmsDbR;
2412
+ ring.records[offset + 7] = meter.correlation;
2413
+ ring.records[offset + 8] = meter.truePeakDbL;
2414
+ ring.records[offset + 9] = meter.truePeakDbR;
2415
+ ring.records[offset + 10] = meter.momentaryLufs;
2416
+ ring.records[offset + 11] = meter.shortTermLufs;
2417
+ ring.records[offset + 12] = meter.integratedLufs;
2418
+ ring.records[offset + 13] = meter.gainReductionDb;
2016
2419
  Atomics.store(ring.header, 0, writeIndex + 1);
2017
2420
  }
2018
2421
  commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
@@ -2144,9 +2547,28 @@ var SonareRtRealtimeEngineRuntime = class {
2144
2547
  this.metronomeConfig.clickSamples
2145
2548
  );
2146
2549
  break;
2550
+ case "syncTempo":
2551
+ this.module._sonare_rt_engine_set_tempo(this.engine, message.bpm);
2552
+ break;
2147
2553
  case "syncClips":
2554
+ case "syncClipsDelta":
2555
+ case "syncMidiClips":
2148
2556
  case "syncMarkers":
2149
2557
  case "syncAutomation":
2558
+ case "syncMixer":
2559
+ case "syncCapture":
2560
+ case "syncTrackStripEqBand":
2561
+ case "syncMasterStripEqBand":
2562
+ case "syncTrackStripInsertBypassed":
2563
+ case "syncMasterStripInsertBypassed":
2564
+ case "syncBuiltinInstrument":
2565
+ case "syncSynthInstrument":
2566
+ case "syncSf2Instrument":
2567
+ case "syncLoadSoundFont":
2568
+ case "syncMidiNoteOn":
2569
+ case "syncMidiNoteOff":
2570
+ case "syncMidiCc":
2571
+ case "syncMidiPanic":
2150
2572
  if (this.telemetryRing) {
2151
2573
  writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
2152
2574
  type: 1 /* Error */,
@@ -2161,6 +2583,28 @@ var SonareRtRealtimeEngineRuntime = class {
2161
2583
  break;
2162
2584
  }
2163
2585
  }
2586
+ receiveCaptureRequest(message, port) {
2587
+ if (this.closed) {
2588
+ return;
2589
+ }
2590
+ port?.postMessage?.({
2591
+ type: "captureResponse",
2592
+ requestId: message.requestId,
2593
+ ok: false,
2594
+ error: "Capture read-back is not supported by the sonare-rt runtime."
2595
+ });
2596
+ }
2597
+ receiveTransportRequest(message, port) {
2598
+ if (this.closed) {
2599
+ return;
2600
+ }
2601
+ port?.postMessage?.({
2602
+ type: "transportResponse",
2603
+ requestId: message.requestId,
2604
+ ok: false,
2605
+ error: "Transport state read-back is not supported by the sonare-rt runtime."
2606
+ });
2607
+ }
2164
2608
  destroy() {
2165
2609
  if (this.closed) {
2166
2610
  return;
@@ -2338,6 +2782,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2338
2782
  this.meterReadIndex = 0;
2339
2783
  this.telemetryListeners = /* @__PURE__ */ new Set();
2340
2784
  this.meterListeners = /* @__PURE__ */ new Set();
2785
+ this.captureRequestId = 1;
2786
+ this.captureRequests = /* @__PURE__ */ new Map();
2787
+ this.transportRequestId = 1;
2788
+ this.transportRequests = /* @__PURE__ */ new Map();
2341
2789
  this.destroyed = false;
2342
2790
  this.node = node;
2343
2791
  this.capabilities = capabilities;
@@ -2348,11 +2796,31 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2348
2796
  this.resolveReady = resolve;
2349
2797
  this.rejectReady = reject;
2350
2798
  });
2351
- if (capabilities.runtimeTarget !== "sonare-rt") {
2799
+ if (!capabilities.readyMessage) {
2352
2800
  this.resolveReady();
2353
2801
  }
2354
2802
  this.node.port.onmessage = (event) => {
2355
- if (isEngineTelemetryRecord(event.data)) {
2803
+ if (isEngineCaptureResponseMessage(event.data)) {
2804
+ const pending = this.captureRequests.get(event.data.requestId);
2805
+ if (pending) {
2806
+ this.captureRequests.delete(event.data.requestId);
2807
+ if (event.data.ok) {
2808
+ pending.resolve(event.data);
2809
+ } else {
2810
+ pending.reject(new Error(event.data.error ?? "Capture request failed"));
2811
+ }
2812
+ }
2813
+ } else if (isEngineTransportResponseMessage(event.data)) {
2814
+ const pending = this.transportRequests.get(event.data.requestId);
2815
+ if (pending) {
2816
+ this.transportRequests.delete(event.data.requestId);
2817
+ if (event.data.ok) {
2818
+ pending.resolve(event.data);
2819
+ } else {
2820
+ pending.reject(new Error(event.data.error ?? "Transport request failed"));
2821
+ }
2822
+ }
2823
+ } else if (isEngineTelemetryRecord(event.data)) {
2356
2824
  this.emitTelemetry(event.data);
2357
2825
  } else if (isMeterSnapshot(event.data)) {
2358
2826
  this.emitMeter(event.data);
@@ -2406,7 +2874,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2406
2874
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
2407
2875
  telemetryRingCapacity: telemetryRing?.capacity,
2408
2876
  meterSharedBuffer: meterRing?.sharedBuffer,
2409
- meterRingCapacity: meterRing?.capacity
2877
+ meterRingCapacity: meterRing?.capacity,
2878
+ wasmBinary: options.wasmBinary,
2879
+ initialSyncMessages: options.initialSyncMessages,
2880
+ initialCommands: options.initialCommands
2410
2881
  };
2411
2882
  const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
2412
2883
  const node = factory(context, processorName, {
@@ -2426,7 +2897,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2426
2897
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
2427
2898
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
2428
2899
  abiCompatible: detectedCapabilities?.abiCompatible,
2429
- degradedReason
2900
+ degradedReason,
2901
+ readyMessage: runtimeTarget === "sonare-rt" || runtimeTarget === "embind" && moduleUrl !== void 0 && !options.nodeFactory
2430
2902
  },
2431
2903
  commandRing,
2432
2904
  telemetryRing,
@@ -2463,6 +2935,32 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2463
2935
  this.node.port.postMessage(command);
2464
2936
  return true;
2465
2937
  }
2938
+ requestCaptureStatus() {
2939
+ return this.sendCaptureRequest("status").then((response) => {
2940
+ if (!response.status) {
2941
+ throw new Error("Capture status response is missing status.");
2942
+ }
2943
+ return response.status;
2944
+ });
2945
+ }
2946
+ requestCapturedAudio() {
2947
+ return this.sendCaptureRequest("read").then(
2948
+ (response) => (response.channels ?? []).map(
2949
+ (channel) => channel instanceof Float32Array ? channel : new Float32Array(channel)
2950
+ )
2951
+ );
2952
+ }
2953
+ requestCaptureReset() {
2954
+ return this.sendCaptureRequest("reset").then(() => void 0);
2955
+ }
2956
+ requestTransportState() {
2957
+ return this.sendTransportRequest().then((response) => {
2958
+ if (!response.state) {
2959
+ throw new Error("Transport state response is missing state.");
2960
+ }
2961
+ return response.state;
2962
+ });
2963
+ }
2466
2964
  pollTelemetry() {
2467
2965
  if (!this.telemetryRing) {
2468
2966
  return [];
@@ -2507,6 +3005,14 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2507
3005
  this.destroyed = true;
2508
3006
  this.node.port.postMessage({ type: 3 /* TransportStop */, sampleTime: -1 });
2509
3007
  this.node.disconnect();
3008
+ for (const pending of this.captureRequests.values()) {
3009
+ pending.reject(new Error("Realtime engine node is destroyed."));
3010
+ }
3011
+ this.captureRequests.clear();
3012
+ for (const pending of this.transportRequests.values()) {
3013
+ pending.reject(new Error("Realtime engine node is destroyed."));
3014
+ }
3015
+ this.transportRequests.clear();
2510
3016
  this.telemetryListeners.clear();
2511
3017
  this.meterListeners.clear();
2512
3018
  }
@@ -2520,14 +3026,50 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2520
3026
  listener(meter);
2521
3027
  }
2522
3028
  }
3029
+ sendCaptureRequest(op) {
3030
+ if (this.destroyed) {
3031
+ return Promise.reject(new Error("Realtime engine node is destroyed."));
3032
+ }
3033
+ const requestId = this.captureRequestId++;
3034
+ const promise = new Promise((resolve, reject) => {
3035
+ this.captureRequests.set(requestId, { resolve, reject });
3036
+ });
3037
+ this.node.port.postMessage({ type: "captureRequest", requestId, op });
3038
+ return promise;
3039
+ }
3040
+ sendTransportRequest() {
3041
+ if (this.destroyed) {
3042
+ return Promise.reject(new Error("Realtime engine node is destroyed."));
3043
+ }
3044
+ const requestId = this.transportRequestId++;
3045
+ const promise = new Promise((resolve, reject) => {
3046
+ this.transportRequests.set(requestId, { resolve, reject });
3047
+ });
3048
+ this.node.port.postMessage({ type: "transportRequest", requestId, op: "state" });
3049
+ return promise;
3050
+ }
2523
3051
  };
2524
3052
  var SonareEngine = class _SonareEngine {
2525
3053
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
2526
3054
  this.automationLanes = /* @__PURE__ */ new Map();
2527
3055
  this.clips = /* @__PURE__ */ new Map();
3056
+ this.midiClips = /* @__PURE__ */ new Map();
2528
3057
  this.markers = /* @__PURE__ */ new Map();
3058
+ this.trackLaneIds = [];
3059
+ this.trackSends = /* @__PURE__ */ new Map();
3060
+ this.buses = [];
3061
+ this.trackStripJson = /* @__PURE__ */ new Map();
3062
+ this.busStripJson = /* @__PURE__ */ new Map();
3063
+ this.tempoBpm = 120;
3064
+ this.timeSignature = { numerator: 4, denominator: 4 };
3065
+ this.tempoSegments = [{ startPpq: 0, bpm: 120 }];
3066
+ this.timeSignatureSegments = [
3067
+ { startPpq: 0, numerator: 4, denominator: 4 }
3068
+ ];
2529
3069
  this.nextClipId = 1;
2530
3070
  this.nextMarkerId = 1;
3071
+ this.transportPlaying = false;
3072
+ this.pendingInstrumentSync = [];
2531
3073
  this.destroyed = false;
2532
3074
  this.context = context;
2533
3075
  this.realtimeNode = realtimeNode;
@@ -2538,8 +3080,21 @@ var SonareEngine = class _SonareEngine {
2538
3080
  this.offlineBlockSize = offlineBlockSize;
2539
3081
  this.offlineChannelCount = offlineChannelCount;
2540
3082
  this.transport = {
2541
- play: (sampleTime = -1) => this.realtimeNode.play(sampleTime),
2542
- stop: (sampleTime = -1) => this.realtimeNode.stop(sampleTime),
3083
+ play: (sampleTime = -1) => {
3084
+ const ok = this.realtimeNode.play(sampleTime);
3085
+ if (ok) {
3086
+ this.transportPlaying = true;
3087
+ }
3088
+ return ok;
3089
+ },
3090
+ stop: (sampleTime = -1) => {
3091
+ const ok = this.realtimeNode.stop(sampleTime);
3092
+ if (ok) {
3093
+ this.transportPlaying = false;
3094
+ this.flushPendingInstrumentSync();
3095
+ }
3096
+ return ok;
3097
+ },
2543
3098
  seekPpq: (ppq, sampleTime = -1) => {
2544
3099
  this.offlineEngine.seekPpq(ppq, sampleTime);
2545
3100
  return this.realtimeNode.seekPpq(ppq, sampleTime);
@@ -2550,6 +3105,7 @@ var SonareEngine = class _SonareEngine {
2550
3105
  return this.realtimeNode.seekSample(timelineSample, sampleTime);
2551
3106
  },
2552
3107
  setTempo: (bpm) => this.setTempo(bpm),
3108
+ setTempoSegments: (segments) => this.setTempoSegments(segments),
2553
3109
  setLoop: (startPpq, endPpq, enabled = true) => this.setLoop(startPpq, endPpq, enabled)
2554
3110
  };
2555
3111
  }
@@ -2584,13 +3140,37 @@ var SonareEngine = class _SonareEngine {
2584
3140
  await this.context.resume?.();
2585
3141
  }
2586
3142
  setTempo(bpm) {
3143
+ this.tempoBpm = bpm;
3144
+ this.tempoSegments = [{ startPpq: 0, bpm }];
2587
3145
  this.offlineEngine.setTempo(bpm);
3146
+ this.postTempoSync();
2588
3147
  this.realtimeNode.sendCommand({
2589
3148
  type: 6 /* SetTempoMap */,
2590
3149
  sampleTime: -1,
2591
3150
  argFloat: bpm
2592
3151
  });
2593
3152
  }
3153
+ setTempoSegments(segments) {
3154
+ this.tempoSegments = segments.map((segment) => ({ ...segment }));
3155
+ this.tempoBpm = this.tempoSegments[0]?.bpm ?? this.tempoBpm;
3156
+ this.offlineEngine.setTempoSegments(this.tempoSegments);
3157
+ this.postTempoSync();
3158
+ }
3159
+ setTimeSignature(numerator, denominator) {
3160
+ this.timeSignature = { numerator, denominator };
3161
+ this.timeSignatureSegments = [{ startPpq: 0, numerator, denominator }];
3162
+ this.offlineEngine.setTimeSignature(numerator, denominator);
3163
+ this.postTempoSync();
3164
+ }
3165
+ setTimeSignatureSegments(segments) {
3166
+ this.timeSignatureSegments = segments.map((segment) => ({ ...segment }));
3167
+ const first = this.timeSignatureSegments[0];
3168
+ if (first) {
3169
+ this.timeSignature = { numerator: first.numerator, denominator: first.denominator };
3170
+ }
3171
+ this.offlineEngine.setTimeSignatureSegments(this.timeSignatureSegments);
3172
+ this.postTempoSync();
3173
+ }
2594
3174
  setLoop(startPpq, endPpq, enabled = true) {
2595
3175
  this.offlineEngine.setLoop(startPpq, endPpq, enabled);
2596
3176
  return this.realtimeNode.sendCommand({
@@ -2601,6 +3181,17 @@ var SonareEngine = class _SonareEngine {
2601
3181
  argInt: Math.round(endPpq * 1e6)
2602
3182
  });
2603
3183
  }
3184
+ countInEndSample(startSample, bars) {
3185
+ return this.offlineEngine.countInEndSample(startSample, bars);
3186
+ }
3187
+ async getTransportState() {
3188
+ const state = await this.realtimeNode.requestTransportState();
3189
+ this.latestTransportState = state;
3190
+ return state;
3191
+ }
3192
+ cachedTransportState() {
3193
+ return this.latestTransportState;
3194
+ }
2604
3195
  setParam(nodeId, param, value) {
2605
3196
  const paramId = this.resolveParamId(nodeId, param);
2606
3197
  this.offlineEngine.setParameter(paramId, value);
@@ -2631,12 +3222,198 @@ var SonareEngine = class _SonareEngine {
2631
3222
  return parameters;
2632
3223
  }
2633
3224
  setSoloMute(target, solo, mute) {
2634
- void target;
2635
- void solo;
2636
- void mute;
2637
- throw new Error(
2638
- "SonareEngine.setSoloMute is not supported: solo/mute is a Mixer feature; use Mixer.setSoloed(stripIndex, ...) / Mixer.setMuted(stripIndex, ...) instead."
3225
+ const laneIndex = this.ensureTrackLane(target);
3226
+ this.offlineEngine.setSoloMute(laneIndex, solo, mute);
3227
+ return this.realtimeNode.sendCommand({
3228
+ type: 10 /* SetSoloMute */,
3229
+ targetId: laneIndex,
3230
+ sampleTime: -1,
3231
+ argInt: (mute ? 1 : 0) | (solo ? 2 : 0)
3232
+ });
3233
+ }
3234
+ setStripGain(target, db) {
3235
+ if (target === "master") {
3236
+ const paramId2 = engineMixerMasterTarget(ENGINE_MIXER_PARAM_FADER_DB);
3237
+ this.offlineEngine.setParameter(paramId2, db);
3238
+ return this.realtimeNode.sendCommand({
3239
+ type: 1 /* SetParamSmoothed */,
3240
+ targetId: paramId2,
3241
+ sampleTime: -1,
3242
+ argFloat: db
3243
+ });
3244
+ }
3245
+ const laneIndex = this.ensureTrackLane(target);
3246
+ const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_FADER_DB);
3247
+ this.offlineEngine.setParameter(paramId, db);
3248
+ return this.realtimeNode.sendCommand({
3249
+ type: 1 /* SetParamSmoothed */,
3250
+ targetId: paramId,
3251
+ sampleTime: -1,
3252
+ argFloat: db
3253
+ });
3254
+ }
3255
+ setStripPan(target, pan) {
3256
+ if (target === "master") {
3257
+ const paramId2 = engineMixerMasterTarget(ENGINE_MIXER_PARAM_PAN);
3258
+ this.offlineEngine.setParameter(paramId2, pan);
3259
+ return this.realtimeNode.sendCommand({
3260
+ type: 1 /* SetParamSmoothed */,
3261
+ targetId: paramId2,
3262
+ sampleTime: -1,
3263
+ argFloat: pan
3264
+ });
3265
+ }
3266
+ const laneIndex = this.ensureTrackLane(target);
3267
+ const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_PAN);
3268
+ this.offlineEngine.setParameter(paramId, pan);
3269
+ return this.realtimeNode.sendCommand({
3270
+ type: 1 /* SetParamSmoothed */,
3271
+ targetId: paramId,
3272
+ sampleTime: -1,
3273
+ argFloat: pan
3274
+ });
3275
+ }
3276
+ /**
3277
+ * Declares the mixer track lanes in an explicit order.
3278
+ *
3279
+ * Lane indices are append-only: once a track id occupies a lane, its index
3280
+ * stays fixed for the engine's lifetime. The given list must therefore start
3281
+ * with the already-declared lane ids in their current order and may only
3282
+ * append new track ids after them. Entries carrying `sends` replace that
3283
+ * track's send list; entries without `sends` leave existing sends untouched.
3284
+ *
3285
+ * @param lanes Track ids or lane descriptors in the desired lane order.
3286
+ */
3287
+ setTrackLanes(lanes) {
3288
+ const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
3289
+ const ids = [];
3290
+ for (const entry of entries) {
3291
+ if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
3292
+ throw new Error(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
3293
+ }
3294
+ ids.push(entry.trackId);
3295
+ }
3296
+ if (new Set(ids).size !== ids.length) {
3297
+ throw new Error("Duplicate track id in mixer lane list");
3298
+ }
3299
+ for (let index = 0; index < this.trackLaneIds.length; index++) {
3300
+ if (ids[index] !== this.trackLaneIds[index]) {
3301
+ throw new Error(
3302
+ "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
3303
+ );
3304
+ }
3305
+ }
3306
+ for (const entry of entries) {
3307
+ if (entry.sends) {
3308
+ this.trackSends.set(
3309
+ entry.trackId,
3310
+ entry.sends.map((send) => ({ ...send }))
3311
+ );
3312
+ }
3313
+ }
3314
+ this.trackLaneIds.splice(0, this.trackLaneIds.length, ...ids);
3315
+ this.syncMixer();
3316
+ }
3317
+ setSends(target, sends) {
3318
+ const laneIndex = this.ensureTrackLane(target);
3319
+ const trackId = this.trackLaneIds[laneIndex];
3320
+ this.trackSends.set(
3321
+ trackId,
3322
+ sends.map((send) => ({ ...send }))
2639
3323
  );
3324
+ this.syncMixer();
3325
+ }
3326
+ setTrackBuses(buses) {
3327
+ this.buses.splice(0, this.buses.length, ...buses.map((bus) => ({ ...bus })));
3328
+ this.syncMixer();
3329
+ }
3330
+ setBusGain(busId, db) {
3331
+ const busIndex = this.ensureBus(busId);
3332
+ this.buses[busIndex] = { ...this.buses[busIndex], busId, gainDb: db };
3333
+ this.offlineEngine.setTrackBuses(this.buses);
3334
+ const paramId = engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB);
3335
+ this.offlineEngine.setParameter(paramId, db);
3336
+ return this.realtimeNode.sendCommand({
3337
+ type: 1 /* SetParamSmoothed */,
3338
+ targetId: paramId,
3339
+ sampleTime: -1,
3340
+ argFloat: db
3341
+ });
3342
+ }
3343
+ setTrackStripJson(target, sceneJson) {
3344
+ const laneIndex = this.ensureTrackLane(target);
3345
+ const trackId = this.trackLaneIds[laneIndex];
3346
+ this.offlineEngine.setTrackStripJson(trackId, sceneJson);
3347
+ this.trackStripJson.set(trackId, sceneJson);
3348
+ this.syncMixer();
3349
+ }
3350
+ setTrackStripEqBand(target, bandIndex, band) {
3351
+ const laneIndex = this.ensureTrackLane(target);
3352
+ const trackId = this.trackLaneIds[laneIndex];
3353
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3354
+ this.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
3355
+ this.postSync({ type: "syncTrackStripEqBand", trackId, bandIndex, bandJson });
3356
+ }
3357
+ setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass = false) {
3358
+ const laneIndex = this.ensureTrackLane(target);
3359
+ const trackId = this.trackLaneIds[laneIndex];
3360
+ this.offlineEngine.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
3361
+ this.postSync({
3362
+ type: "syncTrackStripInsertBypassed",
3363
+ trackId,
3364
+ insertIndex,
3365
+ bypassed,
3366
+ resetOnBypass
3367
+ });
3368
+ }
3369
+ setStripEq(target, bandIndex, band) {
3370
+ if (target === "master") {
3371
+ this.setMasterStripEqBand(bandIndex, band);
3372
+ return;
3373
+ }
3374
+ this.setTrackStripEqBand(target, bandIndex, band);
3375
+ }
3376
+ setStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass = false) {
3377
+ if (target === "master") {
3378
+ this.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
3379
+ return;
3380
+ }
3381
+ this.setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass);
3382
+ }
3383
+ setStripInserts(target, sceneJson) {
3384
+ if (target === "master") {
3385
+ this.setMasterStripJson(sceneJson);
3386
+ return;
3387
+ }
3388
+ this.setTrackStripJson(target, sceneJson);
3389
+ }
3390
+ setBusStripJson(busId, sceneJson) {
3391
+ this.ensureBus(busId);
3392
+ this.offlineEngine.setBusStripJson(busId, sceneJson);
3393
+ this.busStripJson.set(busId, sceneJson);
3394
+ this.syncMixer();
3395
+ }
3396
+ setMasterStripJson(sceneJson) {
3397
+ this.offlineEngine.setMasterStripJson(sceneJson);
3398
+ this.masterStripJson = sceneJson;
3399
+ this.syncMixer();
3400
+ }
3401
+ setMasterStripEqBand(bandIndex, band) {
3402
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3403
+ this.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
3404
+ this.postSync({ type: "syncMasterStripEqBand", bandIndex, bandJson });
3405
+ }
3406
+ setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass = false) {
3407
+ this.offlineEngine.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
3408
+ this.postSync({
3409
+ type: "syncMasterStripInsertBypassed",
3410
+ insertIndex,
3411
+ bypassed,
3412
+ resetOnBypass
3413
+ });
3414
+ }
3415
+ setMasterChain(sceneJson) {
3416
+ this.setMasterStripJson(sceneJson);
2640
3417
  }
2641
3418
  addClip(trackId, buffer, startPpq, opts = {}) {
2642
3419
  const id = opts.id ?? this.nextClipId++;
@@ -2644,18 +3421,108 @@ var SonareEngine = class _SonareEngine {
2644
3421
  ...opts,
2645
3422
  id,
2646
3423
  channels: buffer,
2647
- startPpq
3424
+ startPpq,
3425
+ trackId: this.resolveTargetId(trackId)
2648
3426
  };
3427
+ this.ensureTrackLane(trackId);
2649
3428
  this.clips.set(id, clip);
2650
- this.syncClips();
2651
- void trackId;
3429
+ this.syncClipsDelta([clip], []);
2652
3430
  return id;
2653
3431
  }
2654
3432
  removeClip(clipId) {
2655
3433
  this.clips.delete(clipId);
2656
- this.syncClips();
3434
+ this.syncClipsDelta([], [clipId]);
3435
+ }
3436
+ setMidiClips(clips) {
3437
+ this.midiClips.clear();
3438
+ for (const clip of clips) {
3439
+ const id = clip.id ?? this.nextClipId++;
3440
+ this.midiClips.set(id, { ...clip, id, events: clip.events.map((event) => ({ ...event })) });
3441
+ }
3442
+ this.syncMidiClips();
3443
+ }
3444
+ setBuiltinInstrument(trackId, config = {}) {
3445
+ const destinationId = this.resolveTargetId(trackId);
3446
+ this.offlineEngine.setBuiltinInstrument(config, destinationId);
3447
+ this.postInstrumentSync({ type: "syncBuiltinInstrument", destinationId, config });
3448
+ }
3449
+ setSynthInstrument(trackId, patch = {}) {
3450
+ const destinationId = this.resolveTargetId(trackId);
3451
+ this.offlineEngine.setSynthInstrument(patch, destinationId);
3452
+ this.postInstrumentSync({ type: "syncSynthInstrument", destinationId, patch });
3453
+ }
3454
+ loadSoundFont(data) {
3455
+ this.offlineEngine.loadSoundFont(data);
3456
+ this.postInstrumentSync({ type: "syncLoadSoundFont", data });
3457
+ }
3458
+ setSf2Instrument(trackId, config = {}) {
3459
+ const destinationId = this.resolveTargetId(trackId);
3460
+ this.offlineEngine.setSf2Instrument(config, destinationId);
3461
+ this.postInstrumentSync({ type: "syncSf2Instrument", destinationId, config });
3462
+ }
3463
+ pushMidiNoteOn(trackId, group, channel, note, velocity, renderFrame = -1) {
3464
+ const destinationId = this.resolveTargetId(trackId);
3465
+ this.offlineEngine.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
3466
+ this.postSync({
3467
+ type: "syncMidiNoteOn",
3468
+ destinationId,
3469
+ group,
3470
+ channel,
3471
+ note,
3472
+ velocity,
3473
+ renderFrame
3474
+ });
3475
+ }
3476
+ pushMidiNoteOff(trackId, group, channel, note, velocity = 0, renderFrame = -1) {
3477
+ const destinationId = this.resolveTargetId(trackId);
3478
+ this.offlineEngine.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
3479
+ this.postSync({
3480
+ type: "syncMidiNoteOff",
3481
+ destinationId,
3482
+ group,
3483
+ channel,
3484
+ note,
3485
+ velocity,
3486
+ renderFrame
3487
+ });
3488
+ }
3489
+ pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
3490
+ const destinationId = this.resolveTargetId(trackId);
3491
+ this.offlineEngine.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
3492
+ this.postSync({
3493
+ type: "syncMidiCc",
3494
+ destinationId,
3495
+ group,
3496
+ channel,
3497
+ controller,
3498
+ value,
3499
+ renderFrame
3500
+ });
3501
+ }
3502
+ pushMidiPanic(renderFrame = -1) {
3503
+ this.offlineEngine.pushMidiPanic(renderFrame);
3504
+ this.postSync({ type: "syncMidiPanic", renderFrame });
3505
+ }
3506
+ configureCapture(options) {
3507
+ const bufferFrames = Math.trunc(options.bufferFrames);
3508
+ const channels = Math.trunc(options.channels ?? this.offlineChannelCount);
3509
+ const source = options.source ?? "output";
3510
+ const recordOffsetSamples = Math.trunc(options.recordOffsetSamples ?? 0);
3511
+ const inputMonitor = {
3512
+ enabled: Boolean(options.inputMonitor?.enabled),
3513
+ gain: options.inputMonitor?.gain ?? 1
3514
+ };
3515
+ this.offlineEngine.setCaptureBuffer(channels, bufferFrames);
3516
+ this.offlineEngine.setCaptureSource(source);
3517
+ this.offlineEngine.setRecordOffsetSamples(recordOffsetSamples);
3518
+ this.offlineEngine.setInputMonitor(inputMonitor.enabled, inputMonitor.gain);
3519
+ this.captureConfig = { bufferFrames, channels, source, recordOffsetSamples, inputMonitor };
3520
+ this.postSync({ type: "syncCapture", ...this.captureConfig });
2657
3521
  }
2658
3522
  armRecord(trackId, enabled) {
3523
+ if (enabled && !this.captureConfig) {
3524
+ throw new Error("Capture buffer is not configured");
3525
+ }
2659
3526
  this.offlineEngine.armCapture(enabled);
2660
3527
  return this.realtimeNode.sendCommand({
2661
3528
  type: 13 /* ArmRecord */,
@@ -2665,8 +3532,8 @@ var SonareEngine = class _SonareEngine {
2665
3532
  });
2666
3533
  }
2667
3534
  punch(inPpq, outPpq) {
2668
- const inSample = this.ppqToApproxSample(inPpq);
2669
- const outSample = this.ppqToApproxSample(outPpq);
3535
+ const inSample = this.offlineEngine.sampleAtPpq(inPpq);
3536
+ const outSample = this.offlineEngine.sampleAtPpq(outPpq);
2670
3537
  this.offlineEngine.setCapturePunch(inSample, outSample, true);
2671
3538
  return this.realtimeNode.sendCommand({
2672
3539
  type: 14 /* Punch */,
@@ -2675,6 +3542,16 @@ var SonareEngine = class _SonareEngine {
2675
3542
  argFloat: outSample
2676
3543
  });
2677
3544
  }
3545
+ captureStatus() {
3546
+ return this.realtimeNode.requestCaptureStatus();
3547
+ }
3548
+ capturedAudio() {
3549
+ return this.realtimeNode.requestCapturedAudio();
3550
+ }
3551
+ async resetCapture() {
3552
+ this.offlineEngine.resetCapture();
3553
+ await this.realtimeNode.requestCaptureReset();
3554
+ }
2678
3555
  setMetronome(opts) {
2679
3556
  this.offlineEngine.setMetronome(opts);
2680
3557
  this.postSync({ type: "syncMetronome", config: opts });
@@ -2690,6 +3567,55 @@ var SonareEngine = class _SonareEngine {
2690
3567
  this.syncMarkers();
2691
3568
  return id;
2692
3569
  }
3570
+ /**
3571
+ * Replaces the whole marker set in one call.
3572
+ *
3573
+ * Entries without an `id` are assigned fresh ids; entries carrying an `id`
3574
+ * keep it (ids must be positive and unique within the list). Returns the
3575
+ * resolved markers in the order given, so a caller can map its own marker
3576
+ * identities to the engine ids used by `seekMarker`/`setLoopFromMarkers`.
3577
+ *
3578
+ * @param markers The full marker list (an empty list clears all markers).
3579
+ * @returns The markers with their resolved engine ids.
3580
+ */
3581
+ setMarkers(markers) {
3582
+ const resolved = [];
3583
+ const seen = /* @__PURE__ */ new Set();
3584
+ for (const marker of markers) {
3585
+ if (!Number.isFinite(marker.ppq)) {
3586
+ throw new Error(`Invalid marker ppq: ${String(marker.ppq)}`);
3587
+ }
3588
+ if (marker.id !== void 0) {
3589
+ if (!Number.isInteger(marker.id) || marker.id <= 0) {
3590
+ throw new Error(`Invalid marker id: ${String(marker.id)}`);
3591
+ }
3592
+ if (seen.has(marker.id)) {
3593
+ throw new Error(`Duplicate marker id: ${marker.id}`);
3594
+ }
3595
+ }
3596
+ const id = marker.id ?? this.nextMarkerId++;
3597
+ seen.add(id);
3598
+ if (id >= this.nextMarkerId) {
3599
+ this.nextMarkerId = id + 1;
3600
+ }
3601
+ resolved.push({ id, ppq: marker.ppq, name: marker.name ?? "" });
3602
+ }
3603
+ this.markers.clear();
3604
+ for (const marker of resolved) {
3605
+ this.markers.set(marker.id, marker);
3606
+ }
3607
+ this.syncMarkers();
3608
+ return resolved.map((marker) => ({ ...marker }));
3609
+ }
3610
+ markerCount() {
3611
+ return this.offlineEngine.markerCount();
3612
+ }
3613
+ markerByIndex(index) {
3614
+ return this.offlineEngine.markerByIndex(index);
3615
+ }
3616
+ marker(markerId) {
3617
+ return this.offlineEngine.marker(markerId);
3618
+ }
2693
3619
  seekMarker(markerId) {
2694
3620
  this.offlineEngine.seekMarker(markerId);
2695
3621
  return this.realtimeNode.sendCommand({
@@ -2698,6 +3624,12 @@ var SonareEngine = class _SonareEngine {
2698
3624
  sampleTime: -1
2699
3625
  });
2700
3626
  }
3627
+ setLoopFromMarkers(startMarkerId, endMarkerId) {
3628
+ this.offlineEngine.setLoopFromMarkers(startMarkerId, endMarkerId);
3629
+ const start = this.offlineEngine.marker(startMarkerId);
3630
+ const end = this.offlineEngine.marker(endMarkerId);
3631
+ return this.setLoop(start.ppq, end.ppq, true);
3632
+ }
2701
3633
  async renderOffline(totalFrames) {
2702
3634
  const frames = Math.max(0, Math.floor(totalFrames));
2703
3635
  const inputs = [];
@@ -2728,16 +3660,80 @@ var SonareEngine = class _SonareEngine {
2728
3660
  this.realtimeNode.destroy();
2729
3661
  this.offlineEngine.destroy();
2730
3662
  }
2731
- syncClips() {
3663
+ syncClipsDelta(upserts, removeIds) {
2732
3664
  const clips = Array.from(this.clips.values());
2733
3665
  this.offlineEngine.setClips(clips);
2734
- this.postSync({ type: "syncClips", clips });
3666
+ this.postSync({
3667
+ type: "syncClipsDelta",
3668
+ upserts,
3669
+ removeIds
3670
+ });
3671
+ }
3672
+ syncMidiClips() {
3673
+ const clips = Array.from(this.midiClips.values());
3674
+ this.offlineEngine.setMidiClips(clips);
3675
+ this.postSync({ type: "syncMidiClips", clips });
3676
+ }
3677
+ syncMixer() {
3678
+ const lanes = this.trackLaneIds.map((trackId) => {
3679
+ const sends = this.trackSends.get(trackId);
3680
+ return sends && sends.length > 0 ? { trackId, sends: sends.map((send) => ({ ...send })) } : { trackId };
3681
+ });
3682
+ const buses = this.buses.map((bus) => ({ ...bus }));
3683
+ this.offlineEngine.setTrackBuses(buses);
3684
+ if (lanes.length > 0) {
3685
+ this.offlineEngine.setTrackLanes(lanes);
3686
+ }
3687
+ const trackStrips = Array.from(this.trackStripJson, ([trackId, sceneJson]) => ({
3688
+ trackId,
3689
+ sceneJson
3690
+ }));
3691
+ const busStrips = Array.from(this.busStripJson, ([busId, sceneJson]) => ({
3692
+ busId,
3693
+ sceneJson
3694
+ }));
3695
+ this.postSync({
3696
+ type: "syncMixer",
3697
+ lanes,
3698
+ buses,
3699
+ trackStrips,
3700
+ busStrips,
3701
+ masterStripJson: this.masterStripJson
3702
+ });
2735
3703
  }
2736
3704
  syncMarkers() {
2737
3705
  const markers = Array.from(this.markers.values()).sort((a, b) => a.ppq - b.ppq);
2738
3706
  this.offlineEngine.setMarkers(markers);
2739
3707
  this.postSync({ type: "syncMarkers", markers });
2740
3708
  }
3709
+ postInstrumentSync(message) {
3710
+ if (this.destroyed) {
3711
+ return;
3712
+ }
3713
+ if (this.transportPlaying) {
3714
+ this.pendingInstrumentSync.push(message);
3715
+ return;
3716
+ }
3717
+ this.postSync(message);
3718
+ }
3719
+ flushPendingInstrumentSync() {
3720
+ if (this.destroyed || this.pendingInstrumentSync.length === 0) {
3721
+ return;
3722
+ }
3723
+ const pending = this.pendingInstrumentSync.splice(0);
3724
+ for (const message of pending) {
3725
+ this.postSync(message);
3726
+ }
3727
+ }
3728
+ postTempoSync() {
3729
+ this.postSync({
3730
+ type: "syncTempo",
3731
+ bpm: this.tempoBpm,
3732
+ timeSignature: { ...this.timeSignature },
3733
+ tempoSegments: this.tempoSegments.map((segment) => ({ ...segment })),
3734
+ timeSignatureSegments: this.timeSignatureSegments.map((segment) => ({ ...segment }))
3735
+ });
3736
+ }
2741
3737
  // Posts an out-of-band control-sync message to the worklet engine processor.
2742
3738
  // Sync messages use a string `type` so the worklet's message handler routes
2743
3739
  // them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
@@ -2764,15 +3760,38 @@ var SonareEngine = class _SonareEngine {
2764
3760
  const parsed = Number.parseInt(target, 10);
2765
3761
  return Number.isFinite(parsed) ? parsed : 0;
2766
3762
  }
3763
+ ensureTrackLane(target) {
3764
+ const trackId = this.resolveTargetId(target);
3765
+ if (!Number.isInteger(trackId) || trackId <= 0) {
3766
+ throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
3767
+ }
3768
+ const existing = this.trackLaneIds.indexOf(trackId);
3769
+ if (existing >= 0) {
3770
+ return existing;
3771
+ }
3772
+ this.trackLaneIds.push(trackId);
3773
+ this.syncMixer();
3774
+ return this.trackLaneIds.length - 1;
3775
+ }
3776
+ ensureBus(busId) {
3777
+ const resolved = Math.trunc(busId);
3778
+ if (!Number.isInteger(resolved) || resolved <= 0) {
3779
+ throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
3780
+ }
3781
+ const existing = this.buses.findIndex((bus) => bus.busId === resolved);
3782
+ if (existing >= 0) {
3783
+ return existing;
3784
+ }
3785
+ this.buses.push({ busId: resolved });
3786
+ this.syncMixer();
3787
+ return this.buses.length - 1;
3788
+ }
2767
3789
  curveCode(curve) {
2768
3790
  if (typeof curve === "number") {
2769
3791
  return curve;
2770
3792
  }
2771
3793
  return curve === "exponential" ? 1 : 0;
2772
3794
  }
2773
- ppqToApproxSample(ppq) {
2774
- return Math.max(0, Math.round(ppq * 60 / 120 * this.sampleRate));
2775
- }
2776
3795
  };
2777
3796
  var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
2778
3797
  constructor(options = {}) {
@@ -2993,23 +4012,33 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2993
4012
  class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
2994
4013
  constructor(options) {
2995
4014
  super();
4015
+ this.pendingMessages = [];
2996
4016
  const port = this.port;
2997
4017
  const processorOptions = options?.processorOptions ?? {};
2998
4018
  if (processorOptions.runtimeTarget === "sonare-rt") {
2999
4019
  void this.initializeSonareRt(processorOptions, port);
3000
4020
  } else {
3001
- this.bridge = new SonareRealtimeEngineWorkletProcessor(processorOptions, {
3002
- postMessage: (message) => port?.postMessage?.(message),
3003
- onMeter: (meter) => port?.postMessage?.(meter)
3004
- });
4021
+ void this.initializeEmbind(processorOptions, port);
3005
4022
  }
3006
4023
  const onMessage = (event) => {
4024
+ if (!this.bridge && !this.rtBridge) {
4025
+ if (this.pendingMessages.length < 1024) {
4026
+ this.pendingMessages.push(event.data);
4027
+ }
4028
+ return;
4029
+ }
3007
4030
  if (isEngineCommandRecord(event.data)) {
3008
4031
  this.bridge?.receiveCommand(event.data);
3009
4032
  this.rtBridge?.receiveCommand(event.data);
3010
4033
  } else if (isEngineSyncMessage(event.data)) {
3011
4034
  this.bridge?.receiveSync(event.data);
3012
4035
  this.rtBridge?.receiveSync(event.data);
4036
+ } else if (isEngineCaptureRequestMessage(event.data)) {
4037
+ this.bridge?.receiveCaptureRequest(event.data);
4038
+ this.rtBridge?.receiveCaptureRequest(event.data, port);
4039
+ } else if (isEngineTransportRequestMessage(event.data)) {
4040
+ this.bridge?.receiveTransportRequest(event.data);
4041
+ this.rtBridge?.receiveTransportRequest(event.data, port);
3013
4042
  }
3014
4043
  };
3015
4044
  if (port?.addEventListener) {
@@ -3032,6 +4061,60 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
3032
4061
  }
3033
4062
  return true;
3034
4063
  }
4064
+ replayPendingMessages(port) {
4065
+ const messages = this.pendingMessages.splice(0);
4066
+ for (const data of messages) {
4067
+ if (isEngineCommandRecord(data)) {
4068
+ this.bridge?.receiveCommand(data);
4069
+ this.rtBridge?.receiveCommand(data);
4070
+ } else if (isEngineSyncMessage(data)) {
4071
+ this.bridge?.receiveSync(data);
4072
+ this.rtBridge?.receiveSync(data);
4073
+ } else if (isEngineCaptureRequestMessage(data)) {
4074
+ this.bridge?.receiveCaptureRequest(data);
4075
+ this.rtBridge?.receiveCaptureRequest(data, port);
4076
+ } else if (isEngineTransportRequestMessage(data)) {
4077
+ this.bridge?.receiveTransportRequest(data);
4078
+ this.rtBridge?.receiveTransportRequest(data, port);
4079
+ }
4080
+ }
4081
+ }
4082
+ async initializeEmbind(options, port) {
4083
+ try {
4084
+ const initPromise2 = globalThis.SonareEmbindInitPromise;
4085
+ if (initPromise2) {
4086
+ await initPromise2;
4087
+ }
4088
+ if (!isInitialized()) {
4089
+ const moduleFactory = globalThis.SonareEmbindModuleFactory;
4090
+ if (!moduleFactory) {
4091
+ throw new Error("embind realtime engine module is not initialized.");
4092
+ }
4093
+ await init({
4094
+ locateFile: (path) => path,
4095
+ wasmBinary: options.wasmBinary,
4096
+ moduleFactory
4097
+ });
4098
+ }
4099
+ this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
4100
+ postMessage: (message) => port?.postMessage?.(message),
4101
+ onMeter: (meter) => port?.postMessage?.(meter)
4102
+ });
4103
+ for (const message of options.initialSyncMessages ?? []) {
4104
+ this.bridge.receiveSync(message);
4105
+ }
4106
+ for (const command of options.initialCommands ?? []) {
4107
+ this.bridge.receiveCommand(command);
4108
+ }
4109
+ this.replayPendingMessages(port);
4110
+ port?.postMessage?.({ type: "ready", runtimeTarget: "embind" });
4111
+ } catch (error) {
4112
+ port?.postMessage?.({
4113
+ type: "error",
4114
+ message: error instanceof Error ? error.message : String(error)
4115
+ });
4116
+ }
4117
+ }
3035
4118
  async initializeSonareRt(options, port) {
3036
4119
  try {
3037
4120
  if (!options.rtModuleUrl) {
@@ -3057,6 +4140,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
3057
4140
  telemetrySharedBuffer: options.telemetrySharedBuffer,
3058
4141
  telemetryRingCapacity: options.telemetryRingCapacity
3059
4142
  });
4143
+ this.replayPendingMessages(port);
3060
4144
  port?.postMessage?.({ type: "ready", runtimeTarget: "sonare-rt" });
3061
4145
  } catch (error) {
3062
4146
  port?.postMessage?.({