The story of eightbitbeats.com: How we built a social 8bit music sequencer in 48hours

eightbitbeats.com

Node Knockout 2011, held between August 27 and 29, was a programming competition in which teams of developers were charged with designing, building, and launching a web app in a single weekend. The only basic requirements were that 1) each entry must use node.js, a javascript-based web server, and 2) that all designing, coding, tweaking, and deploying be done in the 48 hour period allowed for the contest.

Our team, made up of four developers from Mahalo.com, had not competed in last year's Node Knockout, but we were excited and impressed by the entries we saw, and we decided we had to enter this time around. We tossed around a few concepts and eventually settled on the idea of eightbitbeats.com, a multi-user music sequencer built around an 8bit theme. At 5pm pacific time August 27, we got cracking.

But before we get into the nitty gritty, check out the finished product:

Design Process

We were fortunate enough to have a team with very strong backgrounds in design, UI, and product, which was great when it came time to bounce ideas around. Whenever one of us got stuck or needed input, we would call a huddle at the whiteboard, quickly churn through all the options, and make a decision, often within 20 minutes -- sometimes much faster. The ability to do this came partly from having worked together in this way on a daily basis, but beyond that, we had a common understanding of user interface. We didn't need to waste any time defending one idea or lobbying for another. We knew from experience which options would work, which wouldn't, and which ones needed to be explored further.

In terms of visual design, the biggest challenge for eightbitbeats was making efficient use of space. Although available screen real estate gets larger every year, the standard width for a web page still tends to be around 960px, and our first mockups stuck to that standard. However, once we settled on a reasonable size for the sequence buttons (10x10) -- and started laying out additional elements like user avatars, instrument selection, and labels for each instrument's notes -- we realized we would need increase the width, and eventually settled at 1300px. We made a judgement call that, at least for the purposes of the Node Knockout, we could probably assume our audience would be using fairly new hardware with wide-format, high-res displays. (In the days since the code freeze, we've had lots of feedback but no comments regarding page width, so it seems we guessed right.)

ui-zoom2

