@libraz/libsonare 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worklet.js CHANGED
@@ -1,3 +1,58 @@
1
+ // src/codes.ts
2
+ function automationCurveCode(curve) {
3
+ switch (curve) {
4
+ case "linear":
5
+ return 0;
6
+ case "exponential":
7
+ return 1;
8
+ case "hold":
9
+ return 2;
10
+ case "s-curve":
11
+ return 3;
12
+ default:
13
+ throw new Error(`Invalid automation curve: ${curve}`);
14
+ }
15
+ }
16
+ function panLawCode(panLaw) {
17
+ if (typeof panLaw === "number") {
18
+ return panLaw;
19
+ }
20
+ switch (panLaw) {
21
+ case "const4.5dB":
22
+ return 1;
23
+ case "const6dB":
24
+ return 2;
25
+ case "linear0dB":
26
+ return 3;
27
+ default:
28
+ return 0;
29
+ }
30
+ }
31
+ function panModeCode(panMode) {
32
+ if (typeof panMode === "number") {
33
+ return panMode;
34
+ }
35
+ switch (panMode) {
36
+ case "stereoPan":
37
+ case "stereo-pan":
38
+ return 1;
39
+ case "dualPan":
40
+ case "dual-pan":
41
+ return 2;
42
+ default:
43
+ return 0;
44
+ }
45
+ }
46
+ function meterTapCode(tap) {
47
+ return tap === "preFader" || tap === 0 ? 0 : 1;
48
+ }
49
+ function sendTimingCode(timing) {
50
+ if (typeof timing === "number") {
51
+ return timing;
52
+ }
53
+ return timing === "preFader" ? 1 : 0;
54
+ }
55
+
1
56
  // src/errors.ts
