diff --git a/src/entities/song.h b/src/entities/song.h index ac4826a..89c0697 100644 --- a/src/entities/song.h +++ b/src/entities/song.h @@ -49,7 +49,6 @@ Song parseSong(const rapidjson::Document& doc) { header.name = h.HasMember("name") && h["name"].IsString() ? h["name"].GetString() : ""; header.artist = h.HasMember("artist") && h["artist"].IsString() ? h["artist"].GetString() : ""; header.ppq = h["ppq"].GetInt(); - printf("header has tempos: %d\n", h.HasMember("tempos")); if (h.HasMember("tempos") && h["tempos"].IsArray()) { const auto& tempos = h["tempos"].GetArray(); header.bpm = tempos[0]["bpm"].GetFloat(); 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..ea64f4c 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,18 +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); - } - std::string default_path = get_default_song_path(); - Song& song = song_manager->get_song(default_path); - if (!default_path.empty() && song_manager->has_song(default_path)) - { - printf("Song name: %s\n", song.header.name.c_str()); - printf("Song bpm: %f\n", song.header.bpm); - if (!song.tracks.empty() && !song.tracks[0].notes.empty()) - printf("First note duration: %d\n", song.tracks[0].notes[0].duration_ms); + song_manager->load_song(entry.first, entry.first); } diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h index 8ced42b..af02451 100644 --- a/src/samples/ghhb_game.h +++ b/src/samples/ghhb_game.h @@ -23,6 +23,8 @@ constexpr float HIT_ZONE_MARGIN = 20.0f; constexpr float SIMULTANEOUS_NOTE_Y_TOLERANCE = 2.0f; constexpr float SCROLL_PX_PER_SEC = 350.0f; constexpr float LEAD_OFFSET_SECONDS = 3.0f; +constexpr float GLYPH_HEIGHT_FRACTION_OF_LANE = 0.5f; +constexpr float MIN_SUSTAIN_FALLBACK_SEC = 0.05f; const int GAMEPAD_BUTTONS[LANE_COUNT] = { GAMEPAD_BUTTON_LEFT_FACE_LEFT, // Left @@ -138,6 +140,7 @@ const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_ struct Glyph { float time = 0.0f; + float duration_sec = 0.0f; int lane = 0; int instrument_slot = 0; int octave = 0; @@ -148,50 +151,89 @@ struct Glyph } }; -std::vector chart_from_song(const Song& song) +struct PendingSound +{ + float play_time = 0.0f; + float duration_sec = 0.0f; + int lane = 0; + int instrument_slot = 0; + int octave = 0; +}; + +struct ActiveSustainedSound +{ + float end_time = 0.0f; + int lane = 0; + int instrument_slot = 0; +}; + +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; + float duration_sec = note.duration_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, duration_sec, lane, instrument_slot, octave}); + note_index++; + } + 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"); @@ -204,7 +246,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; @@ -213,6 +255,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]; @@ -225,6 +268,8 @@ public: std::vector chart; std::vector spawned; std::unordered_set completed_notes; + std::vector pending_sounds; + std::vector active_sustained; float song_time = 0.0f; float chart_time_offset = 0.0f; int score = 0; @@ -249,7 +294,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()) { @@ -264,6 +309,8 @@ public: dev_auto_hit_mode = false; spawned.clear(); completed_notes.clear(); + pending_sounds.clear(); + active_sustained.clear(); for (int i = 0; i < LANE_COUNT; i++) { press_flash_timer[i] = 0.0f; @@ -354,21 +401,40 @@ public: return false; } + bool is_lane_held_by_instrument(int lane, int instrument_slot) const + { + int physical_id = INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot]; + if (physical_id < 0) + { + if (IsKeyDown(KEY_KEYS[lane])) + return true; + for (int i = 0; i < MAX_GAMEPADS; i++) + { + if (IsGamepadAvailable(i) && IsGamepadButtonDown(i, GAMEPAD_BUTTONS[lane])) + return true; + } + return false; + } + return IsGamepadAvailable(physical_id) && + IsGamepadButtonDown(physical_id, GAMEPAD_BUTTONS[lane]); + } + void stop_playing_released_notes(int lane) { - for (int i = 0; i < MAX_GAMEPADS; i++) + for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++) { - if (IsGamepadAvailable(i) && IsGamepadButtonDown(i, GAMEPAD_BUTTONS[lane])) - { - printf("Button held: [%d][%d]\n", lane, i); + if (is_lane_held_by_instrument(lane, slot)) continue; - } - - if (!note_sounds_playing[lane][i].empty()) + if (!note_sounds_playing[lane][slot].empty()) { - printf("Stopping sound: [%d][%d]\n", lane, i); - StopSound(note_sounds_playing[lane][i].front()); - note_sounds_playing[lane][i].pop_front(); + StopSound(note_sounds_playing[lane][slot].front()); + note_sounds_playing[lane][slot].pop_front(); + auto it = std::find_if(active_sustained.begin(), active_sustained.end(), + [lane, slot](const ActiveSustainedSound& a) { + return a.lane == lane && a.instrument_slot == slot; + }); + if (it != active_sustained.end()) + active_sustained.erase(it); } } } @@ -436,12 +502,10 @@ 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); - if (note_sounds_loaded[n->instrument_slot][n->octave]) { - Sound sound = note_sounds[n->instrument_slot][n->octave]; - PlaySound(sound); - note_sounds_playing[n->lane][n->instrument_slot].push_back(sound); - } + float hit_line_time = n->time + chart_time_offset; + if (note_sounds_loaded[n->instrument_slot][n->octave]) + pending_sounds.push_back( + {hit_line_time, n->duration_sec, n->lane, n->instrument_slot, n->octave}); float y_n = glyph_y(*n); for (auto it2 = spawned.begin(); it2 != spawned.end();) { @@ -450,6 +514,10 @@ 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->duration_sec, + other->lane, other->instrument_slot, other->octave}); it2 = spawned.erase(it2); } else @@ -497,6 +565,44 @@ public: song_time += delta_time; } + float glyph_height_px = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE; + float time_per_glyph_height = + glyph_height_px > 0.f ? glyph_height_px / SCROLL_PX_PER_SEC : MIN_SUSTAIN_FALLBACK_SEC; + for (auto it = pending_sounds.begin(); it != pending_sounds.end();) + { + if (song_time >= it->play_time) + { + Sound s = note_sounds[it->instrument_slot][it->octave]; + PlaySound(s); + note_sounds_playing[it->lane][it->instrument_slot].push_back(s); + float sustain_sec = std::max(it->duration_sec, time_per_glyph_height); + active_sustained.push_back( + {it->play_time + sustain_sec, it->lane, it->instrument_slot}); + it = pending_sounds.erase(it); + } + else + { + ++it; + } + } + + for (auto it = active_sustained.begin(); it != active_sustained.end();) + { + if (song_time >= it->end_time) + { + if (!note_sounds_playing[it->lane][it->instrument_slot].empty()) + { + StopSound(note_sounds_playing[it->lane][it->instrument_slot].front()); + note_sounds_playing[it->lane][it->instrument_slot].pop_front(); + } + it = active_sustained.erase(it); + } + else + { + ++it; + } + } + float last_note_time = 0.0f; for (const auto& n : chart) { @@ -738,7 +844,7 @@ public: } } float slice_width = lane_width / static_cast(instrument_count); - float glyph_height = lane_width / 2.0f; + float glyph_height = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE; int column = 0; for (size_t g = 0; g < group.size();) { 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"); } }