# Enable FastClick $ -> FastClick.attach(document.body) # Represets a sound. class Sound # Constructs the sound. constructor: (@_name) -> @howlerSound() # Returns a promise that's resolved when the sound loads. loadingPromise: -> @_loadingPromise ?= new $.Deferred() # Returns the URL for the Sound with the provided name. url: -> "https://s3-us-west-2.amazonaws.com/s.cdpn.io/49705/#{ @_name }.mp3" # Plays the Sound. play: -> @howlerSound().play() # Returns the Howler object for this Sound. howlerSound: -> @_howlerSound ?= new Howl({ urls: [ @url() ] onload: => @loadingPromise().resolve() onloaderror: => @loadingPromise().reject() }) class SoundBoard @SOUND_NAMES = [ "highHat", "crash", "bell", "rim", "snare", "tom1", "tom2", "kick", "metronome" ] # Constructs this SoundBoard. constructor: -> @sounds() # An object containing the sounds in this SoundBobard. sounds: -> return @_sounds if @_sounds? @_sounds = {} SoundBoard.SOUND_NAMES.forEach (soundName) => @_sounds[soundName] = new Sound(soundName) # Returns a sound in this SoundBoard. sound: (soundName) -> @_sounds[soundName] # Plays the sound with the provided name. play: (soundName) -> @sound(soundName).play() # Returns a promise that resolves when all of the sounds have loaded. loadingPromise: -> @_loadingPromise ?= $.when.apply($, @_soundsArray().map (sound) => sound.loadingPromise() ) # An array of the sound objects in this SoundBoard. _soundsArray: -> Object.keys(@sounds()).map (soundName) => @sound(soundName) class BeatMachine # Constructs the BeatMachine. constructor: (@_beatsPerMinute, @_beatsPerMeasure, @_ticksPerBeat) -> @_soundBoard = new SoundBoard() # Returns the sound board for this BeatMachine. soundBoard: -> @_soundBoard # Returns a promise that resolves when the BeatMachine is loaded. loadingPromise: -> @_soundBoard.loadingPromise() # Called every time the BeatMachine ticks. play: (tick) -> SoundBoard.SOUND_NAMES.forEach (soundName) => @_soundBoard.play(soundName) if @switches()[soundName][tick] # Toggles the sound for the provided tick. Returns true if the sound was toggled on and false if it was toggled off. toggle: (soundName, tick) -> @switches()[soundName][tick] = not @switches()[soundName][tick] # Returns the number of ticks per measure ticksPerMeasure: -> @_beatsPerMeasure * @_ticksPerBeat # Returns the number of ticks per minute. ticksPerMinute: -> @_beatsPerMinute * @_ticksPerBeat switches: -> return @_switches if @_switches? @_switches = {} SoundBoard.SOUND_NAMES.forEach (soundName) => @_switches[soundName] = [0...(@ticksPerMeasure())].map -> false [0...@_beatsPerMeasure].forEach (beat) => @toggle("metronome", beat * @_ticksPerBeat) @_switches class BeatMachineView # The keyboard keys. @KEYS: [ "a", "s", "d", "f", "j", "k", "l", ";" ] # Constructs the BeatMachineView. constructor: (@_$element, @_beatMachine) -> @_$element.find(".tick").click (event) => @toggle($(event.target)) $("body").keypress (event) => @_keyPressed(String.fromCharCode(event.which)) $(".start").click => @start() @_beatMachine.loadingPromise().then => @_$element.removeClass("loading") if $("html").hasClass("touch") then $(".start").removeClass("hidden") else @start() # Starts the BeatMachineView. start: -> @_subtick = -1 @_tick = -1 setInterval((=> @_subticked()), 60000 / @_beatMachine.ticksPerMinute() / 2) # hide the controls if they're not already hidden $(".start").addClass("hidden") # Play a sound when started so sound is enabled on mobile browsers @_beatMachine.soundBoard().play("metronome") if $("html").hasClass("touch") # Called every time a subtick occurred. This is necessary to allow keyboard input to fire before the sound has played. _subticked: -> @_subtick = (@_subtick + 1) % 2 @_ticked() if @_subtick is 0 # Called every time the sound ticks. _ticked: -> @_tick = (@_tick + 1) % @_beatMachine.ticksPerMeasure() @_beatMachine.play(@_tick) $(".tick").removeClass("current") $("[data-tick='#{ @_tick }']").addClass("current") # Fired whenever a key is pressed. _keyPressed: (key) -> instrument = SoundBoard.SOUND_NAMES[BeatMachineView.KEYS.indexOf(key)] return unless instrument? tick = (@_tick + @_subtick;) % @_beatMachine.ticksPerMeasure() @toggle(@$tick(instrument, tick)) # Retrieves the element for the provided instrument and tick. $tick: (instrument, tick) -> @_$element.find("[data-instrument='#{ instrument }']").find("[data-tick='#{ tick }']") # Toggles the provided tick. toggle: ($tick) -> instrument = $tick.parent().data("instrument") tick = $tick.data("tick") active = @_beatMachine.toggle(instrument, tick) $tick.toggleClass("active", active) @_beatMachine.soundBoard().play(instrument) if active and @_subtick is 0 # Kick things off. beatMachine = new BeatMachine(120, 4, 4) $switches = $(".switches") beatMachineView = new BeatMachineView($switches, beatMachine) # Set the BeatMachine's initial values switches = { "highHat": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], "crash": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], "bell": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], "rim": [ true, false, false, false, true, true, true, false, false, true, false, false, true, false, false, false ], "snare": [ true, false, false, false, false, false, true, false, false, true, false, false, true, false, false, false ], "tom1": [ true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false ], "tom2": [ true, false, true, false, false, false, true, false, false, false, true, false, false, true, false, false ], "kick": [ true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false ] } Object.keys(switches).forEach (instrument) => switches[instrument].forEach (enabled, tick) => beatMachineView.toggle(beatMachineView.$tick(instrument, tick)) if enabled