mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 21:01:40 +00:00
static/js/tuning-display.js (228 lines) — bodies VERBATIM. app.js 9,838 → 9,650.
A LEAF: imports nothing.
Tuning NAME resolution (Drop D / Eb Standard / raw-offset fallback), bass
detection, effective string count, and the target FREQUENCIES + note names the
tuner checks against. Pure functions over a small MIDI/note-name table; the 3
_TUNING_* tables are read nowhere else and move in.
NOT A SLICE — a node-level extract. The span 2309-2535 INTERLEAVES the functions
with the `window.*` / `window.feedBack.*` assignments that publish them, and one
of those is `window.feedBack = window.feedBack || {}` — the BUS BOOTSTRAP, not
tuning code at all. Every ExpressionStatement stays exactly where it was; only the
16 functions and 3 tables move. app.js re-exposes the imported bindings from the
same lines, so the public surface and its ordering are untouched (constitution II
names window.feedBack).
app.js -> { plugin-loader, viz, diagnostics-export, dom, highway-colors,
tuning-display }
HARNESSES — 4 broke, and 3 of them broke in the SAME informative way: they sliced
app.js from `function isBassArrangement(` UP TO the marker
`window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;` — an end-marker
that (correctly) stayed behind in app.js. The module is now nothing BUT the tuning
helpers, so there is no block to slice: they read it whole and strip `export ` so
the vm sandbox still evaluates it as a script.
tuner_auto_open is SPLIT — its autoplay-gate test still reads app.js, so it keeps
APP_JS and gains TUNING_JS. Retargeting its path wholesale (my first attempt)
silently pointed the autoplay test at the wrong file.
VERIFIED BY DRIVING THE CONTRACT. A/B against origin/main in two browsers, through
the real window surface: displayTuningName -> "E Standard" / "Drop D" /
"Eb Standard", parseRawTuningOffsets('-2,0,0,0,0,0') -> [-2,0,0,0,0,0],
isBassArrangement, effectiveStringCount, displayTuningTargets, and
window.feedBack.displayTuningName / .songTuningContext — IDENTICAL on both, zero
console/page errors either side.
pytest 2396, node 1038/1038, ESLint 0, tailwind-fresh clean, Codex 0.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
229 lines
8.6 KiB
JavaScript
229 lines
8.6 KiB
JavaScript
// Tuning display — naming, string counts, and target frequencies.
|
|
//
|
|
// Carved verbatim out of static/app.js (R3a). A LEAF module: imports nothing.
|
|
//
|
|
// Turns raw per-string semitone offsets into things a human reads: a tuning NAME
|
|
// ("Drop D", "Eb Standard", or a raw-offsets fallback), whether an arrangement is
|
|
// bass, its effective string count, and the target FREQUENCIES + note names the
|
|
// tuner checks against. Pure functions over a small MIDI/note-name table.
|
|
//
|
|
// The window / window.feedBack assignments for these stay in app.js — they are the
|
|
// public contract (constitution II names window.feedBack), and app.js re-exposes
|
|
// the imported bindings from exactly where it always did, so nothing about the
|
|
// surface or its ordering changes.
|
|
|
|
// Display-only tuning label helpers — never mutate offsets or affect playback.
|
|
function _looksLikeRawTuningOffsets(str) {
|
|
if (!str || typeof str !== 'string') return false;
|
|
const s = str.trim();
|
|
if (!s) return false;
|
|
if (/^-?\d+$/.test(s)) return true;
|
|
if (/^-?\d+(?: -?\d+)+$/.test(s)) return true;
|
|
if (/^-?\d+(?:,-?\d+)+$/.test(s)) return true;
|
|
if (/^-?\d+(-?\d+){2,}$/.test(s)) return true;
|
|
return false;
|
|
}
|
|
|
|
function _tuningNameFromOffsets(offsets) {
|
|
if (!offsets || !offsets.length) return '';
|
|
const standard = {
|
|
0: 'E Standard', '-1': 'Eb Standard', '-2': 'D Standard',
|
|
'-3': 'C# Standard', '-4': 'C Standard', '-5': 'B Standard',
|
|
'-6': 'Bb Standard', '-7': 'A Standard',
|
|
1: 'F Standard', 2: 'F# Standard',
|
|
};
|
|
// Uniform offsets across 4 (bass) / 5 / 6 strings name the same Standard;
|
|
// a 4-string bass [0,0,0,0] must read "E Standard", not "Custom Tuning".
|
|
if (offsets.length >= 4 && offsets.every((o) => o === offsets[0])) {
|
|
const name = standard[offsets[0]];
|
|
if (name) return name;
|
|
}
|
|
if (offsets.length >= 4 && offsets[0] === offsets[1] - 2
|
|
&& offsets.slice(1).every((o) => o === offsets[1])) {
|
|
const noteNames = ['E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'C#', 'D', 'Eb'];
|
|
return 'Drop ' + noteNames[((offsets[0] % 12) + 12) % 12];
|
|
}
|
|
const named = {
|
|
'-2,0,0,0,0,0': 'Drop D',
|
|
'-4,-2,-2,-2,-2,-2': 'Drop C',
|
|
'-2,-2,0,0,0,0': 'Double Drop D',
|
|
'0,0,0,-1,0,0': 'Open G',
|
|
'-2,-2,0,0,-2,-2': 'Open D',
|
|
'-2,0,0,0,-2,0': 'DADGAD',
|
|
'0,2,2,1,0,0': 'Open E',
|
|
'-2,0,0,2,3,2': 'Open D (alt)',
|
|
};
|
|
if (offsets.length === 6) {
|
|
const key = offsets.join(',');
|
|
if (named[key]) return named[key];
|
|
}
|
|
return 'Custom Tuning';
|
|
}
|
|
|
|
export function displayTuningName(value, offsets) {
|
|
// Explicit offsets win — always name them.
|
|
if (Array.isArray(offsets) && offsets.length > 0) {
|
|
return _tuningNameFromOffsets(offsets);
|
|
}
|
|
if (value && typeof value === 'string') {
|
|
const trimmed = value.trim();
|
|
if (!trimmed || trimmed === 'Unknown') return '';
|
|
if (!_looksLikeRawTuningOffsets(trimmed)) {
|
|
return trimmed;
|
|
}
|
|
// A raw offset string (now served by the API) — parse and name it so a
|
|
// known tuning like "-1 -1 -1 -1 -1 -1" reads "Eb Standard" rather than
|
|
// collapsing to "Custom Tuning".
|
|
const parsed = (typeof parseRawTuningOffsets === 'function')
|
|
? parseRawTuningOffsets(trimmed) : null;
|
|
if (parsed && parsed.length) return _tuningNameFromOffsets(parsed);
|
|
return 'Custom Tuning';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function isBassArrangement(context) {
|
|
const ctx = context && typeof context === 'object' ? context : {};
|
|
if (typeof ctx.isBass === 'boolean') return ctx.isBass;
|
|
const label = ((ctx.arrangement || '') + ' ' + (ctx.arrangement_smart_name || '')).toLowerCase();
|
|
if (/\bbass\b/.test(label)) return true;
|
|
if (/\b(lead|rhythm|combo|guitar)\b/.test(label)) return false;
|
|
return false;
|
|
}
|
|
|
|
export function effectiveStringCount(offsets, context) {
|
|
if (!Array.isArray(offsets) || !offsets.length) return 0;
|
|
const ctx = context && typeof context === 'object' ? context : {};
|
|
const isBass = isBassArrangement(ctx);
|
|
let sc = ctx.stringCount > 0 ? Number(ctx.stringCount) : 0;
|
|
if (!isBass) {
|
|
if (sc > 0 && sc <= 5 && offsets.length >= 6) sc = 6;
|
|
if (!sc) sc = offsets.length >= 6 ? offsets.length : 6;
|
|
} else if (!sc) {
|
|
sc = offsets.length >= 5 ? offsets.length : 4;
|
|
}
|
|
return Math.min(sc, offsets.length);
|
|
}
|
|
|
|
export function songTuningContext(songInfo) {
|
|
if (!songInfo || typeof songInfo !== 'object') return {};
|
|
return {
|
|
stringCount: songInfo.stringCount,
|
|
arrangement: songInfo.arrangement,
|
|
arrangement_smart_name: songInfo.arrangement_smart_name,
|
|
};
|
|
}
|
|
|
|
// Open-string target notes (display only) — mirrors plugins/tuner/utils/tuning-utils.js.
|
|
const _TUNING_BASE_MIDI = {
|
|
4: [28, 33, 38, 43],
|
|
5: [23, 28, 33, 38, 43],
|
|
6: [40, 45, 50, 55, 59, 64],
|
|
7: [35, 40, 45, 50, 55, 59, 64],
|
|
8: [30, 35, 40, 45, 50, 55, 59, 64],
|
|
};
|
|
|
|
const _TUNING_NOTE_SHARP = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
|
|
|
const _TUNING_NOTE_FLAT = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
|
|
function _tuningMidiToFreq(m) {
|
|
return Math.pow(2, (m - 69) / 12) * 440;
|
|
}
|
|
|
|
function _tuningOffsetsToFreqs(offsets, isBass) {
|
|
const len = offsets.length;
|
|
let base;
|
|
if (len === 4 || len === 5) {
|
|
base = isBass ? _TUNING_BASE_MIDI[len] : _TUNING_BASE_MIDI[6];
|
|
} else {
|
|
base = _TUNING_BASE_MIDI[len] || _TUNING_BASE_MIDI[6];
|
|
}
|
|
return offsets.map((offset, i) => {
|
|
const root = i < base.length ? base[i] : base[base.length - 1];
|
|
return _tuningMidiToFreq(root + offset);
|
|
});
|
|
}
|
|
|
|
function _noteNameFromFreq(freq, useFlats) {
|
|
const midi = 69 + 12 * Math.log2(freq / 440);
|
|
const rounded = Math.round(midi);
|
|
const names = useFlats ? _TUNING_NOTE_FLAT : _TUNING_NOTE_SHARP;
|
|
return names[((rounded % 12) + 12) % 12];
|
|
}
|
|
|
|
function _octaveNoteFromFreq(freq, useFlats) {
|
|
const midi = 69 + 12 * Math.log2(freq / 440);
|
|
const rounded = Math.round(midi);
|
|
const octave = Math.floor(rounded / 12) - 1;
|
|
return _noteNameFromFreq(freq, useFlats) + octave;
|
|
}
|
|
|
|
function _stringOrdinalLabel(n) {
|
|
const v = n % 100;
|
|
if (v >= 11 && v <= 13) return n + 'th';
|
|
const suffix = { 1: 'st', 2: 'nd', 3: 'rd' }[n % 10] || 'th';
|
|
return n + suffix;
|
|
}
|
|
|
|
function _tuningTargetFreqs(offsets, context) {
|
|
if (!Array.isArray(offsets) || !offsets.length) return [];
|
|
const ctx = context && typeof context === 'object' ? context : {};
|
|
const stringCount = effectiveStringCount(offsets, ctx);
|
|
const trimmed = offsets.slice(0, stringCount);
|
|
if (!trimmed.length) return [];
|
|
const isBass = isBassArrangement(ctx);
|
|
try {
|
|
return _tuningOffsetsToFreqs(trimmed, isBass);
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Flat vs sharp spelling. A caller that knows the preference can pass
|
|
// ctx.useFlats; otherwise we infer from a flat-keyed tuning name. The v3
|
|
// card/HUD pass "Custom Tuning" (raw offsets carry no key), so those default
|
|
// to sharps unless an explicit useFlats is supplied.
|
|
function _resolveTargetUseFlats(ctx) {
|
|
if (typeof ctx.useFlats === 'boolean') return ctx.useFlats;
|
|
return typeof ctx.tuningName === 'string' && /\b[A-G]b\b/.test(ctx.tuningName);
|
|
}
|
|
|
|
export function displayTuningTargetDetails(offsets, context) {
|
|
const ctx = context && typeof context === 'object' ? context : {};
|
|
const useFlats = _resolveTargetUseFlats(ctx);
|
|
const freqs = _tuningTargetFreqs(offsets, ctx);
|
|
return freqs.map((f, i) => {
|
|
const stringNumber = freqs.length - i;
|
|
const note = _noteNameFromFreq(f, useFlats);
|
|
const octaveNote = _octaveNoteFromFreq(f, useFlats);
|
|
return {
|
|
stringNumber,
|
|
note,
|
|
octaveNote,
|
|
title: _stringOrdinalLabel(stringNumber) + ' string: ' + octaveNote,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function displayTuningTargets(offsets, context) {
|
|
const ctx = context && typeof context === 'object' ? context : {};
|
|
const useFlats = _resolveTargetUseFlats(ctx);
|
|
const freqs = _tuningTargetFreqs(offsets, ctx);
|
|
if (!freqs.length) return '';
|
|
return freqs.map((f) => _noteNameFromFreq(f, useFlats)).join(' ');
|
|
}
|
|
|
|
export function parseRawTuningOffsets(value) {
|
|
if (Array.isArray(value) && value.length) return value;
|
|
if (!value || typeof value !== 'string') return null;
|
|
const s = value.trim();
|
|
if (/^-?\d+(?: -?\d+)+$/.test(s)) {
|
|
return s.split(/\s+/).map((n) => Number(n));
|
|
}
|
|
if (/^-?\d+(?:,-?\d+)+$/.test(s)) {
|
|
return s.split(',').map((n) => Number(n));
|
|
}
|
|
return null;
|
|
}
|