@libraz/libsonare 1.3.1 → 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
@@ -1,13 +1,153 @@
1
+ // src/errors.ts
2
+ var SonareError = class extends Error {
3
+ constructor(code, codeName, message) {
4
+ super(message);
5
+ this.name = "SonareError";
6
+ this.code = code;
7
+ this.codeName = codeName;
8
+ }
9
+ };
10
+
1
11
  // src/module_state.ts
2
- var wasmModule = null;
12
+ var wrappedModule = null;
13
+ function nativeExceptionPtr(error) {
14
+ if (typeof error === "number") {
15
+ return error;
16
+ }
17
+ if (error !== null && typeof error === "object") {
18
+ const ptr = error.excPtr;
19
+ if (typeof ptr === "number") {
20
+ return ptr;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ function makeSonareError(raw, thrown) {
26
+ let code = 99 /* Unknown */;
27
+ let codeName = "Unknown";
28
+ let message = `libsonare native exception (${thrown})`;
29
+ try {
30
+ const info = raw.sonareExceptionInfo?.(thrown);
31
+ if (info) {
32
+ code = info.code ?? code;
33
+ codeName = info.codeName ?? codeName;
34
+ message = info.message || message;
35
+ }
36
+ } catch {
37
+ }
38
+ return new SonareError(code, codeName, message);
39
+ }
40
+ function wrapModuleErrors(raw) {
41
+ const cache = /* @__PURE__ */ new Map();
42
+ const objectCache = /* @__PURE__ */ new WeakMap();
43
+ const convert = (error) => {
44
+ const ptr = nativeExceptionPtr(error);
45
+ if (ptr !== null) {
46
+ throw makeSonareError(raw, ptr);
47
+ }
48
+ throw error;
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
+ };
127
+ return new Proxy(raw, {
128
+ get(target, prop, receiver) {
129
+ const value = Reflect.get(target, prop, receiver);
130
+ if (typeof value !== "function") {
131
+ return value;
132
+ }
133
+ const cached = cache.get(prop);
134
+ if (cached) {
135
+ return cached;
136
+ }
137
+ const wrapped = wrapFunction(value);
138
+ cache.set(prop, wrapped);
139
+ return wrapped;
140
+ }
141
+ });
142
+ }
3
143
  function setSonareModule(module2) {
4
- wasmModule = module2;
144
+ wrappedModule = wrapModuleErrors(module2);
5
145
  }
6
146
  function getSonareModule() {
7
- if (!wasmModule) {
147
+ if (!wrappedModule) {
8
148
  throw new Error("Module not initialized. Call init() first.");
9
149
  }
10
- return wasmModule;
150
+ return wrappedModule;
11
151
  }
12
152
 
13
153
  // src/codes.ts
@@ -82,6 +222,16 @@ var Mixer = class _Mixer {
82
222
  compile() {
83
223
  this.mixer.compile();
84
224
  }
225
+ /**
226
+ * Non-fatal warnings captured when this mixer was built from scene JSON: one
227
+ * entry per channel-strip insert that was handed param keys it does not read
228
+ * (a likely typo, or a key meant for a different processor). The scene still
229
+ * loaded; these keys simply took no effect. Empty when every key was consumed.
230
+ * Use {@link masteringInsertParamNames} to discover the keys an insert accepts.
231
+ */
232
+ sceneWarnings() {
233
+ return this.mixer.sceneWarnings();
234
+ }
85
235
  /**
86
236
  * Mix one block of per-strip stereo audio into the stereo master.
87
237
  *
@@ -644,9 +794,6 @@ function engineCapabilities() {
644
794
  };
645
795
  }
646
796
  var RealtimeEngine = class {
647
- nativeExt() {
648
- return this.native;
649
- }
650
797
  constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
651
798
  const module2 = getSonareModule();
652
799
  const capabilities = engineCapabilities();
@@ -673,8 +820,14 @@ var RealtimeEngine = class {
673
820
  setParameterSmoothed(paramId, value, renderFrame = -1) {
674
821
  this.native.setParameterSmoothed(paramId, value, renderFrame);
675
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
+ }
676
829
  setBuiltinInstrument(config = {}, destinationId = config.destinationId ?? 0) {
677
- this.nativeExt().setBuiltinInstrument(destinationId, config);
830
+ this.native.setBuiltinInstrument(destinationId, config);
678
831
  }
679
832
  /**
680
833
  * Bind the patch-driven NativeSynth to a realtime MIDI destination. `patch`
@@ -686,7 +839,7 @@ var RealtimeEngine = class {
686
839
  * binding convenience, not part of the NativeSynth patch itself.
687
840
  */
688
841
  setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
689
- this.nativeExt().setSynthInstrument(destinationId, patch);
842
+ this.native.setSynthInstrument(destinationId, patch);
690
843
  }
691
844
  /**
692
845
  * Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
@@ -695,7 +848,7 @@ var RealtimeEngine = class {
695
848
  * not referenced afterwards. Replaces any previously loaded SoundFont.
696
849
  */
697
850
  loadSoundFont(data) {
698
- this.nativeExt().loadSoundFont(data);
851
+ this.native.loadSoundFont(data);
699
852
  }
700
853
  /**
701
854
  * Bind a GS-compatible SoundFont player to a realtime MIDI destination, fed
@@ -707,13 +860,13 @@ var RealtimeEngine = class {
707
860
  * synthesizer GM fallback bank (the data-free floor).
708
861
  */
709
862
  setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
710
- this.nativeExt().setSf2Instrument(destinationId, config);
863
+ this.native.setSf2Instrument(destinationId, config);
711
864
  }
712
865
  clearMidiInstrument(destinationId = 0) {
713
- this.nativeExt().clearMidiInstrument(destinationId);
866
+ this.native.clearMidiInstrument(destinationId);
714
867
  }
715
868
  midiInstrumentCount() {
716
- return this.nativeExt().midiInstrumentCount();
869
+ return this.native.midiInstrumentCount();
717
870
  }
718
871
  /**
719
872
  * Bind a live MIDI CC to an engine automation parameter. The MIDI event still
@@ -721,7 +874,7 @@ var RealtimeEngine = class {
721
874
  * mapped into [minValue, maxValue] for `paramId`.
722
875
  */
723
876
  bindMidiCc(channel, controller, paramId, options = {}) {
724
- this.nativeExt().bindMidiCc(
877
+ this.native.bindMidiCc(
725
878
  channel,
726
879
  controller,
727
880
  paramId,
@@ -730,42 +883,42 @@ var RealtimeEngine = class {
730
883
  );
731
884
  }
732
885
  clearMidiCcBindings() {
733
- this.nativeExt().clearMidiCcBindings();
886
+ this.native.clearMidiCcBindings();
734
887
  }
735
888
  midiCcBindingCount() {
736
- return this.nativeExt().midiCcBindingCount();
889
+ return this.native.midiCcBindingCount();
737
890
  }
738
891
  /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
739
892
  setMidiFx(destinationId, configJson) {
740
- this.nativeExt().setMidiFx(destinationId, configJson);
893
+ this.native.setMidiFx(destinationId, configJson);
741
894
  }
742
895
  clearMidiFx(destinationId = 0) {
743
- this.nativeExt().clearMidiFx(destinationId);
896
+ this.native.clearMidiFx(destinationId);
744
897
  }
745
898
  /** Enable the engine-owned live MIDI input source for a destination. */
746
899
  setMidiInputSource(destinationId = 0) {
747
- this.nativeExt().setMidiInputSource(destinationId);
900
+ this.native.setMidiInputSource(destinationId);
748
901
  }
749
902
  clearMidiInputSource() {
750
- this.nativeExt().clearMidiInputSource();
903
+ this.native.clearMidiInputSource();
751
904
  }
752
905
  midiInputPendingCount() {
753
- return this.nativeExt().midiInputPendingCount();
906
+ return this.native.midiInputPendingCount();
754
907
  }
755
908
  pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
756
- this.nativeExt().pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
909
+ this.native.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
757
910
  }
758
911
  pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
759
- this.nativeExt().pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
912
+ this.native.pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
760
913
  }
761
914
  pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
762
- this.nativeExt().pushMidiInputCc(group, channel, controller, value, portTimeSamples);
915
+ this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
763
916
  }
764
917
  pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
765
- this.nativeExt().pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
918
+ this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
766
919
  }
767
920
  pushMidiNoteOff(destinationId, group, channel, note, velocity = 0, renderFrame = -1) {
768
- this.nativeExt().pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
921
+ this.native.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
769
922
  }
770
923
  /**
771
924
  * Queue an immediate (live) MIDI control change to a MIDI destination
@@ -774,21 +927,21 @@ var RealtimeEngine = class {
774
927
  * immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
775
928
  */
776
929
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
777
- this.nativeExt().pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
930
+ this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
778
931
  }
