@libraz/libsonare 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worklet.js CHANGED
@@ -229,6 +229,31 @@ var RealtimeEngine = class {
229
229
  process(channels) {
230
230
  return this.native.process(channels);
231
231
  }
232
+ /**
233
+ * Allocates persistent per-channel WASM-heap scratch for the zero-copy
234
+ * `getChannelBuffer` / `processPrepared` realtime path. Call once (off the
235
+ * audio thread) before driving `processPrepared` from an AudioWorklet so the
236
+ * render callback never allocates on the C++/JS heap.
237
+ */
238
+ prepareChannels(numChannels, maxFrames) {
239
+ this.native.prepareChannels(numChannels, maxFrames);
240
+ }
241
+ /**
242
+ * Returns a Float32Array view onto the persistent WASM-heap scratch for one
243
+ * channel (valid for up to `numFrames`). Fill it, call `processPrepared`, then
244
+ * read the same view back. Re-acquire after WASM memory growth.
245
+ */
246
+ getChannelBuffer(channel, numFrames) {
247
+ return this.native.getChannelBuffer(channel, numFrames);
248
+ }
249
+ /**
250
+ * Runs the engine in place over the prepared per-channel scratch buffers.
251
+ * Allocation-free: safe to call on the AudioWorklet render thread after
252
+ * `prepareChannels`.
253
+ */
254
+ processPrepared(numFrames) {
255
+ this.native.processPrepared(numFrames);
256
+ }
232
257
  processWithMonitor(channels) {
233
258
  return this.native.processWithMonitor(channels);
234
259
  }
@@ -696,8 +721,20 @@ var Mixer = class _Mixer {
696
721
 
697
722
  // src/worklet.ts
698
723
  var SONARE_METER_RING_HEADER_INTS = 4;
699
- var SONARE_METER_RING_RECORD_FLOATS = 6;
724
+ var SONARE_METER_RING_RECORD_FLOATS = 7;
700
725
  var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
726
+ var SONARE_FRAME_LANE_BASE = 16777216;
727
+ function encodeFrameLo(frame) {
728
+ const f = Math.max(0, Math.floor(frame));
729
+ return f % SONARE_FRAME_LANE_BASE;
730
+ }
731
+ function encodeFrameHi(frame) {
732
+ const f = Math.max(0, Math.floor(frame));
733
+ return Math.floor(f / SONARE_FRAME_LANE_BASE);
734
+ }
735
+ function decodeFrame(lo, hi) {
736
+ return hi * SONARE_FRAME_LANE_BASE + lo;
737
+ }
701
738
  var SONARE_ENGINE_RING_HEADER_INTS = 5;
702
739
  var SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
703
740
  var SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
@@ -744,6 +781,18 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
744
781
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["SmoothedParameterCapacity"] = 13] = "SmoothedParameterCapacity";
745
782
  return SonareEngineTelemetryError2;
746
783
  })(SonareEngineTelemetryError || {});
784
+ var DEFAULT_METRONOME_CONFIG = {
785
+ beatGain: 0.35,
786
+ accentGain: 0.7,
787
+ clickSamples: 96
788
+ };
789
+ function resolveMetronomeConfig(config) {
790
+ return {
791
+ beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
792
+ accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
793
+ clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples
794
+ };
795
+ }
747
796
  function toDb(value) {
748
797
  return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
749
798
  }
