Above, Below, and Beyond Tech Talk

by Rahel Lüthy

November 11, 2015

Play Audio via Elm

In preparation for a MaKey-MaKey-workshop, I needed a simple HTML page which plays a sound whenever any key is pressed. I am using all sorts of excuses to write Elm lately, so here’s how I ended up triggering the audio clip (full sources on GitHub, deployed version here).

First, we need a signal that indicates whether any key is currently down:

isAnyKeyDown : Signal Bool
isAnyKeyDown =
  Signal.map (\keys -> not (Set.isEmpty keys)) Keyboard.keysDown

For debugging, let’s visualize our events by displaying a small note image:

view : Bool -> Html
view isAnyKeyDown =
  if | isAnyKeyDown -> img [ src "note.png" ] []
     | otherwise -> div [] []

main : Signal Html
main =
  Signal.map view isAnyKeyDown

Next, we need to actually play the audio. Elm itself does not support this out-of-the-box, but triggering it via HTML5 and JavaScript is quite straight forward:

<body>
    <audio id="audio" src="piano.ogg" preload="auto"></audio>
</body>
var audio = document.getElementById('audio')
function playAudio() {
    // reset if already running
    audio.pause();
    audio.currentTime = 0;

    // go!
    audio.play();
}

Finally, we need to wire our JS function to our Elm signal. This is where Elm ports come in:

port playAudio : Signal ()
port playAudio =
  Signal.map (\_ -> ()) (Signal.filter identity False isAnyKeyDown)

There are two things to note here: First, we’re filtering our isAnyKeyDown to no longer contain key release events. Second, we’re mapping our signal of boolean values to a signal of unit types () to match the signature of our parameter-less JS function.

All there is left, is to actually trigger the JS callback whenever Elm sends an event:

    var app = Elm.fullscreen(Elm.Main, {});
    app.ports.playAudio.subscribe(playAudio);