@viji-dev/core 0.5.0 → 0.5.2

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/index.d.ts CHANGED
@@ -235,6 +235,8 @@ export declare class AudioSystem {
235
235
  private beatDetectionEnabled;
236
236
  private onsetDetectionEnabled;
237
237
  private autoGainEnabled;
238
+ private idleTickerHandle;
239
+ private idleTickerLastTime;
238
240
  /**
239
241
  * Enable or disable comprehensive debug logging for all layers
240
242
  * Enables enhanced logging in: MultiOnsetDetection, BeatStateManager
@@ -349,6 +351,12 @@ export declare class AudioSystem {
349
351
  /**
350
352
  * Connect a channel to Web Audio nodes (source, worklet/analyser).
351
353
  * Used for both main and additional channels.
354
+ *
355
+ * Lifecycle ordering invariant: when wiring the main channel, the idle
356
+ * ticker MUST be stopped before the first `await` so that an in-flight
357
+ * tick cannot race the audio path coming online. The ticker callback
358
+ * additionally guards on `mainChannel.audioState.isConnected` for
359
+ * belt-and-braces.
352
360
  */
353
361
  private connectChannel;
354
362
  /**
@@ -357,6 +365,11 @@ export declare class AudioSystem {
357
365
  private setAudioStream;
358
366
  /**
359
367
  * Disconnect the main audio stream (does NOT close AudioContext -- additional channels may still be active).
368
+ *
369
+ * Lifecycle ordering invariant: tear down the audio nodes first, clear any
370
+ * stale frequency / waveform buffers, then restart the idle ticker. The
371
+ * ticker callback's defensive `isConnected` guard prevents any stale tick
372
+ * from observing a half-disconnected state.
360
373
  */
361
374
  private disconnectMainStream;
362
375
  /**
@@ -451,6 +464,17 @@ export declare class AudioSystem {
451
464
  * Check if an instrument onset is muted
452
465
  */
453
466
  isOnsetMuted(instrument: InstrumentType): boolean;
467
+ onOnsetModeChange(listener: (ev: OnsetModeChangeEvent) => void): Unsubscribe;
468
+ onOnsetSessionEnd(listener: (ev: OnsetSessionEndEvent) => void): Unsubscribe;
469
+ onOnsetMuteChange(listener: (ev: OnsetMuteChangeEvent) => void): Unsubscribe;
470
+ exportOnsetSessionState(): SerializedOnsetState;
471
+ importOnsetSessionState(state: SerializedOnsetState, clockOffset: number): void;
472
+ exportAudioAnalysisState(): SerializedAudioAnalysisState;
473
+ importAudioAnalysisState(state: SerializedAudioAnalysisState, clockOffset: number): void;
474
+ private startIdleTicker;
475
+ private stopIdleTicker;
476
+ private tickIdle;
477
+ private makeEmptyBeatState;
454
478
  /**
455
479
  * Get current BPM (manual or auto-detected)
456
480
  */
@@ -1256,6 +1280,22 @@ declare interface ImageParameter {
1256
1280
  category: ParameterCategory;
1257
1281
  }
1258
1282
 
1283
+ export declare interface InstrumentSerialized {
1284
+ mode: OnsetMode;
1285
+ muted: boolean;
1286
+ pattern: number[] | null;
1287
+ /** Sender wall-clock; null if never set. */
1288
+ replayLastEventTime: number | null;
1289
+ replayIndex: number;
1290
+ tapIOIs: number[];
1291
+ /** Sender wall-clock; null if never set. */
1292
+ lastTapTime: number | null;
1293
+ /** Sender wall-clock; null if not muted. */
1294
+ mutedAt: number | null;
1295
+ refinementIndex: number;
1296
+ refinementCounts: number[];
1297
+ }
1298
+
1259
1299
  declare type InstrumentType = 'kick' | 'snare' | 'hat';
1260
1300
 
1261
1301
  /**
@@ -1404,10 +1444,35 @@ declare interface NumberParameter {
1404
1444
  category: ParameterCategory;
1405
1445
  }
1406
1446
 
1447
+ export declare type OnsetInstrument = 'kick' | 'snare' | 'hat';
1448
+
1407
1449
  declare type OnsetMode = 'auto' | 'tapping' | 'pattern';
1408
1450
 
1409
1451
  declare type OnsetMode_2 = 'auto' | 'tapping' | 'pattern';
1410
1452
 
1453
+ export declare interface OnsetModeChangeEvent {
1454
+ instrument: OnsetInstrument;
1455
+ prevMode: OnsetMode;
1456
+ newMode: OnsetMode;
1457
+ }
1458
+
1459
+ export declare interface OnsetMuteChangeEvent {
1460
+ instrument: OnsetInstrument;
1461
+ prevMuted: boolean;
1462
+ muted: boolean;
1463
+ }
1464
+
1465
+ export declare interface OnsetSessionEndEvent {
1466
+ instrument: OnsetInstrument;
1467
+ /**
1468
+ * `'pattern'` — 500ms idle since last tap with instrument in `'pattern'` mode.
1469
+ * `'cleared'` — 5s idle in `'tapping'` mode without a recognized pattern;
1470
+ * instrument transitions to `'auto'`. Explicit `clear()` calls do NOT fire
1471
+ * this event (caller-initiated; caller already knows).
1472
+ */
1473
+ outcome: 'pattern' | 'cleared';
1474
+ }
1475
+
1411
1476
  export declare interface ParameterAPI {
1412
1477
  define(groups: ParameterGroup[]): void;
1413
1478
  [key: string]: any;
@@ -1660,6 +1725,308 @@ declare interface SelectParameter {
1660
1725
  category: ParameterCategory;
1661
1726
  }
1662
1727
 
1728
+ export declare interface SerializedAudioAnalysisState {
1729
+ /** Per-instrument onset state (mirrors SerializedOnsetState.instruments shape). */
1730
+ onset: SerializedOnsetState['instruments'];
1731
+ bpmTracker: SerializedTempoInductionState;
1732
+ pll: SerializedPLLState;
1733
+ beatState: SerializedBeatStateManagerState;
1734
+ }
1735
+
1736
+ /**
1737
+ * BeatStateManager continuity-relevant state. Treat as opaque on the consumer
1738
+ * side. Envelope follower state is omitted by design — receiver re-triggers
1739
+ * envelopes naturally as detected events flow.
1740
+ */
1741
+ export declare interface SerializedBeatStateManagerState {
1742
+ state: 'TRACKING' | 'LOCKED' | 'BREAKDOWN' | 'LOST';
1743
+ /** Sender wall-clock; 0 if never set. */
1744
+ stateEnteredTime: number;
1745
+ kickProfile: {
1746
+ avgEnergy: number;
1747
+ bassRatio: number;
1748
+ midRatio: number;
1749
+ trebleRatio: number;
1750
+ sampleCount: number;
1751
+ };
1752
+ snareProfile: {
1753
+ avgEnergy: number;
1754
+ bassRatio: number;
1755
+ midRatio: number;
1756
+ trebleRatio: number;
1757
+ sampleCount: number;
1758
+ };
1759
+ hatProfile: {
1760
+ avgEnergy: number;
1761
+ bassRatio: number;
1762
+ midRatio: number;
1763
+ trebleRatio: number;
1764
+ sampleCount: number;
1765
+ };
1766
+ recentOnsetStrengths: number[];
1767
+ averageOnsetStrength: number;
1768
+ tempoMethodAgreement: number;
1769
+ gridScore: number;
1770
+ consistencyScore: number;
1771
+ anchorClarity: number;
1772
+ recentGridScores: number[];
1773
+ lockedBPM: number;
1774
+ /** Sender wall-clock; 0 if never set. */
1775
+ lastOnsetTime: number;
1776
+ /** Sender wall-clock; 0 if never set. */
1777
+ lastKickTime: number;
1778
+ kickIntervals: number[];
1779
+ bpmHistory: number[];
1780
+ /** Sender wall-clocks in nested `time` fields. */
1781
+ adaptiveProfiles: {
1782
+ kick: {
1783
+ samples: Array<{
1784
+ midRatio: number;
1785
+ midToBass: number;
1786
+ time: number;
1787
+ }>;
1788
+ };
1789
+ snareIndependent: {
1790
+ samples: Array<{
1791
+ strength: number;
1792
+ midRatio: number;
1793
+ trebleRatio: number;
1794
+ sharpness: number;
1795
+ time: number;
1796
+ }>;
1797
+ };
1798
+ snareLayered: {
1799
+ samples: Array<{
1800
+ strength: number;
1801
+ midRatio: number;
1802
+ energy: number;
1803
+ sharpness: number;
1804
+ time: number;
1805
+ }>;
1806
+ };
1807
+ hatIndependent: {
1808
+ samples: Array<{
1809
+ strength: number;
1810
+ trebleRatio: number;
1811
+ energy: number;
1812
+ sharpness: number;
1813
+ time: number;
1814
+ }>;
1815
+ };
1816
+ hatLayered: {
1817
+ samples: Array<{
1818
+ strength: number;
1819
+ trebleRatio: number;
1820
+ energy: number;
1821
+ sharpness: number;
1822
+ time: number;
1823
+ }>;
1824
+ };
1825
+ };
1826
+ adaptiveThresholds: {
1827
+ snareMinMidRatio: number;
1828
+ snareMinMidToBass: number;
1829
+ kickMaxMidRatio: number;
1830
+ snareIndependent: {
1831
+ minStrength: number;
1832
+ minMidRatio: number;
1833
+ minTrebleRatio: number;
1834
+ minSharpness: number;
1835
+ };
1836
+ snareLayered: {
1837
+ minStrength: number;
1838
+ minEnergy: number;
1839
+ minSharpness: number;
1840
+ };
1841
+ hatIndependent: {
1842
+ minStrength: number;
1843
+ minTrebleRatio: number;
1844
+ minSharpness: number;
1845
+ };
1846
+ hatLayered: {
1847
+ minStrength: number;
1848
+ minTrebleRatio: number;
1849
+ minSharpness: number;
1850
+ };
1851
+ };
1852
+ /** Sender wall-clocks in nested `lastOnsetTime`. */
1853
+ ioiTrackers: {
1854
+ kick: {
1855
+ intervals: number[];
1856
+ lastOnsetTime: number;
1857
+ };
1858
+ snare: {
1859
+ intervals: number[];
1860
+ lastOnsetTime: number;
1861
+ };
1862
+ hat: {
1863
+ intervals: number[];
1864
+ lastOnsetTime: number;
1865
+ };
1866
+ };
1867
+ /** Each event has a `time` field (sender wall-clock); `expiresAt` is also wall-clock. */
1868
+ eventBuffer: Array<{
1869
+ event: {
1870
+ type: 'kick' | 'snare' | 'hat';
1871
+ time: number;
1872
+ strength: number;
1873
+ isPredicted?: boolean;
1874
+ bpm?: number;
1875
+ };
1876
+ expiresAt: number;
1877
+ }>;
1878
+ /** Per-band history with sender wall-clocks. */
1879
+ bandEnergyHistory: {
1880
+ low: Array<{
1881
+ time: number;
1882
+ value: number;
1883
+ }>;
1884
+ mid: Array<{
1885
+ time: number;
1886
+ value: number;
1887
+ }>;
1888
+ high: Array<{
1889
+ time: number;
1890
+ value: number;
1891
+ }>;
1892
+ };
1893
+ /** Per-class arrays of sender wall-clocks (rate-cap enforcement). */
1894
+ eventTimestamps: {
1895
+ kick: number[];
1896
+ snare: number[];
1897
+ hat: number[];
1898
+ };
1899
+ /** Per-class last event time (cooldown enforcement); 0 = never fired. */
1900
+ lastEventTime: {
1901
+ kick: number;
1902
+ snare: number;
1903
+ hat: number;
1904
+ };
1905
+ }
1906
+
1907
+ export declare interface SerializedCoreState {
1908
+ version: 1;
1909
+ /** Sender wall-clock at serialize time. */
1910
+ senderTime: number;
1911
+ /** Onset state only — cross-device-safe, no audio analysis carry-over. */
1912
+ onset?: SerializedOnsetState['instruments'];
1913
+ /**
1914
+ * Full audio analysis state — same-process scene-switch only. Cross-device
1915
+ * transfer should omit this block (sender's audio analysis doesn't apply
1916
+ * to receiver's audio source). Robust to gaps under ~1 second; longer
1917
+ * scene-loads (>5s) may cause stale-timestamp drift in fields like
1918
+ * `gridLockMemoryTime` / `warmupStartTimeMs` — caller should treat as a
1919
+ * degraded path and skip the audio block on stale exports.
1920
+ */
1921
+ audio?: SerializedAudioAnalysisState;
1922
+ }
1923
+
1924
+ export declare interface SerializedOnsetState {
1925
+ version: 1;
1926
+ /** Sender wall-clock at serialize time. */
1927
+ senderTime: number;
1928
+ /** Per-instrument payload — only included instruments are imported. */
1929
+ instruments: {
1930
+ kick?: InstrumentSerialized;
1931
+ snare?: InstrumentSerialized;
1932
+ hat?: InstrumentSerialized;
1933
+ };
1934
+ }
1935
+
1936
+ /**
1937
+ * PhaseLockedLoop continuity-relevant state. Treat as opaque on the consumer
1938
+ * side.
1939
+ */
1940
+ export declare interface SerializedPLLState {
1941
+ phase: number;
1942
+ /** Sender wall-clock; 0 if never set. */
1943
+ lastBeatTime: number;
1944
+ periodMs: number;
1945
+ trackedBPM: number;
1946
+ beatCounter: number;
1947
+ /** Sender wall-clock; 0 if never set. */
1948
+ lastOnsetTime: number;
1949
+ /** Sender wall-clocks in `time`. */
1950
+ bpmHistory: Array<{
1951
+ time: number;
1952
+ bpm: number;
1953
+ }>;
1954
+ driftRate: number;
1955
+ inBreakdown: boolean;
1956
+ tempoConfidence: number;
1957
+ inTransition: boolean;
1958
+ currentGain: number;
1959
+ /** Sender wall-clock; 0 if never set. */
1960
+ lastBarAdvanceTime: number;
1961
+ pendingBarAdvance: boolean;
1962
+ /** Sender wall-clock; 0 if never set. */
1963
+ lastPhaseWrapTime: number;
1964
+ consecutiveAlignedKicks: number;
1965
+ /** Sender wall-clock; 0 if never set. */
1966
+ lastHardSyncTime: number;
1967
+ /** Sender wall-clock; 0 if never set. */
1968
+ trackingStartTime: number;
1969
+ kickPhaseHistory: number[];
1970
+ phaseOffsetCalibrated: number;
1971
+ /** Sender wall-clock; 0 if never set. */
1972
+ lastCalibrationTime: number;
1973
+ lastPhaseError: number;
1974
+ }
1975
+
1976
+ /**
1977
+ * TempoInduction continuity-relevant state. Treat as opaque on the consumer
1978
+ * side — the field set is internal and may evolve in future versions
1979
+ * (gated by `version`).
1980
+ */
1981
+ export declare interface SerializedTempoInductionState {
1982
+ onsetEnvelope: number[];
1983
+ lowBandEnvelope: number[];
1984
+ midBandEnvelope: number[];
1985
+ highBandEnvelope: number[];
1986
+ fpsEstimate: number;
1987
+ currentBPM: number;
1988
+ confidence: number;
1989
+ method: 'autocorr' | 'ioi' | 'combined';
1990
+ anchorBand: string | null;
1991
+ methodAgreement: number;
1992
+ bpmHistory: number[];
1993
+ /** Sender wall-clock; null if never set. */
1994
+ lastUpdateTime: number | null;
1995
+ hypotheses: Array<{
1996
+ bpm: number;
1997
+ likelihood: number;
1998
+ age: number;
1999
+ /** Sender wall-clock. */
2000
+ lastEvidence: number;
2001
+ type: 'main' | 'half' | 'double' | 'other';
2002
+ }>;
2003
+ inTransition: boolean;
2004
+ /** Sender wall-clocks. */
2005
+ bpmDriftHistory: Array<{
2006
+ time: number;
2007
+ bpm: number;
2008
+ }>;
2009
+ driftRate: number;
2010
+ tempoChangeConfirmCount: number;
2011
+ /** Sender wall-clocks. */
2012
+ onsetHistory: Array<{
2013
+ time: number;
2014
+ strength: number;
2015
+ type: string;
2016
+ }>;
2017
+ gridLockBPM: number | null;
2018
+ gridLockScore: number;
2019
+ /** Sender wall-clock; 0 if not locked. */
2020
+ gridLockTime: number;
2021
+ gridLockMemoryBPM: number | null;
2022
+ /** Sender wall-clock; 0 if no memory. */
2023
+ gridLockMemoryTime: number;
2024
+ syncopationLevel: number;
2025
+ /** Sender wall-clock; 0 if not started. */
2026
+ warmupStartTimeMs: number;
2027
+ warmupComplete: boolean;
2028
+ }
2029
+
1663
2030
  /**
1664
2031
  * Configuration object passed to `viji.slider()`. Defines the slider's bounds,
1665
2032
  * increment, label, and UI grouping/visibility.
@@ -1704,6 +2071,17 @@ declare interface SliderParameter {
1704
2071
  category: ParameterCategory;
1705
2072
  }
1706
2073
 
2074
+ export declare interface StateImportError {
2075
+ code: 'version-mismatch' | 'malformed' | 'invalid-field';
2076
+ details: string;
2077
+ /** Present for `'version-mismatch'`. */
2078
+ payloadVersion?: number;
2079
+ /** Present for `'version-mismatch'`. */
2080
+ expectedVersion?: number;
2081
+ /** Present for `'invalid-field'`. */
2082
+ field?: string;
2083
+ }
2084
+
1707
2085
  /**
1708
2086
  * Configuration object passed to `viji.text()`. The host renders a single-line
1709
2087
  * text input field.
@@ -1863,7 +2241,10 @@ declare interface TouchPoint {
1863
2241
  */
