@viji-dev/core 0.5.4 → 0.5.6

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.
@@ -1897,7 +1897,7 @@ class EssentiaOnsetDetection {
1897
1897
  this.initPromise = (async () => {
1898
1898
  try {
1899
1899
  const essentiaModule = await import("./essentia.js-core.es-DnrJE0uR.js");
1900
- const wasmModule = await import("./essentia-wasm.web-aU6UPupF.js").then((n) => n.e);
1900
+ const wasmModule = await import("./essentia-wasm.web-B2bIxnGE.js").then((n) => n.e);
1901
1901
  const EssentiaClass = essentiaModule.Essentia || essentiaModule.default?.Essentia || essentiaModule.default;
1902
1902
  let WASMModule = wasmModule.default || wasmModule.EssentiaWASM || wasmModule.default?.EssentiaWASM;
1903
1903
  if (!WASMModule) {
@@ -6181,15 +6181,24 @@ class OnsetTapManager {
6181
6181
  muteChangeListeners = /* @__PURE__ */ new Set();
6182
6182
  /**
6183
6183
  * Record a tap on `instrument`. Drives the visual envelope, advances
6184
- * `lastTapTime`, and (by default) feeds the pattern-recognition pipeline.
6184
+ * `lastTapTime`, flips mode to reflect activity, and (by default) feeds
6185
+ * the pattern-recognition pipeline.
6185
6186
  *
6186
- * `options.skipRecognition: true` retains the visual side and session-timer
6187
- * scheduling but bypasses the recognition pipeline (no `tapIOIs` push,
6188
- * no `tryRecognizePattern`, no `applyPattern`, no `handlePatternTap`).
6189
- * Mode stays whatever it was. Useful for host-side relay of forwarded
6190
- * controller-tap messages where another instance is doing the
6191
- * authoritative recognition. The `MIN_TAP_INTERVAL_MS` debounce still
6192
- * applies (visual rate-limit).
6187
+ * `options.skipRecognition: true` opts the tap out of the **recognition
6188
+ * pipeline only** `tapIOIs` accumulation, `tryRecognizePattern`,
6189
+ * `applyPattern`, and `handlePatternTap` are skipped. Mode still flips
6190
+ * `'auto' 'tapping'` on the first tap of a session (already-`'tapping'`
6191
+ * stays; already-`'pattern'` stays). This is load-bearing: `processFrame`'s
6192
+ * drain of `pendingTapEvents` and its audio-event filter both gate on
6193
+ * mode being non-`'auto'`, and downstream consumers (`onModeChange`
6194
+ * listeners, `broadcastOnsetState`) read mode as the signal that a tap
6195
+ * session is in progress. Suppressing the mode flip would silently break
6196
+ * all of them.
6197
+ *
6198
+ * Used for host-side relay of forwarded tap messages where another
6199
+ * instance owns the authoritative recognition (e.g. host receiving
6200
+ * controller taps over WebRTC). The `MIN_TAP_INTERVAL_MS` debounce still
6201
+ * applies regardless of `skipRecognition`.
6193
6202
  */
6194
6203
  tap(instrument, options) {
6195
6204
  const s = this.state[instrument];
@@ -6218,7 +6227,12 @@ class OnsetTapManager {
6218
6227
  s.pendingTapEvents.push(now);
6219
6228
  s.sessionActive = true;
6220
6229
  this.scheduleSessionTimers(instrument);
6221
- if (skipRecognition) return;
6230
+ if (skipRecognition) {
6231
+ if (s.mode === "auto") {
6232
+ this.setMode(instrument, "tapping");
6233
+ }
6234
+ return;
6235
+ }
6222
6236
  if (s.mode === "auto") {
6223
6237
  this.setMode(instrument, "tapping");
6224
6238
  if (ioi > 0) {
@@ -7408,6 +7422,7 @@ class AudioChannel {
7408
7422
  if (this.workletNode) {
7409
7423
  try {
7410
7424
  this.workletNode.port.onmessage = null;
7425
+ this.workletNode.port.close();
7411
7426
  this.workletNode.disconnect();
7412
7427
  } catch (_) {
7413
7428
  }
@@ -7457,6 +7472,14 @@ class AudioSystem {
7457
7472
  onsetLogBuffer = [];
7458
7473
  // Debug logging control
7459
7474
  debugMode = false;
7475
+ /**
7476
+ * One-way lifecycle flag flipped at the top of `resetAudioState()`.
7477
+ * Gates re-entry into `ensureAudioContext()` and the public stream-mutation
7478
+ * entry points so an in-flight async caller (e.g. a worker RPC firing
7479
+ * mid-teardown) cannot resurrect a fresh `AudioContext` after destroy
7480
+ * has already nulled the field. AudioSystem is single-use after reset.
7481
+ */
7482
+ destroyed = false;
7460
7483
  // Diagnostic logger
7461
7484
  diagnosticLogger = new DiagnosticLogger();
7462
7485
  // Industry-grade audio analysis modules (4-layer architecture)
@@ -8042,6 +8065,7 @@ class AudioSystem {
8042
8065
  * Handle audio stream update for main stream (called from VijiCore)
8043
8066
  */
8044
8067
  handleAudioStreamUpdate(data) {
8068
+ if (this.destroyed) return;
8045
8069
  try {
8046
8070
  if (data.audioStream) {
8047
8071
  this.setAudioStream(data.audioStream);
@@ -8058,6 +8082,9 @@ class AudioSystem {
8058
8082
  * Ensure the shared AudioContext is created and resumed.
8059
8083
  */
8060
8084
  async ensureAudioContext() {
8085
+ if (this.destroyed) {
8086
+ throw new Error("AudioSystem is destroyed");
8087
+ }
8061
8088
  if (!this.audioContext) {
8062
8089
  this.audioContext = new AudioContext();
8063
8090
  }
@@ -8109,6 +8136,7 @@ class AudioSystem {
8109
8136
  if (ch.workletNode) {
8110
8137
  try {
8111
8138
  ch.workletNode.port.onmessage = null;
8139
+ ch.workletNode.port.close();
8112
8140
  ch.workletNode.disconnect();
8113
8141
  } catch {
8114
8142
  }
@@ -8159,6 +8187,7 @@ class AudioSystem {
8159
8187
  * Set the main audio stream for analysis (preserves original public API surface).
8160
8188
  */
8161
8189
  async setAudioStream(audioStream) {
8190
+ if (this.destroyed) return;
8162
8191
  this.disconnectMainStream();
8163
8192
  this.resetEssentiaBandHistories();
8164
8193
  await this.connectChannel(this.mainChannel, audioStream, true);
@@ -8430,8 +8459,14 @@ class AudioSystem {
8430
8459
  /**
8431
8460
  * Reset all audio state (called when destroying).
8432
8461
  * Disconnects all channels, closes AudioContext, resets all modules.
8462
+ *
8463
+ * Returns a Promise that resolves once `AudioContext.close()` has fully
8464
+ * transitioned the context to `closed`. Callers tearing the system down
8465
+ * (e.g. `VijiCore.destroy()`) MUST await this; otherwise Blink keeps the
8466
+ * context alive in `Pending activities` and leaks ~4 MB per cycle.
8433
8467
  */
8434
- resetAudioState() {
8468
+ async resetAudioState() {
8469
+ this.destroyed = true;
8435
8470
  this.stopAnalysisLoop();
8436
8471
  this.stopStalenessTimer();
8437
8472
  this.stopIdleTicker();
@@ -8444,9 +8479,14 @@ class AudioSystem {
8444
8479
  this.mainChannel.audioState.isConnected = false;
8445
8480
  this.mainChannel.isAnalysisRunning = false;
8446
8481
  this.mainChannel.currentStream = null;
8447
- if (this.audioContext && this.audioContext.state !== "closed") {
8448
- this.audioContext.close();
8449
- this.audioContext = null;
8482
+ const ctxToClose = this.audioContext;
8483
+ this.audioContext = null;
8484
+ if (ctxToClose && ctxToClose.state !== "closed") {
8485
+ try {
8486
+ await ctxToClose.close();
8487
+ } catch (e) {
8488
+ this.debugLog(`AudioContext.close() rejected: ${e?.message ?? e}`);
8489
+ }
8450
8490
  }
8451
8491
  this.workletRegistered = false;
8452
8492
  this.onsetDetection.reset();
@@ -8468,6 +8508,7 @@ class AudioSystem {
8468
8508
  * @param stream The MediaStream to analyze
8469
8509
  */
8470
8510
  async addStream(streamIndex, stream) {
8511
+ if (this.destroyed) return;
8471
8512
  if (this.additionalChannels.has(streamIndex)) {
8472
8513
  this.removeStream(streamIndex);
8473
8514
  }
@@ -8497,6 +8538,7 @@ class AudioSystem {
8497
8538
  * Mirrors video's reinitializeAdditionalCoordinators pattern.
8498
8539
  */
8499
8540
  async reinitializeAdditionalChannels(streams, baseIndex) {
8541
+ if (this.destroyed) return;
8500
8542
  const toRemove = [];
8501
8543
  for (const [idx, ch] of this.additionalChannels) {
8502
8544
  if (idx >= baseIndex && idx < baseIndex + 100) {
@@ -8525,9 +8567,12 @@ class AudioSystem {
8525
8567
  // ═══════════════════════════════════════════════════════════
8526
8568
  /**
8527
8569
  * Record a tap for the specified instrument onset.
8528
- * `options.skipRecognition` retains visual envelope + session timing but
8529
- * bypasses the recognition pipeline used by host-side relay of forwarded
8530
- * tap messages where another core is doing the authoritative recognition.
8570
+ * `options.skipRecognition` opts the tap out of the recognition pipeline
8571
+ * only (no IOI accumulation, no `tryRecognizePattern`, no `applyPattern`).
8572
+ * Mode still flips `'auto' 'tapping'` so the visual envelope and
8573
+ * audio-event filter run; only the pattern-detection pipeline is bypassed.
8574
+ * Used by host-side relay of forwarded tap messages where another core
8575
+ * owns the authoritative recognition.
8531
8576
  */
8532
8577
  tapOnset(instrument, options) {
8533
8578
  this.onsetTapManager.tap(instrument, options);
@@ -8611,6 +8656,7 @@ class AudioSystem {
8611
8656
  // do when audio is active.
8612
8657
  // ─────────────────────────────────────────────────────────────────────────
8613
8658
  startIdleTicker() {
8659
+ if (this.destroyed) return;
8614
8660
  if (this.idleTickerHandle !== null) return;
8615
8661
  if (typeof requestAnimationFrame === "undefined") return;
8616
8662
  this.idleTickerLastTime = performance.now();
@@ -8625,6 +8671,7 @@ class AudioSystem {
8625
8671
  }
8626
8672
  tickIdle(now) {
8627
8673
  this.idleTickerHandle = null;
8674
+ if (this.destroyed) return;
8628
8675
  if (this.mainChannel.audioState.isConnected) return;
8629
8676
  const dtMs = Math.min(100, now - this.idleTickerLastTime);
8630
8677
  this.idleTickerLastTime = now;
@@ -11139,13 +11186,17 @@ class VijiCore {
11139
11186
  * First tap switches the instrument from auto to tapping mode.
11140
11187
  * If a repeating pattern is recognized, it continues after tapping stops.
11141
11188
  *
11142
- * `options.skipRecognition: true` keeps the visual envelope and session
11143
- * timing but bypasses pattern recognition entirely (no IOI accumulation,
11144
- * no mode transition, no `applyPattern`, no `handlePatternTap`). Use
11145
- * this when relaying tap messages from another instance that owns the
11146
- * authoritative recognition (e.g. host receiving forwarded controller
11147
- * taps over WebRTC). The receiving core's mode is then driven only by
11148
- * `importSessionState` from the authoritative sender.
11189
+ * `options.skipRecognition: true` opts the tap out of the **recognition
11190
+ * pipeline only** no `tapIOIs` accumulation, no `tryRecognizePattern`,
11191
+ * no `applyPattern`, no `handlePatternTap`. Mode still flips
11192
+ * `'auto' 'tapping'` on the first tap of a session (and back to
11193
+ * `'auto'` on the 5s timeout). The mode flip is load-bearing for
11194
+ * `processFrame`'s drain of `pendingTapEvents`, the audio-event filter
11195
+ * that prevents music+tap doubling, and `onModeChange` consumers. Use
11196
+ * `skipRecognition` when relaying tap messages from another instance
11197
+ * that owns the authoritative recognition (e.g. host receiving
11198
+ * controller taps over WebRTC); the receiving core's pattern is then
11199
+ * driven only by `importSessionState` from the authoritative sender.
11149
11200
  *
11150
11201
  * The `MIN_TAP_INTERVAL_MS` debounce still applies regardless of the
11151
11202
  * `skipRecognition` flag.
@@ -11609,7 +11660,7 @@ class VijiCore {
11609
11660
  }
11610
11661
  this.deviceAudioStreamIndices.clear();
11611
11662
  if (this.audioSystem) {
11612
- this.audioSystem.resetAudioState();
11663
+ await this.audioSystem.resetAudioState();
11613
11664
  this.audioSystem = null;
11614
11665
  }
11615
11666
  this.currentAudioStream = null;
@@ -11702,7 +11753,7 @@ function validateCoreStatePayload(state) {
11702
11753
  }
11703
11754
  return null;
11704
11755
  }
11705
- const VERSION = "0.5.4";
11756
+ const VERSION = "0.5.6";
11706
11757
  export {
11707
11758
  AudioSystem as A,
11708
11759
  VERSION as V,
@@ -11710,4 +11761,4 @@ export {
11710
11761
  VijiCoreError as b,
11711
11762
  getDefaultExportFromCjs as g
11712
11763
  };
11713
- //# sourceMappingURL=index-_PbbZgmh.js.map
11764
+ //# sourceMappingURL=index-Yg6_UX8C.js.map