@@ -759,6 +808,12 @@ function isWorkletMessage(value) {
759
808
  function isEngineCommandRecord(value) {
760
809
  return isRecord(value) && typeof value.type === "number";
761
810
  }
811
+ function isEngineSyncMessage(value) {
812
+ if (!isRecord(value) || typeof value.type !== "string") {
813
+ return false;
814
+ }
815
+ return value.type === "syncClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation";
816
+ }
762
817
  function isRealtimeVoiceChangerMessage(value) {
763
818
  if (!isRecord(value) || typeof value.type !== "string") {
764
819
  return false;
@@ -794,7 +849,7 @@ function readSonareMeterRingBuffer(ring, readIndex = 0) {
794
849
  const offset = index % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
795
850
  meters.push({
796
851
  type: "meter",
797
- frame: ring.records[offset],
852
+ frame: decodeFrame(ring.records[offset], ring.records[offset + 6]),
798
853
  peakDbL: ring.records[offset + 1],
799
854
  peakDbR: ring.records[offset + 2],
800
855
  rmsDbL: ring.records[offset + 3],
@@ -807,7 +862,7 @@ function readSonareMeterRingBuffer(ring, readIndex = 0) {
807
862
  function sonareSpectrumRingBufferByteLength(capacity, bands = 16) {
808
863
  const clampedCapacity = Math.max(1, Math.floor(capacity));
809
864
  const clampedBands = Math.max(1, Math.floor(bands));
810
- return SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * (2 + clampedBands) * Float32Array.BYTES_PER_ELEMENT;
865
+ return SONARE_SPECTRUM_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * (3 + clampedBands) * Float32Array.BYTES_PER_ELEMENT;
811
866
  }
812
867
  function createSonareSpectrumRingBuffer(capacity = 128, bands = 16) {
813
868
  const clampedCapacity = Math.max(1, Math.floor(capacity));
@@ -831,7 +886,7 @@ function createSonareSpectrumRingBuffer(capacity = 128, bands = 16) {
831
886
  }
832
887
  function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
833
888
  const writeIndex = Atomics.load(ring.header, 0);
834
- const recordFloats = Atomics.load(ring.header, 2) || 2 + ring.bands;
889
+ const recordFloats = Atomics.load(ring.header, 2) || 3 + ring.bands;
835
890
  const bands = Atomics.load(ring.header, 3) || ring.bands;
836
891
  const nextReadIndex = Math.max(0, Math.min(readIndex, writeIndex));
837
892
  const firstReadable = Math.max(nextReadIndex, writeIndex - ring.capacity);
@@ -839,8 +894,12 @@ function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
839
894
  for (let index = firstReadable; index < writeIndex; index++) {
840
895
  const offset = index % ring.capacity * recordFloats;
841
896
  const values = new Float32Array(bands);
842
- values.set(ring.records.subarray(offset + 2, offset + 2 + bands));
843
- spectra.push({ type: "spectrum", frame: ring.records[offset], bands: values });
897
+ values.set(ring.records.subarray(offset + 3, offset + 3 + bands));
898
+ spectra.push({
899
+ type: "spectrum",
900
+ frame: decodeFrame(ring.records[offset], ring.records[offset + 1]),
901
+ bands: values
902
+ });
844
903
  }
845
904
  return { nextReadIndex: writeIndex, spectra };
846
905
  }
@@ -959,7 +1018,7 @@ function spectrumRingFromSharedBuffer(sharedBuffer, fallbackCapacity, fallbackBa
959
1018
  const existingBands = Atomics.load(header, 3);
960
1019
  const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
961
1020
  const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 16));
962
- const recordFloats = 2 + bands;
1021
+ const recordFloats = 3 + bands;
963
1022
  const minBytes = sonareSpectrumRingBufferByteLength(capacity, bands);
964
1023
  if (sharedBuffer.byteLength < minBytes) {
965
1024
  throw new Error("spectrumSharedBuffer is too small for the requested ring capacity.");
@@ -1008,8 +1067,7 @@ function writeEngineCommandRecord(view, offset, command) {
1008
1067
  view.setUint32(offset, command.type, true);
1009
1068
  view.setUint32(offset + 4, command.targetId ?? 0, true);
1010
1069
  view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
1011
- view.setFloat32(offset + 16, command.argFloat ?? 0, true);
1012
- view.setUint32(offset + 20, 0, true);
1070
+ view.setFloat64(offset + 16, command.argFloat ?? 0, true);
1013
1071
  view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
1014
1072
  }
1015
1073
  function readEngineCommandRecord(view, offset) {
@@ -1017,7 +1075,7 @@ function readEngineCommandRecord(view, offset) {
1017
1075
  type: view.getUint32(offset, true),
1018
1076
  targetId: view.getUint32(offset + 4, true),
1019
1077
  sampleTime: Number(view.getBigInt64(offset + 8, true)),
1020
- argFloat: view.getFloat32(offset + 16, true),
1078
+ argFloat: view.getFloat64(offset + 16, true),
1021
1079
  argInt: Number(view.getBigInt64(offset + 24, true))
1022
1080
  };
1023
1081
  }
@@ -1110,35 +1168,48 @@ var SonareWorkletProcessor = class {
1110
1168
  return true;
1111
1169
  }
1112
1170
  const frames = leftOut.length;
1113
- if (frames !== this.blockSize) {
1114
- return false;
1115
- }
1171
+ const usable = Math.min(frames, this.blockSize);
1116
1172
  for (let strip = 0; strip < this.realtime.leftInputs.length; strip++) {
1117
1173
  const input = inputs[strip];
1118
1174
  const left = input?.[0];
1119
1175
  const right = input?.[1];
1120
1176
  const leftTarget = this.realtime.leftInputs[strip];
1121
1177
  const rightTarget = this.realtime.rightInputs[strip];
1122
- if (left && left.length === frames) {
1123
- leftTarget.set(left);
1124
- if (right && right.length === frames) {
1125
- rightTarget.set(right);
1178
+ if (left && left.length >= usable) {
1179
+ leftTarget.set(left.subarray(0, usable));
1180
+ if (right && right.length >= usable) {
1181
+ rightTarget.set(right.subarray(0, usable));
1126
1182
  } else {
1127
- rightTarget.set(left);
1183
+ rightTarget.set(left.subarray(0, usable));
1128
1184
  }
1129
1185
  } else {
1130
1186
  leftTarget.fill(0);
1131
1187
  rightTarget.fill(0);
1132
1188
  }
1133
1189
  }
1134
- this.realtime.process(frames);
1135
- leftOut.set(this.realtime.outLeft);
1136
- if (rightOut) {
1137
- rightOut.set(this.realtime.outRight);
1190
+ this.realtime.process(usable);
1191
+ if (usable === frames) {
1192
+ leftOut.set(this.realtime.outLeft.subarray(0, usable));
1193
+ if (rightOut) {
1194
+ rightOut.set(this.realtime.outRight.subarray(0, usable));
1195
+ }
1196
+ } else {
1197
+ leftOut.fill(0);
1198
+ leftOut.set(this.realtime.outLeft.subarray(0, usable));
1199
+ if (rightOut) {
1200
+ rightOut.fill(0);
1201
+ rightOut.set(this.realtime.outRight.subarray(0, usable));
1202
+ }
1138
1203
  }
1139
- this.processedFrames += frames;
1140
- this.publishMeter(this.realtime.outLeft, this.realtime.outRight);
1141
- this.publishSpectrum(this.realtime.outLeft, this.realtime.outRight);
1204
+ this.processedFrames += usable;
1205
+ this.publishMeter(
1206
+ this.realtime.outLeft.subarray(0, usable),
1207
+ this.realtime.outRight.subarray(0, usable)
1208
+ );
1209
+ this.publishSpectrum(
1210
+ this.realtime.outLeft.subarray(0, usable),
1211
+ this.realtime.outRight.subarray(0, usable)
1212
+ );
1142
1213
  return true;
1143
1214
  }
1144
1215
  receiveMessage(message) {
@@ -1224,16 +1295,14 @@ var SonareWorkletProcessor = class {
1224
1295
  }
1225
1296
  const writeIndex = Atomics.load(ring.header, 0);
1226
1297
  const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1227
- ring.records[offset] = meter.frame;
1298
+ ring.records[offset] = encodeFrameLo(meter.frame);
1228
1299
  ring.records[offset + 1] = meter.peakDbL;
1229
1300
  ring.records[offset + 2] = meter.peakDbR;
1230
1301
  ring.records[offset + 3] = meter.rmsDbL;
1231
1302
  ring.records[offset + 4] = meter.rmsDbR;
1232
1303
  ring.records[offset + 5] = meter.correlation;
1304
+ ring.records[offset + 6] = encodeFrameHi(meter.frame);
1233
1305
  Atomics.store(ring.header, 0, writeIndex + 1);
1234
- if (writeIndex + 1 > ring.capacity) {
1235
- Atomics.store(ring.header, 3, writeIndex + 1 - ring.capacity);
1236
- }
1237
1306
  }
1238
1307
  publishSpectrum(left, right) {
1239
1308
  if (this.spectrumIntervalFrames <= 0) {
@@ -1258,7 +1327,12 @@ var SonareWorkletProcessor = class {
1258
1327
  }
1259
1328
  computeSpectrum(left, right) {
1260
1329
  const n = Math.max(1, Math.min(left.length, right.length));
1330
+ const maxBand = Math.floor(n / 2);
1261
1331
  for (let band = 0; band < this.spectrumBands.length; band++) {
1332
+ if (band >= maxBand) {
1333
+ this.spectrumBands[band] = magnitudeToDb(0);
1334
+ continue;
1335
+ }
1262
1336
  const bin = band + 1;
1263
1337
  let real = 0;
1264
1338
  let imag = 0;
@@ -1278,19 +1352,20 @@ var SonareWorkletProcessor = class {
1278
1352
  }
1279
1353
  const writeIndex = Atomics.load(ring.header, 0);
1280
1354
  const offset = writeIndex % ring.capacity * ring.recordFloats;
1281
- ring.records[offset] = frame;
1282
- ring.records[offset + 1] = bands.length;
1283
- ring.records.set(bands.subarray(0, ring.bands), offset + 2);
1355
+ ring.records[offset] = encodeFrameLo(frame);
1356
+ ring.records[offset + 1] = encodeFrameHi(frame);
1357
+ ring.records[offset + 2] = bands.length;
1358
+ ring.records.set(bands.subarray(0, ring.bands), offset + 3);
1284
1359
  Atomics.store(ring.header, 0, writeIndex + 1);
1285
- if (writeIndex + 1 > ring.capacity) {
1286
- Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
1287
- }
1288
1360
  }
1289
1361
  };
1290
1362
  var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletProcessor {
1291
1363
  constructor(options = {}, transport) {
1292
1364
  this.closed = false;
1293
1365
  this.lastMeterFrame = Number.NEGATIVE_INFINITY;
1366
+ // Latest metronome gains/click length pushed via 'syncMetronome'. The
1367
+ // SetMetronome command only toggles enabled state; the config arrives here.
1368
+ this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
1294
1369
  this.sampleRate = options.sampleRate ?? 48e3;
1295
1370
  this.blockSize = options.blockSize ?? 128;
1296
1371
  this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
@@ -1307,12 +1382,12 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1307
1382
  options.telemetrySharedBuffer,
1308
1383
  options.telemetryRingCapacity
1309
1384
  ) : void 0;
1385
+ this.meterRing = options.meterSharedBuffer ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity) : void 0;
1310
1386
  this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
1311
- this.channelScratch = new Array(this.channelCount);
1312
- this.channelScratchViews = new Array(this.channelCount);
1387
+ this.engine.prepareChannels(this.channelCount, this.blockSize);
1388
+ this.channelBuffers = new Array(this.channelCount);
1313
1389
  for (let ch = 0; ch < this.channelCount; ch++) {
1314
- this.channelScratch[ch] = new Float32Array(this.blockSize);
1315
- this.channelScratchViews[ch] = this.channelScratch[ch];
1390
+ this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
1316
1391
  }
1317
1392
  }
1318
1393
  process(inputs, outputs) {
@@ -1333,34 +1408,38 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1333
1408
  return true;
1334
1409
  }
1335
1410
  this.drainCommands();
1336
- const scratchCapacity = this.channelScratch[0]?.length ?? 0;
1337
1411
  let usableFrames = frames;
1338
- if (usableFrames > scratchCapacity) {
1412
+ if (usableFrames > this.blockSize) {
1339
1413
  if (!_SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow) {
1340
1414
  _SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow = true;
1341
1415
  console.warn(
1342
- `SonareRealtimeEngineWorkletProcessor: requested ${usableFrames} frames exceeds pre-allocated capacity ${scratchCapacity}; clamping.`
1416
+ `SonareRealtimeEngineWorkletProcessor: requested ${usableFrames} frames exceeds pre-allocated capacity ${this.blockSize}; clamping.`
1343
1417
  );
1344
1418
  }
1345
- usableFrames = scratchCapacity;
1419
+ usableFrames = this.blockSize;
1420
+ }
1421
+ if ((this.channelBuffers[0]?.byteLength ?? 0) === 0) {
1422
+ this.reacquireChannelBuffers();
1346
1423
  }
1347
1424
  const input = inputs[0];
1348
1425
  for (let ch = 0; ch < this.channelCount; ch++) {
1349
- const scratch = this.channelScratch[ch];
1426
+ const dst = this.channelBuffers[ch];
1350
1427
  const source = input?.[ch];
1351
1428
  if (source && source.length === usableFrames) {
1352
- scratch.set(source, 0);
1429
+ dst.set(source.subarray(0, usableFrames));
1353
1430
  } else {
1354
- scratch.fill(0, 0, usableFrames);
1431
+ dst.fill(0, 0, usableFrames);
1355
1432
  }
1356
- this.channelScratchViews[ch] = scratch.subarray(0, usableFrames);
1357
1433
  }
1358
- const processed = this.engine.process(this.channelScratchViews);
1434
+ this.engine.processPrepared(usableFrames);
1359
1435
  for (let ch = 0; ch < output.length; ch++) {
1360
1436
  const target = output[ch];
1361
- const source = processed[ch] ?? processed[0];
1437
+ const source = this.channelBuffers[ch] ?? this.channelBuffers[0];
1362
1438
  if (source) {
1363
- target.set(source.subarray(0, target.length));
1439
+ target.set(source.subarray(0, Math.min(target.length, usableFrames)));
1440
+ if (target.length > usableFrames) {
1441
+ target.fill(0, usableFrames);
1442
+ }
1364
1443
  } else {
1365
1444
  target.fill(0);
1366
1445
  }
@@ -1369,11 +1448,41 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1369
1448
  this.publishMeters();
1370
1449
  return true;
1371
1450
  }
1451
+ reacquireChannelBuffers() {
1452
+ for (let ch = 0; ch < this.channelCount; ch++) {
1453
+ this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
1454
+ }
1455
+ }
1372
1456
  receiveCommand(command) {
1373
1457
  if (!this.closed) {
1374
1458
  this.applyCommand(command);
1375
1459
  }
1376
1460
  }
1461
+ // Applies an out-of-band control-plane sync message. Runs on the AudioWorklet
1462
+ // global scope but OUTSIDE process() (the message-port callback), so the
1463
+ // bulk/allocating engine setters (setClips/setMarkers) are safe here — they
1464
+ // never run on the realtime render path. This is the audio-thread equivalent
1465
+ // of the engine's control-thread RtPublisher setters.
1466
+ receiveSync(message) {
1467
+ if (this.closed) {
1468
+ return;
1469
+ }
1470
+ switch (message.type) {
1471
+ case "syncClips":
1472
+ this.engine.setClips(message.clips);
1473
+ break;
1474
+ case "syncMarkers":
1475
+ this.engine.setMarkers(message.markers);
1476
+ break;
1477
+ case "syncMetronome":
1478
+ this.metronomeConfig = resolveMetronomeConfig(message.config);
1479
+ this.engine.setMetronome(message.config);
1480
+ break;
1481
+ case "syncAutomation":
1482
+ this.engine.setAutomationLane(message.paramId, message.points);
1483
+ break;
1484
+ }
1485
+ }
1377
1486
  destroy() {
1378
1487
  if (!this.closed) {
1379
1488
  this.engine.destroy();
@@ -1395,6 +1504,20 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1395
1504
  applyCommand(command) {
1396
1505
  const sampleTime = Number(command.sampleTime ?? -1);
1397
1506
  switch (command.type) {
1507
+ case 0 /* SetParam */:
1508
+ this.engine.setParameter(
1509
+ Math.trunc(Number(command.targetId ?? 0)),
1510
+ Number(command.argFloat ?? 0),
1511
+ sampleTime
1512
+ );
1513
+ break;
1514
+ case 1 /* SetParamSmoothed */:
1515
+ this.engine.setParameterSmoothed(
1516
+ Math.trunc(Number(command.targetId ?? 0)),
1517
+ Number(command.argFloat ?? 0),
1518
+ sampleTime
1519
+ );
1520
+ break;
1398
1521
  case 2 /* TransportPlay */:
1399
1522
  this.engine.play(sampleTime);
1400
1523
  break;
@@ -1423,18 +1546,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1423
1546
  case 14 /* Punch */:
1424
1547
  this.engine.setCapturePunch(
1425
1548
  Number(command.argInt ?? 0),
1426
- Math.max(0, Math.round(Number(command.argFloat ?? 0) * this.sampleRate)),
1549
+ Math.max(0, Math.round(Number(command.argFloat ?? 0))),
1427
1550
  true
1428
1551
  );
1429
1552
  break;
1430
1553
  case 15 /* SetMetronome */:
1431
1554
  this.engine.setMetronome({
1432
1555
  enabled: Boolean(command.argInt),
1433
- beatGain: 0.25,
1434
- accentGain: 0.75,
1435
- clickSamples: 64
1556
+ beatGain: this.metronomeConfig.beatGain,
1557
+ accentGain: this.metronomeConfig.accentGain,
1558
+ clickSamples: this.metronomeConfig.clickSamples
1436
1559
  });
1437
1560
  break;
1561
+ case 17 /* SeekMarker */:
1562
+ this.engine.seekMarker(Math.trunc(Number(command.targetId ?? 0)), sampleTime);
1563
+ break;
1438
1564
  default:
1439
1565
  this.publishTelemetryRecord({
1440
1566
  type: 1 /* Error */,
@@ -1461,7 +1587,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1461
1587
  this.transport?.postMessage?.(record);
1462
1588
  }
1463
1589
  publishMeters() {
1464
- if (!this.transport || this.meterIntervalFrames <= 0) {
1590
+ if (this.meterIntervalFrames <= 0 || !this.transport && !this.meterRing) {
1465
1591
  return;
1466
1592
  }
1467
1593
  for (const item of this.engine.drainMeterTelemetry(64)) {
@@ -1470,10 +1596,30 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
1470
1596
  continue;
1471
1597
  }
1472
1598
  this.lastMeterFrame = meter.frame;
1473
- this.transport.onMeter?.(meter);
1474
- this.transport.postMessage?.(meter);
1599
+ if (this.meterRing) {
1600
+ this.writeMeterRing(meter);
1601
+ } else {
1602
+ this.transport?.onMeter?.(meter);
1603
+ this.transport?.postMessage?.(meter);
1604
+ }
1475
1605
  }
1476
1606
  }
1607
+ writeMeterRing(meter) {
1608
+ const ring = this.meterRing;
1609
+ if (!ring) {
1610
+ return;
1611
+ }
1612
+ const writeIndex = Atomics.load(ring.header, 0);
1613
+ const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
1614
+ ring.records[offset] = encodeFrameLo(meter.frame);
1615
+ ring.records[offset + 1] = meter.peakDbL;
1616
+ ring.records[offset + 2] = meter.peakDbR;
1617
+ ring.records[offset + 3] = meter.rmsDbL;
1618
+ ring.records[offset + 4] = meter.rmsDbR;
1619
+ ring.records[offset + 5] = meter.correlation;
1620
+ ring.records[offset + 6] = encodeFrameHi(meter.frame);
1621
+ Atomics.store(ring.header, 0, writeIndex + 1);
1622
+ }
1477
1623
  commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
1478
1624
  const ring = engineRingFromSharedBuffer(
1479
1625
  sharedBuffer,
@@ -1495,6 +1641,7 @@ _SonareRealtimeEngineWorkletProcessor.warnedChannelScratchOverflow = false;
1495
1641
  var SonareRealtimeEngineWorkletProcessor = _SonareRealtimeEngineWorkletProcessor;
1496
1642
  var SonareRtRealtimeEngineRuntime = class {
1497
1643
  constructor(options) {
1644
+ this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
1498
1645
  this.closed = false;
1499
1646
  this.module = options.module;
1500
1647
  this.memory = options.memory;
@@ -1576,6 +1723,49 @@ var SonareRtRealtimeEngineRuntime = class {
1576
1723
  this.publishTelemetry();
1577
1724
  return true;
1578
1725
  }
1726
+ receiveCommand(command) {
1727
+ if (!this.closed) {
1728
+ this.applyCommand(command);
1729
+ }
1730
+ }
1731
+ // Out-of-band control sync for the sonare-rt runtime. The sonare-rt C ABI
1732
+ // (src/wasm/rt_bindings.cpp) exposes set_metronome_enabled and seek_marker but
1733
+ // NOT set_clips / set_markers, so clip/marker mutations cannot be applied to a
1734
+ // live sonare-rt engine. We honor the metronome config and surface a clear
1735
+ // telemetry error for the unsupported clip/marker paths instead of silently
1736
+ // dropping them. The default 'embind' runtime wires all three fully.
1737
+ receiveSync(message) {
1738
+ if (this.closed) {
1739
+ return;
1740
+ }
1741
+ switch (message.type) {
1742
+ case "syncMetronome":
1743
+ this.metronomeConfig = resolveMetronomeConfig(message.config);
1744
+ this.module._sonare_rt_engine_set_metronome_enabled(
1745
+ this.engine,
1746
+ message.config.enabled ? 1 : 0,
1747
+ this.metronomeConfig.beatGain,
1748
+ this.metronomeConfig.accentGain,
1749
+ this.metronomeConfig.clickSamples
1750
+ );
1751
+ break;
1752
+ case "syncClips":
1753
+ case "syncMarkers":
1754
+ case "syncAutomation":
1755
+ if (this.telemetryRing) {
1756
+ writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
1757
+ type: 1 /* Error */,
1758
+ error: 7 /* UnknownTarget */,
1759
+ renderFrame: 0,
1760
+ timelineSample: 0,
1761
+ audibleTimelineSample: 0,
1762
+ graphLatencySamplesQ8: 0,
1763
+ value: 0
1764
+ });
1765
+ }
1766
+ break;
1767
+ }
1768
+ }
1579
1769
  destroy() {
1580
1770
  if (this.closed) {
1581
1771
  return;
@@ -1611,6 +1801,20 @@ var SonareRtRealtimeEngineRuntime = class {
1611
1801
  applyCommand(command) {
1612
1802
  const sampleTime = toBigInt64(command.sampleTime, -1n);
1613
1803
  switch (command.type) {
1804
+ case 0 /* SetParam */:
1805
+ case 1 /* SetParamSmoothed */:
1806
+ if (this.telemetryRing) {
1807
+ writeSonareEngineTelemetryRingBuffer(this.telemetryRing, {
1808
+ type: 1 /* Error */,
1809
+ error: 7 /* UnknownTarget */,
1810
+ renderFrame: 0,
1811
+ timelineSample: 0,
1812
+ audibleTimelineSample: 0,
1813
+ graphLatencySamplesQ8: 0,
1814
+ value: Number(command.type)
1815
+ });
1816
+ }
1817
+ break;
1614
1818
  case 2 /* TransportPlay */:
1615
1819
  this.module._sonare_rt_engine_play(this.engine, sampleTime);
1616
1820
  break;
@@ -1649,7 +1853,7 @@ var SonareRtRealtimeEngineRuntime = class {
1649
1853
  this.module._sonare_rt_engine_set_capture_punch(
1650
1854
  this.engine,
1651
1855
  toBigInt64(command.argInt, 0n),
1652
- BigInt(Math.trunc(Number(command.argFloat ?? 0) * this.sampleRate)),
1856
+ BigInt(Math.max(0, Math.round(Number(command.argFloat ?? 0)))),
1653
1857
  1
1654
1858
  );
1655
1859
  break;
@@ -1657,9 +1861,9 @@ var SonareRtRealtimeEngineRuntime = class {
1657
1861
  this.module._sonare_rt_engine_set_metronome_enabled(
1658
1862
  this.engine,
1659
1863
  command.argInt ? 1 : 0,
1660
- 0.25,
1661
- 0.75,
1662
- 64
1864
+ this.metronomeConfig.beatGain,
1865
+ this.metronomeConfig.accentGain,
1866
+ this.metronomeConfig.clickSamples
1663
1867
  );
1664
1868
  break;
1665
1869
  case 17 /* SeekMarker */:
@@ -1734,8 +1938,9 @@ var SonareRtRealtimeEngineRuntime = class {
1734
1938
  }
1735
1939
  };
1736
1940
  var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
1737
- constructor(node, capabilities, commandRing, telemetryRing) {
1941
+ constructor(node, capabilities, commandRing, telemetryRing, meterRing) {
1738
1942
  this.telemetryReadIndex = 0;
1943
+ this.meterReadIndex = 0;
1739
1944
  this.telemetryListeners = /* @__PURE__ */ new Set();
1740
1945
  this.meterListeners = /* @__PURE__ */ new Set();
1741
1946
  this.destroyed = false;
@@ -1743,6 +1948,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
1743
1948
  this.capabilities = capabilities;
1744
1949
  this.commandRing = commandRing;
1745
1950
  this.telemetryRing = telemetryRing;
1951
+ this.meterRing = meterRing;
1746
1952
  this.ready = new Promise((resolve, reject) => {
1747
1953
  this.resolveReady = resolve;
1748
1954
  this.rejectReady = reject;
@@ -1791,6 +1997,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
1791
1997
  }
1792
1998
  const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(options.commandRingCapacity ?? 128) : void 0;
1793
1999
  const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(options.telemetryRingCapacity ?? 128) : void 0;
2000
+ const meterRing = mode === "sab" && runtimeTarget === "embind" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
1794
2001
  const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
1795
2002
  const processorOptions = {
1796
2003
  runtimeTarget,
@@ -1802,7 +2009,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
1802
2009
  commandSharedBuffer: commandRing?.sharedBuffer,
1803
2010
  commandRingCapacity: commandRing?.capacity,
1804
2011
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
1805
- telemetryRingCapacity: telemetryRing?.capacity
2012
+ telemetryRingCapacity: telemetryRing?.capacity,
2013
+ meterSharedBuffer: meterRing?.sharedBuffer,
2014
+ meterRingCapacity: meterRing?.capacity
1806
2015
  };
1807
2016
  const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
1808
2017
  const node = factory(context, processorName, {
@@ -1825,7 +2034,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
1825
2034
  degradedReason
1826
2035
  },
1827
2036
  commandRing,
1828
- telemetryRing
2037
+ telemetryRing,
2038
+ meterRing
1829
2039
  );
1830
2040
  }
1831
2041
  play(sampleTime = -1) {
@@ -1869,6 +2079,20 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
1869
2079
  }
1870
2080
  return read.telemetry;
1871
2081
  }
2082
+ // Drains any meters published into the SAB meter ring (embind SAB mode) and
2083
+ // forwards them to onMeter listeners. In postMessage mode meters arrive via
2084
+ // node.port.onmessage instead, so this is a no-op then.
2085
+ pollMeters() {
2086
+ if (!this.meterRing) {
2087
+ return [];
2088
+ }
2089
+ const read = readSonareMeterRingBuffer(this.meterRing, this.meterReadIndex);
2090
+ this.meterReadIndex = read.nextReadIndex;
2091
+ for (const meter of read.meters) {
2092
+ this.emitMeter(meter);
2093
+ }
2094
+ return read.meters;
2095
+ }
1872
2096
  onTelemetry(callback) {
1873
2097
  this.telemetryListeners.add(callback);
1874
2098
  return () => {
@@ -1983,10 +2207,14 @@ var SonareEngine = class _SonareEngine {
1983
2207
  });
1984
2208
  }
1985
2209
  setParam(nodeId, param, value) {
1986
- void nodeId;
1987
- void param;
1988
- void value;
1989
- return false;
2210
+ const paramId = this.resolveParamId(nodeId, param);
2211
+ this.offlineEngine.setParameter(paramId, value);
2212
+ return this.realtimeNode.sendCommand({
2213
+ type: 0 /* SetParam */,
2214
+ targetId: paramId,
2215
+ sampleTime: -1,
2216
+ argFloat: value
2217
+ });
1990
2218
  }
1991
2219
  scheduleParam(nodeId, param, ppq, value, curve = "linear") {
1992
2220
  const paramId = this.resolveParamId(nodeId, param);
@@ -1995,6 +2223,7 @@ var SonareEngine = class _SonareEngine {
1995
2223
  lane.sort((a, b) => a.ppq - b.ppq);
1996
2224
  this.automationLanes.set(paramId, lane);
1997
2225
  this.offlineEngine.setAutomationLane(paramId, lane);
2226
+ this.postSync({ type: "syncAutomation", paramId, points: lane });
1998
2227
  }
1999
2228
  addAutomationPoint(laneId, ppq, value, curve = "linear") {
2000
2229
  this.scheduleParam("", laneId, ppq, value, curve);
@@ -2010,7 +2239,9 @@ var SonareEngine = class _SonareEngine {
2010
2239
  void target;
2011
2240
  void solo;
2012
2241
  void mute;
2013
- return false;
2242
+ throw new Error(
2243
+ "SonareEngine.setSoloMute is not supported: solo/mute is a Mixer feature; use Mixer.setSoloed(stripIndex, ...) / Mixer.setMuted(stripIndex, ...) instead."
2244
+ );
2014
2245
  }
2015
2246
  addClip(trackId, buffer, startPpq, opts = {}) {
2016
2247
  const id = opts.id ?? this.nextClipId++;
@@ -2046,11 +2277,12 @@ var SonareEngine = class _SonareEngine {
2046
2277
  type: 14 /* Punch */,
2047
2278
  sampleTime: -1,
2048
2279
  argInt: inSample,
2049
- argFloat: outPpq
2280
+ argFloat: outSample
2050
2281
  });
2051
2282
  }
2052
2283
  setMetronome(opts) {
2053
2284
  this.offlineEngine.setMetronome(opts);
2285
+ this.postSync({ type: "syncMetronome", config: opts });
2054
2286
  this.realtimeNode.sendCommand({
2055
2287
  type: 15 /* SetMetronome */,
2056
2288
  sampleTime: -1,
@@ -2065,7 +2297,11 @@ var SonareEngine = class _SonareEngine {
2065
2297
  }
2066
2298
  seekMarker(markerId) {
2067
2299
  this.offlineEngine.seekMarker(markerId);
2068
- return false;
2300
+ return this.realtimeNode.sendCommand({
2301
+ type: 17 /* SeekMarker */,
2302
+ targetId: markerId,
2303
+ sampleTime: -1
2304
+ });
2069
2305
  }
2070
2306
  async renderOffline(totalFrames) {
2071
2307
  const frames = Math.max(0, Math.floor(totalFrames));
@@ -2084,6 +2320,9 @@ var SonareEngine = class _SonareEngine {
2084
2320
  pollTelemetry() {
2085
2321
  return this.realtimeNode.pollTelemetry();
2086
2322
  }
2323
+ pollMeters() {
2324
+ return this.realtimeNode.pollMeters();
2325
+ }
2087
2326
  destroy() {
2088
2327
  if (this.destroyed) {
2089
2328
  return;
@@ -2095,10 +2334,23 @@ var SonareEngine = class _SonareEngine {
2095
2334
  this.offlineEngine.destroy();
2096
2335
  }
2097
2336
  syncClips() {
2098
- this.offlineEngine.setClips(Array.from(this.clips.values()));
2337
+ const clips = Array.from(this.clips.values());
2338
+ this.offlineEngine.setClips(clips);
2339
+ this.postSync({ type: "syncClips", clips });
2099
2340
  }
2100
2341
  syncMarkers() {
2101
- this.offlineEngine.setMarkers(Array.from(this.markers.values()).sort((a, b) => a.ppq - b.ppq));
2342
+ const markers = Array.from(this.markers.values()).sort((a, b) => a.ppq - b.ppq);
2343
+ this.offlineEngine.setMarkers(markers);
2344
+ this.postSync({ type: "syncMarkers", markers });
2345
+ }
2346
+ // Posts an out-of-band control-sync message to the worklet engine processor.
2347
+ // Sync messages use a string `type` so the worklet's message handler routes
2348
+ // them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
2349
+ postSync(message) {
2350
+ if (this.destroyed) {
2351
+ return;
2352
+ }
2353
+ this.realtimeNode.node.port.postMessage(message);
2102
2354
  }
2103
2355
  resolveParamId(nodeId, param) {
2104
2356
  if (typeof param === "number") {
@@ -2169,6 +2421,9 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
2169
2421
  if (this.destroyed || !output || output.length === 0) {
2170
2422
  return !this.destroyed;
2171
2423
  }
2424
+ if (this.monoInput.byteLength === 0) {
2425
+ this.reacquireBuffers();
2426
+ }
2172
2427
  const input = inputs[0];
2173
2428
  const requestedFrames = output[0]?.length ?? 0;
2174
2429
  const requestedChannels = Math.min(this.channelCount, output.length);
@@ -2220,6 +2475,19 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
2220
2475
  this.destroyed = true;
2221
2476
  this.changer.delete();
2222
2477
  }
2478
+ // Re-acquires the cached WASM-heap views after a memory-growth detachment.
2479
+ // The underlying C++ vectors are pre-warmed (ensure_*_capacity ran at prepare
2480
+ // time), so getMono*/getPlanar* return fresh views onto the SAME storage
2481
+ // without reallocating it.
2482
+ reacquireBuffers() {
2483
+ this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
2484
+ this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
2485
+ if (this.channelCount > 1) {
2486
+ for (let ch = 0; ch < this.channelCount; ch++) {
2487
+ this.planarChannels[ch] = this.changer.getPlanarChannelBuffer(ch, this.blockSize);
2488
+ }
2489
+ }
2490
+ }
2223
2491
  /**
2224
2492
  * Returns the number of frames we can actually process given the
2225
2493
  * pre-allocated capacity. If the host requests more frames than the
@@ -2343,6 +2611,10 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
2343
2611
  const onMessage = (event) => {
2344
2612
  if (isEngineCommandRecord(event.data)) {
2345
2613
  this.bridge?.receiveCommand(event.data);
2614
+ this.rtBridge?.receiveCommand(event.data);
2615
+ } else if (isEngineSyncMessage(event.data)) {
2616
+ this.bridge?.receiveSync(event.data);
2617
+ this.rtBridge?.receiveSync(event.data);
2346
2618
  }
2347
2619
  };
2348
2620
  if (port?.addEventListener) {
@@ -2421,6 +2693,9 @@ export {
2421
2693
  createSonareEngineTelemetryRingBuffer,
2422
2694
  createSonareMeterRingBuffer,
2423
2695
  createSonareSpectrumRingBuffer,
2696
+ decodeFrame,
2697
+ encodeFrameHi,
2698
+ encodeFrameLo,
2424
2699
  init,
2425
2700
  isInitialized,
2426
2701
  popSonareEngineCommandRingBuffer,