mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-13 08:29:28 +00:00
feat(audio): stems ride a bespoke mixer channel; tuner/minigames use capture demands
Ownership-plan client migrations (feedBack-desktop feat/audio-ownership-phase-a
counterpart):
- juce-audio.js: the stems graph requests its own mixer channel
(audio.mixer.requestChannel('stems')) and pushes there instead of the
aggregate renderer bus - a renderer stall no longer starves stem audio
behind everything else, and per-channel underflow counters name it in
field logs. Sink already flips to 'none' before any push, so the s5
double-audio guard holds by construction. Legacy renderer-bus path kept
for old desktop mains / no-capacity refusals.
- tuner + minigames: startAudio start-then-remember-to-undo hacks replaced
by the refcounted 'capture' demand (plan s6.1) with legacy fallback;
demand released on stop/supersede, and holder death releases it anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4e0e3c5417
commit
26351e244d
@@ -135,6 +135,7 @@
|
||||
// post-noise-gate frames from the engine instead — same device the player
|
||||
// and note_detect read from. getUserMedia stays as the web fallback.
|
||||
let bridgePoll = null; // setTimeout handle for the engine poll loop
|
||||
let holdsCaptureDemand = false; // engine `capture` demand held (plan §6.1)
|
||||
let usingBridge = false;
|
||||
let bridgeGotFrame = false; // first non-empty frame seen (vs downlevel addon)
|
||||
let bridgeSampleRate = 48000; // queried once from the engine
|
||||
@@ -268,6 +269,10 @@
|
||||
stopped = true;
|
||||
if (bridgePoll) { try { clearTimeout(bridgePoll); } catch (e) {} bridgePoll = null; }
|
||||
usingBridge = false;
|
||||
if (holdsCaptureDemand) {
|
||||
holdsCaptureDemand = false;
|
||||
try { window.feedBackDesktop?.audio?.leases?.releaseDemand('capture', 'minigames'); } catch (e) {}
|
||||
}
|
||||
try { if (processor) processor.disconnect(); } catch (e) {}
|
||||
try { if (source) source.disconnect(); } catch (e) {}
|
||||
try { if (mediaStream) mediaStream.getTracks().forEach(t => t.stop()); } catch (e) {}
|
||||
@@ -285,8 +290,14 @@
|
||||
// is downlevel (getRawAudioFrame resolves an empty array).
|
||||
async function startBridge(audio) {
|
||||
try {
|
||||
// The minigame needs live input; make sure the engine is capturing.
|
||||
if (typeof audio.isAudioRunning === 'function') {
|
||||
// The minigame needs live input. Prefer the refcounted capture
|
||||
// demand (ownership plan §6.1): the engine runs while any holder
|
||||
// needs it, and a dead renderer releases automatically — no
|
||||
// start-then-remember-to-undo. Legacy raw start on old mains.
|
||||
if (typeof audio.leases?.acquireDemand === 'function') {
|
||||
await audio.leases.acquireDemand('capture', 'minigames');
|
||||
holdsCaptureDemand = true;
|
||||
} else if (typeof audio.isAudioRunning === 'function') {
|
||||
const running = await audio.isAudioRunning();
|
||||
if (!running && typeof audio.startAudio === 'function') await audio.startAudio();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@
|
||||
// if it changes mid-flight, so a re-entrant start()/restart() can't orphan
|
||||
// a worker + interval created by a superseded call.
|
||||
let _startGen = 0;
|
||||
// Whether we hold the engine `capture` demand (ownership plan §6.1).
|
||||
let _holdsCaptureDemand = false;
|
||||
|
||||
function _releaseCaptureDemand(desktop) {
|
||||
if (!_holdsCaptureDemand) return;
|
||||
_holdsCaptureDemand = false;
|
||||
try { desktop?.audio?.leases?.releaseDemand('capture', 'tuner'); } catch (_) {}
|
||||
}
|
||||
// Timestamp of the last frame posted to the worker (watchdog, see above).
|
||||
let _frameSentAt = 0;
|
||||
|
||||
@@ -104,20 +112,31 @@
|
||||
|
||||
var started = false;
|
||||
try {
|
||||
var running = typeof desktop.audio.isAudioRunning === 'function'
|
||||
? await desktop.audio.isAudioRunning() : false;
|
||||
if (!running && typeof desktop.audio.startAudio === 'function') {
|
||||
await desktop.audio.startAudio();
|
||||
started = true;
|
||||
if (typeof desktop.audio.leases?.acquireDemand === 'function') {
|
||||
// Ownership plan §6.1: express "the engine must capture" as a
|
||||
// refcounted demand instead of the start-then-remember-to-undo
|
||||
// hack — the engine keeps running while ANY holder needs it,
|
||||
// and a dead renderer releases automatically.
|
||||
await desktop.audio.leases.acquireDemand('capture', 'tuner');
|
||||
_holdsCaptureDemand = true;
|
||||
} else {
|
||||
// Legacy desktop main without the lease registry.
|
||||
var running = typeof desktop.audio.isAudioRunning === 'function'
|
||||
? await desktop.audio.isAudioRunning() : false;
|
||||
if (!running && typeof desktop.audio.startAudio === 'function') {
|
||||
await desktop.audio.startAudio();
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// A failed startAudio means frames will never arrive — surface it
|
||||
// A failed start/demand means frames will never arrive — surface it
|
||||
// rather than silently claiming a dead bridge.
|
||||
console.warn('[tuner] bridge startAudio failed:', e && e.message ? e.message : e);
|
||||
console.warn('[tuner] bridge capture request failed:', e && e.message ? e.message : e);
|
||||
}
|
||||
if (myGen !== _startGen) {
|
||||
// Superseded by a newer start/stop while awaiting — undo any engine
|
||||
// start we triggered and bail without claiming the bridge.
|
||||
// Superseded by a newer start/stop while awaiting — undo whatever
|
||||
// we acquired and bail without claiming the bridge.
|
||||
_releaseCaptureDemand(desktop);
|
||||
if (started && typeof desktop.audio.stopAudio === 'function') {
|
||||
try { desktop.audio.stopAudio(); } catch (_) {}
|
||||
}
|
||||
@@ -249,6 +268,7 @@
|
||||
// Invalidate any start still suspended on an await so it aborts instead
|
||||
// of installing a worker/interval after we've torn down.
|
||||
_startGen++;
|
||||
_releaseCaptureDemand((typeof window !== 'undefined') ? window.feedBackDesktop : null);
|
||||
if (_bridgeInterval) { clearInterval(_bridgeInterval); _bridgeInterval = null; }
|
||||
_usingDesktopBridge = false;
|
||||
if (_detectInterval) { clearInterval(_detectInterval); _detectInterval = null; }
|
||||
|
||||
Reference in New Issue
Block a user