Skip to content

Extract shared natural-HOPO helpers into natural-hopo.ts - #105

Merged
Geomitron merged 2 commits into
Geomitron:masterfrom
elicwhite:shared-natural-hopo-helpers
Apr 21, 2026
Merged

Extract shared natural-HOPO helpers into natural-hopo.ts#105
Geomitron merged 2 commits into
Geomitron:masterfrom
elicwhite:shared-natural-hopo-helpers

Conversation

@elicwhite

@elicwhite elicwhite commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Summary

The chart scanner, chart writer, and MIDI writer each had their own near-identical copies of isFretChord / isSameFretNote / isInFretNote / isNaturalHopo over NoteEvent[], and the parser had the same three helpers again over TrackEvent[]. This PR consolidates all five callsites into src/chart/natural-hopo.ts.

The helpers are generic (<T, E extends { type: T }> + an isFret: (t: T) => boolean predicate); thin wrappers are exported for each concrete group type:

  • NoteEvent-based (scanner + writers): isFretChord, isSameFretNote, isInFretNote, plus the combined isNaturalHopo.
  • TrackEvent-based (parser's resolveFretModifiers): isFretChordRawEvents, isSameFretNoteRawEvents, isInFretNoteRawEvents. (The parser still builds its own natural-HOPO check inline because it compares effectiveNotes to lastNotes while passing the raw pre-coalesced events to isSameFretNoteRawEvents — a subtlety no NoteEvent callsite has.)

The isFretNoteType / isFretEventType predicates are also exported, so the parser's two other isFretNote(...) callsites (filtering event lists) share the same definition too. The parser's four local helpers (isFretNote, isSameFretNote, isFretChord, isInFretNote) are deleted.

Net: ~70 LOC of duplication removed across the scanner, parser, and (in the later writer PRs) two writers. One file owns every natural-HOPO rule; changes to the rules happen in one place.

Stack (bottom → top)

  1. Move hasLyrics/hasVocals/hasForcedNotes from ParsedChart to ScannedChart (state-derived) #98 Move hasLyrics/hasVocals/hasForcedNotes to ScannedChart
  2. this PR Extract shared natural-HOPO helpers
  3. Add createEmptyChart() counterpart to parseChartAndIni #99 createEmptyChart
  4. Add writeIniFile() for song.ini emission #100 writeIniFile
  5. writeChartFile: [Song] / [SyncTrack] / [Events] / unrecognized sections #101 writeChartFile core
  6. writeChartFile: emit instrument track sections #102 writeChartFile tracks
  7. Add writeMidiFile() with TEMPO/EVENTS/unrecognized-track emission #103 writeMidiFile core
  8. writeMidiFile: emit PART DRUMS instrument tracks #104 writeMidiFile drums
  9. writeMidiFile: emit PART GUITAR / GHL instrument tracks #106 writeMidiFile guitar
  10. writeMidiFile: emit PART VOCALS / HARM1-3 vocal tracks #107 writeMidiFile vocals
  11. Add same-format round-trip integration tests #108 round-trip integration tests
  12. Add ChartDocument + writeChartFolder orchestrator #109 ChartDocument + writeChartFolder

Test plan

  • yarn test green at this branch (290 tests) and at the tip of the stack (442 tests).
  • No behavioral change — scanner / parser / writers use the same logic, just routed through shared helpers.

…art (state-derived)

The three derived boolean flags on ParsedChart (hasLyrics, hasVocals,
hasForcedNotes) were parse-time snapshots that went stale whenever
consumers mutated chart data post-parse. Remove them from ParsedChart
entirely; scanChart derives all three at scan time from the current
chart state, piggybacking on its existing single note-walk (no extra
iterations).

- hasLyrics / hasVocals: computed from parsedChart.vocalTracks.parts —
  constant-time existence check.

- hasForcedNotes: inverts resolveFretModifiers. A note is 'forced' iff
  its resolved flag disagrees with the natural HOPO state the parser
  would pick without any force events, or it carries the tap flag
  (tap can only come from explicit forceTap). Inlined helpers for
  isNaturalHopo, isFretChord, isSameFretNote, isInFretNote,
  computeHopoThresholdTicks in chart-scanner.ts (duplicated from
  notes-parser.ts; a follow-up can extract to a shared helper).

Semantic change to note: under the new definition, redundantly-applied
force events (e.g. explicit forceHopo on a naturally-HOPO note) no
longer contribute to hasForcedNotes — the chart plays identically with
or without them, so state-derived detection correctly says false. This
eliminates the need for any 'hasForcedNotes backstop' in writers since
the flag round-trips naturally: the writer emits force events exactly
when a flag disagrees with natural state, and the scanner detects
those same disagreements on re-parse.

ScannedChart.notesData's shape is unchanged (hasLyrics / hasVocals /
hasForcedNotes still present); only the population source changes.

Consumers that previously read parsedChart.hasLyrics /
parsedChart.hasVocals / parsedChart.hasForcedNotes must switch to
scanChart output.
The chart scanner, chart writer, and MIDI writer each had their own
near-identical copies of isFretChord / isSameFretNote / isInFretNote /
isNaturalHopo over NoteEvent[], and the parser had the same three
helpers again over TrackEvent[]. This consolidates all five callsites
into src/chart/natural-hopo.ts.

The helpers are generic (<T, E extends { type: T }> + an isFret
predicate); thin wrappers are exported for each concrete group type:

  - NoteEvent-based (scanner + writers): isFretChord / isSameFretNote /
    isInFretNote, plus the combined isNaturalHopo.
  - TrackEvent-based (parser's resolveFretModifiers):
    isFretChordRawEvents / isSameFretNoteRawEvents /
    isInFretNoteRawEvents. The parser still builds its natural-HOPO
    check inline — it compares effectiveNotes to lastNotes while
    passing the raw pre-coalesced events to isSameFretNoteRawEvents,
    a subtlety no NoteEvent callsite has.

The isFretNoteType / isFretEventType predicates are also exported,
so the parser's two standalone isFretNote(...) callsites (filtering
event lists outside the group helpers) share the same definition too.
The parser's four local helpers (isFretNote, isSameFretNote, isFretChord,
isInFretNote) are deleted.

Net: ~70 LOC of duplication removed across scanner, parser, and (in
the writer branches further up the stack) the two writers. One file
owns every natural-HOPO rule.

This commit updates the scanner + parser; the writers move to the
shared helpers in the two branches further up the stack that own them.
@elicwhite
elicwhite force-pushed the shared-natural-hopo-helpers branch from f850ad9 to ec22459 Compare April 21, 2026 04:29
@Geomitron
Geomitron merged commit 8cee8e5 into Geomitron:master Apr 21, 2026
@elicwhite
elicwhite deleted the shared-natural-hopo-helpers branch April 21, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants