Playnote Progress Report #1

Playnote Progress Report #1

Whew, has it really been half a year? Back then I didn’t expect I would still be working on it after all this time, let alone see it become a Serious Project. But, I’m getting ahead of myself. This is the first Progress Report, so proper introductions are in order.

The story so far

Skip this section if you’re familiar with BMS. If you’re not – it’s a rhythm game that looks roughly like this:

DP BMS gameplay in beatoraja, by yours truly

Rectangles are falling, hit the lane’s assigned key to play the missing sounds in the music and complete the song. The gameplay is the same as beatmania IIDX, a commercial arcade rhythm game by Konami’s Bemani division; BMS is the fan-made version, with all music and charts made by members of the community (yes, all the music is original!) The video shows the 14-key mode (Double Play), my personal preference, but 7-key and others are available as well and are arguably far more popular.

Many clients were made that all play charts in the BMS format, but probably the most legendary of them all was Lunatic Rave 2 by lavalse. It was built largely to resemble and replicate the functionality of the PS2 releases of IIDX, and enjoyed great popularity due to its level of polish and performance. During its time, the BMS community grew by orders of magnitude. Sadly, the game’s development was cut short in 2010, allegedly due the developer losing access to the source code. By 2015, the official website went offline. Projects exist to add features and fix bugs in the most recent version, but since all they have is a compiled executable, it takes painstaking decompilation effort and making larger changes is difficult.

A screenshot of Lunatic Rave 2 song select screen
LR2 song select, in all its 640×480 glory

LR2 was followed up by beatoraja, a next-gen BMS client by exch. Written in Java and hosted on GitHub, it expands on LR2’s feature set, adding higher resolutions, more QoL and better stability. While still clunky in some aspects, it’s an incredibly impressive effort, and the best we have right now. The language barrier proved to make it difficult to contribute improvements to the original repository, so western development has continued in forks like Endless Dream.

Motivation

One fine afternoon, I was setting up beatoraja on a new Linux install. Right away, it took half an hour just to get it to start, since I didn’t have Java installed, let alone the required JavaFX library runtime. Importing my massive BMS library took hours, and then… none of the songs were actually imported. The game didn’t tell me why. All I could do was split the library into multiple groups, then try importing each one. Most of them worked, a few didn’t. A few more bisects later, and I identified the three songs that broke the import process if they were present. It was already evening, and I didn’t get to play that day. The next day, I discovered that the game has more latency on Linux, due to the bindings of PortAudio it was using. It also dropped inputs when both Shifts were pressed on the keyboard at the same time, which is apparently an inherent limitation of LWJGL.

I walked away from this experience feeling that we deserve better than this. LR2 is great but dead and buried, and beatoraja is opensource but held back by the choice of programming language and its own architecture. If I wanted my perfect BMS client, I was going to have to do it myself.

A Windows folder properties window, showing a large BMS song library
6.8 million tiny files, 27 gigabytes’ worth of sector padding

I spent the next few months toying with the idea. Why do we need to store each song as folders of 2000 files that waste disk space and take ages to copy, even on an SSD? Why does updating the library mean I don’t get to play until it’s finished? Why does the game only feel responsive at ~500fps, even though my screen is 144 Hz? Why can’t I play against a friend locally with each of us picking a different difficulty of the same song? None of these problems are inherent limitations of the BMS format, they just need someone to go out there and solve them. I’ve already been walking around with half-designed solutions in my head, and I was confident I’d be able to do it if I put my mind to it.

Finally, the time was right. I lost my job, and needed something to pass the time while waiting for a new one to come along. I pushed the initial commit, and Playnote was born. Soon after, my favorite C++ IDE became free for non-commercial use, further bolstering my motivation. I decided on a goal of making at least one commit per day, no matter how small, which was a great idea in retrospect – it’s really satisfying to watch that daily streak number go up.

Progress

Fast-forward to the present – what do we have so far? Well, the game looks like this:

Screenshot of Playnote in its current state
Don’t judge, the graphics are temporary

It’s being developed in C++23 for Windows and Linux, with a custom audio engine and Vulkan for graphics. Currently, it can load one BMS chart at a time via the command line, and allows you to play it yourself (with a keyboard or a controller) or watch a perfect autoplay. Most charts work, though there are edge cases, and a few uncommon BMS features, like mines and control flow, aren’t yet supported.

The interesting parts, and the most difficult ones, are under the hood. The total input-to-keysound latency is 6ms on Linux and 8ms on Windows (2 or 3ms of audio buffer, +1ms limiter look-ahead, times two for round-trip latency.) The game is heavily threaded, inputs always polled at the highest possible rate, and engine ticks completely independent from the renderer. And it loads songs directly from archives!

There are a few smaller things it already does that no other BMS client has done so far:

  • All songs play at the same volume by analyzing them with a ReplayGain-like algorithm.
  • A heuristic detects the BMS file’s text encoding, fixing mojibake in both Japanese (Shift-JIS) and Korean (EUC-KR) charts with no need to mess with system locale.
  • Note density graph is a sum of Gaussians, and the average notes-per-second value uses a root-mean-square based algorithm to be a more useful measure of overall chart density than an arithmetic mean of time-bins.
Screenshot of Playnote note density graph
Blue line shows average NPS, purple shows peak NPS

The future

The next milestone, version 0.0.4, focuses on the chart database and song import process. In other words, it’ll be the first version that’s usable without messing with the command line.

Stay tuned for the next Progress Report! Between them, I might also post some deep-dives into the design process, and stories from development.

If you are interested in giving Playnote a try, you can find the repository here, and the automated Windows build is available in the latest GitHub Actions artifact.