mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-15 05:07:15 +00:00
Serve exact MIDI notes from GET /api/tunings (tuningMidis) (#829)
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1c1a0e0268
commit
751209b80e
@@ -98,6 +98,22 @@ def open_midis_to_freqs(midis: list[int], reference_pitch: float = DEFAULT_REFER
|
|||||||
return [round(midi_to_freq(m, reference_pitch), 2) for m in midis]
|
return [round(midi_to_freq(m, reference_pitch), 2) for m in midis]
|
||||||
|
|
||||||
|
|
||||||
|
def freqs_to_midis(freqs: list[float], reference_pitch: float = DEFAULT_REFERENCE_PITCH) -> list[int] | None:
|
||||||
|
"""Return absolute open-string MIDI notes for frequencies at the supplied
|
||||||
|
A4 reference — the inverse of open_midis_to_freqs. None if any entry is
|
||||||
|
non-numeric or non-positive (a provider could hand us anything)."""
|
||||||
|
out: list[int] = []
|
||||||
|
for f in freqs:
|
||||||
|
try:
|
||||||
|
f = float(f)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if f <= 0:
|
||||||
|
return None
|
||||||
|
out.append(int(round(69 + 12 * math.log2(f / reference_pitch))))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def tuning_offsets_from_midis(instrument_key: str, midis: list[int]) -> list[int] | None:
|
def tuning_offsets_from_midis(instrument_key: str, midis: list[int]) -> list[int] | None:
|
||||||
"""Return semitone offsets from the instrument's standard open strings."""
|
"""Return semitone offsets from the instrument's standard open strings."""
|
||||||
standard = STANDARD_OPEN_MIDIS.get(instrument_key)
|
standard = STANDARD_OPEN_MIDIS.get(instrument_key)
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ from song import (
|
|||||||
from audio import find_wem_files, convert_wem
|
from audio import find_wem_files, convert_wem
|
||||||
from tunings import (
|
from tunings import (
|
||||||
DEFAULT_REFERENCE_PITCH, DEFAULT_TUNINGS, PROFILE_IDS, PROFILE_PATHWAYS,
|
DEFAULT_REFERENCE_PITCH, DEFAULT_TUNINGS, PROFILE_IDS, PROFILE_PATHWAYS,
|
||||||
apply_flat_instrument_patch_to_profiles, apply_reference_pitch,
|
TUNING_PRESET_MIDIS, apply_flat_instrument_patch_to_profiles,
|
||||||
normalize_instrument_profile, normalize_instrument_profiles,
|
apply_reference_pitch, freqs_to_midis, normalize_instrument_profile,
|
||||||
settings_with_instrument_profiles, tuning_name,
|
normalize_instrument_profiles, settings_with_instrument_profiles,
|
||||||
|
tuning_name,
|
||||||
)
|
)
|
||||||
import sloppak as sloppak_mod
|
import sloppak as sloppak_mod
|
||||||
import drums as drums_mod
|
import drums as drums_mod
|
||||||
@@ -10766,7 +10767,25 @@ def get_tunings():
|
|||||||
ref = DEFAULT_REFERENCE_PITCH
|
ref = DEFAULT_REFERENCE_PITCH
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
ref = DEFAULT_REFERENCE_PITCH
|
ref = DEFAULT_REFERENCE_PITCH
|
||||||
return {"referencePitch": ref, "tunings": tuning_providers.get_merged(ref)}
|
merged = tuning_providers.get_merged(ref)
|
||||||
|
# tuningMidis: the same catalog as exact integer MIDI notes (low → high).
|
||||||
|
# Built-ins come straight from TUNING_PRESET_MIDIS (no float round-trip);
|
||||||
|
# provider-contributed entries are recovered from their frequencies at the
|
||||||
|
# served reference pitch. Every consumer today (the v3 badges, plugins)
|
||||||
|
# reconstructs midis client-side via log2 — a rounding footgun at non-440
|
||||||
|
# references — so serve the integers once, host-side. Additive: the
|
||||||
|
# existing referencePitch/tunings shape is unchanged.
|
||||||
|
tuning_midis: dict[str, dict[str, list[int]]] = {}
|
||||||
|
for key, names in merged.items():
|
||||||
|
builtin = TUNING_PRESET_MIDIS.get(key, {})
|
||||||
|
resolved: dict[str, list[int]] = {}
|
||||||
|
for name, freqs in names.items():
|
||||||
|
midis = builtin.get(name) or freqs_to_midis(freqs, ref)
|
||||||
|
if midis:
|
||||||
|
resolved[name] = list(midis)
|
||||||
|
if resolved:
|
||||||
|
tuning_midis[key] = resolved
|
||||||
|
return {"referencePitch": ref, "tunings": merged, "tuningMidis": tuning_midis}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/settings")
|
@app.get("/api/settings")
|
||||||
|
|||||||
@@ -249,3 +249,30 @@ def test_flat_string_count_patch_resets_incompatible_named_tuning():
|
|||||||
patched = apply_flat_instrument_patch_to_profiles(settings, {"string_count": 7})
|
patched = apply_flat_instrument_patch_to_profiles(settings, {"string_count": 7})
|
||||||
assert patched["string_count"] == 7
|
assert patched["string_count"] == 7
|
||||||
assert patched["tuning"] == "Standard"
|
assert patched["tuning"] == "Standard"
|
||||||
|
|
||||||
|
|
||||||
|
# ── freqs_to_midis (the /api/tunings tuningMidis inverse) ────────────────────
|
||||||
|
|
||||||
|
def test_freqs_to_midis_round_trips_every_builtin_at_440():
|
||||||
|
from tunings import freqs_to_midis
|
||||||
|
for key, presets in TUNING_PRESET_MIDIS.items():
|
||||||
|
for name, midis in presets.items():
|
||||||
|
assert freqs_to_midis(open_midis_to_freqs(midis)) == midis, f"{key}/{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_freqs_to_midis_round_trips_at_nonstandard_reference():
|
||||||
|
# The consumer footgun this exists to kill: frequencies served at a 432/450
|
||||||
|
# reference must recover the SAME integer midis when inverted at that
|
||||||
|
# reference (client-side log2-at-440 reconstruction drifts here).
|
||||||
|
from tunings import freqs_to_midis
|
||||||
|
for ref in (430.0, 432.0, 444.0, 450.0):
|
||||||
|
for midis in (TUNING_PRESET_MIDIS["guitar-8"]["Standard"], TUNING_PRESET_MIDIS["bass-5"]["Standard"]):
|
||||||
|
freqs = open_midis_to_freqs(midis, ref)
|
||||||
|
assert freqs_to_midis(freqs, ref) == midis, f"ref={ref}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_freqs_to_midis_rejects_garbage():
|
||||||
|
from tunings import freqs_to_midis
|
||||||
|
assert freqs_to_midis([82.41, 0]) is None # non-positive
|
||||||
|
assert freqs_to_midis([82.41, "x"]) is None # non-numeric
|
||||||
|
assert freqs_to_midis([]) == [] # vacuously fine
|
||||||
|
|||||||
Reference in New Issue
Block a user