mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-10-02 19:32:38 +00:00
Revert "fix(audio): user-stop latch - plugin keep-alives can no longer undo a user stop"
This reverts commit bb0f79e0be.
This commit is contained in:
@@ -624,37 +624,19 @@ export function initAudioBridge(): void {
|
||||
// ── Audio Control ──────────────────────────────────────────────────────
|
||||
|
||||
ipcMain.handle('audio:startAudio', (event) => {
|
||||
// Legacy raw start (unmigrated plugins, programmatic paths). While
|
||||
// the user-stop latch is set this is SUPPRESSED — found in the field:
|
||||
// nam_tone's keep-alive watchdog restarted the engine 1.5 s after a
|
||||
// user stop (§8.3). User authority lives on audio:userStartAudio.
|
||||
// Raw start = user authority (device screen). Resumes any demands the
|
||||
// last raw stop suspended (§8.3). Plugin callers should migrate to
|
||||
// the `capture` demand (audio:leases:acquireDemand) — log-once
|
||||
// telemetry tracks who still comes through here (§6.8).
|
||||
leaseBridge?.noteLegacyCall(event.sender, 'audio:startAudio');
|
||||
if (leaseBridge?.isUserStopLatched()) {
|
||||
leaseBridge.noteLegacyCall(event.sender, 'audio:startAudio[suppressed-by-user-stop]');
|
||||
return;
|
||||
}
|
||||
audio?.startAudio();
|
||||
leaseBridge?.onUserStartAudio();
|
||||
});
|
||||
|
||||
ipcMain.handle('audio:stopAudio', (event) => {
|
||||
// Legacy raw stop: stops the engine but is NOT user authority — the
|
||||
// device-apply flow and unmigrated plugins use it transiently. The
|
||||
// user's stop (latch + demand suspension) is audio:userStopAudio.
|
||||
// Raw stop always wins: engine stops, demands suspend (not clear) so
|
||||
// holders resume on the next user start (§8.3).
|
||||
leaseBridge?.noteLegacyCall(event.sender, 'audio:stopAudio');
|
||||
audio?.stopAudio();
|
||||
});
|
||||
|
||||
ipcMain.handle('audio:userStartAudio', () => {
|
||||
// Device screen only: clears the user-stop latch, resumes suspended
|
||||
// demands, starts the engine (§8.3).
|
||||
leaseBridge?.onUserStartAudio();
|
||||
audio?.startAudio();
|
||||
});
|
||||
|
||||
ipcMain.handle('audio:userStopAudio', () => {
|
||||
// Device screen only: user stop always wins — latch on, demands
|
||||
// suspended, engine stopped. Legacy raw starts stay suppressed until
|
||||
// the user starts again (§8.3).
|
||||
leaseBridge?.onUserStopAudio();
|
||||
audio?.stopAudio();
|
||||
});
|
||||
|
||||
@@ -43,10 +43,6 @@ export type LeaseBridge = {
|
||||
// Hooks for the legacy raw channels (§6.8 migration semantics):
|
||||
onUserStartAudio(): void;
|
||||
onUserStopAudio(): void;
|
||||
// §8.3 user-stop latch: while set, legacy raw startAudio (nam_tone's
|
||||
// keep-alive watchdog, any unmigrated caller) must be suppressed — only
|
||||
// an explicit user start clears it.
|
||||
isUserStopLatched(): boolean;
|
||||
// true = swallow the raw disarm because a demand holder still needs
|
||||
// detection armed (the 6.3 "last disarmer kills a concurrent consumer" fix).
|
||||
shouldIgnoreRawDetectionDisarm(): boolean;
|
||||
@@ -66,14 +62,9 @@ export function initLeaseBridge(getAudio: () => AudioModule, options: { broadcas
|
||||
// "that telemetry IS the migration progress dashboard").
|
||||
const legacyCallsLogged = new Set<string>();
|
||||
// Whether the engine is running because capture demand started it (as
|
||||
// opposed to a user start). Demand-started engines stop when the demand
|
||||
// drains; user-started engines only stop on user stop (§8.3).
|
||||
// opposed to a user raw start). Demand-started engines stop when the
|
||||
// demand drains; user-started engines only stop on user raw stop (§8.3).
|
||||
let engineStartedByDemand = false;
|
||||
// User-stop latch (§8.3): the device screen's explicit stop wins over
|
||||
// EVERYTHING — legacy raw starts and fresh capture demands are held off
|
||||
// until the user starts again. Found in the field: nam_tone's 1.5 s
|
||||
// keep-alive watchdog restarted the engine right after a user stop.
|
||||
let userStopLatch = false;
|
||||
|
||||
function sanitizeTag(tag: unknown): string | null {
|
||||
if (typeof tag !== 'string' || !TAG_RE.test(tag)) return null;
|
||||
@@ -142,9 +133,6 @@ export function initLeaseBridge(getAudio: () => AudioModule, options: { broadcas
|
||||
if (!audio) return;
|
||||
if (scope === 'capture') {
|
||||
try {
|
||||
// A demand arriving while the user has stopped audio does not
|
||||
// restart the engine — it waits for the user start (§8.3).
|
||||
if (userStopLatch && active) return;
|
||||
const running = typeof audio.isAudioRunning === 'function' && audio.isAudioRunning() === true;
|
||||
if (active && !running) {
|
||||
audio.startAudio?.();
|
||||
@@ -209,13 +197,7 @@ export function initLeaseBridge(getAudio: () => AudioModule, options: { broadcas
|
||||
const holderId = deriveHolder(sender, tag);
|
||||
trackSender(sender, holderId);
|
||||
registry.tryRestore(identityKey(sender, tag), holderId);
|
||||
const ok = registry.acquireDemand(String(scope), holderId);
|
||||
// A demand born during a user stop starts suspended, so the user
|
||||
// start resumes it like every pre-existing demand (§8.3).
|
||||
if (ok && userStopLatch && String(scope).startsWith('capture')) {
|
||||
registry.suspendDemands(String(scope));
|
||||
}
|
||||
return ok;
|
||||
return registry.acquireDemand(String(scope), holderId);
|
||||
},
|
||||
|
||||
releaseDemand(sender, scope, tag) {
|
||||
@@ -227,28 +209,21 @@ export function initLeaseBridge(getAudio: () => AudioModule, options: { broadcas
|
||||
},
|
||||
|
||||
onUserStartAudio() {
|
||||
// User start is the only thing that clears the stop latch and
|
||||
// resumes suspended demands (§8.3).
|
||||
userStopLatch = false;
|
||||
// User raw start is the only thing that resumes suspended
|
||||
// demands (§8.3).
|
||||
engineStartedByDemand = false;
|
||||
registry.resumeDemands('capture');
|
||||
registry.resumeDemands('detection:');
|
||||
},
|
||||
|
||||
onUserStopAudio() {
|
||||
// User stop always wins: the latch holds off legacy raw starts
|
||||
// and fresh demands; existing demands suspend (registration
|
||||
// kept), holders learn via demand-suspended events (§8.3).
|
||||
userStopLatch = true;
|
||||
// User raw stop always wins: demands suspend (registration kept),
|
||||
// holders learn via demand-suspended events (§8.3).
|
||||
engineStartedByDemand = false;
|
||||
registry.suspendDemands('capture');
|
||||
registry.suspendDemands('detection:');
|
||||
},
|
||||
|
||||
isUserStopLatched() {
|
||||
return userStopLatch;
|
||||
},
|
||||
|
||||
shouldIgnoreRawDetectionDisarm() {
|
||||
return anyDetectionActive();
|
||||
},
|
||||
|
||||
@@ -247,12 +247,6 @@ const feedBackDesktopApi = {
|
||||
// Audio control
|
||||
startAudio: () => ipcRenderer.invoke('audio:startAudio'),
|
||||
stopAudio: () => ipcRenderer.invoke('audio:stopAudio'),
|
||||
// User-authority start/stop (device screen only, §8.3): stop latches
|
||||
// — legacy raw starts and fresh capture demands are held off until
|
||||
// the user starts again; start clears the latch and resumes
|
||||
// suspended demands.
|
||||
userStartAudio: () => ipcRenderer.invoke('audio:userStartAudio'),
|
||||
userStopAudio: () => ipcRenderer.invoke('audio:userStopAudio'),
|
||||
isAudioRunning: () => ipcRenderer.invoke('audio:isAudioRunning'),
|
||||
|
||||
// Engine-owned mixer (docs/audio-ownership-plan.md §5.1). Tier 1
|
||||
|
||||
@@ -965,8 +965,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
|
||||
if (ok) {
|
||||
const inputChannel = parseInt(inputChannelSelect.value);
|
||||
if (Number.isFinite(inputChannel)) await api.setInputChannel(inputChannel);
|
||||
// User-initiated device change: clear any user-stop latch.
|
||||
await (api.userStartAudio ? api.userStartAudio() : api.startAudio());
|
||||
await api.startAudio();
|
||||
audioRunning = true;
|
||||
toggleBtn.textContent = 'Stop';
|
||||
statusDot.className = 'w-3 h-3 rounded-full bg-emerald-500';
|
||||
@@ -1524,16 +1523,13 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
|
||||
// Start/Stop audio
|
||||
toggleBtn.addEventListener('click', async () => {
|
||||
if (audioRunning) {
|
||||
// User authority (§8.3): latches the stop so plugin keep-alive
|
||||
// watchdogs (nam_tone) and fresh capture demands can't restart
|
||||
// the engine until the user starts again.
|
||||
await (api.userStopAudio ? api.userStopAudio() : api.stopAudio());
|
||||
await api.stopAudio();
|
||||
audioRunning = false;
|
||||
toggleBtn.textContent = 'Start';
|
||||
statusDot.className = 'w-3 h-3 rounded-full bg-yellow-500';
|
||||
statusText.textContent = 'Audio stopped';
|
||||
} else {
|
||||
await (api.userStartAudio ? api.userStartAudio() : api.startAudio());
|
||||
await api.startAudio();
|
||||
audioRunning = true;
|
||||
toggleBtn.textContent = 'Stop';
|
||||
statusDot.className = 'w-3 h-3 rounded-full bg-emerald-500';
|
||||
@@ -1612,8 +1608,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
|
||||
if (Number.isFinite(inputChannel)) await api.setInputChannel(inputChannel);
|
||||
await api.setMonitorMute(monitorMuteCheckbox.checked);
|
||||
await api.setMonitorKill?.(monitorKillCheckbox.checked);
|
||||
// Apply-device is a user action: clear any user-stop latch.
|
||||
await (api.userStartAudio ? api.userStartAudio() : api.startAudio());
|
||||
await api.startAudio();
|
||||
audioRunning = true;
|
||||
toggleBtn.textContent = 'Stop';
|
||||
statusDot.className = 'w-3 h-3 rounded-full bg-emerald-500';
|
||||
|
||||
@@ -117,7 +117,5 @@
|
||||
"audio:stopAudio",
|
||||
"audio:stopBacking",
|
||||
"audio:unbindInputDevice",
|
||||
"audio:userStartAudio",
|
||||
"audio:userStopAudio",
|
||||
"debug:isEnabled"
|
||||
]
|
||||
|
||||
@@ -100,9 +100,7 @@
|
||||
"startBacking",
|
||||
"stopAudio",
|
||||
"stopBacking",
|
||||
"unbindInputDevice",
|
||||
"userStartAudio",
|
||||
"userStopAudio"
|
||||
"unbindInputDevice"
|
||||
],
|
||||
"audioEffects": [
|
||||
"activateSegment",
|
||||
|
||||
@@ -100,33 +100,6 @@ test('user stop suspends demands; user start resumes and restarts (§8.3)', () =
|
||||
bridge.dispose();
|
||||
});
|
||||
|
||||
test('user-stop latch: fresh demands and legacy starts held off until user start (§8.3)', () => {
|
||||
const audio = fakeAudio();
|
||||
const { bridge } = makeBridge(audio);
|
||||
const tuner = fakeSender(1);
|
||||
const watchdog = fakeSender(2);
|
||||
|
||||
bridge.onUserStopAudio();
|
||||
assert.equal(bridge.isUserStopLatched(), true);
|
||||
|
||||
// nam_tone-style watchdog raw start would be suppressed (the bridge
|
||||
// exposes the latch; the IPC handler consults it before starting).
|
||||
assert.equal(bridge.isUserStopLatched(), true);
|
||||
|
||||
// A brand-new capture demand during the latch must NOT start the engine…
|
||||
bridge.acquireDemand(tuner, 'capture', 'tuner');
|
||||
assert.deepEqual(audio.calls, []);
|
||||
|
||||
// …but the user start resumes it like any pre-existing demand.
|
||||
bridge.onUserStartAudio();
|
||||
assert.equal(bridge.isUserStopLatched(), false);
|
||||
assert.deepEqual(audio.calls, ['start']);
|
||||
|
||||
// Watchdog sender only matters for telemetry attribution; unused here.
|
||||
void watchdog;
|
||||
bridge.dispose();
|
||||
});
|
||||
|
||||
test('detection demand arms native; raw disarm guarded while demand active (6.3)', () => {
|
||||
const audio = fakeAudio();
|
||||
const { bridge } = makeBridge(audio);
|
||||
|
||||
Reference in New Issue
Block a user