@viji-dev/core 0.3.19 → 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-C7QoUtrj.js → essentia-wasm.web-C1URJxCY.js} +2 -2
- package/dist/{essentia-wasm.web-C7QoUtrj.js.map → essentia-wasm.web-C1URJxCY.js.map} +1 -1
- package/dist/{index-BKGarA3m.js → index-trkn0FNW.js} +699 -739
- 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-BKGarA3m.js.map +0 -1
|
@@ -73,8 +73,11 @@ class IFrameManager {
|
|
|
73
73
|
document.body.appendChild(hiddenContainer);
|
|
74
74
|
hiddenContainer.appendChild(iframe);
|
|
75
75
|
} else {
|
|
76
|
+
if (getComputedStyle(this.hostContainer).position === "static") {
|
|
77
|
+
this.hostContainer.style.position = "relative";
|
|
78
|
+
}
|
|
76
79
|
const visibility = this.initiallyVisible ? "" : "visibility:hidden;";
|
|
77
|
-
iframe.style.cssText = `width:100%;height:100%;border:none
|
|
80
|
+
iframe.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;border:none;${visibility}`;
|
|
78
81
|
this.hostContainer.appendChild(iframe);
|
|
79
82
|
}
|
|
80
83
|
const iframeContent = this.generateIFrameHTML();
|
|
@@ -564,7 +567,7 @@ class IFrameManager {
|
|
|
564
567
|
}
|
|
565
568
|
function WorkerWrapper(options) {
|
|
566
569
|
return new Worker(
|
|
567
|
-
"" + new URL("assets/viji.worker-
|
|
570
|
+
"" + new URL("assets/viji.worker-bm-hvzXt.js", import.meta.url).href,
|
|
568
571
|
{
|
|
569
572
|
type: "module",
|
|
570
573
|
name: options?.name
|
|
@@ -1478,13 +1481,17 @@ class MultiOnsetDetection {
|
|
|
1478
1481
|
const displayName = bandName === "low" ? "low" : "high";
|
|
1479
1482
|
if (onset) {
|
|
1480
1483
|
if (bandName === "low") this.debugOnsetCount++;
|
|
1481
|
-
if (this.
|
|
1482
|
-
|
|
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
|
+
}
|
|
1483
1488
|
}
|
|
1484
1489
|
this.consecutiveMisses[displayName] = { count: 0, lastReason: "" };
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
+
}
|
|
1488
1495
|
if (this.debugMode) {
|
|
1489
1496
|
const sixteenthNote = 6e4 / this.currentBPM / 4;
|
|
1490
1497
|
console.log(` 📊 Sharpness: ${sharpness.toFixed(3)} | Raw Flux: ${flux.toFixed(3)} | Emphasized: ${emphasizedFlux.toFixed(3)}`);
|
|
@@ -1500,7 +1507,7 @@ class MultiOnsetDetection {
|
|
|
1500
1507
|
}
|
|
1501
1508
|
this.consecutiveMisses[displayName].count++;
|
|
1502
1509
|
this.consecutiveMisses[displayName].lastReason = failReason;
|
|
1503
|
-
if (this.consecutiveMisses[displayName].count === 1) {
|
|
1510
|
+
if (this.debugMode && this.consecutiveMisses[displayName].count === 1) {
|
|
1504
1511
|
const timeSince = (now - state.lastOnsetTime).toFixed(0);
|
|
1505
1512
|
const logCutoff = bandName === "high" ? adjustedCutoff : state.cutoff;
|
|
1506
1513
|
console.log(`⚠️ [${displayName}] MISS (${failReason}) flux=${flux.toFixed(3)} energy=${energy.toFixed(2)} cutoff=${logCutoff.toFixed(2)} lastOnset=${timeSince}ms ago`);
|
|
@@ -1546,7 +1553,7 @@ class MultiOnsetDetection {
|
|
|
1546
1553
|
const state = this.bandStates.get(band);
|
|
1547
1554
|
if (state) {
|
|
1548
1555
|
state.primaryOnsetHistory.push(now);
|
|
1549
|
-
if (band === "low") {
|
|
1556
|
+
if (band === "low" && this.debugMode) {
|
|
1550
1557
|
console.log(` ✓ PRIMARY [low] onset recorded (total: ${state.primaryOnsetHistory.length})`);
|
|
1551
1558
|
}
|
|
1552
1559
|
}
|
|
@@ -1913,7 +1920,7 @@ class EssentiaOnsetDetection {
|
|
|
1913
1920
|
this.initPromise = (async () => {
|
|
1914
1921
|
try {
|
|
1915
1922
|
const essentiaModule = await import("./essentia.js-core.es-DnrJE0uR.js");
|
|
1916
|
-
const wasmModule = await import("./essentia-wasm.web-
|
|
1923
|
+
const wasmModule = await import("./essentia-wasm.web-C1URJxCY.js").then((n) => n.e);
|
|
1917
1924
|
const EssentiaClass = essentiaModule.Essentia || essentiaModule.default?.Essentia || essentiaModule.default;
|
|
1918
1925
|
let WASMModule = wasmModule.default || wasmModule.EssentiaWASM || wasmModule.default?.EssentiaWASM;
|
|
1919
1926
|
if (!WASMModule) {
|
|
@@ -2115,43 +2122,32 @@ class EssentiaOnsetDetection {
|
|
|
2115
2122
|
onset: globalOnset,
|
|
2116
2123
|
strength: clampedStrength,
|
|
2117
2124
|
bands: {
|
|
2118
|
-
// Low bands: Use complex domain (best for kicks)
|
|
2119
2125
|
low: {
|
|
2120
2126
|
onset: kickOnset,
|
|
2121
|
-
// INDEPENDENT detection!
|
|
2122
2127
|
strength: clampedComplex,
|
|
2123
2128
|
energy: bandEnergies.low,
|
|
2124
2129
|
sharpness: kickSharpness
|
|
2125
|
-
// Attack sharpness for kick/bassline discrimination
|
|
2126
2130
|
},
|
|
2127
2131
|
lowMid: {
|
|
2128
2132
|
onset: kickOnset,
|
|
2129
|
-
// Follows kick detection (kick harmonics)
|
|
2130
2133
|
strength: clampedComplex,
|
|
2131
2134
|
energy: bandEnergies.lowMid,
|
|
2132
2135
|
sharpness: kickSharpness
|
|
2133
2136
|
},
|
|
2134
|
-
// Mid band: Use spectral flux (best for snares)
|
|
2135
2137
|
mid: {
|
|
2136
2138
|
onset: snareOnset,
|
|
2137
|
-
// INDEPENDENT detection!
|
|
2138
2139
|
strength: clampedFlux,
|
|
2139
2140
|
energy: bandEnergies.mid,
|
|
2140
2141
|
sharpness: snareSharpness
|
|
2141
|
-
// Transient quality for snare detection
|
|
2142
2142
|
},
|
|
2143
|
-
// High bands: Use HFC (best for hats)
|
|
2144
2143
|
highMid: {
|
|
2145
2144
|
onset: hatOnset,
|
|
2146
|
-
// INDEPENDENT detection!
|
|
2147
2145
|
strength: clampedHFC,
|
|
2148
2146
|
energy: bandEnergies.highMid,
|
|
2149
2147
|
sharpness: hatSharpness
|
|
2150
|
-
// Transient quality for hat detection
|
|
2151
2148
|
},
|
|
2152
2149
|
high: {
|
|
2153
2150
|
onset: hatOnset,
|
|
2154
|
-
// INDEPENDENT detection!
|
|
2155
2151
|
strength: clampedHFC,
|
|
2156
2152
|
energy: bandEnergies.high,
|
|
2157
2153
|
sharpness: hatSharpness
|
|
@@ -10230,7 +10226,7 @@ const CONFIDENCE_LOCKED_THRESHOLD = 0.6;
|
|
|
10230
10226
|
const BREAKDOWN_THRESHOLD_MS = 2e3;
|
|
10231
10227
|
const LOST_THRESHOLD_MS = 3e4;
|
|
10232
10228
|
const GRID_INVALID_THRESHOLD = 0.25;
|
|
10233
|
-
const SAMPLE_RATE = 60;
|
|
10229
|
+
const SAMPLE_RATE$1 = 60;
|
|
10234
10230
|
class BeatStateManager {
|
|
10235
10231
|
// State machine
|
|
10236
10232
|
state = "TRACKING";
|
|
@@ -10267,13 +10263,14 @@ class BeatStateManager {
|
|
|
10267
10263
|
// Running average of onset strength
|
|
10268
10264
|
// Envelope followers for beat energy curves
|
|
10269
10265
|
envelopes = {
|
|
10270
|
-
kick: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10271
|
-
snare: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10272
|
-
hat: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10273
|
-
any: new EnvelopeFollower(0, 300, SAMPLE_RATE),
|
|
10274
|
-
kickSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE),
|
|
10275
|
-
snareSmoothed: new EnvelopeFollower(5, 500, SAMPLE_RATE),
|
|
10276
|
-
|
|
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)
|
|
10277
10274
|
};
|
|
10278
10275
|
// Confidence factors
|
|
10279
10276
|
tempoMethodAgreement = 0;
|
|
@@ -10514,10 +10511,11 @@ class BeatStateManager {
|
|
|
10514
10511
|
this.debugKickCount++;
|
|
10515
10512
|
const kickEvent = detectorEvents.find((e) => e.type === "kick");
|
|
10516
10513
|
const bands = kickEvent && "bands" in kickEvent ? kickEvent.bands : kickBands;
|
|
10514
|
+
const rawKickStrength = kickEvent?.strength || onsets.bands.low.strength;
|
|
10517
10515
|
const event = {
|
|
10518
10516
|
type: "kick",
|
|
10519
10517
|
time: now,
|
|
10520
|
-
strength:
|
|
10518
|
+
strength: Math.min(1, rawKickStrength / 5),
|
|
10521
10519
|
phase: pll.getPhase(),
|
|
10522
10520
|
bar: pll.getBar(),
|
|
10523
10521
|
isLayered: classification.snare || classification.hat,
|
|
@@ -10538,10 +10536,11 @@ class BeatStateManager {
|
|
|
10538
10536
|
this.debugSnareCount++;
|
|
10539
10537
|
const snareEvent = detectorEvents.find((e) => e.type === "snare");
|
|
10540
10538
|
const bands = snareEvent && "bands" in snareEvent ? snareEvent.bands : snareBands;
|
|
10539
|
+
const rawSnareStrength = snareEvent?.strength || onsets.bands.mid.strength;
|
|
10541
10540
|
const event = {
|
|
10542
10541
|
type: "snare",
|
|
10543
10542
|
time: now,
|
|
10544
|
-
strength:
|
|
10543
|
+
strength: Math.min(1, rawSnareStrength / 5),
|
|
10545
10544
|
phase: pll.getPhase(),
|
|
10546
10545
|
bar: pll.getBar(),
|
|
10547
10546
|
isLayered: classification.kick || classification.hat,
|
|
@@ -10562,10 +10561,11 @@ class BeatStateManager {
|
|
|
10562
10561
|
this.debugHatCount++;
|
|
10563
10562
|
const hatEvent = detectorEvents.find((e) => e.type === "hat");
|
|
10564
10563
|
const bands = hatEvent && "bands" in hatEvent ? hatEvent.bands : hatBands;
|
|
10564
|
+
const rawHatStrength = hatEvent?.strength || onsets.bands.high.strength;
|
|
10565
10565
|
const event = {
|
|
10566
10566
|
type: "hat",
|
|
10567
10567
|
time: now,
|
|
10568
|
-
strength:
|
|
10568
|
+
strength: Math.min(1, rawHatStrength / 3e3),
|
|
10569
10569
|
phase: pll.getPhase(),
|
|
10570
10570
|
bar: pll.getBar(),
|
|
10571
10571
|
isLayered: classification.kick || classification.snare,
|
|
@@ -10596,6 +10596,7 @@ class BeatStateManager {
|
|
|
10596
10596
|
any: this.envelopes.any.getValue(),
|
|
10597
10597
|
kickSmoothed: this.envelopes.kickSmoothed.getValue(),
|
|
10598
10598
|
snareSmoothed: this.envelopes.snareSmoothed.getValue(),
|
|
10599
|
+
hatSmoothed: this.envelopes.hatSmoothed.getValue(),
|
|
10599
10600
|
anySmoothed: this.envelopes.anySmoothed.getValue(),
|
|
10600
10601
|
events: bufferedEvents,
|
|
10601
10602
|
// Buffered events (persisted for 20ms, reliable for async access)
|
|
@@ -11184,9 +11185,7 @@ class BeatStateManager {
|
|
|
11184
11185
|
if (snare) {
|
|
11185
11186
|
const isLayered = timeSinceLastPrimaryKick <= 80;
|
|
11186
11187
|
const midSharpness2 = bands.mid.sharpness ?? 1;
|
|
11187
|
-
const isHighQuality = midSharpness2 > 0.25 &&
|
|
11188
|
-
bands.mid.strength > 0.3 && // Strong enough to be reliable
|
|
11189
|
-
midRatio > 0.1;
|
|
11188
|
+
const isHighQuality = midSharpness2 > 0.25 && bands.mid.strength > 0.3 && midRatio > 0.1;
|
|
11190
11189
|
if (isHighQuality) {
|
|
11191
11190
|
if (isLayered) {
|
|
11192
11191
|
this.adaptiveProfiles.snareLayered.samples.push({
|
|
@@ -11416,6 +11415,7 @@ class BeatStateManager {
|
|
|
11416
11415
|
}
|
|
11417
11416
|
if (classification.hat) {
|
|
11418
11417
|
this.envelopes.hat.trigger(1);
|
|
11418
|
+
this.envelopes.hatSmoothed.trigger(1);
|
|
11419
11419
|
this.envelopes.any.trigger(0.5);
|
|
11420
11420
|
}
|
|
11421
11421
|
if (classification.any && !classification.kick && !classification.snare && !classification.hat) {
|
|
@@ -11428,8 +11428,32 @@ class BeatStateManager {
|
|
|
11428
11428
|
this.envelopes.any.process(0, dtMs);
|
|
11429
11429
|
this.envelopes.kickSmoothed.process(0, dtMs);
|
|
11430
11430
|
this.envelopes.snareSmoothed.process(0, dtMs);
|
|
11431
|
+
this.envelopes.hatSmoothed.process(0, dtMs);
|
|
11431
11432
|
this.envelopes.anySmoothed.process(0, dtMs);
|
|
11432
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
|
+
}
|
|
11433
11457
|
// ========== INDUSTRY-STANDARD HELPER METHODS ==========
|
|
11434
11458
|
// NOTE: calculateGridAlignment() and calculateIOIConsistency() removed in Phase 1
|
|
11435
11459
|
// These were part of advanced filtering that we disabled to maintain compatibility
|
|
@@ -12348,6 +12372,382 @@ FFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff, off
|
|
|
12348
12372
|
out[outOff + 7] = FDi;
|
|
12349
12373
|
};
|
|
12350
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
|
+
}
|
|
12351
12751
|
class AudioSystem {
|
|
12352
12752
|
// Audio context and analysis nodes
|
|
12353
12753
|
audioContext = null;
|
|
@@ -12357,14 +12757,8 @@ class AudioSystem {
|
|
|
12357
12757
|
analysisMode = "analyser";
|
|
12358
12758
|
workletNode = null;
|
|
12359
12759
|
workletReady = false;
|
|
12760
|
+
workletRegistered = false;
|
|
12360
12761
|
currentSampleRate = 44100;
|
|
12361
|
-
pcmRing = null;
|
|
12362
|
-
pcmWriteIndex = 0;
|
|
12363
|
-
pcmFilled = 0;
|
|
12364
|
-
pcmHistorySeconds = 8;
|
|
12365
|
-
lastTempoExtraction = 0;
|
|
12366
|
-
tempoExtractionIntervalMs = 2e3;
|
|
12367
|
-
analysisBackend = "auto";
|
|
12368
12762
|
bandNoiseFloor = {
|
|
12369
12763
|
low: 1e-4,
|
|
12370
12764
|
lowMid: 1e-4,
|
|
@@ -12376,16 +12770,11 @@ class AudioSystem {
|
|
|
12376
12770
|
lastNonZeroBpm = 120;
|
|
12377
12771
|
/** Tracks which source provided the current BPM (pll | tempo | carry | default) */
|
|
12378
12772
|
lastBpmSource = "default";
|
|
12379
|
-
aubioWarningLogged = false;
|
|
12380
|
-
useEssentiaTempo = false;
|
|
12381
|
-
// disabled by default to avoid WASM exception in some builds
|
|
12382
12773
|
workletFrameCount = 0;
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12774
|
+
lastFrameTime = 0;
|
|
12775
|
+
stalenessTimer = null;
|
|
12776
|
+
static STALENESS_THRESHOLD_MS = 500;
|
|
12386
12777
|
analysisTicks = 0;
|
|
12387
|
-
lastLoopWarn = 0;
|
|
12388
|
-
verboseLabLogs = this.isAudioLab;
|
|
12389
12778
|
lastPhaseLogTime = 0;
|
|
12390
12779
|
onsetLogBuffer = [];
|
|
12391
12780
|
// Debug logging control
|
|
@@ -12414,21 +12803,14 @@ class AudioSystem {
|
|
|
12414
12803
|
};
|
|
12415
12804
|
/** Last dt used in analysis loop (ms) */
|
|
12416
12805
|
lastDtMs = 0;
|
|
12417
|
-
/** Flag to disable auto-gain for debugging */
|
|
12418
|
-
debugDisableAutoGain = false;
|
|
12419
12806
|
/** Flag to track if we've logged detection method (to avoid spam) */
|
|
12420
12807
|
bandNames = ["low", "lowMid", "mid", "highMid", "high"];
|
|
12421
12808
|
essentiaBandHistories = /* @__PURE__ */ new Map();
|
|
12422
12809
|
essentiaHistoryWindowMs = 5e3;
|
|
12423
|
-
//
|
|
12424
|
-
|
|
12425
|
-
manualBPM = null;
|
|
12426
|
-
tapHistory = [];
|
|
12427
|
-
tapTimeout = null;
|
|
12810
|
+
// Per-instrument onset tap manager
|
|
12811
|
+
onsetTapManager;
|
|
12428
12812
|
// Envelope followers for smooth energy curves
|
|
12429
12813
|
envelopeFollowers;
|
|
12430
|
-
// Global sensitivity multiplier
|
|
12431
|
-
sensitivity = 1;
|
|
12432
12814
|
// Feature enable flags
|
|
12433
12815
|
beatDetectionEnabled = true;
|
|
12434
12816
|
onsetDetectionEnabled = true;
|
|
@@ -12443,13 +12825,9 @@ class AudioSystem {
|
|
|
12443
12825
|
this.tempoInduction.setDebugMode(enabled);
|
|
12444
12826
|
this.stateManager.setDebugMode(enabled);
|
|
12445
12827
|
this.essentiaOnsetDetection?.setDebugMode(enabled);
|
|
12446
|
-
this.toggleStatusLogger(enabled);
|
|
12447
|
-
this.verboseLabLogs = this.isAudioLab || this.debugMode;
|
|
12448
12828
|
if (enabled) {
|
|
12449
12829
|
this.diagnosticLogger.start();
|
|
12450
|
-
|
|
12451
|
-
if (enabled) {
|
|
12452
|
-
console.log("🔬 [AudioSystem] Debug mode enabled");
|
|
12830
|
+
this.debugLog("[AudioSystem] Debug mode enabled");
|
|
12453
12831
|
}
|
|
12454
12832
|
}
|
|
12455
12833
|
/**
|
|
@@ -12487,42 +12865,6 @@ class AudioSystem {
|
|
|
12487
12865
|
}
|
|
12488
12866
|
return clone;
|
|
12489
12867
|
}
|
|
12490
|
-
logVerbose(label, payload) {
|
|
12491
|
-
if (this.debugMode || this.verboseLabLogs) {
|
|
12492
|
-
console.log(label, payload);
|
|
12493
|
-
}
|
|
12494
|
-
}
|
|
12495
|
-
/**
|
|
12496
|
-
* Start/stop periodic status logging. If maxSamples provided, stops after that many ticks.
|
|
12497
|
-
*/
|
|
12498
|
-
toggleStatusLogger(enabled, maxSamples) {
|
|
12499
|
-
if (enabled && !this.statusLogTimer) {
|
|
12500
|
-
let samples = 0;
|
|
12501
|
-
this.statusLogTimer = setInterval(() => {
|
|
12502
|
-
samples++;
|
|
12503
|
-
const a = this.audioState;
|
|
12504
|
-
const maxBand = Math.max(a.bands.low, a.bands.lowMid, a.bands.mid, a.bands.highMid, a.bands.high);
|
|
12505
|
-
console.log("[AudioSystem][status]", {
|
|
12506
|
-
mode: this.analysisMode,
|
|
12507
|
-
workletReady: this.workletReady,
|
|
12508
|
-
workletFrames: this.workletFrameCount,
|
|
12509
|
-
isConnected: a.isConnected,
|
|
12510
|
-
vol: { cur: a.volume.current.toFixed(3), peak: a.volume.peak.toFixed(3) },
|
|
12511
|
-
maxBand: maxBand.toFixed(3),
|
|
12512
|
-
bpm: a.beat.bpm.toFixed(1),
|
|
12513
|
-
bpmSource: this.lastBpmSource,
|
|
12514
|
-
locked: a.beat.isLocked,
|
|
12515
|
-
events: a.beat.events.length
|
|
12516
|
-
});
|
|
12517
|
-
if (maxSamples && samples >= maxSamples) {
|
|
12518
|
-
this.toggleStatusLogger(false);
|
|
12519
|
-
}
|
|
12520
|
-
}, 1e3);
|
|
12521
|
-
} else if (!enabled && this.statusLogTimer) {
|
|
12522
|
-
clearInterval(this.statusLogTimer);
|
|
12523
|
-
this.statusLogTimer = null;
|
|
12524
|
-
}
|
|
12525
|
-
}
|
|
12526
12868
|
/**
|
|
12527
12869
|
* Handle frames pushed from AudioWorklet
|
|
12528
12870
|
*/
|
|
@@ -12536,8 +12878,9 @@ class AudioSystem {
|
|
|
12536
12878
|
*/
|
|
12537
12879
|
analyzeFrame(frame, sampleRate, timestampMs) {
|
|
12538
12880
|
if (!frame || frame.length === 0) return;
|
|
12881
|
+
this.lastFrameTime = performance.now();
|
|
12539
12882
|
if (this.workletFrameCount === 1) {
|
|
12540
|
-
|
|
12883
|
+
this.debugLog("[AudioSystem] First frame received", { sampleRate, mode: this.analysisMode });
|
|
12541
12884
|
}
|
|
12542
12885
|
const dtMs = this.lastAnalysisTimestamp > 0 ? timestampMs - this.lastAnalysisTimestamp : 1e3 / 60;
|
|
12543
12886
|
this.lastAnalysisTimestamp = timestampMs;
|
|
@@ -12546,15 +12889,11 @@ class AudioSystem {
|
|
|
12546
12889
|
const { rms, peak } = this.calculateVolumeMetrics(frame);
|
|
12547
12890
|
this.audioState.volume.current = rms;
|
|
12548
12891
|
this.audioState.volume.peak = peak;
|
|
12549
|
-
this.
|
|
12550
|
-
if (this.useEssentiaTempo && timestampMs - this.lastTempoExtraction > this.tempoExtractionIntervalMs) {
|
|
12551
|
-
this.runEssentiaTempoEstimate(sampleRate);
|
|
12552
|
-
this.lastTempoExtraction = timestampMs;
|
|
12553
|
-
}
|
|
12892
|
+
this.lastWaveformFrame = frame;
|
|
12554
12893
|
const fftResult = this.computeFFT(frame);
|
|
12555
12894
|
if (!fftResult) {
|
|
12556
|
-
if (this.analysisTicks === 1 || this.analysisTicks % 240 === 0) {
|
|
12557
|
-
|
|
12895
|
+
if (this.debugMode && (this.analysisTicks === 1 || this.analysisTicks % 240 === 0)) {
|
|
12896
|
+
this.debugLog("[AudioSystem][analyzeFrame] computeFFT returned null at tick", this.analysisTicks);
|
|
12558
12897
|
}
|
|
12559
12898
|
return;
|
|
12560
12899
|
}
|
|
@@ -12575,80 +12914,76 @@ class AudioSystem {
|
|
|
12575
12914
|
high: 0
|
|
12576
12915
|
};
|
|
12577
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 = [];
|
|
12578
12928
|
this.updateSmoothBands(dtMs);
|
|
12579
12929
|
this.sendAnalysisResultsToWorker();
|
|
12580
12930
|
return;
|
|
12581
12931
|
}
|
|
12582
|
-
const rawBandsForOnsets = {
|
|
12583
|
-
low: bandEnergies.low * this.sensitivity,
|
|
12584
|
-
lowMid: bandEnergies.lowMid * this.sensitivity,
|
|
12585
|
-
mid: bandEnergies.mid * this.sensitivity,
|
|
12586
|
-
highMid: bandEnergies.highMid * this.sensitivity,
|
|
12587
|
-
high: bandEnergies.high * this.sensitivity
|
|
12588
|
-
};
|
|
12589
12932
|
this.rawBandsPreGain = { ...bandEnergies };
|
|
12590
12933
|
if (this.autoGainEnabled) {
|
|
12591
12934
|
this.applyAutoGain();
|
|
12592
12935
|
}
|
|
12593
12936
|
this.calculateSpectralFeaturesFromMagnitude(magnitudes, sampleRate, maxMagnitude);
|
|
12594
12937
|
if (this.onsetDetectionEnabled && this.beatDetectionEnabled) {
|
|
12595
|
-
const beatState = this.runBeatPipeline(
|
|
12938
|
+
const beatState = this.runBeatPipeline(bandEnergies, dtMs, timestampMs, sampleRate);
|
|
12596
12939
|
if (beatState) {
|
|
12597
|
-
|
|
12598
|
-
const
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
trackingData.bpmVariance = bpmVariance;
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
trackingData.
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
lowMid: this.audioState.bands.lowMid,
|
|
12645
|
-
mid: this.audioState.bands.mid,
|
|
12646
|
-
highMid: this.audioState.bands.highMid,
|
|
12647
|
-
high: this.audioState.bands.high
|
|
12648
|
-
},
|
|
12649
|
-
this.stateManager.getLastRejections(),
|
|
12650
|
-
trackingData
|
|
12651
|
-
);
|
|
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
|
+
}
|
|
12652
12987
|
}
|
|
12653
12988
|
}
|
|
12654
12989
|
this.updateSmoothBands(dtMs);
|
|
@@ -12658,30 +12993,9 @@ class AudioSystem {
|
|
|
12658
12993
|
* Compute FFT and derived arrays
|
|
12659
12994
|
*/
|
|
12660
12995
|
computeFFT(frame) {
|
|
12661
|
-
if (this.analysisTicks === 1) {
|
|
12662
|
-
console.log("[AudioSystem][computeFFT] FIRST CALL", {
|
|
12663
|
-
hasEngine: !!this.fftEngine,
|
|
12664
|
-
hasInput: !!this.fftInput,
|
|
12665
|
-
hasOutput: !!this.fftOutput,
|
|
12666
|
-
hasWindow: !!this.hannWindow,
|
|
12667
|
-
hasFreqData: !!this.frequencyData,
|
|
12668
|
-
hasMagnitude: !!this.fftMagnitude,
|
|
12669
|
-
hasMagnitudeDb: !!this.fftMagnitudeDb,
|
|
12670
|
-
hasPhase: !!this.fftPhase
|
|
12671
|
-
});
|
|
12672
|
-
}
|
|
12673
12996
|
if (!this.fftEngine || !this.fftInput || !this.fftOutput || !this.hannWindow || !this.frequencyData || !this.fftMagnitude || !this.fftMagnitudeDb || !this.fftPhase) {
|
|
12674
|
-
if (this.analysisTicks === 1 || this.analysisTicks % 240 === 0) {
|
|
12675
|
-
|
|
12676
|
-
console.warn(" tick:", this.analysisTicks);
|
|
12677
|
-
console.warn(" hasEngine:", !!this.fftEngine);
|
|
12678
|
-
console.warn(" hasInput:", !!this.fftInput);
|
|
12679
|
-
console.warn(" hasOutput:", !!this.fftOutput);
|
|
12680
|
-
console.warn(" hasWindow:", !!this.hannWindow);
|
|
12681
|
-
console.warn(" hasFreqData:", !!this.frequencyData);
|
|
12682
|
-
console.warn(" hasMagnitude:", !!this.fftMagnitude);
|
|
12683
|
-
console.warn(" hasMagnitudeDb:", !!this.fftMagnitudeDb);
|
|
12684
|
-
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);
|
|
12685
12999
|
}
|
|
12686
13000
|
return null;
|
|
12687
13001
|
}
|
|
@@ -12838,63 +13152,13 @@ class AudioSystem {
|
|
|
12838
13152
|
} else {
|
|
12839
13153
|
this.audioState.spectral.flatness = 0;
|
|
12840
13154
|
}
|
|
12841
|
-
const centroidChange = Math.abs(centroid - this.prevSpectralCentroid);
|
|
12842
|
-
this.audioState.spectral.flux = Math.min(1, centroidChange / 1e3);
|
|
12843
|
-
this.prevSpectralCentroid = centroid;
|
|
12844
|
-
}
|
|
12845
|
-
/**
|
|
12846
|
-
* Append PCM frame to ring buffer for periodic Essentia tempo extraction
|
|
12847
|
-
*/
|
|
12848
|
-
appendPcmFrame(frame, sampleRate) {
|
|
12849
|
-
const requiredSize = Math.max(this.fftSize, Math.floor(sampleRate * this.pcmHistorySeconds));
|
|
12850
|
-
if (!this.pcmRing || this.pcmRing.length !== requiredSize) {
|
|
12851
|
-
this.pcmRing = new Float32Array(requiredSize);
|
|
12852
|
-
this.pcmWriteIndex = 0;
|
|
12853
|
-
this.pcmFilled = 0;
|
|
12854
|
-
}
|
|
12855
|
-
for (let i = 0; i < frame.length; i++) {
|
|
12856
|
-
this.pcmRing[this.pcmWriteIndex] = frame[i];
|
|
12857
|
-
this.pcmWriteIndex = (this.pcmWriteIndex + 1) % this.pcmRing.length;
|
|
12858
|
-
if (this.pcmFilled < this.pcmRing.length) {
|
|
12859
|
-
this.pcmFilled++;
|
|
12860
|
-
}
|
|
12861
|
-
}
|
|
12862
|
-
}
|
|
12863
|
-
/**
|
|
12864
|
-
* Periodically estimate tempo using Essentia's RhythmExtractor2013 (offline chunk)
|
|
12865
|
-
*/
|
|
12866
|
-
runEssentiaTempoEstimate(sampleRate) {
|
|
12867
|
-
if (!this.essentiaOnsetDetection?.isReady() || !this.pcmRing) {
|
|
12868
|
-
return;
|
|
12869
|
-
}
|
|
12870
|
-
const maxSamples = Math.min(this.pcmRing.length, Math.floor(sampleRate * this.pcmHistorySeconds));
|
|
12871
|
-
const available = Math.min(this.pcmFilled, maxSamples);
|
|
12872
|
-
if (available < sampleRate * 2) return;
|
|
12873
|
-
const window2 = new Float32Array(available);
|
|
12874
|
-
let idx = (this.pcmWriteIndex - available + this.pcmRing.length) % this.pcmRing.length;
|
|
12875
|
-
for (let i = 0; i < available; i++) {
|
|
12876
|
-
window2[i] = this.pcmRing[idx];
|
|
12877
|
-
idx = (idx + 1) % this.pcmRing.length;
|
|
12878
|
-
}
|
|
12879
|
-
const tempo = this.essentiaOnsetDetection.detectTempoFromPCM(window2, sampleRate);
|
|
12880
|
-
if (tempo?.bpm) {
|
|
12881
|
-
const bpm = Math.max(60, Math.min(200, tempo.bpm));
|
|
12882
|
-
this.pll.setBPM(bpm);
|
|
12883
|
-
this.tempoInduction.setManualBPM(bpm);
|
|
12884
|
-
}
|
|
12885
13155
|
}
|
|
12886
13156
|
/**
|
|
12887
13157
|
* Run onset + beat detection pipeline and return BeatState
|
|
12888
13158
|
*/
|
|
12889
13159
|
runBeatPipeline(rawBandsForOnsets, dtMs, timestampMs, sampleRate) {
|
|
12890
13160
|
let onsets;
|
|
12891
|
-
const
|
|
12892
|
-
const allowEssentia = backend === "auto" || backend === "essentia";
|
|
12893
|
-
if (backend === "aubio" && !this.aubioWarningLogged) {
|
|
12894
|
-
console.warn("[AudioSystem] Aubio backend selected but not wired yet; using custom pipeline.");
|
|
12895
|
-
this.aubioWarningLogged = true;
|
|
12896
|
-
}
|
|
12897
|
-
const essentiaReady = allowEssentia && (this.essentiaOnsetDetection?.isReady() || false) && !!this.fftMagnitudeDb && !!this.fftPhase;
|
|
13161
|
+
const essentiaReady = (this.essentiaOnsetDetection?.isReady() || false) && !!this.fftMagnitudeDb && !!this.fftPhase;
|
|
12898
13162
|
const hasFftData = !!(this.fftMagnitudeDb && this.fftPhase);
|
|
12899
13163
|
if (essentiaReady && hasFftData) {
|
|
12900
13164
|
const essentiaResult = this.essentiaOnsetDetection.detectFromSpectrum(
|
|
@@ -12917,9 +13181,6 @@ class AudioSystem {
|
|
|
12917
13181
|
} else {
|
|
12918
13182
|
onsets = this.onsetDetection.detect(rawBandsForOnsets, dtMs);
|
|
12919
13183
|
}
|
|
12920
|
-
if (this.beatMode === "manual" && this.manualBPM !== null) {
|
|
12921
|
-
this.tempoInduction.setManualBPM(this.manualBPM);
|
|
12922
|
-
}
|
|
12923
13184
|
const tempo = this.tempoInduction.update(onsets, dtMs);
|
|
12924
13185
|
this.onsetDetection.setBPM(tempo.bpm);
|
|
12925
13186
|
const trackingConf = this.stateManager.calculateTrackingConfidence(tempo);
|
|
@@ -12998,7 +13259,7 @@ class AudioSystem {
|
|
|
12998
13259
|
const prevSource = this.lastBpmSource;
|
|
12999
13260
|
this.lastNonZeroBpm = resolvedBpm;
|
|
13000
13261
|
this.lastBpmSource = selected.source;
|
|
13001
|
-
if (
|
|
13262
|
+
if (this.debugMode && prevSource !== this.lastBpmSource) {
|
|
13002
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})`);
|
|
13003
13264
|
}
|
|
13004
13265
|
const mergedBeatState = beatState.bpm === resolvedBpm ? beatState : { ...beatState, bpm: resolvedBpm };
|
|
@@ -13016,8 +13277,6 @@ class AudioSystem {
|
|
|
13016
13277
|
// Analysis configuration (optimized for onset detection)
|
|
13017
13278
|
fftSize = 2048;
|
|
13018
13279
|
// Good balance for quality vs performance
|
|
13019
|
-
smoothingTimeConstant = 0;
|
|
13020
|
-
// NO smoothing for onset detection (was 0.5 which killed transients!)
|
|
13021
13280
|
// High-speed analysis for onset detection (separate from RAF)
|
|
13022
13281
|
analysisInterval = null;
|
|
13023
13282
|
analysisIntervalMs = 8;
|
|
@@ -13060,28 +13319,26 @@ class AudioSystem {
|
|
|
13060
13319
|
any: 0,
|
|
13061
13320
|
kickSmoothed: 0,
|
|
13062
13321
|
snareSmoothed: 0,
|
|
13322
|
+
hatSmoothed: 0,
|
|
13063
13323
|
anySmoothed: 0,
|
|
13064
13324
|
events: [],
|
|
13065
13325
|
bpm: 120,
|
|
13066
|
-
phase: 0,
|
|
13067
|
-
bar: 0,
|
|
13068
13326
|
confidence: 0,
|
|
13069
13327
|
isLocked: false
|
|
13070
13328
|
},
|
|
13071
13329
|
spectral: {
|
|
13072
13330
|
brightness: 0,
|
|
13073
|
-
flatness: 0
|
|
13074
|
-
flux: 0
|
|
13331
|
+
flatness: 0
|
|
13075
13332
|
}
|
|
13076
13333
|
};
|
|
13334
|
+
// Waveform data for transfer to worker
|
|
13335
|
+
lastWaveformFrame = null;
|
|
13077
13336
|
// Analysis loop
|
|
13078
13337
|
analysisLoopId = null;
|
|
13079
13338
|
isAnalysisRunning = false;
|
|
13080
13339
|
lastAnalysisTimestamp = 0;
|
|
13081
13340
|
// Callback to send results to worker
|
|
13082
13341
|
sendAnalysisResults = null;
|
|
13083
|
-
// Previous spectral centroid for change calculation
|
|
13084
|
-
prevSpectralCentroid = 0;
|
|
13085
13342
|
constructor(sendAnalysisResultsCallback) {
|
|
13086
13343
|
this.handleAudioStreamUpdate = this.handleAudioStreamUpdate.bind(this);
|
|
13087
13344
|
this.performAnalysis = this.performAnalysis.bind(this);
|
|
@@ -13092,6 +13349,7 @@ class AudioSystem {
|
|
|
13092
13349
|
this.tempoInduction = new TempoInduction();
|
|
13093
13350
|
this.pll = new PhaseLockedLoop();
|
|
13094
13351
|
this.stateManager = new BeatStateManager();
|
|
13352
|
+
this.onsetTapManager = new OnsetTapManager();
|
|
13095
13353
|
this.volumeAutoGain = new AutoGain(3e3, 60);
|
|
13096
13354
|
this.bandAutoGain = {
|
|
13097
13355
|
low: new AutoGain(3e3, 60),
|
|
@@ -13109,6 +13367,7 @@ class AudioSystem {
|
|
|
13109
13367
|
any: new EnvelopeFollower(0, 300, sampleRate),
|
|
13110
13368
|
kickSmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13111
13369
|
snareSmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13370
|
+
hatSmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13112
13371
|
anySmoothed: new EnvelopeFollower(5, 500, sampleRate),
|
|
13113
13372
|
// Volume smoothing
|
|
13114
13373
|
volumeSmoothed: new EnvelopeFollower(50, 200, sampleRate),
|
|
@@ -13121,10 +13380,6 @@ class AudioSystem {
|
|
|
13121
13380
|
};
|
|
13122
13381
|
this.refreshFFTResources();
|
|
13123
13382
|
this.resetEssentiaBandHistories();
|
|
13124
|
-
if (this.isAudioLab) {
|
|
13125
|
-
this.forceAnalyser = true;
|
|
13126
|
-
console.warn("[AudioSystem] Audio-lab context detected: forcing analyser path for stability.");
|
|
13127
|
-
}
|
|
13128
13383
|
}
|
|
13129
13384
|
/**
|
|
13130
13385
|
* Prepare FFT buffers and windowing for the selected fftSize
|
|
@@ -13160,9 +13415,7 @@ class AudioSystem {
|
|
|
13160
13415
|
if (!this.essentiaOnsetDetection) return;
|
|
13161
13416
|
try {
|
|
13162
13417
|
await this.essentiaOnsetDetection.initialize();
|
|
13163
|
-
|
|
13164
|
-
console.log("✅ [AudioSystem] Essentia.js initialized successfully!");
|
|
13165
|
-
}
|
|
13418
|
+
this.debugLog("[AudioSystem] Essentia.js initialized successfully");
|
|
13166
13419
|
} catch (error) {
|
|
13167
13420
|
console.warn("⚠️ [AudioSystem] Essentia.js initialization failed, using custom onset detection:", error);
|
|
13168
13421
|
}
|
|
@@ -13192,9 +13445,6 @@ class AudioSystem {
|
|
|
13192
13445
|
} else {
|
|
13193
13446
|
this.disconnectAudioStream();
|
|
13194
13447
|
}
|
|
13195
|
-
if (data.analysisConfig) {
|
|
13196
|
-
this.updateAnalysisConfig(data.analysisConfig);
|
|
13197
|
-
}
|
|
13198
13448
|
} catch (error) {
|
|
13199
13449
|
console.error("Error handling audio stream update:", error);
|
|
13200
13450
|
this.audioState.isConnected = false;
|
|
@@ -13225,7 +13475,7 @@ class AudioSystem {
|
|
|
13225
13475
|
}
|
|
13226
13476
|
}
|
|
13227
13477
|
this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioStream);
|
|
13228
|
-
this.workletReady =
|
|
13478
|
+
this.workletReady = await this.setupAudioWorklet();
|
|
13229
13479
|
if (this.workletReady && this.workletNode) {
|
|
13230
13480
|
this.analysisMode = "worklet";
|
|
13231
13481
|
this.currentSampleRate = this.audioContext.sampleRate;
|
|
@@ -13234,7 +13484,7 @@ class AudioSystem {
|
|
|
13234
13484
|
this.debugLog("Audio worklet analysis enabled");
|
|
13235
13485
|
setTimeout(() => {
|
|
13236
13486
|
if (this.analysisMode === "worklet" && this.workletFrameCount === 0) {
|
|
13237
|
-
|
|
13487
|
+
this.debugLog("[AudioSystem] Worklet produced no frames, falling back to analyser.");
|
|
13238
13488
|
this.analysisMode = "analyser";
|
|
13239
13489
|
if (this.workletNode) {
|
|
13240
13490
|
try {
|
|
@@ -13247,7 +13497,7 @@ class AudioSystem {
|
|
|
13247
13497
|
this.workletReady = false;
|
|
13248
13498
|
this.analyser = this.audioContext.createAnalyser();
|
|
13249
13499
|
this.analyser.fftSize = this.fftSize;
|
|
13250
|
-
this.analyser.smoothingTimeConstant =
|
|
13500
|
+
this.analyser.smoothingTimeConstant = 0;
|
|
13251
13501
|
this.mediaStreamSource.connect(this.analyser);
|
|
13252
13502
|
bufferLength = this.analyser.frequencyBinCount;
|
|
13253
13503
|
this.frequencyData = new Uint8Array(bufferLength);
|
|
@@ -13259,7 +13509,7 @@ class AudioSystem {
|
|
|
13259
13509
|
this.analysisMode = "analyser";
|
|
13260
13510
|
this.analyser = this.audioContext.createAnalyser();
|
|
13261
13511
|
this.analyser.fftSize = this.fftSize;
|
|
13262
|
-
this.analyser.smoothingTimeConstant =
|
|
13512
|
+
this.analyser.smoothingTimeConstant = 0;
|
|
13263
13513
|
this.mediaStreamSource.connect(this.analyser);
|
|
13264
13514
|
this.debugLog("Audio worklet unavailable, using analyser fallback");
|
|
13265
13515
|
bufferLength = this.analyser.frequencyBinCount;
|
|
@@ -13275,17 +13525,17 @@ class AudioSystem {
|
|
|
13275
13525
|
this.startAnalysisLoop();
|
|
13276
13526
|
}
|
|
13277
13527
|
if (this.analysisMode === "analyser" && !this.isAnalysisRunning) {
|
|
13278
|
-
|
|
13528
|
+
this.debugLog("[AudioSystem] Analysis loop not running after setup, forcing start.");
|
|
13279
13529
|
this.startAnalysisLoop();
|
|
13280
13530
|
}
|
|
13281
13531
|
const tracks = audioStream.getAudioTracks();
|
|
13282
|
-
this.
|
|
13532
|
+
this.debugLog("[AudioSystem] Stream info", {
|
|
13283
13533
|
trackCount: tracks.length,
|
|
13284
13534
|
trackSettings: tracks.map((t) => t.getSettings?.() || {}),
|
|
13285
13535
|
trackMuted: tracks.map((t) => t.muted),
|
|
13286
|
-
forceAnalyser: this.forceAnalyser,
|
|
13287
13536
|
mode: this.analysisMode
|
|
13288
13537
|
});
|
|
13538
|
+
this.startStalenessTimer();
|
|
13289
13539
|
this.debugLog("Audio stream connected successfully (host-side)", {
|
|
13290
13540
|
sampleRate: this.audioContext.sampleRate,
|
|
13291
13541
|
fftSize: this.fftSize,
|
|
@@ -13303,6 +13553,7 @@ class AudioSystem {
|
|
|
13303
13553
|
*/
|
|
13304
13554
|
disconnectAudioStream() {
|
|
13305
13555
|
this.stopAnalysisLoop();
|
|
13556
|
+
this.stopStalenessTimer();
|
|
13306
13557
|
this.resetEssentiaBandHistories();
|
|
13307
13558
|
if (this.mediaStreamSource) {
|
|
13308
13559
|
this.mediaStreamSource.disconnect();
|
|
@@ -13333,27 +13584,6 @@ class AudioSystem {
|
|
|
13333
13584
|
this.sendAnalysisResultsToWorker();
|
|
13334
13585
|
this.debugLog("Audio stream disconnected (host-side)");
|
|
13335
13586
|
}
|
|
13336
|
-
/**
|
|
13337
|
-
* Update analysis configuration
|
|
13338
|
-
*/
|
|
13339
|
-
updateAnalysisConfig(config) {
|
|
13340
|
-
let needsReconnect = false;
|
|
13341
|
-
if (config.fftSize && config.fftSize !== this.fftSize) {
|
|
13342
|
-
this.fftSize = config.fftSize;
|
|
13343
|
-
this.refreshFFTResources();
|
|
13344
|
-
needsReconnect = true;
|
|
13345
|
-
}
|
|
13346
|
-
if (config.smoothing !== void 0) {
|
|
13347
|
-
this.smoothingTimeConstant = config.smoothing;
|
|
13348
|
-
if (this.analyser) {
|
|
13349
|
-
this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
|
|
13350
|
-
}
|
|
13351
|
-
}
|
|
13352
|
-
if (needsReconnect && this.currentStream) {
|
|
13353
|
-
const stream = this.currentStream;
|
|
13354
|
-
this.setAudioStream(stream);
|
|
13355
|
-
}
|
|
13356
|
-
}
|
|
13357
13587
|
/**
|
|
13358
13588
|
* Initialize audio worklet for high-quality capture (complex STFT path)
|
|
13359
13589
|
*/
|
|
@@ -13362,50 +13592,54 @@ class AudioSystem {
|
|
|
13362
13592
|
return false;
|
|
13363
13593
|
}
|
|
13364
13594
|
try {
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
process(inputs, outputs) {
|
|
13376
|
-
const input = inputs[0];
|
|
13377
|
-
const output = outputs[0];
|
|
13378
|
-
const channel = input && input[0];
|
|
13379
|
-
if (output) {
|
|
13380
|
-
for (let ch = 0; ch < output.length; ch++) {
|
|
13381
|
-
output[ch].fill(0);
|
|
13382
|
-
}
|
|
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;
|
|
13383
13605
|
}
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
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);
|
|
13613
|
+
}
|
|
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;
|
|
13397
13630
|
}
|
|
13398
|
-
this.writeIndex = overlap > 0 ? overlap : 0;
|
|
13399
13631
|
}
|
|
13632
|
+
return true;
|
|
13400
13633
|
}
|
|
13401
|
-
return true;
|
|
13402
13634
|
}
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
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
|
+
}
|
|
13409
13643
|
this.workletNode = new AudioWorkletNode(this.audioContext, "audio-analysis-processor", {
|
|
13410
13644
|
numberOfOutputs: 1,
|
|
13411
13645
|
outputChannelCount: [1],
|
|
@@ -13431,26 +13665,15 @@ class AudioSystem {
|
|
|
13431
13665
|
* Start the audio analysis loop at high speed (8ms intervals for transient capture)
|
|
13432
13666
|
*/
|
|
13433
13667
|
startAnalysisLoop() {
|
|
13434
|
-
console.log("[AudioSystem] startAnalysisLoop called", {
|
|
13435
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13436
|
-
analysisMode: this.analysisMode,
|
|
13437
|
-
hasAnalyser: !!this.analyser,
|
|
13438
|
-
hasContext: !!this.audioContext
|
|
13439
|
-
});
|
|
13440
13668
|
if (this.isAnalysisRunning || this.analysisMode !== "analyser") {
|
|
13441
|
-
console.warn("[AudioSystem] startAnalysisLoop SKIPPED", {
|
|
13442
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13443
|
-
analysisMode: this.analysisMode
|
|
13444
|
-
});
|
|
13445
13669
|
return;
|
|
13446
13670
|
}
|
|
13447
13671
|
this.isAnalysisRunning = true;
|
|
13448
13672
|
this.analysisInterval = window.setInterval(() => {
|
|
13449
13673
|
this.performAnalysis();
|
|
13450
13674
|
}, this.analysisIntervalMs);
|
|
13451
|
-
|
|
13452
|
-
intervalMs: this.analysisIntervalMs
|
|
13453
|
-
intervalId: this.analysisInterval
|
|
13675
|
+
this.debugLog("[AudioSystem] Analysis loop started", {
|
|
13676
|
+
intervalMs: this.analysisIntervalMs
|
|
13454
13677
|
});
|
|
13455
13678
|
}
|
|
13456
13679
|
/**
|
|
@@ -13467,6 +13690,24 @@ class AudioSystem {
|
|
|
13467
13690
|
this.analysisLoopId = null;
|
|
13468
13691
|
}
|
|
13469
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
|
+
}
|
|
13470
13711
|
/**
|
|
13471
13712
|
* Pause audio analysis (for tests or temporary suspension)
|
|
13472
13713
|
* The setInterval continues but performAnalysis() exits early
|
|
@@ -13489,25 +13730,7 @@ class AudioSystem {
|
|
|
13489
13730
|
* Layer 4: BeatStateManager (state machine + confidence)
|
|
13490
13731
|
*/
|
|
13491
13732
|
performAnalysis() {
|
|
13492
|
-
if (this.analysisTicks === 0) {
|
|
13493
|
-
console.log("[AudioSystem] performAnalysis FIRST CALL", {
|
|
13494
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13495
|
-
mode: this.analysisMode,
|
|
13496
|
-
hasContext: !!this.audioContext,
|
|
13497
|
-
hasAnalyser: !!this.analyser
|
|
13498
|
-
});
|
|
13499
|
-
}
|
|
13500
13733
|
if (!this.isAnalysisRunning || this.analysisMode !== "analyser" || !this.audioContext || !this.analyser) {
|
|
13501
|
-
const now = performance.now();
|
|
13502
|
-
if (now - this.lastLoopWarn > 2e3) {
|
|
13503
|
-
console.warn("[AudioSystem] performAnalysis skipped", {
|
|
13504
|
-
isAnalysisRunning: this.isAnalysisRunning,
|
|
13505
|
-
mode: this.analysisMode,
|
|
13506
|
-
hasContext: !!this.audioContext,
|
|
13507
|
-
hasAnalyser: !!this.analyser
|
|
13508
|
-
});
|
|
13509
|
-
this.lastLoopWarn = now;
|
|
13510
|
-
}
|
|
13511
13734
|
return;
|
|
13512
13735
|
}
|
|
13513
13736
|
if (!this.timeDomainData || this.timeDomainData.length !== this.fftSize) {
|
|
@@ -13515,14 +13738,6 @@ class AudioSystem {
|
|
|
13515
13738
|
}
|
|
13516
13739
|
this.analyser.getFloatTimeDomainData(this.timeDomainData);
|
|
13517
13740
|
this.analysisTicks++;
|
|
13518
|
-
if (this.analysisTicks === 1) {
|
|
13519
|
-
console.log("[AudioSystem][analyser] first tick", {
|
|
13520
|
-
fftSize: this.fftSize,
|
|
13521
|
-
sampleRate: this.audioContext.sampleRate,
|
|
13522
|
-
bufLen: this.timeDomainData.length
|
|
13523
|
-
});
|
|
13524
|
-
this.logVerbose("[AudioSystem][analyser] first 8 samples", Array.from(this.timeDomainData.slice(0, 8)));
|
|
13525
|
-
}
|
|
13526
13741
|
for (let i = 0; i < 32 && i < this.timeDomainData.length; i++) {
|
|
13527
13742
|
Math.abs(this.timeDomainData[i]);
|
|
13528
13743
|
}
|
|
@@ -13530,9 +13745,6 @@ class AudioSystem {
|
|
|
13530
13745
|
this.analyzeFrame(this.timeDomainData, this.audioContext.sampleRate, detectionTimeMs);
|
|
13531
13746
|
}
|
|
13532
13747
|
applyAutoGain() {
|
|
13533
|
-
if (this.debugDisableAutoGain) {
|
|
13534
|
-
return;
|
|
13535
|
-
}
|
|
13536
13748
|
this.audioState.volume.current = this.volumeAutoGain.process(this.audioState.volume.current);
|
|
13537
13749
|
this.audioState.volume.peak = this.volumeAutoGain.process(this.audioState.volume.peak);
|
|
13538
13750
|
const bands = this.audioState.bands;
|
|
@@ -13578,12 +13790,17 @@ class AudioSystem {
|
|
|
13578
13790
|
sendAnalysisResultsToWorker() {
|
|
13579
13791
|
if (this.sendAnalysisResults) {
|
|
13580
13792
|
const frequencyData = this.frequencyData ? new Uint8Array(this.frequencyData) : new Uint8Array(0);
|
|
13793
|
+
const waveformData = this.lastWaveformFrame ? new Float32Array(this.lastWaveformFrame) : new Float32Array(0);
|
|
13581
13794
|
this.sendAnalysisResults({
|
|
13582
13795
|
type: "audio-analysis-update",
|
|
13583
13796
|
data: {
|
|
13584
|
-
|
|
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,
|
|
13585
13802
|
frequencyData,
|
|
13586
|
-
|
|
13803
|
+
waveformData,
|
|
13587
13804
|
timestamp: performance.now()
|
|
13588
13805
|
}
|
|
13589
13806
|
});
|
|
@@ -13607,18 +13824,16 @@ class AudioSystem {
|
|
|
13607
13824
|
any: 0,
|
|
13608
13825
|
kickSmoothed: 0,
|
|
13609
13826
|
snareSmoothed: 0,
|
|
13827
|
+
hatSmoothed: 0,
|
|
13610
13828
|
anySmoothed: 0,
|
|
13611
13829
|
events: [],
|
|
13612
13830
|
bpm: 120,
|
|
13613
|
-
phase: 0,
|
|
13614
|
-
bar: 0,
|
|
13615
13831
|
confidence: 0,
|
|
13616
13832
|
isLocked: false
|
|
13617
13833
|
};
|
|
13618
13834
|
this.audioState.spectral = {
|
|
13619
13835
|
brightness: 0,
|
|
13620
|
-
flatness: 0
|
|
13621
|
-
flux: 0
|
|
13836
|
+
flatness: 0
|
|
13622
13837
|
};
|
|
13623
13838
|
}
|
|
13624
13839
|
/**
|
|
@@ -13633,6 +13848,7 @@ class AudioSystem {
|
|
|
13633
13848
|
}
|
|
13634
13849
|
this.workletNode = null;
|
|
13635
13850
|
this.workletReady = false;
|
|
13851
|
+
this.workletRegistered = false;
|
|
13636
13852
|
this.analysisMode = "analyser";
|
|
13637
13853
|
this.onsetDetection.reset();
|
|
13638
13854
|
this.tempoInduction.reset();
|
|
@@ -13640,183 +13856,48 @@ class AudioSystem {
|
|
|
13640
13856
|
this.stateManager.reset();
|
|
13641
13857
|
this.volumeAutoGain.reset();
|
|
13642
13858
|
Object.values(this.bandAutoGain).forEach((g) => g.reset());
|
|
13643
|
-
this.
|
|
13644
|
-
this.manualBPM = null;
|
|
13645
|
-
this.tapHistory = [];
|
|
13646
|
-
if (this.tapTimeout) {
|
|
13647
|
-
clearTimeout(this.tapTimeout);
|
|
13648
|
-
this.tapTimeout = null;
|
|
13649
|
-
}
|
|
13859
|
+
this.onsetTapManager.reset();
|
|
13650
13860
|
Object.values(this.envelopeFollowers).forEach((env) => env.reset());
|
|
13651
|
-
this.pcmRing = null;
|
|
13652
|
-
this.pcmWriteIndex = 0;
|
|
13653
|
-
this.pcmFilled = 0;
|
|
13654
|
-
this.lastTempoExtraction = 0;
|
|
13655
13861
|
this.resetAudioValues();
|
|
13656
13862
|
}
|
|
13657
|
-
/**
|
|
13658
|
-
* Get current analysis configuration
|
|
13659
|
-
*/
|
|
13660
|
-
getAnalysisConfig() {
|
|
13661
|
-
return {
|
|
13662
|
-
fftSize: this.fftSize,
|
|
13663
|
-
smoothing: this.smoothingTimeConstant
|
|
13664
|
-
};
|
|
13665
|
-
}
|
|
13666
|
-
/**
|
|
13667
|
-
* Set analysis backend preference
|
|
13668
|
-
*/
|
|
13669
|
-
setAnalysisBackend(backend) {
|
|
13670
|
-
this.analysisBackend = backend;
|
|
13671
|
-
}
|
|
13672
|
-
getAnalysisBackend() {
|
|
13673
|
-
return this.analysisBackend;
|
|
13674
|
-
}
|
|
13675
|
-
/**
|
|
13676
|
-
* Force analyser path (skip worklet) for debugging
|
|
13677
|
-
*/
|
|
13678
|
-
setForceAnalyser(enabled) {
|
|
13679
|
-
this.forceAnalyser = enabled;
|
|
13680
|
-
if (enabled) {
|
|
13681
|
-
console.warn("[AudioSystem] forceAnalyser enabled - worklet will be skipped.");
|
|
13682
|
-
}
|
|
13683
|
-
}
|
|
13684
|
-
isForceAnalyser() {
|
|
13685
|
-
return this.forceAnalyser;
|
|
13686
|
-
}
|
|
13687
|
-
/**
|
|
13688
|
-
* Enable/disable Essentia tempo extraction (disabled by default due to WASM exception config)
|
|
13689
|
-
*/
|
|
13690
|
-
setEssentiaTempoEnabled(enabled) {
|
|
13691
|
-
this.useEssentiaTempo = enabled;
|
|
13692
|
-
}
|
|
13693
|
-
isEssentiaTempoEnabled() {
|
|
13694
|
-
return this.useEssentiaTempo;
|
|
13695
|
-
}
|
|
13696
13863
|
// ═══════════════════════════════════════════════════════════
|
|
13697
13864
|
// Public API Methods for Audio Analysis Configuration
|
|
13698
13865
|
// ═══════════════════════════════════════════════════════════
|
|
13699
13866
|
/**
|
|
13700
|
-
*
|
|
13701
|
-
* @param value - Sensitivity (0.5-2.0, default 1.0)
|
|
13867
|
+
* Record a tap for the specified instrument onset
|
|
13702
13868
|
*/
|
|
13703
|
-
|
|
13704
|
-
this.
|
|
13869
|
+
tapOnset(instrument) {
|
|
13870
|
+
this.onsetTapManager.tap(instrument);
|
|
13705
13871
|
}
|
|
13706
13872
|
/**
|
|
13707
|
-
*
|
|
13873
|
+
* Clear tap pattern for an instrument, restoring auto-detection
|
|
13708
13874
|
*/
|
|
13709
|
-
|
|
13710
|
-
|
|
13875
|
+
clearOnsetTap(instrument) {
|
|
13876
|
+
this.onsetTapManager.clear(instrument);
|
|
13711
13877
|
}
|
|
13712
13878
|
/**
|
|
13713
|
-
*
|
|
13879
|
+
* Get the current onset mode for an instrument
|
|
13714
13880
|
*/
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
if (this.tapTimeout) {
|
|
13718
|
-
clearTimeout(this.tapTimeout);
|
|
13719
|
-
}
|
|
13720
|
-
if (this.tapHistory.length > 0) {
|
|
13721
|
-
const lastTap = this.tapHistory[this.tapHistory.length - 1];
|
|
13722
|
-
const interval = now - lastTap;
|
|
13723
|
-
if (interval < 200) {
|
|
13724
|
-
this.startTapClearTimeout();
|
|
13725
|
-
return;
|
|
13726
|
-
}
|
|
13727
|
-
if (interval > 2e3) {
|
|
13728
|
-
this.tapHistory = [];
|
|
13729
|
-
}
|
|
13730
|
-
}
|
|
13731
|
-
this.tapHistory.push(now);
|
|
13732
|
-
if (this.tapHistory.length > 8) {
|
|
13733
|
-
this.tapHistory.shift();
|
|
13734
|
-
}
|
|
13735
|
-
if (this.tapHistory.length >= 2) {
|
|
13736
|
-
const bpm = this.calculateBPMFromTaps();
|
|
13737
|
-
this.manualBPM = bpm;
|
|
13738
|
-
this.beatMode = "manual";
|
|
13739
|
-
this.pll.setBPM(bpm);
|
|
13740
|
-
this.tempoInduction.setManualBPM(bpm);
|
|
13741
|
-
}
|
|
13742
|
-
this.startTapClearTimeout();
|
|
13743
|
-
}
|
|
13744
|
-
/**
|
|
13745
|
-
* Calculate BPM from tap history
|
|
13746
|
-
*/
|
|
13747
|
-
calculateBPMFromTaps() {
|
|
13748
|
-
if (this.tapHistory.length < 2) return 120;
|
|
13749
|
-
const intervals = [];
|
|
13750
|
-
for (let i = 1; i < this.tapHistory.length; i++) {
|
|
13751
|
-
intervals.push(this.tapHistory[i] - this.tapHistory[i - 1]);
|
|
13752
|
-
}
|
|
13753
|
-
if (intervals.length >= 4) {
|
|
13754
|
-
const sorted = [...intervals].sort((a, b) => a - b);
|
|
13755
|
-
const median = sorted[Math.floor(sorted.length / 2)];
|
|
13756
|
-
const filtered = intervals.filter((val) => {
|
|
13757
|
-
const deviation = Math.abs(val - median) / median;
|
|
13758
|
-
return deviation < 0.3;
|
|
13759
|
-
});
|
|
13760
|
-
if (filtered.length >= 2) {
|
|
13761
|
-
intervals.splice(0, intervals.length, ...filtered);
|
|
13762
|
-
}
|
|
13763
|
-
}
|
|
13764
|
-
const avgInterval = intervals.reduce((sum, val) => sum + val, 0) / intervals.length;
|
|
13765
|
-
let bpm = 6e4 / avgInterval;
|
|
13766
|
-
while (bpm > 200) bpm /= 2;
|
|
13767
|
-
while (bpm < 60) bpm *= 2;
|
|
13768
|
-
return Math.round(bpm * 10) / 10;
|
|
13769
|
-
}
|
|
13770
|
-
/**
|
|
13771
|
-
* Start auto-clear timeout for tap tempo
|
|
13772
|
-
*/
|
|
13773
|
-
startTapClearTimeout() {
|
|
13774
|
-
this.tapTimeout = setTimeout(() => {
|
|
13775
|
-
this.tapHistory = [];
|
|
13776
|
-
}, 2e3);
|
|
13881
|
+
getOnsetMode(instrument) {
|
|
13882
|
+
return this.onsetTapManager.getMode(instrument);
|
|
13777
13883
|
}
|
|
13778
13884
|
/**
|
|
13779
|
-
*
|
|
13885
|
+
* Get pattern info for an instrument (null if no pattern recognized)
|
|
13780
13886
|
*/
|
|
13781
|
-
|
|
13782
|
-
this.
|
|
13783
|
-
if (this.tapTimeout) {
|
|
13784
|
-
clearTimeout(this.tapTimeout);
|
|
13785
|
-
this.tapTimeout = null;
|
|
13786
|
-
}
|
|
13887
|
+
getOnsetPatternInfo(instrument) {
|
|
13888
|
+
return this.onsetTapManager.getPatternInfo(instrument);
|
|
13787
13889
|
}
|
|
13788
13890
|
/**
|
|
13789
|
-
*
|
|
13891
|
+
* Mute/unmute an instrument onset (suppresses output without destroying state)
|
|
13790
13892
|
*/
|
|
13791
|
-
|
|
13792
|
-
|
|
13893
|
+
setOnsetMuted(instrument, muted) {
|
|
13894
|
+
this.onsetTapManager.setMuted(instrument, muted);
|
|
13793
13895
|
}
|
|
13794
13896
|
/**
|
|
13795
|
-
*
|
|
13897
|
+
* Check if an instrument onset is muted
|
|
13796
13898
|
*/
|
|
13797
|
-
|
|
13798
|
-
this.
|
|
13799
|
-
if (mode === "auto") {
|
|
13800
|
-
this.clearTaps();
|
|
13801
|
-
this.manualBPM = null;
|
|
13802
|
-
}
|
|
13803
|
-
}
|
|
13804
|
-
/**
|
|
13805
|
-
* Get beat sync mode
|
|
13806
|
-
*/
|
|
13807
|
-
getBeatMode() {
|
|
13808
|
-
return this.beatMode;
|
|
13809
|
-
}
|
|
13810
|
-
/**
|
|
13811
|
-
* Set manual BPM
|
|
13812
|
-
*/
|
|
13813
|
-
setManualBPM(bpm) {
|
|
13814
|
-
bpm = Math.max(60, Math.min(200, bpm));
|
|
13815
|
-
this.manualBPM = bpm;
|
|
13816
|
-
this.beatMode = "manual";
|
|
13817
|
-
this.pll.setBPM(bpm);
|
|
13818
|
-
this.tempoInduction.setManualBPM(bpm);
|
|
13819
|
-
this.clearTaps();
|
|
13899
|
+
isOnsetMuted(instrument) {
|
|
13900
|
+
return this.onsetTapManager.isMuted(instrument);
|
|
13820
13901
|
}
|
|
13821
13902
|
/**
|
|
13822
13903
|
* Get current BPM (manual or auto-detected)
|
|
@@ -13848,21 +13929,11 @@ class AudioSystem {
|
|
|
13848
13929
|
this.setAudioStream(stream);
|
|
13849
13930
|
}
|
|
13850
13931
|
}
|
|
13851
|
-
/**
|
|
13852
|
-
* Set smoothing time constant
|
|
13853
|
-
*/
|
|
13854
|
-
setSmoothing(value) {
|
|
13855
|
-
this.smoothingTimeConstant = Math.max(0, Math.min(1, value));
|
|
13856
|
-
if (this.analyser) {
|
|
13857
|
-
this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
|
|
13858
|
-
}
|
|
13859
|
-
}
|
|
13860
13932
|
/**
|
|
13861
13933
|
* Enable/disable auto-gain
|
|
13862
13934
|
*/
|
|
13863
13935
|
setAutoGain(enabled) {
|
|
13864
13936
|
this.autoGainEnabled = enabled;
|
|
13865
|
-
this.debugDisableAutoGain = !enabled;
|
|
13866
13937
|
if (!enabled) {
|
|
13867
13938
|
this.volumeAutoGain.reset();
|
|
13868
13939
|
Object.values(this.bandAutoGain).forEach((g) => g.reset());
|
|
@@ -13896,11 +13967,13 @@ class AudioSystem {
|
|
|
13896
13967
|
isConnected: this.audioState.isConnected,
|
|
13897
13968
|
currentBPM: this.pll.getBPM(),
|
|
13898
13969
|
confidence: this.tempoInduction.getConfidence(),
|
|
13899
|
-
mode: this.beatMode,
|
|
13900
|
-
tapCount: this.tapHistory.length,
|
|
13901
13970
|
isLocked: this.stateManager.isLocked(),
|
|
13902
|
-
|
|
13903
|
-
|
|
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
|
+
}
|
|
13904
13977
|
};
|
|
13905
13978
|
}
|
|
13906
13979
|
/**
|
|
@@ -13979,7 +14052,6 @@ class AudioSystem {
|
|
|
13979
14052
|
volumeGain: this.volumeAutoGain.getGain(),
|
|
13980
14053
|
// Timing info
|
|
13981
14054
|
dtMs: this.lastDtMs,
|
|
13982
|
-
smoothingTimeConstant: this.smoothingTimeConstant,
|
|
13983
14055
|
analysisIntervalMs: this.analysisIntervalMs,
|
|
13984
14056
|
// Current events
|
|
13985
14057
|
events: [...this.audioState.beat.events]
|
|
@@ -14538,15 +14610,25 @@ class VijiCore {
|
|
|
14538
14610
|
currentAudioStream = null;
|
|
14539
14611
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
14540
14612
|
// VIDEO STREAM INDEX CONTRACT:
|
|
14541
|
-
//
|
|
14542
|
-
//
|
|
14543
|
-
//
|
|
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.
|
|
14544
14623
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
14624
|
+
static ADDITIONAL_STREAM_BASE = 1;
|
|
14625
|
+
static DEVICE_VIDEO_BASE = 100;
|
|
14626
|
+
static DIRECT_FRAME_BASE = 200;
|
|
14545
14627
|
// Separated video stream management
|
|
14546
14628
|
videoStream = null;
|
|
14547
14629
|
// Main stream (CV enabled) - always index 0
|
|
14548
14630
|
videoStreams = [];
|
|
14549
|
-
// Additional streams (no CV)
|
|
14631
|
+
// Additional streams (no CV)
|
|
14550
14632
|
// Video coordinators
|
|
14551
14633
|
mainVideoCoordinator = null;
|
|
14552
14634
|
additionalCoordinators = [];
|
|
@@ -14554,7 +14636,6 @@ class VijiCore {
|
|
|
14554
14636
|
directFrameSlots = 0;
|
|
14555
14637
|
// Device video management (coordinators only, for cleanup)
|
|
14556
14638
|
deviceVideoCoordinators = /* @__PURE__ */ new Map();
|
|
14557
|
-
// Track assigned stream indices to prevent collisions (indices start at 1 + videoStreams.length)
|
|
14558
14639
|
deviceVideoStreamIndices = /* @__PURE__ */ new Map();
|
|
14559
14640
|
// Auto-capture frame buffer (zero-copy transfer)
|
|
14560
14641
|
latestFrameBuffer = null;
|
|
@@ -14595,7 +14676,6 @@ class VijiCore {
|
|
|
14595
14676
|
this.config = {
|
|
14596
14677
|
...config,
|
|
14597
14678
|
frameRateMode: config.frameRateMode || "full",
|
|
14598
|
-
parameters: config.parameters || [],
|
|
14599
14679
|
noInputs: config.noInputs ?? false,
|
|
14600
14680
|
// Canvas DOM interaction disabled in headless by default
|
|
14601
14681
|
// InteractionManager (viji.mouse/etc) always exists regardless
|
|
@@ -14632,7 +14712,8 @@ class VijiCore {
|
|
|
14632
14712
|
this.latestFrameBuffer = bitmap;
|
|
14633
14713
|
}
|
|
14634
14714
|
/**
|
|
14635
|
-
* Get latest frame (transfers ownership, zero-copy)
|
|
14715
|
+
* Get latest frame (transfers ownership, zero-copy).
|
|
14716
|
+
* Internal: consumed by getLatestFramesFromSources().
|
|
14636
14717
|
*/
|
|
14637
14718
|
getLatestFrame() {
|
|
14638
14719
|
const frame = this.latestFrameBuffer;
|
|
@@ -14688,7 +14769,6 @@ class VijiCore {
|
|
|
14688
14769
|
if (this.interactionManager && "setDebugMode" in this.interactionManager) {
|
|
14689
14770
|
this.interactionManager.setDebugMode(enabled);
|
|
14690
14771
|
}
|
|
14691
|
-
if (this.audioSystem && "setDebugMode" in this.audioSystem) ;
|
|
14692
14772
|
if (this.mainVideoCoordinator && "setDebugMode" in this.mainVideoCoordinator) {
|
|
14693
14773
|
this.mainVideoCoordinator.setDebugMode(enabled);
|
|
14694
14774
|
}
|
|
@@ -14722,15 +14802,6 @@ class VijiCore {
|
|
|
14722
14802
|
hide() {
|
|
14723
14803
|
this.iframeManager?.hide();
|
|
14724
14804
|
}
|
|
14725
|
-
/**
|
|
14726
|
-
* Select audio analysis backend
|
|
14727
|
-
*/
|
|
14728
|
-
setAudioAnalysisBackend(backend) {
|
|
14729
|
-
this.audioSystem?.setAnalysisBackend(backend);
|
|
14730
|
-
}
|
|
14731
|
-
getAudioAnalysisBackend() {
|
|
14732
|
-
return this.audioSystem?.getAnalysisBackend() ?? "auto";
|
|
14733
|
-
}
|
|
14734
14805
|
/**
|
|
14735
14806
|
* Initializes the core components in sequence
|
|
14736
14807
|
*/
|
|
@@ -14793,7 +14864,7 @@ class VijiCore {
|
|
|
14793
14864
|
});
|
|
14794
14865
|
}
|
|
14795
14866
|
for (let i = 0; i < this.videoStreams.length; i++) {
|
|
14796
|
-
const streamIndex =
|
|
14867
|
+
const streamIndex = VijiCore.ADDITIONAL_STREAM_BASE + i;
|
|
14797
14868
|
const coordinator = new VideoCoordinator((message, transfer) => {
|
|
14798
14869
|
if (this.workerManager) {
|
|
14799
14870
|
if (message.type === "video-canvas-setup") {
|
|
@@ -15209,13 +15280,6 @@ class VijiCore {
|
|
|
15209
15280
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
15210
15281
|
// Direct Frame Injection API (Compositor Pipeline)
|
|
15211
15282
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
15212
|
-
/**
|
|
15213
|
-
* Worker-side stream index offset for direct frame slots.
|
|
15214
|
-
* Accounts for main video stream and additional media streams.
|
|
15215
|
-
*/
|
|
15216
|
-
get directFrameStartIndex() {
|
|
15217
|
-
return (this.videoStream ? 1 : 0) + this.videoStreams.length;
|
|
15218
|
-
}
|
|
15219
15283
|
/**
|
|
15220
15284
|
* Ensure the worker has enough direct frame slots prepared.
|
|
15221
15285
|
* Auto-grows as needed; skips if already sufficient.
|
|
@@ -15224,8 +15288,7 @@ class VijiCore {
|
|
|
15224
15288
|
if (count <= this.directFrameSlots) return;
|
|
15225
15289
|
this.directFrameSlots = count;
|
|
15226
15290
|
this.workerManager.postMessage("video-streams-prepare", {
|
|
15227
|
-
|
|
15228
|
-
mediaStreamCount: this.videoStreams.length,
|
|
15291
|
+
directFrameBaseIndex: VijiCore.DIRECT_FRAME_BASE,
|
|
15229
15292
|
directFrameCount: count
|
|
15230
15293
|
});
|
|
15231
15294
|
this.debugLog(`[Compositor] Prepared ${count} direct frame slot(s)`);
|
|
@@ -15242,27 +15305,10 @@ class VijiCore {
|
|
|
15242
15305
|
this.ensureDirectFrameSlots(streamIndex + 1);
|
|
15243
15306
|
this.workerManager.postMessage("video-frame-direct", {
|
|
15244
15307
|
imageBitmap: bitmap,
|
|
15245
|
-
streamIndex:
|
|
15308
|
+
streamIndex: VijiCore.DIRECT_FRAME_BASE + streamIndex,
|
|
15246
15309
|
timestamp: performance.now()
|
|
15247
15310
|
}, [bitmap]);
|
|
15248
15311
|
}
|
|
15249
|
-
/**
|
|
15250
|
-
* Inject frames for all direct-frame stream slots at once (compositor pipeline).
|
|
15251
|
-
* Auto-prepares slots on first call. Transfers ownership of all bitmaps (zero-copy).
|
|
15252
|
-
*/
|
|
15253
|
-
injectFrames(bitmaps) {
|
|
15254
|
-
if (!this.workerManager?.ready) {
|
|
15255
|
-
throw new VijiCoreError("Core not ready", "NOT_READY");
|
|
15256
|
-
}
|
|
15257
|
-
this.ensureDirectFrameSlots(bitmaps.length);
|
|
15258
|
-
bitmaps.forEach((bitmap, index) => {
|
|
15259
|
-
this.workerManager.postMessage("video-frame-direct", {
|
|
15260
|
-
imageBitmap: bitmap,
|
|
15261
|
-
streamIndex: this.directFrameStartIndex + index,
|
|
15262
|
-
timestamp: performance.now()
|
|
15263
|
-
}, [bitmap]);
|
|
15264
|
-
});
|
|
15265
|
-
}
|
|
15266
15312
|
/**
|
|
15267
15313
|
* Link this core to receive events from a source core
|
|
15268
15314
|
* @param syncResolution Smart default: true for headless, false for visible
|
|
@@ -15597,7 +15643,6 @@ class VijiCore {
|
|
|
15597
15643
|
if (this.audioSystem) {
|
|
15598
15644
|
this.audioSystem.handleAudioStreamUpdate({
|
|
15599
15645
|
audioStream,
|
|
15600
|
-
...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
|
|
15601
15646
|
timestamp: performance.now()
|
|
15602
15647
|
});
|
|
15603
15648
|
}
|
|
@@ -15657,31 +15702,10 @@ class VijiCore {
|
|
|
15657
15702
|
return this.videoStream;
|
|
15658
15703
|
}
|
|
15659
15704
|
// ═══════════════════════════════════════════════════════
|
|
15660
|
-
// ADDITIONAL VIDEO STREAMS API
|
|
15705
|
+
// ADDITIONAL VIDEO STREAMS API
|
|
15661
15706
|
// ═══════════════════════════════════════════════════════
|
|
15662
15707
|
/**
|
|
15663
|
-
*
|
|
15664
|
-
*/
|
|
15665
|
-
async setVideoStreams(streams) {
|
|
15666
|
-
this.validateReady();
|
|
15667
|
-
this.videoStreams = [...streams];
|
|
15668
|
-
await this.reinitializeAdditionalCoordinators();
|
|
15669
|
-
this.debugLog(`Additional video streams updated: ${streams.length} streams`);
|
|
15670
|
-
}
|
|
15671
|
-
/**
|
|
15672
|
-
* Gets all additional video streams
|
|
15673
|
-
*/
|
|
15674
|
-
getVideoStreams() {
|
|
15675
|
-
return [...this.videoStreams];
|
|
15676
|
-
}
|
|
15677
|
-
/**
|
|
15678
|
-
* Gets video stream at specific index
|
|
15679
|
-
*/
|
|
15680
|
-
getVideoStreamAt(index) {
|
|
15681
|
-
return this.videoStreams[index] || null;
|
|
15682
|
-
}
|
|
15683
|
-
/**
|
|
15684
|
-
* Adds a video stream
|
|
15708
|
+
* Adds an additional video stream (no CV). Returns its index in viji.streams[].
|
|
15685
15709
|
*/
|
|
15686
15710
|
async addVideoStream(stream) {
|
|
15687
15711
|
this.validateReady();
|
|
@@ -15689,7 +15713,7 @@ class VijiCore {
|
|
|
15689
15713
|
if (existingIndex !== -1) return existingIndex;
|
|
15690
15714
|
this.videoStreams.push(stream);
|
|
15691
15715
|
const newIndex = this.videoStreams.length - 1;
|
|
15692
|
-
const streamIndex =
|
|
15716
|
+
const streamIndex = VijiCore.ADDITIONAL_STREAM_BASE + newIndex;
|
|
15693
15717
|
const coordinator = new VideoCoordinator((message, transfer) => {
|
|
15694
15718
|
if (this.workerManager) {
|
|
15695
15719
|
if (message.type === "video-canvas-setup") {
|
|
@@ -15714,9 +15738,9 @@ class VijiCore {
|
|
|
15714
15738
|
return newIndex;
|
|
15715
15739
|
}
|
|
15716
15740
|
/**
|
|
15717
|
-
* Removes video stream
|
|
15741
|
+
* Removes an additional video stream by index.
|
|
15718
15742
|
*/
|
|
15719
|
-
async
|
|
15743
|
+
async removeVideoStream(index) {
|
|
15720
15744
|
this.validateReady();
|
|
15721
15745
|
if (index < 0 || index >= this.videoStreams.length) {
|
|
15722
15746
|
throw new VijiCoreError(`Invalid stream index: ${index}`, "INVALID_INDEX");
|
|
@@ -15726,43 +15750,19 @@ class VijiCore {
|
|
|
15726
15750
|
await this.reinitializeAdditionalCoordinators();
|
|
15727
15751
|
}
|
|
15728
15752
|
/**
|
|
15729
|
-
*
|
|
15753
|
+
* Gets the number of additional video streams.
|
|
15730
15754
|
*/
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
if (index === -1) return false;
|
|
15734
|
-
await this.removeVideoStreamAt(index);
|
|
15735
|
-
return true;
|
|
15755
|
+
getVideoStreamCount() {
|
|
15756
|
+
return this.videoStreams.length;
|
|
15736
15757
|
}
|
|
15737
15758
|
/**
|
|
15738
|
-
*
|
|
15739
|
-
*/
|
|
15740
|
-
async setVideoStreamAt(index, stream) {
|
|
15741
|
-
this.validateReady();
|
|
15742
|
-
if (index < 0 || index >= this.videoStreams.length) {
|
|
15743
|
-
throw new VijiCoreError(`Invalid stream index: ${index}`, "INVALID_INDEX");
|
|
15744
|
-
}
|
|
15745
|
-
this.videoStreams[index].getTracks().forEach((track) => track.stop());
|
|
15746
|
-
this.videoStreams[index] = stream;
|
|
15747
|
-
if (this.additionalCoordinators[index]) {
|
|
15748
|
-
this.additionalCoordinators[index].resetVideoState();
|
|
15749
|
-
this.additionalCoordinators[index].handleVideoStreamUpdate({
|
|
15750
|
-
videoStream: stream,
|
|
15751
|
-
streamIndex: 1 + index,
|
|
15752
|
-
streamType: "additional",
|
|
15753
|
-
targetFrameRate: 30,
|
|
15754
|
-
timestamp: performance.now()
|
|
15755
|
-
});
|
|
15756
|
-
}
|
|
15757
|
-
}
|
|
15758
|
-
/**
|
|
15759
|
-
* Reinitializes all additional coordinators
|
|
15759
|
+
* Reinitializes all additional coordinators after array mutation.
|
|
15760
15760
|
*/
|
|
15761
15761
|
async reinitializeAdditionalCoordinators() {
|
|
15762
15762
|
this.additionalCoordinators.forEach((coord) => coord.resetVideoState());
|
|
15763
15763
|
this.additionalCoordinators = [];
|
|
15764
15764
|
for (let i = 0; i < this.videoStreams.length; i++) {
|
|
15765
|
-
const streamIndex =
|
|
15765
|
+
const streamIndex = VijiCore.ADDITIONAL_STREAM_BASE + i;
|
|
15766
15766
|
const coordinator = new VideoCoordinator((message, transfer) => {
|
|
15767
15767
|
if (this.workerManager) {
|
|
15768
15768
|
if (message.type === "video-canvas-setup") {
|
|
@@ -15822,98 +15822,82 @@ class VijiCore {
|
|
|
15822
15822
|
// Audio Analysis API (Namespace-based)
|
|
15823
15823
|
// ═══════════════════════════════════════════════════════════
|
|
15824
15824
|
audio = {
|
|
15825
|
-
/**
|
|
15826
|
-
* Set global audio sensitivity (0.5-2.0, default 1.0)
|
|
15827
|
-
*/
|
|
15828
|
-
setSensitivity: (value) => {
|
|
15829
|
-
this.validateReady();
|
|
15830
|
-
if (value < 0.5 || value > 2) {
|
|
15831
|
-
throw new VijiCoreError("Sensitivity must be between 0.5 and 2.0", "INVALID_VALUE");
|
|
15832
|
-
}
|
|
15833
|
-
this.audioSystem?.setSensitivity(value);
|
|
15834
|
-
this.debugLog(`Audio sensitivity set to ${value} (${this.instanceId})`);
|
|
15835
|
-
},
|
|
15836
|
-
/**
|
|
15837
|
-
* Get current sensitivity
|
|
15838
|
-
*/
|
|
15839
|
-
getSensitivity: () => {
|
|
15840
|
-
this.validateReady();
|
|
15841
|
-
return this.audioSystem?.getSensitivity() ?? 1;
|
|
15842
|
-
},
|
|
15843
15825
|
/**
|
|
15844
15826
|
* Beat control namespace
|
|
15845
15827
|
*/
|
|
15846
15828
|
beat: {
|
|
15847
15829
|
/**
|
|
15848
|
-
*
|
|
15830
|
+
* Get current BPM (auto-detected)
|
|
15849
15831
|
*/
|
|
15850
|
-
|
|
15832
|
+
getBPM: () => {
|
|
15851
15833
|
this.validateReady();
|
|
15852
|
-
this.audioSystem?.
|
|
15853
|
-
this.debugLog(`Tap tempo recorded (${this.instanceId})`);
|
|
15834
|
+
return this.audioSystem?.getCurrentBPM() ?? 120;
|
|
15854
15835
|
},
|
|
15855
15836
|
/**
|
|
15856
|
-
*
|
|
15837
|
+
* Nudge beat phase
|
|
15857
15838
|
*/
|
|
15858
|
-
|
|
15839
|
+
nudge: (amount) => {
|
|
15859
15840
|
this.validateReady();
|
|
15860
|
-
this.audioSystem?.
|
|
15861
|
-
this.debugLog(`Tap tempo cleared (${this.instanceId})`);
|
|
15841
|
+
this.audioSystem?.nudgeBeatPhase(amount);
|
|
15862
15842
|
},
|
|
15863
15843
|
/**
|
|
15864
|
-
*
|
|
15844
|
+
* Reset beat phase to next beat
|
|
15865
15845
|
*/
|
|
15866
|
-
|
|
15846
|
+
resetPhase: () => {
|
|
15867
15847
|
this.validateReady();
|
|
15868
|
-
|
|
15869
|
-
|
|
15848
|
+
this.audioSystem?.resetBeatPhase();
|
|
15849
|
+
this.debugLog(`Beat phase reset (${this.instanceId})`);
|
|
15850
|
+
}
|
|
15851
|
+
},
|
|
15852
|
+
/**
|
|
15853
|
+
* Per-instrument onset tap control
|
|
15854
|
+
*/
|
|
15855
|
+
onset: {
|
|
15870
15856
|
/**
|
|
15871
|
-
*
|
|
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.
|
|
15872
15860
|
*/
|
|
15873
|
-
|
|
15861
|
+
tap: (instrument) => {
|
|
15874
15862
|
this.validateReady();
|
|
15875
|
-
this.audioSystem?.
|
|
15876
|
-
this.debugLog(`Beat mode set to ${mode} (${this.instanceId})`);
|
|
15863
|
+
this.audioSystem?.tapOnset(instrument);
|
|
15877
15864
|
},
|
|
15878
15865
|
/**
|
|
15879
|
-
*
|
|
15866
|
+
* Clear the tap pattern for an instrument, restoring auto-detection
|
|
15880
15867
|
*/
|
|
15881
|
-
|
|
15868
|
+
clear: (instrument) => {
|
|
15882
15869
|
this.validateReady();
|
|
15883
|
-
|
|
15870
|
+
this.audioSystem?.clearOnsetTap(instrument);
|
|
15884
15871
|
},
|
|
15885
15872
|
/**
|
|
15886
|
-
*
|
|
15873
|
+
* Get onset mode for an instrument: 'auto' | 'tapping' | 'pattern'
|
|
15887
15874
|
*/
|
|
15888
|
-
|
|
15875
|
+
getMode: (instrument) => {
|
|
15889
15876
|
this.validateReady();
|
|
15890
|
-
|
|
15891
|
-
throw new VijiCoreError("BPM must be between 60 and 240", "INVALID_VALUE");
|
|
15892
|
-
}
|
|
15893
|
-
this.audioSystem?.setManualBPM(bpm);
|
|
15894
|
-
this.debugLog(`Manual BPM set to ${bpm} (${this.instanceId})`);
|
|
15877
|
+
return this.audioSystem?.getOnsetMode(instrument) ?? "auto";
|
|
15895
15878
|
},
|
|
15896
15879
|
/**
|
|
15897
|
-
* Get
|
|
15880
|
+
* Get recognized pattern info for an instrument, or null
|
|
15898
15881
|
*/
|
|
15899
|
-
|
|
15882
|
+
getPatternInfo: (instrument) => {
|
|
15900
15883
|
this.validateReady();
|
|
15901
|
-
return this.audioSystem?.
|
|
15884
|
+
return this.audioSystem?.getOnsetPatternInfo(instrument) ?? null;
|
|
15902
15885
|
},
|
|
15903
15886
|
/**
|
|
15904
|
-
*
|
|
15887
|
+
* Mute/unmute an instrument onset.
|
|
15888
|
+
* Suppresses all output (auto, tap, pattern) without destroying state.
|
|
15889
|
+
* Unmuting resumes pattern replay in-phase.
|
|
15905
15890
|
*/
|
|
15906
|
-
|
|
15891
|
+
setMuted: (instrument, muted) => {
|
|
15907
15892
|
this.validateReady();
|
|
15908
|
-
this.audioSystem?.
|
|
15893
|
+
this.audioSystem?.setOnsetMuted(instrument, muted);
|
|
15909
15894
|
},
|
|
15910
15895
|
/**
|
|
15911
|
-
*
|
|
15896
|
+
* Check if an instrument onset is muted
|
|
15912
15897
|
*/
|
|
15913
|
-
|
|
15898
|
+
isMuted: (instrument) => {
|
|
15914
15899
|
this.validateReady();
|
|
15915
|
-
this.audioSystem?.
|
|
15916
|
-
this.debugLog(`Beat phase reset (${this.instanceId})`);
|
|
15900
|
+
return this.audioSystem?.isOnsetMuted(instrument) ?? false;
|
|
15917
15901
|
}
|
|
15918
15902
|
},
|
|
15919
15903
|
/**
|
|
@@ -15928,17 +15912,6 @@ class VijiCore {
|
|
|
15928
15912
|
this.audioSystem?.setFFTSize(size);
|
|
15929
15913
|
this.debugLog(`FFT size set to ${size} (${this.instanceId})`);
|
|
15930
15914
|
},
|
|
15931
|
-
/**
|
|
15932
|
-
* Set smoothing time constant (0-1)
|
|
15933
|
-
*/
|
|
15934
|
-
setSmoothing: (value) => {
|
|
15935
|
-
this.validateReady();
|
|
15936
|
-
if (value < 0 || value > 1) {
|
|
15937
|
-
throw new VijiCoreError("Smoothing must be between 0 and 1", "INVALID_VALUE");
|
|
15938
|
-
}
|
|
15939
|
-
this.audioSystem?.setSmoothing(value);
|
|
15940
|
-
this.debugLog(`Smoothing set to ${value} (${this.instanceId})`);
|
|
15941
|
-
},
|
|
15942
15915
|
/**
|
|
15943
15916
|
* Enable/disable auto-gain
|
|
15944
15917
|
*/
|
|
@@ -15948,20 +15921,10 @@ class VijiCore {
|
|
|
15948
15921
|
this.debugLog(`Auto-gain ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
15949
15922
|
},
|
|
15950
15923
|
/**
|
|
15951
|
-
* Enable/disable
|
|
15924
|
+
* Enable/disable audio analysis debug mode (independent from core debug mode)
|
|
15952
15925
|
*/
|
|
15953
|
-
|
|
15954
|
-
this.
|
|
15955
|
-
this.audioSystem?.setBeatDetection(enabled);
|
|
15956
|
-
this.debugLog(`Beat detection ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
15957
|
-
},
|
|
15958
|
-
/**
|
|
15959
|
-
* Enable/disable onset detection
|
|
15960
|
-
*/
|
|
15961
|
-
setOnsetDetection: (enabled) => {
|
|
15962
|
-
this.validateReady();
|
|
15963
|
-
this.audioSystem?.setOnsetDetection(enabled);
|
|
15964
|
-
this.debugLog(`Onset detection ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
15926
|
+
setDebugMode: (enabled) => {
|
|
15927
|
+
this.audioSystem?.setDebugMode(enabled);
|
|
15965
15928
|
}
|
|
15966
15929
|
},
|
|
15967
15930
|
/**
|
|
@@ -15973,10 +15936,9 @@ class VijiCore {
|
|
|
15973
15936
|
isConnected: false,
|
|
15974
15937
|
currentBPM: 120,
|
|
15975
15938
|
confidence: 0,
|
|
15976
|
-
mode: "auto",
|
|
15977
|
-
tapCount: 0,
|
|
15978
15939
|
isLocked: false,
|
|
15979
|
-
|
|
15940
|
+
trackingState: "LEARNING",
|
|
15941
|
+
onsetModes: { kick: "auto", snare: "auto", hat: "auto" }
|
|
15980
15942
|
};
|
|
15981
15943
|
},
|
|
15982
15944
|
/**
|
|
@@ -16008,15 +15970,14 @@ class VijiCore {
|
|
|
16008
15970
|
any: 0,
|
|
16009
15971
|
kickSmoothed: 0,
|
|
16010
15972
|
snareSmoothed: 0,
|
|
15973
|
+
hatSmoothed: 0,
|
|
16011
15974
|
anySmoothed: 0,
|
|
16012
15975
|
triggers: { any: false, kick: false, snare: false, hat: false },
|
|
16013
15976
|
bpm: 120,
|
|
16014
|
-
phase: 0,
|
|
16015
|
-
bar: 0,
|
|
16016
15977
|
confidence: 0,
|
|
16017
15978
|
isLocked: false
|
|
16018
15979
|
},
|
|
16019
|
-
spectral: { brightness: 0, flatness: 0
|
|
15980
|
+
spectral: { brightness: 0, flatness: 0 }
|
|
16020
15981
|
};
|
|
16021
15982
|
}
|
|
16022
15983
|
};
|
|
@@ -16152,9 +16113,8 @@ class VijiCore {
|
|
|
16152
16113
|
async setDeviceVideo(deviceId, stream) {
|
|
16153
16114
|
this.validateReady();
|
|
16154
16115
|
await this.clearDeviceVideo(deviceId);
|
|
16155
|
-
const baseOffset = 1 + this.videoStreams.length;
|
|
16156
16116
|
const usedIndices = new Set(this.deviceVideoStreamIndices.values());
|
|
16157
|
-
let streamIndex =
|
|
16117
|
+
let streamIndex = VijiCore.DEVICE_VIDEO_BASE;
|
|
16158
16118
|
while (usedIndices.has(streamIndex)) {
|
|
16159
16119
|
streamIndex++;
|
|
16160
16120
|
}
|
|
@@ -16317,4 +16277,4 @@ export {
|
|
|
16317
16277
|
VijiCoreError as b,
|
|
16318
16278
|
getDefaultExportFromCjs as g
|
|
16319
16279
|
};
|
|
16320
|
-
//# sourceMappingURL=index-
|
|
16280
|
+
//# sourceMappingURL=index-trkn0FNW.js.map
|