For this year Ruby Rampage (previously called Rails Rumble) hackathon I really wanted to build something using the new features of Rails 5. A few years ago I tried to build a multiplayer HTML 5 game in two days, but I had to use NodeJS… now was the time to see if ActionCable was any better! I also was curious about the new improvements of the Web Audio API ever since I’ve read articles about it.

I went ahead and after two days of work I managed to get a working site and deployed it on Heroku. You can try it out for yourself online or take a look at this quick demo video:





It only works on Chrome and runs on a free Heroku server, but it still handles a decent number of concurrent connections.

Building The Sequencer

Since it was a hackathon, I had to cut some corners in terms of test coverage and general code quality. However I was always aiming to build a stable enough project, meaning the site should handle gracefully any bug, timeout or server restart.

Action Cable & Websockets

Action Cable provides a simple way to deal with real-time features in a Rails application. It uses Redis a lot, so I had to use a free Redis Cloud instance. I also set up Puma since a threaded server is really the way to go when dealing with real-time.

Overall the experience was nicer than what I used to with NodeJS and in a few hours I had a somewhat working multiplayer snake “game” that looked terrible:

After a bit I realized that it was very hard to maintain the list of connected players and update it when they leave, get disconnected or just stop responding. It’s at this point that I started investing more time into getting a stable way of handling players, with a dedicated model and better integration with Redis. One challenge there was to link a request signature to a player, and make sure the link was properly persisted and updated at the right time.

I shared some Ruby code here if you’re interested - but keep in mind that it’s “hackathon code”, so don’t go and just use this :)

Broadcasting

An error I made in the past when building a simple real-time multiplayer HTML 5 game was passing around too much information. In this situation, any network access can be a real problem as any millisecond lost is felt in the UI.

This time I decided to only broadcast the changes and not the whole world, making the site way more responsive but complexifying the draw method. I also made sure to clean up what was not used anymore, mostly using the very useful EXPIRE method provided by Redis.

Here’s a simple example with the Note model, representing an active note in the sequencer:

And here is the very simplified version of the client side code handling broadcasts:

App . room = App . cable . subscriptions . create ({ channel : " RoomChannel " , from : " room.js " , }, received : function ( data ) { // Routing if ( data . type == " note " ){ App . music . addNote ( data . player \ _id , data . x , data . y )} })

Making Music Using Javascript

Once I had a somewhat working server connection, I had to get the client to actually produce sounds. The first version was very simple and used a sine wave to produce random bleeps and boops.

During the rest of the hackathon I kept on tweaking this until I got the sound and timings I wanted. I talked more about it in this article: Generate Sounds Programmatically With Javascript.

Overall, getting the different notes to sound good was the hardest part of this project, because there was the double challenge of understanding both the Web Audio API and picking notes that would fit with one another.

I ended up with 3 instruments plus a kick and a snare. Each instrument is based on a simple signal (sawtooth, square and triangle) that is modified to give a better sound. They are then given a full octave on complementary scales, meaning that even if people play totally random notes, it won’t be dissonant.

var highScale = [ " D6 " , " C6 " , " B5 " , " A5 " , " G5 " , " F#5 " , " E5 " , " D5 " ] var midScale = [ " F#4 " , " E4 " , " D4 " , " C4 " , " B3 " , " A3 " , " G3 " , " F#3 " ] var lowScale = [ " F#3 " , " E3 " , " D3 " , " C3 " , " B2 " , " A2 " , " G2 " , " F#2 " ]

Stability

Running a real-time service on a free Heroku server that restarts randomly is challenging, but doable. I tried different approaches and ended with a simple yet effective way of dealing with this as demonstrated in this screen recording.

Basically I would detect on the client side any sign that the server was poorly responding. If it was the case, the client would then retry and, if it failed again, the browser would simply reload the page after a delay. There was a couple of places in the flow where this could happen, so I had to handle all of them with custom messages to make the app feel responsive and stable. Of course all this is just an approximation for “real stability”, but from a user perspective it gave the impression that everything was running smoothly.

Later on I tried with more clients connected and more random restarts / dropped connections and it worked fine.

Designing

I decided to go for an old school design to match the 8 bits sound the project was producing. It was hard to get something that looked decent since design is really not my strong suit, but the result turned out ok in my opinon:

I also had fun designing a favicon and using palettes to pick colors that kind of matched together.

Playing

In the last half day of the hackathon I was mostly tweaking the sound and design to get it just the way I wanted it. It was also very enjoyable to see people try out the app!

Conclusion

Overall it was a very interesting project that was fun to build and is also fun to use every once in a while. When there is a lot of people playing it’s a real mess, but it was expected and is also very funny to watch.

Something I didn’t anticipate was that, while I love coding with music, for this project I absolutely couldn’t. Instead I spent a couple of days listening to beepings and sine waves. The worse was when I introduced bugs and a piercing noise would destroy my ears.

It’s also worth mentioning that I won the popularity prize of the hackathon, so I’d say the whole experience was a success!