Repository navigation
fix(tuning): score against the effective (chart-transform) tuning #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -316,7 +316,7 @@ const _ND_AUTO_ENABLE_RETRY_MS = 1500; | |
| // exact build that produced it. The script tag has no `import`/`fetch` | ||
| // hook to read package.json at load time, so this is the single | ||
| // hand-maintained constant the diagnostic path keys off of. | ||
| const _ND_VERSION = '1.32.0'; | ||
| const _ND_VERSION = '1.33.0'; | ||
|
|
||
| // Bleed-rescue tuning for the low-bass blind spot. A bass DI's fundamental is | ||
| // weaker than its 2nd harmonic and an open string / neighbour often rings the | ||
|
|
@@ -613,6 +613,34 @@ function _ndMidiFromStringFret(string, fret, arrangement, stringCount, offsets, | |
| return base[string] + offset + (capo || 0) + fret; | ||
| } | ||
|
|
||
| // Whole-chart reference-pitch shift (cents) → the part note detection applies. | ||
| // The host's effective `centOffset` (`highway.getCentOffset()`: a chart- | ||
| // transform provider's value, else the chart's own `<centOffset>`) says the | ||
| // audio sits N cents off the chart's nominal pitch — a retune/pitch-shift | ||
| // transform, a true-tuned A443 recording (+11.8), a song shifted to E standard. | ||
| // Whole octaves are dropped: matching is already octave-folded and the | ||
| // per-arrangement base tables already place bass in its own octave, so the | ||
| // common extended-range-bass `-1200` must keep scoring exactly as before. The | ||
| // remainder lands in (-600, 600] and is folded into the per-string offsets | ||
| // (as fractional semitones) by _syncChartStateFromHw, so it reaches every | ||
| // expected MIDI / band range through _ndMidiFromStringFret unchanged. | ||
| function _ndRefShiftCents(centOffset) { | ||
| const c = Number(centOffset); | ||
| if (!Number.isFinite(c)) return 0; | ||
| const r = c - 1200 * Math.round(c / 1200); | ||
| return Math.abs(r) < 1e-6 ? 0 : r; | ||
| } | ||
|
|
||
| // The desktop engine's chart/scoreChord contexts read tuning offsets as | ||
| // Int32 (truncating), so a fractional reference shift is rounded to the | ||
| // nearest semitone at the bridge boundary — a whole-semitone retune still | ||
| // reaches the engine exactly; a sub-semitone residue (e.g. A443) stays a | ||
| // JS-path refinement that sits inside the engine's pitch tolerance anyway. | ||
| function _ndEngineOffsets(offsets) { | ||
| if (!Array.isArray(offsets)) return offsets; | ||
| return offsets.map(o => Math.round(Number(o) || 0)); | ||
| } | ||
|
|
||
| function _ndClassifyTiming(timingErrorMs, timingThresholdMs, lateGraceMs) { | ||
| if (!Number.isFinite(timingErrorMs)) return null; | ||
| const grace = Number.isFinite(lateGraceMs) && lateGraceMs > 0 ? lateGraceMs : 0; | ||
|
|
@@ -3373,6 +3401,9 @@ function createNoteDetector(options = {}) { | |
| let tuningOffsets = [0, 0, 0, 0, 0, 0]; | ||
| let capo = 0; | ||
| let currentStringCount = 6; // kept in sync with tuningOffsets.length | ||
| // Effective-tuning inputs the fields above were last synced from (see | ||
| // _ndResyncIfTuningChanged); null = never synced. | ||
| let _ndSyncedTuningKey = null; | ||
|
|
||
| // Audio buffers | ||
| let accumBuffer = new Float32Array(0); | ||
|
|
@@ -3526,6 +3557,7 @@ function createNoteDetector(options = {}) { | |
| } | ||
| function _ndBridgeScoreChord(ctx) { | ||
| const a = _ndBridgeAudio(); if (!a) return Promise.resolve(null); | ||
| if (ctx && Array.isArray(ctx.offsets)) ctx = { ...ctx, offsets: _ndEngineOffsets(ctx.offsets) }; | ||
| if (sourceId != null && typeof a.scoreSourceChord === 'function') | ||
| return a.scoreSourceChord(sourceId, ctx); | ||
| if (sourceId == null && typeof a.scoreChord === 'function') | ||
|
|
@@ -3540,6 +3572,9 @@ function createNoteDetector(options = {}) { | |
| } | ||
| function _ndBridgeSetChart(chart) { | ||
| const a = _ndBridgeAudio(); if (!a) return Promise.resolve(null); | ||
| if (chart && Array.isArray(chart.tuningOffsets)) { | ||
| chart = { ...chart, tuningOffsets: _ndEngineOffsets(chart.tuningOffsets) }; | ||
| } | ||
| if (sourceId != null && typeof a.setSourceChart === 'function') | ||
| return a.setSourceChart(sourceId, chart); | ||
| if (sourceId == null && typeof a.setChart === 'function') | ||
|
|
@@ -4181,6 +4216,10 @@ function createNoteDetector(options = {}) { | |
| // song:loaded (and can switch mid-session); | ||
| // re-push whenever the live chart diverges | ||
| // from what the engine currently holds. | ||
| // A live retune restage changes the | ||
| // effective tuning, which is in the | ||
| // signature, so it re-pushes here too. | ||
| _ndResyncIfTuningChanged(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Poll effective tuning on the desktop fallback path. If a desktop addon lacks the chart-verifier API, the detection tick reaches 🧰 Tools🪛 ast-grep (0.45.3)[error] 4148-4395: React's useState should not be directly called Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice. (usestate-direct-usage) 🤖 Prompt for AI Agents |
||
| if (_ndChartSignature() !== _ndVerifierChartSig) { | ||
| await _ndPushChartToBridge(); | ||
| if (!enabled || gen !== sessionGen) return; | ||
|
|
@@ -5059,6 +5098,9 @@ function createNoteDetector(options = {}) { | |
|
|
||
| // ── Frame processing ────────────────────────────────────────────── | ||
| async function processFrame(buffer) { | ||
| // Follow a mid-song chart-transform restage (live retune) before | ||
| // mapping any (string, fret) to an expected pitch this frame. | ||
| _ndResyncIfTuningChanged(); | ||
| // Append to the rolling raw-audio buffer for the bass miss rescue | ||
| // (checkMisses). Bass only — guitar's higher fundamentals resolve in | ||
| // the short window, so it doesn't need (or pay for) this. | ||
|
|
@@ -13626,10 +13668,26 @@ function createNoteDetector(options = {}) { | |
| tuningOffsets = [0, 0, 0, 0, 0, 0]; | ||
| capo = 0; | ||
| const info = (hw && hw.getSongInfo) ? hw.getSongInfo() : null; | ||
| const eff = _ndReadEffectiveTuning(); | ||
| _ndSyncedTuningKey = eff; | ||
| if (!info) return; | ||
| if (info.arrangement) currentArrangement = _ndArrangementKindFromName(info.arrangement); | ||
| if (Array.isArray(info.tuning)) tuningOffsets = info.tuning; | ||
| if (Number.isFinite(info.capo)) capo = info.capo; | ||
| // Tuning / capo / reference pitch come from the host's EFFECTIVE | ||
| // getters, not getSongInfo(): getSongInfo() keeps the original chart | ||
| // metadata, while getNotes()/getChords() (what we score) already carry | ||
| // any chart-transform provider's output. Reading the original tuning | ||
| // against transformed frets made a retuned / converted-to-E-standard | ||
| // chart score against its old pitches. Hosts without the getters | ||
| // (pre-chart-transform) fall back to song_info. | ||
| const tuning = Array.isArray(eff.tuning) ? eff.tuning : info.tuning; | ||
| if (Array.isArray(tuning)) { | ||
| const shift = _ndRefShiftCents(eff.centOffset !== undefined ? eff.centOffset : info.centOffset); | ||
| tuningOffsets = shift | ||
| ? tuning.map(o => (Number(o) || 0) + shift / 100) | ||
| : tuning; | ||
| } | ||
| const effCapo = eff.capo !== undefined ? eff.capo : info.capo; | ||
| if (Number.isFinite(effCapo)) capo = effCapo; | ||
|
|
||
| // String-count resolution, in precedence order: | ||
| // 1. hw.getStringCount() — host's authoritative count. Asked | ||
|
|
@@ -13654,7 +13712,7 @@ function createNoteDetector(options = {}) { | |
| if (Number.isFinite(hostStringCount)) { | ||
| currentStringCount = hostStringCount; | ||
| } else if (info.arrangement) { | ||
| const tuneLen = Array.isArray(info.tuning) ? info.tuning.length : null; | ||
| const tuneLen = Array.isArray(tuning) ? tuning.length : null; | ||
| const consistent = currentArrangement === 'bass' | ||
| ? (tuneLen === 4 || tuneLen === 5) | ||
| : (tuneLen === 6 || tuneLen === 7 || tuneLen === 8); | ||
|
|
@@ -13663,11 +13721,47 @@ function createNoteDetector(options = {}) { | |
| } else { | ||
| currentStringCount = currentArrangement === 'bass' ? 4 : 6; | ||
| } | ||
| } else if (Array.isArray(info.tuning)) { | ||
| currentStringCount = info.tuning.length; | ||
| } else if (Array.isArray(tuning)) { | ||
| currentStringCount = tuning.length; | ||
| } | ||
| } | ||
|
|
||
| // The host's effective tuning inputs, read raw (undefined when the host | ||
| // predates the chart-transform getters). Compared by identity in | ||
| // _ndResyncIfTuningChanged: the highway hands back the same array/number | ||
| // until a transform restages, so this is a few getter calls per tick. | ||
| function _ndReadEffectiveTuning() { | ||
| const read = (name) => { | ||
| if (!hw || typeof hw[name] !== 'function') return undefined; | ||
| try { return hw[name](); } catch (_) { return undefined; } | ||
| }; | ||
| return { | ||
| tuning: read('getTuning'), | ||
| capo: read('getCapo'), | ||
| centOffset: read('getCentOffset'), | ||
| }; | ||
| } | ||
| // A chart-transform provider can restage mid-song (select / clear / | ||
| // refresh — e.g. a live retune) without any song or arrangement event. | ||
| // Called from the detect tick so scoring follows the effective tuning | ||
| // within a frame; returns true when it resynced. | ||
| function _ndResyncIfTuningChanged() { | ||
| // Pre-chart-transform host: nothing can restage mid-song, and the | ||
| // song/arrangement events already cover every change. | ||
| if (!hw || (typeof hw.getTuning !== 'function' | ||
| && typeof hw.getCapo !== 'function' | ||
| && typeof hw.getCentOffset !== 'function')) return false; | ||
| const k = _ndSyncedTuningKey; | ||
| const now = _ndReadEffectiveTuning(); | ||
| if (k && k.tuning === now.tuning && k.capo === now.capo | ||
| && (k.centOffset === now.centOffset | ||
| || (Number.isNaN(k.centOffset) && Number.isNaN(now.centOffset)))) { | ||
| return false; | ||
| } | ||
| _syncChartStateFromHw(); | ||
| return true; | ||
| } | ||
|
|
||
| // ── Engine-side chart verification (desktop bridge) ─────────────────── | ||
| // Build the song's note chart from the host and push it to the desktop | ||
| // engine, which scores it continuously on a background thread. Replaces | ||
|
|
@@ -13723,7 +13817,8 @@ function createNoteDetector(options = {}) { | |
| // value up live — without this the popup sliders are inert on the | ||
| // engine-verifier path until the next song/arrangement change. | ||
| return n + ':' + c + ':' + currentArrangement + ':' + currentStringCount | ||
| + ':' + capo + ':' + timingTolerance + ':' + pitchTolerance | ||
| + ':' + capo + ':' + tuningOffsets.slice(0, currentStringCount).join(',') | ||
| + ':' + timingTolerance + ':' + pitchTolerance | ||
| + ':' + (fnv >>> 0); | ||
| } | ||
|
|
||
|
|
@@ -18541,6 +18636,7 @@ function createNoteDetector(options = {}) { | |
| _bindChartStateEvents: _chartStateBindEvents, | ||
| _unbindChartStateEvents: _chartStateUnbindEvents, | ||
| _syncChartStateFromHw: _syncChartStateFromHw, | ||
| _resyncIfTuningChanged: _ndResyncIfTuningChanged, | ||
| // Auto-record test hooks — same rationale. Production binds from | ||
| // enableImpl() / unbinds from destroy(); tests drive them without | ||
| // the audio pipeline to assert the song:loaded auto-arm. | ||
|
|
@@ -18585,6 +18681,9 @@ function createNoteDetector(options = {}) { | |
| if (Number.isFinite(ctx.stringCount)) currentStringCount = ctx.stringCount; | ||
| if (Array.isArray(ctx.tuningOffsets)) tuningOffsets = ctx.tuningOffsets.slice(); | ||
| if (Number.isFinite(ctx.capo)) capo = ctx.capo; | ||
| // An injected context is authoritative — don't let the | ||
| // per-tick effective-tuning resync overwrite it. | ||
| _ndSyncedTuningKey = _ndReadEffectiveTuning(); | ||
| }, | ||
| setSettings: (s) => { | ||
| s = s || {}; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: got-feedBack/feedBack-plugin-notedetect
Length of output: 7289
🏁 Script executed:
Repository: got-feedBack/feedBack-plugin-notedetect
Length of output: 30717
🏁 Script executed:
Repository: got-feedBack/feedBack-plugin-notedetect
Length of output: 42549
🏁 Script executed:
Repository: got-feedBack/feedBack-plugin-notedetect
Length of output: 43700
Compensate the engine chart threshold for rounded offsets.
_ndEngineOffsets()rounds fractional offsets beforesetChart(). The host chart then passes the user’s unadjustedpitchToleranceaspitchCheckCents. A 40-cent effective offset rounded to zero can therefore make a correctly played note fail a 10-cent engine check.The addon contract in this repository uses integer offsets, so preserve the rounding and widen only the host chart threshold by the maximum rounding error. The normal
scoreChordpath already uses a 50/60-cent verification window, which covers the maximum 50-cent rounding error.🐛 Suggested fix
function _ndEngineOffsets(offsets) { if (!Array.isArray(offsets)) return offsets; return offsets.map(o => Math.round(Number(o) || 0)); } +function _ndEngineOffsetErrorCents(offsets) { + if (!Array.isArray(offsets)) return 0; + return offsets.reduce((max, o) => { + const n = Number(o) || 0; + if (!Number.isFinite(n)) return max; + return Math.max(max, Math.ceil(Math.abs(Math.round(n) - n) * 100)); + }, 0); +} + ... + const engineOffsetErrorCents = _ndEngineOffsetErrorCents( + tuningOffsets.slice(0, currentStringCount) + ); const ok = await _ndBridgeSetChart({ ... - pitchCheckCents: pitchTolerance, + pitchCheckCents: pitchTolerance + engineOffsetErrorCents,🤖 Prompt for AI Agents