Skip to content

Latest commit

 

History

History
251 lines (177 loc) · 10.4 KB

music.adoc

File metadata and controls

251 lines (177 loc) · 10.4 KB

Music and Bass Shake

In Hexagon, the music is an important element of the game. It is even part of the physical game mechanics: Whenever there is a bass sequence, the whole stage is shaking.

The original game music is copyrighted. Therefore we provide an alternative track for this tutorial that has a Creative Commons license.

There is also an issue with Safari, so use a different browser for this like Google Chrome or the newest Internet Explorer.

Download the Music

The track that we are going to use is called "Shiny Tech" by Kevin MacLeod. Download it here: Incompetech

Rename it shinytech.mp3 and copy it into a src/music folder.


If you live in a country where it is allowed to download copyrighted material for private use, such as in Switzerland, then you can actually use the original track. Download the Hexagon SWF file from here and unzip it. (On a Mac, just double-click the file.) The music is the file "Sound 11". Rename the downloaded file to "music.mp3" and copy it into the music folder.


Install the audio library

The audio library abstracts the HTML5 Audio tag for the elm language. It uses native bindings. Unfortunately Elm discourages the use of native bindings for 3rd party libraries. But until audio support is available in core, we have to use one.

It is not possible to install such libraries through elm-package. So we have to use git or download it ourselves. We copy the elm-audio library in a folder called lib/elm-audio.

The easiest way is to do this with git. Create the folder lib in your project directory. Then clone the git repository:

git clone https://github.com/xarvh/elm-audio.git lib/elm-audio

If you are already using git for this tutorial, then you can add it as a submodule:

git submodule add https://github.com/xarvh/elm-audio.git lib/elm-audio

This is not enough information though. Elm expects the native module to be named after the package. So open the file elm-audio/src/Native/Audio.js and change the name of the global function to match the name that you gave your project in elm-package.json in the repository field. It uses the syntax _<username>$<project>$Native_Audio, so if you left it at the default setting the first line of the module is:

var _sbaechler$polygon$Native_Audio = function() {

While we are here, we can add a few strategic console.log statements for debugging: * Inside the oncanplaythrough() method that fires when the audio has been completely loaded * Inside the playSound and stopSound method.

Cue the music

Import the audio and the task module:

The music should play when the game state is Play and pause for all other states. The music should restart at the beginning whenever a game is restarted. This means we have to cue the audio whenever a game is restarted.

Therefore we have to create a few more states. Those states are short-lived. They are only used to cue our side-effect tasks that start and stop the music.

The reference to the music object has to be added to the model, our Game object. Because it is the result of a Task, it might have failed. So the music property is an optional Maybe Sound type. Add a hasBass : Bool property as well.

The music property is initialized with the value Nothing in the init method. hasBass is set to False.

The loading of the music file is a task as well. It sends the message MusicLoaded when the mp3 has been downloaded (when the browser has fired the canplaythrough event). The task could also fail, therefore we need to add an Error message. A Noop message is needed as well as a callback for the stop command.

We curry the loadSound method with the file name for the audio file:

The audio library exposes a Sound type, which is a reference to a HTML5 Audio tag. The PlaybackOptions object has three attributes: volume, start and loop.

We set loop to True and startAt to Nothing, which means that it continues playing from its current position.

The playSound and stopSound functions start and stop the music. They take a reference to the sound object and an options object, then returning a command of type message.

They are tasks because playing audio is a side-effect. In case of an error, the Error message is sent. In case of success (the audio is done playing) the Noop message is sent. We use anonymous functions to define the callbacks.

With all the new states the update methods become a bit more complex. However, they are still easily readable and understandable.

We now add the new intermediate helper states. In the onUserInput function, update the nextState assignment so it uses the new states.

In the onFrame function we are going to output new commands as well, so we assign those in the let block. At first we have to make sure that the music is not Nothing. (It won’t compile if this check was missing.)
In the Starting state, the music is played from the beginning. The startAt value is a Maybe as well so we have to use Just 0 here. The other states should be self-explanatory.

The update method needs to handle the new states. When the MusicLoaded message arrives, the game state is set to NewGame and the music is stored in the model.
In case of an error, we throw an exception.

Finally we have to load the audio file in the init method. We use Cmd.batch to queue multiple commands.

Bass pump

The playing field should pump in sync with the beat. Therefore, we have to define the speed of the track and the sections where there is a bass part.

Add three variables for beat, amplitude and phase:

beat = 138.0 |> bpm
beatAmplitude = 0.06
beatPhase = 270 |> degrees

The original music requires slightly different values:

beat = 130.0 |> bpm
beatAmplitude = 0.06
beatPhase = 180 |> degrees

Phase lets you adjust the start of the pumping so it matches the music exactly.

The beat is given in bpm. It is later used as an angle with the sinus function. One rotation should equal one beat. We convert the value using this function:

We have already added a hasBass : Bool property to the Game model and hasBass = False to the defaultGame object.

Next we add a function hasBass that takes a time value and returns True if there is a bass passage or False otherwise.

hasBass : Time -> Bool
hasBass time =
  if time < 20894 then False
  else if time < 41976 then True
  else if time < 55672 then False
  else if time < 67842 then True
  else if time < 187846 then False
  else if time < 215938 then True
  else False

For the original track use these values:

hasBass time =
  if time < 14760 then False
  else if time < 44313 then True
  else if time < 51668 then False
  else if time < 129193 then True
  else if time < 14387 then False
  else True

The hasBass value is set in the onFrame method:

{ game |
...
, hasBass = Music.hasBass game.msRunning

The beatPulse method takes the game state and returns a function that goes from Form → Form. The input is the playing field. The output is either the same or the pulsating playing field.

The pump method calculates the value that is passed to the scale method using a sin function.
The input is the game progress (a Float, the time in ms). The sin function returns a value between -1 and 1 so we are multiplying it with beatAmplitude to specify how much the stage should shake.
The beatPhase value is used to adjust the timing so it matches with the music.

The center hole is always pulsating but it should be in sync with the rest of the stage during a bass sequence. For that effect we adjust the makeCenterHole function. Whenever there is a bass sequence the radius of the center hole should remain constant, or otherwise it should be pumping.

That was the last piece of the puzzle. Now it is time to test it out. The music should start playing when the game starts. The stage starts pumping after 21 seconds.

If something is not working, compare your code with the full source code here.