Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Changelog

## [Unreleased] - 1.33.0

Guitar scoring accuracy: notes the player got right no longer score as misses, and stats plugins
get the per-string detail they used to patch `screen.js` for.

### Fixed

- **Capo charts** (#73): fretted notes are read from the nut, the Rocksmith way (a capo-4 E chord
is `A7 D6 G0 B5`); only fret 0 sounds at the capo. Expected pitch used to add the capo to every
fret, so capo charts barely scored. Covers the browser detector, the desktop engine (both the
pushed chart and `scoreChord`: capo 0, open notes moved to the capo fret) and the detected-fret
display fallback.
- **Fast chord runs** (#75): a ringing chord in a fast run of one shape no longer scores as an
EARLY miss. The engine verifier stamps a chord with the member detected closest to the chart
time, and a stamp within one poll of the window edge counts as on time; on the `scoreChord`
path one early strummed frame no longer locks the chord as a miss.
- **Retuned / transformed charts** (#24): expected pitches follow the host's effective
`getTuning()` / `getCapo()` / `getCentOffset()` (core chart-transform providers) instead of the
original song metadata, and resync within a tick when a provider restages mid-song, on every
detect path including desktop addons without the engine verifier. The desktop engine gets
whole-semitone offsets with its pitch gate widened by the rounding error (up to 50 cents), so a
+45-cent record still scores.
- **Technique flags** (#79): bend / slide are read off core's wire format (`sl`/`slu` = -1 means
no slide, `bn` is the bend amount). Every plain note used to look like a slide and get the
widened pitch window, and real bends were never seen.
- **Section stats on desktop** (#81): per-section hits / misses are tracked on the engine-verifier
path (they were always empty there), and each judgment is binned by the section its note sits in.

### Added

- **Richer `notedetect:hit` / `notedetect:miss` payloads** (#77), all additive: `key`, per-string
`strings` verdicts (`{ s, f, hit, centsError, snr?, detectedAt? }`), named `techniques`,
`latencyMs`, `avOffsetMs`, `detector`, `section`, `counted`, `points`, `multiplier`, `streak`,
`isDefault`, and `verdictAt` / `noteEnd` so a renderer can still flash a hit whose verdict landed
after the gem was culled. See README "Events".
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defer the window copy a task and drop it if the element copy arrived

| Event | Bus too? | Payload (additive-only contract) |
|---|---|---|
| `notedetect:hit` / `notedetect:miss` | as `note:hit`/`note:miss` | the full judgment (`{ hit, chord, note, notes, noteTime, timingState, timingError, pitchState, pitchError, ... }`) |
| `notedetect:hit` / `notedetect:miss` | as `note:hit`/`note:miss` | the full judgment (`{ hit, chord, note, notes, noteTime, timingState, timingError, pitchState, pitchError, ... }`), plus from v1.33 `{ key, strings, techniques, latencyMs, avOffsetMs, detector, section, counted, points, multiplier, streak, isDefault, verdictAt, noteEnd }` (field reference in README "Events"; stamped by `_ndEnrichJudgment` at emit) |
| `notedetect:fx` | yes | `{ fxType: 'multiplier', mult, prevMult, streak }` on tier change; `{ fxType: 'milestone', streak, mult }` at 25/50/every 100; `{ fxType: 'streakBreak', lostStreak, prevMult }` after a ≥10 run dies. All carry `{ isDefault, ts }` |
| `notedetect:session` | no | end-of-song summary: `{ title, artist, arrangement, accuracy, hits, misses, bestStreak, score, maxMultiplier, grade, fullCombo, sections, timestamp }` (consumed by the practice journal — never rename/remove keys) |
| `notedetect:skin` | yes | `{ skin }` — re-resolve any cached palette on this |
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ git clone https://github.com/got-feedback/feedback-plugin-notedetect.git note_de
5. The note's **gem lights up brightly** on the highway when you hit it cleanly — and a sustained note keeps glowing for as long as you keep playing it on-pitch; a miss is flagged on the note too
6. Running accuracy and streak shown in the HUD

**Capo charts** are read the Rocksmith way: a fretted note's fret counts from the nut (it already includes the capo), and only fret 0 means an open string, which sounds at the capo. A capo-4 E chord is charted `A7 D6 G0 B5`. The expected pitch is `open + fret` for fretted notes and `open + capo` for open ones. A `setVerifyTarget` context that passes `tuning` + `capo` is read the same way.

> **How step 5 renders depends on the active highway renderer.** The plugin publishes a per-note judgment via Slopsmith core's `highway.setNoteStateProvider` hook (slopsmith#254 — honored by the bundled 2D and 3D highways; other renderers can read the same data from the core renderer `bundle`'s `getNoteState(note, chartTime)` method). On the **default 2D highway** the plugin *also* draws its own canvas overlay — the slide-down red ✕ miss markers below the now-line, the EARLY/LATE/SHARP/FLAT diagnostic labels, and the cyan "currently detected" indicator. On a **custom renderer** (e.g. the 3D highway) that canvas overlay is suppressed — the renderer owns the per-note feedback there (the 3D highway shows hit/active glow and a red miss outline + the diagnostic labels itself). The HUD (accuracy/streak), the end-of-song summary, and the optional screen-edge flash are DOM, not canvas, so they show under any renderer. On an **older core** without the hook, the plugin falls back to the 2D-canvas green hit ring / red miss marker overlay near the note regardless of renderer.

## Audio Input Channel Selection
Expand Down Expand Up @@ -123,6 +125,19 @@ Field reference for the per-note events:
| `hit` | `true` only when timing and pitch are both clean | all events (`false` for misses) |
| `timingState` / `pitchState` | Independent diagnostic axes: `OK`, `EARLY`, `LATE`, `SHARP`, or `FLAT` | clean hits and matched diagnostic misses; `null` for pure misses |
| `timingError` / `pitchError` | Signed timing error in milliseconds and signed pitch error in cents; `pitchError` is octave-folded to the nearest octave (so it is not necessarily equal to `(detectedMidi - expectedMidi) * 100`) | clean hits and matched diagnostic misses; `null` for pure misses |
| `chord` / `notes` | `true` for a chord-level judgment; `notes` lists the chord's `{ s, f }` (`note` is then the lead string) | all events |
| `hitStrings` / `totalStrings` / `score` | How many of the chord's strings rang, out of how many, and the ratio the Chord Leniency setting is compared with | chord judgments where the scorer ran |
| `key` | Stable id of the judged chart note, the same key the highway's `getNoteState` hands back as `popKey`: `"<time>_<string>_<fret>"` for a single note (e.g. `"12.345_2_7"`), `"<time>_chord"` for a chord | all events (v1.33+) |
| `strings` | Per-string verdicts, `[{ s, f, hit, centsError, snr?, detectedAt? }]` (`centsError` rounded cents or `null`; `snr` and `detectedAt` from the desktop engine). For a partial chord this says which strings rang. A missed browser-path chord reports the best frame the scorer saw in its window | chord judgments; single notes on the desktop engine path. `null` when no per-string measurement exists |
| `techniques` | Named techniques of the chart note: `bend`, `slide`, `harmonic`, `pinchHarmonic`, `hammerOn`, `pullOff`, `tap`, `palmMute`, `mute`, `fretHandMute`, `vibrato`, `tremolo`, `accent`, `slap`, `pluck`. `[]` for a plain note. The raw chart note stays in `chartNote` | all chart judgments (v1.33+); `null` for external verdicts |
| `latencyMs` / `avOffsetMs` | The input-latency compensation and A/V offset in effect when the note was judged. `timingError` already has both applied; add `latencyMs` back to get the uncompensated offset | all events (v1.33+) |
| `detector` | Which path judged it: `desktop-engine-verifier`, a desktop detector label, `web-yin` / `web-hps` / `web-crepe`, or `external` | all events (v1.33+) |
| `section` | Name of the chart section the note sits in (by chart time), or `null` | all events (v1.33+) |
| `counted` / `points` / `multiplier` / `streak` | Whether the judgment counted toward the score, the points it earned, and the multiplier and streak after it | all events (v1.33+) |
| `isDefault` | `true` for the main player's detector, `false` for a splitscreen panel's | all events (v1.33+) |
| `verdictAt` / `noteEnd` | Chart time (highway clock, seconds) when the verdict was emitted, and when the judged note / chord finished (`noteTime` + its longest sustain). The desktop engine judges a sustained note only after its sustain closes, so `verdictAt` can be well past `noteTime`; a renderer that has already culled the gem can use these to still flash the hit | all chart judgments (v1.33+); `null` without a note time |

Fields are additive: existing ones keep their meaning, and new ones may appear. Timing class for a scorecard is `timingState` (`OK` / `EARLY` / `LATE`) with `timingError` as the signed offset.

Example — log every hit:

Expand All @@ -133,6 +148,21 @@ window.addEventListener('notedetect:hit', (e) => {
});
```

Example — a per-string tally for partial chords:

```js
const perString = {};
window.addEventListener('notedetect:miss', (e) => {
const j = e.detail;
if (!j.counted || !j.isDefault || !j.chord || !j.strings) return;
for (const st of j.strings) {
const k = `${st.s}`;
perString[k] = perString[k] || { rang: 0, missed: 0 };
perString[k][st.hit ? 'rang' : 'missed']++;
}
});
```

## Pitch Detection Methods

YIN is the default and handles most rigs well. The other methods are opt-in for specific failure modes:
Expand Down Expand Up @@ -183,6 +213,8 @@ On **Slopsmith Desktop**, when the native audio engine has the Spotify **Basic P

This path is fully feature-detected and fail-soft: a desktop build without `detectNotes` still scores single notes via `getPitchDetection`; one without the `scoreChord` IPC skips chord scoring; and a build without the ML model falls back to YIN/`ChordScorer`. The browser build is unaffected and keeps the JS YIN/HPS/CREPE + constraint path. See `slopsmith-desktop/src/audio/MlNoteDetector.*`.

> **Retuned / transformed charts.** Expected pitches follow the chart the player actually sees. When a core [`chart-transform`](https://github.com/got-feedback/feedback/blob/main/docs/capability-recipes.md#chart-transform-provider) provider rewrites the chart (e.g. a retune, transposition or "convert to E standard" view), the plugin scores against the host's *effective* `highway.getTuning()`, `getCapo()` and `getCentOffset()` instead of the original `getSongInfo()` metadata, and re-syncs within a frame when a provider restages mid-song. The effective `centOffset` is applied too (minus whole octaves, which octave-folded matching and the bass base tables already cover — an extended-range bass's `-1200` changes nothing), so a pitch-shifted backing track (say `-100` cents for a half-step-down retune, or `+11.8` for an A443 recording) scores against where the audio really sits. A plugin that pitch-shifts the song should publish that shift as a chart-transform provider returning `tuning` and/or `centOffset`; note detection follows with no extra wiring. On the desktop engine path offsets are rounded to whole semitones (the engine reads integers), and the engine's pitch gate is widened by the rounding error (up to 50 cents) so a sub-semitone shift such as a +45-cent record can't push notes out of tolerance.

## Requirements

- Browser with `getUserMedia` support (all modern browsers)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "slopsmith-plugin-notedetect",
"version": "1.32.0",
"version": "1.33.0",
"private": true,
"description": "Slopsmith note detection plugin — test harness for detection core",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "note_detect",
"name": "Note Detection",
"version": "1.32.0",
"version": "1.33.0",
"private": false,
"script": "screen.js",
"styles": "assets/plugin.css",
Expand Down
Loading