This is an Elm 0.18 module that encapsulates a control for the playback of single-track MIDI recordings which conform to the MidiRecording format (i.e. those produced by the elm-comidi parser). The control uses stop, start and pause buttons and includes a capsule that indicates the proportion of the tune that has been played. It uses as an instrument the acoustic grand piano.
It exposes the following message type which sends a recording to the player:
SetRecording (Result String MidiRecording)
Other messages which control the buttons are autonomous and invisibe to the caller. If the recording is single track, it is played as is; if multitrack, it plays track zero.
The player is implemented using ports. As such, it is not possible to produce a single build artefact that contains the complete module. The module exposes the following:
Model, Msg (SetRecording), init, update, view, subscriptions
This can be imported into a main elm program using the normal conventions. The player will only become visible once the instructions to load the sound fonts and to set the recording have been issued to it.
The following section describes how a calling program that (somehow) gets hold of a MIDI recording via the MIDI message might integrate the player:
import Midi.Player exposing (Model, Msg, init, update, view, subscriptions)
type alias Model =
{
myStuff :....
, recording : Result String MidiRecording
, player : Midi.Player.Model
}
type Msg
= MyMessage MyStuff
| Midi (Result String MidiRecording )
| PlayerMsg Midi.Player.Msg -- delegated messages for the player
It is important that the calling program allows the player to be initialised:
init : (Model, Cmd Msg)
init =
let
myStuff = ....
(player, playerCmd) = Midi.Player.init recording
in
{
myStuff = myStuff
, recording = Err "not started"
, player = player
} ! [Cmd.map PlayerMsg playerCmd]
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
MyMessage stuff -> ...
Midi result ->
( { model | recording = result }, establishRecording result )
PlayerMsg playerMsg ->
let
(newPlayer, cmd) = Midi.Player.update playerMsg model.player
in
{ model | player = newPlayer } ! [Cmd.map PlayerMsg cmd]
where establishRecording sends a command to the player which establishes the recording to play:
establishRecording : Result String MidiRecording -> Cmd Msg
establishRecording r =
Task.perform (\_ -> NoOp)
(\_ -> PlayerMsg (Midi.Player.SetRecording r))
(Task.succeed (\_ -> ()))
view : Model -> Html Msg
view model =
div []
[
myView ..
, Html.map PlayerMsg (Midi.Player.view model.player)
]
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ mySubs ...
, Sub.map PlayerMsg (Midi.Player.subscriptions model.player)
]
The following components are required by the player:
- The javascript for the combined calling program and player
- The javascript for the sound fonts called by the player through elm ports
- The javascript for loading the sample MIDI file
- The soundfonts used by the player, assumed to be in the directory assets/soundfonts
- The image files used by the player widget assumed to be in the directory assets/images
The various pieces of javascript can be assembled (here for a calling program named MidiFilePlayer) in the html file as follows
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Elm 0.18 Midi Player module sample</title>
</head>
<body>
<div id="elmMidiFilePlayer"></div>
<script src="js/soundfont-player.js"></script>
<script src="distjs/elmMidiFilePlayer.js"></script>
<script>
var node = document.getElementById('elmMidiFilePlayer');
var myapp = Elm.MidiFilePlayer.embed(node);
<!-- the javascript below is written to accept an initial parameter named node-->
</script>
<script src="js/nativeSoundFont.js"></script>
<script src="js/nativeBinaryFileIO.js"></script>
</body>
</html>
Although designed for playback of MIDI files, the player can be used with other sound sources. The Header contains the following integer fields:
formatType - set this to 0 (single track)
trackCount - set this to 1
ticksPerBeat - it is usually convenient to set this to 480
The minimal set of MidiEvents that are most usefully implemented are these:
Tempo microsecondsPerQuarterNoteBeat
NoteOn channel pitch velocity
NoteOff channel pitch velocity
Typically, NoteOn messages define the note to be played at a time delay of zero whilst NoteOff messages define the time it is played for because of a positive time delay.In the NoteOn and NoteOff messages, channel is ignored and can be set to any integer value, pitch is the MIDI pitch number and velocity (related to gain) is a number between 0 and 127.
The tempo setting defines the number of microseconds taken by each beat (see later). This is then built into a MidiMessage:
MidiMessage = (Ticks, MidiEvent)
It is often convenient to produce both a NoteOn and NoteOff for each note where there is no delta time (in Ticks) for the NoteOn, but a positive time for NoteOff. The simplest thing to do is to set the note ticks to 480 for a whole note, 240 for a half note and so on. Then you can vary the pace of the playback simply by setting an appropriate Tempo value at the start of the track (or whenever the tempo subsequently changes)
Finally, the overall MIDI track is represented like this:
Track = List MidiMessage
and the complete MIDIRecording is simply a tuple containing the Header and the Track.