Next up, we needed to consider the sequencer matrix. We kept image assets to a minimum in favor of CSS3 to lighten the load, but even so we still had to contend with potentially hundreds of divs on screen at any time. Add to that the fact that each button needed several states including disabled, enabled, active, played, downbeat, measure downbeat, some of which would require nested divs, and we were quickly talking about thousands of elements. Rather than go down that road, we opted to use :before and :after pseudo classes to insert containers dynamically. (See Nicolas Gallagher's excellent post for details on this method). Using this approach simplified our markup considerably and allowed us, with the help of less.css, to handle all the assorted states in a legible stylesheet (less than 400 lines total).


    .step {
        float: left;
        margin-right: 3px;

        &.active .note {
            border-color: rgba(255,255,255,.1);
            box-shadow: 0 0 3px rgba(255,255,255,.1);
        }
        &.active .note.on:after {
            background: rgba(253,154,0,1.0) !important;
        }
    }
                    

The design of the buttons was inspired by a fantastic gallery of control surfaces, mixers, and other pro audio gear at by livid instruments. We were lucky to have lots of great reference material and a well-defined real-world paradigm to follow. From there, we just had to choose a color scheme and start coding.

Front-End Architecture

Due to the time limit, it was obvious that we needed to use a frontend framework to move quickly and keep logic and presentation separate. After zero deliberation we settled on Backbone.js. It gave us just the right amount of tools to be really powerful while remaining super lightweight and flexible. Using backbone's collections allowed us to easily organize the elements of the player as well as index individual tracks, steps, and notes when it was time to play them. In addition, two devs on our team had already built multiple backbone applications together, so it was a prime choice for our frontend framework. Our architecture looked something like this:


    Socket

    App model
        User model

        Instruments collection
            Instrument model

        Player model
            MegaMan collection
                MegaMan models

            Tracks Collection
                Track models
                    Steps Collection
                        Step models
                            notes: [0,1,0,0,1,0,0,0,0,0]    
        ChatLog model
            Messages collection
                Message models
                        

It's not shown in this diagram, but each model has a view associated with it to render its state into html. View the source of our page and you can see the full app unminified

diagram

App

At the top level we have an App model and the socket.io connection we communicate through. The App is responsible for initializing the application and is a good place to make references to app-wide resources like the User, Player, and ChatLog models. When the app starts it syncs state with the server and either creates itself based on data given, or, if there are no other users at the time, just renders itself into an empty player.

User

The User model just gets populated with the current user's info and is referenced when permissions are calculated or messages are sent.

Instruments

This is simply a collection of all the instruments you can choose from. When ever you switch instruments it grabs the appropriate Instrument and mounts it on the Track model for reference when notes are played.

Player

The Player contains most of the meat of the application. It holds the Tracks and is used to store the state of the current iteration. Essentially what happens is when the player's step attribute is incremented by the game loop, each the listening Tracks and the MegaMen respond by rendering themselves into the new state. Since the callbacks run asynchronously it makes it easy to keep all tracks and then Megaman in sync with eachother. The tracks also told to loop over the specified step's notes and play the associated sound files.

We will be doing a more technical follow up post that goes over the code involved in making the player work

ChatLog

The ChatLog was thrown in last and we didn't have time to complete it. As you may have noticed, there is no chat log in the app :). We are still mounting our Messages collection there so we can easily build it out later. The working part behaves as such: When a user chats, it adds a new Message to the Messages collection and sends that data through the socket with the username. This way we know which user to place the chat bubble in the other clients apps.

Node.js Server

For the backend architecture, we went with a classic client/server model where all events and changes are sent to the server. Upon receiving these messages, the server would record any changes, do any necessary processing, and finally notify all other clients that the change occurred, so that they could individually update their state.

Client/Server

In this way, eightbitbeat's node.js instance primarily serves as a message relay system that sits between all players to make sure there aren't any syncronization issues. It also ensures that there is an authoritative representation of the room that we can send to new user connections. This type of communication model is essentially what node.js is known to excel at, so once we coded and fired up the server, everything ran quite smoothly.

To get the client talking with the node.js server, we simply dropped in the amazing socket.io module. Using socket.io meant that all the we needed to do was establish a clear API and then split off to code our own respective pieces, without worrying about integration issues. During the entire length of the Knockout event, the members that were focused on the frontend coding didn't need to look at the server code -- nor did they have the time to. As long as communication between the client and server followed the "documentation" we outlined on the whiteboard, every piece of the app knew how to interact, and thus worked flawlessly.

Deployment

For deployment we decided to use Linode. Although there were a bunch of great deployment options provided by sponsors that we would have loved to try out, we had experience with Linode from personal projects, and knew that we could quickly get things up and running from scratch.

We were grateful that the Node Knockout team provided easy-to-follow steps for contestants to set up their local and production environments. It was clear that they really made an effort to provide solid resources and documentation to contestants to ensure maximum time could be spent coding and building a product.

The Game Loop

The Game Loop

We anticipated the main play loop to be one of the more challenging aspects of the project. It had to be optimized since it would be responsible for playing collections of audio samples precisely enough to pass as music to sensitive human ears. We began by looking to the internet for ideas for our implementation, and found this article on Javascript Game Loops by Arthur Schreiber. While the article covers some bits of screen rendering, it served as a solid starting point. The basic requirement of our play loop was that it had to loop through each step in the sequence at a consistent rate determined by a beats per minute (BPM) value and then play every active note in that step. This was easily achieved with the following:


    // Pseudo javascript for demonstration
    Player.play = (function() {
        // Set up timing variables
            var skipTicks = 60000 / 4, // 60000ms per min over 4ticks per beat
            nextTick = (new Date).getTime();

            return function() {
                while ((new Date).getTime() > nextTick) {
                    // While it's time to tick, tick until it isn't time anymore.

                    // Do the work
                    $.each(Player.notes, function(note) {
                        note.play();    
                    });

                    // Increments nextTick by the correct ms per tick
                    nextTick += skipTicks / Player.bpm; 
                }
            };
    })()

    // Start the loop
    Player.loopInterval = setInterval(Player.play, 0);
                        

However, we noticed that it did not perform very well with a reasonable amounts of notes, so we built a sound manager object to collect every sound that needed to play on a given step, and only play each unique sound once. We achieved this by throwing every sound into a makeshift hashset (we used an associative array) and then triggering a play on each of those sounds at the end of the loop. Then we had the idea for the manager to pre-cache the notes in the coming step ahead of time, so that they would be ready to play immediately upon each new time step.


    soundManager = (function () {
        // Make-shift HashSets
        var toPlay = {};  // Sounds to play for the current step.
        var cache = {};  // Sounds to cache for the next step.
        return {
            // Add a file if it’s not been added.
            addFile: function(file) {
                if (!cache.file) {
                    cache[file] = file;
                    // This value should probably just be set to true.
                }
            },

            // Play the collected sounds.   
            play: function() {
                // Play sounds if we have them
                if (!!_.size(toPlay)) {
                    _.each(toPlay, function(name) {
                        playSound(name);
                    });
                }

                // Update the sets.
                toPlay = $.extend({}, cache);
                cache = {};
            }
        };
    })();
                        

This was as far as we got with the loop optimizations in the allotted 48 hours. We would have liked to spend a bit more time tinkering with the loop, but it is currently holding up with 4 to 5 users worth of notes. By nature, sequencers are intimately bound with loops. At the highest level, they contain the user-facing music loop, while further down they rely on precise "tick" timing loops. Game loops were a relatively new topic to most of us. We had fun learning about them and are excited to correct our mistakes in coming versions of the application.

Wrap-up

We were pretty beat by the time Sunday afternoon rolled around, but overall very pleased with the amount of work we were able to crank out. We should note one key habit we nailed down very early Saturday morning. We realized the Feature Creature can run rampant during events like this, with minds stuck in high gear and ideas flying every which way. That's why the minute our feature list became more than a handful of items, we drew a thick line down the middle of the board and labeled the two sides "must have" and "nice to have". Any time a new (or previously forgotten) feature was suggested, it went into one of the two buckets, and we were sticklers about adding any features to the "must have" section. Through this process, we made sure we wouldn't burn time on anything unless it truly fit into our notion of the minimum viable product. This saved us a ton of time (and headache), and we finished all the "must haves" on Sunday morning with time to spare, which gave us the opportunity to work on some fun, non-critical bits in the final hours.

At the time of this writing, all that remains is for judging and voting to finish on September 6th. We would be thrilled if you took the time to visit eightbitbeats.com and play around with our creation. If you dig it and want to support us in the contest, you can head over to the Node Knockout site and cast your vote (it's a little tricky... make sure you click the heart after logging in with Facebook), or toss us an upvote on Hacker News. Or if you don't dig it, leave a comment and let us know why! We'll be continue development once the competition is over, so we'd love to hear your feedback suggestions.

Thanks so much, and happy coding!