Skip to content

writeMidiFile: emit PART GUITAR / GHL instrument tracks#106

Open
elicwhite wants to merge 7 commits into
Geomitron:masterfrom
elicwhite:midi-writer-guitar
Open

writeMidiFile: emit PART GUITAR / GHL instrument tracks#106
elicwhite wants to merge 7 commits into
Geomitron:masterfrom
elicwhite:midi-writer-guitar

Conversation

@elicwhite

Copy link
Copy Markdown
Contributor

Summary

Adds fret-instrument emission to writeMidiFile.

  • buildFretTrack: one MIDI track per instrument group (guitar / bass / guitarghl / bassghl / keys / guitarcoop / rhythm / GHL variants); multiple difficulties merge into the same track.

  • emitFretNotes: 5-fret (95/83/71/59 diffStarts) and 6-fret/GHL (94/82/70/58) note-number mapping; open notes routed two ways:

    • ENHANCED_OPENS mode (diffStart+0, preserves chord-with-open) when any group contains both open and non-open,
    • else forceOpen-SysEx fallback (diffStart+1 + SysEx, collapses chords with open but safer next to animations at easy diffStart).

    Coalesces fret-note length with overlapping animations at the same tick:noteNumber via pre-computed seq slots so animation ordering survives re-parse.

  • Force modifier ranges (forceHopo / forceStrum / forceTap) via emitFretModifierRanges — inverts resolveFretModifiers: natural HOPO state is re-derived with the MIDI threshold (1 + res/3), and a modifier emits only when the resolved flag disagrees. forceHopo = diffStart+6 (5-fret) / +7 (GHL); forceStrum = +7 / +8; forceTap as SysEx.

  • reconstructModifierRanges: collapses per-tick hit sets into minimal contiguous ranges over noteTicksInOrder.

  • Instrument-wide emission: star power (MIDI 116), solo (MIDI 103), flex lanes (126/127 with per-difficulty LDS velocity), per-track text events, versus phrases (105/106), animations (with skip when the fret note above already emitted at the same tick:noteNumber), per-track unrecognizedMidiEvents.

  • [ENHANCED_OPENS] text event when that mode is active.

  • Helpers added: addSysExOnOff, reconstructModifierRanges, midiIsFretChord / midiIsSameFretNote / midiIsInFretNote.

Vocal tracks (PART VOCALS / HARM1-3) land in the follow-up PR.

Stack (bottom → top)

  1. Move hasLyrics/hasVocals/hasForcedNotes from ParsedChart to ScannedChart (state-derived) #98 Move hasLyrics/hasVocals/hasForcedNotes to ScannedChart
  2. 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. this PR writeMidiFile guitar

Test plan

  • 15 new test cases covering track naming, 5-fret + GHL note-number mapping, open-note encoding (both modes), force modifier emission (hopo/strum/tap + non-emission when natural matches), instrument-wide sections, and round-trip through parseChartAndIni for both 5-fret modifiers and GHL chord-with-open.
  • 53 midi-writer tests pass; 397 total scan-chart tests green at the tip.

Builds a minimal valid ParsedChart from scratch: default 480 resolution,
120 BPM at tick 0, 4/4 time signature at tick 0, empty tracks/sections/
metadata/vocal parts/unrecognized events. chartBytes defaults to an empty
Uint8Array (no source bytes), format defaults to 'chart', iniChartModifiers
to the library defaults.

Options let callers override resolution, bpm, timeSignature, and format.

Useful for programmatic chart generation (e.g. downstream code that builds
charts up from scratch rather than parsing source bytes).
Serializes IniMetadata (Partial<typeof defaultMetadata> + extraIniFields)
back to song.ini text with:
- [song] header
- Known fields emitted in the canonical defaultMetadata order
- Undefined values skipped
- Booleans as True/False (Clone Hero convention)
- extraIniFields appended in insertion order
- CRLF line endings

Derives its field order from Object.keys(defaultMetadata) rather than
hardcoding a separate FIELD_ORDER list, so new fields added to
defaultMetadata automatically participate in writing.

Tests exercise writeIniFile only via round-trip through parseChartAndIni,
per reviewer feedback: build a metadata object, write it, re-parse, and
assert on parsedChart.metadata. No assertions about the serialized ini
text itself (CRLF, field order, True/False formatting, quoting) — those
are implementation details.
…emission

