forked from joshontheweb/somethingcoded
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
345 lines (313 loc) · 24.4 KB
/
Copy pathindex.html
File metadata and controls
345 lines (313 loc) · 24.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
<!doctype html>
<html>
<head>
<link rel="icon" href="/media/styles/images/favicon.ico" />
<title>SomethingCoded</title>
<meta name="description" content="Creators of eightbitbeats.com">
<meta name="author" content="SomethingCoded">
<link rel="stylesheet" href="media/styles/reset.css" />
<link rel="stylesheet" href="media/styles/text.css" />
<link rel="stylesheet" href="media/styles/960.css" />
<link rel="stylesheet" href="media/scripts/etch/styles/etch.css" />
<link rel="stylesheet/less" href="media/styles/style.less" />
<link rel='stylesheet' href='http://fonts.googleapis.com/css?family=Cabin+Sketch:bold'>
<!--[if lt IE 9]>
<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<script src="media/scripts/lib/less.min.js"></script>
</head>
<body>
<div class="main">
<div class="main-header container_12">
<div class="logo grid_12">
<img id="logo" src="media/styles/images/logo.png" alt="something coded" width="615" height="98" />
</div>
</div>
<div class="section first node-js-server container_12">
<div class="header grid_8">
<h3>The story of <a href="http://eightbitbeats.com">eightbitbeats.com</a>: How we built a social 8bit music sequencer in 48hours</h3>
</div>
<div class="grid_8">
<img src="media/styles/images/promo.png" alt="eightbitbeats.com" width="608" height="303" />
</div>
<div class="grid_8">
<p><a href="http://nodeknockout.com">Node Knockout 2011</a>, 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.</p>
<p>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 <a class="eightbitbeatslink" href="http://eightbitbeats.com">eightbitbeats.com, a multi-user music sequencer built around an 8bit theme</a>. At 5pm pacific time August 27, we got cracking.</p>
</div>
<div class="grid_8">
<p>But before we get into the nitty gritty, check out the finished product:</p>
<iframe width="640" height="510" src="http://www.youtube.com/embed/QmtB9o4HBQU?hd=1" frameborder="0" allowfullscreen></iframe>
</div>
<div class="aside grid_4">
<div class="social_stuff">
<iframe class="vote-button" src="http://nodeknockout.com/iframe/somethingcoded" frameborder=0 scrolling=no allowtransparency=true width=115 height=25></iframe>
<br /><br />
<a href="http://twitter.com/share" class="twitter-share-button" data-count="horizontal" data-via="joshontheweb" data-related="travhimself">Tweet</a><script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
<br /><br />
<iframe src="http://www.facebook.com/plugins/like.php?app_id=243470959023380&href=http%3A%2F%2Fsomethingcoded.com&send=false&layout=button_count&width=150&show_faces=false&action=like&colorscheme=light&font&height=21" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:150px; height:21px;" allowTransparency="true"></iframe>
</div>
</div>
</div>
<div class="section design-process container_12">
<div class="header grid_12">
<h2>Design Process</h2>
</div>
<div class="grid_8">
<p>
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.
</p>
<p>
<p>
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.)
</p>
<img src="media/styles/images/ui-zoom2.png" alt="ui-zoom2" width="794" height="309" />
<p>
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 <a href="http://nicolasgallagher.com/multiple-backgrounds-and-borders-with-css2">Nicolas Gallagher's excellent post</a> 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).
</p>
<pre><code>
.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;
}
}
</code></pre>
<p>
The design of the buttons was inspired by a fantastic gallery of control surfaces, mixers, and other pro audio gear at by <a href="http://www.flickr.com/photos/31245410@N07/">livid instruments</a>. 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.
</p>
</div>
<div class="aside grid_4">
</div>
</div>
<div class="section front-end-architecture container_12">
<div class="header grid_12">
<h2>Front-End Architecture</h2>
</div>
<div class="grid_8">
<p>
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 <a href="http://documentcloud.github.com/backbone/">Backbone.js</a>.
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:
</p>
<p>
<pre><code>
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
</pre></code>
</p>
<p class="note">
It's not shown in this diagram, but each model has a view associated with it to render its state into html.
View the <a href="http://eightbitbeats.com/media/scripts/script.js">source</a> of our page and you can see the full app unminified
</p>
<img src="media/styles/images/diagram.png" alt="diagram" />
<h3>App</h3>
<p>
At the top level we have an <code>App</code> 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 <code>User</code>, <code>Player</code>, and <code>ChatLog</code> 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.
<p>
<h3>User</h3>
</p>
The <code>User</code> model just gets populated with the current user's info and is referenced when permissions are calculated
or messages are sent.
</p>
<h3>Instruments</h3>
</p>
This is simply a collection of all the instruments you can choose from. When ever you switch instruments it grabs the appropriate <code>Instrument</code>
and mounts it on the <code>Track</code> model for reference when notes are played.
</p>
<h3>Player</h3>
<p>
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.
</p>
<p class="note">We will be doing a more technical follow up post that goes over the code involved in making the player work</p>
<h3>ChatLog</h3>
<p>
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.
</p>
</div>
<div class="aside grid_4">
</div>
</div>
<div class="section node-js-server container_12">
<div class="header grid_12">
<h2>Node.js Server</h2>
</div>
<div class="grid_8">
<p>
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.
</p>
<img src="media/styles/images/clientserver.png" alt="Client/Server" />
<p>
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.
</p>
<p>
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.
</p>
<h3>Deployment</h3>
<p>
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.
</p>
<p>
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.
</p>
</div>
<div class="aside grid_4">
</div>
</div>
<div class="section node-js-server container_12">
<div class="header grid_12">
<h2>The Game Loop</h2>
</div>
<div class="grid_8">
<img src="media/styles/images/activenotes.png" alt="The Game Loop" />
<p>
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 <a href="http://nokarma.org/2011/02/02/javascript-game-development-the-game-loop/index.html">this article on Javascript Game Loops by Arthur Schreiber</a>. 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:
</p>
<p>
<pre><code>
// 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);
</code></pre>
</p>
<p>
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.
</p>
<p><pre><code>
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 = {};
}
};
})();
</code></pre></p>
<p>
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.
</p>
</div>
<div class="aside grid_4">
</div>
</div>
<div class="section front-end-architecture container_12">
<div class="header grid_12">
<h2>Wrap-up</h2>
</div>
<div class="grid_8">
<p>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.
</p>
<p>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 <a href="http://www.eightbitbeats.com">eightbitbeats.com</a> 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 <a href="http://nodeknockout.com/teams/somethingcoded">cast your vote</a> (it's a little tricky... make sure you click the heart after logging in with Facebook), or <a href="http://news.ycombinator.com/item?id=2955693">toss us an upvote on Hacker News</a>. 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.</p>
<p>Thanks so much, and happy coding!</p>
</div>
<div class="aside grid_4">
</div>
</div>
<div class="main-footer container_12">
<ul>
<li>Front-end: <a href="http://twitter.com/mtoymil/">@mtoymil</a></li>
<li>Front-end: <a href="http://twitter.com/joshontheweb/">@joshontheweb</a></li>
<li>Design/UX: <a href="http://twitter.com/travhimself/">@travhimself</a></li>
<li>Backend/Systems: <a href="http://twitter.com/bundy_kim/">@bundy_kim</a></li>
</ul>
</div>
</div>
<script src="media/scripts/lib/jquery.js"></script>
<script src="media/scripts/lib/jquery-ui.min.js"></script>
<script src="media/scripts/lib/less.min.js"></script>
<script src="media/scripts/lib/underscore.js"></script>
<script src="media/scripts/lib/backbone.js"></script>
<script src="media/scripts/etch/scripts/rangy-core.js"></script>
<script src="media/scripts/etch/scripts/etch.js"></script>
<script src="media/scripts/script.js"></script>
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-11906758-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>