1864
2242
  declare type TrackingState = 'TRACKING' | 'LOCKED' | 'BREAKDOWN' | 'LOST';
1865
2243
 
1866
- export declare const VERSION = "0.5.0";
2244
+ /** Returned by `on*` listener registration calls; invoke to unsubscribe. */
2245
+ export declare type Unsubscribe = () => void;
2246
+
2247
+ export declare const VERSION = "0.5.2";
1867
2248
 
1868
2249
  /**
1869
2250
  * Real-time video API: drawable frame, dimensions, and the source-side
@@ -1898,10 +2279,10 @@ export declare interface VideoAPI {
1898
2279
  * (`enable*`/`disable*`/`getActiveFeatures`/`isProcessing`).
1899
2280
  *
1900
2281
  * Multiple features can be active simultaneously, but each adds CPU/GPU
1901
- * and WebGL-context cost toggle only what you need.
2282
+ * and WebGL-context cost; toggle only what you need.
1902
2283
  *
1903
2284
  * `analysedFrame` and the result fields refresh together once per host
1904
- * frame (atomic snapshot) within a single `render()` call they are
2285
+ * frame as an atomic snapshot: within a single `render()` call they are
1905
2286
  * always paired.
1906
2287
  */
1907
2288
  cv: VideoCVAPI;
