@viji-dev/core 0.5.2 → 0.5.4

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-DE6gem4m.js").then((n) => n.e);
1900
+ const wasmModule = await import("./essentia-wasm.web-aU6UPupF.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) {
@@ -6179,33 +6179,46 @@ class OnsetTapManager {
6179
6179
  modeChangeListeners = /* @__PURE__ */ new Set();
6180
6180
  sessionEndListeners = /* @__PURE__ */ new Set();
6181
6181
  muteChangeListeners = /* @__PURE__ */ new Set();
6182
- suppressEmissions = false;
6183
- tap(instrument) {
6182
+ /**
6183
+ * Record a tap on `instrument`. Drives the visual envelope, advances
6184
+ * `lastTapTime`, and (by default) feeds the pattern-recognition pipeline.
6185
+ *
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).
6193
+ */
6194
+ tap(instrument, options) {
6184
6195
  const s = this.state[instrument];
6185
6196
  const now = performance.now();
6197
+ const skipRecognition = options?.skipRecognition === true;
6186
6198
  if (s.muted) {
6187
6199
  s.muted = false;
6188
6200
  s.mutedAt = 0;
6189
- if (!this.suppressEmissions) {
6190
- this.fireMuteChange({ instrument, prevMuted: true, muted: false });
6191
- }
6201
+ this.fireMuteChange({ instrument, prevMuted: true, muted: false });
6192
6202
  }
6193
6203
  let ioi = -1;
6194
6204
  if (s.lastTapTime > 0) {
6195
6205
  ioi = now - s.lastTapTime;
6196
6206
  if (ioi < MIN_TAP_INTERVAL_MS) return;
6197
- if (ioi > MAX_TAP_INTERVAL_MS) {
6198
- s.tapIOIs = [];
6199
- ioi = -1;
6200
- } else {
6201
- s.tapIOIs.push(ioi);
6202
- if (s.tapIOIs.length > MAX_TAP_HISTORY) s.tapIOIs.shift();
6207
+ if (!skipRecognition) {
6208
+ if (ioi > MAX_TAP_INTERVAL_MS) {
6209
+ s.tapIOIs = [];
6210
+ ioi = -1;
6211
+ } else {
6212
+ s.tapIOIs.push(ioi);
6213
+ if (s.tapIOIs.length > MAX_TAP_HISTORY) s.tapIOIs.shift();
6214
+ }
6203
6215
  }
6204
6216
  }
6205
6217
  s.lastTapTime = now;
6206
6218
  s.pendingTapEvents.push(now);
6207
6219
  s.sessionActive = true;
6208
6220
  this.scheduleSessionTimers(instrument);
6221
+ if (skipRecognition) return;
6209
6222
  if (s.mode === "auto") {
6210
6223
  this.setMode(instrument, "tapping");
6211
6224
  if (ioi > 0) {
@@ -6241,7 +6254,7 @@ class OnsetTapManager {
6241
6254
  s.pendingTapEvents = [];
6242
6255
  s.envelope.reset();
6243
6256
  s.envelopeSmoothed.reset();
6244
- if (prevMuted && !this.suppressEmissions) {
6257
+ if (prevMuted) {
6245
6258
  this.fireMuteChange({ instrument, prevMuted: true, muted: false });
6246
6259
  }
6247
6260
  }
@@ -6273,9 +6286,7 @@ class OnsetTapManager {
6273
6286
  s.lastTapTime += pauseDuration;
6274
6287
  }
6275
6288
  }
6276
- if (!this.suppressEmissions) {
6277
- this.fireMuteChange({ instrument, prevMuted, muted: s.muted });
6278
- }
6289
+ this.fireMuteChange({ instrument, prevMuted, muted: s.muted });
6279
6290
  }
6280
6291
  isMuted(instrument) {
6281
6292
  return this.state[instrument].muted;
@@ -6307,16 +6318,27 @@ class OnsetTapManager {
6307
6318
  /**
6308
6319
  * Serialize per-instrument onset state for cross-instance transfer.
6309
6320
  * Wall-clock fields are in this instance's `performance.now()` clock space.
6321
+ *
6322
+ * `options.instruments` scopes the output to the listed instruments only;
6323
+ * omitted instruments are absent from the payload (the receiver's
6324
+ * `applyInstrumentPayload` skips missing keys, leaving the receiver's
6325
+ * existing state for those instruments untouched). Default = all three —
6326
+ * the full-state-transfer used for same-process scene-switch.
6327
+ *
6328
+ * Cross-device commits should typically scope to the just-completed
6329
+ * instrument so an unrelated instrument's state on the receiver isn't
6330
+ * inadvertently overwritten.
6310
6331
  */
6311
- exportSessionState() {
6332
+ exportSessionState(options) {
6333
+ const list = options?.instruments ?? INSTRUMENTS;
6334
+ const instruments = {};
6335
+ for (const inst of list) {
6336
+ instruments[inst] = this.serializeInstrument(inst);
6337
+ }
6312
6338
  return {
6313
6339
  version: STATE_SCHEMA_VERSION,
6314
6340
  senderTime: performance.now(),
6315
- instruments: {
6316
- kick: this.serializeInstrument("kick"),
6317
- snare: this.serializeInstrument("snare"),
6318
- hat: this.serializeInstrument("hat")
6319
- }
6341
+ instruments
6320
6342
  };
6321
6343
  }
6322
6344
  /**
@@ -6330,22 +6352,22 @@ class OnsetTapManager {
6330
6352
  * is older than one pattern period (phase-preserving — events still land on
6331
6353
  * the original beat positions modulo `patternSum`).
6332
6354
  *
6333
- * Mutation is synchronous; no events are emitted (state replacement is not
6334
- * a transition). Throws nothing malformed payloads should be filtered by
6335
- * the caller via the `version` field.
6355
+ * Mutation is synchronous. Field-level events (`onModeChange`,
6356
+ * `onMuteChange`) fire when an imported value differs from the current one
6357
+ * consumer-visible state is consistent with what polling would have
6358
+ * observed. Session-boundary events (`onSessionEnd`) do NOT fire on
6359
+ * import; imports are state replacement, not session boundaries.
6360
+ *
6361
+ * Throws nothing — malformed payloads should be filtered by the caller
6362
+ * via the `version` field.
6336
6363
  */
6337
6364
  importSessionState(state, clockOffset) {
6338
6365
  if (state.version !== STATE_SCHEMA_VERSION) return;
6339
- this.suppressEmissions = true;
6340
- try {
6341
- const now = performance.now();
6342
- for (const inst of INSTRUMENTS) {
6343
- const payload = state.instruments[inst];
6344
- if (!payload) continue;
6345
- this.applyInstrumentPayload(inst, payload, clockOffset, now);
6346
- }
6347
- } finally {
6348
- this.suppressEmissions = false;
6366
+ const now = performance.now();
6367
+ for (const inst of INSTRUMENTS) {
6368
+ const payload = state.instruments[inst];
6369
+ if (!payload) continue;
6370
+ this.applyInstrumentPayload(inst, payload, clockOffset, now);
6349
6371
  }
6350
6372
  }
6351
6373
  /**
@@ -6482,18 +6504,16 @@ class OnsetTapManager {
6482
6504
  // Private helpers
6483
6505
  // ---------------------------------------------------------------------------
6484
6506
  /**
6485
- * Single mutation point for `s.mode`. Fires `onModeChange` (when not
6486
- * suppressed) so listeners stay consistent regardless of which code path
6487
- * triggered the transition.
6507
+ * Single mutation point for `s.mode`. Fires `onModeChange` so listeners
6508
+ * stay consistent regardless of which code path triggered the transition
6509
+ * (including imports, which fire via `applyInstrumentPayload` directly).
6488
6510
  */
6489
6511
  setMode(instrument, newMode) {
6490
6512
  const s = this.state[instrument];
6491
6513
  const prevMode = s.mode;
6492
6514
  if (prevMode === newMode) return;
6493
6515
  s.mode = newMode;
6494
- if (!this.suppressEmissions) {
6495
- this.fireModeChange({ instrument, prevMode, newMode });
6496
- }
6516
+ this.fireModeChange({ instrument, prevMode, newMode });
6497
6517
  }
6498
6518
  fireModeChange(ev) {
6499
6519
  for (const listener of this.modeChangeListeners) {
@@ -6612,6 +6632,8 @@ class OnsetTapManager {
6612
6632
  }
6613
6633
  applyInstrumentPayload(instrument, payload, clockOffset, now) {
6614
6634
  const s = this.state[instrument];
6635
+ const prevMode = s.mode;
6636
+ const prevMuted = s.muted;
6615
6637
  this.cancelSessionTimers(s);
6616
6638
  s.sessionActive = false;
6617
6639
  const translatedReplayLast = payload.replayLastEventTime !== null ? payload.replayLastEventTime + clockOffset : 0;
@@ -6628,7 +6650,6 @@ class OnsetTapManager {
6628
6650
  }
6629
6651
  }
6630
6652
  }
6631
- s.mode;
6632
6653
  s.mode = payload.mode;
6633
6654
  s.muted = payload.muted;
6634
6655
  s.mutedAt = translatedMutedAt;
@@ -6642,6 +6663,12 @@ class OnsetTapManager {
6642
6663
  s.pendingTapEvents = [];
6643
6664
  s.envelope.reset();
6644
6665
  s.envelopeSmoothed.reset();
6666
+ if (s.mode !== prevMode) {
6667
+ this.fireModeChange({ instrument, prevMode, newMode: s.mode });
6668
+ }
6669
+ if (s.muted !== prevMuted) {
6670
+ this.fireMuteChange({ instrument, prevMuted, muted: s.muted });
6671
+ }
6645
6672
  }
6646
6673
  /**
6647
6674
  * Handle a tap that arrives while already in pattern mode.
@@ -8497,10 +8524,13 @@ class AudioSystem {
8497
8524
  // Public API Methods for Audio Analysis Configuration
8498
8525
  // ═══════════════════════════════════════════════════════════
8499
8526
  /**
8500
- * Record a tap for the specified instrument onset
8527
+ * 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.
8501
8531
  */
8502
- tapOnset(instrument) {
8503
- this.onsetTapManager.tap(instrument);
8532
+ tapOnset(instrument, options) {
8533
+ this.onsetTapManager.tap(instrument, options);
8504
8534
  }
8505
8535
  /**
8506
8536
  * Clear tap pattern for an instrument, restoring auto-detection
@@ -8550,8 +8580,8 @@ class AudioSystem {
8550
8580
  // audio block is for same-process scene-switch transfer (sender's audio
8551
8581
  // analysis state would corrupt receiver's tracking on a different source).
8552
8582
  // ─────────────────────────────────────────────────────────────────────────
8553
- exportOnsetSessionState() {
8554
- return this.onsetTapManager.exportSessionState();
8583
+ exportOnsetSessionState(options) {
8584
+ return this.onsetTapManager.exportSessionState(options);
8555
8585
  }
8556
8586
  importOnsetSessionState(state, clockOffset) {
8557
8587
  this.onsetTapManager.importSessionState(state, clockOffset);
@@ -10671,8 +10701,12 @@ class VijiCore {
10671
10701
  /**
10672
10702
  * Replace the audio analysis + onset state from a serialized payload.
10673
10703
  * `clockOffset` is added to all sender-clocked fields (`0` for same-process,
10674
- * NTP-derived for cross-device). Mutation is synchronous; no `onModeChange`
10675
- * or `onSessionEnd` events are emitted (state replacement is not a transition).
10704
+ * NTP-derived for cross-device). Mutation is synchronous.
10705
+ *
10706
+ * Field-level events (`onModeChange`, `onMuteChange`) fire when imported
10707
+ * values differ from current — consumer-visible state stays consistent
10708
+ * with what polling would have observed. Session-boundary events
10709
+ * (`onSessionEnd`) do NOT fire on import.
10676
10710
  *
10677
10711
  * Validates `version`; on mismatch, fires `onStateImportError` and leaves
10678
10712
  * existing state intact.
@@ -11104,10 +11138,21 @@ class VijiCore {
11104
11138
  * Tap an onset for a specific instrument.
11105
11139
  * First tap switches the instrument from auto to tapping mode.
11106
11140
  * If a repeating pattern is recognized, it continues after tapping stops.
11141
+ *
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.
11149
+ *
11150
+ * The `MIN_TAP_INTERVAL_MS` debounce still applies regardless of the
11151
+ * `skipRecognition` flag.
11107
11152
  */
11108
- tap: (instrument) => {
11153
+ tap: (instrument, options) => {
11109
11154
  this.validateReady();
11110
- this.audioSystem?.tapOnset(instrument);
11155
+ this.audioSystem?.tapOnset(instrument, options);
11111
11156
  },
11112
11157
  /**
11113
11158
  * Clear the tap pattern for an instrument, restoring auto-detection
@@ -11148,9 +11193,13 @@ class VijiCore {
11148
11193
  },
11149
11194
  /**
11150
11195
  * Listen for instrument mode transitions (`'auto' | 'tapping' | 'pattern'`).
11151
- * Fires on every transition including the first tap (`'auto' → 'tapping'`)
11152
- * and pattern recognition (`'tapping' → 'pattern'`). Imported state does
11153
- * NOT fire mode-change events state replacement is not a transition.
11196
+ * Fires whenever the underlying mode field actually changes, regardless
11197
+ * of which code path triggered it — first tap (`'auto' → 'tapping'`),
11198
+ * pattern recognition (`'tapping' 'pattern'`), the 5s tap timeout
11199
+ * (`'tapping' → 'auto'`), explicit `clear()`, or `importSessionState`
11200
+ * landing a different mode value.
11201
+ *
11202
+ * Idempotent transitions (already in target mode) do not fire.
11154
11203
  *
11155
11204
  * @returns Unsubscribe function. Call to remove the listener.
11156
11205
  */
@@ -11160,14 +11209,20 @@ class VijiCore {
11160
11209
  });
11161
11210
  },
11162
11211
  /**
11163
- * Listen for natural session-end events. Fires when:
11212
+ * Listen for natural session-end events. Fires only on natural session
11213
+ * boundaries:
11164
11214
  * - 500ms elapse since last tap with instrument in `'pattern'` mode
11165
11215
  * (outcome `'pattern'`), or
11166
11216
  * - 5s elapse in `'tapping'` mode without a recognized pattern
11167
11217
  * (outcome `'cleared'`; instrument transitions to `'auto'`).
11168
11218
  *
11169
- * Explicit `clear()` calls do NOT fire this event (caller-initiated;
11170
- * caller already knows). Imported state does NOT fire either.
11219
+ * Does NOT fire on:
11220
+ * - Explicit `clear()` calls (caller-initiated; not a natural boundary).
11221
+ * - `importSessionState` (state replacement; not a session boundary).
11222
+ *
11223
+ * Field-change events (`onModeChange`, `onMuteChange`) fire on imports
11224
+ * when fields differ; this event does not because session-end is a
11225
+ * different category — about session lifecycle, not field state.
11171
11226
  *
11172
11227
  * @returns Unsubscribe function. Call to remove the listener.
11173
11228
  */
@@ -11178,14 +11233,15 @@ class VijiCore {
11178
11233
  },
11179
11234
  /**
11180
11235
  * Listen for instrument mute-state transitions (`true ↔ false`). Fires
11181
- * whenever the underlying mute state actually changes, regardless of
11182
- * which method triggered it:
11183
- * - `setMuted(instrument, muted)` when `prev !== next`
11184
- * - `tap(instrument)` auto-unmute on first tap of a muted instrument
11185
- * - `clear(instrument)` when the instrument was muted
11236
+ * whenever the underlying mute field actually changes, regardless of
11237
+ * which code path triggered it:
11238
+ * - `setMuted(instrument, muted)` when `prev !== next`.
11239
+ * - `tap(instrument)` auto-unmute on first tap of a muted instrument.
11240
+ * - `clear(instrument)` when the instrument was muted at call time.
11241
+ * - `importSessionState` landing a different muted value.
11186
11242
  *
11187
11243
  * Idempotent calls (e.g. `setMuted(true)` on an already-muted instrument)
11188
- * do not fire. Imported state does NOT fire either.
11244
+ * do not fire.
11189
11245
  *
11190
11246
  * @returns Unsubscribe function. Call to remove the listener.
11191
11247
  */
@@ -11199,10 +11255,22 @@ class VijiCore {
11199
11255
  * Cross-device-safe (no audio analysis state included). Pair with
11200
11256
  * `importSessionState` on a receiver. Wall-clock fields are in this
11201
11257
  * instance's `performance.now()` clock space.
11258
+ *
11259
+ * `options.instruments` scopes the snapshot to the listed instruments
11260
+ * only; omitted instruments are absent from the payload. The receiver
11261
+ * leaves its existing state for those instruments untouched (the
11262
+ * receiver's `applyInstrumentPayload` skips missing keys). Default =
11263
+ * all three.
11264
+ *
11265
+ * Cross-device commits should typically scope to the just-completed
11266
+ * instrument (e.g. `exportSessionState({ instruments: [ev.instrument] })`
11267
+ * inside an `onSessionEnd` handler) so the receiver's unrelated
11268
+ * instrument state isn't inadvertently overwritten by the sender's
11269
+ * default values.
11202
11270
  */
11203
- exportSessionState: () => {
11271
+ exportSessionState: (options) => {
11204
11272
  this.validateReady();
11205
- return this.audioSystem?.exportOnsetSessionState() ?? {
11273
+ return this.audioSystem?.exportOnsetSessionState(options) ?? {
11206
11274
  version: 1,
11207
11275
  senderTime: performance.now(),
11208
11276
  instruments: {}
@@ -11212,7 +11280,12 @@ class VijiCore {
11212
11280
  * Replace per-instrument onset state from a serialized payload.
11213
11281
  * `clockOffset` is added to all sender-clocked fields during import
11214
11282
  * (use `0` for same-process transfer; NTP-derived offset for cross-device).
11215
- * Mutation is synchronous; no events are emitted.
11283
+ * Mutation is synchronous.
11284
+ *
11285
+ * Field-level events fire when imported values differ from current:
11286
+ * `onModeChange` if the imported mode is different, `onMuteChange` if
11287
+ * the imported muted state is different. Session-boundary events
11288
+ * (`onSessionEnd`) do NOT fire — imports aren't session boundaries.
11216
11289
  *
11217
11290
  * Patterns are rebased forward by whole pattern cycles to eliminate
11218
11291
  * the catch-up burst that would otherwise occur on stale payloads
@@ -11629,7 +11702,7 @@ function validateCoreStatePayload(state) {
11629
11702
  }
11630
11703
  return null;
11631
11704
  }
11632
- const VERSION = "0.5.2";
11705
+ const VERSION = "0.5.4";
11633
11706
  export {
11634
11707
  AudioSystem as A,
11635
11708
  VERSION as V,
@@ -11637,4 +11710,4 @@ export {
11637
11710
  VijiCoreError as b,
11638
11711
  getDefaultExportFromCjs as g
11639
11712
  };
11640
- //# sourceMappingURL=index-B8LJ9m47.js.map
11713
+ //# sourceMappingURL=index-_PbbZgmh.js.map