In my post Playing a sine wave in javascript using audioContext we looked at the audio context, and how to create a sound in Javascript.
Using that code (the SineWave object) I will play around and create a simple javascript sound tracker. The result can be found on jsfiddle.
First off, the SineWave object had the following “public” interface:
SineWave (context, frequency) SineWave.play() SineWave.pause()
What we aim for is a tracker looking like the chess table above, where each square is a sound, each row is a “keyboard” and the blue line being the player position, moving from the left to the right, over and over.
The html setup is the following. A sound editor div, that will contain all sound squares, a player position div, for marking the current time postion, an input for the tempo of the tune, a textarea for importin / exporting tunes and buttons supporting this import / export:
<h1>8bit runner</h1> <div id="inputs"> <input id="bpm" type="range" value="80" /></div> Space = Audio Play/Pause Return = Life Play Pause <textarea id="melody"> [0,2,8,24,42,56,64,90,104,122,128,130,140,172,184,194,202,216,234,252] </textarea> <button id="import">Import </button><button id="export">Export </button>
We create a function named Player. First we populate it with some values that we will use:
var self = this; this.sounds = {}; this.fqs = [130.81, 146.83, 164.81, 174.61, 196.00, 220.00, 246.94, 261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88]; this.sounds = []; this.playing = false; this.context = new webkitAudioContext(); this.mouseDown = false;
I always use the self variable instead of this, so we don’t have to be confused in callback functions. The sounds is an array that we will populate with our SineWave objects, and fqs is an array containing the frequency’s of C minor. The playing bool indicates if the tracker is playing or not, and the mouseDown bool if the mousebutton is down or not.
First, lets initialize the Player and create the DOM elements needed:
this.init = function () { $("#soundEditor").mousedown(function (e) { self.mouseDown = true; }).mouseup(function (e) { self.mouseDown = false; }); // Initialize the tracker ui and the sounds for (var i = 0; i < 16; i++) { // One sound for each row this.sounds[i] = new SineWave(this.context, this.fqs[i]); // 16 timesteps for each sound for (var j = 0; j < 16; j++) { // Create and append each sound div var newDiv = $(" ").addClass("soundElement"); newDiv.css({ left: (32 * i), top: (32 * j) }); $("#soundEditor").append(newDiv); // Save their position in data attributes, // for fast retrieval when clicked or played newDiv.attr("data-x", i).attr("data-y", j); // Handle click and mouseenter events for // sound on / off toggling newDiv.click(function () { $(this).toggleClass("selected"); }).mouseenter(function (e) { if (self.mouseDown) { $(this).toggleClass("selected"); } }); } } $("#import").click(function () { self.impSound() }); $("#export").click(function () { self.expSound() }); $("#bpm").change(function () { self.setBPM($("#bpm").val()); }); $("body").keydown(function (e) { if (e.keyCode == 32) { if (self.playing) { self.stop(); } else { self.play(); } } }); this.play(); };
First, we handle mouse button press/release, so that it will be easy to toggle div selections for multiple divs in one mouse move. After that, we create 16 sounds, and for each sound 16 divs for different time steps. A sound div that is selected is one that will be played when the tracker is at it’s position.
#import and #export are two buttons that, together with a textarea used for “saving” and “loading” a tune. #bpm is a input of type range, that we use to set the tempo of the tune. We setup some keyboard shortcuts for play/pause and we end the initialization by starting the player.
this.play = function () { if (!this.playing) { this.playing = true; this.curPos = 0; this.calcTimePerSquare(); this.playPos(); this.timer = setInterval(function () { self.playPos(); }, this.timePerSquare); } };
In the play function, we reset the player position to 0, calculate how much time we should use for each time step, plays the current position using playPos, and setup a timer for continuous playing of the player.
this.calcTimePerSquare = function () { this.timePerSquare = 15000 / $("#bpm").val(); };
calcTimePerSquare isn’t much to talk about. We grab the value from the range input #bpm, and use it to get the player speed.
var soundPos = function(attr){ return function(obj){ return parseInt( $(obj).attr(attr) ); } }; var soundPosX = soundPos('data-x'); var soundPosY = soundPos('data-y'); this.playPos = function () { // Move the position marker $("#playPos").css({ left: (this.curPos * 32) }); // Turn off all sounds that are not supposed to play $('.soundElement[data-x="' + this.curPos + '"]').not(".selected").each(function () { var curSound = self.sounds[soundPosY(this)]; if(curSound.playing) curSound.pause(); }); // Turn on all sounds that are supposed to play $('.soundElement.selected[data-x="' + this.curPos + '"]').each(function () { var curSound = self.sounds[soundPosY(this)]; if(!curSound.playing) curSound.play(); }); this.curPos++; this.curPos %= 16; };
We use the data-x attribute of the sound element divs to select just the divs at the current time step. Then, we use their data-y attribute to handle the right sounds. Since all divs on the same row are using the same sounds (objects of type SineWave), we don’t have to handle pausing sounds that was turned on in previous time steps. This is a great example of to create easy to follow code with jQuery queries. soundPos, soundPosX and soundPosY are helper functions used to make sure that the data-x and data-y attributes are handled as integers and not strings. Here, that wouldn’t matter, but elsewhere it is, and they look prettier than using the $(this).attr(“data-x”) call.
We end the function but going to the players next time position. Very simple.
this.stop = function () { if (this.playing) { this.playing = false; clearInterval(this.timer); delete this.timer; } };
The stop function is of little interest – changes the playing flag and clears the interval timer.
this.expSound = function () { var resArr = []; $(".soundElement.selected").each(function(){ resArr.push(soundPosX(this)*16 + soundPosY(this)); }); $("#melody").val(JSON.stringify(resArr)); }; this.impSound = function () { $(".soundElement").removeClass('selected'); var soundString = $("#melody").val(); var resArr = JSON.parse(soundString); _.each(resArr, function (i) { var x = parseInt(i / 16); var y = i % 16; self.soundElement(x, y).addClass('selected'); }); };
Export sound on the other hand, is kind of fun. Since we have a chess board, with 256 squares, what could be better than to store the tune in an array, listing only the squares that are selected, letting them identify themselves with their index between 0 and 255? Stringify the array, and push it into the melody textarea. Importing is done just the same way.
Voila, a fun and simple Javascript keyboard tracker! The full source is available on JSFiddle.