mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 03:41:40 +00:00
feat(tuner): gate playback until you've tuned — hold autoplay + no-trap Skip/Back/Esc (working-tuning PR 4) (#666)
* feat(tuner): gate playback until you've tuned — hold autoplay + no-trap Skip/Back/Esc (working-tuning PR 4) When the opt-in auto-open fires because a song needs a different tuning, playback now WAITS behind the tuner instead of starting underneath it — the "tune before you play" model. Built on a new generic core hook window.feedBack.holdAutoplay() (mirrors holdAutoExit): the tuner claims the hold synchronously on song:loading (beating the song:ready autostart) and releases it — or a 12s fail-open backstop does — so a wedged plugin can never strand a song. Generation-guarded; manual Play always wins. No one-way trap: - Skip = "I've tuned" -> plays and records the song's tuning as the instrument's current working tuning (the explicit write-point PR 3 left as 'assumed'). - Back to library / Esc -> leave the song, record nothing (reuses requestExitSong; Esc is the existing player shortcut). - The in-panel x is dropped for an auto-open — Skip/Back/Esc are the dismiss surface. This also keeps the write honest: Skip is the only on-player dismiss that records, so leaving never falsely records a tuning. Stacked on #660 (working-tuning PR 3). Core app.js gains only the generic hook (a test asserts it never references the tuner's internals); shell-agnostic. Needs a desktop smoke-test that the tuner mic doesn't contend with note_detect's scoring input under ASIO/exclusive mode (per the design charrette). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(tuner): backstop can't cut off tuning + gate race/token hardening (PR #666 review) Review fixes for the autoplay gate: - The 12s fail-open backstop could start playback UNDER a legitimately-open tuner (a slow / mic-verify retune > 12s). holdAutoplay()'s release now carries a .settle() that cancels the backstop; the tuner calls it once the tuner is confirmed open (_gateClaimed), so the hold becomes deliberate and only a dismiss / song switch releases it. (Fail-open still covers "claimed but wedged before deciding".) - The async song:ready handler could release a NEWER song's gate after its await (global _gateClaimed, no guard). It now snapshots _autoOpenGeneration and bails if a newer song took over. - holdAutoplay guarded by song generation, not per-hold — a stale release from an earlier hold could clear a later one. Each hold now mints a unique token that release()/settle() must match. Tests: source-level assertions for the token, settle(), the settle-on-open call, and the song:ready gen-guard. 45 tuner+speed tests green. Codex-reviewed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
48f435408f
commit
115c96a3f0
@@ -148,6 +148,7 @@ function loadPlaySong(sandbox) {
|
||||
var _playerOriginScreen = null;
|
||||
var _pendingAutostart = false;
|
||||
function _clearAutoExit() {}
|
||||
function _clearAutoplayHold() {}
|
||||
function _resolvePlayerOrigin() { return 'home'; }
|
||||
function _recordPlaybackBridge() {}
|
||||
function _cancelCountIn() {}
|
||||
|
||||
@@ -9,6 +9,7 @@ const vm = require('node:vm');
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||
const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js');
|
||||
const TUNING_UTILS_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'tuning-utils.js');
|
||||
const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js');
|
||||
|
||||
function loadTuningHelpers() {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
@@ -461,6 +462,71 @@ test('the tuner badge surfaces a passive coverage cue (badges.js)', () => {
|
||||
assert.match(badgesSrc, /boxShadow/);
|
||||
});
|
||||
|
||||
// ── Autoplay gate (E2) ─────────────────────────────────────────────────────
|
||||
test('gate: claims an autoplay hold on song:loading and releases it on dismiss', async () => {
|
||||
const sandbox = createTunerSandbox({ player: PLAYER_GUITAR_6 });
|
||||
let holds = 0, releases = 0;
|
||||
sandbox.window.feedBack.holdAutoplay = () => { holds++; return () => { releases++; }; };
|
||||
const api = sandbox.window._tunerAutoOpen;
|
||||
await ready(sandbox, DROP_D); // loads config (feature on)
|
||||
api.resetState();
|
||||
api.onSongLoading(); // song:loading → claim the gate
|
||||
assert.equal(holds, 1);
|
||||
assert.equal(releases, 0);
|
||||
sandbox.window.tuner.disable(); // dismiss → release (playback proceeds)
|
||||
assert.equal(releases, 1);
|
||||
});
|
||||
|
||||
test('gate: does not claim a hold when the feature is off', async () => {
|
||||
const sandbox = createTunerSandbox({ player: PLAYER_GUITAR_6, autoOpen: false });
|
||||
let holds = 0;
|
||||
sandbox.window.feedBack.holdAutoplay = () => { holds++; return () => {}; };
|
||||
const api = sandbox.window._tunerAutoOpen;
|
||||
await ready(sandbox, DROP_D); // loads config (feature OFF)
|
||||
api.onSongLoading();
|
||||
assert.equal(holds, 0);
|
||||
});
|
||||
|
||||
test('the autoplay gate is a generic core hook with a fail-open backstop (app.js)', () => {
|
||||
const appSrc = fs.readFileSync(APP_JS, 'utf8');
|
||||
assert.match(appSrc, /window\.feedBack\.holdAutoplay = function/);
|
||||
assert.match(appSrc, /AUTOPLAY_HOLD_BACKSTOP_MS/); // fail-open: never strand the song
|
||||
assert.match(appSrc, /if \(_autoplayHeld\) \{ _autoplayStart = start;/); // a gated start is stashed
|
||||
assert.match(appSrc, /_clearAutoplayHold\(\);[\s\S]{0,160}emit\('song:loading'/); // reset before plugins re-claim
|
||||
// Per-hold identity: a stale release from an earlier hold must not clear a later one.
|
||||
assert.match(appSrc, /token !== _autoplayHoldToken/);
|
||||
// settle(): a committed holder can cancel the fail-open backstop (no timed release).
|
||||
assert.match(appSrc, /release\.settle = function/);
|
||||
// The hook is generic — app.js still doesn't reference the tuner's internals.
|
||||
assert.doesNotMatch(appSrc, /_tunerAutoOpen|maybeAutoOpenOnTuningChange/);
|
||||
});
|
||||
|
||||
test('the tuner gates playback via holdAutoplay (screen.js)', () => {
|
||||
const src = fs.readFileSync(TUNER_SCREEN_JS, 'utf8');
|
||||
assert.match(src, /window\.feedBack\.holdAutoplay\(\)/); // claimed on song:loading
|
||||
assert.match(src, /_gateClaimed = true/); // kept when the tuner opens
|
||||
assert.match(src, /if \(!_gateClaimed\) _releaseGate\(\)/); // released when we don't open
|
||||
assert.match(src, /_releaseGate\(\);[\s\S]{0,80}dismissing a gated/); // released on dismiss
|
||||
// Once open, the tuner settles the hold so the 12s backstop can't start playback
|
||||
// while the player is still tuning.
|
||||
assert.match(src, /_autoplayRelease\.settle\(\)/);
|
||||
// The async song:ready handler is generation-guarded so it can't release a NEWER
|
||||
// song's gate after its await.
|
||||
assert.match(src, /_onAutoOpenSongReady = async \(\) => \{[\s\S]{0,320}myGen !== _autoOpenGeneration\) return;[\s\S]{0,120}_releaseGate/);
|
||||
});
|
||||
|
||||
test('gate escape hatch: auto-open offers "Back to library" (+ Esc) and hides the × (ui.js/screen.js)', () => {
|
||||
const ui = fs.readFileSync(TUNER_UI_JS, 'utf8');
|
||||
assert.match(ui, /Back to library/); // an explicit way out, not only play-now
|
||||
assert.match(ui, /requestExitSong/); // reuses the standard song-exit path (mirrors Esc)
|
||||
assert.match(ui, /state\.backBtn =/);
|
||||
assert.match(ui, /state\.closeBtn =/); // × captured so it can be toggled
|
||||
|
||||
const screen = fs.readFileSync(TUNER_SCREEN_JS, 'utf8');
|
||||
assert.match(screen, /_state\.backBtn[\s\S]{0,60}toggle\('hidden', !auto\)/); // shown on auto-open
|
||||
assert.match(screen, /_state\.closeBtn[\s\S]{0,60}toggle\('hidden', !!auto\)/); // × hidden on auto-open
|
||||
});
|
||||
|
||||
test('auto-open does not require app.js changes', () => {
|
||||
const appSrc = fs.readFileSync(APP_JS, 'utf8');
|
||||
assert.doesNotMatch(appSrc, /_tunerAutoOpen|maybeAutoOpenOnTuningChange/);
|
||||
|
||||
Reference in New Issue
Block a user