@@ -1914,30 +2295,38 @@ export declare interface VideoAPI {
1914
2295
  *
1915
2296
  * Available only on the main video stream (`viji.video.cv`). Additional
1916
2297
  * streams (`viji.videoStreams[i].cv`) and device videos
1917
- * (`viji.devices[i].video.cv`) do not run CV their `cv` surface exists for
2298
+ * (`viji.devices[i].video.cv`) do not run CV; their `cv` surface exists for
1918
2299
  * API consistency but the data fields stay at their empty defaults and the
1919
2300
  * `enable*` verbs are no-ops.
1920
2301
  */
1921
2302
  export declare interface VideoCVAPI {
1922
2303
  /**
1923
2304
  * The exact video frame that produced the current `faces` / `hands` /
1924
- * `pose` / `segmentation` results the frame MediaPipe actually analysed.
1925
- * Use this (not `viji.video.currentFrame`) when overlaying pixel-precise
1926
- * CV-driven effects (face-mesh masks, body-segmentation compositing,
1927
- * makeup, distortion). `currentFrame` is the just-arrived frame and runs
1928
- * ~15–40 ms ahead of the analysis; `analysedFrame` is the frame the
1929
- * landmarks/mask actually correspond to, so the two stay locked under
1930
- * fast head/body motion.
2305
+ * `pose` / `segmentation` results: the frame MediaPipe actually analysed.
2306
+ *
2307
+ * Choose between `analysedFrame` and `viji.video.currentFrame` by asking
2308
+ * whether the effect reads pixels from the displayed frame at CV-derived
2309
+ * positions. Reach for `analysedFrame` when the answer is yes
2310
+ * (compositing the segmentation mask onto the body, sampling skin tone
2311
+ * under a face landmark, warping the face along its mesh, texture-mapped
2312
+ * face filters): pairing the displayed frame with the CV results is
2313
+ * required for those effects. Stay on `currentFrame` when the answer is
2314
+ * no (drawing landmark dots, particles, debug overlays, geometry driven
2315
+ * by CV positions): `analysedFrame` only refreshes when MediaPipe
2316
+ * completes an inference, which is not every frame, so reaching for it
2317
+ * without a reason makes the displayed video stutter or hold between
2318
+ * inferences.
1931
2319
  *
1932
2320
  * Pairing guarantee: within a single `render()` call, `analysedFrame`
1933
2321
  * is paired with the values of `faces`, `hands`, `pose`, `segmentation`
1934
- * you read in the same call. They refresh together once per host frame.
2322
+ * read in the same call.
1935
2323
  *
1936
2324
  * `null` until the first CV inference completes after a feature is
1937
2325
  * enabled, after the video disconnects, or after a CV-feature toggle
1938
2326
  * clears state. A common pattern is
1939
2327
  * `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back
1940
- * to the live feed during the brief startup window.
2328
+ * to the live feed during the brief startup window; expect a small
2329
+ * visual hitch when the source switches as the first inference lands.
1941
2330
  *
1942
2331
  * Read-only: this `OffscreenCanvas` is owned by the engine. Do not
1943
2332
  * mutate it (no `getContext`-and-paint, no `transferToImageBitmap`).
@@ -1950,7 +2339,7 @@ export declare interface VideoCVAPI {
1950
2339
  * analysis, or `null` when no CV result has landed yet (or the stream is
1951
2340
  * disconnected). Cached: re-extracted only when a new CV result arrives,
1952
2341
  * so multiple readers within a render share one allocation. Slow
1953
- * compared to drawing `analysedFrame` directly use only when you need
2342
+ * compared to drawing `analysedFrame` directly. Use only when you need
1954
2343
  * pixel values aligned with CV landmark positions (e.g. sampling skin
1955
2344
  * colour at a face landmark).
1956
2345
  */
@@ -2220,6 +2609,7 @@ export declare class VijiCore {
2220
2609
  private parameterDefinedListeners;
2221
2610
  private parameterErrorListeners;
2222
2611
  private capabilitiesChangeListeners;
2612
+ private stateImportErrorListeners;
2223
2613
  private stats;
2224
2614
  constructor(config: VijiCoreConfig);
2225
2615
  /**
@@ -2473,6 +2863,44 @@ export declare class VijiCore {
2473
2863
  message: string;
2474
2864
  code: string;
2475
2865
  }) => void): void;
2866
+ /**
2867
+ * Snapshot the full audio analysis + onset state for cross-instance
2868
+ * transfer. Use for **same-process scene-switch** continuity (a fresh
2869
+ * `VijiCore` can resume detection where the previous one left off).
2870
+ *
2871
+ * For **cross-device** transfer, prefer `audio.onset.exportSessionState`
2872
+ * — the controller's audio analysis state doesn't apply to the host's
2873
+ * audio source and would corrupt detection.
2874
+ *
2875
+ * Wall-clock fields are in this instance's `performance.now()` clock space.
2876
+ * Receiver applies `clockOffset` on import (use `0` for same-process).
2877
+ *
2878
+ * **Staleness caveat**: the audio block is robust to scene-load gaps under
2879
+ * ~1 second. Longer gaps (>5s, e.g. cold cache + slow network) may cause
2880
+ * stale-timestamp drift in `gridLockMemoryTime` / `warmupStartTimeMs`;
2881
+ * callers should treat that as a degraded path and skip the audio block.
2882
+ */
2883
+ exportFullState(): SerializedCoreState;
2884
+ /**
2885
+ * Replace the audio analysis + onset state from a serialized payload.
2886
+ * `clockOffset` is added to all sender-clocked fields (`0` for same-process,
2887
+ * NTP-derived for cross-device). Mutation is synchronous; no `onModeChange`
2888
+ * or `onSessionEnd` events are emitted (state replacement is not a transition).
2889
+ *
2890
+ * Validates `version`; on mismatch, fires `onStateImportError` and leaves
2891
+ * existing state intact.
2892
+ */
2893
+ importFullState(state: SerializedCoreState, clockOffset: number): void;
2894
+ /**
2895
+ * Listen for state-import validation errors. Fires when `importFullState`
2896
+ * or `audio.onset.importSessionState` rejects a payload (version mismatch,
2897
+ * malformed shape, invalid field). Existing state is left intact when
2898
+ * an error fires.
2899
+ *
2900
+ * @returns Unsubscribe function. Call to remove the listener.
2901
+ */
2902
+ onStateImportError(listener: (error: StateImportError) => void): Unsubscribe;
2903
+ private fireStateImportError;
2476
2904
  /**
2477
2905
  * Notify parameter change listeners
2478
2906
  */
@@ -2611,6 +3039,64 @@ export declare class VijiCore {
2611
3039
  * Check if an instrument onset is muted
2612
3040
  */
2613
3041
  isMuted: (instrument: "kick" | "snare" | "hat") => boolean;
3042
+ /**
3043
+ * Listen for instrument mode transitions (`'auto' | 'tapping' | 'pattern'`).
3044
+ * Fires on every transition including the first tap (`'auto' → 'tapping'`)
3045
+ * and pattern recognition (`'tapping' → 'pattern'`). Imported state does
3046
+ * NOT fire mode-change events — state replacement is not a transition.
3047
+ *
3048
+ * @returns Unsubscribe function. Call to remove the listener.
3049
+ */
3050
+ onModeChange: (listener: (ev: OnsetModeChangeEvent) => void) => Unsubscribe;
3051
+ /**
3052
+ * Listen for natural session-end events. Fires when:
3053
+ * - 500ms elapse since last tap with instrument in `'pattern'` mode
3054
+ * (outcome `'pattern'`), or
3055
+ * - 5s elapse in `'tapping'` mode without a recognized pattern
3056
+ * (outcome `'cleared'`; instrument transitions to `'auto'`).
3057
+ *
3058
+ * Explicit `clear()` calls do NOT fire this event (caller-initiated;
3059
+ * caller already knows). Imported state does NOT fire either.
3060
+ *
3061
+ * @returns Unsubscribe function. Call to remove the listener.
3062
+ */
3063
+ onSessionEnd: (listener: (ev: OnsetSessionEndEvent) => void) => Unsubscribe;
3064
+ /**
3065
+ * Listen for instrument mute-state transitions (`true ↔ false`). Fires
3066
+ * whenever the underlying mute state actually changes, regardless of
3067
+ * which method triggered it:
3068
+ * - `setMuted(instrument, muted)` when `prev !== next`
3069
+ * - `tap(instrument)` auto-unmute on first tap of a muted instrument
3070
+ * - `clear(instrument)` when the instrument was muted
3071
+ *
3072
+ * Idempotent calls (e.g. `setMuted(true)` on an already-muted instrument)
3073
+ * do not fire. Imported state does NOT fire either.
3074
+ *
3075
+ * @returns Unsubscribe function. Call to remove the listener.
3076
+ */
3077
+ onMuteChange: (listener: (ev: OnsetMuteChangeEvent) => void) => Unsubscribe;
3078
+ /**
3079
+ * Snapshot per-instrument onset state for cross-instance transfer.
3080
+ * Cross-device-safe (no audio analysis state included). Pair with
3081
+ * `importSessionState` on a receiver. Wall-clock fields are in this
3082
+ * instance's `performance.now()` clock space.
3083
+ */
3084
+ exportSessionState: () => SerializedOnsetState;
3085
+ /**
3086
+ * Replace per-instrument onset state from a serialized payload.
3087
+ * `clockOffset` is added to all sender-clocked fields during import
3088
+ * (use `0` for same-process transfer; NTP-derived offset for cross-device).
3089
+ * Mutation is synchronous; no events are emitted.
3090
+ *
3091
+ * Patterns are rebased forward by whole pattern cycles to eliminate
3092
+ * the catch-up burst that would otherwise occur on stale payloads
3093
+ * (phase-preserving — events still land on the original beat positions
3094
+ * modulo pattern length).
3095
+ *
3096
+ * Validates `version`; on mismatch, fires `onStateImportError` and
3097
+ * leaves existing state intact.
3098
+ */
3099
+ importSessionState: (state: SerializedOnsetState, clockOffset: number) => void;
2614
3100
  };
2615
3101
  /**
2616
3102
  * Advanced audio controls
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A, V, a, b } from "./index-Cp9G0z4E.js";
1
+ import { A, V, a, b } from "./index-B8LJ9m47.js";
2
2
  export {
3
3
  A as AudioSystem,
4
4
  V as VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viji-dev/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Universal execution engine for Viji Creative scenes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",