@viji-dev/core 0.5.1 → 0.5.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.
@@ -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-x6zu4Vib.js").then((n) => n.e);
1900
+ const wasmModule = await import("./essentia-wasm.web-CPrFAj59.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) {
@@ -6178,13 +6178,14 @@ class OnsetTapManager {
6178
6178
  };
6179
6179
  modeChangeListeners = /* @__PURE__ */ new Set();
6180
6180
  sessionEndListeners = /* @__PURE__ */ new Set();
6181
- suppressEmissions = false;
6181
+ muteChangeListeners = /* @__PURE__ */ new Set();
6182
6182
  tap(instrument) {
6183
6183
  const s = this.state[instrument];
6184
6184
  const now = performance.now();
6185
6185
  if (s.muted) {
6186
6186
  s.muted = false;
6187
6187
  s.mutedAt = 0;
6188
+ this.fireMuteChange({ instrument, prevMuted: true, muted: false });
6188
6189
  }
6189
6190
  let ioi = -1;
6190
6191
  if (s.lastTapTime > 0) {
@@ -6221,6 +6222,7 @@ class OnsetTapManager {
6221
6222
  }
6222
6223
  clear(instrument) {
6223
6224
  const s = this.state[instrument];
6225
+ const prevMuted = s.muted;
6224
6226
  this.cancelSessionTimers(s);
6225
6227
  s.sessionActive = false;
6226
6228
  this.setMode(instrument, "auto");
@@ -6236,6 +6238,9 @@ class OnsetTapManager {
6236
6238
  s.pendingTapEvents = [];
6237
6239
  s.envelope.reset();
6238
6240
  s.envelopeSmoothed.reset();
6241
+ if (prevMuted) {
6242
+ this.fireMuteChange({ instrument, prevMuted: true, muted: false });
6243
+ }
6239
6244
  }
6240
6245
  getMode(instrument) {
6241
6246
  return this.state[instrument].mode;
@@ -6252,6 +6257,7 @@ class OnsetTapManager {
6252
6257
  setMuted(instrument, muted) {
6253
6258
  const s = this.state[instrument];
6254
6259
  if (s.muted === muted) return;
6260
+ const prevMuted = s.muted;
6255
6261
  const now = performance.now();
6256
6262
  if (muted) {
6257
6263
  s.muted = true;
@@ -6264,6 +6270,7 @@ class OnsetTapManager {
6264
6270
  s.lastTapTime += pauseDuration;
6265
6271
  }
6266
6272
  }
6273
+ this.fireMuteChange({ instrument, prevMuted, muted: s.muted });
6267
6274
  }
6268
6275
  isMuted(instrument) {
6269
6276
  return this.state[instrument].muted;
@@ -6283,6 +6290,12 @@ class OnsetTapManager {
6283
6290
  this.sessionEndListeners.delete(listener);
6284
6291
  };
6285
6292
  }
6293
+ onMuteChange(listener) {
6294
+ this.muteChangeListeners.add(listener);
6295
+ return () => {
6296
+ this.muteChangeListeners.delete(listener);
6297
+ };
6298
+ }
6286
6299
  // ─────────────────────────────────────────────────────────────────────────
6287
6300
  // State serialization
6288
6301
  // ─────────────────────────────────────────────────────────────────────────
@@ -6312,22 +6325,22 @@ class OnsetTapManager {
6312
6325
  * is older than one pattern period (phase-preserving — events still land on
6313
6326
  * the original beat positions modulo `patternSum`).
6314
6327
  *
6315
- * Mutation is synchronous; no events are emitted (state replacement is not
6316
- * a transition). Throws nothing malformed payloads should be filtered by
6317
- * the caller via the `version` field.
6328
+ * Mutation is synchronous. Field-level events (`onModeChange`,
6329
+ * `onMuteChange`) fire when an imported value differs from the current one
6330
+ * consumer-visible state is consistent with what polling would have
6331
+ * observed. Session-boundary events (`onSessionEnd`) do NOT fire on
6332
+ * import; imports are state replacement, not session boundaries.
6333
+ *
6334
+ * Throws nothing — malformed payloads should be filtered by the caller
6335
+ * via the `version` field.
6318
6336
  */
6319
6337
  importSessionState(state, clockOffset) {
6320
6338
  if (state.version !== STATE_SCHEMA_VERSION) return;
6321
- this.suppressEmissions = true;
6322
- try {
6323
- const now = performance.now();
6324
- for (const inst of INSTRUMENTS) {
6325
- const payload = state.instruments[inst];
6326
- if (!payload) continue;
6327
- this.applyInstrumentPayload(inst, payload, clockOffset, now);
6328
- }
6329
- } finally {
6330
- this.suppressEmissions = false;
6339
+ const now = performance.now();
6340
+ for (const inst of INSTRUMENTS) {
6341
+ const payload = state.instruments[inst];
6342
+ if (!payload) continue;
6343
+ this.applyInstrumentPayload(inst, payload, clockOffset, now);
6331
6344
  }
6332
6345
  }
6333
6346
  /**
@@ -6464,18 +6477,16 @@ class OnsetTapManager {
6464
6477
  // Private helpers
6465
6478
  // ---------------------------------------------------------------------------
6466
6479
  /**
6467
- * Single mutation point for `s.mode`. Fires `onModeChange` (when not
6468
- * suppressed) so listeners stay consistent regardless of which code path
6469
- * triggered the transition.
6480
+ * Single mutation point for `s.mode`. Fires `onModeChange` so listeners
6481
+ * stay consistent regardless of which code path triggered the transition
6482
+ * (including imports, which fire via `applyInstrumentPayload` directly).
6470
6483
  */
6471
6484
  setMode(instrument, newMode) {
6472
6485
  const s = this.state[instrument];
6473
6486
  const prevMode = s.mode;
6474
6487
  if (prevMode === newMode) return;
6475
6488
  s.mode = newMode;
6476
- if (!this.suppressEmissions) {
6477
- this.fireModeChange({ instrument, prevMode, newMode });
6478
- }
6489
+ this.fireModeChange({ instrument, prevMode, newMode });
6479
6490
  }
6480
6491
  fireModeChange(ev) {
6481
6492
  for (const listener of this.modeChangeListeners) {
@@ -6495,6 +6506,15 @@ class OnsetTapManager {
6495
6506
  }
6496
6507
  }
6497
6508
  }
6509
+ fireMuteChange(ev) {
6510
+ for (const listener of this.muteChangeListeners) {
6511
+ try {
6512
+ listener(ev);
6513
+ } catch (err) {
6514
+ console.error("Error in onMuteChange listener:", err);
6515
+ }
6516
+ }
6517
+ }
6498
6518
  /**
6499
6519
  * Schedule (or reschedule) the per-instrument session timers on every tap.
6500
6520
  * 500ms timer fires `'pattern'` outcome if instrument is in pattern mode at
@@ -6585,6 +6605,8 @@ class OnsetTapManager {
6585
6605
  }
6586
6606
  applyInstrumentPayload(instrument, payload, clockOffset, now) {
6587
6607
  const s = this.state[instrument];
6608
+ const prevMode = s.mode;
6609
+ const prevMuted = s.muted;
6588
6610
  this.cancelSessionTimers(s);
6589
6611
  s.sessionActive = false;
6590
6612
  const translatedReplayLast = payload.replayLastEventTime !== null ? payload.replayLastEventTime + clockOffset : 0;
@@ -6601,7 +6623,6 @@ class OnsetTapManager {
6601
6623
  }
6602
6624
  }
6603
6625
  }
6604
- s.mode;
6605
6626
  s.mode = payload.mode;
6606
6627
  s.muted = payload.muted;
6607
6628
  s.mutedAt = translatedMutedAt;
@@ -6615,6 +6636,12 @@ class OnsetTapManager {
6615
6636
  s.pendingTapEvents = [];
6616
6637
  s.envelope.reset();
6617
6638
  s.envelopeSmoothed.reset();
6639
+ if (s.mode !== prevMode) {
6640
+ this.fireModeChange({ instrument, prevMode, newMode: s.mode });
6641
+ }
6642
+ if (s.muted !== prevMuted) {
6643
+ this.fireMuteChange({ instrument, prevMuted, muted: s.muted });
6644
+ }
6618
6645
  }
6619
6646
  /**
6620
6647
  * Handle a tap that arrives while already in pattern mode.
@@ -8515,6 +8542,9 @@ class AudioSystem {
8515
8542
  onOnsetSessionEnd(listener) {
8516
8543
  return this.onsetTapManager.onSessionEnd(listener);
8517
8544
  }
8545
+ onOnsetMuteChange(listener) {
8546
+ return this.onsetTapManager.onMuteChange(listener);
8547
+ }
8518
8548
  // ─────────────────────────────────────────────────────────────────────────
8519
8549
  // State serialization. Onset-only export is cross-device-safe; the full
8520
8550
  // audio block is for same-process scene-switch transfer (sender's audio
@@ -10641,8 +10671,12 @@ class VijiCore {
10641
10671
  /**
10642
10672
  * Replace the audio analysis + onset state from a serialized payload.
10643
10673
  * `clockOffset` is added to all sender-clocked fields (`0` for same-process,
10644
- * NTP-derived for cross-device). Mutation is synchronous; no `onModeChange`
10645
- * or `onSessionEnd` events are emitted (state replacement is not a transition).
10674
+ * NTP-derived for cross-device). Mutation is synchronous.
10675
+ *
10676
+ * Field-level events (`onModeChange`, `onMuteChange`) fire when imported
10677
+ * values differ from current — consumer-visible state stays consistent
10678
+ * with what polling would have observed. Session-boundary events
10679
+ * (`onSessionEnd`) do NOT fire on import.
10646
10680
  *
10647
10681
  * Validates `version`; on mismatch, fires `onStateImportError` and leaves
10648
10682
  * existing state intact.
@@ -11118,9 +11152,13 @@ class VijiCore {
11118
11152
  },
11119
11153
  /**
11120
11154
  * Listen for instrument mode transitions (`'auto' | 'tapping' | 'pattern'`).
11121
- * Fires on every transition including the first tap (`'auto' → 'tapping'`)
11122
- * and pattern recognition (`'tapping' → 'pattern'`). Imported state does
11123
- * NOT fire mode-change events state replacement is not a transition.
11155
+ * Fires whenever the underlying mode field actually changes, regardless
11156
+ * of which code path triggered it — first tap (`'auto' → 'tapping'`),
11157
+ * pattern recognition (`'tapping' 'pattern'`), the 5s tap timeout
11158
+ * (`'tapping' → 'auto'`), explicit `clear()`, or `importSessionState`
11159
+ * landing a different mode value.
11160
+ *
11161
+ * Idempotent transitions (already in target mode) do not fire.
11124
11162
  *
11125
11163
  * @returns Unsubscribe function. Call to remove the listener.
11126
11164
  */
@@ -11130,14 +11168,20 @@ class VijiCore {
11130
11168
  });
11131
11169
  },
11132
11170
  /**
11133
- * Listen for natural session-end events. Fires when:
11171
+ * Listen for natural session-end events. Fires only on natural session
11172
+ * boundaries:
11134
11173
  * - 500ms elapse since last tap with instrument in `'pattern'` mode
11135
11174
  * (outcome `'pattern'`), or
11136
11175
  * - 5s elapse in `'tapping'` mode without a recognized pattern
11137
11176
  * (outcome `'cleared'`; instrument transitions to `'auto'`).
11138
11177
  *
11139
- * Explicit `clear()` calls do NOT fire this event (caller-initiated;
11140
- * caller already knows). Imported state does NOT fire either.
11178
+ * Does NOT fire on:
11179
+ * - Explicit `clear()` calls (caller-initiated; not a natural boundary).
11180
+ * - `importSessionState` (state replacement; not a session boundary).
11181
+ *
11182
+ * Field-change events (`onModeChange`, `onMuteChange`) fire on imports
11183
+ * when fields differ; this event does not because session-end is a
11184
+ * different category — about session lifecycle, not field state.
11141
11185
  *
11142
11186
  * @returns Unsubscribe function. Call to remove the listener.
11143
11187
  */
@@ -11146,6 +11190,25 @@ class VijiCore {
11146
11190
  return this.audioSystem?.onOnsetSessionEnd(listener) ?? (() => {
11147
11191
  });
11148
11192
  },
11193
+ /**
11194
+ * Listen for instrument mute-state transitions (`true ↔ false`). Fires
11195
+ * whenever the underlying mute field actually changes, regardless of
11196
+ * which code path triggered it:
11197
+ * - `setMuted(instrument, muted)` when `prev !== next`.
11198
+ * - `tap(instrument)` auto-unmute on first tap of a muted instrument.
11199
+ * - `clear(instrument)` when the instrument was muted at call time.
11200
+ * - `importSessionState` landing a different muted value.
11201
+ *
11202
+ * Idempotent calls (e.g. `setMuted(true)` on an already-muted instrument)
11203
+ * do not fire.
11204
+ *
11205
+ * @returns Unsubscribe function. Call to remove the listener.
11206
+ */
11207
+ onMuteChange: (listener) => {
11208
+ this.validateReady();
11209
+ return this.audioSystem?.onOnsetMuteChange(listener) ?? (() => {
11210
+ });
11211
+ },
11149
11212
  /**
11150
11213
  * Snapshot per-instrument onset state for cross-instance transfer.
11151
11214
  * Cross-device-safe (no audio analysis state included). Pair with
@@ -11164,7 +11227,12 @@ class VijiCore {
11164
11227
  * Replace per-instrument onset state from a serialized payload.
11165
11228
  * `clockOffset` is added to all sender-clocked fields during import
11166
11229
  * (use `0` for same-process transfer; NTP-derived offset for cross-device).
11167
- * Mutation is synchronous; no events are emitted.
11230
+ * Mutation is synchronous.
11231
+ *
11232
+ * Field-level events fire when imported values differ from current:
11233
+ * `onModeChange` if the imported mode is different, `onMuteChange` if
11234
+ * the imported muted state is different. Session-boundary events
11235
+ * (`onSessionEnd`) do NOT fire — imports aren't session boundaries.
11168
11236
  *
11169
11237
  * Patterns are rebased forward by whole pattern cycles to eliminate
11170
11238
  * the catch-up burst that would otherwise occur on stale payloads
@@ -11581,7 +11649,7 @@ function validateCoreStatePayload(state) {
11581
11649
  }
11582
11650
  return null;
11583
11651
  }
11584
- const VERSION = "0.5.1";
11652
+ const VERSION = "0.5.3";
11585
11653
  export {
11586
11654
  AudioSystem as A,
11587
11655
  VERSION as V,
@@ -11589,4 +11657,4 @@ export {
11589
11657
  VijiCoreError as b,
11590
11658
  getDefaultExportFromCjs as g
11591
11659
  };
11592
- //# sourceMappingURL=index-Cqh1k_49.js.map
11660
+ //# sourceMappingURL=index-Bhq4eJe_.js.map