mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 03:24:10 +00:00
fix(audio): gate the executor's chain-plan startAudio behind the user-stop latch
Second field finding: a user-stopped engine came back when a song load ran a rig_builder chain plan with startAudio:true - the executor calls native startAudio directly in the main process, bypassing the IPC latch check entirely (plan 6.1's second writer). Decision (strict 8.3): only the device screen's user start resumes. lease-bridge gains gateNativeAudio(), a proxy over the native accessor that suppresses startAudio while latched (log-once telemetry line); audio-bridge hands the executor the gated accessor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bb0f79e0be
commit
a142b0f3de
@@ -257,8 +257,11 @@ function loadNativeAddon(): AudioModule | null {
|
|||||||
|
|
||||||
export function initAudioBridge(): void {
|
export function initAudioBridge(): void {
|
||||||
audio = loadNativeAddon();
|
audio = loadNativeAddon();
|
||||||
const audioEffects = createAudioEffectsExecutor(() => audio);
|
|
||||||
leaseBridge = initLeaseBridge(() => audio);
|
leaseBridge = initLeaseBridge(() => audio);
|
||||||
|
// The executor sees a gated native surface: its `startAudio: true` chain
|
||||||
|
// plans (rig_builder song loads) must not undo a user stop (§8.3) — that
|
||||||
|
// path calls native directly and never crosses the IPC latch check.
|
||||||
|
const audioEffects = createAudioEffectsExecutor(leaseBridge.gateNativeAudio(() => audio));
|
||||||
|
|
||||||
// ── Lease registry surface (ownership plan §2/§8; wiring in lease-bridge) ──
|
// ── Lease registry surface (ownership plan §2/§8; wiring in lease-bridge) ──
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export type LeaseBridge = {
|
|||||||
// keep-alive watchdog, any unmigrated caller) must be suppressed — only
|
// keep-alive watchdog, any unmigrated caller) must be suppressed — only
|
||||||
// an explicit user start clears it.
|
// an explicit user start clears it.
|
||||||
isUserStopLatched(): boolean;
|
isUserStopLatched(): boolean;
|
||||||
|
// Wrap a native-audio accessor so its startAudio honors the user-stop
|
||||||
|
// latch — for main-process callers that bypass the IPC handlers (the
|
||||||
|
// audio-effects executor's `startAudio: true` chain plans, plan 6.1).
|
||||||
|
gateNativeAudio<T extends Record<string, (...args: any[]) => any>>(getAudio: () => T | null): () => T | null;
|
||||||
// true = swallow the raw disarm because a demand holder still needs
|
// true = swallow the raw disarm because a demand holder still needs
|
||||||
// detection armed (the 6.3 "last disarmer kills a concurrent consumer" fix).
|
// detection armed (the 6.3 "last disarmer kills a concurrent consumer" fix).
|
||||||
shouldIgnoreRawDetectionDisarm(): boolean;
|
shouldIgnoreRawDetectionDisarm(): boolean;
|
||||||
@@ -249,6 +253,30 @@ export function initLeaseBridge(getAudio: () => AudioModule, options: { broadcas
|
|||||||
return userStopLatch;
|
return userStopLatch;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
gateNativeAudio(getAudioFn) {
|
||||||
|
return () => {
|
||||||
|
const target = getAudioFn();
|
||||||
|
if (!target) return null;
|
||||||
|
return new Proxy(target, {
|
||||||
|
get(t, prop, receiver) {
|
||||||
|
if (prop === 'startAudio') {
|
||||||
|
return (...args: unknown[]) => {
|
||||||
|
if (userStopLatch) {
|
||||||
|
// Strict §8.3: only the device screen's
|
||||||
|
// user start resumes — a chain plan's
|
||||||
|
// startAudio waits it out.
|
||||||
|
console.info('[leases] executor startAudio suppressed — user stop latched (plan §8.3)');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return t.startAudio?.(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Reflect.get(t, prop, receiver);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
shouldIgnoreRawDetectionDisarm() {
|
shouldIgnoreRawDetectionDisarm() {
|
||||||
return anyDetectionActive();
|
return anyDetectionActive();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,6 +127,31 @@ test('user-stop latch: fresh demands and legacy starts held off until user start
|
|||||||
bridge.dispose();
|
bridge.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('gateNativeAudio: executor startAudio suppressed under the latch, transparent otherwise', () => {
|
||||||
|
const audio = fakeAudio();
|
||||||
|
const { bridge } = makeBridge(audio);
|
||||||
|
const gated = bridge.gateNativeAudio(() => audio)();
|
||||||
|
|
||||||
|
// Transparent pass-through when not latched — other methods untouched.
|
||||||
|
gated.startAudio();
|
||||||
|
assert.deepEqual(audio.calls, ['start']);
|
||||||
|
assert.equal(gated.isAudioRunning(), true);
|
||||||
|
|
||||||
|
audio.stopAudio();
|
||||||
|
audio.calls.length = 0;
|
||||||
|
bridge.onUserStopAudio();
|
||||||
|
|
||||||
|
// Chain plan's startAudio:true waits out the user stop (strict §8.3).
|
||||||
|
gated.startAudio();
|
||||||
|
assert.deepEqual(audio.calls, []);
|
||||||
|
|
||||||
|
bridge.onUserStartAudio(); // user start clears; engine started by it
|
||||||
|
audio.calls.length = 0;
|
||||||
|
gated.startAudio();
|
||||||
|
assert.deepEqual(audio.calls, ['start']);
|
||||||
|
bridge.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('detection demand arms native; raw disarm guarded while demand active (6.3)', () => {
|
test('detection demand arms native; raw disarm guarded while demand active (6.3)', () => {
|
||||||
const audio = fakeAudio();
|
const audio = fakeAudio();
|
||||||
const { bridge } = makeBridge(audio);
|
const { bridge } = makeBridge(audio);
|
||||||
|
|||||||
Reference in New Issue
Block a user