@viji-dev/core 0.3.20 → 0.3.21
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/README.md +2 -13
- package/dist/artist-dts-p5.js +1 -1
- package/dist/artist-dts.js +1 -1
- package/dist/artist-global.d.ts +65 -7
- package/dist/assets/cv-tasks.worker.js +302 -47
- package/dist/assets/viji.worker-bm-hvzXt.js +25975 -0
- package/dist/assets/viji.worker-bm-hvzXt.js.map +1 -0
- package/dist/{essentia-wasm.web-MJg8deal.js → essentia-wasm.web-C1URJxCY.js} +2 -2
- package/dist/{essentia-wasm.web-MJg8deal.js.map → essentia-wasm.web-C1URJxCY.js.map} +1 -1
- package/dist/{index-DW8r2Eux.js → index-trkn0FNW.js} +695 -738
- package/dist/index-trkn0FNW.js.map +1 -0
- package/dist/index.d.ts +161 -206
- package/dist/index.js +1 -1
- package/dist/shader-uniforms.js +403 -23
- package/package.json +3 -2
- package/dist/assets/viji.worker-BnDb6mPh.js +0 -4325
- package/dist/assets/viji.worker-BnDb6mPh.js.map +0 -1
- package/dist/index-DW8r2Eux.js.map +0 -1
|
@@ -567,7 +567,7 @@ class IFrameManager {
|
|
|
567
567
|
}
|
|
568
568
|
function WorkerWrapper(options) {
|
|
569
569
|
return new Worker(
|
|
570
|
-
"" + new URL("assets/viji.worker-
|
|
570
|
+
"" + new URL("assets/viji.worker-bm-hvzXt.js", import.meta.url).href,
|
|
571
571
|
{
|
|
572
572
|
type: "module",
|
|
573
573
|
name: options?.name
|
|
@@ -1481,13 +1481,17 @@ class MultiOnsetDetection {
|
|
|
1481
1481
|
const displayName = bandName === "low" ? "low" : "high";
|
|
1482
1482
|
if (onset) {
|
|
1483
1483
|
if (bandName === "low") this.debugOnsetCount++;
|
|
1484
|
-
if (this.
|
|
1485
|
-
|
|
1484
|
+
if (this.debugMode) {
|
|
1485
|
+
if (this.consecutiveMisses[displayName] && this.consecutiveMisses[displayName].count > 3) {
|
|
1486
|
+
console.log(` ⚠️ [${displayName}] ${this.consecutiveMisses[displayName].count} consecutive misses (${this.consecutiveMisses[displayName].lastReason})`);
|
|
1487
|
+
}
|
|
1486
1488
|
}
|
|
1487
1489
|
this.consecutiveMisses[displayName] = { count: 0, lastReason: "" };
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1490
|
+
if (this.debugMode) {
|
|
1491
|
+
const logCutoff = bandName === "high" ? adjustedCutoff : state.cutoff;
|
|
1492
|
+
const emoji = bandName === "low" ? "🔥" : "✨";
|
|
1493
|
+
console.log(`${emoji} [${displayName}] ONSET${bandName === "low" ? " #" + this.debugOnsetCount : ""}! flux=${flux.toFixed(3)} energy=${energy.toFixed(2)} cutoff=${logCutoff.toFixed(2)}`);
|
|
1494
|
+
}
|
|
1491
1495
|
if (this.debugMode) {
|
|
1492
1496
|
const sixteenthNote = 6e4 / this.currentBPM / 4;
|
|
1493
1497
|
console.log(` 📊 Sharpness: ${sharpness.toFixed(3)} | Raw Flux: ${flux.toFixed(3)} | Emphasized: ${emphasizedFlux.toFixed(3)}`);
|
|
@@ -1503,7 +1507,7 @@ class MultiOnsetDetection {
|
|
|
1503
1507
|
}
|
|
1504
1508
|
this.consecutiveMisses[displayName].count++;
|
|
1505
1509
|
this.consecutiveMisses[displayName].lastReason = failReason;
|
|
1506
|
-
if (this.consecutiveMisses[displayName].count === 1) {
|
|
1510
|
+
if (this.debugMode && this.consecutiveMisses[displayName].count === 1) {
|
|
1507
1511
|
const timeSince = (now - state.lastOnsetTime).toFixed(0);
|
|
1508
1512
|
const logCutoff = bandName === "high" ? adjustedCutoff : state.cutoff;
|
|
1509
1513
|
console.log(`⚠️ [${displayName}] MISS (${failReason}) flux=${flux.toFixed(3)} energy=${energy.toFixed(2)} cutoff=${logCutoff.toFixed(2)} lastOnset=${timeSince}ms ago`);
|
|
@@ -1549,7 +1553,7 @@ class MultiOnsetDetection {
|
|
|
1549
1553
|
const state = this.bandStates.get(band);
|
|
1550
1554
|
if (state) {
|
|
1551
1555
|
state.primaryOnsetHistory.push(now);
|
|
1552
|
-
if (band === "low") {
|
|
1556
|
+
if (band === "low" && this.debugMode) {
|
|
1553
1557
|
console.log(` ✓ PRIMARY [low] onset recorded (total: ${state.primaryOnsetHistory.length})`);
|
|
1554
1558
|
}
|
|
1555
1559
|
}
|
|
@@ -1916,7 +1920,7 @@ class EssentiaOnsetDetection {
|
|
|
1916
1920
|
this.initPromise = (async () => {
|
|
1917
1921
|
try {
|
|
1918
1922
|
const essentiaModule = await import("./essentia.js-core.es-DnrJE0uR.js");
|
|
1919
|
-
const wasmModule = await import("./essentia-wasm.web-
|
|
1923
|
+
const wasmModule = await import("./essentia-wasm.web-C1URJxCY.js").then((n) => n.e);
|
|
1920
1924
|
const EssentiaClass = essentiaModule.Essentia || essentiaModule.default?.Essentia || essentiaModule.default;
|
|
1921
1925
|
let WASMModule = wasmModule.default || wasmModule.EssentiaWASM || wasmModule.default?.EssentiaWASM;
|
|
1922
1926
|
if (!WASMModule) {
|
|
@@ -2118,43 +2122,32 @@ class EssentiaOnsetDetection {
|
|
|
2118
2122
|
onset: globalOnset,
|
|
2119
2123
|
strength: clampedStrength,
|
|
2120
2124
|
bands: {
|
|
2121
|
-
// Low bands: Use complex domain (best for kicks)
|
|
2122
2125
|
low: {
|
|
2123
2126
|
onset: kickOnset,
|
|
2124
|
-
// INDEPENDENT detection!
|
|
2125
2127
|
strength: clampedComplex,
|
|
2126
2128
|
energy: bandEnergies.low,
|
|
2127
2129
|
sharpness: kickSharpness
|
|
2128
|
-
// Attack sharpness for kick/bassline discrimination
|
|
2129
2130
|
},
|
|
2130
2131
|
lowMid: {
|
|
2131
2132
|
onset: kickOnset,
|
|
2132
|
-
// Follows kick detection (kick harmonics)
|
|
2133
2133
|
strength: clampedComplex,
|
|
2134
2134
|
energy: bandEnergies.lowMid,
|
|
2135
2135
|
sharpness: kickSharpness
|
|
2136
2136
|
},
|
|
2137
|
-
// Mid band: Use spectral flux (best for snares)
|
|
2138
2137
|
mid: {
|
|
2139
2138
|
onset: snareOnset,
|
|
2140
|
-
// INDEPENDENT detection!
|
|
2141
2139
|
strength: clampedFlux,
|
|
2142
2140
|
energy: bandEnergies.mid,
|
|
2143
2141
|
sharpness: snareSharpness
|
|
2144
|
-
// Transient quality for snare detection
|
|
2145
2142
|
},
|
|
2146
|
-
// High bands: Use HFC (best for hats)
|
|
2147
2143
|
highMid: {
|
|
2148
2144
|
onset: hatOnset,
|
|
2149
|
-
// INDEPENDENT detection!
|
|
2150
2145
|
strength: clampedHFC,
|
|
2151
2146
|
energy: bandEnergies.highMid,
|
|
2152
2147
|
sharpness: hatSharpness
|
|
2153
|
-
// Transient quality for hat detection
|
|
2154
2148
|
},
|
|
2155
2149
|
high: {
|
|
2156
2150
|
onset: hatOnset,
|
|
2157
|
-
// INDEPENDENT detection!
|
|
2158
2151
|
strength: clampedHFC,
|
|
2159
2152
|
energy: bandEnergies.high,
|
|
2160
2153
|
sharpness: hatSharpness
|
|
@@ -10233,7 +10226,7 @@ const CONFIDENCE_LOCKED_THRESHOLD = 0.6;
|
|
|
10233
10226
|
const BREAKDOWN_THRESHOLD_MS = 2e3;
|
|
10234
10227
|
const LOST_THRESHOLD_MS = 3e4;
|
|
10235
10228
|
const GRID_INVALID_THRESHOLD = 0.25;
|
|
10236
|
-
const SAMPLE_RATE = 60;
|
|
10229
|
+
const SAMPLE_RATE$1 = 60;
|
|
10237
10230
|
class BeatStateManager {
|
|
10238
10231
|
// State machine
|
|
10239
10232
|
state = "TRACKING";
|
|
@@ -10270,13 +10263,14 @@ class BeatStateManager {
|
|
|
10270
10263
|
// Running average of onset strength
|
|
10271
10264
|
// Envelope followers for beat energy curves
|
|
10272
10265
|
envelopes = {
|
|
10273
|
-
kick: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10274
|
-
snare: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10275
|
-
hat: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10276
|
-
any: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10277
|
-
kickSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE),
|
|
10278
|
-
snareSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE),
|
|
10279
|
-
|
|
10266
|
+
kick: new EnvelopeFollower(0, 300, SAMPLE_RATE$1),
|
|
10267
|
+
snare: new EnvelopeFollower(0, 300, SAMPLE_RATE$1),
|
|
10268
|
+
hat: new EnvelopeFollower(0, 300, SAMPLE_RATE$1),
|
|
10269
|
+
any: new EnvelopeFollower(0, 300, SAMPLE_RATE$1),
|
|
10270
|
+
kickSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE$1),
|
|
10271
|
+
snareSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE$1),
|
|
10272
|
+
hatSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE$1),
|
|
10273
|
+
anySmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE$1)
|
|
10280
10274
|
};
|
|
10281
10275
|
// Confidence factors
|
|
10282
10276
|
tempoMethodAgreement = 0;
|
|
@@ -10517,10 +10511,11 @@ class BeatStateManager {
|
|
|
10517
10511
|
this.debugKickCount++;
|
|
10518
10512
|
const kickEvent = detectorEvents.find((e) => e.type === "kick");
|
|
10519
10513
|
const bands = kickEvent && "bands" in kickEvent ? kickEvent.bands : kickBands;
|
|
10514
|
+
const rawKickStrength = kickEvent?.strength || onsets.bands.low.strength;
|
|
10520
10515
|
const event = {
|
|
10521
10516
|
type: "kick",
|
|
10522
10517
|
time: now,
|
|
10523
|
-
strength:
|
|
10518
|
+
strength: Math.min(1, rawKickStrength / 5),
|
|
10524
10519
|
phase: pll.getPhase(),
|
|
10525
10520
|
bar: pll.getBar(),
|
|
10526
10521
|
isLayered: classification.snare || classification.hat,
|
|
@@ -10541,10 +10536,11 @@ class BeatStateManager {
|
|
|
10541
10536
|
this.debugSnareCount++;
|
|
10542
10537
|
const snareEvent = detectorEvents.find((e) => e.type === "snare");
|
|
10543
10538
|
const bands = snareEvent && "bands" in snareEvent ? snareEvent.bands : snareBands;
|
|
10539
|
+
const rawSnareStrength = snareEvent?.strength || onsets.bands.mid.strength;
|
|
10544
10540
|
const event = {
|
|
10545
10541
|
type: "snare",
|
|
10546
10542
|
time: now,
|
|
10547
|
-
strength:
|
|
10543
|
+
strength: Math.min(1, rawSnareStrength / 5),
|
|
10548
10544
|
phase: pll.getPhase(),
|
|
10549
10545
|
bar: pll.getBar(),
|
|
10550
10546
|
isLayered: classification.kick || classification.hat,
|
|
@@ -10565,10 +10561,11 @@ class BeatStateManager {
|
|
|
10565
10561
|
this.debugHatCount++;
|
|
10566
10562
|
const hatEvent = detectorEvents.find((e) => e.type === "hat");
|
|
10567
10563
|
const bands = hatEvent && "bands" in hatEvent ? hatEvent.bands : hatBands;
|
|
10564
|
+
const rawHatStrength = hatEvent?.strength || onsets.bands.high.strength;
|
|
10568
10565
|
const event = {
|
|
10569
10566
|
type: "hat",
|
|
10570
10567
|
time: now,
|
|
10571
|
-
strength:
|
|
10568
|
+
strength: Math.min(1, rawHatStrength / 3e3),
|
|
10572
10569
|
phase: pll.getPhase(),
|
|
10573
10570
|
bar: pll.getBar(),
|
|
10574
10571
|
isLayered: classification.kick || classification.snare,
|
|
@@ -10599,6 +10596,7 @@ class BeatStateManager {
|
|
|
10599
10596
|
any: this.envelopes.any.getValue(),
|
|
10600
10597
|
kickSmoothed: this.envelopes.kickSmoothed.getValue(),
|
|
10601
10598
|
snareSmoothed: this.envelopes.snareSmoothed.getValue(),
|
|
10599
|
+
hatSmoothed: this.envelopes.hatSmoothed.getValue(),
|
|
10602
10600
|
anySmoothed: this.envelopes.anySmoothed.getValue(),
|
|
10603
10601
|
events: bufferedEvents,
|
|
10604
10602
|
// Buffered events (persisted for 20ms, reliable for async access)
|
|
@@ -11187,9 +11185,7 @@ class BeatStateManager {
|
|
|
11187
11185
|
if (snare) {
|
|
11188
11186
|
const isLayered = timeSinceLastPrimaryKick <= 80;
|
|
11189
11187
|
const midSharpness2 = bands.mid.sharpness ?? 1;
|
|
11190
|
-
const isHighQuality = midSharpness2 > 0.25 &&
|
|
11191
|
-
bands.mid.strength > 0.3 && // Strong enough to be reliable
|
|
11192
|
-
midRatio > 0.1;
|
|
11188
|
+
const isHighQuality = midSharpness2 > 0.25 && bands.mid.strength > 0.3 && midRatio > 0.1;
|
|
11193
11189
|
if (isHighQuality) {
|
|
11194
11190
|
if (isLayered) {
|
|
11195
11191
|
this.adaptiveProfiles.snareLayered.samples.push({
|
|
@@ -11419,6 +11415,7 @@ class BeatStateManager {
|
|
|
11419
11415
|
}
|
|
11420
11416
|
if (classification.hat) {
|
|
11421
11417
|
this.envelopes.hat.trigger(1);
|
|
11418
|
+
this.envelopes.hatSmoothed.trigger(1);
|
|
11422
11419
|
this.envelopes.any.trigger(0.5);
|
|
11423
11420
|
}
|
|
11424
11421
|
if (classification.any && !classification.kick && !classification.snare && !classification.hat) {
|
|
@@ -11431,8 +11428,32 @@ class BeatStateManager {
|
|
|
11431
11428
|
this.envelopes.any.process(0, dtMs);
|
|
11432
11429
|
this.envelopes.kickSmoothed.process(0, dtMs);
|
|
11433
11430
|
this.envelopes.snareSmoothed.process(0, dtMs);
|
|
11431
|
+
this.envelopes.hatSmoothed.process(0, dtMs);
|
|
11434
11432
|
this.envelopes.anySmoothed.process(0, dtMs);
|
|
11435
11433
|
}
|
|
11434
|
+
/**
|
|
11435
|
+
* Decay envelopes without triggering new beats (used during silent frames)
|
|
11436
|
+
*/
|
|
11437
|
+
processEnvelopeDecay(dtMs) {
|
|
11438
|
+
this.envelopes.kick.process(0, dtMs);
|
|
11439
|
+
this.envelopes.snare.process(0, dtMs);
|
|
11440
|
+
this.envelopes.hat.process(0, dtMs);
|
|
11441
|
+
this.envelopes.any.process(0, dtMs);
|
|
11442
|
+
this.envelopes.kickSmoothed.process(0, dtMs);
|
|
11443
|
+
this.envelopes.snareSmoothed.process(0, dtMs);
|
|
11444
|
+
this.envelopes.hatSmoothed.process(0, dtMs);
|
|
11445
|
+
this.envelopes.anySmoothed.process(0, dtMs);
|
|
11446
|
+
return {
|
|
11447
|
+
kick: this.envelopes.kick.getValue(),
|
|
11448
|
+
snare: this.envelopes.snare.getValue(),
|
|
11449
|
+
hat: this.envelopes.hat.getValue(),
|
|
11450
|
+
any: this.envelopes.any.getValue(),
|
|
11451
|
+
kickSmoothed: this.envelopes.kickSmoothed.getValue(),
|
|
11452
|
+
snareSmoothed: this.envelopes.snareSmoothed.getValue(),
|
|
11453
|
+
hatSmoothed: this.envelopes.hatSmoothed.getValue(),
|
|
11454
|
+
anySmoothed: this.envelopes.anySmoothed.getValue()
|
|
11455
|
+
};
|
|
11456
|
+
}
|
|
11436
11457
|
// ========== INDUSTRY-STANDARD HELPER METHODS ==========
|
|
11437
11458
|
// NOTE: calculateGridAlignment() and calculateIOIConsistency() removed in Phase 1
|
|
11438
11459
|
// These were part of advanced filtering that we disabled to maintain compatibility
|
|
@@ -12351,6 +12372,382 @@ FFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff, off
|
|
|
12351
12372
|
out[outOff + 7] = FDi;
|
|
12352
12373
|
};
|
|
12353
12374
|
const FFT$1 = /* @__PURE__ */ getDefaultExportFromCjs(fft);
|
|
12375
|
+
const INSTRUMENTS = ["kick", "snare", "hat"];
|
|
12376
|
+
const SAMPLE_RATE = 60;
|
|
12377
|
+
const TAP_TIMEOUT_MS = 5e3;
|
|
12378
|
+
const MAX_CYCLE_LENGTH = 8;
|
|
12379
|
+
const FUZZY_TOLERANCE = 0.18;
|
|
12380
|
+
const MIN_REPETITIONS = 3;
|
|
12381
|
+
const MAX_TAP_INTERVAL_MS = 3e3;
|
|
12382
|
+
const MIN_TAP_INTERVAL_MS = 100;
|
|
12383
|
+
const MAX_TAP_HISTORY = 64;
|
|
12384
|
+
const MIN_EMA_ALPHA = 0.05;
|
|
12385
|
+
const PATTERN_SAME_TOLERANCE = 0.15;
|
|
12386
|
+
function createInstrumentState() {
|
|
12387
|
+
return {
|
|
12388
|
+
mode: "auto",
|
|
12389
|
+
muted: false,
|
|
12390
|
+
mutedAt: 0,
|
|
12391
|
+
tapIOIs: [],
|
|
12392
|
+
lastTapTime: 0,
|
|
12393
|
+
pattern: null,
|
|
12394
|
+
refinementIndex: 0,
|
|
12395
|
+
refinementCounts: [],
|
|
12396
|
+
replayLastEventTime: 0,
|
|
12397
|
+
replayIndex: 0,
|
|
12398
|
+
pendingTapEvents: [],
|
|
12399
|
+
envelope: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
12400
|
+
envelopeSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE)
|
|
12401
|
+
};
|
|
12402
|
+
}
|
|
12403
|
+
class OnsetTapManager {
|
|
12404
|
+
state = {
|
|
12405
|
+
kick: createInstrumentState(),
|
|
12406
|
+
snare: createInstrumentState(),
|
|
12407
|
+
hat: createInstrumentState()
|
|
12408
|
+
};
|
|
12409
|
+
tap(instrument) {
|
|
12410
|
+
const s = this.state[instrument];
|
|
12411
|
+
const now = performance.now();
|
|
12412
|
+
if (s.muted) {
|
|
12413
|
+
s.muted = false;
|
|
12414
|
+
s.mutedAt = 0;
|
|
12415
|
+
}
|
|
12416
|
+
let ioi = -1;
|
|
12417
|
+
if (s.lastTapTime > 0) {
|
|
12418
|
+
ioi = now - s.lastTapTime;
|
|
12419
|
+
if (ioi < MIN_TAP_INTERVAL_MS) return;
|
|
12420
|
+
if (ioi > MAX_TAP_INTERVAL_MS) {
|
|
12421
|
+
s.tapIOIs = [];
|
|
12422
|
+
ioi = -1;
|
|
12423
|
+
} else {
|
|
12424
|
+
s.tapIOIs.push(ioi);
|
|
12425
|
+
if (s.tapIOIs.length > MAX_TAP_HISTORY) s.tapIOIs.shift();
|
|
12426
|
+
}
|
|
12427
|
+
}
|
|
12428
|
+
s.lastTapTime = now;
|
|
12429
|
+
s.pendingTapEvents.push(now);
|
|
12430
|
+
if (s.mode === "auto") {
|
|
12431
|
+
s.mode = "tapping";
|
|
12432
|
+
if (ioi > 0) {
|
|
12433
|
+
const pattern = this.tryRecognizePattern(instrument);
|
|
12434
|
+
if (pattern) this.applyPattern(instrument, pattern);
|
|
12435
|
+
}
|
|
12436
|
+
} else if (s.mode === "tapping") {
|
|
12437
|
+
if (ioi > 0) {
|
|
12438
|
+
const pattern = this.tryRecognizePattern(instrument);
|
|
12439
|
+
if (pattern) this.applyPattern(instrument, pattern);
|
|
12440
|
+
}
|
|
12441
|
+
} else if (s.mode === "pattern") {
|
|
12442
|
+
if (ioi > 0) {
|
|
12443
|
+
this.handlePatternTap(instrument, ioi, now);
|
|
12444
|
+
}
|
|
12445
|
+
}
|
|
12446
|
+
}
|
|
12447
|
+
clear(instrument) {
|
|
12448
|
+
const s = this.state[instrument];
|
|
12449
|
+
s.mode = "auto";
|
|
12450
|
+
s.muted = false;
|
|
12451
|
+
s.mutedAt = 0;
|
|
12452
|
+
s.tapIOIs = [];
|
|
12453
|
+
s.lastTapTime = 0;
|
|
12454
|
+
s.pattern = null;
|
|
12455
|
+
s.refinementIndex = 0;
|
|
12456
|
+
s.refinementCounts = [];
|
|
12457
|
+
s.replayLastEventTime = 0;
|
|
12458
|
+
s.replayIndex = 0;
|
|
12459
|
+
s.pendingTapEvents = [];
|
|
12460
|
+
s.envelope.reset();
|
|
12461
|
+
s.envelopeSmoothed.reset();
|
|
12462
|
+
}
|
|
12463
|
+
getMode(instrument) {
|
|
12464
|
+
return this.state[instrument].mode;
|
|
12465
|
+
}
|
|
12466
|
+
getPatternInfo(instrument) {
|
|
12467
|
+
const s = this.state[instrument];
|
|
12468
|
+
if (!s.pattern) return null;
|
|
12469
|
+
return {
|
|
12470
|
+
length: s.pattern.length,
|
|
12471
|
+
intervals: [...s.pattern],
|
|
12472
|
+
confidence: 1
|
|
12473
|
+
};
|
|
12474
|
+
}
|
|
12475
|
+
setMuted(instrument, muted) {
|
|
12476
|
+
const s = this.state[instrument];
|
|
12477
|
+
if (s.muted === muted) return;
|
|
12478
|
+
const now = performance.now();
|
|
12479
|
+
if (muted) {
|
|
12480
|
+
s.muted = true;
|
|
12481
|
+
s.mutedAt = now;
|
|
12482
|
+
} else {
|
|
12483
|
+
const pauseDuration = now - s.mutedAt;
|
|
12484
|
+
s.muted = false;
|
|
12485
|
+
s.mutedAt = 0;
|
|
12486
|
+
if (s.mode === "tapping" && s.lastTapTime > 0) {
|
|
12487
|
+
s.lastTapTime += pauseDuration;
|
|
12488
|
+
}
|
|
12489
|
+
}
|
|
12490
|
+
}
|
|
12491
|
+
isMuted(instrument) {
|
|
12492
|
+
return this.state[instrument].muted;
|
|
12493
|
+
}
|
|
12494
|
+
/**
|
|
12495
|
+
* Post-process a beat state produced by BeatStateManager.
|
|
12496
|
+
* For instruments in auto mode, values pass through unchanged.
|
|
12497
|
+
* For tapping/pattern instruments, auto events are suppressed and
|
|
12498
|
+
* tap/pattern events + envelopes are injected instead.
|
|
12499
|
+
*/
|
|
12500
|
+
processFrame(beatState, now, dtMs) {
|
|
12501
|
+
const result = { ...beatState, events: [...beatState.events] };
|
|
12502
|
+
const newEvents = [];
|
|
12503
|
+
for (const inst of INSTRUMENTS) {
|
|
12504
|
+
const s = this.state[inst];
|
|
12505
|
+
if (s.muted) {
|
|
12506
|
+
result.events = result.events.filter((e) => e.type !== inst);
|
|
12507
|
+
s.pendingTapEvents = [];
|
|
12508
|
+
if (s.mode === "pattern" && s.pattern && s.replayLastEventTime > 0) {
|
|
12509
|
+
for (let i = 0; i < 3; i++) {
|
|
12510
|
+
const interval = s.pattern[s.replayIndex % s.pattern.length];
|
|
12511
|
+
if (now - s.replayLastEventTime >= interval) {
|
|
12512
|
+
s.replayLastEventTime += interval;
|
|
12513
|
+
s.replayIndex = (s.replayIndex + 1) % s.pattern.length;
|
|
12514
|
+
} else {
|
|
12515
|
+
break;
|
|
12516
|
+
}
|
|
12517
|
+
}
|
|
12518
|
+
}
|
|
12519
|
+
s.envelope.process(0, dtMs);
|
|
12520
|
+
s.envelopeSmoothed.process(0, dtMs);
|
|
12521
|
+
const envKey2 = inst;
|
|
12522
|
+
const smoothKey2 = `${inst}Smoothed`;
|
|
12523
|
+
result[envKey2] = s.envelope.getValue();
|
|
12524
|
+
result[smoothKey2] = s.envelopeSmoothed.getValue();
|
|
12525
|
+
continue;
|
|
12526
|
+
}
|
|
12527
|
+
if (s.mode === "auto") {
|
|
12528
|
+
s.envelope.process(0, dtMs);
|
|
12529
|
+
s.envelopeSmoothed.process(0, dtMs);
|
|
12530
|
+
continue;
|
|
12531
|
+
}
|
|
12532
|
+
result.events = result.events.filter((e) => e.type !== inst);
|
|
12533
|
+
if (s.mode === "tapping") {
|
|
12534
|
+
if (now - s.lastTapTime > TAP_TIMEOUT_MS) {
|
|
12535
|
+
s.tapIOIs = [];
|
|
12536
|
+
s.pendingTapEvents = [];
|
|
12537
|
+
if (s.pattern) {
|
|
12538
|
+
s.mode = "pattern";
|
|
12539
|
+
s.replayLastEventTime = now;
|
|
12540
|
+
s.replayIndex = 0;
|
|
12541
|
+
} else {
|
|
12542
|
+
s.mode = "auto";
|
|
12543
|
+
}
|
|
12544
|
+
continue;
|
|
12545
|
+
}
|
|
12546
|
+
while (s.pendingTapEvents.length > 0) {
|
|
12547
|
+
const tapTime = s.pendingTapEvents.shift();
|
|
12548
|
+
newEvents.push(this.createTapEvent(inst, tapTime, result.bpm));
|
|
12549
|
+
s.envelope.trigger(1);
|
|
12550
|
+
s.envelopeSmoothed.trigger(1);
|
|
12551
|
+
s.replayLastEventTime = tapTime;
|
|
12552
|
+
s.replayIndex = 0;
|
|
12553
|
+
}
|
|
12554
|
+
}
|
|
12555
|
+
if (s.mode === "pattern") {
|
|
12556
|
+
while (s.pendingTapEvents.length > 0) {
|
|
12557
|
+
const tapTime = s.pendingTapEvents.shift();
|
|
12558
|
+
newEvents.push(this.createTapEvent(inst, tapTime, result.bpm));
|
|
12559
|
+
s.envelope.trigger(1);
|
|
12560
|
+
s.envelopeSmoothed.trigger(1);
|
|
12561
|
+
}
|
|
12562
|
+
const isUserActive = now - s.lastTapTime < 500;
|
|
12563
|
+
if (!isUserActive && s.pattern) {
|
|
12564
|
+
const scheduled = this.checkPatternReplay(s, inst, now, result.bpm);
|
|
12565
|
+
for (const ev of scheduled) {
|
|
12566
|
+
newEvents.push(ev);
|
|
12567
|
+
s.envelope.trigger(1);
|
|
12568
|
+
s.envelopeSmoothed.trigger(1);
|
|
12569
|
+
}
|
|
12570
|
+
}
|
|
12571
|
+
}
|
|
12572
|
+
s.envelope.process(0, dtMs);
|
|
12573
|
+
s.envelopeSmoothed.process(0, dtMs);
|
|
12574
|
+
const envKey = inst;
|
|
12575
|
+
const smoothKey = `${inst}Smoothed`;
|
|
12576
|
+
result[envKey] = s.envelope.getValue();
|
|
12577
|
+
result[smoothKey] = s.envelopeSmoothed.getValue();
|
|
12578
|
+
}
|
|
12579
|
+
result.events.push(...newEvents);
|
|
12580
|
+
const anyMax = Math.max(
|
|
12581
|
+
result.kick,
|
|
12582
|
+
result.snare,
|
|
12583
|
+
result.hat
|
|
12584
|
+
);
|
|
12585
|
+
const anySmoothedMax = Math.max(
|
|
12586
|
+
result.kickSmoothed,
|
|
12587
|
+
result.snareSmoothed,
|
|
12588
|
+
result.hatSmoothed
|
|
12589
|
+
);
|
|
12590
|
+
if (anyMax > result.any) result.any = anyMax;
|
|
12591
|
+
if (anySmoothedMax > result.anySmoothed) result.anySmoothed = anySmoothedMax;
|
|
12592
|
+
return result;
|
|
12593
|
+
}
|
|
12594
|
+
// ---------------------------------------------------------------------------
|
|
12595
|
+
// Private helpers
|
|
12596
|
+
// ---------------------------------------------------------------------------
|
|
12597
|
+
/**
|
|
12598
|
+
* Handle a tap that arrives while already in pattern mode.
|
|
12599
|
+
* Matching taps refine the pattern via EMA and re-anchor phase.
|
|
12600
|
+
* Non-matching taps trigger new pattern detection on the full IOI history.
|
|
12601
|
+
*/
|
|
12602
|
+
handlePatternTap(instrument, ioi, now) {
|
|
12603
|
+
const s = this.state[instrument];
|
|
12604
|
+
if (!s.pattern) return;
|
|
12605
|
+
const matchedPos = this.findMatchingPosition(s, ioi);
|
|
12606
|
+
if (matchedPos >= 0) {
|
|
12607
|
+
s.refinementCounts[matchedPos] = (s.refinementCounts[matchedPos] || 0) + 1;
|
|
12608
|
+
const alpha = Math.max(MIN_EMA_ALPHA, 1 / s.refinementCounts[matchedPos]);
|
|
12609
|
+
s.pattern[matchedPos] = (1 - alpha) * s.pattern[matchedPos] + alpha * ioi;
|
|
12610
|
+
s.refinementIndex = (matchedPos + 1) % s.pattern.length;
|
|
12611
|
+
s.replayLastEventTime = now;
|
|
12612
|
+
s.replayIndex = s.refinementIndex;
|
|
12613
|
+
} else {
|
|
12614
|
+
const currentPattern = [...s.pattern];
|
|
12615
|
+
const newPattern = this.tryRecognizePattern(instrument);
|
|
12616
|
+
if (newPattern && !this.isPatternSame(newPattern, currentPattern)) {
|
|
12617
|
+
this.applyPattern(instrument, newPattern);
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12620
|
+
}
|
|
12621
|
+
/**
|
|
12622
|
+
* Find which pattern position the given IOI matches.
|
|
12623
|
+
* Tries the expected refinementIndex first, then scans all positions.
|
|
12624
|
+
* Returns the matched position index, or -1 if no match.
|
|
12625
|
+
*/
|
|
12626
|
+
findMatchingPosition(s, ioi) {
|
|
12627
|
+
if (!s.pattern) return -1;
|
|
12628
|
+
const expectedPos = s.refinementIndex % s.pattern.length;
|
|
12629
|
+
const expectedInterval = s.pattern[expectedPos];
|
|
12630
|
+
if (Math.abs(ioi - expectedInterval) / expectedInterval <= FUZZY_TOLERANCE) {
|
|
12631
|
+
return expectedPos;
|
|
12632
|
+
}
|
|
12633
|
+
let bestPos = -1;
|
|
12634
|
+
let bestDeviation = Infinity;
|
|
12635
|
+
for (let i = 0; i < s.pattern.length; i++) {
|
|
12636
|
+
if (i === expectedPos) continue;
|
|
12637
|
+
const deviation = Math.abs(ioi - s.pattern[i]) / s.pattern[i];
|
|
12638
|
+
if (deviation <= FUZZY_TOLERANCE && deviation < bestDeviation) {
|
|
12639
|
+
bestDeviation = deviation;
|
|
12640
|
+
bestPos = i;
|
|
12641
|
+
}
|
|
12642
|
+
}
|
|
12643
|
+
return bestPos;
|
|
12644
|
+
}
|
|
12645
|
+
/**
|
|
12646
|
+
* Compare two patterns for equivalence.
|
|
12647
|
+
* Same length and each interval within PATTERN_SAME_TOLERANCE → same.
|
|
12648
|
+
*/
|
|
12649
|
+
isPatternSame(a, b) {
|
|
12650
|
+
if (a.length !== b.length) return false;
|
|
12651
|
+
for (let i = 0; i < a.length; i++) {
|
|
12652
|
+
if (Math.abs(a[i] - b[i]) / Math.max(a[i], b[i]) > PATTERN_SAME_TOLERANCE) return false;
|
|
12653
|
+
}
|
|
12654
|
+
return true;
|
|
12655
|
+
}
|
|
12656
|
+
/**
|
|
12657
|
+
* Pattern recognition: find the shortest repeating IOI cycle
|
|
12658
|
+
* that has been tapped at least MIN_REPETITIONS times.
|
|
12659
|
+
* Returns the recognized pattern as float intervals, or null.
|
|
12660
|
+
* Does NOT mutate state — caller decides what to do with the result.
|
|
12661
|
+
*/
|
|
12662
|
+
tryRecognizePattern(instrument) {
|
|
12663
|
+
const s = this.state[instrument];
|
|
12664
|
+
const iois = s.tapIOIs;
|
|
12665
|
+
if (iois.length < MIN_REPETITIONS) return null;
|
|
12666
|
+
const maxL = Math.min(MAX_CYCLE_LENGTH, Math.floor(iois.length / MIN_REPETITIONS));
|
|
12667
|
+
for (let L = 1; L <= maxL; L++) {
|
|
12668
|
+
const needed = MIN_REPETITIONS * L;
|
|
12669
|
+
if (iois.length < needed) continue;
|
|
12670
|
+
const tolerance = L <= 2 ? FUZZY_TOLERANCE : FUZZY_TOLERANCE * (2 / L);
|
|
12671
|
+
const recent = iois.slice(-needed);
|
|
12672
|
+
const groups = [];
|
|
12673
|
+
for (let g = 0; g < MIN_REPETITIONS; g++) {
|
|
12674
|
+
groups.push(recent.slice(g * L, (g + 1) * L));
|
|
12675
|
+
}
|
|
12676
|
+
const avgGroup = groups[0].map((_, i) => {
|
|
12677
|
+
let sum = 0;
|
|
12678
|
+
for (const grp of groups) sum += grp[i];
|
|
12679
|
+
return sum / groups.length;
|
|
12680
|
+
});
|
|
12681
|
+
let allMatch = true;
|
|
12682
|
+
for (const grp of groups) {
|
|
12683
|
+
for (let i = 0; i < L; i++) {
|
|
12684
|
+
const deviation = Math.abs(grp[i] - avgGroup[i]) / avgGroup[i];
|
|
12685
|
+
if (deviation > tolerance) {
|
|
12686
|
+
allMatch = false;
|
|
12687
|
+
break;
|
|
12688
|
+
}
|
|
12689
|
+
}
|
|
12690
|
+
if (!allMatch) break;
|
|
12691
|
+
}
|
|
12692
|
+
if (allMatch) {
|
|
12693
|
+
return avgGroup;
|
|
12694
|
+
}
|
|
12695
|
+
}
|
|
12696
|
+
return null;
|
|
12697
|
+
}
|
|
12698
|
+
/**
|
|
12699
|
+
* Apply a recognized pattern to the instrument state.
|
|
12700
|
+
* Sets pattern, mode, and initializes replay anchor.
|
|
12701
|
+
*/
|
|
12702
|
+
applyPattern(instrument, pattern) {
|
|
12703
|
+
const s = this.state[instrument];
|
|
12704
|
+
s.pattern = pattern;
|
|
12705
|
+
s.mode = "pattern";
|
|
12706
|
+
s.refinementIndex = 0;
|
|
12707
|
+
s.refinementCounts = new Array(pattern.length).fill(MIN_REPETITIONS);
|
|
12708
|
+
if (s.replayLastEventTime === 0 || s.replayLastEventTime < s.lastTapTime) {
|
|
12709
|
+
s.replayLastEventTime = s.lastTapTime;
|
|
12710
|
+
s.replayIndex = 0;
|
|
12711
|
+
}
|
|
12712
|
+
}
|
|
12713
|
+
createTapEvent(instrument, time, bpm) {
|
|
12714
|
+
return {
|
|
12715
|
+
type: instrument,
|
|
12716
|
+
time,
|
|
12717
|
+
strength: 0.85,
|
|
12718
|
+
isPredicted: false,
|
|
12719
|
+
bpm
|
|
12720
|
+
};
|
|
12721
|
+
}
|
|
12722
|
+
checkPatternReplay(s, instrument, now, bpm) {
|
|
12723
|
+
if (!s.pattern || s.pattern.length === 0) return [];
|
|
12724
|
+
const events = [];
|
|
12725
|
+
const maxEventsPerFrame = 3;
|
|
12726
|
+
for (let safety = 0; safety < maxEventsPerFrame; safety++) {
|
|
12727
|
+
const expectedInterval = s.pattern[s.replayIndex % s.pattern.length];
|
|
12728
|
+
const elapsed = now - s.replayLastEventTime;
|
|
12729
|
+
if (elapsed >= expectedInterval) {
|
|
12730
|
+
s.replayLastEventTime += expectedInterval;
|
|
12731
|
+
s.replayIndex = (s.replayIndex + 1) % s.pattern.length;
|
|
12732
|
+
events.push({
|
|
12733
|
+
type: instrument,
|
|
12734
|
+
time: s.replayLastEventTime,
|
|
12735
|
+
strength: 0.8,
|
|
12736
|
+
isPredicted: true,
|
|
12737
|
+
bpm
|
|
12738
|
+
});
|
|
12739
|
+
} else {
|
|
12740
|
+
break;
|
|
12741
|
+
}
|
|
12742
|
+
}
|
|
12743
|
+
return events;
|
|
12744
|
+
}
|
|
12745
|
+
reset() {
|
|
12746
|
+
for (const inst of INSTRUMENTS) {
|
|
12747
|
+
this.clear(inst);
|
|
12748
|
+
}
|
|
12749
|
+
}
|
|
12750
|
+
}
|
|
12354
12751
|
class AudioSystem {
|
|
12355
12752
|
// Audio context and analysis nodes
|
|
12356
12753
|
audioContext = null;
|
|
@@ -12360,14 +12757,8 @@ class AudioSystem {
|
|
|
12360
12757
|
analysisMode = "analyser";
|
|
12361
12758
|
workletNode = null;
|
|
12362
12759
|
workletReady = false;
|
|
12760
|
+
workletRegistered = false;
|
|
12363
12761
|
currentSampleRate = 44100;
|
|
12364
|
-
pcmRing = null;
|
|
12365
|
-
pcmWriteIndex = 0;
|
|
12366
|
-
pcmFilled = 0;
|
|
12367
|
-
pcmHistorySeconds = 8;
|
|
12368
|
-
lastTempoExtraction = 0;
|
|
12369
|
-
tempoExtractionIntervalMs = 2e3;
|
|
12370
|
-
analysisBackend = "auto";
|
|
12371
12762
|
bandNoiseFloor = {
|
|
12372
12763
|
low: 1e-4,
|
|
12373
12764
|
lowMid: 1e-4,
|
|
@@ -12379,16 +12770,11 @@ class AudioSystem {
|
|
|
12379
12770
|
lastNonZeroBpm = 120;
|
|
12380
12771
|
/** Tracks which source provided the current BPM (pll | tempo | carry | default) */
|
|
12381
12772
|
lastBpmSource = "default";
|
|
12382
|
-
aubioWarningLogged = false;
|
|
12383
|
-
useEssentiaTempo = false;
|
|
12384
|
-
// disabled by default to avoid WASM exception in some builds
|
|
12385
12773
|
workletFrameCount = 0;
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12774
|
+
lastFrameTime = 0;
|
|
12775
|
+
stalenessTimer = null;
|
|
12776
|
+
static STALENESS_THRESHOLD_MS = 500;
|
|
12389
12777
|
analysisTicks = 0;
|
|
12390
|
-
lastLoopWarn = 0;
|
|
12391
|
-
verboseLabLogs = this.isAudioLab;
|
|
12392
12778
|
lastPhaseLogTime = 0;
|
|
12393
12779
|
onsetLogBuffer = [];
|
|
12394
12780
|
// Debug logging control
|
|
@@ -12417,21 +12803,14 @@ class AudioSystem {
|
|
|
12417
12803
|
};
|
|
12418
12804
|
/** Last dt used in analysis loop (ms) */
|
|
12419
12805
|
lastDtMs = 0;
|
|
12420
|
-
/** Flag to disable auto-gain for debugging */
|
|
12421
|
-
debugDisableAutoGain = false;
|
|
12422
12806
|
/** Flag to track if we've logged detection method (to avoid spam) */
|
|
12423
12807
|
bandNames = ["low", "lowMid", "mid", "highMid", "high"];
|
|
12424
12808
|
essentiaBandHistories = /* @__PURE__ */ new Map();
|
|
12425
12809
|
essentiaHistoryWindowMs = 5e3;
|
|
12426
|
-
//
|
|
12427
|
-
|
|
12428
|
-
manualBPM = null;
|
|
12429
|
-
tapHistory = [];
|
|
12430
|
-
tapTimeout = null;
|
|
12810
|
+
// Per-instrument onset tap manager
|
|
12811
|
+
onsetTapManager;
|
|
12431
12812
|
// Envelope followers for smooth energy curves
|
|
12432
12813
|
envelopeFollowers;
|
|
12433
|
-
// Global sensitivity multiplier
|
|
12434
|
-
sensitivity = 1;
|
|
12435
12814
|
// Feature enable flags
|
|
12436
12815
|
beatDetectionEnabled = true;
|
|
12437
12816
|
onsetDetectionEnabled = true;
|
|
@@ -12446,13 +12825,9 @@ class AudioSystem {
|
|
|
12446
12825
|
this.tempoInduction.setDebugMode(enabled);
|
|
12447
12826
|
this.stateManager.setDebugMode(enabled);
|
|
12448
12827
|
this.essentiaOnsetDetection?.setDebugMode(enabled);
|
|
12449
|
-
this.toggleStatusLogger(enabled);
|
|
12450
|
-
this.verboseLabLogs = this.isAudioLab || this.debugMode;
|
|
12451
12828
|
if (enabled) {
|
|
12452
12829
|
this.diagnosticLogger.start();
|
|
12453
|
-
|
|
12454
|
-
if (enabled) {
|
|
12455
|
-
console.log("🔬 [AudioSystem] Debug mode enabled");
|
|
12830
|
+
this.debugLog("[AudioSystem] Debug mode enabled");
|
|
12456
12831
|
}
|
|
12457
12832
|
}
|
|
12458
12833
|
/**
|
|
@@ -12490,42 +12865,6 @@ class AudioSystem {
|
|
|
12490
12865
|
}
|
|
12491
12866
|
return clone;
|
|
12492
12867
|
}
|
|
12493
|
-
logVerbose(label, payload) {
|
|
12494
|
-
if (this.debugMode || this.verboseLabLogs) {
|
|
12495
|
-
console.log(label, payload);
|
|
12496
|
-
}
|
|
12497
|
-
}
|
|
12498
|
-
/**
|
|
12499
|
-
* Start/stop periodic status logging. If maxSamples provided, stops after that many ticks.
|
|
12500
|
-
*/
|
|
12501
|
-
toggleStatusLogger(enabled, maxSamples) {
|
|
12502
|
-
if (enabled && !this.statusLogTimer) {
|
|
12503
|
-
let samples = 0;
|
|
12504
|
-
this.statusLogTimer = setInterval(() => {
|
|
12505
|
-
samples++;
|
|
12506
|
-
const a = this.audioState;
|
|
12507
|
-
const maxBand = Math.max(a.bands.low, a.bands.lowMid, a.bands.mid, a.bands.highMid, a.bands.high);
|
|
12508
|
-
console.log("[AudioSystem][status]", {
|
|
12509
|
-
mode: this.analysisMode,
|
|
12510
|
-
workletReady: this.workletReady,
|
|
12511
|
-
workletFrames: this.workletFrameCount,
|
|
12512
|
-
isConnected: a.isConnected,
|
|
12513
|
-
vol: { cur: a.volume.current.toFixed(3), peak: a.volume.peak.toFixed(3) },
|
|
12514
|
-
maxBand: maxBand.toFixed(3),
|
|
12515
|
-
bpm: a.beat.bpm.toFixed(1),
|
|
12516
|
-
bpmSource: this.lastBpmSource,
|
|
12517
|
-
locked: a.beat.isLocked,
|
|
12518
|
-
events: a.beat.events.length
|
|
12519
|
-
});
|
|
12520
|
-
if (maxSamples && samples >= maxSamples) {
|
|
12521
|
-
this.toggleStatusLogger(false);
|
|
12522
|
-
}
|
|
12523
|
-
}, 1e3);
|
|
12524
|
-
} else if (!enabled && this.statusLogTimer) {
|
|
12525
|
-
clearInterval(this.statusLogTimer);
|
|
12526
|
-
this.statusLogTimer = null;
|
|
12527
|
-
}
|
|
12528
|
-
}
|
|
12529
12868
|
/**
|
|
12530
12869
|
* Handle frames pushed from AudioWorklet
|
|
12531
12870
|
*/
|
|
@@ -12539,8 +12878,9 @@ class AudioSystem {
|
|
|
12539
12878
|
*/
|
|
12540
12879
|
analyzeFrame(frame, sampleRate, timestampMs) {
|
|
12541
12880
|
if (!frame || frame.length === 0) return;
|
|
12881
|
+
this.lastFrameTime = performance.now();
|
|
12542
12882
|
if (this.workletFrameCount === 1) {
|
|
12543
|
-
|
|
12883
|
+
this.debugLog("[AudioSystem] First frame received", { sampleRate, mode: this.analysisMode });
|
|
12544
12884
|
}
|
|
12545
12885
|
const dtMs = this.lastAnalysisTimestamp > 0 ? timestampMs - this.lastAnalysisTimestamp : 1e3 / 60;
|
|
12546
12886
|
this.lastAnalysisTimestamp = timestampMs;
|
|
@@ -12549,15 +12889,11 @@ class AudioSystem {
|
|
|
12549
12889
|
const { rms, peak } = this.calculateVolumeMetrics(frame);
|
|
12550
12890
|
this.audioState.volume.current = rms;
|
|
12551
12891
|
this.audioState.volume.peak = peak;
|
|
12552
|
-
this.
|
|
12553
|
-
if (this.useEssentiaTempo && timestampMs - this.lastTempoExtraction > this.tempoExtractionIntervalMs) {
|
|
12554
|
-
this.runEssentiaTempoEstimate(sampleRate);
|
|
12555
|
-
this.lastTempoExtraction = timestampMs;
|
|
12556
|
-
}
|
|
12892
|
+
this.lastWaveformFrame = frame;
|
|
12557
12893
|
const fftResult = this.computeFFT(frame);
|
|
12558
12894
|
if (!fftResult) {
|
|
12559
|
-
if (this.analysisTicks === 1 || this.analysisTicks % 240 === 0) {
|
|
12560
|
-
|
|
12895
|
+
if (this.debugMode && (this.analysisTicks === 1 || this.analysisTicks % 240 === 0)) {
|
|
12896
|
+
this.debugLog("[AudioSystem][analyzeFrame] computeFFT returned null at tick", this.analysisTicks);
|
|
12561
12897
|
}
|
|
12562
12898
|
return;
|
|
12563
12899
|
}
|
|
@@ -12578,80 +12914,76 @@ class AudioSystem {
|
|
|
12578
12914
|
high: 0
|
|
12579
12915
|
};
|
|
12580
12916
|
this.rawBandsPreGain = { low: 0, lowMid: 0, mid: 0, highMid: 0, high: 0 };
|
|
12917
|
+
this.audioState.spectral = { brightness: 0, flatness: 0 };
|
|
12918
|
+
const decayed = this.stateManager.processEnvelopeDecay(dtMs);
|
|
12919
|
+
this.audioState.beat.kick = decayed.kick;
|
|
12920
|
+
this.audioState.beat.snare = decayed.snare;
|
|
12921
|
+
this.audioState.beat.hat = decayed.hat;
|
|
12922
|
+
this.audioState.beat.any = decayed.any;
|
|
12923
|
+
this.audioState.beat.kickSmoothed = decayed.kickSmoothed;
|
|
12924
|
+
this.audioState.beat.snareSmoothed = decayed.snareSmoothed;
|
|
12925
|
+
this.audioState.beat.hatSmoothed = decayed.hatSmoothed;
|
|
12926
|
+
this.audioState.beat.anySmoothed = decayed.anySmoothed;
|
|
12927
|
+
this.audioState.beat.events = [];
|
|
12581
12928
|
this.updateSmoothBands(dtMs);
|
|
12582
12929
|
this.sendAnalysisResultsToWorker();
|
|
12583
12930
|
return;
|
|
12584
12931
|
}
|
|
12585
|
-
const rawBandsForOnsets = {
|
|
12586
|
-
low: bandEnergies.low * this.sensitivity,
|
|
12587
|
-
lowMid: bandEnergies.lowMid * this.sensitivity,
|
|
12588
|
-
mid: bandEnergies.mid * this.sensitivity,
|
|
12589
|
-
highMid: bandEnergies.highMid * this.sensitivity,
|
|
12590
|
-
high: bandEnergies.high * this.sensitivity
|
|
12591
|
-
};
|
|
12592
12932
|
this.rawBandsPreGain = { ...bandEnergies };
|
|
12593
12933
|
if (this.autoGainEnabled) {
|
|
12594
12934
|
this.applyAutoGain();
|
|
12595
12935
|
}
|
|
12596
12936
|
this.calculateSpectralFeaturesFromMagnitude(magnitudes, sampleRate, maxMagnitude);
|
|
12597
12937
|
if (this.onsetDetectionEnabled && this.beatDetectionEnabled) {
|
|
12598
|
-
const beatState = this.runBeatPipeline(
|
|
12938
|
+
const beatState = this.runBeatPipeline(bandEnergies, dtMs, timestampMs, sampleRate);
|
|
12599
12939
|
if (beatState) {
|
|
12600
|
-
|
|
12601
|
-
const
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
trackingData.bpmVariance = bpmVariance;
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
trackingData.
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
lowMid: this.audioState.bands.lowMid,
|
|
12648
|
-
mid: this.audioState.bands.mid,
|
|
12649
|
-
highMid: this.audioState.bands.highMid,
|
|
12650
|
-
high: this.audioState.bands.high
|
|
12651
|
-
},
|
|
12652
|
-
this.stateManager.getLastRejections(),
|
|
12653
|
-
trackingData
|
|
12654
|
-
);
|
|
12940
|
+
const { phase: _phase, bar: _bar, debug: _debug, ...beatForState } = beatState;
|
|
12941
|
+
const now = performance.now();
|
|
12942
|
+
this.audioState.beat = this.onsetTapManager.processFrame(beatForState, now, dtMs);
|
|
12943
|
+
if (this.debugMode) {
|
|
12944
|
+
const pllState = this.pll.getState();
|
|
12945
|
+
const currentState = this.stateManager.getState();
|
|
12946
|
+
const trackingData = {
|
|
12947
|
+
timeSinceKick: this.stateManager.getTimeSinceLastKick(),
|
|
12948
|
+
phaseError: this.pll.getLastPhaseError(),
|
|
12949
|
+
adaptiveFloors: this.stateManager.getAdaptiveFloors()
|
|
12950
|
+
};
|
|
12951
|
+
const kickPattern = this.stateManager.getKickPattern();
|
|
12952
|
+
if (kickPattern !== void 0) trackingData.kickPattern = kickPattern;
|
|
12953
|
+
const bpmVariance = this.stateManager.getBPMVariance();
|
|
12954
|
+
if (bpmVariance !== void 0) trackingData.bpmVariance = bpmVariance;
|
|
12955
|
+
const compressed = this.stateManager.isCompressed();
|
|
12956
|
+
if (compressed) trackingData.compressed = compressed;
|
|
12957
|
+
if (beatState.events.some((e) => e.type === "kick")) {
|
|
12958
|
+
trackingData.kickPhase = pllState.phase;
|
|
12959
|
+
}
|
|
12960
|
+
if (beatState.debug?.pllGain !== void 0) trackingData.pllGain = beatState.debug.pllGain;
|
|
12961
|
+
if (beatState.debug?.tempoConfidence !== void 0) trackingData.tempoConf = beatState.debug.tempoConfidence;
|
|
12962
|
+
if (beatState.debug?.trackingConfidence !== void 0) trackingData.trackingConf = beatState.debug.trackingConfidence;
|
|
12963
|
+
this.diagnosticLogger.logFrame(
|
|
12964
|
+
timestampMs,
|
|
12965
|
+
{
|
|
12966
|
+
bpm: beatState.bpm,
|
|
12967
|
+
phase: pllState.phase,
|
|
12968
|
+
bar: pllState.bar,
|
|
12969
|
+
confidence: beatState.confidence,
|
|
12970
|
+
kick: beatState.events.some((e) => e.type === "kick"),
|
|
12971
|
+
snare: beatState.events.some((e) => e.type === "snare"),
|
|
12972
|
+
hat: beatState.events.some((e) => e.type === "hat"),
|
|
12973
|
+
isLocked: currentState === "LOCKED",
|
|
12974
|
+
state: currentState
|
|
12975
|
+
},
|
|
12976
|
+
{
|
|
12977
|
+
low: this.audioState.bands.low,
|
|
12978
|
+
lowMid: this.audioState.bands.lowMid,
|
|
12979
|
+
mid: this.audioState.bands.mid,
|
|
12980
|
+
highMid: this.audioState.bands.highMid,
|
|
12981
|
+
high: this.audioState.bands.high
|
|
12982
|
+
},
|
|
12983
|
+
this.stateManager.getLastRejections(),
|
|
12984
|
+
trackingData
|
|
12985
|
+
);
|
|
12986
|
+
}
|
|
12655
12987
|
}
|
|
12656
12988
|
}
|
|
12657
12989
|
this.updateSmoothBands(dtMs);
|
|
@@ -12661,30 +12993,9 @@ class AudioSystem {
|
|
|
12661
12993
|
* Compute FFT and derived arrays
|
|
12662
12994
|
*/
|
|
12663
12995
|
computeFFT(frame) {
|
|
12664
|
-
if (this.analysisTicks === 1) {
|
|
12665
|
-
console.log("[AudioSystem][computeFFT] FIRST CALL", {
|
|
12666
|
-
hasEngine: !!this.fftEngine,
|
|
12667
|
-
hasInput: !!this.fftInput,
|
|
12668
|
-
hasOutput: !!this.fftOutput,
|
|
12669
|
-
hasWindow: !!this.hannWindow,
|
|
12670
|
-
hasFreqData: !!this.frequencyData,
|
|
12671
|
-
hasMagnitude: !!this.fftMagnitude,
|
|
12672
|
-
hasMagnitudeDb: !!this.fftMagnitudeDb,
|
|
12673
|
-
hasPhase: !!this.fftPhase
|
|
12674
|
-
});
|
|
12675
|
-
}
|
|
12676
12996
|
if (!this.fftEngine || !this.fftInput || !this.fftOutput || !this.hannWindow || !this.frequencyData || !this.fftMagnitude || !this.fftMagnitudeDb || !this.fftPhase) {
|
|
12677
|
-
if (this.analysisTicks === 1 || this.analysisTicks % 240 === 0) {
|
|
12678
|
-
|
|
12679
|
-
console.warn(" tick:", this.analysisTicks);
|
|
12680
|
-
console.warn(" hasEngine:", !!this.fftEngine);
|
|
12681
|
-
console.warn(" hasInput:", !!this.fftInput);
|
|
12682
|
-
console.warn(" hasOutput:", !!this.fftOutput);
|
|
12683
|
-
console.warn(" hasWindow:", !!this.hannWindow);
|
|
12684
|
-
console.warn(" hasFreqData:", !!this.frequencyData);
|
|
12685
|
-
console.warn(" hasMagnitude:", !!this.fftMagnitude);
|
|
12686
|
-
console.warn(" hasMagnitudeDb:", !!this.fftMagnitudeDb);
|
|
12687
|
-
console.warn(" hasPhase:", !!this.fftPhase);
|
|
12997
|
+
if (this.debugMode && (this.analysisTicks === 1 || this.analysisTicks % 240 === 0)) {
|
|
12998
|
+
this.debugLog("[AudioSystem][computeFFT] RETURNING NULL - resources missing at tick", this.analysisTicks);
|
|
12688
12999
|
}
|
|
12689
13000
|
return null;
|
|
12690
13001
|
}
|
|
@@ -12841,63 +13152,13 @@ class AudioSystem {
|
|
|
12841
13152
|
} else {
|
|
12842
13153
|
this.audioState.spectral.flatness = 0;
|
|
12843
13154
|
}
|
|
12844
|
-
const centroidChange = Math.abs(centroid - this.prevSpectralCentroid);
|
|
12845
|
-
this.audioState.spectral.flux = Math.min(1, centroidChange / 1e3);
|
|
12846
|
-
this.prevSpectralCentroid = centroid;
|
|
12847
|
-
}
|
|
12848
|
-
/**
|
|
12849
|
-
* Append PCM frame to ring buffer for periodic Essentia tempo extraction
|
|
12850
|
-
*/
|
|
12851
|
-
appendPcmFrame(frame, sampleRate) {
|
|
12852
|
-
const requiredSize = Math.max(this.fftSize, Math.floor(sampleRate * this.pcmHistorySeconds));
|
|
12853
|
-
if (!this.pcmRing || this.pcmRing.length !== requiredSize) {
|
|
12854
|
-
this.pcmRing = new Float32Array(requiredSize);
|
|
12855
|
-
this.pcmWriteIndex = 0;
|
|
12856
|
-
this.pcmFilled = 0;
|
|
12857
|
-
}
|
|
12858
|
-
for (let i = 0; i < frame.length; i++) {
|
|
12859
|
-
this.pcmRing[this.pcmWriteIndex] = frame[i];
|
|
12860
|
-
this.pcmWriteIndex = (this.pcmWriteIndex + 1) % this.pcmRing.length;
|
|
12861
|
-
if (this.pcmFilled < this.pcmRing.length) {
|
|
12862
|
-
this.pcmFilled++;
|
|
12863
|
-
}
|
|
12864
|
-
}
|
|
12865
|
-
}
|
|
12866
|
-
/**
|
|
12867
|
-
* Periodically estimate tempo using Essentia's RhythmExtractor2013 (offline chunk)
|
|
12868
|
-
*/
|
|
12869
|
-
runEssentiaTempoEstimate(sampleRate) {
|
|
12870
|
-
if (!this.essentiaOnsetDetection?.isReady() || !this.pcmRing) {
|
|
12871
|
-
return;
|
|
12872
|
-
}
|
|
12873
|
-
const maxSamples = Math.min(this.pcmRing.length, Math.floor(sampleRate * this.pcmHistorySeconds));
|
|
12874
|
-
const available = Math.min(this.pcmFilled, maxSamples);
|
|
12875
|
-
if (available < sampleRate * 2) return;
|
|
12876
|
-
const window2 = new Float32Array(available);
|
|
12877
|
-
let idx = (this.pcmWriteIndex - available + this.pcmRing.length) % this.pcmRing.length;
|
|
12878
|
-
for (let i = 0; i < available; i++) {
|
|
12879
|
-
window2[i] = this.pcmRing[idx];
|
|
12880
|
-
idx = (idx + 1) % this.pcmRing.length;
|
|
12881
|
-
}
|
|
12882
|
-
const tempo = this.essentiaOnsetDetection.detectTempoFromPCM(window2, sampleRate);
|
|
12883
|
-
if (tempo?.bpm) {
|
|
12884
|
-
const bpm = Math.max(60, Math.min(200, tempo.bpm));
|
|
12885
|
-
this.pll.setBPM(bpm);
|
|
12886
|
-
this.tempoInduction.setManualBPM(bpm);
|
|
12887
|
-
}
|
|
12888
13155
|
}
|
|
12889
13156
|
/**
|
|
12890
13157
|
* Run onset + beat detection pipeline and return BeatState
|
|
12891
13158
|
*/
|
|
12892
13159
|
runBeatPipeline(rawBandsForOnsets, dtMs, timestampMs, sampleRate) {
|
|
12893
13160
|
let onsets;
|
|
12894
|
-
const
|
|
12895
|
-
const allowEssentia = backend === "auto" || backend === "essentia";
|
|
12896
|
-
if (backend === "aubio" && !this.aubioWarningLogged) {
|
|
12897
|
-
console.warn("[AudioSystem] Aubio backend selected but not wired yet; using custom pipeline.");
|
|
12898
|
-
this.aubioWarningLogged = true;
|
|
12899
|
-
}
|
|
12900
|
-
const essentiaReady = allowEssentia && (this.essentiaOnsetDetection?.isReady() || false) && !!this.fftMagnitudeDb && !!this.fftPhase;
|
|
13161
|
+
const essentiaReady = (this.essentiaOnsetDetection?.isReady() || false) && !!this.fftMagnitudeDb && !!this.fftPhase;
|
|
12901
13162
|
const hasFftData = !!(this.fftMagnitudeDb && this.fftPhase);
|
|
12902
13163
|
if (essentiaReady && hasFftData) {
|
|
12903
13164
|
const essentiaResult = this.essentiaOnsetDetection.detectFromSpectrum(
|
|
@@ -12920,9 +13181,6 @@ class AudioSystem {
|
|
|
12920
13181
|
} else {
|
|
12921
13182
|
onsets = this.onsetDetection.detect(rawBandsForOnsets, dtMs);
|
|
12922
13183
|
}
|
|
12923
|
-
if (this.beatMode === "manual" && this.manualBPM !== null) {
|
|
12924
|
-
this.tempoInduction.setManualBPM(this.manualBPM);
|
|
12925
|
-
}
|
|
12926
13184
|
const tempo = this.tempoInduction.update(onsets, dtMs);
|
|
12927
13185
|
this.onsetDetection.setBPM(tempo.bpm);
|
|
12928
13186
|
const trackingConf = this.stateManager.calculateTrackingConfidence(tempo);
|
|
@@ -13001,7 +13259,7 @@ class AudioSystem {
|
|
|
13001
13259
|
const prevSource = this.lastBpmSource;
|
|
13002
13260
|
this.lastNonZeroBpm = resolvedBpm;
|
|
13003
13261
|
this.lastBpmSource = selected.source;
|
|
13004
|
-
if (
|
|
13262
|
+
if (this.debugMode && prevSource !== this.lastBpmSource) {
|
|
13005
13263
|
console.log(`[AudioSystem][bpm] Source changed: ${prevSource} -> ${this.lastBpmSource} (resolved=${resolvedBpm.toFixed(2)}, pll=${beatState.bpm?.toFixed?.(2) ?? beatState.bpm}, tempo=${tempo.bpm?.toFixed?.(2) ?? tempo.bpm})`);
|
|
13006
13264
|
}
|
|
13007
13265
|
const mergedBeatState = beatState.bpm === resolvedBpm ? beatState : { ...beatState, bpm: resolvedBpm };
|
|
@@ -13019,8 +13277,6 @@ class AudioSystem {
|
|
|
13019
13277
|
// Analysis configuration (optimized for onset detection)
|
|
13020
13278
|
fftSize = 2048;
|
|
13021
13279
|
// Good balance for quality vs performance
|
|
13022
|
-
smoothingTimeConstant = 0;
|
|
13023
|
-
// NO smoothing for onset detection (was 0.5 which killed transients!)
|
|
13024
13280
|
// High-speed analysis for onset detection (separate from RAF)
|
|
13025
13281
|
analysisInterval = null;
|
|
13026
13282
|
analysisIntervalMs = 8;
|
|
@@ -13063,28 +13319,26 @@ class AudioSystem {
|
|
|
13063
13319
|
any: 0,
|
|
13064
13320
|
kickSmoothed: 0,
|
|
13065
13321
|
snareSmoothed: 0,
|
|
13322
|
+
hatSmoothed: 0,
|
|
13066
13323
|
anySmoothed: 0,
|
|
13067
13324
|
events: [],
|
|
13068
13325
|
bpm: 120,
|
|
13069
|
-
phase: 0,
|
|
13070
|
-
bar: 0,
|
|
13071
13326
|
confidence: 0,
|
|
13072
13327
|
isLocked: false
|
|
13073
13328
|
},
|
|
13074
13329
|
spectral: {
|
|
13075
13330
|
brightness: 0,
|
|
13076
|
-
flatness: 0
|
|
13077
|
-
flux: 0
|
|
13331
|
+
flatness: 0
|
|
13078
13332
|
}
|
|
13079
13333
|
};
|
|
13334
|
+
// Waveform data for transfer to worker
|
|
13335
|
+
lastWaveformFrame = null;
|
|
13080
13336
|
// Analysis loop
|
|
13081
13337
|
analysisLoopId = null;
|
|
13082
13338
|
isAnalysisRunning = false;
|
|
13083
13339
|
lastAnalysisTimestamp = 0;
|
|
13084
13340
|
// Callback to send results to worker
|
|
13085
13341
|
sendAnalysisResults = null;
|
|
13086
|
-
// Previous spectral centroid for change calculation
|
|
13087
|
-
prevSpectralCentroid = 0;
|
|
13088
13342
|
constructor(sendAnalysisResultsCallback) {
|
|
13089
13343
|
this.handleAudioStreamUpdate = this.handleAudioStreamUpdate.bind(this);
|
|
13090
13344
|
this.performAnalysis = this.performAnalysis.bind(this);
|
|
@@ -13095,6 +13349,7 @@ class AudioSystem {
|
|
|
13095
13349
|
this.tempoInduction = new TempoInduction();
|
|
13096
13350
|
this.pll = new PhaseLockedLoop();
|
|
13097
13351
|
this.stateManager = new BeatStateManager();
|
|
13352
|
+
this.onsetTapManager = new OnsetTapManager();
|
|
13098
13353
|
this.volumeAutoGain = new AutoGain(3e3, 60);
|
|
13099
13354
|
this.bandAutoGain = {
|
|
13100
13355
|
low: new AutoGain(3e3, 60),
|
|
@@ -13112,6 +13367,7 @@ class AudioSystem {
|
|
|
13112
13367
|
any: new EnvelopeFollower(0, 300, sampleRate),
|
|
13113
13368
|
kickSmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13114
13369
|
snareSmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13370
|
+
hatSmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13115
13371
|
anySmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13116
13372
|
// Volume smoothing
|
|
13117
13373
|
volumeSmoothed: new EnvelopeFollower(50, 200, sampleRate),
|
|
@@ -13124,10 +13380,6 @@ class AudioSystem {
|
|
|
13124
13380
|
};
|
|
13125
13381
|
this.refreshFFTResources();
|
|
13126
13382
|
this.resetEssentiaBandHistories();
|
|
13127
|
-
if (this.isAudioLab) {
|
|
13128
|
-
this.forceAnalyser = true;
|
|
13129
|
-
console.warn("[AudioSystem] Audio-lab context detected: forcing analyser path for stability.");
|
|
13130
|
-
}
|
|
13131
13383
|
}
|
|
13132
13384
|
/**
|
|
13133
13385
|
* Prepare FFT buffers and windowing for the selected fftSize
|
|
@@ -13163,9 +13415,7 @@ class AudioSystem {
|
|
|
13163
13415
|
if (!this.essentiaOnsetDetection) return;
|
|
13164
13416
|
try {
|
|
13165
13417
|
await this.essentiaOnsetDetection.initialize();
|
|
13166
|
-
|
|
13167
|
-
console.log("✅ [AudioSystem] Essentia.js initialized successfully!");
|
|
13168
|
-
}
|
|
13418
|
+
this.debugLog("[AudioSystem] Essentia.js initialized successfully");
|
|
13169
13419
|
} catch (error) {
|
|
13170
13420
|
console.warn("⚠️ [AudioSystem] Essentia.js initialization failed, using custom onset detection:", error);
|
|
13171
13421
|
}
|
|
@@ -13195,9 +13445,6 @@ class AudioSystem {
|
|
|
13195
13445
|
} else {
|
|
13196
13446
|
this.disconnectAudioStream();
|
|
13197
13447
|
}
|
|
13198
|
-
if (data.analysisConfig) {
|
|
13199
|
-
this.updateAnalysisConfig(data.analysisConfig);
|
|
13200
|
-
}
|
|
13201
13448
|
} catch (error) {
|
|
13202
13449
|
console.error("Error handling audio stream update:", error);
|
|
13203
13450
|
this.audioState.isConnected = false;
|
|
@@ -13228,7 +13475,7 @@ class AudioSystem {
|
|
|
13228
13475
|
}
|
|
13229
13476
|
}
|
|
13230
13477
|
this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioStream);
|
|
13231
|
-
this.workletReady =
|
|
13478
|
+
this.workletReady = await this.setupAudioWorklet();
|
|
13232
13479
|
if (this.workletReady && this.workletNode) {
|
|
13233
13480
|
this.analysisMode = "worklet";
|
|
13234
13481
|
this.currentSampleRate = this.audioContext.sampleRate;
|
|
@@ -13237,7 +13484,7 @@ class AudioSystem {
|
|
|
13237
13484
|
this.debugLog("Audio worklet analysis enabled");
|
|
13238
13485
|
setTimeout(() => {
|
|
13239
13486
|
if (this.analysisMode === "worklet" && this.workletFrameCount === 0) {
|
|
13240
|
-
|
|
13487
|
+
this.debugLog("[AudioSystem] Worklet produced no frames, falling back to analyser.");
|
|
13241
13488
|
this.analysisMode = "analyser";
|
|
13242
13489
|
if (this.workletNode) {
|
|
13243
13490
|
try {
|
|
@@ -13250,7 +13497,7 @@ class AudioSystem {
|
|
|
13250
13497
|
this.workletReady = false;
|
|
13251
13498
|
this.analyser = this.audioContext.createAnalyser();
|
|
13252
13499
|
this.analyser.fftSize = this.fftSize;
|
|
13253
|
-
this.analyser.smoothingTimeConstant =
|
|
13500
|
+
this.analyser.smoothingTimeConstant = 0;
|
|
13254
13501
|
this.mediaStreamSource.connect(this.analyser);
|
|
13255
13502
|
bufferLength = this.analyser.frequencyBinCount;
|
|
13256
13503
|
this.frequencyData = new Uint8Array(bufferLength);
|
|
@@ -13262,7 +13509,7 @@ class AudioSystem {
|
|
|
13262
13509
|
this.analysisMode = "analyser";
|
|
13263
13510
|
this.analyser = this.audioContext.createAnalyser();
|
|
13264
13511
|
this.analyser.fftSize = this.fftSize;
|
|
13265
|
-
this.analyser.smoothingTimeConstant =
|
|
13512
|
+
this.analyser.smoothingTimeConstant = 0;
|
|
13266
13513
|
this.mediaStreamSource.connect(this.analyser);
|
|
13267
13514
|
this.debugLog("Audio worklet unavailable, using analyser fallback");
|
|
13268
13515
|
bufferLength = this.analyser.frequencyBinCount;
|
|
@@ -13278,17 +13525,17 @@ class AudioSystem {
|
|
|
13278
13525
|
this.startAnalysisLoop();
|
|
13279
13526
|
}
|
|
13280
13527
|
if (this.analysisMode === "analyser" && !this.isAnalysisRunning) {
|
|
13281
|
-
|
|
13528
|
+
this.debugLog("[AudioSystem] Analysis loop not running after setup, forcing start.");
|
|
13282
13529
|
this.startAnalysisLoop();
|
|
13283
13530
|
}
|
|
13284
13531
|
const tracks = audioStream.getAudioTracks();
|
|
13285
|
-
this.
|
|
13532
|
+
this.debugLog("[AudioSystem] Stream info", {
|
|
13286
13533
|
trackCount: tracks.length,
|
|
13287
13534
|
trackSettings: tracks.map((t) => t.getSettings?.() || {}),
|
|
13288
13535
|
trackMuted: tracks.map((t) => t.muted),
|
|
13289
|
-
forceAnalyser: this.forceAnalyser,
|
|
13290
13536
|
mode: this.analysisMode
|
|
13291
13537
|
});
|
|
13538
|
+
this.startStalenessTimer();
|
|
13292
13539
|
this.debugLog("Audio stream connected successfully (host-side)", {
|
|
13293
13540
|
sampleRate: this.audioContext.sampleRate,
|
|
13294
13541
|
fftSize: this.fftSize,
|
|
@@ -13306,6 +13553,7 @@ class AudioSystem {
|
|
|
13306
13553
|
*/
|
|
13307
13554
|
disconnectAudioStream() {
|
|
13308
13555
|
this.stopAnalysisLoop();
|
|
13556
|
+
this.stopStalenessTimer();
|
|
13309
13557
|
this.resetEssentiaBandHistories();
|
|
13310
13558
|
if (this.mediaStreamSource) {
|
|
13311
13559
|
this.mediaStreamSource.disconnect();
|
|
@@ -13336,27 +13584,6 @@ class AudioSystem {
|
|
|
13336
13584
|
this.sendAnalysisResultsToWorker();
|
|
13337
13585
|
this.debugLog("Audio stream disconnected (host-side)");
|
|
13338
13586
|
}
|
|
13339
|
-
/**
|
|
13340
|
-
* Update analysis configuration
|
|
13341
|
-
*/
|
|
13342
|
-
updateAnalysisConfig(config) {
|
|
13343
|
-
let needsReconnect = false;
|
|
13344
|
-
if (config.fftSize && config.fftSize !== this.fftSize) {
|
|
13345
|
-
this.fftSize = config.fftSize;
|
|
13346
|
-
this.refreshFFTResources();
|
|
13347
|
-
needsReconnect = true;
|
|
13348
|
-
}
|
|
13349
|
-
if (config.smoothing !== void 0) {
|
|
13350
|
-
this.smoothingTimeConstant = config.smoothing;
|
|
13351
|
-
if (this.analyser) {
|
|
13352
|
-
this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
|
|
13353
|
-
}
|
|
13354
|
-
}
|
|
13355
|
-
if (needsReconnect && this.currentStream) {
|
|
13356
|
-
const stream = this.currentStream;
|
|
13357
|
-
this.setAudioStream(stream);
|
|
13358
|
-
}
|
|
13359
|
-
}
|
|
13360
13587
|
/**
|
|
13361
13588
|
* Initialize audio worklet for high-quality capture (complex STFT path)
|
|
13362
13589
|
*/
|
|
@@ -13365,50 +13592,54 @@ class AudioSystem {
|
|
|
13365
13592
|
return false;
|
|
13366
13593
|
}
|
|
13367
13594
|
try {
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
process(inputs, outputs) {
|
|
13379
|
-
const input = inputs[0];
|
|
13380
|
-
const output = outputs[0];
|
|
13381
|
-
const channel = input && input[0];
|
|
13382
|
-
if (output) {
|
|
13383
|
-
for (let ch = 0; ch < output.length; ch++) {
|
|
13384
|
-
output[ch].fill(0);
|
|
13385
|
-
}
|
|
13595
|
+
if (!this.workletRegistered) {
|
|
13596
|
+
const workletSource = `
|
|
13597
|
+
class AudioAnalysisProcessor extends AudioWorkletProcessor {
|
|
13598
|
+
constructor(options) {
|
|
13599
|
+
super();
|
|
13600
|
+
const opts = (options && options.processorOptions) || {};
|
|
13601
|
+
this.windowSize = opts.fftSize || 2048;
|
|
13602
|
+
this.hopSize = opts.hopSize || this.windowSize / 2;
|
|
13603
|
+
this.buffer = new Float32Array(this.windowSize);
|
|
13604
|
+
this.writeIndex = 0;
|
|
13386
13605
|
}
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
{ type: 'audio-frame', samples: frame, sampleRate, timestamp: currentTime * 1000 },
|
|
13395
|
-
[frame.buffer]
|
|
13396
|
-
);
|
|
13397
|
-
const overlap = this.windowSize - this.hopSize;
|
|
13398
|
-
if (overlap > 0) {
|
|
13399
|
-
this.buffer.set(this.buffer.subarray(this.hopSize, this.windowSize), 0);
|
|
13606
|
+
process(inputs, outputs) {
|
|
13607
|
+
const input = inputs[0];
|
|
13608
|
+
const output = outputs[0];
|
|
13609
|
+
const channel = input && input[0];
|
|
13610
|
+
if (output) {
|
|
13611
|
+
for (let ch = 0; ch < output.length; ch++) {
|
|
13612
|
+
output[ch].fill(0);
|
|
13400
13613
|
}
|
|
13401
|
-
this.writeIndex = overlap > 0 ? overlap : 0;
|
|
13402
13614
|
}
|
|
13615
|
+
if (!channel) return true;
|
|
13616
|
+
for (let i = 0; i < channel.length; i++) {
|
|
13617
|
+
this.buffer[this.writeIndex++] = channel[i];
|
|
13618
|
+
if (this.writeIndex >= this.windowSize) {
|
|
13619
|
+
const frame = new Float32Array(this.windowSize);
|
|
13620
|
+
frame.set(this.buffer);
|
|
13621
|
+
this.port.postMessage(
|
|
13622
|
+
{ type: 'audio-frame', samples: frame, sampleRate, timestamp: currentTime * 1000 },
|
|
13623
|
+
[frame.buffer]
|
|
13624
|
+
);
|
|
13625
|
+
const overlap = this.windowSize - this.hopSize;
|
|
13626
|
+
if (overlap > 0) {
|
|
13627
|
+
this.buffer.set(this.buffer.subarray(this.hopSize, this.windowSize), 0);
|
|
13628
|
+
}
|
|
13629
|
+
this.writeIndex = overlap > 0 ? overlap : 0;
|
|
13630
|
+
}
|
|
13631
|
+
}
|
|
13632
|
+
return true;
|
|
13403
13633
|
}
|
|
13404
|
-
return true;
|
|
13405
13634
|
}
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13635
|
+
registerProcessor('audio-analysis-processor', AudioAnalysisProcessor);
|
|
13636
|
+
`;
|
|
13637
|
+
const blob = new Blob([workletSource], { type: "application/javascript" });
|
|
13638
|
+
const workletUrl = URL.createObjectURL(blob);
|
|
13639
|
+
await this.audioContext.audioWorklet.addModule(workletUrl);
|
|
13640
|
+
URL.revokeObjectURL(workletUrl);
|
|
13641
|
+
this.workletRegistered = true;
|
|
13642
|
+
}
|
|
13412
13643
|
this.workletNode = new AudioWorkletNode(this.audioContext, "audio-analysis-processor", {
|
|
13413
13644
|
numberOfOutputs: 1,
|
|
13414
13645
|
outputChannelCount: [1],
|
|
@@ -13434,26 +13665,15 @@ class AudioSystem {
|
|
|
13434
13665
|
* Start the audio analysis loop at high speed (8ms intervals for transient capture)
|
|
13435
13666
|
*/
|
|
13436
13667
|
startAnalysisLoop() {
|
|
13437
|
-
console.log("[AudioSystem] startAnalysisLoop called", {
|
|
13438
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13439
|
-
analysisMode: this.analysisMode,
|
|
13440
|
-
hasAnalyser: !!this.analyser,
|
|
13441
|
-
hasContext: !!this.audioContext
|
|
13442
|
-
});
|
|
13443
13668
|
if (this.isAnalysisRunning || this.analysisMode !== "analyser") {
|
|
13444
|
-
console.warn("[AudioSystem] startAnalysisLoop SKIPPED", {
|
|
13445
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13446
|
-
analysisMode: this.analysisMode
|
|
13447
|
-
});
|
|
13448
13669
|
return;
|
|
13449
13670
|
}
|
|
13450
13671
|
this.isAnalysisRunning = true;
|
|
13451
13672
|
this.analysisInterval = window.setInterval(() => {
|
|
13452
13673
|
this.performAnalysis();
|
|
13453
13674
|
}, this.analysisIntervalMs);
|
|
13454
|
-
|
|
13455
|
-
intervalMs: this.analysisIntervalMs
|
|
13456
|
-
intervalId: this.analysisInterval
|
|
13675
|
+
this.debugLog("[AudioSystem] Analysis loop started", {
|
|
13676
|
+
intervalMs: this.analysisIntervalMs
|
|
13457
13677
|
});
|
|
13458
13678
|
}
|
|
13459
13679
|
/**
|
|
@@ -13470,6 +13690,24 @@ class AudioSystem {
|
|
|
13470
13690
|
this.analysisLoopId = null;
|
|
13471
13691
|
}
|
|
13472
13692
|
}
|
|
13693
|
+
startStalenessTimer() {
|
|
13694
|
+
this.stopStalenessTimer();
|
|
13695
|
+
this.lastFrameTime = performance.now();
|
|
13696
|
+
this.stalenessTimer = setInterval(() => {
|
|
13697
|
+
if (!this.audioState.isConnected || this.lastFrameTime === 0) return;
|
|
13698
|
+
const elapsed = performance.now() - this.lastFrameTime;
|
|
13699
|
+
if (elapsed > AudioSystem.STALENESS_THRESHOLD_MS) {
|
|
13700
|
+
this.resetAudioValues();
|
|
13701
|
+
this.sendAnalysisResultsToWorker();
|
|
13702
|
+
}
|
|
13703
|
+
}, 250);
|
|
13704
|
+
}
|
|
13705
|
+
stopStalenessTimer() {
|
|
13706
|
+
if (this.stalenessTimer !== null) {
|
|
13707
|
+
clearInterval(this.stalenessTimer);
|
|
13708
|
+
this.stalenessTimer = null;
|
|
13709
|
+
}
|
|
13710
|
+
}
|
|
13473
13711
|
/**
|
|
13474
13712
|
* Pause audio analysis (for tests or temporary suspension)
|
|
13475
13713
|
* The setInterval continues but performAnalysis() exits early
|
|
@@ -13492,25 +13730,7 @@ class AudioSystem {
|
|
|
13492
13730
|
* Layer 4: BeatStateManager (state machine + confidence)
|
|
13493
13731
|
*/
|
|
13494
13732
|
performAnalysis() {
|
|
13495
|
-
if (this.analysisTicks === 0) {
|
|
13496
|
-
console.log("[AudioSystem] performAnalysis FIRST CALL", {
|
|
13497
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13498
|
-
mode: this.analysisMode,
|
|
13499
|
-
hasContext: !!this.audioContext,
|
|
13500
|
-
hasAnalyser: !!this.analyser
|
|
13501
|
-
});
|
|
13502
|
-
}
|
|
13503
13733
|
if (!this.isAnalysisRunning || this.analysisMode !== "analyser" || !this.audioContext || !this.analyser) {
|
|
13504
|
-
const now = performance.now();
|
|
13505
|
-
if (now - this.lastLoopWarn > 2e3) {
|
|
13506
|
-
console.warn("[AudioSystem] performAnalysis skipped", {
|
|
13507
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13508
|
-
mode: this.analysisMode,
|
|
13509
|
-
hasContext: !!this.audioContext,
|
|
13510
|
-
hasAnalyser: !!this.analyser
|
|
13511
|
-
});
|
|
13512
|
-
this.lastLoopWarn = now;
|
|
13513
|
-
}
|
|
13514
13734
|
return;
|
|
13515
13735
|
}
|
|
13516
13736
|
if (!this.timeDomainData || this.timeDomainData.length !== this.fftSize) {
|
|
@@ -13518,14 +13738,6 @@ class AudioSystem {
|
|
|
13518
13738
|
}
|
|
13519
13739
|
this.analyser.getFloatTimeDomainData(this.timeDomainData);
|
|
13520
13740
|
this.analysisTicks++;
|
|
13521
|
-
if (this.analysisTicks === 1) {
|
|
13522
|
-
console.log("[AudioSystem][analyser] first tick", {
|
|
13523
|
-
fftSize: this.fftSize,
|
|
13524
|
-
sampleRate: this.audioContext.sampleRate,
|
|
13525
|
-
bufLen: this.timeDomainData.length
|
|
13526
|
-
});
|
|
13527
|
-
this.logVerbose("[AudioSystem][analyser] first 8 samples", Array.from(this.timeDomainData.slice(0, 8)));
|
|
13528
|
-
}
|
|
13529
13741
|
for (let i = 0; i < 32 && i < this.timeDomainData.length; i++) {
|
|
13530
13742
|
Math.abs(this.timeDomainData[i]);
|
|
13531
13743
|
}
|
|
@@ -13533,9 +13745,6 @@ class AudioSystem {
|
|
|
13533
13745
|
this.analyzeFrame(this.timeDomainData, this.audioContext.sampleRate, detectionTimeMs);
|
|
13534
13746
|
}
|
|
13535
13747
|
applyAutoGain() {
|
|
13536
|
-
if (this.debugDisableAutoGain) {
|
|
13537
|
-
return;
|
|
13538
|
-
}
|
|
13539
13748
|
this.audioState.volume.current = this.volumeAutoGain.process(this.audioState.volume.current);
|
|
13540
13749
|
this.audioState.volume.peak = this.volumeAutoGain.process(this.audioState.volume.peak);
|
|
13541
13750
|
const bands = this.audioState.bands;
|
|
@@ -13581,12 +13790,17 @@ class AudioSystem {
|
|
|
13581
13790
|
sendAnalysisResultsToWorker() {
|
|
13582
13791
|
if (this.sendAnalysisResults) {
|
|
13583
13792
|
const frequencyData = this.frequencyData ? new Uint8Array(this.frequencyData) : new Uint8Array(0);
|
|
13793
|
+
const waveformData = this.lastWaveformFrame ? new Float32Array(this.lastWaveformFrame) : new Float32Array(0);
|
|
13584
13794
|
this.sendAnalysisResults({
|
|
13585
13795
|
type: "audio-analysis-update",
|
|
13586
13796
|
data: {
|
|
13587
|
-
|
|
13797
|
+
isConnected: this.audioState.isConnected,
|
|
13798
|
+
volume: this.audioState.volume,
|
|
13799
|
+
bands: this.audioState.bands,
|
|
13800
|
+
beat: this.audioState.beat,
|
|
13801
|
+
spectral: this.audioState.spectral,
|
|
13588
13802
|
frequencyData,
|
|
13589
|
-
|
|
13803
|
+
waveformData,
|
|
13590
13804
|
timestamp: performance.now()
|
|
13591
13805
|
}
|
|
13592
13806
|
});
|
|
@@ -13610,18 +13824,16 @@ class AudioSystem {
|
|
|
13610
13824
|
any: 0,
|
|
13611
13825
|
kickSmoothed: 0,
|
|
13612
13826
|
snareSmoothed: 0,
|
|
13827
|
+
hatSmoothed: 0,
|
|
13613
13828
|
anySmoothed: 0,
|
|
13614
13829
|
events: [],
|
|
13615
13830
|
bpm: 120,
|
|
13616
|
-
phase: 0,
|
|
13617
|
-
bar: 0,
|
|
13618
13831
|
confidence: 0,
|
|
13619
13832
|
isLocked: false
|
|
13620
13833
|
};
|
|
13621
13834
|
this.audioState.spectral = {
|
|
13622
13835
|
brightness: 0,
|
|
13623
|
-
flatness: 0
|
|
13624
|
-
flux: 0
|
|
13836
|
+
flatness: 0
|
|
13625
13837
|
};
|
|
13626
13838
|
}
|
|
13627
13839
|
/**
|
|
@@ -13636,6 +13848,7 @@ class AudioSystem {
|
|
|
13636
13848
|
}
|
|
13637
13849
|
this.workletNode = null;
|
|
13638
13850
|
this.workletReady = false;
|
|
13851
|
+
this.workletRegistered = false;
|
|
13639
13852
|
this.analysisMode = "analyser";
|
|
13640
13853
|
this.onsetDetection.reset();
|
|
13641
13854
|
this.tempoInduction.reset();
|
|
@@ -13643,183 +13856,48 @@ class AudioSystem {
|
|
|
13643
13856
|
this.stateManager.reset();
|
|
13644
13857
|
this.volumeAutoGain.reset();
|
|
13645
13858
|
Object.values(this.bandAutoGain).forEach((g) => g.reset());
|
|
13646
|
-
this.
|
|
13647
|
-
this.manualBPM = null;
|
|
13648
|
-
this.tapHistory = [];
|
|
13649
|
-
if (this.tapTimeout) {
|
|
13650
|
-
clearTimeout(this.tapTimeout);
|
|
13651
|
-
this.tapTimeout = null;
|
|
13652
|
-
}
|
|
13859
|
+
this.onsetTapManager.reset();
|
|
13653
13860
|
Object.values(this.envelopeFollowers).forEach((env) => env.reset());
|
|
13654
|
-
this.pcmRing = null;
|
|
13655
|
-
this.pcmWriteIndex = 0;
|
|
13656
|
-
this.pcmFilled = 0;
|
|
13657
|
-
this.lastTempoExtraction = 0;
|
|
13658
13861
|
this.resetAudioValues();
|
|
13659
13862
|
}
|
|
13660
|
-
/**
|
|
13661
|
-
* Get current analysis configuration
|
|
13662
|
-
*/
|
|
13663
|
-
getAnalysisConfig() {
|
|
13664
|
-
return {
|
|
13665
|
-
fftSize: this.fftSize,
|
|
13666
|
-
smoothing: this.smoothingTimeConstant
|
|
13667
|
-
};
|
|
13668
|
-
}
|
|
13669
|
-
/**
|
|
13670
|
-
* Set analysis backend preference
|
|
13671
|
-
*/
|
|
13672
|
-
setAnalysisBackend(backend) {
|
|
13673
|
-
this.analysisBackend = backend;
|
|
13674
|
-
}
|
|
13675
|
-
getAnalysisBackend() {
|
|
13676
|
-
return this.analysisBackend;
|
|
13677
|
-
}
|
|
13678
|
-
/**
|
|
13679
|
-
* Force analyser path (skip worklet) for debugging
|
|
13680
|
-
*/
|
|
13681
|
-
setForceAnalyser(enabled) {
|
|
13682
|
-
this.forceAnalyser = enabled;
|
|
13683
|
-
if (enabled) {
|
|
13684
|
-
console.warn("[AudioSystem] forceAnalyser enabled - worklet will be skipped.");
|
|
13685
|
-
}
|
|
13686
|
-
}
|
|
13687
|
-
isForceAnalyser() {
|
|
13688
|
-
return this.forceAnalyser;
|
|
13689
|
-
}
|
|
13690
|
-
/**
|
|
13691
|
-
* Enable/disable Essentia tempo extraction (disabled by default due to WASM exception config)
|
|
13692
|
-
*/
|
|
13693
|
-
setEssentiaTempoEnabled(enabled) {
|
|
13694
|
-
this.useEssentiaTempo = enabled;
|
|
13695
|
-
}
|
|
13696
|
-
isEssentiaTempoEnabled() {
|
|
13697
|
-
return this.useEssentiaTempo;
|
|
13698
|
-
}
|
|
13699
13863
|
// ═══════════════════════════════════════════════════════════
|
|
13700
13864
|
// Public API Methods for Audio Analysis Configuration
|
|
13701
13865
|
// ═══════════════════════════════════════════════════════════
|
|
13702
13866
|
/**
|
|
13703
|
-
*
|
|
13704
|
-
* @param value - Sensitivity (0.5-2.0, default 1.0)
|
|
13867
|
+
* Record a tap for the specified instrument onset
|
|
13705
13868
|
*/
|
|
13706
|
-
|
|
13707
|
-
this.
|
|
13869
|
+
tapOnset(instrument) {
|
|
13870
|
+
this.onsetTapManager.tap(instrument);
|
|
13708
13871
|
}
|
|
13709
13872
|
/**
|
|
13710
|
-
*
|
|
13873
|
+
* Clear tap pattern for an instrument, restoring auto-detection
|
|
13711
13874
|
*/
|
|
13712
|
-
|
|
13713
|
-
|
|
13875
|
+
clearOnsetTap(instrument) {
|
|
13876
|
+
this.onsetTapManager.clear(instrument);
|
|
13714
13877
|
}
|
|
13715
13878
|
/**
|
|
13716
|
-
*
|
|
13879
|
+
* Get the current onset mode for an instrument
|
|
13717
13880
|
*/
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
if (this.tapTimeout) {
|
|
13721
|
-
clearTimeout(this.tapTimeout);
|
|
13722
|
-
}
|
|
13723
|
-
if (this.tapHistory.length > 0) {
|
|
13724
|
-
const lastTap = this.tapHistory[this.tapHistory.length - 1];
|
|
13725
|
-
const interval = now - lastTap;
|
|
13726
|
-
if (interval < 200) {
|
|
13727
|
-
this.startTapClearTimeout();
|
|
13728
|
-
return;
|
|
13729
|
-
}
|
|
13730
|
-
if (interval > 2e3) {
|
|
13731
|
-
this.tapHistory = [];
|
|
13732
|
-
}
|
|
13733
|
-
}
|
|
13734
|
-
this.tapHistory.push(now);
|
|
13735
|
-
if (this.tapHistory.length > 8) {
|
|
13736
|
-
this.tapHistory.shift();
|
|
13737
|
-
}
|
|
13738
|
-
if (this.tapHistory.length >= 2) {
|
|
13739
|
-
const bpm = this.calculateBPMFromTaps();
|
|
13740
|
-
this.manualBPM = bpm;
|
|
13741
|
-
this.beatMode = "manual";
|
|
13742
|
-
this.pll.setBPM(bpm);
|
|
13743
|
-
this.tempoInduction.setManualBPM(bpm);
|
|
13744
|
-
}
|
|
13745
|
-
this.startTapClearTimeout();
|
|
13746
|
-
}
|
|
13747
|
-
/**
|
|
13748
|
-
* Calculate BPM from tap history
|
|
13749
|
-
*/
|
|
13750
|
-
calculateBPMFromTaps() {
|
|
13751
|
-
if (this.tapHistory.length < 2) return 120;
|
|
13752
|
-
const intervals = [];
|
|
13753
|
-
for (let i = 1; i < this.tapHistory.length; i++) {
|
|
13754
|
-
intervals.push(this.tapHistory[i] - this.tapHistory[i - 1]);
|
|
13755
|
-
}
|
|
13756
|
-
if (intervals.length >= 4) {
|
|
13757
|
-
const sorted = [...intervals].sort((a, b) => a - b);
|
|
13758
|
-
const median = sorted[Math.floor(sorted.length / 2)];
|
|
13759
|
-
const filtered = intervals.filter((val) => {
|
|
13760
|
-
const deviation = Math.abs(val - median) / median;
|
|
13761
|
-
return deviation < 0.3;
|
|
13762
|
-
});
|
|
13763
|
-
if (filtered.length >= 2) {
|
|
13764
|
-
intervals.splice(0, intervals.length, ...filtered);
|
|
13765
|
-
}
|
|
13766
|
-
}
|
|
13767
|
-
const avgInterval = intervals.reduce((sum, val) => sum + val, 0) / intervals.length;
|
|
13768
|
-
let bpm = 6e4 / avgInterval;
|
|
13769
|
-
while (bpm > 200) bpm /= 2;
|
|
13770
|
-
while (bpm < 60) bpm *= 2;
|
|
13771
|
-
return Math.round(bpm * 10) / 10;
|
|
13772
|
-
}
|
|
13773
|
-
/**
|
|
13774
|
-
* Start auto-clear timeout for tap tempo
|
|
13775
|
-
*/
|
|
13776
|
-
startTapClearTimeout() {
|
|
13777
|
-
this.tapTimeout = setTimeout(() => {
|
|
13778
|
-
this.tapHistory = [];
|
|
13779
|
-
}, 2e3);
|
|
13780
|
-
}
|
|
13781
|
-
/**
|
|
13782
|
-
* Clear tap tempo history
|
|
13783
|
-
*/
|
|
13784
|
-
clearTaps() {
|
|
13785
|
-
this.tapHistory = [];
|
|
13786
|
-
if (this.tapTimeout) {
|
|
13787
|
-
clearTimeout(this.tapTimeout);
|
|
13788
|
-
this.tapTimeout = null;
|
|
13789
|
-
}
|
|
13881
|
+
getOnsetMode(instrument) {
|
|
13882
|
+
return this.onsetTapManager.getMode(instrument);
|
|
13790
13883
|
}
|
|
13791
13884
|
/**
|
|
13792
|
-
* Get
|
|
13885
|
+
* Get pattern info for an instrument (null if no pattern recognized)
|
|
13793
13886
|
*/
|
|
13794
|
-
|
|
13795
|
-
return this.
|
|
13796
|
-
}
|
|
13797
|
-
/**
|
|
13798
|
-
* Set beat sync mode
|
|
13799
|
-
*/
|
|
13800
|
-
setBeatMode(mode) {
|
|
13801
|
-
this.beatMode = mode;
|
|
13802
|
-
if (mode === "auto") {
|
|
13803
|
-
this.clearTaps();
|
|
13804
|
-
this.manualBPM = null;
|
|
13805
|
-
}
|
|
13887
|
+
getOnsetPatternInfo(instrument) {
|
|
13888
|
+
return this.onsetTapManager.getPatternInfo(instrument);
|
|
13806
13889
|
}
|
|
13807
13890
|
/**
|
|
13808
|
-
*
|
|
13891
|
+
* Mute/unmute an instrument onset (suppresses output without destroying state)
|
|
13809
13892
|
*/
|
|
13810
|
-
|
|
13811
|
-
|
|
13893
|
+
setOnsetMuted(instrument, muted) {
|
|
13894
|
+
this.onsetTapManager.setMuted(instrument, muted);
|
|
13812
13895
|
}
|
|
13813
13896
|
/**
|
|
13814
|
-
*
|
|
13897
|
+
* Check if an instrument onset is muted
|
|
13815
13898
|
*/
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
this.manualBPM = bpm;
|
|
13819
|
-
this.beatMode = "manual";
|
|
13820
|
-
this.pll.setBPM(bpm);
|
|
13821
|
-
this.tempoInduction.setManualBPM(bpm);
|
|
13822
|
-
this.clearTaps();
|
|
13899
|
+
isOnsetMuted(instrument) {
|
|
13900
|
+
return this.onsetTapManager.isMuted(instrument);
|
|
13823
13901
|
}
|
|
13824
13902
|
/**
|
|
13825
13903
|
* Get current BPM (manual or auto-detected)
|
|
@@ -13851,21 +13929,11 @@ class AudioSystem {
|
|
|
13851
13929
|
this.setAudioStream(stream);
|
|
13852
13930
|
}
|
|
13853
13931
|
}
|
|
13854
|
-
/**
|
|
13855
|
-
* Set smoothing time constant
|
|
13856
|
-
*/
|
|
13857
|
-
setSmoothing(value) {
|
|
13858
|
-
this.smoothingTimeConstant = Math.max(0, Math.min(1, value));
|
|
13859
|
-
if (this.analyser) {
|
|
13860
|
-
this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
|
|
13861
|
-
}
|
|
13862
|
-
}
|
|
13863
13932
|
/**
|
|
13864
13933
|
* Enable/disable auto-gain
|
|
13865
13934
|
*/
|
|
13866
13935
|
setAutoGain(enabled) {
|
|
13867
13936
|
this.autoGainEnabled = enabled;
|
|
13868
|
-
this.debugDisableAutoGain = !enabled;
|
|
13869
13937
|
if (!enabled) {
|
|
13870
13938
|
this.volumeAutoGain.reset();
|
|
13871
13939
|
Object.values(this.bandAutoGain).forEach((g) => g.reset());
|
|
@@ -13899,11 +13967,13 @@ class AudioSystem {
|
|
|
13899
13967
|
isConnected: this.audioState.isConnected,
|
|
13900
13968
|
currentBPM: this.pll.getBPM(),
|
|
13901
13969
|
confidence: this.tempoInduction.getConfidence(),
|
|
13902
|
-
mode: this.beatMode,
|
|
13903
|
-
tapCount: this.tapHistory.length,
|
|
13904
13970
|
isLocked: this.stateManager.isLocked(),
|
|
13905
|
-
|
|
13906
|
-
|
|
13971
|
+
trackingState: this.stateManager.getState(),
|
|
13972
|
+
onsetModes: {
|
|
13973
|
+
kick: this.onsetTapManager.getMode("kick"),
|
|
13974
|
+
snare: this.onsetTapManager.getMode("snare"),
|
|
13975
|
+
hat: this.onsetTapManager.getMode("hat")
|
|
13976
|
+
}
|
|
13907
13977
|
};
|
|
13908
13978
|
}
|
|
13909
13979
|
/**
|
|
@@ -13982,7 +14052,6 @@ class AudioSystem {
|
|
|
13982
14052
|
volumeGain: this.volumeAutoGain.getGain(),
|
|
13983
14053
|
// Timing info
|
|
13984
14054
|
dtMs: this.lastDtMs,
|
|
13985
|
-
smoothingTimeConstant: this.smoothingTimeConstant,
|
|
13986
14055
|
analysisIntervalMs: this.analysisIntervalMs,
|
|
13987
14056
|
// Current events
|
|
13988
14057
|
events: [...this.audioState.beat.events]
|
|
@@ -14541,15 +14610,25 @@ class VijiCore {
|
|
|
14541
14610
|
currentAudioStream = null;
|
|
14542
14611
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
14543
14612
|
// VIDEO STREAM INDEX CONTRACT:
|
|
14544
|
-
//
|
|
14545
|
-
//
|
|
14546
|
-
//
|
|
14613
|
+
// Each stream category uses a fixed, independent index range in the worker's
|
|
14614
|
+
// videoSystems[] array. Categories cannot collide regardless of add/remove order.
|
|
14615
|
+
//
|
|
14616
|
+
// Index 0: Main video (with CV) - always reserved, even when absent
|
|
14617
|
+
// Index 1..99: Additional MediaStreams (no CV)
|
|
14618
|
+
// Index 100..199: Device video streams
|
|
14619
|
+
// Index 200..299: Direct frame injection (compositor pipeline)
|
|
14620
|
+
//
|
|
14621
|
+
// The worker filters by streamType when building artist-facing arrays
|
|
14622
|
+
// (viji.streams[], viji.devices[].video), so absolute indices are internal.
|
|
14547
14623
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
14624
|
+
static ADDITIONAL_STREAM_BASE = 1;
|
|
14625
|
+
static DEVICE_VIDEO_BASE = 100;
|
|
14626
|
+
static DIRECT_FRAME_BASE = 200;
|
|
14548
14627
|
// Separated video stream management
|
|
14549
14628
|
videoStream = null;
|
|
14550
14629
|
// Main stream (CV enabled) - always index 0
|
|
14551
14630
|
videoStreams = [];
|
|
14552
|
-
// Additional streams (no CV)
|
|
14631
|
+
// Additional streams (no CV)
|
|
14553
14632
|
// Video coordinators
|
|
14554
14633
|
mainVideoCoordinator = null;
|
|
14555
14634
|
additionalCoordinators = [];
|
|
@@ -14557,7 +14636,6 @@ class VijiCore {
|
|
|
14557
14636
|
directFrameSlots = 0;
|
|
14558
14637
|
// Device video management (coordinators only, for cleanup)
|
|
14559
14638
|
deviceVideoCoordinators = /* @__PURE__ */ new Map();
|
|
14560
|
-
// Track assigned stream indices to prevent collisions (indices start at 1 + videoStreams.length)
|
|
14561
14639
|
deviceVideoStreamIndices = /* @__PURE__ */ new Map();
|
|
14562
14640
|
// Auto-capture frame buffer (zero-copy transfer)
|
|
14563
14641
|
latestFrameBuffer = null;
|
|
@@ -14598,7 +14676,6 @@ class VijiCore {
|
|
|
14598
14676
|
this.config = {
|
|
14599
14677
|
...config,
|
|
14600
14678
|
frameRateMode: config.frameRateMode || "full",
|
|
14601
|
-
parameters: config.parameters || [],
|
|
14602
14679
|
noInputs: config.noInputs ?? false,
|
|
14603
14680
|
// Canvas DOM interaction disabled in headless by default
|
|
14604
14681
|
// InteractionManager (viji.mouse/etc) always exists regardless
|
|
@@ -14635,7 +14712,8 @@ class VijiCore {
|
|
|
14635
14712
|
this.latestFrameBuffer = bitmap;
|
|
14636
14713
|
}
|
|
14637
14714
|
/**
|
|
14638
|
-
* Get latest frame (transfers ownership, zero-copy)
|
|
14715
|
+
* Get latest frame (transfers ownership, zero-copy).
|
|
14716
|
+
* Internal: consumed by getLatestFramesFromSources().
|
|
14639
14717
|
*/
|
|
14640
14718
|
getLatestFrame() {
|
|
14641
14719
|
const frame = this.latestFrameBuffer;
|
|
@@ -14691,7 +14769,6 @@ class VijiCore {
|
|
|
14691
14769
|
if (this.interactionManager && "setDebugMode" in this.interactionManager) {
|
|
14692
14770
|
this.interactionManager.setDebugMode(enabled);
|
|
14693
14771
|
}
|
|
14694
|
-
if (this.audioSystem && "setDebugMode" in this.audioSystem) ;
|
|
14695
14772
|
if (this.mainVideoCoordinator && "setDebugMode" in this.mainVideoCoordinator) {
|
|
14696
14773
|
this.mainVideoCoordinator.setDebugMode(enabled);
|
|
14697
14774
|
}
|
|
@@ -14725,15 +14802,6 @@ class VijiCore {
|
|
|
14725
14802
|
hide() {
|
|
14726
14803
|
this.iframeManager?.hide();
|
|
14727
14804
|
}
|
|
14728
|
-
/**
|
|
14729
|
-
* Select audio analysis backend
|
|
14730
|
-
*/
|
|
14731
|
-
setAudioAnalysisBackend(backend) {
|
|
14732
|
-
this.audioSystem?.setAnalysisBackend(backend);
|
|
14733
|
-
}
|
|
14734
|
-
getAudioAnalysisBackend() {
|
|
14735
|
-
return this.audioSystem?.getAnalysisBackend() ?? "auto";
|
|
14736
|
-
}
|
|
14737
14805
|
/**
|
|
14738
14806
|
* Initializes the core components in sequence
|
|
14739
14807
|
*/
|
|
@@ -14796,7 +14864,7 @@ class VijiCore {
|
|
|
14796
14864
|
});
|
|
14797
14865
|
}
|
|
14798
14866
|
for (let i = 0; i < this.videoStreams.length; i++) {
|
|
14799
|
-
const streamIndex =
|
|
14867
|
+
const streamIndex = VijiCore.ADDITIONAL_STREAM_BASE + i;
|
|
14800
14868
|
const coordinator = new VideoCoordinator((message, transfer) => {
|
|
14801
14869
|
if (this.workerManager) {
|
|
14802
14870
|
if (message.type === "video-canvas-setup") {
|
|
@@ -15212,13 +15280,6 @@ class VijiCore {
|
|
|
15212
15280
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
15213
15281
|
// Direct Frame Injection API (Compositor Pipeline)
|
|
15214
15282
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
15215
|
-
/**
|
|
15216
|
-
* Worker-side stream index offset for direct frame slots.
|
|
15217
|
-
* Accounts for main video stream and additional media streams.
|
|
15218
|
-
*/
|
|
15219
|
-
get directFrameStartIndex() {
|
|
15220
|
-
return (this.videoStream ? 1 : 0) + this.videoStreams.length;
|
|
15221
|
-
}
|
|
15222
15283
|
/**
|
|
15223
15284
|
* Ensure the worker has enough direct frame slots prepared.
|
|
15224
15285
|
* Auto-grows as needed; skips if already sufficient.
|
|
@@ -15227,8 +15288,7 @@ class VijiCore {
|
|
|
15227
15288
|
if (count <= this.directFrameSlots) return;
|
|
15228
15289
|
this.directFrameSlots = count;
|
|
15229
15290
|
this.workerManager.postMessage("video-streams-prepare", {
|
|
15230
|
-
|
|
15231
|
-
mediaStreamCount: this.videoStreams.length,
|
|
15291
|
+
directFrameBaseIndex: VijiCore.DIRECT_FRAME_BASE,
|
|
15232
15292
|
directFrameCount: count
|
|
15233
15293
|
});
|
|
15234
15294
|
this.debugLog(`[Compositor] Prepared ${count} direct frame slot(s)`);
|
|
@@ -15245,27 +15305,10 @@ class VijiCore {
|
|
|
15245
15305
|
this.ensureDirectFrameSlots(streamIndex + 1);
|
|
15246
15306
|
this.workerManager.postMessage("video-frame-direct", {
|
|
15247
15307
|
imageBitmap: bitmap,
|
|
15248
|
-
streamIndex:
|
|
15308
|
+
streamIndex: VijiCore.DIRECT_FRAME_BASE + streamIndex,
|
|
15249
15309
|
timestamp: performance.now()
|
|
15250
15310
|
}, [bitmap]);
|
|
15251
15311
|
}
|
|
15252
|
-
/**
|
|
15253
|
-
* Inject frames for all direct-frame stream slots at once (compositor pipeline).
|
|
15254
|
-
* Auto-prepares slots on first call. Transfers ownership of all bitmaps (zero-copy).
|
|
15255
|
-
*/
|
|
15256
|
-
injectFrames(bitmaps) {
|
|
15257
|
-
if (!this.workerManager?.ready) {
|
|
15258
|
-
throw new VijiCoreError("Core not ready", "NOT_READY");
|
|
15259
|
-
}
|
|
15260
|
-
this.ensureDirectFrameSlots(bitmaps.length);
|
|
15261
|
-
bitmaps.forEach((bitmap, index) => {
|
|
15262
|
-
this.workerManager.postMessage("video-frame-direct", {
|
|
15263
|
-
imageBitmap: bitmap,
|
|
15264
|
-
streamIndex: this.directFrameStartIndex + index,
|
|
15265
|
-
timestamp: performance.now()
|
|
15266
|
-
}, [bitmap]);
|
|
15267
|
-
});
|
|
15268
|
-
}
|
|
15269
15312
|
/**
|
|
15270
15313
|
* Link this core to receive events from a source core
|
|
15271
15314
|
* @param syncResolution Smart default: true for headless, false for visible
|
|
@@ -15600,7 +15643,6 @@ class VijiCore {
|
|
|
15600
15643
|
if (this.audioSystem) {
|
|
15601
15644
|
this.audioSystem.handleAudioStreamUpdate({
|
|
15602
15645
|
audioStream,
|
|
15603
|
-
...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
|
|
15604
15646
|
timestamp: performance.now()
|
|
15605
15647
|
});
|
|
15606
15648
|
}
|
|
@@ -15660,31 +15702,10 @@ class VijiCore {
|
|
|
15660
15702
|
return this.videoStream;
|
|
15661
15703
|
}
|
|
15662
15704
|
// ═══════════════════════════════════════════════════════
|
|
15663
|
-
// ADDITIONAL VIDEO STREAMS API
|
|
15705
|
+
// ADDITIONAL VIDEO STREAMS API
|
|
15664
15706
|
// ═══════════════════════════════════════════════════════
|
|
15665
15707
|
/**
|
|
15666
|
-
*
|
|
15667
|
-
*/
|
|
15668
|
-
async setVideoStreams(streams) {
|
|
15669
|
-
this.validateReady();
|
|
15670
|
-
this.videoStreams = [...streams];
|
|
15671
|
-
await this.reinitializeAdditionalCoordinators();
|
|
15672
|
-
this.debugLog(`Additional video streams updated: ${streams.length} streams`);
|
|
15673
|
-
}
|
|
15674
|
-
/**
|
|
15675
|
-
* Gets all additional video streams
|
|
15676
|
-
*/
|
|
15677
|
-
getVideoStreams() {
|
|
15678
|
-
return [...this.videoStreams];
|
|
15679
|
-
}
|
|
15680
|
-
/**
|
|
15681
|
-
* Gets video stream at specific index
|
|
15682
|
-
*/
|
|
15683
|
-
getVideoStreamAt(index) {
|
|
15684
|
-
return this.videoStreams[index] || null;
|
|
15685
|
-
}
|
|
15686
|
-
/**
|
|
15687
|
-
* Adds a video stream
|
|
15708
|
+
* Adds an additional video stream (no CV). Returns its index in viji.streams[].
|
|
15688
15709
|
*/
|
|
15689
15710
|
async addVideoStream(stream) {
|
|
15690
15711
|
this.validateReady();
|
|
@@ -15692,7 +15713,7 @@ class VijiCore {
|
|
|
15692
15713
|
if (existingIndex !== -1) return existingIndex;
|
|
15693
15714
|
this.videoStreams.push(stream);
|
|
15694
15715
|
const newIndex = this.videoStreams.length - 1;
|
|
15695
|
-
const streamIndex =
|
|
15716
|
+
const streamIndex = VijiCore.ADDITIONAL_STREAM_BASE + newIndex;
|
|
15696
15717
|
const coordinator = new VideoCoordinator((message, transfer) => {
|
|
15697
15718
|
if (this.workerManager) {
|
|
15698
15719
|
if (message.type === "video-canvas-setup") {
|
|
@@ -15717,9 +15738,9 @@ class VijiCore {
|
|
|
15717
15738
|
return newIndex;
|
|
15718
15739
|
}
|
|
15719
15740
|
/**
|
|
15720
|
-
* Removes video stream
|
|
15741
|
+
* Removes an additional video stream by index.
|
|
15721
15742
|
*/
|
|
15722
|
-
async
|
|
15743
|
+
async removeVideoStream(index) {
|
|
15723
15744
|
this.validateReady();
|
|
15724
15745
|
if (index < 0 || index >= this.videoStreams.length) {
|
|
15725
15746
|
throw new VijiCoreError(`Invalid stream index: ${index}`, "INVALID_INDEX");
|
|
@@ -15729,43 +15750,19 @@ class VijiCore {
|
|
|
15729
15750
|
await this.reinitializeAdditionalCoordinators();
|
|
15730
15751
|
}
|
|
15731
15752
|
/**
|
|
15732
|
-
*
|
|
15753
|
+
* Gets the number of additional video streams.
|
|
15733
15754
|
*/
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
if (index === -1) return false;
|
|
15737
|
-
await this.removeVideoStreamAt(index);
|
|
15738
|
-
return true;
|
|
15755
|
+
getVideoStreamCount() {
|
|
15756
|
+
return this.videoStreams.length;
|
|
15739
15757
|
}
|
|
15740
15758
|
/**
|
|
15741
|
-
*
|
|
15742
|
-
*/
|
|
15743
|
-
async setVideoStreamAt(index, stream) {
|
|
15744
|
-
this.validateReady();
|
|
15745
|
-
if (index < 0 || index >= this.videoStreams.length) {
|
|
15746
|
-
throw new VijiCoreError(`Invalid stream index: ${index}`, "INVALID_INDEX");
|
|
15747
|
-
}
|
|
15748
|
-
this.videoStreams[index].getTracks().forEach((track) => track.stop());
|
|
15749
|
-
this.videoStreams[index] = stream;
|
|
15750
|
-
if (this.additionalCoordinators[index]) {
|
|
15751
|
-
this.additionalCoordinators[index].resetVideoState();
|
|
15752
|
-
this.additionalCoordinators[index].handleVideoStreamUpdate({
|
|
15753
|
-
videoStream: stream,
|
|
15754
|
-
streamIndex: 1 + index,
|
|
15755
|
-
streamType: "additional",
|
|
15756
|
-
targetFrameRate: 30,
|
|
15757
|
-
timestamp: performance.now()
|
|
15758
|
-
});
|
|
15759
|
-
}
|
|
15760
|
-
}
|
|
15761
|
-
/**
|
|
15762
|
-
* Reinitializes all additional coordinators
|
|
15759
|
+
* Reinitializes all additional coordinators after array mutation.
|
|
15763
15760
|
*/
|
|
15764
15761
|
async reinitializeAdditionalCoordinators() {
|
|
15765
15762
|
this.additionalCoordinators.forEach((coord) => coord.resetVideoState());
|
|
15766
15763
|
this.additionalCoordinators = [];
|
|
15767
15764
|
for (let i = 0; i < this.videoStreams.length; i++) {
|
|
15768
|
-
const streamIndex =
|
|
15765
|
+
const streamIndex = VijiCore.ADDITIONAL_STREAM_BASE + i;
|
|
15769
15766
|
const coordinator = new VideoCoordinator((message, transfer) => {
|
|
15770
15767
|
if (this.workerManager) {
|
|
15771
15768
|
if (message.type === "video-canvas-setup") {
|
|
@@ -15825,98 +15822,82 @@ class VijiCore {
|
|
|
15825
15822
|
// Audio Analysis API (Namespace-based)
|
|
15826
15823
|
// ═══════════════════════════════════════════════════════════
|
|
15827
15824
|
audio = {
|
|
15828
|
-
/**
|
|
15829
|
-
* Set global audio sensitivity (0.5-2.0, default 1.0)
|
|
15830
|
-
*/
|
|
15831
|
-
setSensitivity: (value) => {
|
|
15832
|
-
this.validateReady();
|
|
15833
|
-
if (value < 0.5 || value > 2) {
|
|
15834
|
-
throw new VijiCoreError("Sensitivity must be between 0.5 and 2.0", "INVALID_VALUE");
|
|
15835
|
-
}
|
|
15836
|
-
this.audioSystem?.setSensitivity(value);
|
|
15837
|
-
this.debugLog(`Audio sensitivity set to ${value} (${this.instanceId})`);
|
|
15838
|
-
},
|
|
15839
|
-
/**
|
|
15840
|
-
* Get current sensitivity
|
|
15841
|
-
*/
|
|
15842
|
-
getSensitivity: () => {
|
|
15843
|
-
this.validateReady();
|
|
15844
|
-
return this.audioSystem?.getSensitivity() ?? 1;
|
|
15845
|
-
},
|
|
15846
15825
|
/**
|
|
15847
15826
|
* Beat control namespace
|
|
15848
15827
|
*/
|
|
15849
15828
|
beat: {
|
|
15850
15829
|
/**
|
|
15851
|
-
*
|
|
15830
|
+
* Get current BPM (auto-detected)
|
|
15852
15831
|
*/
|
|
15853
|
-
|
|
15832
|
+
getBPM: () => {
|
|
15854
15833
|
this.validateReady();
|
|
15855
|
-
this.audioSystem?.
|
|
15856
|
-
this.debugLog(`Tap tempo recorded (${this.instanceId})`);
|
|
15834
|
+
return this.audioSystem?.getCurrentBPM() ?? 120;
|
|
15857
15835
|
},
|
|
15858
15836
|
/**
|
|
15859
|
-
*
|
|
15837
|
+
* Nudge beat phase
|
|
15860
15838
|
*/
|
|
15861
|
-
|
|
15839
|
+
nudge: (amount) => {
|
|
15862
15840
|
this.validateReady();
|
|
15863
|
-
this.audioSystem?.
|
|
15864
|
-
this.debugLog(`Tap tempo cleared (${this.instanceId})`);
|
|
15841
|
+
this.audioSystem?.nudgeBeatPhase(amount);
|
|
15865
15842
|
},
|
|
15866
15843
|
/**
|
|
15867
|
-
*
|
|
15844
|
+
* Reset beat phase to next beat
|
|
15868
15845
|
*/
|
|
15869
|
-
|
|
15846
|
+
resetPhase: () => {
|
|
15870
15847
|
this.validateReady();
|
|
15871
|
-
|
|
15872
|
-
|
|
15848
|
+
this.audioSystem?.resetBeatPhase();
|
|
15849
|
+
this.debugLog(`Beat phase reset (${this.instanceId})`);
|
|
15850
|
+
}
|
|
15851
|
+
},
|
|
15852
|
+
/**
|
|
15853
|
+
* Per-instrument onset tap control
|
|
15854
|
+
*/
|
|
15855
|
+
onset: {
|
|
15873
15856
|
/**
|
|
15874
|
-
*
|
|
15857
|
+
* Tap an onset for a specific instrument.
|
|
15858
|
+
* First tap switches the instrument from auto to tapping mode.
|
|
15859
|
+
* If a repeating pattern is recognized, it continues after tapping stops.
|
|
15875
15860
|
*/
|
|
15876
|
-
|
|
15861
|
+
tap: (instrument) => {
|
|
15877
15862
|
this.validateReady();
|
|
15878
|
-
this.audioSystem?.
|
|
15879
|
-
this.debugLog(`Beat mode set to ${mode} (${this.instanceId})`);
|
|
15863
|
+
this.audioSystem?.tapOnset(instrument);
|
|
15880
15864
|
},
|
|
15881
15865
|
/**
|
|
15882
|
-
*
|
|
15866
|
+
* Clear the tap pattern for an instrument, restoring auto-detection
|
|
15883
15867
|
*/
|
|
15884
|
-
|
|
15868
|
+
clear: (instrument) => {
|
|
15885
15869
|
this.validateReady();
|
|
15886
|
-
|
|
15870
|
+
this.audioSystem?.clearOnsetTap(instrument);
|
|
15887
15871
|
},
|
|
15888
15872
|
/**
|
|
15889
|
-
*
|
|
15873
|
+
* Get onset mode for an instrument: 'auto' | 'tapping' | 'pattern'
|
|
15890
15874
|
*/
|
|
15891
|
-
|
|
15875
|
+
getMode: (instrument) => {
|
|
15892
15876
|
this.validateReady();
|
|
15893
|
-
|
|
15894
|
-
throw new VijiCoreError("BPM must be between 60 and 240", "INVALID_VALUE");
|
|
15895
|
-
}
|
|
15896
|
-
this.audioSystem?.setManualBPM(bpm);
|
|
15897
|
-
this.debugLog(`Manual BPM set to ${bpm} (${this.instanceId})`);
|
|
15877
|
+
return this.audioSystem?.getOnsetMode(instrument) ?? "auto";
|
|
15898
15878
|
},
|
|
15899
15879
|
/**
|
|
15900
|
-
* Get
|
|
15880
|
+
* Get recognized pattern info for an instrument, or null
|
|
15901
15881
|
*/
|
|
15902
|
-
|
|
15882
|
+
getPatternInfo: (instrument) => {
|
|
15903
15883
|
this.validateReady();
|
|
15904
|
-
return this.audioSystem?.
|
|
15884
|
+
return this.audioSystem?.getOnsetPatternInfo(instrument) ?? null;
|
|
15905
15885
|
},
|
|
15906
15886
|
/**
|
|
15907
|
-
*
|
|
15887
|
+
* Mute/unmute an instrument onset.
|
|
15888
|
+
* Suppresses all output (auto, tap, pattern) without destroying state.
|
|
15889
|
+
* Unmuting resumes pattern replay in-phase.
|
|
15908
15890
|
*/
|
|
15909
|
-
|
|
15891
|
+
setMuted: (instrument, muted) => {
|
|
15910
15892
|
this.validateReady();
|
|
15911
|
-
this.audioSystem?.
|
|
15893
|
+
this.audioSystem?.setOnsetMuted(instrument, muted);
|
|
15912
15894
|
},
|
|
15913
15895
|
/**
|
|
15914
|
-
*
|
|
15896
|
+
* Check if an instrument onset is muted
|
|
15915
15897
|
*/
|
|
15916
|
-
|
|
15898
|
+
isMuted: (instrument) => {
|
|
15917
15899
|
this.validateReady();
|
|
15918
|
-
this.audioSystem?.
|
|
15919
|
-
this.debugLog(`Beat phase reset (${this.instanceId})`);
|
|
15900
|
+
return this.audioSystem?.isOnsetMuted(instrument) ?? false;
|
|
15920
15901
|
}
|
|
15921
15902
|
},
|
|
15922
15903
|
/**
|
|
@@ -15931,17 +15912,6 @@ class VijiCore {
|
|
|
15931
15912
|
this.audioSystem?.setFFTSize(size);
|
|
15932
15913
|
this.debugLog(`FFT size set to ${size} (${this.instanceId})`);
|
|
15933
15914
|
},
|
|
15934
|
-
/**
|
|
15935
|
-
* Set smoothing time constant (0-1)
|
|
15936
|
-
*/
|
|
15937
|
-
setSmoothing: (value) => {
|
|
15938
|
-
this.validateReady();
|
|
15939
|
-
if (value < 0 || value > 1) {
|
|
15940
|
-
throw new VijiCoreError("Smoothing must be between 0 and 1", "INVALID_VALUE");
|
|
15941
|
-
}
|
|
15942
|
-
this.audioSystem?.setSmoothing(value);
|
|
15943
|
-
this.debugLog(`Smoothing set to ${value} (${this.instanceId})`);
|
|
15944
|
-
},
|
|
15945
15915
|
/**
|
|
15946
15916
|
* Enable/disable auto-gain
|
|
15947
15917
|
*/
|
|
@@ -15951,20 +15921,10 @@ class VijiCore {
|
|
|
15951
15921
|
this.debugLog(`Auto-gain ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
15952
15922
|
},
|
|
15953
15923
|
/**
|
|
15954
|
-
* Enable/disable
|
|
15924
|
+
* Enable/disable audio analysis debug mode (independent from core debug mode)
|
|
15955
15925
|
*/
|
|
15956
|
-
|
|
15957
|
-
this.
|
|
15958
|
-
this.audioSystem?.setBeatDetection(enabled);
|
|
15959
|
-
this.debugLog(`Beat detection ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
15960
|
-
},
|
|
15961
|
-
/**
|
|
15962
|
-
* Enable/disable onset detection
|
|
15963
|
-
*/
|
|
15964
|
-
setOnsetDetection: (enabled) => {
|
|
15965
|
-
this.validateReady();
|
|
15966
|
-
this.audioSystem?.setOnsetDetection(enabled);
|
|
15967
|
-
this.debugLog(`Onset detection ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
15926
|
+
setDebugMode: (enabled) => {
|
|
15927
|
+
this.audioSystem?.setDebugMode(enabled);
|
|
15968
15928
|
}
|
|
15969
15929
|
},
|
|
15970
15930
|
/**
|
|
@@ -15976,10 +15936,9 @@ class VijiCore {
|
|
|
15976
15936
|
isConnected: false,
|
|
15977
15937
|
currentBPM: 120,
|
|
15978
15938
|
confidence: 0,
|
|
15979
|
-
mode: "auto",
|
|
15980
|
-
tapCount: 0,
|
|
15981
15939
|
isLocked: false,
|
|
15982
|
-
|
|
15940
|
+
trackingState: "LEARNING",
|
|
15941
|
+
onsetModes: { kick: "auto", snare: "auto", hat: "auto" }
|
|
15983
15942
|
};
|
|
15984
15943
|
},
|
|
15985
15944
|
/**
|
|
@@ -16011,15 +15970,14 @@ class VijiCore {
|
|
|
16011
15970
|
any: 0,
|
|
16012
15971
|
kickSmoothed: 0,
|
|
16013
15972
|
snareSmoothed: 0,
|
|
15973
|
+
hatSmoothed: 0,
|
|
16014
15974
|
anySmoothed: 0,
|
|
16015
15975
|
triggers: { any: false, kick: false, snare: false, hat: false },
|
|
16016
15976
|
bpm: 120,
|
|
16017
|
-
phase: 0,
|
|
16018
|
-
bar: 0,
|
|
16019
15977
|
confidence: 0,
|
|
16020
15978
|
isLocked: false
|
|
16021
15979
|
},
|
|
16022
|
-
spectral: { brightness: 0, flatness: 0
|
|
15980
|
+
spectral: { brightness: 0, flatness: 0 }
|
|
16023
15981
|
};
|
|
16024
15982
|
}
|
|
16025
15983
|
};
|
|
@@ -16155,9 +16113,8 @@ class VijiCore {
|
|
|
16155
16113
|
async setDeviceVideo(deviceId, stream) {
|
|
16156
16114
|
this.validateReady();
|
|
16157
16115
|
await this.clearDeviceVideo(deviceId);
|
|
16158
|
-
const baseOffset = 1 + this.videoStreams.length;
|
|
16159
16116
|
const usedIndices = new Set(this.deviceVideoStreamIndices.values());
|
|
16160
|
-
let streamIndex =
|
|
16117
|
+
let streamIndex = VijiCore.DEVICE_VIDEO_BASE;
|
|
16161
16118
|
while (usedIndices.has(streamIndex)) {
|
|
16162
16119
|
streamIndex++;
|
|
16163
16120
|
}
|
|
@@ -16320,4 +16277,4 @@ export {
|
|
|
16320
16277
|
VijiCoreError as b,
|
|
16321
16278
|
getDefaultExportFromCjs as g
|
|
16322
16279
|
};
|
|
16323
|
-
//# sourceMappingURL=index-
|
|
16280
|
+
//# sourceMappingURL=index-trkn0FNW.js.map
|