779
932
  /**
780
933
  * Queue a MIDI panic (all-notes-off) releasing every sounding note at
781
934
  * `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
782
935
  */
783
936
  pushMidiPanic(renderFrame = -1) {
784
- this.nativeExt().pushMidiPanic(renderFrame);
937
+ this.native.pushMidiPanic(renderFrame);
785
938
  }
786
939
  /**
787
940
  * Remove all registered parameters (and their automation lanes). Control-thread
788
941
  * only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
789
942
  */
790
943
  clearParameters() {
791
- this.nativeExt().clearParameters();
944
+ this.native.clearParameters();
792
945
  }
793
946
  /** Read back the current transport state snapshot. */
794
947
  getTransportState() {
@@ -809,9 +962,18 @@ var RealtimeEngine = class {
809
962
  setTempo(bpm) {
810
963
  this.native.setTempo(bpm);
811
964
  }
965
+ setTempoSegments(segments) {
966
+ this.native.setTempoSegments([...segments]);
967
+ }
812
968
  setTimeSignature(numerator, denominator) {
813
969
  this.native.setTimeSignature(numerator, denominator);
814
970
  }
971
+ setTimeSignatureSegments(segments) {
972
+ this.native.setTimeSignatureSegments([...segments]);
973
+ }
974
+ sampleAtPpq(ppq) {
975
+ return Number(this.native.sampleAtPpq(ppq));
976
+ }
815
977
  setLoop(startPpq, endPpq, enabled = true) {
816
978
  this.native.setLoop(startPpq, endPpq, enabled);
817
979
  }
@@ -880,21 +1042,81 @@ var RealtimeEngine = class {
880
1042
  clipCount() {
881
1043
  return this.native.clipCount();
882
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
+ }
883
1105
  createClipPageProvider(numChannels, numSamples, pageFrames) {
884
- const id = this.nativeExt().createClipPageProvider(numChannels, numSamples, pageFrames);
1106
+ const id = this.native.createClipPageProvider(numChannels, numSamples, pageFrames);
885
1107
  return new ClipPageProvider(this, id);
886
1108
  }
887
1109
  supplyClipPage(providerId, pageIndex, channels) {
888
- this.nativeExt().supplyClipPage(providerId, pageIndex, channels);
1110
+ this.native.supplyClipPage(providerId, pageIndex, channels);
889
1111
  }
890
1112
  clearClipPage(providerId, pageIndex) {
891
- this.nativeExt().clearClipPage(providerId, pageIndex);
1113
+ this.native.clearClipPage(providerId, pageIndex);
892
1114
  }
893
1115
  destroyClipPageProvider(providerId) {
894
- this.nativeExt().destroyClipPageProvider(providerId);
1116
+ this.native.destroyClipPageProvider(providerId);
895
1117
  }
896
1118
  popClipPageRequest() {
897
- return this.nativeExt().popClipPageRequest();
1119
+ return this.native.popClipPageRequest();
898
1120
  }
899
1121
  setCaptureBuffer(numChannels, capacityFrames) {
900
1122
  this.native.setCaptureBuffer(numChannels, capacityFrames);
@@ -1012,7 +1234,7 @@ async function init(options) {
1012
1234
  }
1013
1235
  initPromise = (async () => {
1014
1236
  try {
1015
- const createModule = (await import("./sonare.js")).default;
1237
+ const createModule = options?.moduleFactory ?? (await import("./sonare.js")).default;
1016
1238
  module = await createModule(options);
1017
1239
  setSonareModule(module);
1018
1240
  } catch (error) {
@@ -1027,8 +1249,20 @@ function isInitialized() {
1027
1249
  }
1028
1250
 
1029
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
+ }
1030
1264
  var SONARE_METER_RING_HEADER_INTS = 4;
1031
- var SONARE_METER_RING_RECORD_FLOATS = 7;
1265
+ var SONARE_METER_RING_RECORD_FLOATS = 14;
1032
1266
  var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
1033
1267
  var SONARE_FRAME_LANE_BASE = 16777216;
1034
1268
  function encodeFrameLo(frame) {
@@ -1119,7 +1353,19 @@ function isEngineSyncMessage(value) {
1119
1353
  if (!isRecord(value) || typeof value.type !== "string") {
1120
1354
  return false;
1121
1355
  }
1122
- 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";
1123
1369
  }
1124
1370
  function isRealtimeVoiceChangerMessage(value) {
1125
1371
  if (!isRecord(value) || typeof value.type !== "string") {
@@ -1131,7 +1377,7 @@ function isEngineTelemetryRecord(value) {
1131
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";
1132
1378
  }
1133
1379
  function isMeterSnapshot(value) {
1134
- 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);
1135
1381
  }
1136
1382
  function sonareMeterRingBufferByteLength(capacity) {
1137
1383
  const clampedCapacity = Math.max(1, Math.floor(capacity));
@@ -1149,19 +1395,27 @@ function createSonareMeterRingBuffer(capacity = 128) {
1149
1395
  }
1150
1396
  function readSonareMeterRingBuffer(ring, readIndex = 0) {
1151
1397
  const writeIndex = Atomics.load(ring.header, 0);
1398
+ const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
1152
1399
  const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
1153
1400
  const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
1154
1401
  const meters = [];
1155
1402
  for (let index = firstReadable; index < writeIndex; index++) {
1156
- const offset = index % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1403
+ const offset = index % ring.capacity * recordFloats;
1157
1404
  meters.push({
1158
1405
  type: "meter",
1159
- frame: decodeFrame(ring.records[offset], ring.records[offset + 6]),
1160
- peakDbL: ring.records[offset + 1],
1161
- peakDbR: ring.records[offset + 2],
1162
- rmsDbL: ring.records[offset + 3],
1163
- rmsDbR: ring.records[offset + 4],
1164
- 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]
1165
1419
  });
1166
1420
  }
1167
1421
  return { nextReadIndex: writeIndex, meters };
@@ -1421,12 +1675,19 @@ function telemetryFromEngine(telemetry) {
1421
1675
  function meterFromEngine(meter) {
1422
1676
  return {
1423
1677
  type: "meter",
1678
+ targetId: meter.targetId,
1424
1679
  frame: meter.renderFrame,
1425
1680
  peakDbL: meter.peakDbL,
1426
1681
  peakDbR: meter.peakDbR,
1427
1682
  rmsDbL: meter.rmsDbL,
1428
1683
  rmsDbR: meter.rmsDbR,
1429
- 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
1430
1691
  };
1431
1692
  }
1432
1693
  function magnitudeToDb(value) {
@@ -1581,12 +1842,19 @@ var SonareWorkletProcessor = class {
1581
1842
  const denominator = Math.sqrt(sumL * sumR);
1582
1843
  const meter = {
1583
1844
  type: "meter",
1845
+ targetId: 0,
1584
1846
  frame: this.processedFrames,
1585
1847
  peakDbL: toDb(peakL),
1586
1848
  peakDbR: toDb(peakR),
1587
1849
  rmsDbL: toDb(rmsL),
1588
1850
  rmsDbR: toDb(rmsR),
1589
- 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
1590
1858
  };
1591
1859
  this.transport.onMeter?.(meter);
1592
1860
  if (this.meterRing) {
@@ -1603,12 +1871,19 @@ var SonareWorkletProcessor = class {
1603
1871
  const writeIndex = Atomics.load(ring.header, 0);
1604
1872
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1605
1873
  ring.records[offset] = encodeFrameLo(meter.frame);
1606
- ring.records[offset + 1] = meter.peakDbL;
1607
- ring.records[offset + 2] = meter.peakDbR;
1608
- ring.records[offset + 3] = meter.rmsDbL;
1609
- ring.records[offset + 4] = meter.rmsDbR;
1610
- ring.records[offset + 5] = meter.correlation;
1611
- 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;
1612
1887
  Atomics.store(ring.header, 0, writeIndex + 1);
1613
1888
  }
1614
1889
  publishSpectrum(left, right) {
@@ -1673,6 +1948,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1673
1948
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
1674
1949
  // SetMetronome command only toggles enabled state; the config arrives here.
1675
1950
  this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
1951
+ this.liveClips = /* @__PURE__ */ new Map();
1676
1952
  this.sampleRate = options.sampleRate ?? 48e3;
1677
1953
  this.blockSize = options.blockSize ?? 128;
1678
1954
  this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
@@ -1776,8 +2052,28 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1776
2052
  }
1777
2053
  switch (message.type) {
1778
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
+ }
1779
2061
  this.engine.setClips(message.clips);
1780
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;
1781
2077
  case "syncMarkers":
1782
2078
  this.engine.setMarkers(message.markers);
1783
2079
  break;
@@ -1788,6 +2084,184 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1788
2084
  case "syncAutomation":
1789
2085
  this.engine.setAutomationLane(message.paramId, message.points);
1790
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
+ });
1791
2265
  }
1792
2266
  }
1793
2267
  destroy() {
@@ -1868,6 +2342,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1868
2342
  case 17 /* SeekMarker */:
1869
2343
  this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
1870
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;
1871
2353
  default:
1872
2354
  this.publishTelemetryRecord({
1873
2355
  type: 1 /* Error */,
@@ -1899,10 +2381,12 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1899
2381
  }
1900
2382
  for (const item of this.engine.drainMeterTelemetry(64)) {
1901
2383
  const meter = meterFromEngine(item);
1902
- if (meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
2384
+ if (meter.frame !== this.lastMeterFrame && meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
1903
2385
  continue;
1904
2386
  }
1905
- this.lastMeterFrame = meter.frame;
2387
+ if (meter.frame !== this.lastMeterFrame) {
2388
+ this.lastMeterFrame = meter.frame;
2389
+ }
1906
2390
  if (this.meterRing) {
1907
2391
  this.writeMeterRing(meter);
1908
2392
  } else {
@@ -1919,12 +2403,19 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1919
2403
  const writeIndex = Atomics.load(ring.header, 0);
1920
2404
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1921
2405
  ring.records[offset] = encodeFrameLo(meter.frame);
1922
- ring.records[offset + 1] = meter.peakDbL;
1923
- ring.records[offset + 2] = meter.peakDbR;
1924
- ring.records[offset + 3] = meter.rmsDbL;
1925
- ring.records[offset + 4] = meter.rmsDbR;
1926
- ring.records[offset + 5] = meter.correlation;
1927
- 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;
1928
2419
  Atomics.store(ring.header, 0, writeIndex + 1);
1929
2420
  }
1930
2421
  commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
@@ -2056,9 +2547,28 @@ var SonareRtRealtimeEngineRuntime = class {
2056
2547
  this.metronomeConfig.clickSamples
2057
2548
  );
2058
2549
  break;
2550
+ case "syncTempo":
2551
+ this.module._sonare_rt_engine_set_tempo(this.engine, message.bpm);
2552
+ break;
2059
2553
  case "syncClips":
2554
+ case "syncClipsDelta":
2555
+ case "syncMidiClips":
2060
2556
  case "syncMarkers":
2061
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":
2062
2572
  if (this.telemetryRing) {
2063
2573
  writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
2064
2574
  type: 1 /* Error */,
@@ -2073,6 +2583,28 @@ var SonareRtRealtimeEngineRuntime = class {
2073
2583
  break;
2074
2584
  }
2075
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
+ }
2076
2608
  destroy() {
2077
2609
  if (this.closed) {
2078
2610
  return;
@@ -2250,6 +2782,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2250
2782
  this.meterReadIndex = 0;
2251
2783
  this.telemetryListeners = /* @__PURE__ */ new Set();
2252
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();
2253
2789
  this.destroyed = false;
2254
2790
  this.node = node;
2255
2791
  this.capabilities = capabilities;
@@ -2260,11 +2796,31 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2260
2796
  this.resolveReady = resolve;
2261
2797
  this.rejectReady = reject;
2262
2798
  });
2263
- if (capabilities.runtimeTarget !== "sonare-rt") {
2799
+ if (!capabilities.readyMessage) {
2264
2800
  this.resolveReady();
2265
2801
  }
2266
2802
  this.node.port.onmessage = (event) => {
2267
- 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)) {
2268
2824
  this.emitTelemetry(event.data);
2269
2825
  } else if (isMeterSnapshot(event.data)) {
2270
2826
  this.emitMeter(event.data);
@@ -2318,7 +2874,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2318
2874
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
2319
2875
  telemetryRingCapacity: telemetryRing?.capacity,
2320
2876
  meterSharedBuffer: meterRing?.sharedBuffer,
2321
- meterRingCapacity: meterRing?.capacity
2877
+ meterRingCapacity: meterRing?.capacity,
2878
+ wasmBinary: options.wasmBinary,
2879
+ initialSyncMessages: options.initialSyncMessages,
2880
+ initialCommands: options.initialCommands
2322
2881
  };
2323
2882
  const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
2324
2883
  const node = factory(context, processorName, {
@@ -2338,7 +2897,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2338
2897
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
2339
2898
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
2340
2899
  abiCompatible: detectedCapabilities?.abiCompatible,
2341
- degradedReason
2900
+ degradedReason,
2901
+ readyMessage: runtimeTarget === "sonare-rt" || runtimeTarget === "embind" && moduleUrl !== void 0 && !options.nodeFactory
2342
2902
  },
2343
2903
  commandRing,
2344
2904
  telemetryRing,
@@ -2375,6 +2935,32 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2375
2935
  this.node.port.postMessage(command);
2376
2936
  return true;
2377
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
+ }
2378
2964
  pollTelemetry() {
2379
2965
  if (!this.telemetryRing) {
2380
2966
  return [];
@@ -2419,6 +3005,14 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2419
3005
  this.destroyed = true;
2420
3006
  this.node.port.postMessage({ type: 3 /* TransportStop */, sampleTime: -1 });
2421
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();
2422
3016
  this.telemetryListeners.clear();
2423
3017
  this.meterListeners.clear();
2424
3018
  }
@@ -2432,14 +3026,50 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2432
3026
  listener(meter);
2433
3027
  }
2434
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
+ }
2435
3051
  };
2436
3052
  var SonareEngine = class _SonareEngine {
2437
3053
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
2438
3054
  this.automationLanes = /* @__PURE__ */ new Map();
2439
3055
  this.clips = /* @__PURE__ */ new Map();
3056
+ this.midiClips = /* @__PURE__ */ new Map();
2440
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
+ ];
2441
3069
  this.nextClipId = 1;
2442
3070
  this.nextMarkerId = 1;
3071
+ this.transportPlaying = false;
3072
+ this.pendingInstrumentSync = [];
2443
3073
  this.destroyed = false;
2444
3074
  this.context = context;
2445
3075
  this.realtimeNode = realtimeNode;
@@ -2450,8 +3080,21 @@ var SonareEngine = class _SonareEngine {
2450
3080
  this.offlineBlockSize = offlineBlockSize;
2451
3081
  this.offlineChannelCount = offlineChannelCount;
2452
3082
  this.transport = {
2453
- play: (sampleTime = -1) => this.realtimeNode.play(sampleTime),
2454
- 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
+ },
2455
3098
  seekPpq: (ppq, sampleTime = -1) => {
2456
3099
  this.offlineEngine.seekPpq(ppq, sampleTime);
2457
3100
  return this.realtimeNode.seekPpq(ppq, sampleTime);
@@ -2462,6 +3105,7 @@ var SonareEngine = class _SonareEngine {
2462
3105
  return this.realtimeNode.seekSample(timelineSample, sampleTime);
2463
3106
  },
2464
3107
  setTempo: (bpm) => this.setTempo(bpm),
3108
+ setTempoSegments: (segments) => this.setTempoSegments(segments),
2465
3109
  setLoop: (startPpq, endPpq, enabled = true) => this.setLoop(startPpq, endPpq, enabled)
2466
3110
  };
2467
3111
  }
@@ -2496,13 +3140,37 @@ var SonareEngine = class _SonareEngine {
2496
3140
  await this.context.resume?.();
2497
3141
  }
2498
3142
  setTempo(bpm) {
3143
+ this.tempoBpm = bpm;
3144
+ this.tempoSegments = [{ startPpq: 0, bpm }];
2499
3145
  this.offlineEngine.setTempo(bpm);
3146
+ this.postTempoSync();
2500
3147
  this.realtimeNode.sendCommand({
2501
3148
  type: 6 /* SetTempoMap */,
2502
3149
  sampleTime: -1,
2503
3150
  argFloat: bpm
2504
3151
  });
2505
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
+ }
2506
3174
  setLoop(startPpq, endPpq, enabled = true) {
2507
3175
  this.offlineEngine.setLoop(startPpq, endPpq, enabled);
2508
3176
  return this.realtimeNode.sendCommand({
@@ -2513,6 +3181,17 @@ var SonareEngine = class _SonareEngine {
2513
3181
  argInt: Math.round(endPpq * 1e6)
2514
3182
  });
2515
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
+ }
2516
3195
  setParam(nodeId, param, value) {
2517
3196
  const paramId = this.resolveParamId(nodeId, param);
2518
3197
  this.offlineEngine.setParameter(paramId, value);
@@ -2543,12 +3222,198 @@ var SonareEngine = class _SonareEngine {
2543
3222
  return parameters;
2544
3223
  }
2545
3224
  setSoloMute(target, solo, mute) {
2546
- void target;
2547
- void solo;
2548
- void mute;
2549
- throw new Error(
2550
- "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 }))
2551
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);
2552
3417
  }
2553
3418
  addClip(trackId, buffer, startPpq, opts = {}) {
2554
3419
  const id = opts.id ?? this.nextClipId++;
@@ -2556,18 +3421,108 @@ var SonareEngine = class _SonareEngine {
2556
3421
  ...opts,
2557
3422
  id,
2558
3423
  channels: buffer,
2559
- startPpq
3424
+ startPpq,
3425
+ trackId: this.resolveTargetId(trackId)
2560
3426
  };
3427
+ this.ensureTrackLane(trackId);
2561
3428
  this.clips.set(id, clip);
2562
- this.syncClips();
2563
- void trackId;
3429
+ this.syncClipsDelta([clip], []);
2564
3430
  return id;
2565
3431
  }
2566
3432
  removeClip(clipId) {
2567
3433
  this.clips.delete(clipId);
2568
- 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 });
2569
3521
  }
2570
3522
  armRecord(trackId, enabled) {
3523
+ if (enabled && !this.captureConfig) {
3524
+ throw new Error("Capture buffer is not configured");
3525
+ }
2571
3526
  this.offlineEngine.armCapture(enabled);
2572
3527
  return this.realtimeNode.sendCommand({
2573
3528
  type: 13 /* ArmRecord */,
@@ -2577,8 +3532,8 @@ var SonareEngine = class _SonareEngine {
2577
3532
  });
2578
3533
  }
2579
3534
  punch(inPpq, outPpq) {
2580
- const inSample = this.ppqToApproxSample(inPpq);
2581
- const outSample = this.ppqToApproxSample(outPpq);
3535
+ const inSample = this.offlineEngine.sampleAtPpq(inPpq);
3536
+ const outSample = this.offlineEngine.sampleAtPpq(outPpq);
2582
3537
  this.offlineEngine.setCapturePunch(inSample, outSample, true);
2583
3538
  return this.realtimeNode.sendCommand({
2584
3539
  type: 14 /* Punch */,
@@ -2587,6 +3542,16 @@ var SonareEngine = class _SonareEngine {
2587
3542
  argFloat: outSample
2588
3543
  });
2589
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
+ }
2590
3555
  setMetronome(opts) {
2591
3556
  this.offlineEngine.setMetronome(opts);
2592
3557
  this.postSync({ type: "syncMetronome", config: opts });
@@ -2602,6 +3567,55 @@ var SonareEngine = class _SonareEngine {
2602
3567
  this.syncMarkers();
2603
3568
  return id;
2604
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
+ }
2605
3619
  seekMarker(markerId) {
2606
3620
  this.offlineEngine.seekMarker(markerId);
2607
3621
  return this.realtimeNode.sendCommand({
@@ -2610,6 +3624,12 @@ var SonareEngine = class _SonareEngine {
2610
3624
  sampleTime: -1
2611
3625
  });
2612
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
+ }
2613
3633
  async renderOffline(totalFrames) {
2614
3634
  const frames = Math.max(0, Math.floor(totalFrames));
2615
3635
  const inputs = [];
@@ -2640,16 +3660,80 @@ var SonareEngine = class _SonareEngine {
2640
3660
  this.realtimeNode.destroy();
2641
3661
  this.offlineEngine.destroy();
2642
3662
  }
2643
- syncClips() {
3663
+ syncClipsDelta(upserts, removeIds) {
2644
3664
  const clips = Array.from(this.clips.values());
2645
3665
  this.offlineEngine.setClips(clips);
2646
- 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
+ });
2647
3703
  }
2648
3704
  syncMarkers() {
2649
3705
  const markers = Array.from(this.markers.values()).sort((a, b) => a.ppq - b.ppq);
2650
3706
  this.offlineEngine.setMarkers(markers);
2651
3707
  this.postSync({ type: "syncMarkers", markers });
2652
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
+ }
2653
3737
  // Posts an out-of-band control-sync message to the worklet engine processor.
2654
3738
  // Sync messages use a string `type` so the worklet's message handler routes
2655
3739
  // them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
@@ -2676,15 +3760,38 @@ var SonareEngine = class _SonareEngine {
2676
3760
  const parsed = Number.parseInt(target, 10);
2677
3761
  return Number.isFinite(parsed) ? parsed : 0;
2678
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
+ }
2679
3789
  curveCode(curve) {
2680
3790
  if (typeof curve === "number") {
2681
3791
  return curve;
2682
3792
  }
2683
3793
  return curve === "exponential" ? 1 : 0;
2684
3794
  }
2685
- ppqToApproxSample(ppq) {
2686
- return Math.max(0, Math.round(ppq * 60 / 120 * this.sampleRate));
2687
- }
2688
3795
  };
2689
3796
  var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
2690
3797
  constructor(options = {}) {
@@ -2905,23 +4012,33 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2905
4012
  class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
2906
4013
  constructor(options) {
2907
4014
  super();
4015
+ this.pendingMessages = [];
2908
4016
  const port = this.port;
2909
4017
  const processorOptions = options?.processorOptions ?? {};
2910
4018
  if (processorOptions.runtimeTarget === "sonare-rt") {
2911
4019
  void this.initializeSonareRt(processorOptions, port);
2912
4020
  } else {
2913
- this.bridge = new SonareRealtimeEngineWorkletProcessor(processorOptions, {
2914
- postMessage: (message) => port?.postMessage?.(message),
2915
- onMeter: (meter) => port?.postMessage?.(meter)
2916
- });
4021
+ void this.initializeEmbind(processorOptions, port);
2917
4022
  }
2918
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
+ }
2919
4030
  if (isEngineCommandRecord(event.data)) {
2920
4031
  this.bridge?.receiveCommand(event.data);
2921
4032
  this.rtBridge?.receiveCommand(event.data);
2922
4033
  } else if (isEngineSyncMessage(event.data)) {
2923
4034
  this.bridge?.receiveSync(event.data);
2924
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);
2925
4042
  }
2926
4043
  };
2927
4044
  if (port?.addEventListener) {
@@ -2944,6 +4061,60 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2944
4061
  }
2945
4062
  return true;
2946
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
+ }
2947
4118
  async initializeSonareRt(options, port) {
2948
4119
  try {
2949
4120
  if (!options.rtModuleUrl) {
@@ -2969,6 +4140,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2969
4140
  telemetrySharedBuffer: options.telemetrySharedBuffer,
2970
4141
  telemetryRingCapacity: options.telemetryRingCapacity
2971
4142
  });
4143
+ this.replayPendingMessages(port);
2972
4144
  port?.postMessage?.({ type: "ready", runtimeTarget: "sonare-rt" });
2973
4145
  } catch (error) {
2974
4146
  port?.postMessage?.({