mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-13 20:21:40 +00:00
refactor(app): carve the tuning-display helpers out of app.js (R3a) (#884)
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ebbfc8da6f
commit
d47883c5e5
+9
-196
@@ -24,6 +24,15 @@ import {
|
|||||||
hwcInitSettingsUI,
|
hwcInitSettingsUI,
|
||||||
initHighwayColors,
|
initHighwayColors,
|
||||||
} from './js/highway-colors.js';
|
} from './js/highway-colors.js';
|
||||||
|
import {
|
||||||
|
displayTuningName,
|
||||||
|
displayTuningTargetDetails,
|
||||||
|
displayTuningTargets,
|
||||||
|
effectiveStringCount,
|
||||||
|
isBassArrangement,
|
||||||
|
parseRawTuningOffsets,
|
||||||
|
songTuningContext,
|
||||||
|
} from './js/tuning-display.js';
|
||||||
|
|
||||||
// Demo analytics — real impl set by demo.js; no-op in normal builds
|
// Demo analytics — real impl set by demo.js; no-op in normal builds
|
||||||
window.feedBackDemoTrack = window.feedBackDemoTrack ?? null;
|
window.feedBackDemoTrack = window.feedBackDemoTrack ?? null;
|
||||||
@@ -2305,227 +2314,31 @@ function toggleAllFavoriteArtists(expand) {
|
|||||||
_toggleAllInTree('fav-tree', expand, _FAV_TREE_EXPAND_KEY);
|
_toggleAllInTree('fav-tree', expand, _FAV_TREE_EXPAND_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
|
|
||||||
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 '';
|
|
||||||
}
|
|
||||||
|
|
||||||
window.displayTuningName = displayTuningName;
|
window.displayTuningName = displayTuningName;
|
||||||
window.feedBack = window.feedBack || {};
|
window.feedBack = window.feedBack || {};
|
||||||
window.slopsmith = window.feedBack;
|
window.slopsmith = window.feedBack;
|
||||||
window.feedBack.displayTuningName = displayTuningName;
|
window.feedBack.displayTuningName = displayTuningName;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function songTuningContext(songInfo) {
|
|
||||||
if (!songInfo || typeof songInfo !== 'object') return {};
|
|
||||||
return {
|
|
||||||
stringCount: songInfo.stringCount,
|
|
||||||
arrangement: songInfo.arrangement,
|
|
||||||
arrangement_smart_name: songInfo.arrangement_smart_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
window.feedBack.isBassArrangement = isBassArrangement;
|
window.feedBack.isBassArrangement = isBassArrangement;
|
||||||
window.feedBack.effectiveStringCount = effectiveStringCount;
|
window.feedBack.effectiveStringCount = effectiveStringCount;
|
||||||
window.feedBack.songTuningContext = songTuningContext;
|
window.feedBack.songTuningContext = songTuningContext;
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.displayTuningTargets = displayTuningTargets;
|
window.displayTuningTargets = displayTuningTargets;
|
||||||
window.displayTuningTargetDetails = displayTuningTargetDetails;
|
window.displayTuningTargetDetails = displayTuningTargetDetails;
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -7,20 +7,23 @@ const path = require('node:path');
|
|||||||
const vm = require('node:vm');
|
const vm = require('node:vm');
|
||||||
|
|
||||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||||
|
// The tuning-display helpers were carved out of app.js into their own module (R3a);
|
||||||
|
// the autoplay-gate test below still reads app.js.
|
||||||
|
const TUNING_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js');
|
||||||
const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js');
|
const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js');
|
||||||
const TUNING_UTILS_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'tuning-utils.js');
|
const TUNING_UTILS_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'tuning-utils.js');
|
||||||
const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js');
|
const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js');
|
||||||
|
|
||||||
function loadTuningHelpers() {
|
function loadTuningHelpers() {
|
||||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
const src = fs.readFileSync(TUNING_JS, 'utf8');
|
||||||
const start = src.indexOf('function isBassArrangement(');
|
// The module is nothing BUT the tuning helpers now, so there is no block to
|
||||||
const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;';
|
// slice out — take it whole. `export` is stripped so the vm sandbox can still
|
||||||
const end = src.indexOf(endMarker);
|
// evaluate it as a plain script (the window.* contract lives in app.js).
|
||||||
if (start === -1 || end === -1) throw new Error('tuning helper block not found in app.js');
|
const body = src.replace(/^export /gm, '');
|
||||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||||
vm.createContext(sandbox);
|
vm.createContext(sandbox);
|
||||||
vm.runInContext(
|
vm.runInContext(
|
||||||
src.slice(start, end + endMarker.length),
|
body,
|
||||||
sandbox
|
sandbox
|
||||||
);
|
);
|
||||||
return sandbox.window.feedBack;
|
return sandbox.window.feedBack;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const fs = require('node:fs');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const vm = require('node:vm');
|
const vm = require('node:vm');
|
||||||
|
|
||||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
// The tuning-display helpers were carved out of app.js into their own module (R3a).
|
||||||
|
const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js');
|
||||||
const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js');
|
const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js');
|
||||||
const V3_HTML = path.join(__dirname, '..', '..', 'static', 'v3', 'index.html');
|
const V3_HTML = path.join(__dirname, '..', '..', 'static', 'v3', 'index.html');
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const fs = require('node:fs');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const vm = require('node:vm');
|
const vm = require('node:vm');
|
||||||
|
|
||||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
// The tuning-display helpers were carved out of app.js into their own module (R3a).
|
||||||
|
const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js');
|
||||||
const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js');
|
const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js');
|
||||||
const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js');
|
const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js');
|
||||||
const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js');
|
const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js');
|
||||||
@@ -14,14 +15,14 @@ const V3_HTML = path.join(__dirname, '..', '..', 'static', 'v3', 'index.html');
|
|||||||
|
|
||||||
function loadTuningHelpers() {
|
function loadTuningHelpers() {
|
||||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||||
const start = src.indexOf('function isBassArrangement(');
|
// The module is nothing BUT the tuning helpers now, so there is no block to
|
||||||
const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;';
|
// slice out — take it whole. `export` is stripped so the vm sandbox can still
|
||||||
const end = src.indexOf(endMarker);
|
// evaluate it as a plain script (the window.* contract lives in app.js).
|
||||||
if (start === -1 || end === -1) throw new Error('tuning helper block not found in app.js');
|
const body = src.replace(/^export /gm, '');
|
||||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||||
vm.createContext(sandbox);
|
vm.createContext(sandbox);
|
||||||
vm.runInContext(
|
vm.runInContext(
|
||||||
src.slice(start, end + endMarker.length) + '\n'
|
body + '\n'
|
||||||
+ 'exports.displayTuningTargets = displayTuningTargets;\n'
|
+ 'exports.displayTuningTargets = displayTuningTargets;\n'
|
||||||
+ 'exports.displayTuningTargetDetails = displayTuningTargetDetails;\n'
|
+ 'exports.displayTuningTargetDetails = displayTuningTargetDetails;\n'
|
||||||
+ 'exports.isBassArrangement = isBassArrangement;\n'
|
+ 'exports.isBassArrangement = isBassArrangement;\n'
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const path = require('node:path');
|
|||||||
const vm = require('node:vm');
|
const vm = require('node:vm');
|
||||||
|
|
||||||
const SONGS_JS = path.join(__dirname, '..', '..', 'static', 'v3', 'songs.js');
|
const SONGS_JS = path.join(__dirname, '..', '..', 'static', 'v3', 'songs.js');
|
||||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
// The tuning-display helpers were carved out of app.js into their own module (R3a).
|
||||||
|
const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js');
|
||||||
|
|
||||||
function extractBlock(src, startMarker) {
|
function extractBlock(src, startMarker) {
|
||||||
const start = src.indexOf(startMarker);
|
const start = src.indexOf(startMarker);
|
||||||
@@ -27,14 +28,14 @@ function extractBlock(src, startMarker) {
|
|||||||
|
|
||||||
function loadTuningHelpers() {
|
function loadTuningHelpers() {
|
||||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||||
const start = src.indexOf('function _looksLikeRawTuningOffsets(');
|
// The module is nothing BUT the tuning helpers now, so there is no block to
|
||||||
const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;';
|
// slice out — take it whole. `export` is stripped so the vm sandbox can still
|
||||||
const end = src.indexOf(endMarker);
|
// evaluate it as a plain script (the window.* contract lives in app.js).
|
||||||
if (start === -1 || end === -1) throw new Error('tuning helpers not found');
|
const body = src.replace(/^export /gm, '');
|
||||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||||
vm.createContext(sandbox);
|
vm.createContext(sandbox);
|
||||||
vm.runInContext(
|
vm.runInContext(
|
||||||
src.slice(start, end + endMarker.length) + '\n'
|
body + '\n'
|
||||||
+ 'exports.displayTuningName = displayTuningName;\n'
|
+ 'exports.displayTuningName = displayTuningName;\n'
|
||||||
+ 'exports.displayTuningTargets = displayTuningTargets;\n'
|
+ 'exports.displayTuningTargets = displayTuningTargets;\n'
|
||||||
+ 'exports.parseRawTuningOffsets = parseRawTuningOffsets;',
|
+ 'exports.parseRawTuningOffsets = parseRawTuningOffsets;',
|
||||||
|
|||||||
Reference in New Issue
Block a user