477 lines
20 KiB
Text
477 lines
20 KiB
Text
== Lesson 27: Music and MIDI
|
|
|
|
image::learn/27-midi.png[The finished lesson, 480]
|
|
|
|
This lesson is two lessons that happen to live together.
|
|
|
|
The first half is music: how a game plays a piece of music properly, why that
|
|
is not the same thing as playing a sound effect, and what to do when the music
|
|
has to stay in step with video.
|
|
|
|
The second half is MIDI, which most people have never met and which gives a
|
|
game something nothing else does. By the end of it, a keyboard plugged into
|
|
your computer will be playing your game.
|
|
|
|
=== A Sound Effect and a Piece of Music Are Different Things
|
|
|
|
You already know how to play a sound. Lesson ten loaded a `.wav` with
|
|
`soundLoad` and fired it with `soundPlay`, and if you point those two calls at
|
|
a four minute song they will play it.
|
|
|
|
Do not. Singe keeps sound effects and music apart, and the separation is
|
|
worth having for four reasons.
|
|
|
|
* *They have their own volumes.* `soundSetVolume` never touches the music and
|
|
`musicSetVolume` never touches the gunshots. Every game that has ever
|
|
shipped needed that, because players turn the music down and the effects up.
|
|
* *Music has no channel limit.* Sound effects share sixteen channels, and when
|
|
all sixteen are busy the seventeenth does not play. A music track is not on
|
|
a channel, so nothing can crowd it out.
|
|
* *One handle is one track*, and it pauses, resumes, stops, and fades on its
|
|
own without disturbing anything else.
|
|
* *Music pauses with the game.* Hit the pause key and it holds where it is.
|
|
Come back and it picks up. You write nothing to make that happen.
|
|
|
|
One thing that is *not* different is worth knowing, because people assume the
|
|
opposite: Singe decodes a music file into memory up front, exactly as it does
|
|
a sound effect. A long track costs memory. It is not streamed off the disk a
|
|
piece at a time. Five minutes of music is a few tens of megabytes of memory
|
|
while it is loaded, so load the track for the level you are on and
|
|
`musicUnload` it when you leave.
|
|
|
|
=== Playing a Track
|
|
|
|
Every reader of this book already has a piece of music, because the engine
|
|
unpacked one into your work folder the first time you ran anything. Start a file
|
|
with these three lines:
|
|
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
musicSetVolume(64)
|
|
musicPlay(theme, -1)
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Music: " .. (musicIsPlaying(theme) and "playing" or "stopped"))
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Run it with `Singe -R music`. Ten seconds of music, and then it starts
|
|
again, and it keeps starting again.
|
|
|
|
That is what the `-1` does. `musicPlay` takes a number of *loops*: leave it out
|
|
or pass `0` and the track plays once, pass `3` and it plays four times in all,
|
|
and pass `-1` and it repeats until something stops it. A menu, a title screen,
|
|
or a level's background is nearly always `-1`.
|
|
|
|
Two details in that little script are worth the ink. `musicLoad` takes a path
|
|
and not a `DIR ..` path, because `Singe/menuIntro.flac` is one of the engine's
|
|
own files and sits in the folder the engine unpacked into your work folder.
|
|
`musicIsPlaying` answers `true` or `false`, and the `and ... or ...` in the
|
|
middle of that `overlayPrint` is the Lua shorthand from lesson three for
|
|
choosing one of two values.
|
|
|
|
=== The Volume Scale Is Not the One You Know
|
|
|
|
`musicSetVolume(64)` sets it to about half. Sound effects run from `0` to
|
|
`63`; music runs from `0` to `128`, which is twice as far.
|
|
|
|
That is not a mistake and it is not a joke. Singe can run games written for
|
|
Hypseus, an engine whose music volume has always been a `0` to `128` scale,
|
|
and changing the numbers would silently make those games' music twice as loud
|
|
as their authors meant. So Singe keeps both scales, each attached to the
|
|
family of calls it belongs to. Effects: `0` to `63`. Music: `0` to `128`. Give
|
|
`musicSetVolume` a number outside its range and the script stops, which you
|
|
will see for yourself at the end of this lesson.
|
|
|
|
=== What Singe Will Play
|
|
|
|
Nearly anything. WAV, Ogg Vorbis, Opus, MP3, FLAC, WavPack, AAC, and a long
|
|
list past that, all through the same `musicLoad`.
|
|
|
|
Two families are more interesting than the ordinary ones, and both exist
|
|
because they are tiny.
|
|
|
|
*Tracker modules* -- `.mod`, `.s3m`, `.xm`, `.it`, and fifty-odd relatives --
|
|
hold a handful of short recorded instruments and a list of which notes to play
|
|
them at. A four minute tune can be under a hundred kilobytes.
|
|
|
|
*Chiptunes* -- `.nsf`, `.spc`, `.vgm`, `.gbs`, and their kin -- go further:
|
|
they hold the *register writes* for the actual sound chip of a Nintendo, a
|
|
Mega Drive, or a Spectrum, and Singe emulates the chip to play them. If you
|
|
want a game to sound like 1989 rather than to sound like a recording of 1989,
|
|
this is how.
|
|
|
|
Either loads with `musicLoad` like anything else. The manual's *Video, Audio,
|
|
and Container Formats* section has the full list, including which container
|
|
goes with which codec.
|
|
|
|
=== Stopping, Pausing, and Fading
|
|
|
|
[source,lua]
|
|
----
|
|
musicPause(theme)
|
|
musicResume(theme)
|
|
musicStop(theme)
|
|
musicStop(theme, 2000)
|
|
----
|
|
|
|
`musicPause` holds the track where it is and `musicResume` carries on from
|
|
that spot. `musicStop` ends it, and the track goes back to its beginning the
|
|
next time you play it. With a second number, `musicStop` fades the track out
|
|
over that many milliseconds instead of cutting it dead -- two seconds here.
|
|
That fade is the single easiest thing you can do to make a game feel finished.
|
|
Music that stops mid-bar when the player walks through a door sounds like a
|
|
bug, even to someone who could not say why.
|
|
|
|
Call any of the four with no handle at all and they act on every piece of
|
|
music at once, which is what a pause screen wants.
|
|
|
|
Now the honest limit, because you will look for this and it is not there:
|
|
*Singe cannot crossfade two tracks.* `musicSetVolume` is one gain over all
|
|
music, not a volume per track, so there is no way to bring one track up while
|
|
another goes down. What you can do is fade the old one out and start the new
|
|
one as it goes, which is close enough for a level change and audibly not the
|
|
same thing for a battle theme sliding in under an exploration theme. If you
|
|
need a true crossfade today, the way to get it is to make the two pieces one
|
|
file and seek within it, or to accept the overlap.
|
|
|
|
=== Music, Video, and Staying in Step
|
|
|
|
If your game plays video -- part three's subject -- there is one thing to know
|
|
about sound, and it is not a music call at all.
|
|
|
|
Singe shows a video frame when the sound that belongs with it is heard, not on
|
|
a timer. The audio is the clock and the picture follows it. That is the right
|
|
way round, because an ear notices a fifth of a second of drift in a voice and
|
|
an eye does not notice the same drift in a mouth.
|
|
|
|
The engine measures its own audio path when it starts. What it cannot measure
|
|
is everything after the operating system hands the sound away: an amplifier, a
|
|
television, an HDMI receiver, a Bluetooth headset. Each of those adds a delay
|
|
of its own, it is different on every machine, and it shows up as sound arriving
|
|
slightly after the thing that made it.
|
|
|
|
The fix is one number:
|
|
|
|
[source,lua]
|
|
----
|
|
singeSetAudioDelay(45)
|
|
----
|
|
|
|
Forty-five milliseconds of "the audio is late; hold the picture back to
|
|
match". You can also pass it as `--audiodelay=45` on the command line or put
|
|
it in the game's `games.dat` entry. `singeGetAudioDelay` reads back whatever
|
|
is in force, which is what a service menu shows next to its slider.
|
|
|
|
You do not have to guess the number. Singe's own menu has a calibration
|
|
screen: it plays a click once a second and flashes the screen white, you
|
|
adjust until the flash and the click happen together, and it saves the result
|
|
for every game on that machine. The manual's *Audio Sync* section describes it
|
|
and explains why it works. If you build a cabinet, run it once after you wire
|
|
the speakers, and never think about it again.
|
|
|
|
=== The Other Thing Called MIDI
|
|
|
|
MIDI is two unrelated things with one name, and keeping them apart is most of
|
|
understanding it.
|
|
|
|
Here is the first, in one paragraph. A recording -- a `.wav`, an `.mp3` -- is a
|
|
measurement of a sound, taken forty-four thousand times a second. MIDI is not
|
|
that. A MIDI file holds *what was played*: at this moment, on this channel,
|
|
press key sixty this hard; a little later, let go of it. It is closer to sheet
|
|
music, or to a player piano's punched roll, than to a recording. That is why a
|
|
MIDI file is a few kilobytes when the same music as audio is a few megabytes,
|
|
and it is also why a MIDI file does not contain any sound at all. Something
|
|
has to play the notes.
|
|
|
|
A MIDI file is music, so it plays like music:
|
|
|
|
[source,lua]
|
|
----
|
|
local tune = musicLoad("music/overworld.mid")
|
|
musicPlay(tune, -1)
|
|
----
|
|
|
|
But the thing that plays the notes has to come from somewhere, and that thing
|
|
is a *soundfont*: a file full of recorded instruments -- a piano, a trumpet, a
|
|
snare drum -- with the notes and the loop points marked, so the engine can
|
|
build any note of any instrument out of them.
|
|
|
|
Singe does not ship one. A good soundfont is tens of megabytes, and it would
|
|
be tens of megabytes carried by every copy of the engine for a format most
|
|
games never touch. So you name one with `--soundfont`, or you put one at
|
|
`Singe/soundfont.sf2` so that a packed game carries its own, or you let Singe
|
|
find the one your Linux distribution installed. With no soundfont anywhere,
|
|
loading a `.mid` fails and says so.
|
|
|
|
This is why the same MIDI file sounds different on two machines and why it
|
|
sounded so bad on the computers of the nineteen nineties. The file is a list
|
|
of notes. What you hear is whichever soundfont happened to play them: a good
|
|
one is a room full of instruments, a cheap one is a room full of toys, and the
|
|
notes are identical in both. If a MIDI file is going in your game, ship the
|
|
soundfont with it. Otherwise you have not chosen how your music sounds; you
|
|
have left it to whatever is installed on the player's machine.
|
|
|
|
=== MIDI as a Wire
|
|
|
|
Now the second thing, which is the interesting one.
|
|
|
|
MIDI is also a cable, and messages that travel along it. Plug a musical
|
|
keyboard into your computer and every key you press sends a message: *note on,
|
|
channel 1, key 60, velocity 90*. Turn a knob on a control surface and it sends
|
|
*controller 7 on channel 1 is now 40*. These messages exist whether or not
|
|
anything is playing any music. They are another input device, like a
|
|
joystick with eighty-eight buttons that know how hard you hit them.
|
|
|
|
And it goes the other way. Your game can send those messages out, to a
|
|
synthesiser, a sound module, a stage light controller, or anything else on the
|
|
wire.
|
|
|
|
Ports are numbered from zero, and Singe opens nothing until you ask -- a game
|
|
that never mentions MIDI never pays for MIDI. One input port and one output
|
|
port may be open at a time. Take stock of the machine like this:
|
|
|
|
[source,lua]
|
|
----
|
|
local inPort = "none"
|
|
local outPort = "none"
|
|
|
|
if midiInputCount() > 0 and midiOpenInput(0) then
|
|
inPort = midiInputName(0)
|
|
end
|
|
if midiOutputCount() > 0 and midiOpenOutput(0) then
|
|
outPort = midiOutputName(0)
|
|
end
|
|
----
|
|
|
|
On a machine with no MIDI at all the counts are zero, the open calls answer
|
|
`false`, and nothing fails. That matters: it means you can write this code
|
|
into a real game and ship it, and the ninety-nine players in a hundred with no
|
|
MIDI keyboard will never know it is there. The names are for a settings screen
|
|
that lets a player choose which port, because port zero is rarely the one they
|
|
want. `midiRescan` looks again for something plugged in while the game is
|
|
running.
|
|
|
|
=== Listening
|
|
|
|
Every message that arrives goes to a callback, in the same way that every key
|
|
press goes to `onInputPressed`:
|
|
|
|
[source,lua]
|
|
----
|
|
local MIDI_NOTE_ON = 0x90
|
|
|
|
|
|
function onMidiMessage(status, data1, data2, bytes)
|
|
if (status & 0xF0) ~= MIDI_NOTE_ON or data2 == 0 then
|
|
return
|
|
end
|
|
heard = noteName(data1)
|
|
end
|
|
----
|
|
|
|
Most MIDI messages are three bytes long, and the three numbers you are handed
|
|
are those bytes exactly as the device sent them. A message with fewer bytes
|
|
hands you a `0` in place of each one it does not have.
|
|
|
|
`status` says what kind of message it is and which channel it came from, mixed
|
|
together: the kind is the top four bits and the channel is the bottom four.
|
|
`0x90` is note on, and a note on channel 1 arrives as `0x90` while the same
|
|
note on channel 5 arrives as `0x94`. `status & 0xF0` keeps the top half and
|
|
throws the channel away, which is what you want unless you care which channel
|
|
it was. (`&` is Lua's bitwise and, and `0x90` is a number written in
|
|
hexadecimal -- base sixteen -- because that is how every MIDI document in the
|
|
world writes them.)
|
|
|
|
`data1` and `data2` are the other two bytes, and what they mean depends on the
|
|
kind. For a note on they are the key and the velocity: key `60` is middle C,
|
|
and velocity is how hard it was struck, `1` to `127`.
|
|
|
|
The check for `data2 == 0` is not fussiness. A note on with a velocity of zero
|
|
means *note off*: it is how nearly every keyboard ever made says "I let go",
|
|
and if you do not filter it your game will fire twice for every key.
|
|
|
|
`bytes` is the whole message as a string, which is how you read a message
|
|
longer than three bytes.
|
|
|
|
=== Playing
|
|
|
|
Sending is easier than receiving.
|
|
|
|
[source,lua]
|
|
----
|
|
midiNoteOn(1, 60, 100)
|
|
midiNoteOff(1, 60)
|
|
----
|
|
|
|
Middle C on channel 1, struck at a hundred, and then released. Channels here
|
|
are `1` to `16` as a person counts them, not `0` to `15` as the wire does,
|
|
which is Singe being kind to you.
|
|
|
|
Every `midiNoteOn` needs a `midiNoteOff`. A note that is never released sounds
|
|
until something stops it, and the classic first MIDI bug is a game that
|
|
crashes or quits with three notes held down and leaves a chord hanging in the
|
|
room. So pair them, and pair them through a timer rather than by hand:
|
|
|
|
[source,lua]
|
|
----
|
|
function playTarget()
|
|
if not midiIsOutputOpen() then
|
|
return
|
|
end
|
|
local key = target
|
|
midiNoteOn(1, key, 100)
|
|
timerAfter(HOLD_MS, function()
|
|
midiNoteOff(1, key)
|
|
end)
|
|
end
|
|
----
|
|
|
|
`local key = target` is doing real work. `timerAfter` runs its function four
|
|
tenths of a second later, and by then `target` may have changed to a different
|
|
note; copying it into a local means the timer releases the note that was
|
|
actually pressed. Copy anything a timer will need later.
|
|
|
|
The other sending calls do the rest of what MIDI is: `midiProgramChange`
|
|
chooses which instrument a channel plays, `midiControlChange` moves a
|
|
controller (volume is 7, pan is 10, the sustain pedal is 64),
|
|
`midiPitchBend` bends a channel, and `midiSend` sends any message at all as
|
|
raw bytes for the things the named calls do not cover.
|
|
|
|
=== A Reason to Care
|
|
|
|
All of which is machinery. Here is what it is for.
|
|
|
|
Put the two halves together and the game asks for a note, the player plays it
|
|
on a real instrument, and the game knows whether they got it right:
|
|
|
|
[source,lua]
|
|
----
|
|
function onMidiMessage(status, data1, data2, bytes)
|
|
if (status & 0xF0) ~= MIDI_NOTE_ON or data2 == 0 then
|
|
return
|
|
end
|
|
heard = noteName(data1)
|
|
if data1 == target then
|
|
score = score + 1
|
|
pickTarget()
|
|
playTarget()
|
|
end
|
|
end
|
|
----
|
|
|
|
Twelve lines, and you have the skeleton of a music teaching game. Add a clock
|
|
and you have a rhythm game that scores real playing rather than four plastic
|
|
buttons. Add `velocity` to the test and it cares how hard you hit the note.
|
|
Compare the time the note arrived against the time it was due and you are
|
|
grading timing to the millisecond.
|
|
|
|
And because MIDI goes out as well as in, a cabinet can do things nothing else
|
|
can. A real instrument wired to the machine. An external sound module playing
|
|
the score so the tune has depth no sample bank gives it. Stage lights on the
|
|
cabinet that flash on the beat because the game sent them the beat. If you are
|
|
the sort of person who builds arcade cabinets, this is the call that lets the
|
|
cabinet be a thing rather than a box round a computer.
|
|
|
|
The finished script is `learn/27-midi.singe`: the music controls, the port
|
|
list, and the guessing game, in about a hundred lines. The space bar sends the
|
|
note out, Left Alt pauses the music, Left Shift fades it, and the up and down
|
|
arrows move the volume. Run it with no MIDI hardware and the music half works
|
|
and the MIDI half says `none` twice, which is how it should behave on a
|
|
player's machine.
|
|
|
|
=== Cleaning Up
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
midiCloseInput()
|
|
midiCloseOutput()
|
|
musicUnload(theme)
|
|
end
|
|
----
|
|
|
|
`onShutdown` is called once when the game ends, and it is where a script gives
|
|
things back. Closing a port that is not open does nothing, so there is nothing
|
|
to check first.
|
|
|
|
=== Try It
|
|
|
|
. *Make it loop four times instead of forever.* Change `musicPlay(theme, -1)`
|
|
to `musicPlay(theme, 3)` and count. The number is how many times it
|
|
*repeats*, not how many times it plays.
|
|
. *Fade instead of stopping.* Change the button that stops the music to
|
|
`musicStop(theme, 4000)` and listen to the difference four seconds makes.
|
|
Then try `musicStop(theme, 100)`.
|
|
. *Show the channel.* In `onMidiMessage`, work out the channel with
|
|
`(status & 0x0F) + 1` and print it. Play your keyboard and see which channel
|
|
it is on -- and then find its settings and change that channel, to prove to
|
|
yourself that the number is real.
|
|
. *Score the velocity.* Give a point for the right note, and three points if
|
|
`data2` is over a hundred, so the game rewards playing it like you mean it.
|
|
. *Play the answer.* When the player gets a note right, make the game play a
|
|
small chord back at them with three `midiNoteOn` calls a couple of notes
|
|
apart and three timers to release them. You will need a real output port or
|
|
a software synthesiser for this, and on Linux that is any program that
|
|
registers itself with ALSA.
|
|
|
|
=== Break It on Purpose
|
|
|
|
The two volume scales are easy to mix up, and Singe would rather stop than
|
|
play your music at a level you did not mean. Add one line under
|
|
`musicPlay(theme, -1)`, using the number that would be full volume for a sound
|
|
effect if you doubled it in your head and got it wrong:
|
|
|
|
[source,lua]
|
|
----
|
|
musicSetVolume(200)
|
|
----
|
|
|
|
Singe stops before the window opens, naming the line that call is on in your
|
|
file:
|
|
|
|
----
|
|
107:musicSetVolume: Invalid music volume value: 200
|
|
----
|
|
|
|
The line, the call that objected, and the complaint, which here quotes the
|
|
number back at you so there is no argument about what arrived. Music is `0` to
|
|
`128`. Sound effects are `0` to `63`. When a volume call stops your game, the
|
|
question to ask is which of the two families you are talking to.
|
|
|
|
=== What You Learned
|
|
|
|
* Music and sound effects are separate in Singe: their own volumes, no channel
|
|
limit for music, and a pause that follows the game's pause.
|
|
* A music file is decoded into memory up front, so a long track costs memory
|
|
and should be unloaded when you leave the level.
|
|
* `musicPlay(track, -1)` loops forever; a positive number is how many times it
|
|
repeats.
|
|
* Music volume is `0` to `128` and effects volume is `0` to `63`, and the
|
|
difference is Hypseus compatibility, not a mistake.
|
|
* `musicStop(track, milliseconds)` fades out, and a fade is the cheapest
|
|
polish there is. There is no crossfade, because one gain covers all music.
|
|
* Singe presents video against the audio clock, and `singeSetAudioDelay`
|
|
corrects for the delay of whatever the sound comes out of.
|
|
* A MIDI file holds notes, not sound, and needs a soundfont to be heard at
|
|
all. Singe ships none, so ship your own or your music is whatever the
|
|
player's machine happens to have.
|
|
* MIDI ports are a separate thing entirely: live messages in and out, on a
|
|
wire, with nothing to do with MIDI files.
|
|
* `onMidiMessage` hands you the raw bytes. The top four bits of `status` are
|
|
the kind, the bottom four are the channel, and a note on with velocity zero
|
|
means note off.
|
|
* Every `midiNoteOn` needs a `midiNoteOff`, and a timer is how you pair them.
|
|
* On a machine with no MIDI, every call answers `false` or zero and nothing
|
|
fails, so MIDI support is safe to ship.
|
|
|
|
=== Next Time
|
|
|
|
Your game has music, and a score that a MIDI keyboard can run up. Lesson
|
|
twenty-eight takes that score off the machine: accounts, high score tables
|
|
that other people can see, and the catalogue that lists your game to the
|
|
world.
|