feat(tuner): mic-verify — promote working tuning assumed→verified via a per-string check (working-tuning PR 9b) (#670)

* feat(tuner): mic-verify — promote the working tuning assumed→verified via a per-string check (working-tuning PR 9b)

Adds the choreographed per-string mic verification the design reserved for
'verified' provenance (audio-engine's honesty rule — nothing else may claim it):
the player plays each string, and once every one reads in tune (±6 cents) and
holds stable for 8 frames, the tuner stamps the working tuning
provenance:'verified' + verifiedStrings via workingTuning.set.

- screen.js: a pure verify state machine (verifyStart/verifyFeed/verifyCancel/
  verifyState, exposed on the tuner API) + the set-verified writer; cancels on close.
- ui.js: updateUI feeds each processed frame (matched string + cents) into the
  session; a "Verify tuning" button + per-string progress + status, shown for a
  selected (non-free) tuning.

Pairs with the 9a lifecycle: a 'verified' decays back to 'assumed' on the next
song load, so mic-verify is a per-session confidence boost, never a sticky claim.

Tests: tests/js/tuner_auto_open.test.js +4 (all-strings->verified, out-of-tune
never completes, streak resets on drift, API exposed / only it claims verified) —
33/33. The state machine is headless-verified with synthetic frames; the real
per-string mic detection + the button flow need an on-device pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

* fix(tuner): mic-verify writes the confirmed tuning + no clobber + stricter streak (PR #670 review)

Review fixes for mic-verify (working-tuning PR 9b):

- 'verified' could attach to STALE offsets: _publishVerified stamped provenance
  without writing offsets, so the slot's pre-tuning offsets got marked verified.
  verifyStart(targets, offsets) now captures the confirmed tuning's offsets, and
  _publishVerified writes offsets + stringCount + instrument + referencePitch +
  verifiedStrings ATOMICALLY with provenance:'verified' into the selected slot
  (and refuses to stamp verified with no concrete offsets).
- The assumed publish-on-clear immediately clobbered a just-earned 'verified':
  disable() now skips it when a mic-verify wrote verified this session
  (_verifiedPublished).
- The per-string streak could accumulate across silence / wrong-string frames.
  verifyFeed now requires CONSECUTIVE in-tune frames: the one confirmed string
  advances, every other unfinished string resets each frame.
- A mid-verify tuning change (song switch) left stale captured offsets; verify is
  now cancelled in _syncCurrentTuning when the song tuning changes.

Tests: verify writes the confirmed offsets (not stale); source-guard for the
no-clobber path. 47 tuner + 77 tuner/capability 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:
ChrisBeWithYou
2026-07-01 11:33:52 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 byrongamatos
parent 115c96a3f0
commit 2910a3c8cb
3 changed files with 205 additions and 2 deletions
+65
View File
@@ -305,6 +305,12 @@ window._tunerUI = function(state, actions) {
function renderStringNotes() {
if (!state.stringNoteContainer) return;
state.stringNoteContainer.innerHTML = '';
// Show the mic-verify control only for a selected (non-free) tuning.
if (state.verifyRow) {
const hasTuning = !!(state.selectedTuning && state.selectedTuning.length && !state.freeTune);
state.verifyRow.classList.toggle('hidden', !hasTuning);
if (!hasTuning) resetVerifyUI();
}
if (!state.selectedTuning || state.selectedTuning.length === 0) {
_syncStringOrderHelp(0);
return;
@@ -327,6 +333,41 @@ window._tunerUI = function(state, actions) {
_syncStringOrderHelp(total);
}
// ── Mic-verify UI (working-tuning PR 9b) ───────────────────────────────────
function _markVerifiedStrings(done) {
if (!state.stringNoteContainer) return;
state.stringNoteContainer.querySelectorAll('[data-freq]').forEach((btn, i) => {
btn.classList.toggle('ring-2', !!done[i]);
btn.classList.toggle('ring-emerald-400', !!done[i]);
});
}
function _syncVerifyProgress(vs) {
if (!vs || !state.verifyStatus) return;
const total = vs.done.length;
const done = vs.done.filter(Boolean).length;
state.verifyStatus.classList.remove('hidden');
state.verifyStatus.textContent = vs.complete
? '✓ In tune — tuning verified'
: (done + ' of ' + total + ' strings in tune');
_markVerifiedStrings(vs.done);
if (vs.complete && state.verifyBtn) state.verifyBtn.textContent = 'Verify tuning';
}
function _startVerify() {
if (!window._tunerAutoOpen || typeof window._tunerAutoOpen.verifyStart !== 'function') return;
const vs = window._tunerAutoOpen.verifyStart();
if (!vs) return;
if (state.verifyBtn) state.verifyBtn.textContent = 'Verifying — play each string…';
_syncVerifyProgress(vs);
}
function resetVerifyUI() {
if (window._tunerAutoOpen && typeof window._tunerAutoOpen.verifyCancel === 'function') {
window._tunerAutoOpen.verifyCancel();
}
if (state.verifyBtn) state.verifyBtn.textContent = 'Verify tuning';
if (state.verifyStatus) { state.verifyStatus.classList.add('hidden'); state.verifyStatus.textContent = ''; }
_markVerifiedStrings([]);
}
function updateUI(result) {
const { smoothedFreq, rms, hasSignal } = result;
const vizMode = state.manualTargetFreq ? 'manual'
@@ -387,6 +428,13 @@ window._tunerUI = function(state, actions) {
if (window.feedBack && window.feedBack.emit) {
window.feedBack.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true });
}
// Mic-verify: feed the matched string + cents to a running verify session
// and reflect per-string progress on the panel.
if (!isManual && !state.freeTune && window._tunerAutoOpen
&& typeof window._tunerAutoOpen.verifyFeed === 'function') {
const vs = window._tunerAutoOpen.verifyFeed(targetFreq, Math.round(cents));
if (vs) _syncVerifyProgress(vs);
}
}
function updateFloatingButtonVisibility() {
@@ -635,6 +683,22 @@ window._tunerUI = function(state, actions) {
state.vizContainer.className = 'w-full';
state.uiContainer.appendChild(state.vizContainer);
// Mic-verify control (working-tuning PR 9b): play each string in tune to
// confirm your tuning — promotes 'assumed' → 'verified'. Shown only for a
// selected (non-free) tuning; visibility managed in renderStringNotes().
state.verifyRow = document.createElement('div');
state.verifyRow.className = 'w-full mt-2 hidden';
state.verifyBtn = document.createElement('button');
state.verifyBtn.className = 'tuner-verify-btn w-full text-[11px] text-fb-textDim hover:text-fb-text border border-fb-border/40 hover:border-fb-border/70 rounded-lg py-1.5 transition-colors';
state.verifyBtn.textContent = 'Verify tuning';
state.verifyBtn.title = 'Play each string in tune to confirm — marks your tuning verified';
state.verifyBtn.onclick = () => _startVerify();
state.verifyRow.appendChild(state.verifyBtn);
state.verifyStatus = document.createElement('div');
state.verifyStatus.className = 'text-[10px] text-fb-textDim text-center mt-1 hidden';
state.verifyRow.appendChild(state.verifyStatus);
state.uiContainer.appendChild(state.verifyRow);
// Auto-open nudge's explicit dismiss (hidden unless auto-opened; enable()
// toggles it). Closes the same way as the × — disable().
const skipBtn = document.createElement('button');
@@ -778,6 +842,7 @@ window._tunerUI = function(state, actions) {
renderTuningOptions,
renderStringNotes,
updateUI,
resetVerify: resetVerifyUI,
updateInstrumentDisplay: _updateInstrumentDisplay,
updateSaveAsCustomVisibility: _updateSaveAsCustomVisibility,
updateFreeTuneUI,