Reported by a 6-string bassist: a Sleep Token chart tuned A0 D1 G1 C2 F2
A#2 (standard 6-string bass, whole step down) imported and displayed as
"6 string D Standard". They called it A standard and they were right.
Root cause: `tuning_name()` gated its naming ladder on
`len(offsets) == 6`, treating six offsets as proof of a 6-string GUITAR.
A 6-string BASS also has six offsets, but its lowest string is B, not E
— so the guitar ladder mislabels the whole family: an all-zeros bass
read "E Standard" (it is Standard/B) and a whole-step-down bass read
"D Standard" (it is A Standard). The function's own comment warned about
exactly this error for 7-string guitars; nobody guarded the bass axis.
The stored value feeds the library's Tuning filter, so every 5/6-string
bass song in every library was filed under a guitar name.
- `tuning_name(offsets, *, is_bass=False)`: bass 5/6 use the low-B
ladder (Standard / Bb / A / G# / G) and bass 4 keeps the E ladder it
shares with guitar. Drop names come off the resulting low string.
Default stays guitar, so existing callers are unaffected.
- `sloppak._tuning_for_meta_kind()` reports WHICH kind supplied the
tuning; `extract_meta` emits `tuning_is_bass` and scan_worker passes
it through. Guitar-first selection for the library index is unchanged
— a pack with a guitar part still indexes by the guitar.
- `TUNING_PRESET_MIDIS` bass-5/bass-6 renamed to match, with
`TUNING_PRESET_ALIASES` so `_valid_tuning_for_key` MIGRATES a saved
profile carrying an old name instead of rejecting it (it refuses names
belonging to another key's built-ins, and "D Standard" still exists
for guitar-6/bass-4 — so a rename alone would have invalidated those
profiles). Pitches are untouched; only labels change.
Convention confirmed by the bass- and guitar-pedagogy seats: name
extended range off the ACTUAL lowest string, which is what the 7-string
guitar presets already do. The old names came from the band-level habit
of saying "we're in D standard" — true of the guitars, while the bassist
in that band is in A standard. Right answer, wrong scope. The bass table
was also internally inconsistent: its Drop A was already named off the
low string while its standards were not.
Tests: bass ladders for 4/5/6 strings, the reported chart pinned both
ways (is_bass=True -> "A Standard", same offsets as guitar -> "D
Standard"), bass drop naming, a table-wide invariant that every bass
preset name matches the note its low string sounds, and alias migration
(incl. not leaking into guitar-6/bass-4). The existing legacy-flat-bass
migration test now asserts the corrected label — same pitches, right
name. Suite: 1735 passed vs 1720 on main, with the same 99 pre-existing
env failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01929LgKdJMyPGLf8N1WpEVW
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Follow-ups to #829 (CodeRabbit's review nit + the consumer adoption the PR
body promised):
- freqs_to_midis: reject non-finite frequencies (NaN/Infinity) — a provider
handing one through would otherwise raise inside int(round(...)) and 500
GET /api/tunings. Tests cover nan/inf/-inf alongside the existing garbage
cases.
- v3 instrument badge: TUNING_NOTE now prefers the exact integer midis the
server serves (tuningMidis) over reconstructing the note from the lowest
string's frequency via log2 against a hardcoded 440 — which can land a
semitone off at non-440 reference pitches. Frequency path kept as the
fallback for older cached responses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS2YFb6UUSwJVV6CmEa25i
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
The tunings catalog is served as frequencies scaled to the reference pitch,
so every consumer that needs note identities (the v3 instrument badge's
TUNING_NOTE, plugins converging on the host profile model) reconstructs MIDI
numbers client-side via log2 — a rounding footgun at non-440 references, and
N copies of code the host can run once.
Add `tuningMidis` to the response: the same catalog keyed instrument-count →
name → absolute open-string MIDI notes (low → high). Built-ins come straight
from TUNING_PRESET_MIDIS (no float round-trip at all); provider-contributed
entries are inverted from their frequencies at the served reference via the
new freqs_to_midis() (the inverse of open_midis_to_freqs, garbage-guarded).
Purely additive — referencePitch/tunings are unchanged.
Tests: every built-in round-trips at 440; round-trip holds at 430/432/444/450
(the exact case client-side reconstruction drifts on); garbage rejected.
Claude-Session: https://claude.ai/code/session_01MS2YFb6UUSwJVV6CmEa25i
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* settings: add host instrument profiles
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* settings: add instrument pathway selection
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* fix(settings): profile-aware saves/resets/switch, provider tunings, bass-5
Five regressions from the instrument-profiles rework:
1. save_settings canonicalized profiles on EVERY save -> empty/unrelated POST
froze default profiles into config.json (broke
test_empty_post_preserves_all_existing_keys). Gate on the save touching
instrument settings; GET already virtualizes profiles.
2. pathway is profile-mirrored, so the Gameplay reset (flat-key delete) was a
no-op. reset_settings now resets pathway inside the persisted profiles too.
3. Per-profile tuning validation rejected provider/custom tunings (tuner
plugin, /api/tunings). _valid_tuning_for_key now accepts a name unknown to
every built-in table while still rejecting a built-in misapplied to the
wrong key.
4. First-migration overwrote an explicit active_instrument_profile with the
legacy-inferred one, so a fresh-config switch to 'bass' was lost. Use
setdefault so an explicit request wins.
5. Pre-existing test_instrument_fields_persist used bass-5 + 'Drop D' (a
4-string tuning). Updated to the valid 'Drop A'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(settings): partial-merge instrument_profiles; clamp tuning on string-count switch
Two partial-update follow-ups:
- save_settings normalized a POSTed instrument_profiles by FILLING every omitted
profile with defaults and replacing wholesale, so a one-profile update reset
the others. Validate each PROVIDED profile individually and merge the partial
over the persisted set inside the lock — /api/settings is partial-merge.
- the string-count picker posted only string_count, so the backend silently
reset a now-invalid tuning to Standard while the UI kept the old value
(settings/tuner desync). Clamp + post the valid tuning too, mirroring the
instrument-switch path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>