Port of the non-instrument-track half of the chart writer into scan-chart:
- [Song] emits the subset of metadata the chart body supports (Name,
  Artist, Charter, Album, Genre, Year, Resolution, Offset, PreviewStart,
  Difficulty). Other fields live exclusively in song.ini.
- [SyncTrack] emits tempos as `B millibeats` and time signatures as
  `TS numerator [denominator-exponent]`, sorted by tick with TS before B
  at the same tick.
- [Events] emits section markers (wrapped as [section name] to survive
  the parser's greedy trailing-\] regex), end events, unrecognized global
  events (with bracket-stripping when source is .mid so round-trip to
  .chart emits naked text), and vocal phrase_start/phrase_end/lyric
  events from the normalized vocalTracks.parts.vocals.
- Unrecognized chart sections are re-emitted verbatim.

Instrument tracks ([ExpertSingle] etc.) land in a follow-up PR.
Completes writeChartFile with per-track section emission:

- [ExpertSingle], [HardDrums], etc. named from instrument + difficulty.
- Drum tracks: base notes (N 0..4 or 0..5 for 5-lane), cymbal markers
  (N 66/67/68) only in fourLanePro, accent (N 34..38) and ghost (N 40..44)
  markers matching the emitted note's eventType, one N 109 flam per group,
  double kick as N 32 only.
- 5-lane round-trip: greenDrum+cymbal emits as N 4 (orange) while plain
  greenDrum stays at N 5; blueDrums coinciding with greenDrum+cymbal emit
  as N 5 to keep the parser's drumType detection on fiveLane.
- Fret/GHL tracks: notes via the 5-fret and 6-fret maps, tap as N 6,
  forceUnnatural (N 5) when the note's hopo/strum flag disagrees with
  the natural-HOPO state.
- Star power S 2, activation lanes S 64, flex lanes S 65/66, versus
  phrases S 0/1, solo sections as E 'solo'/'soloend' (tick + length - 1
  for soloend to round-trip).
- Per-track text events (including disco flip mix markers).
- Disco-flip state transitions per difficulty from red/yellow drum
  disco/discoNoflip flags → 'mix <diff> drums0[d|dnoflip]' text events.
- Coda events: if no 'coda' in unrecognizedEvents, synthesize from
  drumFreestyleSections where isCoda=true.
- hasForcedNotes backstop: if hasForcedNotes is set but no forceUnnatural
  emitted, append N 5 0 at a vacant tick in the first fret track to
  preserve the flag on round-trip.
Port of the core MIDI writer infrastructure:
- writeMidiFile(chart) entry point: builds a Format-1 MIDI and returns
  Uint8Array via midi-file's writeMidi
- TEMPO TRACK: trackName + setTempo (from chart.tempos) + timeSignature
  (from chart.timeSignatures), all at their absolute ticks
- EVENTS track: trackName + section text events (unwrapped so YARG's
  NormalizeTextEvent doesn't strip names containing ]) + [end] events +
  global events (bracket-wrap when source is .chart so MIDI output
  follows convention) + [coda] derived from drumFreestyleSections when
  not already present in unrecognizedEvents
- Unrecognized MIDI tracks: verbatim pass-through with abs-tick →
  delta-tick conversion; duplicate track names suffixed to keep the
  internal trackMap unique
- finalizeMidiTrack helper: sorts by tick with a type-priority
  tiebreaker, converts absolute ticks to delta times, appends endOfTrack

Instrument tracks (PART DRUMS, PART GUITAR, …) and vocal tracks
(PART VOCALS, HARM1/2/3) land in follow-up PRs.
Adds drum emission to writeMidiFile:

- buildDrumTrack: groups one or more ParsedTrackData entries (one per
  difficulty) into a single PART DRUMS MIDI track. Emits trackName,
  delegates per-difficulty notes to emitDrumNotes, collects and dedupes
  instrument-wide star power (MIDI 116) / solo (MIDI 103) / activation
  lanes (MIDI 120), emits flex lanes (MIDI 126/127 with per-difficulty
  LDS velocity), passes through per-track text events and
  unrecognizedMidiEvents from the first difficulty.

- emitDrumNotes: per-difficulty note emission with full modifier support:
  base drum notes (MIDI 96..100 expert / 84..88 hard / 72..76 medium /
  60..64 easy); double-kick as MIDI 95 only, emitted AFTER regular kick
  at the same tick (YARG insertion order quirk); tom markers (MIDI
  110/111/112) only in fourLanePro; accent (velocity 127) / ghost
  (velocity 1) encoding; one flam marker (MIDI 109) per group; 5-lane
  green+cymbal remapped to offset 4 (orange pad); blue-at-same-tick-as-
  green+cymbal remapped to offset 5 so fiveLane detection succeeds;
  fourLanePro green+tom fallback to offset 5 when a conflicting
  green+cymbal exists at the same tick across difficulties; disco-flip
  state-transition text events (`[mix <diff> drums0[d|dnoflip]]`).

- fourLanePro sentinel greenTomMarker: when drumType=1 but no tom
  markers were emitted (all yellow/blue defaulted to cymbal, green-toms
  used offset-5 encoding), emit a MIDI 112 at a safe tick so scan-chart's
  drumType detection still picks fourLanePro.

- [ENABLE_CHART_DYNAMICS] text event at tick 0 when any accent or ghost
  was emitted.

- computeLengthOverrides: prevents scan-chart's trimSustains from
  collapsing adjacent short-sustain drum-note chains by attributing the
  combined chain length to the first note.

- Shared helpers: addNoteOnOff, addNoteOnOffWithChannel (with
  zero-length seq-pairing to survive finalizeMidiTrack's sort),
  deduplicateSections, instrumentTrackNames table.

Fret tracks (PART GUITAR, GHL) and vocal tracks (PART VOCALS, HARM1/2/3)
remain no-ops until the follow-up PRs land — the writeMidiFile entry
iterates trackData but skips any non-drum instrument group.
@elicwhite
elicwhite force-pushed the midi-writer-guitar branch from 21b132d to f4cca25 Compare April 21, 2026 23:18
Adds fret-instrument emission to writeMidiFile.

- buildFretTrack: one MIDI track per instrument group (guitar / bass /
  guitarghl / bassghl / keys / guitarcoop / rhythm / GHL variants);
  multiple difficulties merge into the same track.
- emitFretNotes: 5-fret (95/83/71/59 diffStarts) and 6-fret/GHL
  (94/82/70/58) note-number mapping; open notes routed two ways:
  ENHANCED_OPENS mode (diffStart+0, preserves chord-with-open) when any
  group contains both open and non-open, else forceOpen-SysEx fallback
  (diffStart+1 + SysEx, collapses chords with open but safer next to
  animations at easy diffStart). Coalesces fret-note length with
  overlapping animations at the same tick:noteNumber via pre-computed
  seq slots so animation ordering survives re-parse.
- Force modifier ranges (forceHopo / forceStrum / forceTap) via
  emitFretModifierRanges — inverts resolveFretModifiers: natural HOPO
  state is re-derived with the MIDI threshold (1 + res/3), and a modifier
  emits only when the resolved flag disagrees. forceHopo = diffStart+6
  (5-fret) / +7 (GHL); forceStrum = +7 / +8; forceTap as SysEx.
- reconstructModifierRanges: collapses per-tick hit sets into minimal
  contiguous ranges over noteTicksInOrder.
- Instrument-wide emission: star power (MIDI 116), solo (MIDI 103),
  flex lanes (126/127 with per-difficulty LDS velocity), per-track text
  events, versus phrases (105/106), animations (with skip when the fret
  note above already emitted at the same tick:noteNumber), per-track
  unrecognizedMidiEvents.
- [ENHANCED_OPENS] text event when that mode is active.
- Helpers added: addSysExOnOff, reconstructModifierRanges,
  midiIsFretChord / midiIsSameFretNote / midiIsInFretNote.

Vocal tracks (PART VOCALS / HARM1-3) land in the follow-up PR.

Tests: 15 new test cases covering track naming, 5-fret + GHL
note-number mapping, open-note encoding (both modes), force modifier
emission (hopo/strum/tap + non-emission when natural matches),
instrument-wide sections, and round-trip through parseChartAndIni for
both 5-fret modifiers and GHL chord-with-open. 53 midi-writer tests,
397 total scan-chart tests passing at the tip.
@elicwhite
elicwhite force-pushed the midi-writer-guitar branch from f4cca25 to 940eeea Compare April 22, 2026 16:34
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.

1 participant