mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
749af31cc3f9fa93bcacd3952744473711ec84f0
157
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
749af31cc3 |
fix: correctly import and notate multi-staff (piano/keys) tracks from GP8 (#692)
* fix: correctly import and notate multi-staff (piano/keys) tracks from GP8 Fixes bass stave being dropped on import (bar-column enumeration bug) and wrong hand-split heuristic in notation_lift for chords straddling middle C. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: byrongamatos <xasiklas@gmail.com> * fix(gp-import): fold all grand-staff staves, per-stave tuning, playable hand-splits Addresses review on #692 (topkoa): - split_hands: only use the middle-C boundary when both resulting hands are within HAND_SPLIT_SPAN_SEMITONES, else fall back to the largest-gap heuristic — a hard middle-C split otherwise put a 19-semitone (unplayable) span in one hand for bass-under-treble voicings (e.g. E2+B3 under an Em7 shape). - Treat any multi-stave (grand-staff) track as keys end-to-end, so the stave-0 and folded stave-1+ notes share one encoding and note_count (which sums every stave column) matches what actually imports — closing the phantom-count case for grand-staff instruments the name/program heuristics miss (harp, celesta, marimba). - Fold *every* extra stave (stave_columns[1:]), not just stave 1. - Per-staff tuning fall-back to the track-level Tuning property so an untuned staff never yields an empty pitch list (silent note loss); via a shared _parse_tuning helper. - Extract _collect_column_notes / _merge_lh_notes so the GPX LH/RH pair merge and the GP8 grand-staff fold share one implementation and can't drift in tie/timing/dedup handling. - Rebuild filtered_to_raw from the already-computed stave_columns (one source of truth for the counting rule) and drop the dead num_raw_tracks/raw_tracks. Tests: grand-staff fold + bar-column offset (test_gp2notation.py); both middle-C split cases (test_notation_lift.py). CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: byrongamatos <xasiklas@gmail.com> --------- Signed-off-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
991eadeff6 |
Merge pull request #694 from got-feedback/perf/highway-frame-hotspots
perf: eliminate per-frame layout thrash, shader-compile spikes, and per-frame allocations in the player |
||
|
|
56419dc789 |
feat(drum_highway_3d): hit FX — sparks, timing colors, lane flashes, kick pulse, open hi-hat (D2) (#697)
- Pooled spark bursts (PORTED highway_3d, pool 160) at the struck lane, timing-colored via _timingHex/_classifyTiming (±50ms window, inner 40% = on-time); streak-scaled counts (streakFx) - Lane flashes resurrected as pooled additive gauss-tex quads at the hit line (timing-colored; red for wrong hits); kick = full-width quad - Kick pulse: camera dip + amber floor wash, exp decay, hitFx-scaled - Approach highlight: lane stripes brighten toward their next note - Open hi-hat: hh_open renders a warm ring around the gem (closes TODO); orthogonal to accent/ghost/flam variants - Settings: Hit sparks / Timing colours / Streak feedback toggles + Hit feedback intensity slider (drum_h3d_bg_*, live-applying) - All FX resources pooled/shared, disposed in BOTH teardown paths - Tests: _classifyTiming boundaries + FX defaults (10 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
beefd7bc09 |
feat(keys_highway_3d): foundation for guitar-highway visual parity (K1) (#696)
- setPixelRatio at last: DPR (cap 2; 1.25 when >1 instance) x host adaptive bundle.renderScale — HiDPI displays were rendering at CSS resolution and upscaling (soft/aliased) - Bloom: port _bloomEnsure/_bloomDispose (UnrealBloomPass 0.65/0.5/0.82, MSAA HalfFloat target, ACES<->None switch); hit-line, flames and consume-glow benefit immediately; direct render is the degrade path - First settings panel: settings.html (graphics category) with a live Glow (bloom) toggle; FX scaffold (FX_DEFAULTS/readFxSettings/ window.keys3dSetFx, keys3d_bg_* keys, keys3d:settings event) - Combo/accuracy/best-streak DOM HUD (drum_highway_3d pattern), gated on a live MIDI session - tests/fx_settings.test.js (3 tests; vm harness) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a7ea719652 |
feat(drum_highway_3d): foundation for guitar-highway visual parity (D1) (#695)
- Consume host adaptive-quality bundle.renderScale; fold into DPR like highway_3d (splitscreen proxy: >1 instance caps baseDPR at 1.25) - Bloom: port _bloomEnsure/_bloomDispose (UnrealBloomPass 0.65/0.5/0.82, MSAA HalfFloat target, ACES<->None tone-mapping switch); rebuilt across the kit-change renderer recreation; direct render is the degrade path - FX settings scaffold: FX_DEFAULTS + readFxSettings + drumH3dSetFx (drum_h3d_bg_* keys), Graphics section in settings.html (bloom toggle, default ON, live-applies) - _applyLaneFlashes dead-code comment updated (visual consumer lands in the hit-FX PR) - __test export + tests/data_layer.test.js (vm harness, 8 tests) — picked up by the CI glob from the bundling PR; README refreshed Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
95cb51b2ad |
perf(highway_3d): forceSinglePass on transparent DoubleSide quads
The retrace after the label-swap fix showed getParameters unchanged (~2.5s / ~4% throttled main thread) — the real driver is Three r158+'s transparent-DoubleSide two-pass path: renderBufferDirect renders such objects back side then front side, setting material.needsUpdate BOTH times, i.e. a full getParameters/program-cache lookup twice per object per frame, plus double draw calls. (Found by reading the two-pass branch in the vendored three.module.min.js right next to the getParameters call site.) All 18 transparent DoubleSide materials in this renderer are flat unlit quads — technique markers, sustain rails, chord frames, lane planes, halo bars — where the two-pass self-occlusion ordering buys nothing. Declare forceSinglePass: true on all of them. Also corrects the _setLabelMap comment's churn attribution (that fix removes the label-swap contribution; this one removes the dominant source). Plugin 3.31.1 -> 3.31.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
59aa70ce5a |
perf: remove throttled-trace residuals — program churn, per-frame rect, HUD clock
A 4x-CPU-throttled retrace (the honest weak-hardware proxy) surfaced three residual per-frame costs; stack attribution pinned each: - getParameters/getProgramCacheKey (~4% of main thread): every pooled label sprite map swap set material.needsUpdate, bumping material.version and forcing full program re-resolution next render. Swapping between two non-null cached textures never changes the compiled program (USE_MAP define unchanged) — new _setLabelMap() helper only flags needsUpdate on a null<->texture transition, used at all 7 swap sites. - getBoundingClientRect (~1.2%): the 3D highway's per-frame canvas-size self-check forced a layout read every frame. The CSS-box drift read now runs every 10th frame (or when the wrap isn't pinned); the backing-store comparison stays per-frame with cheap property reads and forces an immediate box read + applySize when it fires. - set textContent: the core 60 Hz HUD clock rewrote hud-time (and getElementById'd it) every tick for a display that changes 1/s — now write-on-change with a cached element ref. (The remaining textContent writer in the trace is notedetect's badges.js — external repo, to be filed there.) tests/js: resize-reframe shape test updated for the hoisted _bsChanged gate, incl. an assertion that the throttle can never delay the backing-store path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1e9741043b |
review: address PR #694 findings
- isVisible() forces a fresh DOM sample (was serving the rAF loop's
throttled cache, contradicting its 'live DOM check' docstring).
- v3 chrome: reconcile the edge-driven overControls hover flag against
matches(':hover') on the throttled ~6 Hz tick — covers a missed
mouseleave (flag stuck true, transport never hides) and a re-created
#player-controls node with lost listeners.
- highway_3d pre-warm now also covers teachFg/teachSd label textures
and the technique sprite factories (mute X, hammer/pull triangles,
bend chevrons, slide arrows) per active-palette string colour, plus
a maintenance note tying new label styles to the warm list.
- Document that the visibility throttle's manual invalidations are
latency-only (periodic resample self-heals within ~10 frames), and
why highway_3d keeps its local lowerBoundT (downlevel hosts).
External-repo audit (finding 1): staffview, tabview, piano, drums,
keys_highway_3d, drum_highway_3d grepped — no cross-frame bundle
retention or bundle-identity checks found.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1e2d5a6162 |
Bundle drum_highway_3d + keys_highway_3d as in-tree core plugins (#693)
* Initial commit — clean relaunch * Initial commit — clean relaunch * Remove external game/format terminology from docs and code Reword references to the external game and its proprietary file formats in comments, docstrings, UI text and identifiers; no behaviour change. * Repoint dead slopsmith URLs -> got-feedback Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(midi): consume core midi-input domain + session robustness fixes (#2) * feat(midi): consume the core midi-input domain instead of private requestMIDIAccess (#881) Route Web-MIDI device access through window.slopsmith.midiInput (discover/list/select/open/close) rather than a private navigator.requestMIDIAccess(). One shared device-access boundary with piano/drums/onboarding; retires this plugin's private Web-MIDI. The note-detection 'midi' exact-verdict provider role and the audio-input source export are preserved (now backed by midi-input source data). Saved pick ({id,name}) stays compatible — domain sourceId == the old MIDIInput.id. Live listener is the domain handle's addListener/removeListener; the async init/resume/pause gate and device-vanish handling are kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): stop exporting MIDI into the audio-input domain Keys MIDI now lives in the dedicated midi-input domain (#881). Continuing to register pseudonymized 'midi-input-N' sources into audio-input polluted audio device pickers — notably the onboarding guitar audio-input dropdown showed MIDI devices under cryptic names. Unregister any leftovers and register none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard async connect races, denied-discovery latch, key consistency Codex preflight findings on the midi-input consumption: - Don't latch `_midiReady` when discover() resolves denied/unavailable — only on a handled outcome — so reopening retries the permission prompt. - Add a generation guard to async `_midiConnect`: a slower open() from an earlier selection can resolve after a device/None switch and install a stale handle/listener. Discard (and close) superseded opens. - Carry the domain `logicalSourceKey` on the source/selection descriptor and use it for select/open/close instead of synthesizing `web-midi::<id>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): invalidate pending opens on detach; clear phantom selection; manifest Codex re-review (round 2): - A detach driven by device removal (sources-changed) didn't advance _midiConnectSeq, so a pending _midiConnect open could resume and install a handle/listener for a now-gone source. Bump the generation inside _midiDetach and capture myGen after it, so any later detach supersedes an in-flight open. - If mi.open() yields no handle, _midiInput stayed set — a phantom connected device the render loop's miss-sweeping would penalize. Clear it on the no-handle and catch paths. - plugin.json: replace the stale audio-input provider role (the removed MIDI-into-audio-input export bridge) with the midi-input requester it now uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): make _midiResume idempotent under multiple live instances Codex re-review (round 3): with the domain addListener API, a second live renderer instance (splitscreen/overlapping lifetimes) calling _midiResume() while already active could register the same listener again and double-deliver a MIDI note to the focused instance (hit + duplicate misses). Return early when already active rather than relying on the provider's Set-backed de-dup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): release domain session on teardown; counter-free legacy source cleanup Codex re-review (round 4): - [P2] Last-instance teardown only detached the listener and never released the midi-input domain session, leaking the requester ref. Add _midiReleaseSession() (delegates to _midiDetach: close + null + generation bump) at the destroy site; re-mount's _midiInit auto-connects from the saved pick. - [P3] The legacy audio-input MIDI-source cleanup looped over the module-local _aiRegisteredCount, which resets to 0 on an in-page upgrade so the prior build's 'keys-midi:input-N' entries were never unregistered. Iterate a fixed bound over the known sourceId pattern instead (unregister of an absent source is a no-op). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): gate miss-sweep on live handle; don't open without a live renderer Codex re-review (round 5): - Miss-sweeping was gated on _midiInput, set as soon as a device is picked, but the async mi.open() may still be pending (slow / permission prompt) — notes passing during that window banked false misses. Gate on _midiHandle, truthy only after a handle is opened and wired. - A settings-only ensure-init (or a discover resolving after the last instance was torn down) could open a midi-input session with no renderer to release it. Gate mi.open() on _instances.size > 0; the pick is saved and a later mount re-runs auto-connect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard open-failure clobber + don't clobber saved device on unplug Bringing keys-highway-3d in line with the piano/drums hardening: - The _midiConnect catch cleared _midiInput unconditionally; a stale older open's rejection could wipe a newer connect's _midiInput/_midiHandle (and leak the handle). Only clear when myGen === _midiConnectSeq. - The sources-changed recovery called _midiAutoConnect(), whose fallback persists a substitute device — overwriting the user's saved pick on a transient multi-device unplug. Parameterize _midiAutoConnect(allowFallback); recovery passes false (reconnect the saved device only, never a persisted fallback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): keep first-hotplug working; don't tear down a live session on re-init Codex round-11: - The unplug-recovery _midiAutoConnect(false) skipped ALL fallbacks, which broke first-hotplug (open visualizer with no keyboard, plug one in → never picked → no connect). Skip the substitute only when a saved pick exists but is absent (preserve it); allow the fallback when nothing was ever picked. - _midiInit re-ran _midiAutoConnect on every ready re-init (settings/splitscreen), tearing down the live handle + releasing held keys. Only re-connect when there's no live handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(midi): consume core midi-input domain + session robustness fixes (#2) * feat(midi): consume the core midi-input domain instead of private requestMIDIAccess (#881) Route Web-MIDI device access through window.slopsmith.midiInput (discover/list/select/open/close) rather than a private navigator.requestMIDIAccess(). One shared device-access boundary with piano/drums/keys/onboarding. Saved pick ({id,name}, with legacy id-only fallback) stays compatible — domain sourceId == the old MIDIInput.id. The async in-flight init guard, _midiActive teardown gate, device-vanish handling, and settings device APIs are preserved; the live listener is the domain handle's addListener/removeListener. Degrades to no-MIDI when the domain is absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard async connect races, denied-discovery latch, key consistency Codex preflight findings on the midi-input consumption: - Don't latch `_midiReady` when discover() resolves denied/unavailable — only on a handled outcome — so reopening retries the permission prompt. - Add a generation guard to async `_midiConnect`: a slower open() from an earlier selection can resolve after a device/None switch and install a stale handle/listener. Discard (and close) superseded opens. - Carry the domain `logicalSourceKey` on the source/selection descriptor and use it for select/open/close instead of synthesizing `web-midi::<id>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): invalidate pending opens on detach; clear phantom selection; manifest Codex re-review (round 2): - A detach driven by device removal (sources-changed) didn't advance _midiConnectSeq, so a pending _midiConnect open could resume and install a handle/listener for a now-gone source. Bump the generation inside _midiDetach and capture myGen after it, so any later detach supersedes an in-flight open. - If mi.open() yields no handle, _midiInput stayed set — a phantom connected device the render loop's miss-sweeping would penalize. Clear it on the no-handle and catch paths. - Declare the midi-input requester capability (degrade-noop) in plugin.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): make _midiResume idempotent under multiple live instances Proactively mirror the keys-highway-3d round-3 fix (same multi-instance lifecycle and identical code): a second live renderer instance calling _midiResume() while already active could double-register the listener and double-deliver a MIDI hit. Return early when already active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): release the domain session on final teardown Mirror the keys-highway-3d round-4 fix: last-instance teardown only detached the listener and never released the midi-input domain session, leaking the requester ref. Add _midiReleaseSession() (delegates to _midiDetach: close + null + generation bump) at the destroy site; re-mount's _midiInit auto-connects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): gate miss-sweep on live handle; don't open without a live renderer Codex re-review (round 5): - [P2] drumH3dEnsureMidiInit (settings-only, no renderer) auto-connected and opened a midi-input session that destroy()/_midiReleaseSession would never run to release — held until reload. Gate mi.open() on _instances.size > 0; the pick is saved and a later renderer mount re-runs auto-connect. - Mirror the keys-highway-3d miss-sweep fix: gate accumulation on _midiHandle (live wired session), not _midiInput (set before the async open resolves), so notes during a pending open don't bank false misses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard open-failure clobber + reconnect saved device on replug Bringing drum-highway-3d in line with the piano/drums hardening: - The _midiConnect catch cleared _midiInput unconditionally; a stale older open's rejection could wipe a newer connect's _midiInput/_midiHandle. Only clear when myGen === _midiConnectSeq. - The sources-changed handler only refreshed the list (never reconnected), so a replug of the saved device didn't reattach. Reconnect on sources-changed via _midiAutoConnect(false) — saved device only, no fallback, so a transient unplug can't switch to / persist another input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): keep first-hotplug working; don't tear down a live session on re-init Same two round-11 fixes as keys-highway-3d (shared structure): the recovery _midiAutoConnect(false) now allows a fallback when nothing was ever picked (first-hotplug) while still preserving a saved-but-absent pick; and _midiInit only re-connects when there's no live handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): select by logicalSourceKey, not provider-local sourceId (#4) Codex review of the midi-input migration: resolving a selection with find(s => s.id === id) treats the provider-local sourceId as globally unique, so when the core midi-input domain aggregates multiple providers two devices sharing a sourceId can't be distinguished — the wrong device opens and unplug detection can miss that the selected key vanished. Thread the globally-unique logicalSourceKey through save/read, _midiConnect, auto-connect reconnect, the public keysH3dSetMidiInput entry, and the capability _aiOpen path, preferring the key and keeping the bare sourceId / name only as a legacy fallback. Mirrors the already-merged drums/piano migration (_midiResolveSaved). Self-reviewed against that reference. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): select by logicalSourceKey, not provider-local sourceId (#3) Codex review of the midi-input migration (same issue as keys-highway-3d): resolving a selection with find(s => s.id === id) treats the provider-local sourceId as globally unique, so when the core midi-input domain aggregates multiple providers two devices sharing a sourceId can't be distinguished — the wrong device opens and unplug detection can miss the vanished key. Thread the globally-unique logicalSourceKey through save/read, _midiConnect, the auto-connect reconnect, and the public drumH3dSetMidiInput entry, preferring the key with the bare sourceId / name only as a legacy fallback. Mirrors the already-merged drums/piano migration. Self-reviewed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: add plugin metadata (category, description, thumbnail) (#5) * chore: add plugin metadata (category/description/thumbnail) Refs got-feedback/feedBack#571 * chore: add placeholder thumbnail Refs got-feedback/feedBack#571 * chore: add plugin metadata (category, description, thumbnail) (#4) * chore: add plugin metadata (category/description/thumbnail) Refs got-feedback/feedBack#571 * chore: add placeholder thumbnail Refs got-feedback/feedBack#571 * fix(viz): register feedBackViz_<id> so the player viz picker lists this plugin (#5) After the slopsmith->feedBack rename the factory was still registered as window.slopsmithViz_<id>; the host viz picker only looks up window.feedBackViz_<id> (no slopsmithViz alias exists) and silently skipped this plugin. Add the feedBackViz_ global (aliased to the existing slopsmithViz_ one for back-compat). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viz): register feedBackViz_<id> so the player viz picker lists this plugin (#6) After the slopsmith->feedBack rename the factory was still registered as window.slopsmithViz_<id>; the host viz picker only looks up window.feedBackViz_<id> (no slopsmithViz alias exists) and silently skipped this plugin. Add the feedBackViz_ global (aliased to the existing slopsmithViz_ one for back-compat). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(plugins): bundle drum_highway_3d + keys_highway_3d as in-tree core plugins Import both 3D highway plugins from their standalone repos via git subtree (history preserved), ahead of the visual-parity epic that ports the guitar highway's polish to them. - .gitignore: !plugins/drum_highway_3d/ + !plugins/keys_highway_3d/ exceptions - keys plugin.json: "bundled": true (drum already had it) - CI: JS test step gains 'plugins/*/tests/*.test.js' (+20 keys tests) - static/tailwind.min.css regenerated (core build scans plugins/**) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drum_highway_3d): narrow Auto-mode predicate so bundling can't steal guitar arrangements has_drum_tab is pack-level; first-match-wins Auto order sorts this plugin before highway_3d. Claim only drum arrangements, or packs nothing more specific can render (Codex preflight P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG — note the deliberate Auto-predicate narrowing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drum_highway_3d): MIDI lifecycle — no false misses on mid-song connect, promote survivor on destroy - _missSweepFloor exempts notes that passed before the device wired up from the miss sweep (lowered on seek-back; cleared with scoring resets) - destroy() promotes a surviving instance to _activeInstance so splitscreen panel teardown doesn't drop all MIDI routing (Codex preflight round 2, both pre-existing in the imported code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sin <deathlysin@outlook.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
77547af110 |
perf: allocation/scan hardening for weaker hardware
Static-analysis follow-ups to the trace-backed fixes; each is cheap insurance on machines where the profiled headroom doesn't exist. - highway.js: _makeBundle now mutates one persistent per-instance object instead of allocating a fresh ~35-field bundle every rAF frame (xN under splitscreen). Object identity is stable and meaningless; array fields still swap reference on chart changes, which field-identity caches rely on. Contract documented in both CLAUDE.mds. - highway.js: new bsearchTime (lower-bound on .time) windows the default 2D renderer's beat-line scan (was O(all beats) per frame); bundle.lowerBoundT / bundle.lowerBoundTime expose the searches to custom viz so they stop reimplementing visible-window culling. - highway_3d: localStorage 'h3d_full_sus' polled at ~1 Hz instead of every frame (synchronous storage read on the hot path). - highway_3d: drawLyrics caches the measureText row layout keyed on (lyrics ref, line index, shown count, font size, width) — per-frame work is now just drawing over cached widths. - tests/js: bundle source-shape assertions widened to accept the assignment form ([:=]) alongside the old object-literal form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5239665e2b |
perf(highway_3d): pre-warm shaders and label textures at init
Trace showed frame spikes from Three.js first-use costs mid-song: shader program compilation (getParameters/getProgramCacheKey) and lazy texture uploads (texSubImage2D) whenever a chord name, section banner, or fret label first appeared. - ren.compile(scene, cam) after initScene (pools already warmed by feedBack#226, board built, background mounted) so programs compile during the load spinner. - Pre-rasterise + GPU-upload (ren.initTexture) the deterministic txtMat entries: fret numbers 0-24 in the noteFret/fretRow/ghostFret combos the per-frame paths request. - Chart-dependent labels (chord template names, section names) prewarm once on the first draw() after each init, when bundle arrays are guaranteed populated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd840011d2 |
perf(core): stop per-frame layout thrash in visibility check + v3 chrome loop
Chrome trace showed ~0.5s self-time in _isHighwayVisible (offsetParent
read every rAF frame forces style/layout recalc) and ~1.5s in the v3
player-chrome loop (matches(':hover') per frame, unconditional
textContent/width writes at 6 Hz -> ~1800 layout passes in 63s).
- highway.js: sample offsetParent every 10th frame, cached in between;
fresh sample forced on init/canvas-replace/resize/override-clear.
- player-chrome.js: hover tracked via mouseenter/mouseleave; Up-Next
refs cached, text written only on change (eta coarsened to 1s steps
beyond 10s), progress bar moved from width to scaleX (compositor-only).
- v3.css: bar fill uses transform-origin:left + scaleX transition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
744c848636 |
v3 library: mastery filter + sort (needs-practice / most-mastered) (#687)
* feat(v3): sort the library by mastery (needs-practice / most-mastered)
Adds two sort options to the Songs library: "Needs practice first" (weakest
measured accuracy first) and "Most mastered first". Mastery = MAX(best_accuracy)
across a song's arrangements, from song_stats; because that's a separate table
it's a correlated subquery in the ORDER BY, so these sorts use OFFSET paging like
tuning/year. Unscored ("not started") songs always sort to the bottom in both
directions, so a large unpracticed library doesn't bury the songs you're
actually working on. Never the default.
Verified against a running server: scored songs at 0.90 / 0.30 plus unscored ->
ascending orders 0.30, 0.90, unscored; descending 0.90, 0.30, unscored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* feat(v3): add the 3-state mastery filter (Mastered / In progress / Not started)
Complements the mastery sort with the gentler filter the charrette preferred: a
"Progress" facet in the Filters drawer with Mastered (>= 0.9), In progress
(attempted but < 0.9), and Not started (no score) -- multi-select, OR within the
set. Server-side via a correlated subquery on song_stats threaded through
_build_where / query_page (passed as a separate kwarg so query_artists /
query_stats are unaffected). Smart-collection providers ignore it (they define
their own filters).
Verified live: mastered -> the 0.90 song, in_progress -> the 0.30 song,
not_started -> the unscored songs, combined -> both scored (with correct totals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
41cdfd576a |
feat(v3): play a playlist as a queue with auto-advance (#685)
Playing a playlist previously played one song and returned to the menu -- a play-queue was never implemented. Add window.feedBack.playQueue (start / advance / hasNext / clear) and a "Play all" button on the playlist detail. Advancing rides the same exit choke point as auto-exit and a results-card close: song-end paths call window.closeCurrentSong() (the auto-exit grace timer and a results screen's release()), so wrapping it plays the next track instead of returning to the menu -- advancing AFTER the user dismisses a score card, not through it. A user-initiated exit (Escape / the close button) uses the bareword closeCurrentSong(), left untouched, so leaving the player still leaves and abandons the queue. playSong gains a fromQueue guard (a manual play abandons a stale queue) and closeCurrentSong clears the queue on a real close. Binds via song:ended / the choke point, not the <audio> element, so it advances on the desktop (JUCE) route too. The no-queue path is unchanged. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
23df6767a7 |
feat(v3): Refresh button + live scan progress on the Songs library (#673)
Add a media-server-style "I dropped files in my folder, hit refresh" control to the Songs toolbar. Refresh triggers an incremental /api/rescan and reuses the existing /api/scan-status poll for live progress: a 3-state button (idle / "Scanning..." while listing / "Scanning N/M" once counting) with a title tooltip showing the current file + percent. A scan already running (the Settings buttons or a background pass) is reflected on the button too. On completion it emits library:changed so the grid reloads, and shows an honest, never-punishing fbNotify toast (bottom-right, suppressed while in a song). No backend change -- same machinery the Settings rescan already drives. The precise "N added" count is a follow-up. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f4b59e67d6 |
feat(v3): modern "Add to playlist" picker (replaces run-on prompt) (#672)
The old add-to-playlist flow crammed every playlist into one uiPrompt label
("1. Foo 2. Bar 3. Baz ...") and asked the user to type a number -- unreadable
past a couple of playlists, and reported as a bug.
Replace it with openPlaylistPicker: a checkbox modal with membership pre-check (a
song already in a playlist shows checked; a multi-song selection shows an
indeterminate box when only some are in), an inline "+ New playlist" row, and a
search box once the list is long. Toggling adds/removes via the existing REST
(POST + DELETE .../songs/{filename}); only playlists the user actually touched
change. One shared function still feeds the per-card menu, the batch bar, and the
batch button, so the fix lands in all three. Escape / backdrop close; a
bottom-right fbNotify toast confirms.
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
110b47f2ae |
feat(v3): persist last-used library sort / filter / view (#671)
The Songs library reset sort + filters to defaults on every visit; testers asked
it to remember their choice ("most players pick a preferred sort and leave it").
Persist sort, format, view, and the drawer filters to localStorage
('v3:songs-prefs'), restored once at first build. Cold start stays the neutral
Artist A-Z default. The search query and the artist/album drill-down are
deliberately NOT persisted (navigational). Single write point in reload() (where
every change already funnels); every restored value is validated against its
option list, so a removed/stale setting can never wedge the toolbar. Global (not
per-provider) for now.
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
96a84996a2 |
fix(tuner): mic-verify stamps the tuning it actually checked + on-device test plan (#684)
Working-tuning follow-ups: - Mic-verify used _state.currentSongOffsets for the 'verified' stamp, but for a MANUALLY-selected tuning (tuner opened off a song) that's a stale/different song's tuning — so verify could mark the WRONG tuning verified. It now derives the verified offsets from the tuning actually being checked (its target freqs; the player's reference pitch cancels in the ratio), so 'verified' always attaches to the tuning the player confirmed. Explicit offsets still win. - Adds docs/working-tuning-on-device-tests.md: the checklist for the parts that can't be covered headlessly — the auto-open/gate flow, both-directions prompts, mic-verify detection, and the tuner-mic-vs-note_detect ASIO/exclusive-mode contention flagged in the design charrette. Test: mic-verify with no explicit offsets / no song context derives the correct offsets (Drop-D). 55 tuner tests green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2910a3c8cb |
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> |
||
|
|
115c96a3f0 |
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> |
||
|
|
48f435408f |
feat(core): working-tuning lifecycle — launch default, verified decay, idempotent re-injection + tests (working-tuning PR 9a) (#669)
Hardens the host workingTuning capability (PR 1) with the "polish & safety"
lifecycle:
- Idempotent re-injection: a second load of the module no longer replaces the live
state with a fresh (empty) one — it early-returns once registered.
- Opt-in "launch tuning" default (setLaunchDefault/getLaunchDefault/clearLaunchDefault):
a per-instrument, localStorage-backed seed the player can opt into ("start me in
THIS tuning on app open"). Boot seeds from it when set, else /api/settings as before.
Off by default — a SEED only; the live tuning still resets on restart.
- Verified decay: on song:loading the current instrument's 'verified' provenance
decays to 'assumed' (offsets kept) — a per-string mic check is only trustworthy for
the context it was done in, so a stale 'verified' can never suppress a needed prompt.
Adds a state-machine smoke suite (tests/js/working_tuning_capability.test.js, 12/12):
defaults, per-instrument isolation, both-directions, verified-invalidation-on-retune,
decay-on-song-load, resetToDefault, launch-default set/seed/clear, idempotent
re-injection, the change event.
The opt-in UI + the mic-verify writer land with the tuner (PR 9b).
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
7173007412 |
feat(v3): show the live working tuning on the instrument card (working-tuning PR 5) (#667)
* feat(v3): show the live working tuning on the instrument card (working-tuning PR 5) The topbar instrument selector now surfaces the selected instrument's live working tuning from the host workingTuning capability (PR 1): a compact label on the card — dim while you're in your home tuning, amber once you've retuned — and, in the dropdown, a "Now in: <tuning> <assumed/verified glyph>" banner with a one-tap "Back to default" (resetToDefault). Switching instrument/strings calls setCurrentInstrument so the card follows the right instrument (guitar's tuning vs bass's, tracked separately); it re-renders on working-tuning-changed. Names offsets via the shared window.displayTuningName resolver. Fully feature-detected — without the workingTuning capability the card renders exactly as before. v3-only, single file (static/v3/badges.js). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(badges): consistent home-tuning check + gate working-context on save (PR #667 review) Review fixes for the instrument-card working-tuning label: - isHome used display-label equality where the home label was the raw settings string ('Custom') but the working label came from displayTuningName(offsets) — a real home tuning could show amber. Both now resolve through the same namer. - The instrument pill moved the working-tuning context (setCurrentInstrument) even when saveSettings() rejected the patch, desyncing the selector from the card. saveSettings() now returns whether it was accepted; the pill only switches the working context on success. (No boot-time setCurrentInstrument: the host already seeds its current instrument from /api/settings and emits working-tuning-changed on hydration — which this card re-renders on — so pre-touching would only race/suppress that seed.) 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> |
||
|
|
df4e17bc99 |
feat(v3): flag library songs by working-tuning match (working-tuning PR 6) (#668)
* feat(v3): flag library songs by working-tuning match (working-tuning PR 6) Each song's tuning chip in the v3 library grid is now coloured by whether your CURRENT working tuning covers it: green = play it now, amber = needs a retune (with a matching tooltip). Uses the tuner plugin's coverageReport (async), so it runs as a post-paint decoration pass — chips render instantly, then colour a tick later; a token cancels a superseded pass so scrolling stays snappy. Re-flags on working-tuning-changed (retune / instrument swap / reset), no re-fetch. Fully feature-detected: without the tuner coverage API + the host workingTuning state, the chips render exactly as before. v3-only, single file (static/v3/songs.js). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(tuner/library): correct bass matching + memoize player tuning (PR #668 review) Review fixes for working-tuning PR 6 (library tuning-match chips): - Bass songs were scored against the guitar tuning. The chip passed no arrangement to coverageReport, so isBassArrangement fell back to guitar — a 4-string bass drop-D read as guitar could FALSE-MATCH a drop-D guitar player (green). songCard now flags a bass-only song (every arrangement name matches /\bbass\b/) with data-tuning-bass, and decorateTuningChips passes arrangement 'Bass'/'Lead' so coverage uses the right base pitches. Mixed guitar+bass songs → guitar (the song-level tuning is the guitar one); least-wrong given one tuning per song. - Per-chip /api/settings fetch storm. coverageReport()→_playerTuning() fetched /api/settings once per visible chip per grid paint (~60). _playerTuning is now memoized (the player's tuning is song-independent) so all callers share one read; invalidated on instrument:changed / working-tuning-changed, with a 3s TTL so a settings write that doesn't emit an event still heals. A transient fetch failure is NOT cached (next read retries) — else one hiccup would freeze coverage. Tests: player tuning shared across songs (one fetch); transient-failure retry (fails without the fix). The prior #680 dedup test updated for the memoized behavior. 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> |
||
|
|
6aed8510d7 |
Tuner: passive "different tuning" badge cue naming the retune (issue E, stage 2.5/3) (#657)
* Tuner: passive "different tuning" badge cue that names the retune
Building on the coverage check: when you enter a song your current
instrument doesn't cover, the topbar tuner badge gets an amber ring + a
tooltip naming the change (e.g. "retune B->A", or "the reference pitch"
for an A440 vs A432 mismatch). Advisory only -- it never auto-opens the
panel; recomputed on song:ready, cleared on song-load / leaving the
player.
Refactors the coverage check into a structured report
(window._tunerAutoOpen.coverageReport -> { covered, retune:[{from,to}],
reference, cantCover }); the boolean gate now wraps it. The cue is
CSS-free (inline ring + native tooltip, no Tailwind rebuild) and no-ops
when the tuner plugin is absent.
Touches static/v3/badges.js (cue) + plugins/tuner/screen.js (report).
v3-only. Stacked on #656 (issue E stage 2.5/3). The splitscreen-suppress
and no-usable-input guards move to E2 (the playback gate).
Tests: tests/js/tuner_auto_open.test.js (report names the strings,
reference mismatch, badge wiring).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* feat(tuner): read/write the live per-instrument working tuning — both-directions retune prompt (working-tuning PR 3) (#660)
The §4 coverage check compared each song against the player's fixed
instrument-profile tuning, so the tuner only ever prompted *away* from a "home"
tuning (E -> Drop C#) and stayed silent coming back (Drop C# -> E), even though
the player had physically retuned.
_playerTuning() now reads the host's live per-instrument working tuning
(window.feedBack.workingTuning, keyed by the selected instrument from
/api/settings) instead of re-deriving from the static settings tuning, so
coverage is measured against what the instrument is ACTUALLY in and prompts both
directions. On clearing an auto-opened tuner, _publishWorkingTuning() writes that
song's tuning as the instrument's live working tuning ('assumed' — PR 4's
explicit "I tuned / Skip" refines the write-point), so the next song is judged
against where the player now is.
Per-instrument (guitar vs bass tracked separately). Feature-detected: falls back
to the static /api/settings tuning when the working-tuning capability is absent,
so the 27 existing coverage tests are unchanged. Builds on PR 1 (host
workingTuning) + PR 2 (instrument->chart routing).
Tests: tests/js/tuner_auto_open.test.js — +2 (both-directions coverage via a live
Drop-D working tuning; publish-on-clear targets the right instrument slot); 29
pass total.
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tuner): transactional open + fail-closed auto-open config (tuner-E #655 review) (#681)
Two review fixes for the auto-open opt-in+persist stage:
- enable() wasn't transactional. The panel (with the ×/Skip buttons) is shown
before `await _tunerAudio.start()`, and `_state.enabled` was only set after it.
A ×/Skip dismiss during that await hit disable() with wasEnabled=false, then
enable() completed and flipped enabled on — an enabled-but-hidden zombie. Guard
the open with an `_openGen` token bumped on every enable()/disable(); after the
audio-start await, bail if superseded instead of enabling. Closes #675.
- Config wasn't fail-closed. routes.py normalized the opt-in with
bool(data.get("autoOpenOnTuningChange", False)), so "false"/"0"/junk coerced to
True. Accept only a real JSON boolean. Closes #676.
Tests: tuner_auto_open.test.js (dismiss-mid-open stays disabled — fails without
the token guard), test_config.py (auto-open default-false + fail-closed on
non-bool). 34 JS + 24 config tests green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tuner): coverage stays conservative when the instrument is unknown (tuner-E #656 review) (#682)
_playerTuning() is documented as conservative ("missing data → not covered → still
prompt"), but when /api/settings carried no instrument identity (a fresh profile:
_default_settings() omits instrument/string_count/tuning) it invented guitar/6/440/
standard, so an unconfigured player was treated as 6-string E-standard and coverage
suppressed the auto-open (and badge cue) for matching songs. The post-#660 rewrite
only returned null when the whole fetch failed (!s), not when settings existed but
lacked an instrument.
Now return null unless there's a confident identity — any of instrument/string_count/
tuning in settings, or live working-tuning offsets. A configured standard guitar still
covers a standard song (no regression). Closes #677.
Tests: tuner_auto_open.test.js — empty-settings → not covered (fails without the fix);
configured standard guitar → still covered.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tuner): badge coverage cue staleness + unknown-as-warning + dedupe (tuner-E #657 review) (#683)
Three review fixes for the passive "different tuning" badge cue stage:
- Stale async cue (#678): _refreshCoverageCue awaited coverageReport then wrote the
DOM unconditionally, so a slow /api/settings fetch could restore the previous
song's amber ring after song:loading / leaving the player. Add a monotonic token
bumped on every refresh and both clear paths; apply the awaited report only if the
token still matches.
- "Unknown" rendered as "needs retune" (#679): the plugin returns a conservative
all-false report on a fetch hiccup; the cue painted that as an amber "retune the
reference pitch" ring. Collapse a no-signal report (not covered, no reference /
retune / cantCover) to null (no cue) via _meaningfulReport(). A genuine not-covered
report always carries reference / retune / cantCover, so real cues are preserved.
- Duplicate /api/settings fetch (#680): the auto-open gate and the badge cue both
call coverageReport() per song:ready. Cache the coverage promise per song (keyed by
session + tuning + centOffset) so they share one fetch; invalidate on song:loading,
instrument:changed, and working-tuning-changed so it can't go stale within a song.
Tests: tuner_auto_open.test.js — concurrent reports share one fetch, a new song
refetches (fails without the cache). 34 JS 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: Byron Gamatos <xasiklas@gmail.com>
|
||
|
|
6b0e37aa35 |
Tuner auto-open: instrument-coverage check (issue E, stage 2/3) (#656)
With the opt-in auto-open on, only prompt when the player's current physical tuning doesn't already cover the song. FeedBack is tune-to-song (the highway draws tab in the song's tuning), so the check aligns the song's open-string tuning string-for-string against the player's instrument: - An 8-string F# player gets no prompt for a 6-/7-string standard song (its top strings already match those tunings). - A song needing an open string the player lacks (e.g. a Drop-A 7-string's low A on an F# 8-string) still prompts. - A whole-instrument reference difference (A440 vs A432, or an octave-down centOffset, previously ignored) also prompts. Reads the player's instrument from core /api/settings (the v3 instrument selector, a stable physical reference); conservative fallback (prompt) when undeclared or unavailable, so a real retune is never silently skipped. v3-only. All in plugins/tuner/screen.js; no core changes. Stacked on #655. Follow-up E1.6: a passive badge cue that names the strings to retune, plus splitscreen / no-usable-input guards. Tests: tests/js/tuner_auto_open.test.js (covered/uncovered, the Drop-A case, reference mismatch, direct contiguous alignment). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6bfd92aa06 |
Fix tuner auto-open flash: opt-in + persist (issue E, stage 1/3) (#655)
The tuner self-closes on song:play; autoplay fires it right after a song switch, so an auto-opened tuner flashed shut ~1s later. An arrangement switch (which never arms autoplay) instead persisted — the opposite tester reports, and not the mic. - New opt-in setting autoOpenOnTuningChange (tuner Settings, default OFF) - An auto-opened tuner persists: it ignores the autoplay song:play, stray outside-clicks, and same-screen re-emits, closing only via the new in-panel x / Skip buttons or leaving the song. A manual open keeps the classic click-away / play-to-close behaviour. - Adds the panel's first in-box close (x + contextual Skip). - All in the tuner plugin; no core app.js changes. Default (opt-in vs opt-out) is teed up for Byron to flip one boolean. Staged follow-ups: E1.5 = instrument-coverage smart prompting + badge cue; E2 = holdAutoplay gate. Tests: tests/js/tuner_auto_open.test.js (opt-in gate, persist mode, play/click-proofing). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bc4d2a3592 |
Merge pull request #533 from got-feedback/feat/feedpak-jsonc
feat(core): read .jsonc data files (strip C-style comments) (feedpak-spec §8) |
||
|
|
7713ca92f4 |
Merge remote-tracking branch 'origin/main' into feat/feedpak-jsonc
Signed-off-by: topkoa <topkoa@gmail.com> # Conflicts: # CHANGELOG.md |
||
|
|
7e977b9572 |
Merge pull request #674 from got-feedback/feat/3d-wide-pane-tuner-per-panel
3D highway wide-pane tuner: dismiss + per-pane targeting |
||
|
|
095d718b85 |
Address review: Reset on All restores defaults verbatim
The Reset handler forced base.enabled = true after copying _ASPECT_DEFAULTS (where enabled is false) — a leftover from when enabled controlled panel visibility. Visibility is now independent (Shift+A / ×), so drop the override and let Reset restore the defaults exactly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
c4bb58233d |
feat(core): route the highway chart to the selected instrument's part (working-tuning PR 2) (#659)
When a song loads without an explicit arrangement, highway_ws now reads the player's selected `instrument` from config.json (the same file it already reads for the default-arrangement preference) and picks the arrangement that matches: bass -> the Bass part. Guitar — and any unknown/future instrument (drums, keys) — falls through to the existing preference/most-notes default, which already lands on a guitar part. Previously the instrument selector only fed the tuner, so a bass player was handed the default Lead/guitar chart, and the working-tuning coverage check then compared a 4-string bass against a 6-string part (always "can't cover"). This is the instrument->chart routing the working-tuning series leans on. Server-only (every launch path flows through the WS, so no client change). An explicit arrangement request always wins, so only the default part chosen on load changes. Tests: tests/test_highway_ws_instrument_routing.py (bass->Bass, guitar->default, explicit-wins) — 3 new, existing highway WS tests still green. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1434eb6342 |
Address review: only register panes while the tuner is open
camUpdate registered every pane each frame regardless of whether the tuner had ever been opened, so window.__h3dAspectPanes could grow unbounded (prune runs only while the panel is open) and it ran even for users who never opt in. Gate _aspectRegisterPane behind __h3dAspectPanelOpen (same gate as the readout). The pane key is still resolved every frame so saved overrides keep applying; only the picker bookkeeping is deferred until the panel is open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
24d24ef2cf |
Address review: resolve cache, Date.now fallback, prune-on-open, rename
- Memoize _resolveTuneFor per pane, invalidated by a revision bumped on every tune mutation (all writes funnel through _aspectPersist). Panes with an override no longer rebuild the merged object every frame; panes without one still return the base directly. - _aspectNowMs falls back to Date.now() when the Performance API is absent, so pane/readout pruning still works in older/borrowed contexts. - _setAspectPanelVisible prunes stale panes before the first dropdown build, so panes from a prior song/split don't flash until the first RAF tick. - Rename _abShortcutRegistered/_registerAspectAbShortcut to _tunerShortcutRegistered/_registerTunerShortcut — the shortcut opens/closes the tuner now, it isn't an A/B toggle. - Fix a stale 'pane1' example in a comment (keys are 'arr:<name>'/'pane:<uid>'). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
58047e6ad6 |
Address review: force target to All when the pane picker is hidden
When only one pane is live the Target row is hidden, but _aspectEditTarget could remain a specific pane key — silently routing edits into a hidden (and persistent arr:*) override in single-player. Reset the edit target to "" in _aspectBuildTargets whenever the row is hidden (or the selected pane is gone), so single-pane edits always go to the shared base. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
817db6382b |
Address review: explicit button types + Target select label
- Set type="button" on the × close control and the Reset/Copy buttons so they can never act as submit if the panel is ever nested in a <form>. - Add aria-label="Target pane" to the Target <select> so screen readers can identify the control. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
9f914770c6 |
Address review: sparse overrides, hfov clear, readout prune
Three fixes from PR review of the per-pane tuner: - Sync no longer writes back. _syncAspectPanel dispatches synthetic input events to refresh slider labels; guard those with _aspectSyncing so the slider handler skips the write. Previously opening/switching a target populated a full override for every field (defeating sparse inherit) and spammed localStorage. - Unchecking "Override held hFOV" on a pane target now clears the override key (via _aspectClearVal) so the pane re-inherits the base value, instead of pinning hfovDeg:null in the override. On the base target it still sets the explicit auto (null). - _aspectPrunePanes now prunes the matching __h3dAspectReadout slot and drops a dangling __last, so the readout cache can't grow unbounded as songs and arrangements churn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
5ef163e9f9 |
Key wide-pane overrides by arrangement, not the split panel index
The Target picker disappeared in split because it keyed panes off the
external splitscreen panel index (panelIndexFor), which isn't always
available — both panes then collapsed to a single 'main' key and the
one-pane row-hide kicked in.
Key panes by arrangement name instead ('arr:Bass'): distinct between split
panes AND stable across songs, with no dependency on the split plugin. A
per-instance id ('pane:N') is the fallback when a pane has no arrangement.
Only arr:* overrides persist to localStorage (instance-id fallback keys are
session-only, so they can't leak a new key each reload). This also gives
nicer semantics — a pane's framing follows its arrangement into the next
song.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
|
||
|
|
64b95d6f34 |
Persist per-pane framing across songs via durable slot keys
Per-pane overrides were keyed by an ephemeral per-instance id, so leaving a
song and opening another rebuilt the renderer with a new id and the pane's
framing was lost.
Key overrides by the durable split slot again ('main' | 'panel<idx>', via
_bgPanelKey) so the same slot means the same pane across songs, and persist
__panels to localStorage. Keep the anti-flicker fixes that were the actual
cause of the earlier dropdown churn (prune stale panes, rebuild only on a
pane-set change, never rebuild while the select is focused). The slot key is
latched to the last real slot so a transient null from panelIndexFor during
a song/layout transition can't flip it to 'main' and drop the override for a
frame; it resets in destroy() for instance reuse in another slot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
|
||
|
|
491039a12d |
feat(core): host per-instrument workingTuning capability + read-API/event (working-tuning PR 1) (#658)
* feat(core): host per-instrument workingTuning capability + read-API/event (working-tuning PR 1)
Introduce window.feedBack.workingTuning — the live, host-authoritative current
instrument tuning (offsets + string-count + reference pitch + assumed/verified
provenance), distinct from any one song's tuning and from a soft opt-in default.
It's the single source of truth the highway, library, and plugins (tuner,
Virtuoso, minigames) will read so a retune or instrument swap is reflected
app-wide instead of being re-derived per surface.
PER-INSTRUMENT: state is a map keyed by `${instrument}-${stringCount}` (e.g.
guitar-6 / bass-4, the selector's key) — your guitar's tuning and your bass's are
kept separately; get() returns the selected instrument's, and switching the
selector surfaces that instrument's own remembered tuning. You only ever deal
with the one you've picked.
Modeled on the shipped `tuning` capability + the `feedBack.theme` read-API:
synchronous get(instrument?), set(state,{provenance,instrument}) mutator,
setCurrentInstrument(), resetToDefault(), and a `working-tuning-changed` event
that fires on change and once on hydration (carrying which instrument changed).
In-memory, seeded from /api/settings, reset-on-restart. Registered as a separate
`working-tuning` exclusive-owner capability (tuner = sole writer, others read).
Foundation only — pure plumbing, nothing writes to it yet and no behavior
changes. The tuner becomes the writer (and the gate's E->C# asymmetry is fixed)
in a later PR.
Frontend-only: new static/capabilities/working-tuning.js, loaded from
static/index.html + static/v3/index.html. Per-instrument state machine verified
by a stubbed node harness (separate guitar/bass slots, selector switch, isolated
writes, verified stamp, reset, defensive copies, capability registration).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* fix(working-tuning): resolve review P1/P2s + add behavioral test harness
Addresses the manual + Codex review of PR 1 (working-tuning foundation).
P1 — named tunings were dropped by the boot seed: /api/settings.tuning may be a
name ("Drop D") OR an offsets list, but the seed only handled the list and stored
offsets:null for names. The seed now resolves a name to per-string semitone offsets
via /api/tunings (ratio vs Standard; reference pitch cancels).
P1 — async seed could clobber state a consumer had already written: _seedFromSettings
resolves after boot and used to overwrite _currentKey/_byInstrument unconditionally.
It now bails when state was already _touched (and re-checks after the /api/tunings
leg), so an explicit set()/setCurrentInstrument()/resetToDefault() before hydration
wins. Hydration still fires.
P1/P2 — shallow copy leaked live nested arrays: get() and set() now clone offsets and
verifiedStrings on both ingress and egress, honouring the "readers can't mutate live
state" contract.
P2 — provenance/verification state machine made coherent by construction:
verified <=> verifiedStrings is an array AND verifiedAt is a finite number. A tuning
change invalidates prior verification unless a fresh bundle is supplied; a "verified"
claim with no strings or a null/absent timestamp is repaired (assumed / stamped now).
P2 — bare-instrument writes targeted a hard-coded default string count: _keyOfResolved()
resolves an omitted string count against the current selection (same instrument), so
set({instrument:'bass'}) / set({stringCount:5}) hit the selected bass-5, not bass-4.
Test — adds tests/js/working_tuning.test.js (the harness the PR described but did not
commit): 11 behavioral cases over a stubbed window — registration, per-instrument
isolation + selector switch, defensive copies, the verification invariant, bare-key
routing, named + offsets-list seeding, and the boot-race guard. Full tests/js suite:
no new failures (the 12 pre-existing branch failures are unrelated).
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>
|
||
|
|
6c42d83c31 |
Fix flickering / wrong panes in the wide-pane Target picker
The Target dropdown keyed panes off feedBackSplitscreen.panelIndexFor, which can return the focused index for any canvas — so both split panes' keys ping-ponged, rebuilding the <select> every frame (flicker) and listing wrong/duplicate entries. The registry also never dropped panes from a prior song or a closed split. - Key each pane by a stable per-renderer-instance id (_paneUid, assigned once in init) instead of the split panel index. - Prune panes not reported within ~1.5s (song change / split teardown). - Mark the dropdown dirty only when the pane SET changes, not on every per-frame re-report, and skip rebuilding while the <select> is focused. - Hide the Target row entirely when there's a single pane. - Label panes by arrangement name, falling back to "Pane N". Per-pane overrides are now session-only (keyed by ephemeral instance ids), so they're no longer persisted to localStorage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
9f0d48cb1f |
3D highway wide-pane tuner: dismiss + per-pane targeting
Two usability gaps in the wide-pane framing tuner: - No way to dismiss the panel. Add a × close button to the header and make the Shift+A shortcut open/close the panel (reveal/dismiss). The A/B enabled toggle now lives as a checkbox in the panel, so closing the panel no longer changes the framing state. - Edits hit every split pane at once. Add a Target selector (All panes, or a specific pane labelled by its arrangement, e.g. "Panel 1 — Rhythm"). Per- pane edits write a sparse override map (__panels[key]); each renderer resolves the shared base with its own pane's overrides laid on top via _resolveTuneFor(paneKey), so one pane can be framed independently. Reset on a pane clears its override (re-inherits the base); Copy exports the resolved values for the selected target. The live readout is keyed per pane. Panes are discovered from the existing per-panel key (_bgPanelKey / feedBackSplitscreen.panelIndexFor) and self-register each frame for the picker. Overrides persist to localStorage alongside the base. Tests extended in tests/js/highway_3d_wide_fov.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> |
||
|
|
f9607c5c94 |
Port the Min res (minimum auto-resolution) selector into the v3 player (#663)
The v2 control bar exposes a "Min res" selector next to Quality that sets highway.setMinRenderScale — capping how far the load-adaptive resolution scaler (feedBack#654) may downscale, or disabling it entirely (Full). The v3 UI only ported the Quality selector, so v3 users had no way to stop the highway auto-downscaling to as low as quarter-res on heavy scenes / weak- GPU launches — pixelated even at Quality = HD, with no workaround (worse than v2). Add the Min res row to the v3 viz/quality rail popover, under Quality, mirroring the v2 control (same options, handler, title, aria). The handler, the setMinRenderScale/getMinRenderScale API, and the shared app.js init that syncs the selector's value (guarded by element id) all already exist — only the v3 markup was missing. Fixes #662 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3e3a98a0d0 |
Aspect-aware framing for ultra-wide 3D highway panes (#652)
* Add aspect-aware framing for ultra-wide highway panes On a top/bottom 2-player split each 3D highway pane is full-width / half-height (~32:9). The camera's vertical FOV was locked at a single value, so at that aspect the horizontal cone ballooned past 130deg and squeezed the fixed-width neck into a thin central sliver with large dead margins on either side. Add a "horizontal-FOV-hold" path: past a configurable start aspect the effective vertical FOV is lowered so the horizontal cone stays roughly constant, letting the neck fill a wide pane. At/under the start aspect it is an exact no-op, so normal ~16:9 single-player and most 2x2 panes are unchanged. Optional pose nudges (height / dolly / pitch / look-depth) further flatten the view toward a low, immersive angle. Everything is driven by a runtime bridge (window.__h3dAspectTune) with a live tuner panel (Shift+A in the player) exposing every knob plus a live aspect/FOV readout, localStorage persistence, and a Copy button. Toggling the feature off restores the exact prior framing, so it doubles as an A/B control. Shipped on by default for wide panes for testing feedback. Source-pinned by tests/js/highway_3d_wide_fov.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> * fix(highway_3d): ship wide-pane framing default-OFF with a coherent config Review fixes for the aspect-aware framing. The first cut shipped _ASPECT_DEFAULTS = { enabled:true, baseVfov:30, blend:0, minVfovDeg:36 }, which contradicted the PR's own "default off → byte-for-byte prior behaviour" claim: - enabled:true made the tune active for everyone, and baseVfov:30 forced every pane's vertical fov from 70° to 30° (normal single-player/2x2 panes included — a drastic global zoom, not the advertised no-op). - blend:0 collapsed the Hor+ math back to base, so the actual horizontal-FOV- hold did nothing even on wide panes — the only net effect was the zoom. - minVfovDeg:36 > baseVfov:30 was an inverted floor (clamped wide panes UP to 36° rather than flooring a real reduction). New defaults: { enabled:false, baseVfov:BASE_VFOV(70), blend:1, minVfovDeg:HORPLUS_MIN_VFOV(28) }. Now: - OFF by default → camUpdate passes a null tune → effectiveVfov returns BASE_VFOV → exact no-op on every pane (verified: 70° at 16:9 and 32:9). - When a tester enables it (Shift+A), baseVfov==BASE_VFOV keeps normal/≤start panes at 70° (still a no-op there) and blend:1 makes the hold actually engage on genuinely wide panes (47.7° at 32:9, flooring toward 28° as aspect grows). - minVfovDeg < baseVfov is a real floor. Also bumps the localStorage key (h3d_aspect_tune → h3d_aspect_tune2) so a machine that persisted the old broken default gets the corrected one, and adds source-pin tests guarding default-off + the coherent base/blend/floor so this can't silently regress to default-on again. The pose-nudge values are left as the author's in-progress wide-pane look (dormant until enabled). 110/110 tests pass; node --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: topkoa <topkoa@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
db81d7dafb |
Add progress bar to v3 "Up Next" pill (#649)
The persistent top-right "Up Next" pill showed the upcoming section name
and a countdown ("in 12.3s") but no at-a-glance sense of how far through
the current section the song is. Add a thin progress bar directly under
the existing text that fills as the current section elapses toward the
next, reaching full when the section flips.
The text row is wrapped unchanged in a flex row and the pill stacks the
bar beneath it; nothing else about the pill's content or styling changes.
Progress is computed in updateUpNext() as the fraction elapsed between the
previous section boundary (last section at/before now, else song start)
and the next section. The fill uses the same gradient as the section name
for visual cohesion.
Signed-off-by: topkoa <topkoa@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a732e1f9d2 |
perf(tuner): idle the always-on tuner viz rAF when there's no signal (#647)
The v3 home tuner card runs continuously, and every tuner visualization drove a self-rescheduling 60fps requestAnimationFrame loop that never stopped — pinning a renderer core even on a silent home screen with the needle/strobe at rest. Make each viz idle its loop once there's nothing left to animate, and re-kick it from update() on the next reading that actually moves it: - analogue-gauge: stop when the needle + drum strip have settled on their targets (|target-current| below a sub-visible epsilon); restart when a new reading moves the target. - strobe / mace-fx-iii / chef-mt3: stop when there's no live signal and the strobe drift (and glow fade) have fully decayed; restart on the next note. - toilet-tuner: stop when silent and the plunger has eased back to centre; restart on the next reading (guarded so repeated no-signal updates don't re-kick a parked loop). Active tuning is unchanged — the loop runs whenever a note is sounding or the indicator is still moving. Bumps tuner 1.3.1 -> 1.3.2. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dfa825b4ab |
docs: host theme contract proposal (#645)
* docs: host theme contract proposal (prevent features carving into one theme) Charrette output after a plugin UI feature (note_detect results card) was built against only the default skin and broke on the others — the colours adapted via tokens but the visual *devices* (glow ring, gradient) did not, because themes are design languages, not palettes, and nothing governs whether a theme does glow. Proposes a host theme contract: always-present semantic role tokens (incl. the missing on-accent + focus-ring), intent-named capability recipe slots where "off" is legal (an EMPHASIS recipe + an ACCENT-TEXT recipe), a window.feedBack.theme read/capability API + theme:changed event, a derive-surfaces-from-host reconciliation rule, accessibility baked in, and a skin-matrix verification gate. All additive + feature-detected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * docs(host-theme-contract): make the proposal normative (review fixes) Address the review of #645 (manual + Codex) — the doc was a sound design sketch but not yet the precise *contract* it claims to be: - P1 Token contract pinned: one public namespace `--fb-*` written on :root by a host-owned contract stylesheet (present themed-or-not); `--fbv-*` explicitly demoted to internal Tailwind-override plumbing (plugins must not read it). Added a normative role table + value grammar (colour roles = `r g b` triplets consumed via `rgb(var(--fb-x))`; recipe slots = full CSS device values). The §5 example now uses `--fb-*` throughout (was bare `--accent`/`--emph-*`). - P1 Invisible-text bug removed from the spec: `--fb-acc-text-fill` is the one slot where `none` is ILLEGAL (always a real paint, defaulting to the solid accent); the example feature-detects `background-clip: text` and keeps a solid `color` base, so the accuracy number can never render transparent — honouring the DoD "a device stays legible when its slot resolves to none". - P1 capabilities() booleans removed: they contradicted "never branch on glowy?" and were too lossy for canvas. The JS API is now CSS/DOM-forbidden and exposes RESOLVED token values (`get().tokens`) for canvas/WebGL renderers only. - P1 Physical home decided: a static `theme-contract.css` (outside the prebuilt Tailwind artifact, so no tailwind-fresh CI churn) holds the :root `--fb-*` defaults + the single focus-visible + reduced-motion rules; existing v3.css focus/motion rules are a tracked reconciliation, not day-one magic. - P2 Full on-fill family (`--fb-on-accent/-good/-warn/-bad`) + good/warn/bad ↔ existing good/mid/low mapping; `theme:changed` lifecycle pinned (get() sync + valid pre-apply, event after commit + once on hydration, plugins read on mount); same-document-light-DOM scope + shadow/iframe bridge stated; prefersReducedMotion() named the single JS motion gate. Resolved open questions folded into the body; the two genuine ones (skins-as- host-themes, component-recipe bundles) remain. 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> |
||
|
|
3120ae3e71 |
fix(highway_3d): dolly back so the fret-number row can't clip off the bottom (#633)
The heat-coloured fret-number row is drawn as a band BELOW the board (sY(lowest) - S_GAP*1.4), but camUpdate's self-correcting framing only anchors the board CENTRE to the lower third of the screen and reserves no headroom for that row. So a tight zoom on a centred active span (worst mid-neck; fine at either end of the neck) pushes the numbers past the bottom edge -- which is why testers saw it "only when centered" and "not every song." Tilt can't fix it (it would only trade a bottom clip for a top clip); the vertical-extent problem at tight zoom needs camera distance. Add a fret-row fit guard: project the row band with the final camera and, when it falls below FRET_ROW_FIT_NDC_MIN, raise a capped, hysteretic _fretRowFitBoost applied to the curDist lerp target (the span-driven tgtDist still owns zooming IN). The boost rises promptly (proportional to the deficit), relaxes lazily past a deadband, and is capped at FRET_ROW_FIT_BOOST_MAX (+60%) so the zoom can't pop or hunt. It cooperates with the tilt loop (pull-back shrinks the scene, tilt keeps the centre anchored) and yields entirely to the Camera Director free-cam. Surgical: passages where the row is already visible never trigger it, so framing is unchanged everywhere it already worked. plugin.json 3.30.0 -> 3.30.2 (screen.js cache-buster; 3.30.1 is taken by the FPS-counter PR). Tests: tests/js/highway_3d_camera_framing.test.js (guard constants, the boosted curDist lerp, the projected-row hysteresis, free-cam yield). Fixes #632 Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
199550e5fb |
fix(v3): dismiss Section Practice popover when another player popover opens (#638)
* fix(v3): dismiss Section Practice popover when another player popover opens The Section Practice popover (Songs > Song > Practice pill) stayed open when the user then clicked a v3 player-rail icon (Plugins, Audio, …), leaving two popovers stacked on top of each other. Reported on 0.3.0 (macOS) and still reproducing in the 2026-06-28 build. Root cause: the popover's outside-click dismiss was bound in the bubbling phase, but the v3 rail's icon buttons call e.stopPropagation() in their click handler (player-chrome.js wireRail), which kills bubbling before the click reaches document. So the dismiss listener never fired for a rail-icon click and the popover was orphaned open. Fix: bind the outside-click dismiss in the capture phase, which runs before the target's handler so stopPropagation() can't swallow it. This mirrors the audio mixer popover (audio-mixer.js), which already dismisses outside-clicks via capture-phase listeners for exactly this reason. Esc handling stays in the bubble phase (no rail handler stops keydown propagation, and capturing it would reorder it ahead of the player's Escape-to-exit handling). Shared app.js code, so v2 is covered too; v2 has no stopPropagation rail, so its outside-click dismiss behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * chore(#638): add CHANGELOG entry + capture-phase regression test Review follow-ups for the Section Practice popover dismiss fix: - CHANGELOG [Unreleased] → Fixed entry (repo workflow requires one). - tests/js/section_practice_dismiss.test.js pins the fix: the outside-click dismiss binds in the CAPTURE phase (so a rail icon's stopPropagation can't swallow it), exactly one capture binding (Escape keydown stays bubble-phase), and the #section-practice-control containment guard (no self-close). A revert to bubble-phase fails the test. 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> |
||
|
|
8fbbc761fc |
fix(v3): reject accidental text-selection of UI chrome (user-select policy) (#637)
* fix(v3): reject accidental text-selection of UI chrome (user-select policy)
Dragging/double-clicking across the v3 UI marquee-highlighted buttons, labels,
the sidebar, transport, and the note-highway HUD — looks broken (reported Mac +
Windows). Default the v3 shell to user-select:none on html, then opt CONTENT
back in. Decided by a 4-lens panel (UX / a11y / dev-ops / plugin-ecosystem);
their guardrails are baked in:
- Form fields ALWAYS re-enabled (input/textarea/select/[contenteditable]) so the
caret + IME composition never break. No `* { user-select:none }` (WebKit input
bug 82692).
- Plugin screens (.screen[id^="plugin-"]) stay selectable BY INHERITANCE (no `*`,
so a plugin's own non-select chrome still wins) — a plugin's copyable text
(lyrics, chords, results), including community/out-of-tree plugins that never
adopt the class, isn't silently locked.
- Core read-only content opts back in by CONTAINER via a hand-authored
`.fb-selectable` (not a Tailwind utility — so runtime-installed plugins get it
too): the whole Settings panel (paths, device names, version, diagnostics,
About) and the now-playing song metadata. Answers the open "keep settings
copyable?" question: yes, at the container.
Cosmetic only — never used to lock copy-worthy text (errors/IDs/paths/versions/
metadata stay selectable; WCAG 2.2 allows copy-paste as a mechanism). v3-only
(v2 unchanged; v3.css loads only on /v3); plain CSS, no Tailwind rebuild; no
desktop/Electron changes (standard OS-framed window). `.fb-selectable` is
documented in CLAUDE.md for plugin authors.
Tests: tests/js/v3_user_select_policy.test.js (html default, form-field
re-enable, plugin-screen carve without `*`, .fb-selectable, container opt-ins,
and the no-`*`-rule guardrail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* fix(v3): address review of the user-select policy (#637)
Review (manual + Codex) of the v3 text-selection policy:
- P1 (real bug): the now-playing HUD metadata opted into `.fb-selectable` but
its `#player-hud` parent is `pointer-events: none`, so the mouse could never
reach the text to select it — the opt-in was inert. Add `pointer-events-auto`
to the metadata block (verified in-browser: user-select:text + pointer-
events:auto, while the HUD parent stays pointer-events:none).
- Coverage: the PR's a11y guardrail promised copyable text stays selectable
"incl. in modals/toasts", but only Settings + the HUD were opted in. Blanket-
opt the focused copyable surfaces back in by selector — `.feedBack-modal`,
`[role="dialog"]`, `#fb-notify-stack`, `#v3-fb-toast`, `#scan-banner` — so
errors / IDs / paths / file names in dialogs, toasts, and the scan banner stay
copyable. These are focused panels, not dense card lists, so re-enabling
selection there can't recreate the across-cards marquee mess.
(Deliberately NOT opting in the library grid / dashboard / profile card lists:
making dense card text selectable would reintroduce exactly that marquee mess
on a drag — copy song metadata from the now-playing HUD / Settings instead.)
- Test (P3): assert the selectable rule's selectors order-independently, cover
the new modal/toast/banner surfaces, and check the HUD block carries BOTH
fb-selectable and pointer-events-auto (class-order independent).
Verified in a real browser (chromium): html=none, sidebar chrome=none, input=
text, Settings=text, HUD meta=text+pointer-events:auto, dialog/modal=text.
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>
|
||
|
|
a791a0d8fe |
feat(v3): DOM-virtualize the Songs grid (#636 item 3 stage 2) (#643)
The v3 Songs grid appended every scrolled page and never released nodes, so card-node count grew unbounded with scroll depth (24 → 624 → 2001 for a 2000-song library). Replace it with a windowed/recycled render: only the visible window (± overscan) is in the DOM while a #v3-songs-gridsizer element sized to ceil(total/cols)*rowH gives the scrollbar full-library geometry; #v3-songs-grid is absolutely positioned to the first visible row. - state.songs is a sparse, absolutely-indexed store filled a page at a time by ensureWindow(): the stage-1 keyset cursor for contiguous forward scroll (O(page)), OFFSET page= for jumps/restore/non-keyset providers. _loadPage shares an in-flight promise per page and an epoch guard discards a stale fetch that lands after a reset. - A–Z rail seeks directly via sort_letters cumulative counts (O(1), no page-through); bounded scan fallback for legacy providers without it. - Snapshot/restore is now scrollTop-based (geometry is stable). Select mode, accuracy badges, ⋮ menu, plugin card actions, and tree/folder coexistence survive cards recycling; renderWindow re-renders when select mode toggles. - Plugins get window.v3Songs.visibleCards() + a v3:library-window-rendered event instead of assuming all cards are present (highway-stutter lesson). Verified in a browser against a seeded 2001-song library: DOM bounded to ~60 nodes while the count reads "2001 songs", rail jump lands on the target row, selection survives recycling, scroll-restore exact. Codex-reviewed (3 findings fixed: stale-fetch epoch guard, await-in-flight page promise, select-mode resync on cached re-entry). Frontend-only. Tests: tests/browser/v3-grid-virtualization.spec.ts pins the bounded-DOM invariant + direct rail jump; tests/js/v3_az_rail.test.js and v3_songs_scroll.test.js updated to the new wiring. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |