The music as a clock
Earfh now knows where the beat is. Eleven new tracks got beat maps, the dash UI pulses on the kick, and dash charges come back on the beat. The hard part was keeping a map from landing on the wrong drum.
Written up later from the repo's history and dated to the days it describes.
I noticed that chaining low-cooldown dashes in Earfh felt almost on the beat of the music. Almost was the interesting part. I had also made eleven new tracks with Suno, using four saved styles built for this, with a kick on every beat and drums from the first bar. The plan was to make the game live on that beat.
It shipped in two parts over September 28 and 29, merged as PRs 55 and 56, with a follow-up fix in PR 57.
Beat maps, not a tempo
A measurement came first. The new tracks have strong beats, six to fourteen times the median onset strength, but they drift about 2 to 3 BPM within a song. A single tempo per track would slide off the beat before the song ended.
So each track gets a beat map, a list of the time of every beat in seconds, stored in a small JSON file beside the audio. A Python tool using librosa builds them. The game reads them through a new autoload called BeatClock.
BeatClock takes the music player's position, adds the time since the last audio mix, and subtracts the output latency, so a beat is placed on what the player hears and not what the engine has queued. It fires a signal once per beat. If the music jumps, because a track changed or a playlist advanced or a loop wrapped, it resets its place instead of firing a burst of old beats. A track with no map gives no beats and no errors.
The first use is small. The dash charge bar and the chain counter swell to 1.12 times their size on each beat and settle back over a tenth of a second. The pulse is size only, and Reduce Flashing turns it off.
The map that sat on the wrong drum
The first beat maps were wrong in a way that needed explaining. The tool built its onset envelope from the full band, and the busy hi-hats between the kicks pulled the beat tracker onto an eighth or sixteenth subdivision for long stretches. The pulse sat a quarter or half beat off the kick.
The fix was to listen only to the kick. The tool now builds the envelope from the 30 to 150 Hz band and tracks with a tight tempo, which keeps it from hopping phase where a kick drops out.
That needed a test, and the first one was not enough. The suite checks every map for phase slips, and each gap between beats has to sit within 15% of the median of its neighbors. But a map that sits a steady quarter or half beat off the kick for its whole length has perfectly even gaps. It passes. A Godot test cannot listen to audio either.
So the tool checks its own work. It samples the kick envelope on the beats and at a quarter, half and three quarters of the way to the next beat, and scores on-beat strength against the strongest off-beat position. A track that scores under 1.195 gets no map at all. The run names it, carries on, and exits with an error at the end. A second pass narrowed the scoring window and set that threshold from measurement. The tool's README is plain about what the score cannot see. If the kick band itself misleads the tracker and the score, it passes. Listening to a click track made from each map stays the real gate.
Part two: charges that return on the beat
Part one made the UI pulse, but the dash itself still ignored the music. A dash charge returns on its own timer, so the perfect moment in a dash chain drifted across the beat.
The idea was to keep the cooldown stat and its upgrades and snap each refill to the nearest beat. A perfect dash and a dash on the beat then become the same press. Early presses get a small grace. A dash pressed within 0.08 seconds before a charge returns is held and fires when it arrives, counting as perfect.
The first version snapped to whole beats. The final review showed that made players who dashed on the beat about 15% slower at 131 BPM, and that levels two to four of the Paid Time Off upgrade bought nothing. The shipped version snaps to the half-beat grid, the beats and the points between them, and refuses to snap when the nearest point is more than a quarter of a beat away. With no music or no map, the timing is exactly what it was.
What is not done
The spec calls for listening to all eleven click tracks and a smoke run before the feature counts as checked by ear. A latency offset setting is not built, since the plan is to add a slider only if the pulse proves early or late in play. Level lamps pulsing on the beat are not built either.