2
57
  var SonareError = class extends Error {
3
58
  constructor(code, codeName, message) {
@@ -39,6 +94,7 @@ function makeSonareError(raw, thrown) {
39
94
  }
40
95
  function wrapModuleErrors(raw) {
41
96
  const cache = /* @__PURE__ */ new Map();
97
+ const objectCache = /* @__PURE__ */ new WeakMap();
42
98
  const convert = (error) => {
43
99
  const ptr = nativeExceptionPtr(error);
44
100
  if (ptr !== null) {
@@ -46,6 +102,83 @@ function wrapModuleErrors(raw) {
46
102
  }
47
103
  throw error;
48
104
  };
105
+ const wrapNativeObject = (value) => {
106
+ if (value === null || typeof value !== "object") {
107
+ return value;
108
+ }
109
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof Promise) {
110
+ return value;
111
+ }
112
+ const objectValue = value;
113
+ const cached = objectCache.get(objectValue);
114
+ if (cached) {
115
+ return cached;
116
+ }
117
+ const methodCache = /* @__PURE__ */ new Map();
118
+ const wrapped = new Proxy(objectValue, {
119
+ get(target, prop, receiver) {
120
+ const member = Reflect.get(target, prop, receiver);
121
+ if (typeof member !== "function") {
122
+ return member;
123
+ }
124
+ const cachedMethod = methodCache.get(prop);
125
+ if (cachedMethod) {
126
+ return cachedMethod;
127
+ }
128
+ const method = member;
129
+ const wrappedMethod = (...args) => {
130
+ try {
131
+ return wrapNativeObject(Reflect.apply(method, target, args));
132
+ } catch (error) {
133
+ return convert(error);
134
+ }
135
+ };
136
+ methodCache.set(prop, wrappedMethod);
137
+ return wrappedMethod;
138
+ }
139
+ });
140
+ objectCache.set(objectValue, wrapped);
141
+ return wrapped;
142
+ };
143
+ const wrapFunction = (value) => {
144
+ const fnCache = /* @__PURE__ */ new Map();
145
+ return new Proxy(value, {
146
+ get(target, prop, receiver) {
147
+ const member = Reflect.get(target, prop, receiver);
148
+ if (typeof member !== "function") {
149
+ return member;
150
+ }
151
+ const cachedMember = fnCache.get(prop);
152
+ if (cachedMember) {
153
+ return cachedMember;
154
+ }
155
+ const fn = member;
156
+ const wrappedMember = (...args) => {
157
+ try {
158
+ return wrapNativeObject(Reflect.apply(fn, target, args));
159
+ } catch (error) {
160
+ return convert(error);
161
+ }
162
+ };
163
+ fnCache.set(prop, wrappedMember);
164
+ return wrappedMember;
165
+ },
166
+ apply(t, thisArg, args) {
167
+ try {
168
+ return wrapNativeObject(Reflect.apply(t, thisArg, args));
169
+ } catch (error) {
170
+ return convert(error);
171
+ }
172
+ },
173
+ construct(t, args, newTarget) {
174
+ try {
175
+ return wrapNativeObject(Reflect.construct(t, args, newTarget));
176
+ } catch (error) {
177
+ return convert(error);
178
+ }
179
+ }
180
+ });
181
+ };
49
182
  return new Proxy(raw, {
50
183
  get(target, prop, receiver) {
51
184
  const value = Reflect.get(target, prop, receiver);
@@ -56,23 +189,7 @@ function wrapModuleErrors(raw) {
56
189
  if (cached) {
57
190
  return cached;
58
191
  }
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
- });
192
+ const wrapped = wrapFunction(value);
76
193
  cache.set(prop, wrapped);
77
194
  return wrapped;
78
195
  }
@@ -88,58 +205,6 @@ function getSonareModule() {
88
205
  return wrappedModule;
89
206
  }
90
207
 
91
- // src/codes.ts
92
- function automationCurveCode(curve) {
93
- switch (curve) {
94
- case "linear":
95
- return 0;
96
- case "exponential":
97
- return 1;
98
- case "hold":
99
- return 2;
100
- case "s-curve":
101
- return 3;
102
- default:
103
- throw new Error(`Invalid automation curve: ${curve}`);
104
- }
105
- }
106
- function panLawCode(panLaw) {
107
- if (typeof panLaw === "number") {
108
- return panLaw;
109
- }
110
- switch (panLaw) {
111
- case "const4.5dB":
112
- return 1;
113
- case "const6dB":
114
- return 2;
115
- case "linear0dB":
116
- return 3;
117
- default:
118
- return 0;
119
- }
120
- }
121
- function panModeCode(panMode) {
122
- if (typeof panMode === "number") {
123
- return panMode;
124
- }
125
- switch (panMode) {
126
- case "stereoPan":
127
- case "stereo-pan":
128
- return 1;
129
- case "dualPan":
130
- case "dual-pan":
131
- return 2;
132
- default:
133
- return 0;
134
- }
135
- }
136
- function meterTapCode(tap) {
137
- return tap === "preFader" || tap === 0 ? 0 : 1;
138
- }
139
- function sendTimingCode(timing) {
140
- return timing === "preFader" || timing === 0 ? 0 : 1;
141
- }
142
-
143
208
  // src/mixer.ts
144
209
  var Mixer = class _Mixer {
145
210
  constructor(mixer) {
@@ -390,6 +455,13 @@ var Mixer = class _Mixer {
390
455
  setDualPan(stripIndex, leftPan, rightPan) {
391
456
  this.mixer.setDualPan(stripIndex, leftPan, rightPan);
392
457
  }
458
+ /**
459
+ * Set the strip's surround pan position, used when it feeds a >2-channel bus.
460
+ * Stored on the scene; inert until the surround DSP path applies it.
461
+ */
462
+ setSurroundPan(stripIndex, pan) {
463
+ this.mixer.setSurroundPan(stripIndex, pan);
464
+ }
393
465
  /**
394
466
  * Add a send to a strip after construction.
395
467
  *
@@ -732,9 +804,6 @@ function engineCapabilities() {
732
804
  };
733
805
  }
734
806
  var RealtimeEngine = class {
735
- nativeExt() {
736
- return this.native;
737
- }
738
807
  constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
739
808
  const module2 = getSonareModule();
740
809
  const capabilities = engineCapabilities();
@@ -761,8 +830,14 @@ var RealtimeEngine = class {
761
830
  setParameterSmoothed(paramId, value, renderFrame = -1) {
762
831
  this.native.setParameterSmoothed(paramId, value, renderFrame);
763
832
  }
833
+ setSoloMute(laneIndex, solo, mute, renderFrame = -1) {
834
+ this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
835
+ }
836
+ setMidiClips(clips) {
837
+ this.native.setMidiClips(clips);
838
+ }
764
839
  setBuiltinInstrument(config = {}, destinationId = config.destinationId ?? 0) {
765
- this.nativeExt().setBuiltinInstrument(destinationId, config);
840
+ this.native.setBuiltinInstrument(destinationId, config);
766
841
  }
767
842
  /**
768
843
  * Bind the patch-driven NativeSynth to a realtime MIDI destination. `patch`
@@ -774,7 +849,7 @@ var RealtimeEngine = class {
774
849
  * binding convenience, not part of the NativeSynth patch itself.
775
850
  */
776
851
  setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
777
- this.nativeExt().setSynthInstrument(destinationId, patch);
852
+ this.native.setSynthInstrument(destinationId, patch);
778
853
  }
779
854
  /**
780
855
  * Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
@@ -783,7 +858,7 @@ var RealtimeEngine = class {
783
858
  * not referenced afterwards. Replaces any previously loaded SoundFont.
784
859
  */
785
860
  loadSoundFont(data) {
786
- this.nativeExt().loadSoundFont(data);
861
+ this.native.loadSoundFont(data);
787
862
  }
788
863
  /**
789
864
  * Bind a GS-compatible SoundFont player to a realtime MIDI destination, fed
@@ -795,13 +870,13 @@ var RealtimeEngine = class {
795
870
  * synthesizer GM fallback bank (the data-free floor).
796
871
  */
797
872
  setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
798
- this.nativeExt().setSf2Instrument(destinationId, config);
873
+ this.native.setSf2Instrument(destinationId, config);
799
874
  }
800
875
  clearMidiInstrument(destinationId = 0) {
801
- this.nativeExt().clearMidiInstrument(destinationId);
876
+ this.native.clearMidiInstrument(destinationId);
802
877
  }
803
878
  midiInstrumentCount() {
804
- return this.nativeExt().midiInstrumentCount();
879
+ return this.native.midiInstrumentCount();
805
880
  }
806
881
  /**
807
882
  * Bind a live MIDI CC to an engine automation parameter. The MIDI event still
@@ -809,7 +884,7 @@ var RealtimeEngine = class {
809
884
  * mapped into [minValue, maxValue] for `paramId`.
810
885
  */
811
886
  bindMidiCc(channel, controller, paramId, options = {}) {
812
- this.nativeExt().bindMidiCc(
887
+ this.native.bindMidiCc(
813
888
  channel,
814
889
  controller,
815
890
  paramId,
@@ -818,42 +893,42 @@ var RealtimeEngine = class {
818
893
  );
819
894
  }
820
895
  clearMidiCcBindings() {
821
- this.nativeExt().clearMidiCcBindings();
896
+ this.native.clearMidiCcBindings();
822
897
  }
823
898
  midiCcBindingCount() {
824
- return this.nativeExt().midiCcBindingCount();
899
+ return this.native.midiCcBindingCount();
825
900
  }
826
901
  /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
827
902
  setMidiFx(destinationId, configJson) {
828
- this.nativeExt().setMidiFx(destinationId, configJson);
903
+ this.native.setMidiFx(destinationId, configJson);
829
904
  }
830
905
  clearMidiFx(destinationId = 0) {
831
- this.nativeExt().clearMidiFx(destinationId);
906
+ this.native.clearMidiFx(destinationId);
832
907
  }
833
908
  /** Enable the engine-owned live MIDI input source for a destination. */
834
909
  setMidiInputSource(destinationId = 0) {
835
- this.nativeExt().setMidiInputSource(destinationId);
910
+ this.native.setMidiInputSource(destinationId);
836
911
  }
837
912
  clearMidiInputSource() {
838
- this.nativeExt().clearMidiInputSource();
913
+ this.native.clearMidiInputSource();
839
914
  }
840
915
  midiInputPendingCount() {
841
- return this.nativeExt().midiInputPendingCount();
916
+ return this.native.midiInputPendingCount();
842
917
  }
843
918
  pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
844
- this.nativeExt().pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
919
+ this.native.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
845
920
  }
846
921
  pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
847
- this.nativeExt().pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
922
+ this.native.pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
848
923
  }
849
924
  pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
850
- this.nativeExt().pushMidiInputCc(group, channel, controller, value, portTimeSamples);
925
+ this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
851
926
  }
852
927
  pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
853
- this.nativeExt().pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
928
+ this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
854
929
  }
855
930
  pushMidiNoteOff(destinationId, group, channel, note, velocity = 0, renderFrame = -1) {
856
- this.nativeExt().pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
931
+ this.native.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
857
932
  }
858
933
  /**
859
934
  * Queue an immediate (live) MIDI control change to a MIDI destination
@@ -862,21 +937,21 @@ var RealtimeEngine = class {
862
937
  * immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
863
938
  */
864
939
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
865
- this.nativeExt().pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
940
+ this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
866
941
  }
867
942
  /**
868
943
  * Queue a MIDI panic (all-notes-off) releasing every sounding note at
869
944
  * `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
870
945
  */
871
946
  pushMidiPanic(renderFrame = -1) {
872
- this.nativeExt().pushMidiPanic(renderFrame);
947
+ this.native.pushMidiPanic(renderFrame);
873
948
  }
874
949
  /**
875
950
  * Remove all registered parameters (and their automation lanes). Control-thread
876
951
  * only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
877
952
  */
878
953
  clearParameters() {
879
- this.nativeExt().clearParameters();
954
+ this.native.clearParameters();
880
955
  }
881
956
  /** Read back the current transport state snapshot. */
882
957
  getTransportState() {
@@ -891,15 +966,33 @@ var RealtimeEngine = class {
891
966
  seekSample(timelineSample, renderFrame = -1) {
892
967
  this.native.seekSample(timelineSample, renderFrame);
893
968
  }
969
+ /**
970
+ * Snaps every in-flight parameter ramp (engine-level smoothed params, mixer
971
+ * lane fader/pan/gate, bus gains) to its target value. Offline renders call
972
+ * this after a priming process() block so the first audible block renders at
973
+ * settled values instead of ramping in from defaults.
974
+ */
975
+ settleParameters() {
976
+ this.native.settleParameters();
977
+ }
894
978
  seekPpq(ppq, renderFrame = -1) {
895
979
  this.native.seekPpq(ppq, renderFrame);
896
980
  }
897
981
  setTempo(bpm) {
898
982
  this.native.setTempo(bpm);
899
983
  }
984
+ setTempoSegments(segments) {
985
+ this.native.setTempoSegments([...segments]);
986
+ }
900
987
  setTimeSignature(numerator, denominator) {
901
988
  this.native.setTimeSignature(numerator, denominator);
902
989
  }
990
+ setTimeSignatureSegments(segments) {
991
+ this.native.setTimeSignatureSegments([...segments]);
992
+ }
993
+ sampleAtPpq(ppq) {
994
+ return Number(this.native.sampleAtPpq(ppq));
995
+ }
903
996
  setLoop(startPpq, endPpq, enabled = true) {
904
997
  this.native.setLoop(startPpq, endPpq, enabled);
905
998
  }
@@ -968,21 +1061,140 @@ var RealtimeEngine = class {
968
1061
  clipCount() {
969
1062
  return this.native.clipCount();
970
1063
  }
1064
+ setTrackLanes(lanes) {
1065
+ this.native.setTrackLanes(
1066
+ lanes.map((lane) => {
1067
+ if (typeof lane === "number") {
1068
+ return { trackId: lane };
1069
+ }
1070
+ if (!lane.sends) {
1071
+ return lane;
1072
+ }
1073
+ return {
1074
+ ...lane,
1075
+ sends: lane.sends.map((send) => ({
1076
+ ...send,
1077
+ // Post-fader (0) is the default for an omitted sendTiming.
1078
+ sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
1079
+ }))
1080
+ };
1081
+ })
1082
+ );
1083
+ }
1084
+ /**
1085
+ * Keys one insert of a lane strip from another lane's post-strip audio
1086
+ * (ducking/sidechainRouter inserts). sourceTrackId 0 removes the binding.
1087
+ */
1088
+ setLaneSidechain(trackId, insertIndex, sourceTrackId) {
1089
+ this.native.setLaneSidechain(trackId, insertIndex, sourceTrackId);
1090
+ }
1091
+ setTrackBuses(buses) {
1092
+ this.native.setTrackBuses(buses);
1093
+ }
1094
+ setBusStripJson(busId, sceneJson) {
1095
+ try {
1096
+ JSON.parse(sceneJson);
1097
+ } catch (error) {
1098
+ const message = error instanceof Error ? error.message : "invalid bus strip JSON";
1099
+ throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
1100
+ }
1101
+ this.native.setBusStripJson(busId, sceneJson);
1102
+ }
1103
+ setTrackStripJson(trackId, sceneJson) {
1104
+ try {
1105
+ JSON.parse(sceneJson);
1106
+ } catch (error) {
1107
+ const message = error instanceof Error ? error.message : "invalid track strip JSON";
1108
+ throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
1109
+ }
1110
+ this.native.setTrackStripJson(trackId, sceneJson);
1111
+ }
1112
+ setTrackStripEqBand(trackId, bandIndex, band) {
1113
+ this.native.setTrackStripEqBandJson(
1114
+ trackId,
1115
+ bandIndex,
1116
+ typeof band === "string" ? band : JSON.stringify(band)
1117
+ );
1118
+ }
1119
+ setTrackStripEqBandJson(trackId, bandIndex, bandJson) {
1120
+ this.native.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
1121
+ }
1122
+ setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass = false) {
1123
+ this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
1124
+ }
1125
+ setMasterStripJson(sceneJson) {
1126
+ try {
1127
+ JSON.parse(sceneJson);
1128
+ } catch (error) {
1129
+ const message = error instanceof Error ? error.message : "invalid master strip JSON";
1130
+ throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
1131
+ }
1132
+ this.native.setMasterStripJson(sceneJson);
1133
+ }
1134
+ setMasterStripEqBand(bandIndex, band) {
1135
+ this.native.setMasterStripEqBandJson(
1136
+ bandIndex,
1137
+ typeof band === "string" ? band : JSON.stringify(band)
1138
+ );
1139
+ }
1140
+ setMasterStripEqBandJson(bandIndex, bandJson) {
1141
+ this.native.setMasterStripEqBandJson(bandIndex, bandJson);
1142
+ }
1143
+ setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass = false) {
1144
+ this.native.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
1145
+ }
1146
+ /**
1147
+ * Changes one track-strip insert parameter in realtime, addressed by the
1148
+ * processor's JSON-key parameter name (see {@link masteringInsertParamInfo}).
1149
+ * Applied at the next block head via the engine command queue; safe during
1150
+ * playback. Throws if the track, insert, or name is unknown, the param is not
1151
+ * realtime-safe, or the command queue is full.
1152
+ */
1153
+ setTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1154
+ this.native.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1155
+ }
1156
+ /** Master-strip counterpart of {@link setTrackStripInsertParamByName}. */
1157
+ setMasterStripInsertParamByName(insertIndex, paramName, value) {
1158
+ this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
1159
+ }
1160
+ /** Sets a track lane strip's pan position in realtime (glitch-free). */
1161
+ setTrackStripPan(trackId, pan) {
1162
+ this.native.setTrackStripPan(trackId, pan);
1163
+ }
1164
+ /** Sets a track lane strip's pan law in realtime. */
1165
+ setTrackStripPanLaw(trackId, panLaw) {
1166
+ this.native.setTrackStripPanLaw(trackId, panLawCode(panLaw));
1167
+ }
1168
+ /** Sets a track lane strip's pan mode in realtime. */
1169
+ setTrackStripPanMode(trackId, panMode) {
1170
+ this.native.setTrackStripPanMode(trackId, panModeCode(panMode));
1171
+ }
1172
+ /** Sets a track lane strip's dual-pan left/right positions in realtime. */
1173
+ setTrackStripDualPan(trackId, leftPan, rightPan) {
1174
+ this.native.setTrackStripDualPan(trackId, leftPan, rightPan);
1175
+ }
1176
+ /**
1177
+ * Sets a track lane strip's inter-channel alignment delay (whole samples).
1178
+ * Adjusts strip latency, so PDC and reported graph latency are refreshed.
1179
+ */
1180
+ setTrackStripChannelDelaySamples(trackId, delaySamples) {
1181
+ this.native.setTrackStripChannelDelaySamples(trackId, delaySamples);
1182
+ }
971
1183
  createClipPageProvider(numChannels, numSamples, pageFrames) {
972
- const id = this.nativeExt().createClipPageProvider(numChannels, numSamples, pageFrames);
1184
+ const id = this.native.createClipPageProvider(numChannels, numSamples, pageFrames);
973
1185
  return new ClipPageProvider(this, id);
974
1186
  }
975
1187
  supplyClipPage(providerId, pageIndex, channels) {
976
- this.nativeExt().supplyClipPage(providerId, pageIndex, channels);
1188
+ this.native.supplyClipPage(providerId, pageIndex, channels);
977
1189
  }
978
1190
  clearClipPage(providerId, pageIndex) {
979
- this.nativeExt().clearClipPage(providerId, pageIndex);
1191
+ this.native.clearClipPage(providerId, pageIndex);
980
1192
  }
981
1193
  destroyClipPageProvider(providerId) {
982
- this.nativeExt().destroyClipPageProvider(providerId);
1194
+ this.native.destroyClipPageProvider(providerId);
983
1195
  }
984
1196
  popClipPageRequest() {
985
- return this.nativeExt().popClipPageRequest();
1197
+ return this.native.popClipPageRequest();
986
1198
  }
987
1199
  setCaptureBuffer(numChannels, capacityFrames) {
988
1200
  this.native.setCaptureBuffer(numChannels, capacityFrames);
@@ -1057,6 +1269,30 @@ var RealtimeEngine = class {
1057
1269
  drainMeterTelemetry(maxRecords = 1024) {
1058
1270
  return this.native.drainMeterTelemetry(maxRecords);
1059
1271
  }
1272
+ /**
1273
+ * Drains pending meter telemetry as per-plane (wide) records for a surround
1274
+ * target. Use this for a surround mix target; {@link drainMeterTelemetry}
1275
+ * stays the stereo fast path. The two share one queue — call only one per
1276
+ * target. The live AudioWorklet path owns the queue via the stereo drain, so
1277
+ * this wide drain is for an offline (non-worklet) engine instance; per-plane
1278
+ * surround meters are not delivered over the live worklet meter ring.
1279
+ */
1280
+ drainMeterTelemetryWide(maxRecords = 1024) {
1281
+ return this.native.drainMeterTelemetryWide(maxRecords);
1282
+ }
1283
+ /**
1284
+ * Enables per-target spectrum + vectorscope capture. @param intervalFrames is
1285
+ * the minimum render-frame gap between snapshots (0 disables). @param bandCount
1286
+ * is the FFT band resolution (1..64); changing it re-prepares the tap. Returns
1287
+ * the band count actually applied.
1288
+ */
1289
+ configureScopeTelemetry(intervalFrames, bandCount) {
1290
+ return this.native.configureScopeTelemetry(intervalFrames, bandCount);
1291
+ }
1292
+ /** Drains pending spectrum + vectorscope snapshots (per mix target). */
1293
+ drainScopeTelemetry(maxRecords = 1024) {
1294
+ return this.native.drainScopeTelemetry(maxRecords);
1295
+ }
1060
1296
  destroy() {
1061
1297
  this.native.delete();
1062
1298
  }
@@ -1100,7 +1336,7 @@ async function init(options) {
1100
1336
  }
1101
1337
  initPromise = (async () => {
1102
1338
  try {
1103
- const createModule = (await import("./sonare.js")).default;
1339
+ const createModule = options?.moduleFactory ?? (await import("./sonare.js")).default;
1104
1340
  module = await createModule(options);
1105
1341
  setSonareModule(module);
1106
1342
  } catch (error) {
@@ -1114,10 +1350,24 @@ function isInitialized() {
1114
1350
  return module !== null;
1115
1351
  }
1116
1352
 
1117
- // src/worklet.ts
1353
+ // src/worklet/protocol.ts
1354
+ var ENGINE_MIXER_TARGET_BASE = 1297612800;
1355
+ var ENGINE_MIXER_PARAM_FADER_DB = 1;
1356
+ var ENGINE_MIXER_PARAM_PAN = 2;
1357
+ function engineMixerLaneTarget(laneIndex, paramKind) {
1358
+ return ENGINE_MIXER_TARGET_BASE | (laneIndex & 255) << 8 | paramKind & 255;
1359
+ }
1360
+ function engineMixerBusTarget(busIndex, paramKind) {
1361
+ return ENGINE_MIXER_TARGET_BASE | (254 - busIndex & 255) << 8 | paramKind & 255;
1362
+ }
1363
+ function engineMixerMasterTarget(paramKind) {
1364
+ return ENGINE_MIXER_TARGET_BASE | 255 << 8 | paramKind & 255;
1365
+ }
1118
1366
  var SONARE_METER_RING_HEADER_INTS = 4;
1119
- var SONARE_METER_RING_RECORD_FLOATS = 7;
1367
+ var SONARE_METER_RING_RECORD_FLOATS = 14;
1120
1368
  var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
1369
+ var SONARE_SCOPE_RING_HEADER_INTS = 6;
1370
+ var SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS = 5;
1121
1371
  var SONARE_FRAME_LANE_BASE = 16777216;
1122
1372
  function encodeFrameLo(frame) {
1123
1373
  const f = Math.max(0, Math.floor(frame));
@@ -1176,51 +1426,12 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
1176
1426
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["SmoothedParameterCapacity"] = 13] = "SmoothedParameterCapacity";
1177
1427
  return SonareEngineTelemetryError2;
1178
1428
  })(SonareEngineTelemetryError || {});
1179
- var DEFAULT_METRONOME_CONFIG = {
1180
- beatGain: 0.35,
1181
- accentGain: 0.7,
1182
- clickSamples: 96
1183
- };
1184
- function resolveMetronomeConfig(config) {
1185
- return {
1186
- beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
1187
- accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
1188
- clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples
1189
- };
1190
- }
1191
1429
  function toDb(value) {
1192
1430
  return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
1193
1431
  }
1194
1432
  function isRecord(value) {
1195
1433
  return typeof value === "object" && value !== null;
1196
1434
  }
1197
- function isWorkletMessage(value) {
1198
- if (!isRecord(value) || typeof value.type !== "string") {
1199
- return false;
1200
- }
1201
- return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
1202
- }
1203
- function isEngineCommandRecord(value) {
1204
- return isRecord(value) && typeof value.type === "number";
1205
- }
1206
- function isEngineSyncMessage(value) {
1207
- if (!isRecord(value) || typeof value.type !== "string") {
1208
- return false;
1209
- }
1210
- return value.type === "syncClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation";
1211
- }
1212
- function isRealtimeVoiceChangerMessage(value) {
1213
- if (!isRecord(value) || typeof value.type !== "string") {
1214
- return false;
1215
- }
1216
- return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
1217
- }
1218
- function isEngineTelemetryRecord(value) {
1219
- 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
- }
1221
- 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";
1223
- }
1224
1435
  function sonareMeterRingBufferByteLength(capacity) {
1225
1436
  const clampedCapacity = Math.max(1, Math.floor(capacity));
1226
1437
  return SONARE_METER_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_METER_RING_RECORD_FLOATS * Float32Array.BYTES_PER_ELEMENT;
@@ -1237,19 +1448,27 @@ function createSonareMeterRingBuffer(capacity = 128) {
1237
1448
  }
1238
1449
  function readSonareMeterRingBuffer(ring, readIndex = 0) {
1239
1450
  const writeIndex = Atomics.load(ring.header, 0);
1451
+ const recordFloats = Atomics.load(ring.header, 2) || SONARE_METER_RING_RECORD_FLOATS;
1240
1452
  const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
1241
1453
  const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
1242
1454
  const meters = [];
1243
1455
  for (let index = firstReadable; index < writeIndex; index++) {
1244
- const offset = index % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1456
+ const offset = index % ring.capacity * recordFloats;
1245
1457
  meters.push({
1246
1458
  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]
1459
+ frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
1460
+ targetId: ring.records[offset + 2],
1461
+ peakDbL: ring.records[offset + 3],
1462
+ peakDbR: ring.records[offset + 4],
1463
+ rmsDbL: ring.records[offset + 5],
1464
+ rmsDbR: ring.records[offset + 6],
1465
+ correlation: ring.records[offset + 7],
1466
+ truePeakDbL: ring.records[offset + 8],
1467
+ truePeakDbR: ring.records[offset + 9],
1468
+ momentaryLufs: ring.records[offset + 10],
1469
+ shortTermLufs: ring.records[offset + 11],
1470
+ integratedLufs: ring.records[offset + 12],
1471
+ gainReductionDb: ring.records[offset + 13]
1253
1472
  });
1254
1473
  }
1255
1474
  return { nextReadIndex: writeIndex, meters };
@@ -1298,29 +1517,125 @@ function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
1298
1517
  }
1299
1518
  return { nextReadIndex: writeIndex, spectra };
1300
1519
  }
1301
- function sonareEngineCommandRingBufferByteLength(capacity) {
1302
- const clampedCapacity = Math.max(1, Math.floor(capacity));
1303
- return SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_ENGINE_COMMAND_RECORD_BYTES;
1520
+ function sonareScopeRingRecordFloats(bands, maxPoints) {
1521
+ return SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bands + 2 * maxPoints;
1304
1522
  }
1305
- function sonareEngineTelemetryRingBufferByteLength(capacity) {
1523
+ function sonareScopeRingBufferByteLength(capacity, bands = 48, maxPoints = 32) {
1306
1524
  const clampedCapacity = Math.max(1, Math.floor(capacity));
1307
- return SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES;
1525
+ const clampedBands = Math.max(1, Math.floor(bands));
1526
+ const clampedPoints = Math.max(0, Math.floor(maxPoints));
1527
+ return SONARE_SCOPE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * sonareScopeRingRecordFloats(clampedBands, clampedPoints) * Float32Array.BYTES_PER_ELEMENT;
1308
1528
  }
1309
- function createSonareEngineCommandRingBuffer(capacity = 128) {
1529
+ function createSonareScopeRingBuffer(capacity = 64, bands = 48, maxPoints = 32) {
1310
1530
  const clampedCapacity = Math.max(1, Math.floor(capacity));
1531
+ const clampedBands = Math.max(1, Math.floor(bands));
1532
+ const clampedPoints = Math.max(0, Math.floor(maxPoints));
1311
1533
  const sharedBuffer = new SharedArrayBuffer(
1312
- sonareEngineCommandRingBufferByteLength(clampedCapacity)
1534
+ sonareScopeRingBufferByteLength(clampedCapacity, clampedBands, clampedPoints)
1313
1535
  );
1314
- const ring = engineRingFromSharedBuffer(
1536
+ const ring = scopeRingFromSharedBuffer(
1315
1537
  sharedBuffer,
1316
- SONARE_ENGINE_COMMAND_RECORD_BYTES,
1317
- clampedCapacity
1538
+ clampedCapacity,
1539
+ clampedBands,
1540
+ clampedPoints
1318
1541
  );
1319
- return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
1320
- }
1321
- function createSonareEngineTelemetryRingBuffer(capacity = 128) {
1322
- const clampedCapacity = Math.max(1, Math.floor(capacity));
1323
- const sharedBuffer = new SharedArrayBuffer(
1542
+ Atomics.store(ring.header, 0, 0);
1543
+ Atomics.store(ring.header, 1, clampedCapacity);
1544
+ Atomics.store(ring.header, 2, ring.recordFloats);
1545
+ Atomics.store(ring.header, 3, clampedBands);
1546
+ Atomics.store(ring.header, 4, clampedPoints);
1547
+ Atomics.store(ring.header, 5, 0);
1548
+ return {
1549
+ sharedBuffer,
1550
+ header: ring.header,
1551
+ records: ring.records,
1552
+ capacity: ring.capacity,
1553
+ bands: ring.bands,
1554
+ maxPoints: ring.maxPoints
1555
+ };
1556
+ }
1557
+ function readSonareScopeRingBuffer(ring, readIndex = 0) {
1558
+ const writeIndex = Atomics.load(ring.header, 0);
1559
+ const bands = Atomics.load(ring.header, 3) || ring.bands;
1560
+ const maxPoints = Atomics.load(ring.header, 4);
1561
+ const recordFloats = Atomics.load(ring.header, 2) || sonareScopeRingRecordFloats(bands, maxPoints);
1562
+ const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
1563
+ const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
1564
+ const scopes = [];
1565
+ for (let index = firstReadable; index < writeIndex; index++) {
1566
+ const offset = index % ring.capacity * recordFloats;
1567
+ const bandCount = Math.min(bands, Math.max(0, ring.records[offset + 3]));
1568
+ const pointCount = Math.min(maxPoints, Math.max(0, ring.records[offset + 4]));
1569
+ const bandsView = new Float32Array(bandCount);
1570
+ bandsView.set(
1571
+ ring.records.subarray(
1572
+ offset + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS,
1573
+ offset + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bandCount
1574
+ )
1575
+ );
1576
+ const pointsBase = offset + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bands;
1577
+ const pointsView = new Float32Array(pointCount * 2);
1578
+ pointsView.set(ring.records.subarray(pointsBase, pointsBase + pointCount * 2));
1579
+ scopes.push({
1580
+ type: "scope",
1581
+ frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
1582
+ targetId: ring.records[offset + 2],
1583
+ bands: bandsView,
1584
+ points: pointsView
1585
+ });
1586
+ }
1587
+ return { nextReadIndex: writeIndex, scopes };
1588
+ }
1589
+ function scopeRingFromSharedBuffer(sharedBuffer, fallbackCapacity, fallbackBands, fallbackMaxPoints) {
1590
+ const headerBytes = SONARE_SCOPE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
1591
+ const header = new Int32Array(sharedBuffer, 0, SONARE_SCOPE_RING_HEADER_INTS);
1592
+ const existingCapacity = Atomics.load(header, 1);
1593
+ const existingBands = Atomics.load(header, 3);
1594
+ const existingMaxPoints = Atomics.load(header, 4);
1595
+ const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
1596
+ const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 48));
1597
+ const maxPoints = Math.max(0, Math.floor(existingMaxPoints || (fallbackMaxPoints ?? 32)));
1598
+ const recordFloats = sonareScopeRingRecordFloats(bands, maxPoints);
1599
+ const minBytes = sonareScopeRingBufferByteLength(capacity, bands, maxPoints);
1600
+ if (sharedBuffer.byteLength < minBytes) {
1601
+ throw new Error("scopeSharedBuffer is too small for the requested ring capacity.");
1602
+ }
1603
+ Atomics.store(header, 1, capacity);
1604
+ Atomics.store(header, 2, recordFloats);
1605
+ Atomics.store(header, 3, bands);
1606
+ Atomics.store(header, 4, maxPoints);
1607
+ return {
1608
+ header,
1609
+ records: new Float32Array(sharedBuffer, headerBytes, capacity * recordFloats),
1610
+ capacity,
1611
+ bands,
1612
+ maxPoints,
1613
+ recordFloats
1614
+ };
1615
+ }
1616
+ function sonareEngineCommandRingBufferByteLength(capacity) {
1617
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
1618
+ return SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_ENGINE_COMMAND_RECORD_BYTES;
1619
+ }
1620
+ function sonareEngineTelemetryRingBufferByteLength(capacity) {
1621
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
1622
+ return SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES;
1623
+ }
1624
+ function createSonareEngineCommandRingBuffer(capacity = 128) {
1625
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
1626
+ const sharedBuffer = new SharedArrayBuffer(
1627
+ sonareEngineCommandRingBufferByteLength(clampedCapacity)
1628
+ );
1629
+ const ring = engineRingFromSharedBuffer(
1630
+ sharedBuffer,
1631
+ SONARE_ENGINE_COMMAND_RECORD_BYTES,
1632
+ clampedCapacity
1633
+ );
1634
+ return { sharedBuffer, header: ring.header, view: ring.view, capacity: ring.capacity };
1635
+ }
1636
+ function createSonareEngineTelemetryRingBuffer(capacity = 128) {
1637
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
1638
+ const sharedBuffer = new SharedArrayBuffer(
1324
1639
  sonareEngineTelemetryRingBufferByteLength(clampedCapacity)
1325
1640
  );
1326
1641
  const ring = engineRingFromSharedBuffer(
@@ -1509,17 +1824,81 @@ function telemetryFromEngine(telemetry) {
1509
1824
  function meterFromEngine(meter) {
1510
1825
  return {
1511
1826
  type: "meter",
1827
+ targetId: meter.targetId,
1512
1828
  frame: meter.renderFrame,
1513
1829
  peakDbL: meter.peakDbL,
1514
1830
  peakDbR: meter.peakDbR,
1515
1831
  rmsDbL: meter.rmsDbL,
1516
1832
  rmsDbR: meter.rmsDbR,
1517
- correlation: meter.correlation
1833
+ correlation: meter.correlation,
1834
+ truePeakDbL: meter.truePeakDbL,
1835
+ truePeakDbR: meter.truePeakDbR,
1836
+ momentaryLufs: meter.momentaryLufs,
1837
+ shortTermLufs: meter.shortTermLufs,
1838
+ integratedLufs: meter.integratedLufs,
1839
+ gainReductionDb: meter.gainReductionDb
1518
1840
  };
1519
1841
  }
1520
1842
  function magnitudeToDb(value) {
1521
1843
  return value > 1e-12 ? 20 * Math.log10(value) : -120;
1522
1844
  }
1845
+
1846
+ // src/worklet/guards.ts
1847
+ function isWorkletMessage(value) {
1848
+ if (!isRecord(value) || typeof value.type !== "string") {
1849
+ return false;
1850
+ }
1851
+ return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
1852
+ }
1853
+ function isEngineCommandRecord(value) {
1854
+ return isRecord(value) && typeof value.type === "number";
1855
+ }
1856
+ function isEngineSyncMessage(value) {
1857
+ if (!isRecord(value) || typeof value.type !== "string") {
1858
+ return false;
1859
+ }
1860
+ 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 === "syncTrackStripInsertParamByName" || value.type === "syncMasterStripInsertParamByName" || value.type === "syncTrackStripPan" || value.type === "syncTrackStripPanLaw" || value.type === "syncTrackStripPanMode" || value.type === "syncTrackStripDualPan" || value.type === "syncTrackStripChannelDelaySamples" || value.type === "syncBuiltinInstrument" || value.type === "syncSynthInstrument" || value.type === "syncSf2Instrument" || value.type === "syncLoadSoundFont" || value.type === "syncMidiNoteOn" || value.type === "syncMidiNoteOff" || value.type === "syncMidiCc" || value.type === "syncMidiPanic";
1861
+ }
1862
+ function isEngineCaptureRequestMessage(value) {
1863
+ return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
1864
+ }
1865
+ function isEngineCaptureResponseMessage(value) {
1866
+ return isRecord(value) && value.type === "captureResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
1867
+ }
1868
+ function isEngineTransportRequestMessage(value) {
1869
+ return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
1870
+ }
1871
+ function isEngineTransportResponseMessage(value) {
1872
+ return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
1873
+ }
1874
+ function isRealtimeVoiceChangerMessage(value) {
1875
+ if (!isRecord(value) || typeof value.type !== "string") {
1876
+ return false;
1877
+ }
1878
+ return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
1879
+ }
1880
+ function isEngineTelemetryRecord(value) {
1881
+ 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";
1882
+ }
1883
+ function isMeterSnapshot(value) {
1884
+ 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);
1885
+ }
1886
+
1887
+ // src/worklet/messages.ts
1888
+ var DEFAULT_METRONOME_CONFIG = {
1889
+ beatGain: 0.35,
1890
+ accentGain: 0.7,
1891
+ clickSamples: 96
1892
+ };
1893
+ function resolveMetronomeConfig(config) {
1894
+ return {
1895
+ beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
1896
+ accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
1897
+ clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples
1898
+ };
1899
+ }
1900
+
1901
+ // src/worklet.ts
1523
1902
  var SonareWorkletProcessor = class {
1524
1903
  constructor(options, transport) {
1525
1904
  this.closed = false;
@@ -1669,12 +2048,19 @@ var SonareWorkletProcessor = class {
1669
2048
  const denominator = Math.sqrt(sumL * sumR);
1670
2049
  const meter = {
1671
2050
  type: "meter",
2051
+ targetId: 0,
1672
2052
  frame: this.processedFrames,
1673
2053
  peakDbL: toDb(peakL),
1674
2054
  peakDbR: toDb(peakR),
1675
2055
  rmsDbL: toDb(rmsL),
1676
2056
  rmsDbR: toDb(rmsR),
1677
- correlation: denominator > 0 ? sumLR / denominator : 0
2057
+ correlation: denominator > 0 ? sumLR / denominator : 0,
2058
+ truePeakDbL: toDb(peakL),
2059
+ truePeakDbR: toDb(peakR),
2060
+ momentaryLufs: Number.NaN,
2061
+ shortTermLufs: Number.NaN,
2062
+ integratedLufs: Number.NaN,
2063
+ gainReductionDb: Number.NaN
1678
2064
  };
1679
2065
  this.transport.onMeter?.(meter);
1680
2066
  if (this.meterRing) {
@@ -1691,12 +2077,19 @@ var SonareWorkletProcessor = class {
1691
2077
  const writeIndex = Atomics.load(ring.header, 0);
1692
2078
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1693
2079
  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);
2080
+ ring.records[offset + 1] = encodeFrameHi(meter.frame);
2081
+ ring.records[offset + 2] = meter.targetId;
2082
+ ring.records[offset + 3] = meter.peakDbL;
2083
+ ring.records[offset + 4] = meter.peakDbR;
2084
+ ring.records[offset + 5] = meter.rmsDbL;
2085
+ ring.records[offset + 6] = meter.rmsDbR;
2086
+ ring.records[offset + 7] = meter.correlation;
2087
+ ring.records[offset + 8] = meter.truePeakDbL;
2088
+ ring.records[offset + 9] = meter.truePeakDbR;
2089
+ ring.records[offset + 10] = meter.momentaryLufs;
2090
+ ring.records[offset + 11] = meter.shortTermLufs;
2091
+ ring.records[offset + 12] = meter.integratedLufs;
2092
+ ring.records[offset + 13] = meter.gainReductionDb;
1700
2093
  Atomics.store(ring.header, 0, writeIndex + 1);
1701
2094
  }
1702
2095
  publishSpectrum(left, right) {
@@ -1761,6 +2154,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1761
2154
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
1762
2155
  // SetMetronome command only toggles enabled state; the config arrives here.
1763
2156
  this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
2157
+ this.liveClips = /* @__PURE__ */ new Map();
1764
2158
  this.sampleRate = options.sampleRate ?? 48e3;
1765
2159
  this.blockSize = options.blockSize ?? 128;
1766
2160
  this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
@@ -1778,12 +2172,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1778
2172
  options.telemetryRingCapacity
1779
2173
  ) : void 0;
1780
2174
  this.meterRing = options.meterSharedBuffer ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity) : void 0;
2175
+ this.scopeRing = options.scopeSharedBuffer ? scopeRingFromSharedBuffer(
2176
+ options.scopeSharedBuffer,
2177
+ options.scopeRingCapacity,
2178
+ options.scopeBands
2179
+ ) : void 0;
1781
2180
  this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
1782
2181
  this.engine.prepareChannels(this.channelCount, this.blockSize);
1783
2182
  this.channelBuffers = new Array(this.channelCount);
1784
2183
  for (let ch = 0; ch < this.channelCount; ch++) {
1785
2184
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
1786
2185
  }
2186
+ if (this.scopeRing) {
2187
+ const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
2188
+ this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
2189
+ }
1787
2190
  }
1788
2191
  process(inputs, outputs) {
1789
2192
  if (this.closed) {
@@ -1841,6 +2244,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1841
2244
  }
1842
2245
  this.publishTelemetry();
1843
2246
  this.publishMeters();
2247
+ this.publishScope();
1844
2248
  return true;
1845
2249
  }
1846
2250
  reacquireChannelBuffers() {
@@ -1864,8 +2268,28 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1864
2268
  }
1865
2269
  switch (message.type) {
1866
2270
  case "syncClips":
2271
+ this.liveClips.clear();
2272
+ for (const clip of message.clips) {
2273
+ if (clip.id !== void 0) {
2274
+ this.liveClips.set(clip.id, clip);
2275
+ }
2276
+ }
1867
2277
  this.engine.setClips(message.clips);
1868
2278
  break;
2279
+ case "syncClipsDelta":
2280
+ for (const clipId of message.removeIds) {
2281
+ this.liveClips.delete(clipId);
2282
+ }
2283
+ for (const clip of message.upserts) {
2284
+ if (clip.id !== void 0) {
2285
+ this.liveClips.set(clip.id, clip);
2286
+ }
2287
+ }
2288
+ this.engine.setClips(Array.from(this.liveClips.values()));
2289
+ break;
2290
+ case "syncMidiClips":
2291
+ this.engine.setMidiClips(message.clips);
2292
+ break;
1869
2293
  case "syncMarkers":
1870
2294
  this.engine.setMarkers(message.markers);
1871
2295
  break;
@@ -1876,6 +2300,217 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1876
2300
  case "syncAutomation":
1877
2301
  this.engine.setAutomationLane(message.paramId, message.points);
1878
2302
  break;
2303
+ case "syncTempo":
2304
+ if (message.tempoSegments) {
2305
+ this.engine.setTempoSegments(message.tempoSegments);
2306
+ } else {
2307
+ this.engine.setTempo(message.bpm);
2308
+ }
2309
+ if (message.timeSignatureSegments) {
2310
+ this.engine.setTimeSignatureSegments(message.timeSignatureSegments);
2311
+ } else {
2312
+ this.engine.setTimeSignature(
2313
+ message.timeSignature.numerator,
2314
+ message.timeSignature.denominator
2315
+ );
2316
+ }
2317
+ break;
2318
+ case "syncMixer":
2319
+ if (message.buses) {
2320
+ this.engine.setTrackBuses(message.buses);
2321
+ }
2322
+ this.engine.setTrackLanes(message.lanes);
2323
+ for (const strip of message.trackStrips ?? []) {
2324
+ this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
2325
+ }
2326
+ for (const strip of message.busStrips ?? []) {
2327
+ this.engine.setBusStripJson(strip.busId, strip.sceneJson);
2328
+ }
2329
+ if (message.masterStripJson) {
2330
+ this.engine.setMasterStripJson(message.masterStripJson);
2331
+ }
2332
+ for (const binding of message.laneSidechains ?? []) {
2333
+ this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
2334
+ }
2335
+ break;
2336
+ case "syncCapture":
2337
+ this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
2338
+ this.engine.setCaptureSource(message.source);
2339
+ this.engine.setRecordOffsetSamples(message.recordOffsetSamples);
2340
+ this.engine.setInputMonitor(message.inputMonitor.enabled, message.inputMonitor.gain);
2341
+ break;
2342
+ case "syncTrackStripEqBand":
2343
+ this.engine.setTrackStripEqBandJson(message.trackId, message.bandIndex, message.bandJson);
2344
+ break;
2345
+ case "syncMasterStripEqBand":
2346
+ this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
2347
+ break;
2348
+ case "syncTrackStripInsertBypassed":
2349
+ this.engine.setTrackStripInsertBypassed(
2350
+ message.trackId,
2351
+ message.insertIndex,
2352
+ message.bypassed,
2353
+ message.resetOnBypass
2354
+ );
2355
+ break;
2356
+ case "syncMasterStripInsertBypassed":
2357
+ this.engine.setMasterStripInsertBypassed(
2358
+ message.insertIndex,
2359
+ message.bypassed,
2360
+ message.resetOnBypass
2361
+ );
2362
+ break;
2363
+ case "syncTrackStripInsertParamByName":
2364
+ this.engine.setTrackStripInsertParamByName(
2365
+ message.trackId,
2366
+ message.insertIndex,
2367
+ message.paramName,
2368
+ message.value
2369
+ );
2370
+ break;
2371
+ case "syncMasterStripInsertParamByName":
2372
+ this.engine.setMasterStripInsertParamByName(
2373
+ message.insertIndex,
2374
+ message.paramName,
2375
+ message.value
2376
+ );
2377
+ break;
2378
+ case "syncTrackStripPan":
2379
+ this.engine.setTrackStripPan(message.trackId, message.pan);
2380
+ break;
2381
+ case "syncTrackStripPanLaw":
2382
+ this.engine.setTrackStripPanLaw(message.trackId, message.panLaw);
2383
+ break;
2384
+ case "syncTrackStripPanMode":
2385
+ this.engine.setTrackStripPanMode(message.trackId, message.panMode);
2386
+ break;
2387
+ case "syncTrackStripDualPan":
2388
+ this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
2389
+ break;
2390
+ case "syncTrackStripChannelDelaySamples":
2391
+ this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
2392
+ break;
2393
+ case "syncBuiltinInstrument":
2394
+ this.engine.setBuiltinInstrument(message.config, message.destinationId);
2395
+ break;
2396
+ case "syncSynthInstrument":
2397
+ this.engine.setSynthInstrument(message.patch, message.destinationId);
2398
+ break;
2399
+ case "syncLoadSoundFont":
2400
+ this.engine.loadSoundFont(message.data);
2401
+ break;
2402
+ case "syncSf2Instrument":
2403
+ this.engine.setSf2Instrument(message.config, message.destinationId);
2404
+ break;
2405
+ case "syncMidiNoteOn":
2406
+ this.engine.pushMidiNoteOn(
2407
+ message.destinationId,
2408
+ message.group,
2409
+ message.channel,
2410
+ message.note,
2411
+ message.velocity,
2412
+ message.renderFrame
2413
+ );
2414
+ break;
2415
+ case "syncMidiNoteOff":
2416
+ this.engine.pushMidiNoteOff(
2417
+ message.destinationId,
2418
+ message.group,
2419
+ message.channel,
2420
+ message.note,
2421
+ message.velocity,
2422
+ message.renderFrame
2423
+ );
2424
+ break;
2425
+ case "syncMidiCc":
2426
+ this.engine.pushMidiCc(
2427
+ message.destinationId,
2428
+ message.group,
2429
+ message.channel,
2430
+ message.controller,
2431
+ message.value,
2432
+ message.renderFrame
2433
+ );
2434
+ break;
2435
+ case "syncMidiPanic":
2436
+ this.engine.pushMidiPanic(message.renderFrame);
2437
+ break;
2438
+ }
2439
+ }
2440
+ receiveCaptureRequest(message) {
2441
+ if (this.closed) {
2442
+ return;
2443
+ }
2444
+ try {
2445
+ if (message.op === "status") {
2446
+ const status = this.engine.captureStatus();
2447
+ this.transport?.postMessage?.({
2448
+ type: "captureResponse",
2449
+ requestId: message.requestId,
2450
+ ok: true,
2451
+ status: {
2452
+ capturedFrames: status.capturedFrames,
2453
+ overflowCount: status.overflowCount,
2454
+ armed: status.armed,
2455
+ punchEnabled: status.punchEnabled,
2456
+ source: status.source,
2457
+ recordOffsetSamples: status.recordOffsetSamples
2458
+ }
2459
+ });
2460
+ return;
2461
+ }
2462
+ if (message.op === "read") {
2463
+ const captured = this.engine.capturedAudio();
2464
+ const channels = [];
2465
+ for (let ch = 0; ch < captured.length; ch++) {
2466
+ const source = captured[ch];
2467
+ const copy = [];
2468
+ for (let i = 0; i < source.length; i++) {
2469
+ copy.push(Number(source[i]));
2470
+ }
2471
+ channels.push(copy);
2472
+ }
2473
+ this.transport?.postMessage?.({
2474
+ type: "captureResponse",
2475
+ requestId: message.requestId,
2476
+ ok: true,
2477
+ channels
2478
+ });
2479
+ return;
2480
+ }
2481
+ this.engine.resetCapture();
2482
+ this.transport?.postMessage?.({
2483
+ type: "captureResponse",
2484
+ requestId: message.requestId,
2485
+ ok: true
2486
+ });
2487
+ } catch (error) {
2488
+ this.transport?.postMessage?.({
2489
+ type: "captureResponse",
2490
+ requestId: message.requestId,
2491
+ ok: false,
2492
+ error: error instanceof Error ? error.message : String(error)
2493
+ });
2494
+ }
2495
+ }
2496
+ receiveTransportRequest(message) {
2497
+ if (this.closed) {
2498
+ return;
2499
+ }
2500
+ try {
2501
+ this.transport?.postMessage?.({
2502
+ type: "transportResponse",
2503
+ requestId: message.requestId,
2504
+ ok: true,
2505
+ state: this.engine.getTransportState()
2506
+ });
2507
+ } catch (error) {
2508
+ this.transport?.postMessage?.({
2509
+ type: "transportResponse",
2510
+ requestId: message.requestId,
2511
+ ok: false,
2512
+ error: error instanceof Error ? error.message : String(error)
2513
+ });
1879
2514
  }
1880
2515
  }
1881
2516
  destroy() {
@@ -1956,6 +2591,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1956
2591
  case 17 /* SeekMarker */:
1957
2592
  this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
1958
2593
  break;
2594
+ case 10 /* SetSoloMute */:
2595
+ this.engine.setSoloMute(
2596
+ Math.trunc(Number(command.targetId ?? 0)),
2597
+ Boolean((Number(command.argInt ?? 0) & 2) !== 0),
2598
+ Boolean((Number(command.argInt ?? 0) & 1) !== 0),
2599
+ sampleTime
2600
+ );
2601
+ break;
1959
2602
  default:
1960
2603
  this.publishTelemetryRecord({
1961
2604
  type: 1 /* Error */,
@@ -1981,16 +2624,29 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1981
2624
  }
1982
2625
  this.transport?.postMessage?.(record);
1983
2626
  }
2627
+ // Drains the engine meter telemetry queue into the stereo meter ring / transport.
2628
+ //
2629
+ // Shared-queue contract: `drainMeterTelemetry` and `drainMeterTelemetryWide`
2630
+ // pop the SAME single-consumer telemetry queue, so exactly ONE of them may run
2631
+ // per engine. The live worklet path owns the queue via the stereo drain below;
2632
+ // the worklet meter ring (SONARE_METER_RING_RECORD_FLOATS) is a fixed stereo
2633
+ // layout carrying planes 0/1 plus the correlation/LUFS summary. Per-plane
2634
+ // surround meters are NOT delivered over the live worklet ring — a host that
2635
+ // needs them must use the offline `drainMeterTelemetryWide()` API on a
2636
+ // non-worklet engine instance (do not also call it on a worklet-driven engine,
2637
+ // or the two drains will starve each other).
1984
2638
  publishMeters() {
1985
2639
  if (this.meterIntervalFrames <= 0 || !this.transport && !this.meterRing) {
1986
2640
  return;
1987
2641
  }
1988
2642
  for (const item of this.engine.drainMeterTelemetry(64)) {
1989
2643
  const meter = meterFromEngine(item);
1990
- if (meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
2644
+ if (meter.frame !== this.lastMeterFrame && meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
1991
2645
  continue;
1992
2646
  }
1993
- this.lastMeterFrame = meter.frame;
2647
+ if (meter.frame !== this.lastMeterFrame) {
2648
+ this.lastMeterFrame = meter.frame;
2649
+ }
1994
2650
  if (this.meterRing) {
1995
2651
  this.writeMeterRing(meter);
1996
2652
  } else {
@@ -2007,12 +2663,54 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
2007
2663
  const writeIndex = Atomics.load(ring.header, 0);
2008
2664
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
2009
2665
  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);
2666
+ ring.records[offset + 1] = encodeFrameHi(meter.frame);
2667
+ ring.records[offset + 2] = meter.targetId;
2668
+ ring.records[offset + 3] = meter.peakDbL;
2669
+ ring.records[offset + 4] = meter.peakDbR;
2670
+ ring.records[offset + 5] = meter.rmsDbL;
2671
+ ring.records[offset + 6] = meter.rmsDbR;
2672
+ ring.records[offset + 7] = meter.correlation;
2673
+ ring.records[offset + 8] = meter.truePeakDbL;
2674
+ ring.records[offset + 9] = meter.truePeakDbR;
2675
+ ring.records[offset + 10] = meter.momentaryLufs;
2676
+ ring.records[offset + 11] = meter.shortTermLufs;
2677
+ ring.records[offset + 12] = meter.integratedLufs;
2678
+ ring.records[offset + 13] = meter.gainReductionDb;
2679
+ Atomics.store(ring.header, 0, writeIndex + 1);
2680
+ }
2681
+ // Drains the engine's scope producer (FFT spectrum + goniometer points) into
2682
+ // the lock-free SAB scope ring. Only the embind runtime publishes scope
2683
+ // telemetry; the sonare-rt runtime owns its own transport. No allocation on
2684
+ // the render path: records are written field-by-field into the ring.
2685
+ publishScope() {
2686
+ const ring = this.scopeRing;
2687
+ if (!ring) {
2688
+ return;
2689
+ }
2690
+ for (const item of this.engine.drainScopeTelemetry(64)) {
2691
+ this.writeScopeRing(ring, item);
2692
+ }
2693
+ }
2694
+ writeScopeRing(ring, record) {
2695
+ const writeIndex = Atomics.load(ring.header, 0);
2696
+ const base = writeIndex % ring.capacity * ring.recordFloats;
2697
+ ring.records[base] = encodeFrameLo(record.renderFrame);
2698
+ ring.records[base + 1] = encodeFrameHi(record.renderFrame);
2699
+ ring.records[base + 2] = record.targetId;
2700
+ const bandCount = Math.min(ring.bands, record.bands.length);
2701
+ ring.records[base + 3] = bandCount;
2702
+ const pointCount = Math.min(ring.maxPoints, record.points.length);
2703
+ ring.records[base + 4] = pointCount;
2704
+ const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
2705
+ for (let i = 0; i < bandCount; i++) {
2706
+ ring.records[bandsBase + i] = record.bands[i];
2707
+ }
2708
+ const pointsBase = bandsBase + ring.bands;
2709
+ for (let i = 0; i < pointCount; i++) {
2710
+ const point = record.points[i];
2711
+ ring.records[pointsBase + 2 * i] = point.left;
2712
+ ring.records[pointsBase + 2 * i + 1] = point.right;
2713
+ }
2016
2714
  Atomics.store(ring.header, 0, writeIndex + 1);
2017
2715
  }
2018
2716
  commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
@@ -2144,9 +2842,28 @@ var SonareRtRealtimeEngineRuntime = class {
2144
2842
  this.metronomeConfig.clickSamples
2145
2843
  );
2146
2844
  break;
2845
+ case "syncTempo":
2846
+ this.module._sonare_rt_engine_set_tempo(this.engine, message.bpm);
2847
+ break;
2147
2848
  case "syncClips":
2849
+ case "syncClipsDelta":
2850
+ case "syncMidiClips":
2148
2851
  case "syncMarkers":
2149
2852
  case "syncAutomation":
2853
+ case "syncMixer":
2854
+ case "syncCapture":
2855
+ case "syncTrackStripEqBand":
2856
+ case "syncMasterStripEqBand":
2857
+ case "syncTrackStripInsertBypassed":
2858
+ case "syncMasterStripInsertBypassed":
2859
+ case "syncBuiltinInstrument":
2860
+ case "syncSynthInstrument":
2861
+ case "syncSf2Instrument":
2862
+ case "syncLoadSoundFont":
2863
+ case "syncMidiNoteOn":
2864
+ case "syncMidiNoteOff":
2865
+ case "syncMidiCc":
2866
+ case "syncMidiPanic":
2150
2867
  if (this.telemetryRing) {
2151
2868
  writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
2152
2869
  type: 1 /* Error */,
@@ -2161,6 +2878,28 @@ var SonareRtRealtimeEngineRuntime = class {
2161
2878
  break;
2162
2879
  }
2163
2880
  }
2881
+ receiveCaptureRequest(message, port) {
2882
+ if (this.closed) {
2883
+ return;
2884
+ }
2885
+ port?.postMessage?.({
2886
+ type: "captureResponse",
2887
+ requestId: message.requestId,
2888
+ ok: false,
2889
+ error: "Capture read-back is not supported by the sonare-rt runtime."
2890
+ });
2891
+ }
2892
+ receiveTransportRequest(message, port) {
2893
+ if (this.closed) {
2894
+ return;
2895
+ }
2896
+ port?.postMessage?.({
2897
+ type: "transportResponse",
2898
+ requestId: message.requestId,
2899
+ ok: false,
2900
+ error: "Transport state read-back is not supported by the sonare-rt runtime."
2901
+ });
2902
+ }
2164
2903
  destroy() {
2165
2904
  if (this.closed) {
2166
2905
  return;
@@ -2333,26 +3072,53 @@ var SonareRtRealtimeEngineRuntime = class {
2333
3072
  }
2334
3073
  };
2335
3074
  var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2336
- constructor(node, capabilities, commandRing, telemetryRing, meterRing) {
3075
+ constructor(node, capabilities, commandRing, telemetryRing, meterRing, scopeRing) {
2337
3076
  this.telemetryReadIndex = 0;
2338
3077
  this.meterReadIndex = 0;
3078
+ this.scopeReadIndex = 0;
2339
3079
  this.telemetryListeners = /* @__PURE__ */ new Set();
2340
3080
  this.meterListeners = /* @__PURE__ */ new Set();
3081
+ this.scopeListeners = /* @__PURE__ */ new Set();
3082
+ this.captureRequestId = 1;
3083
+ this.captureRequests = /* @__PURE__ */ new Map();
3084
+ this.transportRequestId = 1;
3085
+ this.transportRequests = /* @__PURE__ */ new Map();
2341
3086
  this.destroyed = false;
2342
3087
  this.node = node;
2343
3088
  this.capabilities = capabilities;
2344
3089
  this.commandRing = commandRing;
2345
3090
  this.telemetryRing = telemetryRing;
2346
3091
  this.meterRing = meterRing;
3092
+ this.scopeRing = scopeRing;
2347
3093
  this.ready = new Promise((resolve, reject) => {
2348
3094
  this.resolveReady = resolve;
2349
3095
  this.rejectReady = reject;
2350
3096
  });
2351
- if (capabilities.runtimeTarget !== "sonare-rt") {
3097
+ if (!capabilities.readyMessage) {
2352
3098
  this.resolveReady();
2353
3099
  }
2354
3100
  this.node.port.onmessage = (event) => {
2355
- if (isEngineTelemetryRecord(event.data)) {
3101
+ if (isEngineCaptureResponseMessage(event.data)) {
3102
+ const pending = this.captureRequests.get(event.data.requestId);
3103
+ if (pending) {
3104
+ this.captureRequests.delete(event.data.requestId);
3105
+ if (event.data.ok) {
3106
+ pending.resolve(event.data);
3107
+ } else {
3108
+ pending.reject(new Error(event.data.error ?? "Capture request failed"));
3109
+ }
3110
+ }
3111
+ } else if (isEngineTransportResponseMessage(event.data)) {
3112
+ const pending = this.transportRequests.get(event.data.requestId);
3113
+ if (pending) {
3114
+ this.transportRequests.delete(event.data.requestId);
3115
+ if (event.data.ok) {
3116
+ pending.resolve(event.data);
3117
+ } else {
3118
+ pending.reject(new Error(event.data.error ?? "Transport request failed"));
3119
+ }
3120
+ }
3121
+ } else if (isEngineTelemetryRecord(event.data)) {
2356
3122
  this.emitTelemetry(event.data);
2357
3123
  } else if (isMeterSnapshot(event.data)) {
2358
3124
  this.emitMeter(event.data);
@@ -2393,6 +3159,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2393
3159
  const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(options.commandRingCapacity ?? 128) : void 0;
2394
3160
  const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(options.telemetryRingCapacity ?? 128) : void 0;
2395
3161
  const meterRing = mode === "sab" && runtimeTarget === "embind" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
3162
+ const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
3163
+ const scopeRing = mode === "sab" && runtimeTarget === "embind" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48) : void 0;
2396
3164
  const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
2397
3165
  const processorOptions = {
2398
3166
  runtimeTarget,
@@ -2406,7 +3174,14 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2406
3174
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
2407
3175
  telemetryRingCapacity: telemetryRing?.capacity,
2408
3176
  meterSharedBuffer: meterRing?.sharedBuffer,
2409
- meterRingCapacity: meterRing?.capacity
3177
+ meterRingCapacity: meterRing?.capacity,
3178
+ scopeSharedBuffer: scopeRing?.sharedBuffer,
3179
+ scopeRingCapacity: scopeRing?.capacity,
3180
+ scopeBands: scopeRing?.bands,
3181
+ scopeIntervalFrames: scopeRing ? scopeIntervalFrames : void 0,
3182
+ wasmBinary: options.wasmBinary,
3183
+ initialSyncMessages: options.initialSyncMessages,
3184
+ initialCommands: options.initialCommands
2410
3185
  };
2411
3186
  const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
2412
3187
  const node = factory(context, processorName, {
@@ -2426,11 +3201,13 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2426
3201
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
2427
3202
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
2428
3203
  abiCompatible: detectedCapabilities?.abiCompatible,
2429
- degradedReason
3204
+ degradedReason,
3205
+ readyMessage: runtimeTarget === "sonare-rt" || runtimeTarget === "embind" && moduleUrl !== void 0 && !options.nodeFactory
2430
3206
  },
2431
3207
  commandRing,
2432
3208
  telemetryRing,
2433
- meterRing
3209
+ meterRing,
3210
+ scopeRing
2434
3211
  );
2435
3212
  }
2436
3213
  play(sampleTime = -1) {
@@ -2463,6 +3240,32 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2463
3240
  this.node.port.postMessage(command);
2464
3241
  return true;
2465
3242
  }
3243
+ requestCaptureStatus() {
3244
+ return this.sendCaptureRequest("status").then((response) => {
3245
+ if (!response.status) {
3246
+ throw new Error("Capture status response is missing status.");
3247
+ }
3248
+ return response.status;
3249
+ });
3250
+ }
3251
+ requestCapturedAudio() {
3252
+ return this.sendCaptureRequest("read").then(
3253
+ (response) => (response.channels ?? []).map(
3254
+ (channel) => channel instanceof Float32Array ? channel : new Float32Array(channel)
3255
+ )
3256
+ );
3257
+ }
3258
+ requestCaptureReset() {
3259
+ return this.sendCaptureRequest("reset").then(() => void 0);
3260
+ }
3261
+ requestTransportState() {
3262
+ return this.sendTransportRequest().then((response) => {
3263
+ if (!response.state) {
3264
+ throw new Error("Transport state response is missing state.");
3265
+ }
3266
+ return response.state;
3267
+ });
3268
+ }
2466
3269
  pollTelemetry() {
2467
3270
  if (!this.telemetryRing) {
2468
3271
  return [];
@@ -2488,6 +3291,20 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2488
3291
  }
2489
3292
  return read.meters;
2490
3293
  }
3294
+ // Drains scope telemetry (FFT spectrum + goniometer points) published into the
3295
+ // SAB scope ring and forwards each record to onScope listeners. A no-op unless
3296
+ // the node was created with scopeIntervalFrames > 0 (embind SAB mode).
3297
+ pollScope() {
3298
+ if (!this.scopeRing) {
3299
+ return [];
3300
+ }
3301
+ const read = readSonareScopeRingBuffer(this.scopeRing, this.scopeReadIndex);
3302
+ this.scopeReadIndex = read.nextReadIndex;
3303
+ for (const scope of read.scopes) {
3304
+ this.emitScope(scope);
3305
+ }
3306
+ return read.scopes;
3307
+ }
2491
3308
  onTelemetry(callback) {
2492
3309
  this.telemetryListeners.add(callback);
2493
3310
  return () => {
@@ -2500,6 +3317,12 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2500
3317
  this.meterListeners.delete(callback);
2501
3318
  };
2502
3319
  }
3320
+ onScope(callback) {
3321
+ this.scopeListeners.add(callback);
3322
+ return () => {
3323
+ this.scopeListeners.delete(callback);
3324
+ };
3325
+ }
2503
3326
  destroy() {
2504
3327
  if (this.destroyed) {
2505
3328
  return;
@@ -2507,8 +3330,17 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2507
3330
  this.destroyed = true;
2508
3331
  this.node.port.postMessage({ type: 3 /* TransportStop */, sampleTime: -1 });
2509
3332
  this.node.disconnect();
3333
+ for (const pending of this.captureRequests.values()) {
3334
+ pending.reject(new Error("Realtime engine node is destroyed."));
3335
+ }
3336
+ this.captureRequests.clear();
3337
+ for (const pending of this.transportRequests.values()) {
3338
+ pending.reject(new Error("Realtime engine node is destroyed."));
3339
+ }
3340
+ this.transportRequests.clear();
2510
3341
  this.telemetryListeners.clear();
2511
3342
  this.meterListeners.clear();
3343
+ this.scopeListeners.clear();
2512
3344
  }
2513
3345
  emitTelemetry(telemetry) {
2514
3346
  for (const listener of this.telemetryListeners) {
@@ -2520,14 +3352,57 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2520
3352
  listener(meter);
2521
3353
  }
2522
3354
  }
3355
+ emitScope(scope) {
3356
+ for (const listener of this.scopeListeners) {
3357
+ listener(scope);
3358
+ }
3359
+ }
3360
+ sendCaptureRequest(op) {
3361
+ if (this.destroyed) {
3362
+ return Promise.reject(new Error("Realtime engine node is destroyed."));
3363
+ }
3364
+ const requestId = this.captureRequestId++;
3365
+ const promise = new Promise((resolve, reject) => {
3366
+ this.captureRequests.set(requestId, { resolve, reject });
3367
+ });
3368
+ this.node.port.postMessage({ type: "captureRequest", requestId, op });
3369
+ return promise;
3370
+ }
3371
+ sendTransportRequest() {
3372
+ if (this.destroyed) {
3373
+ return Promise.reject(new Error("Realtime engine node is destroyed."));
3374
+ }
3375
+ const requestId = this.transportRequestId++;
3376
+ const promise = new Promise((resolve, reject) => {
3377
+ this.transportRequests.set(requestId, { resolve, reject });
3378
+ });
3379
+ this.node.port.postMessage({ type: "transportRequest", requestId, op: "state" });
3380
+ return promise;
3381
+ }
2523
3382
  };
2524
3383
  var SonareEngine = class _SonareEngine {
2525
3384
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
2526
3385
  this.automationLanes = /* @__PURE__ */ new Map();
2527
3386
  this.clips = /* @__PURE__ */ new Map();
3387
+ this.midiClips = /* @__PURE__ */ new Map();
2528
3388
  this.markers = /* @__PURE__ */ new Map();
3389
+ this.trackLaneIds = [];
3390
+ this.trackSends = /* @__PURE__ */ new Map();
3391
+ this.trackOutputBus = /* @__PURE__ */ new Map();
3392
+ this.laneSidechains = /* @__PURE__ */ new Map();
3393
+ this.buses = [];
3394
+ this.trackStripJson = /* @__PURE__ */ new Map();
3395
+ this.busStripJson = /* @__PURE__ */ new Map();
3396
+ this.tempoBpm = 120;
3397
+ this.timeSignature = { numerator: 4, denominator: 4 };
3398
+ this.tempoSegments = [{ startPpq: 0, bpm: 120 }];
3399
+ this.timeSignatureSegments = [
3400
+ { startPpq: 0, numerator: 4, denominator: 4 }
3401
+ ];
2529
3402
  this.nextClipId = 1;
2530
3403
  this.nextMarkerId = 1;
3404
+ this.transportPlaying = false;
3405
+ this.pendingInstrumentSync = [];
2531
3406
  this.destroyed = false;
2532
3407
  this.context = context;
2533
3408
  this.realtimeNode = realtimeNode;
@@ -2538,8 +3413,21 @@ var SonareEngine = class _SonareEngine {
2538
3413
  this.offlineBlockSize = offlineBlockSize;
2539
3414
  this.offlineChannelCount = offlineChannelCount;
2540
3415
  this.transport = {
2541
- play: (sampleTime = -1) => this.realtimeNode.play(sampleTime),
2542
- stop: (sampleTime = -1) => this.realtimeNode.stop(sampleTime),
3416
+ play: (sampleTime = -1) => {
3417
+ const ok = this.realtimeNode.play(sampleTime);
3418
+ if (ok) {
3419
+ this.transportPlaying = true;
3420
+ }
3421
+ return ok;
3422
+ },
3423
+ stop: (sampleTime = -1) => {
3424
+ const ok = this.realtimeNode.stop(sampleTime);
3425
+ if (ok) {
3426
+ this.transportPlaying = false;
3427
+ this.flushPendingInstrumentSync();
3428
+ }
3429
+ return ok;
3430
+ },
2543
3431
  seekPpq: (ppq, sampleTime = -1) => {
2544
3432
  this.offlineEngine.seekPpq(ppq, sampleTime);
2545
3433
  return this.realtimeNode.seekPpq(ppq, sampleTime);
@@ -2550,6 +3438,7 @@ var SonareEngine = class _SonareEngine {
2550
3438
  return this.realtimeNode.seekSample(timelineSample, sampleTime);
2551
3439
  },
2552
3440
  setTempo: (bpm) => this.setTempo(bpm),
3441
+ setTempoSegments: (segments) => this.setTempoSegments(segments),
2553
3442
  setLoop: (startPpq, endPpq, enabled = true) => this.setLoop(startPpq, endPpq, enabled)
2554
3443
  };
2555
3444
  }
@@ -2584,13 +3473,37 @@ var SonareEngine = class _SonareEngine {
2584
3473
  await this.context.resume?.();
2585
3474
  }
2586
3475
  setTempo(bpm) {
3476
+ this.tempoBpm = bpm;
3477
+ this.tempoSegments = [{ startPpq: 0, bpm }];
2587
3478
  this.offlineEngine.setTempo(bpm);
3479
+ this.postTempoSync();
2588
3480
  this.realtimeNode.sendCommand({
2589
3481
  type: 6 /* SetTempoMap */,
2590
3482
  sampleTime: -1,
2591
3483
  argFloat: bpm
2592
3484
  });
2593
3485
  }
3486
+ setTempoSegments(segments) {
3487
+ this.tempoSegments = segments.map((segment) => ({ ...segment }));
3488
+ this.tempoBpm = this.tempoSegments[0]?.bpm ?? this.tempoBpm;
3489
+ this.offlineEngine.setTempoSegments(this.tempoSegments);
3490
+ this.postTempoSync();
3491
+ }
3492
+ setTimeSignature(numerator, denominator) {
3493
+ this.timeSignature = { numerator, denominator };
3494
+ this.timeSignatureSegments = [{ startPpq: 0, numerator, denominator }];
3495
+ this.offlineEngine.setTimeSignature(numerator, denominator);
3496
+ this.postTempoSync();
3497
+ }
3498
+ setTimeSignatureSegments(segments) {
3499
+ this.timeSignatureSegments = segments.map((segment) => ({ ...segment }));
3500
+ const first = this.timeSignatureSegments[0];
3501
+ if (first) {
3502
+ this.timeSignature = { numerator: first.numerator, denominator: first.denominator };
3503
+ }
3504
+ this.offlineEngine.setTimeSignatureSegments(this.timeSignatureSegments);
3505
+ this.postTempoSync();
3506
+ }
2594
3507
  setLoop(startPpq, endPpq, enabled = true) {
2595
3508
  this.offlineEngine.setLoop(startPpq, endPpq, enabled);
2596
3509
  return this.realtimeNode.sendCommand({
@@ -2601,6 +3514,17 @@ var SonareEngine = class _SonareEngine {
2601
3514
  argInt: Math.round(endPpq * 1e6)
2602
3515
  });
2603
3516
  }
3517
+ countInEndSample(startSample, bars) {
3518
+ return this.offlineEngine.countInEndSample(startSample, bars);
3519
+ }
3520
+ async getTransportState() {
3521
+ const state = await this.realtimeNode.requestTransportState();
3522
+ this.latestTransportState = state;
3523
+ return state;
3524
+ }
3525
+ cachedTransportState() {
3526
+ return this.latestTransportState;
3527
+ }
2604
3528
  setParam(nodeId, param, value) {
2605
3529
  const paramId = this.resolveParamId(nodeId, param);
2606
3530
  this.offlineEngine.setParameter(paramId, value);
@@ -2623,6 +3547,64 @@ var SonareEngine = class _SonareEngine {
2623
3547
  addAutomationPoint(laneId, ppq, value, curve = "linear") {
2624
3548
  this.scheduleParam("", laneId, ppq, value, curve);
2625
3549
  }
3550
+ /**
3551
+ * Replaces the automation lane for `paramId` with the given breakpoints.
3552
+ *
3553
+ * Unlike scheduleParam (which appends a single point), this sets the whole
3554
+ * lane at once; an empty array clears the lane. The points are defensively
3555
+ * copied and sorted by ppq before being mirrored to the offline engine and
3556
+ * the live worklet engine.
3557
+ *
3558
+ * @param paramId Automation target id (registered parameter or a reserved
3559
+ * engine mixer target from automationParamId/busAutomationParamId).
3560
+ * @param points Lane breakpoints; order does not matter.
3561
+ */
3562
+ setAutomationLane(paramId, points) {
3563
+ const sorted = points.map((point) => ({ ...point })).sort((a, b) => a.ppq - b.ppq);
3564
+ if (sorted.length === 0) {
3565
+ this.automationLanes.delete(paramId);
3566
+ } else {
3567
+ this.automationLanes.set(paramId, sorted);
3568
+ }
3569
+ this.offlineEngine.setAutomationLane(paramId, sorted);
3570
+ this.postSync({ type: "syncAutomation", paramId, points: sorted });
3571
+ }
3572
+ /**
3573
+ * Returns the automation target id for a mixer strip parameter.
3574
+ *
3575
+ * The id addresses the engine's reserved mixer namespace, so it can be fed
3576
+ * straight to setAutomationLane to automate a fader or pan without
3577
+ * registering a parameter.
3578
+ *
3579
+ * @param target Track id (declares a mixer lane on first use) or 'master'.
3580
+ * @param kind Strip parameter to address.
3581
+ * @returns Reserved engine parameter id for the strip parameter.
3582
+ */
3583
+ automationParamId(target, kind) {
3584
+ const paramKind = kind === "pan" ? ENGINE_MIXER_PARAM_PAN : ENGINE_MIXER_PARAM_FADER_DB;
3585
+ if (target === "master") {
3586
+ return engineMixerMasterTarget(paramKind);
3587
+ }
3588
+ return engineMixerLaneTarget(this.ensureTrackLane(target), paramKind);
3589
+ }
3590
+ /**
3591
+ * Returns the automation target id for a bus fader.
3592
+ *
3593
+ * @param busId Bus id (declares the mixer bus on first use).
3594
+ * @returns Reserved engine parameter id for the bus fader gain (dB).
3595
+ */
3596
+ busAutomationParamId(busId) {
3597
+ return engineMixerBusTarget(this.ensureBus(busId), ENGINE_MIXER_PARAM_FADER_DB);
3598
+ }
3599
+ /**
3600
+ * Returns the number of automation lanes installed on the engine, including
3601
+ * lanes whose breakpoint list is currently empty.
3602
+ *
3603
+ * @returns Engine-side automation lane count.
3604
+ */
3605
+ automationLaneCount() {
3606
+ return this.offlineEngine.automationLaneCount();
3607
+ }
2626
3608
  listParameters() {
2627
3609
  const parameters = [];
2628
3610
  for (let index = 0; index < this.offlineEngine.parameterCount(); index++) {
@@ -2631,12 +3613,300 @@ var SonareEngine = class _SonareEngine {
2631
3613
  return parameters;
2632
3614
  }
2633
3615
  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."
3616
+ const laneIndex = this.ensureTrackLane(target);
3617
+ this.offlineEngine.setSoloMute(laneIndex, solo, mute);
3618
+ return this.realtimeNode.sendCommand({
3619
+ type: 10 /* SetSoloMute */,
3620
+ targetId: laneIndex,
3621
+ sampleTime: -1,
3622
+ argInt: (mute ? 1 : 0) | (solo ? 2 : 0)
3623
+ });
3624
+ }
3625
+ setStripGain(target, db) {
3626
+ if (target === "master") {
3627
+ const paramId2 = engineMixerMasterTarget(ENGINE_MIXER_PARAM_FADER_DB);
3628
+ this.offlineEngine.setParameter(paramId2, db);
3629
+ return this.realtimeNode.sendCommand({
3630
+ type: 1 /* SetParamSmoothed */,
3631
+ targetId: paramId2,
3632
+ sampleTime: -1,
3633
+ argFloat: db
3634
+ });
3635
+ }
3636
+ const laneIndex = this.ensureTrackLane(target);
3637
+ const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_FADER_DB);
3638
+ this.offlineEngine.setParameter(paramId, db);
3639
+ return this.realtimeNode.sendCommand({
3640
+ type: 1 /* SetParamSmoothed */,
3641
+ targetId: paramId,
3642
+ sampleTime: -1,
3643
+ argFloat: db
3644
+ });
3645
+ }
3646
+ setStripPan(target, pan) {
3647
+ if (target === "master") {
3648
+ const paramId2 = engineMixerMasterTarget(ENGINE_MIXER_PARAM_PAN);
3649
+ this.offlineEngine.setParameter(paramId2, pan);
3650
+ return this.realtimeNode.sendCommand({
3651
+ type: 1 /* SetParamSmoothed */,
3652
+ targetId: paramId2,
3653
+ sampleTime: -1,
3654
+ argFloat: pan
3655
+ });
3656
+ }
3657
+ const laneIndex = this.ensureTrackLane(target);
3658
+ const paramId = engineMixerLaneTarget(laneIndex, ENGINE_MIXER_PARAM_PAN);
3659
+ this.offlineEngine.setParameter(paramId, pan);
3660
+ return this.realtimeNode.sendCommand({
3661
+ type: 1 /* SetParamSmoothed */,
3662
+ targetId: paramId,
3663
+ sampleTime: -1,
3664
+ argFloat: pan
3665
+ });
3666
+ }
3667
+ /**
3668
+ * Declares the mixer track lanes in an explicit order.
3669
+ *
3670
+ * Lane indices are append-only: once a track id occupies a lane, its index
3671
+ * stays fixed for the engine's lifetime. The given list must therefore start
3672
+ * with the already-declared lane ids in their current order and may only
3673
+ * append new track ids after them. Entries carrying `sends` replace that
3674
+ * track's send list; entries without `sends` leave existing sends untouched.
3675
+ *
3676
+ * @param lanes Track ids or lane descriptors in the desired lane order.
3677
+ */
3678
+ setTrackLanes(lanes) {
3679
+ const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
3680
+ const ids = [];
3681
+ for (const entry of entries) {
3682
+ if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
3683
+ throw new Error(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
3684
+ }
3685
+ ids.push(entry.trackId);
3686
+ }
3687
+ if (new Set(ids).size !== ids.length) {
3688
+ throw new Error("Duplicate track id in mixer lane list");
3689
+ }
3690
+ for (let index = 0; index < this.trackLaneIds.length; index++) {
3691
+ if (ids[index] !== this.trackLaneIds[index]) {
3692
+ throw new Error(
3693
+ "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
3694
+ );
3695
+ }
3696
+ }
3697
+ for (const entry of entries) {
3698
+ if (entry.sends) {
3699
+ this.trackSends.set(
3700
+ entry.trackId,
3701
+ entry.sends.map((send) => ({ ...send }))
3702
+ );
3703
+ }
3704
+ if (entry.outputBusId !== void 0) {
3705
+ if (entry.outputBusId === 0) {
3706
+ this.trackOutputBus.delete(entry.trackId);
3707
+ } else {
3708
+ this.trackOutputBus.set(entry.trackId, entry.outputBusId);
3709
+ }
3710
+ }
3711
+ }
3712
+ this.trackLaneIds.splice(0, this.trackLaneIds.length, ...ids);
3713
+ this.syncMixer();
3714
+ }
3715
+ /**
3716
+ * Routes a track lane's post-fader output into a declared bus instead of
3717
+ * the master mix (group/folder routing); busId 0 restores the master mix.
3718
+ */
3719
+ setTrackOutputBus(target, busId) {
3720
+ const laneIndex = this.ensureTrackLane(target);
3721
+ const trackId = this.trackLaneIds[laneIndex];
3722
+ if (busId === 0) {
3723
+ this.trackOutputBus.delete(trackId);
3724
+ } else {
3725
+ this.trackOutputBus.set(trackId, busId);
3726
+ }
3727
+ this.syncMixer();
3728
+ }
3729
+ /**
3730
+ * Keys one insert of a lane strip from another lane's post-strip pre-fader
3731
+ * audio (ducking/sidechainRouter inserts). sourceTarget null removes the
3732
+ * binding.
3733
+ */
3734
+ setLaneSidechain(target, insertIndex, sourceTarget) {
3735
+ const laneIndex = this.ensureTrackLane(target);
3736
+ const trackId = this.trackLaneIds[laneIndex];
3737
+ const key = `${trackId}:${insertIndex}`;
3738
+ let sourceTrackId = 0;
3739
+ if (sourceTarget !== null) {
3740
+ const sourceIndex = this.ensureTrackLane(sourceTarget);
3741
+ sourceTrackId = this.trackLaneIds[sourceIndex];
3742
+ }
3743
+ if (sourceTrackId === 0) {
3744
+ this.laneSidechains.delete(key);
3745
+ } else {
3746
+ this.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
3747
+ }
3748
+ this.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
3749
+ this.postSync({
3750
+ type: "syncMixer",
3751
+ lanes: this.mixerLanes(),
3752
+ laneSidechains: [{ trackId, insertIndex, sourceTrackId }]
3753
+ });
3754
+ }
3755
+ setSends(target, sends) {
3756
+ const laneIndex = this.ensureTrackLane(target);
3757
+ const trackId = this.trackLaneIds[laneIndex];
3758
+ this.trackSends.set(
3759
+ trackId,
3760
+ sends.map((send) => ({ ...send }))
2639
3761
  );
3762
+ this.syncMixer();
3763
+ }
3764
+ setTrackBuses(buses) {
3765
+ this.buses.splice(0, this.buses.length, ...buses.map((bus) => ({ ...bus })));
3766
+ this.syncMixer();
3767
+ }
3768
+ setBusGain(busId, db) {
3769
+ const busIndex = this.ensureBus(busId);
3770
+ this.buses[busIndex] = { ...this.buses[busIndex], busId, gainDb: db };
3771
+ this.offlineEngine.setTrackBuses(this.buses);
3772
+ const paramId = engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB);
3773
+ this.offlineEngine.setParameter(paramId, db);
3774
+ return this.realtimeNode.sendCommand({
3775
+ type: 1 /* SetParamSmoothed */,
3776
+ targetId: paramId,
3777
+ sampleTime: -1,
3778
+ argFloat: db
3779
+ });
3780
+ }
3781
+ setTrackStripJson(target, sceneJson) {
3782
+ const laneIndex = this.ensureTrackLane(target);
3783
+ const trackId = this.trackLaneIds[laneIndex];
3784
+ this.offlineEngine.setTrackStripJson(trackId, sceneJson);
3785
+ this.trackStripJson.set(trackId, sceneJson);
3786
+ this.syncMixer();
3787
+ }
3788
+ setTrackStripEqBand(target, bandIndex, band) {
3789
+ const laneIndex = this.ensureTrackLane(target);
3790
+ const trackId = this.trackLaneIds[laneIndex];
3791
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3792
+ this.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
3793
+ this.postSync({ type: "syncTrackStripEqBand", trackId, bandIndex, bandJson });
3794
+ }
3795
+ setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass = false) {
3796
+ const laneIndex = this.ensureTrackLane(target);
3797
+ const trackId = this.trackLaneIds[laneIndex];
3798
+ this.offlineEngine.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
3799
+ this.postSync({
3800
+ type: "syncTrackStripInsertBypassed",
3801
+ trackId,
3802
+ insertIndex,
3803
+ bypassed,
3804
+ resetOnBypass
3805
+ });
3806
+ }
3807
+ setTrackStripInsertParamByName(target, insertIndex, paramName, value) {
3808
+ const laneIndex = this.ensureTrackLane(target);
3809
+ const trackId = this.trackLaneIds[laneIndex];
3810
+ this.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
3811
+ this.postSync({
3812
+ type: "syncTrackStripInsertParamByName",
3813
+ trackId,
3814
+ insertIndex,
3815
+ paramName,
3816
+ value
3817
+ });
3818
+ }
3819
+ setTrackStripPan(target, pan) {
3820
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
3821
+ this.offlineEngine.setTrackStripPan(trackId, pan);
3822
+ this.postSync({ type: "syncTrackStripPan", trackId, pan });
3823
+ }
3824
+ setTrackStripPanLaw(target, panLaw) {
3825
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
3826
+ const code = panLawCode(panLaw);
3827
+ this.offlineEngine.setTrackStripPanLaw(trackId, code);
3828
+ this.postSync({ type: "syncTrackStripPanLaw", trackId, panLaw: code });
3829
+ }
3830
+ setTrackStripPanMode(target, panMode) {
3831
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
3832
+ const code = panModeCode(panMode);
3833
+ this.offlineEngine.setTrackStripPanMode(trackId, code);
3834
+ this.postSync({ type: "syncTrackStripPanMode", trackId, panMode: code });
3835
+ }
3836
+ setTrackStripDualPan(target, leftPan, rightPan) {
3837
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
3838
+ this.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
3839
+ this.postSync({ type: "syncTrackStripDualPan", trackId, leftPan, rightPan });
3840
+ }
3841
+ setTrackStripChannelDelaySamples(target, delaySamples) {
3842
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
3843
+ this.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
3844
+ this.postSync({ type: "syncTrackStripChannelDelaySamples", trackId, delaySamples });
3845
+ }
3846
+ setStripEq(target, bandIndex, band) {
3847
+ if (target === "master") {
3848
+ this.setMasterStripEqBand(bandIndex, band);
3849
+ return;
3850
+ }
3851
+ this.setTrackStripEqBand(target, bandIndex, band);
3852
+ }
3853
+ setStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass = false) {
3854
+ if (target === "master") {
3855
+ this.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
3856
+ return;
3857
+ }
3858
+ this.setTrackStripInsertBypassed(target, insertIndex, bypassed, resetOnBypass);
3859
+ }
3860
+ setStripInserts(target, sceneJson) {
3861
+ if (target === "master") {
3862
+ this.setMasterStripJson(sceneJson);
3863
+ return;
3864
+ }
3865
+ this.setTrackStripJson(target, sceneJson);
3866
+ }
3867
+ setBusStripJson(busId, sceneJson) {
3868
+ this.ensureBus(busId);
3869
+ this.offlineEngine.setBusStripJson(busId, sceneJson);
3870
+ this.busStripJson.set(busId, sceneJson);
3871
+ this.syncMixer();
3872
+ }
3873
+ setMasterStripJson(sceneJson) {
3874
+ this.offlineEngine.setMasterStripJson(sceneJson);
3875
+ this.masterStripJson = sceneJson;
3876
+ this.syncMixer();
3877
+ }
3878
+ setMasterStripEqBand(bandIndex, band) {
3879
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3880
+ this.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
3881
+ this.postSync({ type: "syncMasterStripEqBand", bandIndex, bandJson });
3882
+ }
3883
+ setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass = false) {
3884
+ this.offlineEngine.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
3885
+ this.postSync({
3886
+ type: "syncMasterStripInsertBypassed",
3887
+ insertIndex,
3888
+ bypassed,
3889
+ resetOnBypass
3890
+ });
3891
+ }
3892
+ setMasterStripInsertParamByName(insertIndex, paramName, value) {
3893
+ this.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
3894
+ this.postSync({
3895
+ type: "syncMasterStripInsertParamByName",
3896
+ insertIndex,
3897
+ paramName,
3898
+ value
3899
+ });
3900
+ }
3901
+ setStripInsertParamByName(target, insertIndex, paramName, value) {
3902
+ if (target === "master") {
3903
+ this.setMasterStripInsertParamByName(insertIndex, paramName, value);
3904
+ return;
3905
+ }
3906
+ this.setTrackStripInsertParamByName(target, insertIndex, paramName, value);
3907
+ }
3908
+ setMasterChain(sceneJson) {
3909
+ this.setMasterStripJson(sceneJson);
2640
3910
  }
2641
3911
  addClip(trackId, buffer, startPpq, opts = {}) {
2642
3912
  const id = opts.id ?? this.nextClipId++;
@@ -2644,18 +3914,108 @@ var SonareEngine = class _SonareEngine {
2644
3914
  ...opts,
2645
3915
  id,
2646
3916
  channels: buffer,
2647
- startPpq
3917
+ startPpq,
3918
+ trackId: this.resolveTargetId(trackId)
2648
3919
  };
3920
+ this.ensureTrackLane(trackId);
2649
3921
  this.clips.set(id, clip);
2650
- this.syncClips();
2651
- void trackId;
3922
+ this.syncClipsDelta([clip], []);
2652
3923
  return id;
2653
3924
  }
2654
3925
  removeClip(clipId) {
2655
3926
  this.clips.delete(clipId);
2656
- this.syncClips();
3927
+ this.syncClipsDelta([], [clipId]);
3928
+ }
3929
+ setMidiClips(clips) {
3930
+ this.midiClips.clear();
3931
+ for (const clip of clips) {
3932
+ const id = clip.id ?? this.nextClipId++;
3933
+ this.midiClips.set(id, { ...clip, id, events: clip.events.map((event) => ({ ...event })) });
3934
+ }
3935
+ this.syncMidiClips();
3936
+ }
3937
+ setBuiltinInstrument(trackId, config = {}) {
3938
+ const destinationId = this.resolveTargetId(trackId);
3939
+ this.offlineEngine.setBuiltinInstrument(config, destinationId);
3940
+ this.postInstrumentSync({ type: "syncBuiltinInstrument", destinationId, config });
3941
+ }
3942
+ setSynthInstrument(trackId, patch = {}) {
3943
+ const destinationId = this.resolveTargetId(trackId);
3944
+ this.offlineEngine.setSynthInstrument(patch, destinationId);
3945
+ this.postInstrumentSync({ type: "syncSynthInstrument", destinationId, patch });
3946
+ }
3947
+ loadSoundFont(data) {
3948
+ this.offlineEngine.loadSoundFont(data);
3949
+ this.postInstrumentSync({ type: "syncLoadSoundFont", data });
3950
+ }
3951
+ setSf2Instrument(trackId, config = {}) {
3952
+ const destinationId = this.resolveTargetId(trackId);
3953
+ this.offlineEngine.setSf2Instrument(config, destinationId);
3954
+ this.postInstrumentSync({ type: "syncSf2Instrument", destinationId, config });
3955
+ }
3956
+ pushMidiNoteOn(trackId, group, channel, note, velocity, renderFrame = -1) {
3957
+ const destinationId = this.resolveTargetId(trackId);
3958
+ this.offlineEngine.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
3959
+ this.postSync({
3960
+ type: "syncMidiNoteOn",
3961
+ destinationId,
3962
+ group,
3963
+ channel,
3964
+ note,
3965
+ velocity,
3966
+ renderFrame
3967
+ });
3968
+ }
3969
+ pushMidiNoteOff(trackId, group, channel, note, velocity = 0, renderFrame = -1) {
3970
+ const destinationId = this.resolveTargetId(trackId);
3971
+ this.offlineEngine.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
3972
+ this.postSync({
3973
+ type: "syncMidiNoteOff",
3974
+ destinationId,
3975
+ group,
3976
+ channel,
3977
+ note,
3978
+ velocity,
3979
+ renderFrame
3980
+ });
3981
+ }
3982
+ pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
3983
+ const destinationId = this.resolveTargetId(trackId);
3984
+ this.offlineEngine.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
3985
+ this.postSync({
3986
+ type: "syncMidiCc",
3987
+ destinationId,
3988
+ group,
3989
+ channel,
3990
+ controller,
3991
+ value,
3992
+ renderFrame
3993
+ });
3994
+ }
3995
+ pushMidiPanic(renderFrame = -1) {
3996
+ this.offlineEngine.pushMidiPanic(renderFrame);
3997
+ this.postSync({ type: "syncMidiPanic", renderFrame });
3998
+ }
3999
+ configureCapture(options) {
4000
+ const bufferFrames = Math.trunc(options.bufferFrames);
4001
+ const channels = Math.trunc(options.channels ?? this.offlineChannelCount);
4002
+ const source = options.source ?? "output";
4003
+ const recordOffsetSamples = Math.trunc(options.recordOffsetSamples ?? 0);
4004
+ const inputMonitor = {
4005
+ enabled: Boolean(options.inputMonitor?.enabled),
4006
+ gain: options.inputMonitor?.gain ?? 1
4007
+ };
4008
+ this.offlineEngine.setCaptureBuffer(channels, bufferFrames);
4009
+ this.offlineEngine.setCaptureSource(source);
4010
+ this.offlineEngine.setRecordOffsetSamples(recordOffsetSamples);
4011
+ this.offlineEngine.setInputMonitor(inputMonitor.enabled, inputMonitor.gain);
4012
+ this.captureConfig = { bufferFrames, channels, source, recordOffsetSamples, inputMonitor };
4013
+ this.postSync({ type: "syncCapture", ...this.captureConfig });
2657
4014
  }
2658
4015
  armRecord(trackId, enabled) {
4016
+ if (enabled && !this.captureConfig) {
4017
+ throw new Error("Capture buffer is not configured");
4018
+ }
2659
4019
  this.offlineEngine.armCapture(enabled);
2660
4020
  return this.realtimeNode.sendCommand({
2661
4021
  type: 13 /* ArmRecord */,
@@ -2665,8 +4025,8 @@ var SonareEngine = class _SonareEngine {
2665
4025
  });
2666
4026
  }
2667
4027
  punch(inPpq, outPpq) {
2668
- const inSample = this.ppqToApproxSample(inPpq);
2669
- const outSample = this.ppqToApproxSample(outPpq);
4028
+ const inSample = this.offlineEngine.sampleAtPpq(inPpq);
4029
+ const outSample = this.offlineEngine.sampleAtPpq(outPpq);
2670
4030
  this.offlineEngine.setCapturePunch(inSample, outSample, true);
2671
4031
  return this.realtimeNode.sendCommand({
2672
4032
  type: 14 /* Punch */,
@@ -2675,6 +4035,16 @@ var SonareEngine = class _SonareEngine {
2675
4035
  argFloat: outSample
2676
4036
  });
2677
4037
  }
4038
+ captureStatus() {
4039
+ return this.realtimeNode.requestCaptureStatus();
4040
+ }
4041
+ capturedAudio() {
4042
+ return this.realtimeNode.requestCapturedAudio();
4043
+ }
4044
+ async resetCapture() {
4045
+ this.offlineEngine.resetCapture();
4046
+ await this.realtimeNode.requestCaptureReset();
4047
+ }
2678
4048
  setMetronome(opts) {
2679
4049
  this.offlineEngine.setMetronome(opts);
2680
4050
  this.postSync({ type: "syncMetronome", config: opts });
@@ -2690,6 +4060,55 @@ var SonareEngine = class _SonareEngine {
2690
4060
  this.syncMarkers();
2691
4061
  return id;
2692
4062
  }
4063
+ /**
4064
+ * Replaces the whole marker set in one call.
4065
+ *
4066
+ * Entries without an `id` are assigned fresh ids; entries carrying an `id`
4067
+ * keep it (ids must be positive and unique within the list). Returns the
4068
+ * resolved markers in the order given, so a caller can map its own marker
4069
+ * identities to the engine ids used by `seekMarker`/`setLoopFromMarkers`.
4070
+ *
4071
+ * @param markers The full marker list (an empty list clears all markers).
4072
+ * @returns The markers with their resolved engine ids.
4073
+ */
4074
+ setMarkers(markers) {
4075
+ const resolved = [];
4076
+ const seen = /* @__PURE__ */ new Set();
4077
+ for (const marker of markers) {
4078
+ if (!Number.isFinite(marker.ppq)) {
4079
+ throw new Error(`Invalid marker ppq: ${String(marker.ppq)}`);
4080
+ }
4081
+ if (marker.id !== void 0) {
4082
+ if (!Number.isInteger(marker.id) || marker.id <= 0) {
4083
+ throw new Error(`Invalid marker id: ${String(marker.id)}`);
4084
+ }
4085
+ if (seen.has(marker.id)) {
4086
+ throw new Error(`Duplicate marker id: ${marker.id}`);
4087
+ }
4088
+ }
4089
+ const id = marker.id ?? this.nextMarkerId++;
4090
+ seen.add(id);
4091
+ if (id >= this.nextMarkerId) {
4092
+ this.nextMarkerId = id + 1;
4093
+ }
4094
+ resolved.push({ id, ppq: marker.ppq, name: marker.name ?? "" });
4095
+ }
4096
+ this.markers.clear();
4097
+ for (const marker of resolved) {
4098
+ this.markers.set(marker.id, marker);
4099
+ }
4100
+ this.syncMarkers();
4101
+ return resolved.map((marker) => ({ ...marker }));
4102
+ }
4103
+ markerCount() {
4104
+ return this.offlineEngine.markerCount();
4105
+ }
4106
+ markerByIndex(index) {
4107
+ return this.offlineEngine.markerByIndex(index);
4108
+ }
4109
+ marker(markerId) {
4110
+ return this.offlineEngine.marker(markerId);
4111
+ }
2693
4112
  seekMarker(markerId) {
2694
4113
  this.offlineEngine.seekMarker(markerId);
2695
4114
  return this.realtimeNode.sendCommand({
@@ -2698,6 +4117,12 @@ var SonareEngine = class _SonareEngine {
2698
4117
  sampleTime: -1
2699
4118
  });
2700
4119
  }
4120
+ setLoopFromMarkers(startMarkerId, endMarkerId) {
4121
+ this.offlineEngine.setLoopFromMarkers(startMarkerId, endMarkerId);
4122
+ const start = this.offlineEngine.marker(startMarkerId);
4123
+ const end = this.offlineEngine.marker(endMarkerId);
4124
+ return this.setLoop(start.ppq, end.ppq, true);
4125
+ }
2701
4126
  async renderOffline(totalFrames) {
2702
4127
  const frames = Math.max(0, Math.floor(totalFrames));
2703
4128
  const inputs = [];
@@ -2709,6 +4134,9 @@ var SonareEngine = class _SonareEngine {
2709
4134
  onMeter(callback) {
2710
4135
  return this.realtimeNode.onMeter(callback);
2711
4136
  }
4137
+ onScope(callback) {
4138
+ return this.realtimeNode.onScope(callback);
4139
+ }
2712
4140
  onTelemetry(callback) {
2713
4141
  return this.realtimeNode.onTelemetry(callback);
2714
4142
  }
@@ -2718,6 +4146,9 @@ var SonareEngine = class _SonareEngine {
2718
4146
  pollMeters() {
2719
4147
  return this.realtimeNode.pollMeters();
2720
4148
  }
4149
+ pollScope() {
4150
+ return this.realtimeNode.pollScope();
4151
+ }
2721
4152
  destroy() {
2722
4153
  if (this.destroyed) {
2723
4154
  return;
@@ -2728,16 +4159,89 @@ var SonareEngine = class _SonareEngine {
2728
4159
  this.realtimeNode.destroy();
2729
4160
  this.offlineEngine.destroy();
2730
4161
  }
2731
- syncClips() {
4162
+ syncClipsDelta(upserts, removeIds) {
2732
4163
  const clips = Array.from(this.clips.values());
2733
4164
  this.offlineEngine.setClips(clips);
2734
- this.postSync({ type: "syncClips", clips });
4165
+ this.postSync({
4166
+ type: "syncClipsDelta",
4167
+ upserts,
4168
+ removeIds
4169
+ });
4170
+ }
4171
+ syncMidiClips() {
4172
+ const clips = Array.from(this.midiClips.values());
4173
+ this.offlineEngine.setMidiClips(clips);
4174
+ this.postSync({ type: "syncMidiClips", clips });
4175
+ }
4176
+ mixerLanes() {
4177
+ return this.trackLaneIds.map((trackId) => {
4178
+ const sends = this.trackSends.get(trackId);
4179
+ const outputBusId = this.trackOutputBus.get(trackId);
4180
+ return {
4181
+ trackId,
4182
+ ...sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {},
4183
+ ...outputBusId !== void 0 ? { outputBusId } : {}
4184
+ };
4185
+ });
4186
+ }
4187
+ syncMixer() {
4188
+ const lanes = this.mixerLanes();
4189
+ const buses = this.buses.map((bus) => ({ ...bus }));
4190
+ this.offlineEngine.setTrackBuses(buses);
4191
+ if (lanes.length > 0) {
4192
+ this.offlineEngine.setTrackLanes(lanes);
4193
+ }
4194
+ const trackStrips = Array.from(this.trackStripJson, ([trackId, sceneJson]) => ({
4195
+ trackId,
4196
+ sceneJson
4197
+ }));
4198
+ const busStrips = Array.from(this.busStripJson, ([busId, sceneJson]) => ({
4199
+ busId,
4200
+ sceneJson
4201
+ }));
4202
+ this.postSync({
4203
+ type: "syncMixer",
4204
+ lanes,
4205
+ buses,
4206
+ trackStrips,
4207
+ laneSidechains: Array.from(this.laneSidechains.values()),
4208
+ busStrips,
4209
+ masterStripJson: this.masterStripJson
4210
+ });
2735
4211
  }
2736
4212
  syncMarkers() {
2737
4213
  const markers = Array.from(this.markers.values()).sort((a, b) => a.ppq - b.ppq);
2738
4214
  this.offlineEngine.setMarkers(markers);
2739
4215
  this.postSync({ type: "syncMarkers", markers });
2740
4216
  }
4217
+ postInstrumentSync(message) {
4218
+ if (this.destroyed) {
4219
+ return;
4220
+ }
4221
+ if (this.transportPlaying) {
4222
+ this.pendingInstrumentSync.push(message);
4223
+ return;
4224
+ }
4225
+ this.postSync(message);
4226
+ }
4227
+ flushPendingInstrumentSync() {
4228
+ if (this.destroyed || this.pendingInstrumentSync.length === 0) {
4229
+ return;
4230
+ }
4231
+ const pending = this.pendingInstrumentSync.splice(0);
4232
+ for (const message of pending) {
4233
+ this.postSync(message);
4234
+ }
4235
+ }
4236
+ postTempoSync() {
4237
+ this.postSync({
4238
+ type: "syncTempo",
4239
+ bpm: this.tempoBpm,
4240
+ timeSignature: { ...this.timeSignature },
4241
+ tempoSegments: this.tempoSegments.map((segment) => ({ ...segment })),
4242
+ timeSignatureSegments: this.timeSignatureSegments.map((segment) => ({ ...segment }))
4243
+ });
4244
+ }
2741
4245
  // Posts an out-of-band control-sync message to the worklet engine processor.
2742
4246
  // Sync messages use a string `type` so the worklet's message handler routes
2743
4247
  // them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
@@ -2764,15 +4268,38 @@ var SonareEngine = class _SonareEngine {
2764
4268
  const parsed = Number.parseInt(target, 10);
2765
4269
  return Number.isFinite(parsed) ? parsed : 0;
2766
4270
  }
4271
+ ensureTrackLane(target) {
4272
+ const trackId = this.resolveTargetId(target);
4273
+ if (!Number.isInteger(trackId) || trackId <= 0) {
4274
+ throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
4275
+ }
4276
+ const existing = this.trackLaneIds.indexOf(trackId);
4277
+ if (existing >= 0) {
4278
+ return existing;
4279
+ }
4280
+ this.trackLaneIds.push(trackId);
4281
+ this.syncMixer();
4282
+ return this.trackLaneIds.length - 1;
4283
+ }
4284
+ ensureBus(busId) {
4285
+ const resolved = Math.trunc(busId);
4286
+ if (!Number.isInteger(resolved) || resolved <= 0) {
4287
+ throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
4288
+ }
4289
+ const existing = this.buses.findIndex((bus) => bus.busId === resolved);
4290
+ if (existing >= 0) {
4291
+ return existing;
4292
+ }
4293
+ this.buses.push({ busId: resolved });
4294
+ this.syncMixer();
4295
+ return this.buses.length - 1;
4296
+ }
2767
4297
  curveCode(curve) {
2768
4298
  if (typeof curve === "number") {
2769
4299
  return curve;
2770
4300
  }
2771
4301
  return curve === "exponential" ? 1 : 0;
2772
4302
  }
2773
- ppqToApproxSample(ppq) {
2774
- return Math.max(0, Math.round(ppq * 60 / 120 * this.sampleRate));
2775
- }
2776
4303
  };
2777
4304
  var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
2778
4305
  constructor(options = {}) {
@@ -2993,23 +4520,33 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2993
4520
  class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
2994
4521
  constructor(options) {
2995
4522
  super();
4523
+ this.pendingMessages = [];
2996
4524
  const port = this.port;
2997
4525
  const processorOptions = options?.processorOptions ?? {};
2998
4526
  if (processorOptions.runtimeTarget === "sonare-rt") {
2999
4527
  void this.initializeSonareRt(processorOptions, port);
3000
4528
  } else {
3001
- this.bridge = new SonareRealtimeEngineWorkletProcessor(processorOptions, {
3002
- postMessage: (message) => port?.postMessage?.(message),
3003
- onMeter: (meter) => port?.postMessage?.(meter)
3004
- });
4529
+ void this.initializeEmbind(processorOptions, port);
3005
4530
  }
3006
4531
  const onMessage = (event) => {
4532
+ if (!this.bridge && !this.rtBridge) {
4533
+ if (this.pendingMessages.length < 1024) {
4534
+ this.pendingMessages.push(event.data);
4535
+ }
4536
+ return;
4537
+ }
3007
4538
  if (isEngineCommandRecord(event.data)) {
3008
4539
  this.bridge?.receiveCommand(event.data);
3009
4540
  this.rtBridge?.receiveCommand(event.data);
3010
4541
  } else if (isEngineSyncMessage(event.data)) {
3011
4542
  this.bridge?.receiveSync(event.data);
3012
4543
  this.rtBridge?.receiveSync(event.data);
4544
+ } else if (isEngineCaptureRequestMessage(event.data)) {
4545
+ this.bridge?.receiveCaptureRequest(event.data);
4546
+ this.rtBridge?.receiveCaptureRequest(event.data, port);
4547
+ } else if (isEngineTransportRequestMessage(event.data)) {
4548
+ this.bridge?.receiveTransportRequest(event.data);
4549
+ this.rtBridge?.receiveTransportRequest(event.data, port);
3013
4550
  }
3014
4551
  };
3015
4552
  if (port?.addEventListener) {
@@ -3032,6 +4569,60 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
3032
4569
  }
3033
4570
  return true;
3034
4571
  }
4572
+ replayPendingMessages(port) {
4573
+ const messages = this.pendingMessages.splice(0);
4574
+ for (const data of messages) {
4575
+ if (isEngineCommandRecord(data)) {
4576
+ this.bridge?.receiveCommand(data);
4577
+ this.rtBridge?.receiveCommand(data);
4578
+ } else if (isEngineSyncMessage(data)) {
4579
+ this.bridge?.receiveSync(data);
4580
+ this.rtBridge?.receiveSync(data);
4581
+ } else if (isEngineCaptureRequestMessage(data)) {
4582
+ this.bridge?.receiveCaptureRequest(data);
4583
+ this.rtBridge?.receiveCaptureRequest(data, port);
4584
+ } else if (isEngineTransportRequestMessage(data)) {
4585
+ this.bridge?.receiveTransportRequest(data);
4586
+ this.rtBridge?.receiveTransportRequest(data, port);
4587
+ }
4588
+ }
4589
+ }
4590
+ async initializeEmbind(options, port) {
4591
+ try {
4592
+ const initPromise2 = globalThis.SonareEmbindInitPromise;
4593
+ if (initPromise2) {
4594
+ await initPromise2;
4595
+ }
4596
+ if (!isInitialized()) {
4597
+ const moduleFactory = globalThis.SonareEmbindModuleFactory;
4598
+ if (!moduleFactory) {
4599
+ throw new Error("embind realtime engine module is not initialized.");
4600
+ }
4601
+ await init({
4602
+ locateFile: (path) => path,
4603
+ wasmBinary: options.wasmBinary,
4604
+ moduleFactory
4605
+ });
4606
+ }
4607
+ this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
4608
+ postMessage: (message) => port?.postMessage?.(message),
4609
+ onMeter: (meter) => port?.postMessage?.(meter)
4610
+ });
4611
+ for (const message of options.initialSyncMessages ?? []) {
4612
+ this.bridge.receiveSync(message);
4613
+ }
4614
+ for (const command of options.initialCommands ?? []) {
4615
+ this.bridge.receiveCommand(command);
4616
+ }
4617
+ this.replayPendingMessages(port);
4618
+ port?.postMessage?.({ type: "ready", runtimeTarget: "embind" });
4619
+ } catch (error) {
4620
+ port?.postMessage?.({
4621
+ type: "error",
4622
+ message: error instanceof Error ? error.message : String(error)
4623
+ });
4624
+ }
4625
+ }
3035
4626
  async initializeSonareRt(options, port) {
3036
4627
  try {
3037
4628
  if (!options.rtModuleUrl) {
@@ -3057,6 +4648,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
3057
4648
  telemetrySharedBuffer: options.telemetrySharedBuffer,
3058
4649
  telemetryRingCapacity: options.telemetryRingCapacity
3059
4650
  });
4651
+ this.replayPendingMessages(port);
3060
4652
  port?.postMessage?.({ type: "ready", runtimeTarget: "sonare-rt" });
3061
4653
  } catch (error) {
3062
4654
  port?.postMessage?.({
@@ -3074,6 +4666,7 @@ export {
3074
4666
  SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
3075
4667
  SONARE_METER_RING_HEADER_INTS,
3076
4668
  SONARE_METER_RING_RECORD_FLOATS,
4669
+ SONARE_SCOPE_RING_HEADER_INTS,
3077
4670
  SONARE_SPECTRUM_RING_HEADER_INTS,
3078
4671
  SonareEngine,
3079
4672
  SonareEngineCommandType,
@@ -3087,6 +4680,7 @@ export {
3087
4680
  createSonareEngineCommandRingBuffer,
3088
4681
  createSonareEngineTelemetryRingBuffer,
3089
4682
  createSonareMeterRingBuffer,
4683
+ createSonareScopeRingBuffer,
3090
4684
  createSonareSpectrumRingBuffer,
3091
4685
  decodeFrame,
3092
4686
  encodeFrameHi,
@@ -3097,6 +4691,7 @@ export {
3097
4691
  pushSonareEngineCommandRingBuffer,
3098
4692
  readSonareEngineTelemetryRingBuffer,
3099
4693
  readSonareMeterRingBuffer,
4694
+ readSonareScopeRingBuffer,
3100
4695
  readSonareSpectrumRingBuffer,
3101
4696
  registerSonareRealtimeEngineWorkletProcessor,
3102
4697
  registerSonareRealtimeVoiceChangerWorkletProcessor,
@@ -3104,6 +4699,7 @@ export {
3104
4699
  sonareEngineCommandRingBufferByteLength,
3105
4700
  sonareEngineTelemetryRingBufferByteLength,
3106
4701
  sonareMeterRingBufferByteLength,
4702
+ sonareScopeRingBufferByteLength,
3107
4703
  sonareSpectrumRingBufferByteLength,
3108
4704
  writeSonareEngineTelemetryRingBuffer
3109
4705
  };