From 727fd97f39e40d608a72769825f673ec5c804c26 Mon Sep 17 00:00:00 2001 From: Gordon Weeks <627684+gcweeks@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:49:27 -0800 Subject: [PATCH 1/2] Songs now only play specific tracks from the MIDI, and instruments are ignored (each player still has their own instrument) --- src/entities/song_catalog.h | 22 ++++++--- src/main.cpp | 5 +- src/samples/ghhb_game.h | 99 ++++++++++++++++++++++++------------- src/samples/song_select.h | 12 +++-- 4 files changed, 93 insertions(+), 45 deletions(-) diff --git a/src/entities/song_catalog.h b/src/entities/song_catalog.h index 07c2164..a89d18b 100644 --- a/src/entities/song_catalog.h +++ b/src/entities/song_catalog.h @@ -1,20 +1,30 @@ #pragma once #include +#include #include -inline std::vector get_song_catalog() +/** -1 = pick track by most notes; 0-based index = use that track for the chart. */ +using SongCatalogEntry = std::pair; + +inline std::vector get_song_catalog() { return { - "assets/songs/json/mary.json", - "assets/songs/json/pallettown.json", - "assets/songs/json/tetris.json", - "assets/songs/json/undertale.json", + {"assets/songs/json/mary.json", -1}, + {"assets/songs/json/pallettown.json", -1}, + {"assets/songs/json/tetris.json", -1}, + {"assets/songs/json/undertale.json", 0}, }; } inline std::string get_default_song_path() { auto catalog = get_song_catalog(); - return catalog.empty() ? "" : catalog.front(); + return catalog.empty() ? "" : catalog.front().first; +} + +inline int get_default_track_override() +{ + auto catalog = get_song_catalog(); + return catalog.empty() ? -1 : catalog.front().second; } diff --git a/src/main.cpp b/src/main.cpp index 6ed309a..8d2279c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1}; int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1}; std::string SELECTED_SONG_PATH = get_default_song_path(); +int SELECTED_TRACK_OVERRIDE = get_default_track_override(); Game game; @@ -42,9 +43,9 @@ int main(int argc, char** argv) game.add_scene("ghhb"); auto catalog = get_song_catalog(); - for (const auto& path : catalog) + for (const auto& entry : catalog) { - song_manager->load_song(path, path); + song_manager->load_song(entry.first, entry.first); } std::string default_path = get_default_song_path(); Song& song = song_manager->get_song(default_path); diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h index 76b12b9..0a87fab 100644 --- a/src/samples/ghhb_game.h +++ b/src/samples/ghhb_game.h @@ -147,50 +147,80 @@ struct Glyph } }; -std::vector chart_from_song(const Song& song) +static size_t pick_track_by_note_count(const Song& song) +{ + size_t best = 0; + size_t best_count = 0; + for (size_t i = 0; i < song.tracks.size(); i++) + { + size_t n = song.tracks[i].notes.size(); + if (n > best_count) + { + best_count = n; + best = i; + } + } + return best; +} + +std::vector chart_from_song(const Song& song, int track_override) { std::vector glyphs; + if (song.tracks.empty()) + return glyphs; + + size_t track_idx; + if (track_override >= 0 && + static_cast(track_override) < song.tracks.size()) + { + track_idx = static_cast(track_override); + } + else + { + track_idx = pick_track_by_note_count(song); + } + const Track& track = song.tracks[track_idx]; int ppq = song.header.ppq > 0 ? song.header.ppq : 480; float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f; float ticks_per_sec = ppq * (bpm / 60.0f); - std::vector> track_note_counts; - for (size_t i = 0; i < song.tracks.size(); i++) - track_note_counts.push_back({i, song.tracks[i].notes.size()}); - - std::sort(track_note_counts.begin(), track_note_counts.end(), - [](const auto& a, const auto& b) { return a.second > b.second; }); - - size_t n_used = std::min(static_cast(MAX_INSTRUMENT_TYPES), track_note_counts.size()); - for (size_t slot = 0; slot < n_used; slot++) + std::vector> timed_notes; + for (const Note& note : track.notes) { - size_t track_idx = track_note_counts[slot].first; - size_t note_count = track_note_counts[slot].second; - const Track& track = song.tracks[track_idx]; - std::printf("Instrument %zu: \"%s\" (family=%s number=%d) %zu notes\n", - slot, track.name.c_str(), track.instrument.family.c_str(), - track.instrument.number, note_count); - int instrument_slot = static_cast(slot); - for (const Note& note : track.notes) - { - if (note.midi < 0 || note.midi > 127) - continue; - float time_sec = note.ticks / ticks_per_sec; - int lane = note.midi % LANE_COUNT; - int octave = note.midi % (LANE_COUNT * OCTAVE_COUNT); - glyphs.push_back(Glyph{time_sec, lane, instrument_slot, octave}); - } + if (note.midi >= 0 && note.midi <= 127) + timed_notes.push_back({note.ticks, ¬e}); } - std::sort(glyphs.begin(), glyphs.end(), - [](const Glyph& a, const Glyph& b) { - if (a.time != b.time) - return a.time < b.time; - return a.lane < b.lane; + std::sort(timed_notes.begin(), timed_notes.end(), + [](const auto& a, const auto& b) { + if (a.first != b.first) + return a.first < b.first; + return a.second->midi < b.second->midi; }); + + int note_index = 0; + for (const auto& pair : timed_notes) + { + const Note& note = *pair.second; + float time_sec = note.ticks / ticks_per_sec; + int lane = note.midi % LANE_COUNT; + int octave = note.midi % (LANE_COUNT * OCTAVE_COUNT); + int instrument_slot = note_index % MAX_INSTRUMENT_TYPES; + glyphs.push_back(Glyph{time_sec, lane, instrument_slot, octave}); + std::printf("glyph track=%zu time=%.2f lane=%d slot=%d\n", + track_idx, time_sec, lane, instrument_slot); + note_index++; + } + + std::printf("Chart: single track \"%s\" (%zu notes), round-robin player assignment%s\n", + track.name.c_str(), glyphs.size(), + (track_override >= 0 && + static_cast(track_override) < song.tracks.size()) + ? " [track override]" + : ""); return glyphs; } -std::vector load_chart(const char* path) +std::vector load_chart(const char* path, int track_override) { std::vector empty; std::FILE* fp = std::fopen(path, "rb"); @@ -203,7 +233,7 @@ std::vector load_chart(const char* path) { Song song = parseSong(doc); std::fclose(fp); - return chart_from_song(song); + return chart_from_song(song, track_override); } std::fclose(fp); return empty; @@ -212,6 +242,7 @@ std::vector load_chart(const char* path) } // namespace extern std::string SELECTED_SONG_PATH; +extern int SELECTED_TRACK_OVERRIDE; extern int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES]; extern int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES]; @@ -247,7 +278,7 @@ public: void on_enter() override { - chart = load_chart(SELECTED_SONG_PATH.c_str()); + chart = load_chart(SELECTED_SONG_PATH.c_str(), SELECTED_TRACK_OVERRIDE); float first_note_time = 0.0f; if (!chart.empty()) { diff --git a/src/samples/song_select.h b/src/samples/song_select.h index 828bbef..ae8663b 100644 --- a/src/samples/song_select.h +++ b/src/samples/song_select.h @@ -8,19 +8,23 @@ #include extern std::string SELECTED_SONG_PATH; +extern int SELECTED_TRACK_OVERRIDE; struct SongEntry { std::string name; std::string artist; std::string path; + int track_override; }; -inline std::vector build_song_list(const std::vector& paths) +inline std::vector build_song_list(const std::vector& catalog) { std::vector entries; - for (const std::string& path : paths) + for (const auto& entry : catalog) { + const std::string& path = entry.first; + int track_override = entry.second; std::FILE* fp = std::fopen(path.c_str(), "rb"); if (!fp) continue; @@ -35,7 +39,8 @@ inline std::vector build_song_list(const std::vector& pa std::fclose(fp); Song song = parseSong(doc); entries.push_back( - {song.header.name.empty() ? path : song.header.name, song.header.artist, path}); + {song.header.name.empty() ? path : song.header.name, song.header.artist, path, + track_override}); } while (entries.size() < 4) { @@ -116,6 +121,7 @@ public: if (idx >= 0 && idx < static_cast(songs.size())) { SELECTED_SONG_PATH = songs[idx].path; + SELECTED_TRACK_OVERRIDE = songs[idx].track_override; game->go_to_scene("instrument_select"); } } From a0fab24f28b129e7c41af1c7c2c39a91ded6b3f2 Mon Sep 17 00:00:00 2001 From: Gordon Weeks <627684+gcweeks@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:58:12 -0800 Subject: [PATCH 2/2] Synchronized hit-note WAV playing timing --- src/samples/ghhb_game.h | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h index 0a87fab..30f2c13 100644 --- a/src/samples/ghhb_game.h +++ b/src/samples/ghhb_game.h @@ -147,6 +147,13 @@ struct Glyph } }; +struct PendingSound +{ + float play_time = 0.0f; + int instrument_slot = 0; + int octave = 0; +}; + static size_t pick_track_by_note_count(const Song& song) { size_t best = 0; @@ -255,6 +262,7 @@ public: std::vector chart; std::vector spawned; std::unordered_set completed_notes; + std::vector pending_sounds; float song_time = 0.0f; float chart_time_offset = 0.0f; int score = 0; @@ -293,6 +301,7 @@ public: dev_auto_hit_mode = false; spawned.clear(); completed_notes.clear(); + pending_sounds.clear(); for (int i = 0; i < LANE_COUNT; i++) { press_flash_timer[i] = 0.0f; @@ -446,9 +455,9 @@ public: hit_flash_timer[n->lane] = PRESS_FLASH_DURATION; spawned.erase(it); completed_notes.insert(n); - printf("note lane: %d, note octave: %d\n", n->lane, n->octave); + float hit_line_time = n->time + chart_time_offset; if (note_sounds_loaded[n->instrument_slot][n->octave]) - PlaySound(note_sounds[n->instrument_slot][n->octave]); + pending_sounds.push_back({hit_line_time, n->instrument_slot, n->octave}); float y_n = glyph_y(*n); for (auto it2 = spawned.begin(); it2 != spawned.end();) { @@ -457,6 +466,9 @@ public: fabsf(glyph_y(*other) - y_n) <= SIMULTANEOUS_NOTE_Y_TOLERANCE) { completed_notes.insert(other); + float other_hit_line_time = other->time + chart_time_offset; + if (note_sounds_loaded[other->instrument_slot][other->octave]) + pending_sounds.push_back({other_hit_line_time, other->instrument_slot, other->octave}); it2 = spawned.erase(it2); } else @@ -504,6 +516,19 @@ public: song_time += delta_time; } + for (auto it = pending_sounds.begin(); it != pending_sounds.end();) + { + if (song_time >= it->play_time) + { + PlaySound(note_sounds[it->instrument_slot][it->octave]); + it = pending_sounds.erase(it); + } + else + { + ++it; + } + } + float last_note_time = 0.0f; for (const auto& n : chart) {