Fixed overlapping glyph bug. Reverted mary.json to single track.

This commit is contained in:
Gordon Weeks 2026-01-31 16:30:35 -08:00
parent 7654d1323c
commit c370dbef9a
3 changed files with 393 additions and 141 deletions

View File

@ -26,83 +26,246 @@
{
"channel": 0,
"controlChanges": {
"7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}]
"1": [
{
"number": 1,
"ticks": 0,
"time": 0,
"value": 0
}
],
"6": [
{
"number": 6,
"ticks": 0,
"time": 0,
"value": 0.5039370078740157
},
{
"number": 6,
"ticks": 0,
"time": 0,
"value": 0.5039370078740157
},
{
"number": 6,
"ticks": 0,
"time": 0,
"value": 0.09448818897637795
}
],
"7": [
{
"number": 7,
"ticks": 0,
"time": 0,
"value": 0.7874015748031497
}
],
"10": [
{
"number": 10,
"ticks": 0,
"time": 0,
"value": 0.5039370078740157
}
],
"11": [
{
"number": 11,
"ticks": 0,
"time": 0,
"value": 1
}
],
"38": [
{
"number": 38,
"ticks": 0,
"time": 0,
"value": 0
}
],
"100": [
{
"number": 100,
"ticks": 0,
"time": 0,
"value": 0.015748031496062992
},
{
"number": 100,
"ticks": 0,
"time": 0,
"value": 0.007874015748031496
},
{
"number": 100,
"ticks": 0,
"time": 0,
"value": 0
}
],
"101": [
{
"number": 101,
"ticks": 0,
"time": 0,
"value": 0
},
{
"number": 101,
"ticks": 0,
"time": 0,
"value": 0
},
{
"number": 101,
"ticks": 0,
"time": 0,
"value": 0
}
],
"121": [
{
"number": 121,
"ticks": 0,
"time": 0,
"value": 0
}
]
},
"pitchBends": [{"ticks": 0, "time": 0, "value": 0}],
"instrument": {
"family": "pipe",
"number": 73,
"name": "flute"
},
"name": "Melody 1",
"notes": [
{"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 0, "time": 0, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 480, "time": 0.5, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497}
"pitchBends": [
{
"ticks": 0,
"time": 0,
"value": 0
}
],
"endOfTrackTicks": 1440
},
{
"channel": 1,
"controlChanges": {
"7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}]
},
"pitchBends": [{"ticks": 0, "time": 0, "value": 0}],
"instrument": {
"family": "strings",
"number": 40,
"name": "violin"
"family": "piano",
"number": 0,
"name": "acoustic grand piano"
},
"name": "Melody 2",
"name": "",
"notes": [
{"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 0, "time": 0, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 480, "time": 0.5, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 1440, "time": 1.5, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 1920, "time": 2, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 2400, "time": 2.5, "velocity": 0.7874015748031497},
{"duration": 1, "durationTicks": 960, "midi": 64, "name": "E4", "ticks": 2880, "time": 3, "velocity": 0.7874015748031497}
],
"endOfTrackTicks": 3840
},
{
"channel": 2,
"controlChanges": {
"7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}]
},
"pitchBends": [{"ticks": 0, "time": 0, "value": 0}],
"instrument": {
"family": "guitar",
"number": 24,
"name": "acoustic guitar (nylon)"
},
"name": "Melody 3",
"notes": [
{"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 480, "time": 0.5, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 3840, "time": 4, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 4320, "time": 4.5, "velocity": 0.7874015748031497},
{"duration": 1, "durationTicks": 960, "midi": 62, "name": "D4", "ticks": 4800, "time": 5, "velocity": 0.7874015748031497}
],
"endOfTrackTicks": 5760
},
{
"channel": 3,
"controlChanges": {
"7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}]
},
"pitchBends": [{"ticks": 0, "time": 0, "value": 0}],
"instrument": {
"family": "chromatic percussion",
"number": 9,
"name": "glockenspiel"
},
"name": "Melody 4",
"notes": [
{"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 5760, "time": 6, "velocity": 0.7874015748031497},
{"duration": 0.5, "durationTicks": 480, "midi": 67, "name": "G4", "ticks": 6240, "time": 6.5, "velocity": 0.7874015748031497},
{"duration": 1, "durationTicks": 960, "midi": 67, "name": "G4", "ticks": 6720, "time": 7, "velocity": 0.7874015748031497}
{
"duration": 0.5,
"durationTicks": 480,
"midi": 64,
"name": "E4",
"ticks": 0,
"time": 0,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 62,
"name": "D4",
"ticks": 480,
"time": 0.5,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 60,
"name": "C4",
"ticks": 960,
"time": 1,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 62,
"name": "D4",
"ticks": 1440,
"time": 1.5,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 64,
"name": "E4",
"ticks": 1920,
"time": 2,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 64,
"name": "E4",
"ticks": 2400,
"time": 2.5,
"velocity": 0.7874015748031497
},
{
"duration": 1,
"durationTicks": 960,
"midi": 64,
"name": "E4",
"ticks": 2880,
"time": 3,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 62,
"name": "D4",
"ticks": 3840,
"time": 4,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 62,
"name": "D4",
"ticks": 4320,
"time": 4.5,
"velocity": 0.7874015748031497
},
{
"duration": 1,
"durationTicks": 960,
"midi": 62,
"name": "D4",
"ticks": 4800,
"time": 5,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 64,
"name": "E4",
"ticks": 5760,
"time": 6,
"velocity": 0.7874015748031497
},
{
"duration": 0.5,
"durationTicks": 480,
"midi": 67,
"name": "G4",
"ticks": 6240,
"time": 6.5,
"velocity": 0.7874015748031497
},
{
"duration": 1,
"durationTicks": 960,
"midi": 67,
"name": "G4",
"ticks": 6720,
"time": 7,
"velocity": 0.7874015748031497
}
],
"endOfTrackTicks": 7680
}

View File

@ -10,9 +10,9 @@ using SongCatalogEntry = std::pair<std::string, int>;
inline std::vector<SongCatalogEntry> get_song_catalog()
{
return {
{"assets/songs/json/mary.json", -1},
{"assets/songs/json/pallettown.json", -1},
{"assets/songs/json/tetris.json", -1},
{"assets/songs/json/mary.json", 0},
{"assets/songs/json/pallettown.json", 0},
{"assets/songs/json/tetris.json", 0},
{"assets/songs/json/undertale.json", 0},
};
}

View File

@ -25,6 +25,7 @@ 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;
constexpr float MIN_GLYPH_DURATION_SEC = 0.05f;
const int GAMEPAD_BUTTONS[LANE_COUNT] = {
GAMEPAD_BUTTON_LEFT_FACE_LEFT, // Left
@ -139,12 +140,13 @@ const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_
struct Glyph
{
float time = 0.0f;
float duration_sec = 0.0f;
float time = 0.0f; // Time when the BOTTOM of the glyph hits the line (when note should be played)
float duration_sec = 0.0f; // Duration extends upward from bottom (top = time + duration)
int lane = 0;
int instrument_slot = 0;
int octave = 0;
// Returns Y position of the bottom of the glyph
float y_position(float song_time, float hit_line_y) const
{
return hit_line_y - (time - song_time) * SCROLL_PX_PER_SEC;
@ -162,6 +164,7 @@ struct PendingSound
struct ActiveSustainedSound
{
float start_time = 0.0f;
float end_time = 0.0f;
int lane = 0;
int instrument_slot = 0;
@ -221,12 +224,63 @@ std::vector<Glyph> chart_from_song(const Song& song, int track_override)
for (const auto& pair : timed_notes)
{
const Note& note = *pair.second;
float time_sec = note.ticks / ticks_per_sec;
float time_sec = note.ticks / ticks_per_sec; // This is now the BOTTOM time
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;
// Log original glyph timing (time is bottom, time + duration is top)
float new_bottom_time = time_sec;
float new_top_time = time_sec + duration_sec;
std::printf("[NEW GLYPH] lane=%d bottom=%.3f top=%.3f dur=%.3f\n",
lane, new_bottom_time, new_top_time, duration_sec);
Glyph* last_in_lane = nullptr;
for (size_t i = glyphs.size(); i-- > 0;)
{
if (glyphs[i].lane == lane)
{
last_in_lane = &glyphs[i];
break;
}
}
if (last_in_lane != nullptr)
{
// Check if existing glyph's top overlaps with new glyph's bottom
float existing_bottom = last_in_lane->time;
float existing_top = last_in_lane->time + last_in_lane->duration_sec;
std::printf(" [CHECK] existing lane=%d bottom=%.3f top=%.3f vs new_bottom=%.3f\n",
last_in_lane->lane, existing_bottom, existing_top, new_bottom_time);
if (existing_top > new_bottom_time)
{
// Truncate existing glyph so its top doesn't go past new glyph's bottom
float shortened_duration = new_bottom_time - existing_bottom;
std::printf(" [OVERLAP DETECTED] shortened_dur=%.3f (was %.3f)\n",
shortened_duration, last_in_lane->duration_sec);
if (shortened_duration < MIN_GLYPH_DURATION_SEC)
{
std::printf(" [SKIP] shortened duration %.3f < MIN %.3f, skipping new glyph\n",
shortened_duration, MIN_GLYPH_DURATION_SEC);
note_index++;
continue;
}
float old_duration = last_in_lane->duration_sec;
last_in_lane->duration_sec = shortened_duration;
std::printf(" [TRUNCATED] lane=%d: duration %.3f -> %.3f (new_top=%.3f)\n",
last_in_lane->lane, old_duration, shortened_duration,
last_in_lane->time + shortened_duration);
}
}
glyphs.push_back(Glyph{time_sec, duration_sec, lane, instrument_slot, octave});
std::printf(" [ADDED] glyph count now: %zu\n\n", glyphs.size());
note_index++;
}
@ -268,6 +322,7 @@ public:
std::vector<Glyph> chart;
std::vector<Glyph*> spawned;
std::unordered_set<Glyph*> completed_notes;
std::unordered_set<Glyph*> missed_notes;
std::vector<PendingSound> pending_sounds;
std::vector<ActiveSustainedSound> active_sustained;
float song_time = 0.0f;
@ -283,10 +338,8 @@ public:
bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] = {{false}};
std::deque<Sound> note_sounds_playing[LANE_COUNT][MAX_INSTRUMENT_TYPES];
static constexpr float PRESS_FLASH_DURATION = 0.12f;
static constexpr float MISS_FLASH_DURATION = 0.15f;
float press_flash_timer[LANE_COUNT] = {0};
float hit_flash_timer[LANE_COUNT] = {0};
float miss_flash_timer[LANE_COUNT] = {0};
bool game_ended = false;
bool dev_auto_hit_mode = false;
static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f;
@ -309,13 +362,13 @@ public:
dev_auto_hit_mode = false;
spawned.clear();
completed_notes.clear();
missed_notes.clear();
pending_sounds.clear();
active_sustained.clear();
for (int i = 0; i < LANE_COUNT; i++)
{
press_flash_timer[i] = 0.0f;
hit_flash_timer[i] = 0.0f;
miss_flash_timer[i] = 0.0f;
}
if (music_loaded)
{
@ -419,23 +472,26 @@ public:
IsGamepadButtonDown(physical_id, GAMEPAD_BUTTONS[lane]);
}
void stop_playing_released_notes(int lane)
void stop_playing_released_notes(int lane, float min_sustain_sec)
{
for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++)
{
if (is_lane_held_by_instrument(lane, slot))
continue;
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())
continue;
if (song_time < it->start_time + min_sustain_sec)
continue;
if (!note_sounds_playing[lane][slot].empty())
{
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);
}
active_sustained.erase(it);
}
}
@ -483,15 +539,24 @@ public:
return false;
}
float glyph_y(const Glyph& n) const
// Returns the Y position of the BOTTOM of the glyph
// n.time represents when the bottom should reach the hit line
float glyph_bottom_y(const Glyph& n) const
{
return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC;
}
static float glyph_height_px(const Glyph& n, float lane_width_val)
{
float min_height = lane_width_val * GLYPH_HEIGHT_FRACTION_OF_LANE;
float duration_height = n.duration_sec * SCROLL_PX_PER_SEC;
return std::max(min_height, duration_height);
}
bool is_note_hittable(const Glyph& n) const
{
float y = glyph_y(n);
return y >= upper_bar_y - HIT_ZONE_MARGIN && y <= hit_line_y + HIT_ZONE_MARGIN;
float bottom_y = glyph_bottom_y(n);
return bottom_y >= upper_bar_y - HIT_ZONE_MARGIN && bottom_y <= hit_line_y + HIT_ZONE_MARGIN;
}
void consume_note(Glyph* n)
@ -500,29 +565,27 @@ public:
if (it != spawned.end())
{
hit_flash_timer[n->lane] = PRESS_FLASH_DURATION;
spawned.erase(it);
completed_notes.insert(n);
float hit_line_time = n->time + chart_time_offset;
// n.time is when the bottom should hit the line
float bottom_hits_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();)
{bottom_hits_line_time, n->duration_sec, n->lane, n->instrument_slot, n->octave});
float bottom_y_n = glyph_bottom_y(*n);
for (Glyph* other : spawned)
{
Glyph* other = *it2;
if (other != n && other->lane == n->lane && other->instrument_slot == n->instrument_slot &&
fabsf(glyph_y(*other) - y_n) <= SIMULTANEOUS_NOTE_Y_TOLERANCE)
if (other == n)
continue;
if (other->lane != n->lane || other->instrument_slot != n->instrument_slot)
continue;
float bottom_y_other = glyph_bottom_y(*other);
if (fabsf(bottom_y_other - bottom_y_n) <= SIMULTANEOUS_NOTE_Y_TOLERANCE)
{
completed_notes.insert(other);
float other_hit_line_time = other->time + chart_time_offset;
float other_bottom_hits_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,
pending_sounds.push_back({other_bottom_hits_line_time, other->duration_sec,
other->lane, other->instrument_slot, other->octave});
it2 = spawned.erase(it2);
}
else
{
++it2;
}
}
}
@ -542,6 +605,20 @@ public:
void update(float delta_time) override
{
update_layout();
{
static bool debug_bottom_printed = false;
if (!debug_bottom_printed && !chart.empty() && lane_width > 0.0f)
{
for (const Glyph& g : chart)
{
float bottom_hits_line_time = g.time + chart_time_offset;
float top_hits_line_time = bottom_hits_line_time + g.duration_sec;
std::printf("[chart] glyph lane=%d bottom_time=%.3f top_time=%.3f dur=%.3f\n",
g.lane, bottom_hits_line_time, top_hits_line_time, g.duration_sec);
}
debug_bottom_printed = true;
}
}
if (is_select_pressed())
{
dev_auto_hit_mode = !dev_auto_hit_mode;
@ -565,9 +642,9 @@ public:
song_time += delta_time;
}
float glyph_height_px = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE;
float min_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;
min_glyph_height_px > 0.f ? min_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)
@ -577,7 +654,7 @@ public:
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->play_time, it->play_time + sustain_sec, it->lane, it->instrument_slot});
it = pending_sounds.erase(it);
}
else
@ -634,16 +711,29 @@ public:
}
}
for (Glyph* n : spawned)
{
if (completed_notes.count(n) != 0)
continue;
if (missed_notes.count(n) != 0)
continue;
float bottom_y = glyph_bottom_y(*n);
if (bottom_y > hit_line_y + HIT_ZONE_MARGIN)
{
missed_notes.insert(n);
combo = 0;
}
}
for (auto it = spawned.begin(); it != spawned.end();)
{
Glyph* n = *it;
float y = glyph_y(*n);
if (y > hit_line_y + HIT_ZONE_MARGIN)
float bottom_y = glyph_bottom_y(*n);
if (bottom_y > screen_height + 40.0f)
{
miss_flash_timer[n->lane] = MISS_FLASH_DURATION;
completed_notes.insert(n);
completed_notes.erase(n);
missed_notes.erase(n);
it = spawned.erase(it);
combo = 0;
}
else
{
@ -663,11 +753,6 @@ public:
{
hit_flash_timer[lane] = 0.0f;
}
miss_flash_timer[lane] -= delta_time;
if (miss_flash_timer[lane] < 0.0f)
{
miss_flash_timer[lane] = 0.0f;
}
}
if (dev_auto_hit_mode)
@ -675,6 +760,8 @@ public:
std::vector<Glyph*> to_consume;
for (Glyph* n : spawned)
{
if (completed_notes.count(n) != 0)
continue;
if (is_note_hittable(*n))
{
to_consume.push_back(n);
@ -689,7 +776,7 @@ public:
{
for (int lane = 0; lane < LANE_COUNT; lane++)
{
stop_playing_released_notes(lane);
stop_playing_released_notes(lane, time_per_glyph_height);
bool pressed = is_lane_pressed(lane);
if (pressed)
press_flash_timer[lane] = PRESS_FLASH_DURATION;
@ -713,8 +800,8 @@ public:
{
continue;
}
float y = glyph_y(*n);
float d = fabsf(y - hit_line_y);
float bottom_y = glyph_bottom_y(*n);
float d = fabsf(bottom_y - hit_line_y);
if (d < best_dist)
{
best_dist = d;
@ -781,15 +868,6 @@ public:
static_cast<int>(RECEPTOR_HEIGHT),
Color{255, 255, 255, static_cast<unsigned char>(alpha)});
}
if (miss_flash_timer[lane] > 0.0f)
{
float alpha = 200.0f * (miss_flash_timer[lane] / MISS_FLASH_DURATION);
DrawRectangle(static_cast<int>(lane * lane_width),
static_cast<int>(upper_bar_y),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{255, 80, 80, static_cast<unsigned char>(alpha)});
}
}
DrawLineEx(Vector2{0, upper_bar_y}, Vector2{screen_width, upper_bar_y}, 3.0f, WHITE);
DrawLineEx(Vector2{0, hit_line_y}, Vector2{screen_width, hit_line_y}, 3.0f, WHITE);
@ -806,11 +884,14 @@ public:
button_label_font_size, 1, Color{220, 220, 240, 255});
}
const Color MISSED_GLYPH_COLOR = {140, 140, 140, 255};
std::vector<std::vector<Glyph*>> by_lane(static_cast<size_t>(LANE_COUNT));
for (Glyph* n : spawned)
{
float y = glyph_y(*n);
if (y < -40.0f || y > screen_height + 40.0f)
float height = glyph_height_px(*n, lane_width);
float bottom = glyph_bottom_y(*n);
float top = bottom - height;
if (bottom < -40.0f || top > screen_height + 40.0f)
{
continue;
}
@ -819,16 +900,23 @@ public:
for (int lane = 0; lane < LANE_COUNT; lane++)
{
std::vector<Glyph*>& list = by_lane[static_cast<size_t>(lane)];
// Sort by top Y (which is bottom - height, i.e., earlier in time)
std::sort(list.begin(), list.end(),
[this](Glyph* a, Glyph* b) { return glyph_y(*a) < glyph_y(*b); });
[this](Glyph* a, Glyph* b) {
float top_a = glyph_bottom_y(*a) - glyph_height_px(*a, lane_width);
float top_b = glyph_bottom_y(*b) - glyph_height_px(*b, lane_width);
return top_a < top_b;
});
float left_base = lane * lane_width;
for (size_t i = 0; i < list.size();)
{
size_t j = i;
float y0 = glyph_y(*list[i]);
while (j < list.size() &&
glyph_y(*list[j]) - y0 <= SIMULTANEOUS_NOTE_Y_TOLERANCE)
float top_y0 = glyph_bottom_y(*list[i]) - glyph_height_px(*list[i], lane_width);
while (j < list.size())
{
float top_y_j = glyph_bottom_y(*list[j]) - glyph_height_px(*list[j], lane_width);
if (top_y_j - top_y0 > SIMULTANEOUS_NOTE_Y_TOLERANCE)
break;
j++;
}
std::vector<Glyph*> group(list.begin() + static_cast<std::ptrdiff_t>(i),
@ -844,16 +932,17 @@ public:
}
}
float slice_width = lane_width / static_cast<float>(instrument_count);
float glyph_height = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE;
int column = 0;
for (size_t g = 0; g < group.size();)
{
int slot = group[g]->instrument_slot;
float left = left_base + column * slice_width;
Glyph* n = group[g];
float y = glyph_y(*n);
float top = y - glyph_height / 2.0f;
Color fill = INSTRUMENT_COLORS[n->instrument_slot];
float glyph_height = glyph_height_px(*n, lane_width);
float bottom = glyph_bottom_y(*n);
float top = bottom - glyph_height;
bool missed = missed_notes.count(n) != 0;
Color fill = missed ? MISSED_GLYPH_COLOR : INSTRUMENT_COLORS[n->instrument_slot];
Color edge = Color{
static_cast<unsigned char>(std::min(255, fill.r + 35)),
static_cast<unsigned char>(std::min(255, fill.g + 50)),