476 lines
23 KiB
Text
476 lines
23 KiB
Text
[#_lesson_27_music_and_midi]
|
|
== Lektion 27: Musik und MIDI
|
|
|
|
image::learn/27-midi.png[Die fertige Lektion, 480]
|
|
|
|
Diese Lektion besteht aus zwei Lektionen, die zufällig zusammenwohnen.
|
|
|
|
Die erste Hälfte ist Musik: wie ein Spiel ein Musikstück richtig abspielt, warum das nicht dasselbe
|
|
ist, wie einen Soundeffekt abzuspielen, und was zu tun ist, wenn die Musik mit dem Video im Takt
|
|
bleiben muss.
|
|
|
|
Die zweite Hälfte ist MIDI, dem die meisten Leute noch nie begegnet sind und das einem Spiel etwas
|
|
gibt, was nichts anderes kann. Am Ende spielt ein Keyboard, das an deinen Computer angeschlossen
|
|
ist, dein Spiel.
|
|
|
|
[#_a_sound_effect_and_a_piece_of_music_are_different_things]
|
|
=== Ein Soundeffekt und ein Musikstück sind verschiedene Dinge
|
|
|
|
Wie man einen Sound abspielt, weißt du schon. Lektion zehn hat eine `.wav` mit `soundLoad` geladen
|
|
und mit `soundPlay` abgefeuert, und wenn du diese beiden Aufrufe auf einen vier Minuten langen Song
|
|
richtest, spielen sie ihn ab.
|
|
|
|
Lass es. Singe hält Soundeffekte und Musik auseinander, und diese Trennung lohnt sich aus vier
|
|
Gründen.
|
|
|
|
* *Sie haben ihre eigenen Lautstärken.* `soundSetVolume` rührt die Musik nie an, und
|
|
`musicSetVolume` rührt die Schüsse nie an. Jedes Spiel, das je veröffentlicht wurde, hat das
|
|
gebraucht, denn Spieler drehen die Musik leiser und die Effekte lauter.
|
|
* *Musik hat keine Kanalgrenze.* Soundeffekte teilen sich sechzehn Kanäle, und wenn alle sechzehn
|
|
belegt sind, spielt der siebzehnte nicht. Ein Musiktrack liegt auf keinem Kanal, also kann ihn
|
|
nichts verdrängen.
|
|
* *Ein Handle ist ein Track*, und er pausiert, läuft weiter, stoppt und blendet aus, ganz für sich,
|
|
ohne etwas anderes zu stören.
|
|
* *Musik pausiert mit dem Spiel.* Drück die Pausetaste, und sie hält an, wo sie ist. Komm zurück,
|
|
und sie macht weiter. Du schreibst nichts, damit das passiert.
|
|
|
|
Eine Sache, die *nicht* anders ist, solltest du kennen, weil die Leute das Gegenteil annehmen:
|
|
Singe dekodiert eine Musikdatei vorab in den Speicher, genau wie einen Soundeffekt. Ein langer Track
|
|
kostet Speicher. Er wird nicht Stück für Stück von der Festplatte gestreamt. Fünf Minuten Musik sind
|
|
ein paar Dutzend Megabyte Speicher, solange sie geladen sind, also lade den Track für das Level, in
|
|
dem du gerade bist, und gib ihn mit `musicUnload` frei, wenn du es verlässt.
|
|
|
|
[#_playing_a_track]
|
|
=== Einen Track abspielen
|
|
|
|
Jeder Leser dieses Buchs hat schon ein Musikstück, denn die Engine hat eins in deinen Arbeitsordner
|
|
entpackt, als du zum ersten Mal irgendetwas gestartet hast. Fang eine Datei mit diesen drei Zeilen
|
|
an:
|
|
|
|
[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
|
|
----
|
|
|
|
Starte es mit `Singe -R music`. Zehn Sekunden Musik, dann fängt sie von vorn an, und sie fängt immer
|
|
wieder von vorn an.
|
|
|
|
Das macht die `-1`. `musicPlay` nimmt eine Anzahl von *Wiederholungen*: Lass sie weg oder übergib
|
|
`0`, und der Track spielt einmal; übergib `3`, und er spielt insgesamt viermal; übergib `-1`, und er
|
|
wiederholt sich, bis ihn etwas stoppt. Ein Menü, ein Titelbildschirm oder der Hintergrund eines
|
|
Levels ist fast immer `-1`.
|
|
|
|
Zwei Details in diesem kleinen Skript sind die Tinte wert. `musicLoad` nimmt einen Pfad und keinen
|
|
Pfad mit `DIR ..`, weil `Singe/menuIntro.flac` eine der eigenen Dateien der Engine ist und in dem
|
|
Ordner liegt, den die Engine in deinen Arbeitsordner entpackt hat. `musicIsPlaying` antwortet `true`
|
|
oder `false`, und das `and ... or ...` mitten in diesem `overlayPrint` ist die Lua-Kurzform aus
|
|
Lektion drei, um einen von zwei Werten auszuwählen.
|
|
|
|
[#_the_volume_scale_is_not_the_one_you_know]
|
|
=== Die Lautstärkeskala ist nicht die, die du kennst
|
|
|
|
`musicSetVolume(64)` stellt sie auf etwa die Hälfte. Soundeffekte gehen von `0` bis `63`; Musik geht
|
|
von `0` bis `128`, also doppelt so weit.
|
|
|
|
Das ist kein Fehler und auch kein Witz. Singe kann Spiele ausführen, die für Hypseus geschrieben
|
|
wurden, eine Engine, deren Musiklautstärke schon immer auf einer Skala von `0` bis `128` lag, und
|
|
die Zahlen zu ändern würde die Musik dieser Spiele still und heimlich doppelt so laut machen, wie
|
|
ihre Autoren es wollten. Also behält Singe beide Skalen, jede an die Familie von Aufrufen gebunden,
|
|
zu der sie gehört. Effekte: `0` bis `63`. Musik: `0` bis `128`. Gib `musicSetVolume` eine Zahl
|
|
außerhalb dieses Bereichs, und das Skript hält an; das siehst du am Ende dieser Lektion selbst.
|
|
|
|
[#_what_singe_will_play]
|
|
=== Was Singe abspielt
|
|
|
|
Fast alles. WAV, Ogg Vorbis, Opus, MP3, FLAC, WavPack, AAC und darüber hinaus eine lange Liste,
|
|
alles über dasselbe `musicLoad`.
|
|
|
|
Zwei Familien sind interessanter als die gewöhnlichen, und beide gibt es, weil sie winzig sind.
|
|
|
|
*Tracker-Module* -- `.mod`, `.s3m`, `.xm`, `.it` und gut fünfzig Verwandte -- enthalten eine Handvoll
|
|
kurzer aufgenommener Instrumente und eine Liste, auf welchen Noten sie gespielt werden sollen. Eine
|
|
vier Minuten lange Melodie kann unter hundert Kilobyte bleiben.
|
|
|
|
*Chiptunes* -- `.nsf`, `.spc`, `.vgm`, `.gbs` und ihre Verwandtschaft -- gehen noch weiter: Sie
|
|
enthalten die *Registerschreibzugriffe* für den echten Soundchip eines Nintendo, eines Mega Drive
|
|
oder eines Spectrum, und Singe emuliert den Chip, um sie abzuspielen. Wenn ein Spiel nach 1989
|
|
klingen soll und nicht nach einer Aufnahme von 1989, geht es so.
|
|
|
|
Beide lassen sich wie alles andere mit `musicLoad` laden. Der Abschnitt *Video-, Audio- und
|
|
Containerformate* in der Referenz hat die vollständige Liste, auch welcher Container zu welchem Codec
|
|
gehört.
|
|
|
|
[#_stopping_pausing_and_fading]
|
|
=== Stoppen, Pausieren und Ausblenden
|
|
|
|
[source,lua]
|
|
----
|
|
musicPause(theme)
|
|
musicResume(theme)
|
|
musicStop(theme)
|
|
musicStop(theme, 2000)
|
|
----
|
|
|
|
`musicPause` hält den Track an, wo er ist, und `musicResume` macht an dieser Stelle weiter.
|
|
`musicStop` beendet ihn, und beim nächsten Abspielen fängt der Track wieder von vorn an. Mit einer
|
|
zweiten Zahl blendet `musicStop` den Track über so viele Millisekunden aus, statt ihn abrupt
|
|
abzuschneiden -- hier zwei Sekunden. Dieses Ausblenden ist das Einfachste überhaupt, was du tun
|
|
kannst, damit sich ein Spiel fertig anfühlt. Musik, die mitten im Takt aufhört, wenn der Spieler
|
|
durch eine Tür geht, klingt wie ein Bug, selbst für jemanden, der nicht sagen könnte, warum.
|
|
|
|
Ruf einen der vier ganz ohne Handle auf, und er wirkt auf alle Musik auf einmal; genau das will ein
|
|
Pausenbildschirm.
|
|
|
|
Jetzt die ehrliche Grenze, denn du wirst nach dieser Funktion suchen, und sie ist nicht da: *Singe
|
|
kann keine zwei Tracks überblenden.* `musicSetVolume` ist eine einzige Verstärkung über alle Musik,
|
|
keine Lautstärke pro Track, also gibt es keine Möglichkeit, einen Track lauter zu machen, während ein
|
|
anderer leiser wird. Was du tun kannst, ist, den alten auszublenden und den neuen zu starten, während
|
|
er verklingt; für einen Levelwechsel ist das nah genug dran, und für ein Kampfthema, das unter ein
|
|
Erkundungsthema gleitet, hörbar nicht dasselbe. Wenn du heute eine echte Überblendung brauchst,
|
|
machst du aus den beiden Stücken eine einzige Datei und springst darin, oder du nimmst die
|
|
Überlappung in Kauf.
|
|
|
|
[#_music_video_and_staying_in_step]
|
|
=== Musik, Video und im Takt bleiben
|
|
|
|
Wenn dein Spiel Video abspielt -- das Thema von Teil drei -- gibt es eine Sache über Sound zu
|
|
wissen, und sie ist überhaupt kein Musik-Aufruf.
|
|
|
|
Singe zeigt einen Frame des Videos dann, wenn der Sound zu hören ist, der dazugehört, nicht nach
|
|
einem Timer. Der Ton ist die Uhr, und das Bild folgt ihm. So herum ist es richtig, denn ein Ohr
|
|
bemerkt eine Fünftelsekunde Versatz in einer Stimme, und ein Auge bemerkt denselben Versatz an
|
|
einem Mund nicht.
|
|
|
|
Die Engine misst beim Start ihren eigenen Audioweg. Was sie nicht messen kann, ist alles, nachdem
|
|
das Betriebssystem den Ton weitergereicht hat: ein Verstärker, ein Fernseher, ein HDMI-Receiver,
|
|
ein Bluetooth-Headset. Jedes davon fügt seine eigene Verzögerung hinzu, sie ist auf jedem Rechner
|
|
anders, und sie zeigt sich darin, dass der Ton etwas später ankommt als das Ding, das ihn gemacht
|
|
hat.
|
|
|
|
Die Lösung ist eine einzige Zahl:
|
|
|
|
[source,lua]
|
|
----
|
|
singeSetAudioDelay(45)
|
|
----
|
|
|
|
Fünfundvierzig Millisekunden „der Ton ist spät; halte das Bild entsprechend zurück“. Du kannst sie
|
|
auch als `--audiodelay=45` auf der Befehlszeile übergeben oder in den Eintrag des Spiels in
|
|
`games.dat` schreiben. `singeGetAudioDelay` liest zurück, was gerade gilt, und das zeigt ein
|
|
Service-Menü neben seinem Schieberegler an.
|
|
|
|
Du musst die Zahl nicht raten. Singes eigenes Menü hat einen Kalibrierungsbildschirm: Er spielt
|
|
einmal pro Sekunde einen Klick und lässt den Bildschirm weiß aufblitzen, du stellst nach, bis Blitz
|
|
und Klick gleichzeitig passieren, und er speichert das Ergebnis für jedes Spiel auf diesem Rechner.
|
|
Der Abschnitt *Audio-Synchronisation* in der Referenz beschreibt ihn und erklärt, warum er funktioniert.
|
|
Wenn du einen Automaten baust, lass ihn einmal laufen, nachdem du die Lautsprecher angeschlossen
|
|
hast, und denk nie wieder daran.
|
|
|
|
[#_the_other_thing_called_midi]
|
|
=== Das andere Ding namens MIDI
|
|
|
|
MIDI sind zwei Dinge, die nichts miteinander zu tun haben, unter einem Namen, und sie
|
|
auseinanderzuhalten ist schon der größte Teil davon, es zu verstehen.
|
|
|
|
Hier ist das erste, in einem Absatz. Eine Aufnahme -- eine `.wav`, eine `.mp3` -- ist eine Messung
|
|
eines Klangs, vierundvierzigtausendmal pro Sekunde genommen. MIDI ist das nicht. Eine MIDI-Datei
|
|
enthält, *was gespielt wurde*: in diesem Moment, auf diesem Kanal, drück Taste sechzig so fest;
|
|
etwas später lass sie los. Sie ist näher an Noten oder an der Lochrolle eines selbstspielenden
|
|
Klaviers als an einer Aufnahme. Darum hat eine MIDI-Datei ein paar Kilobyte, wenn dieselbe Musik als
|
|
Audio ein paar Megabyte hat, und darum enthält eine MIDI-Datei auch überhaupt keinen Klang.
|
|
Irgendetwas muss die Noten spielen.
|
|
|
|
Eine MIDI-Datei ist Musik, also spielt sie wie Musik:
|
|
|
|
[source,lua]
|
|
----
|
|
local tune = musicLoad("music/overworld.mid")
|
|
musicPlay(tune, -1)
|
|
----
|
|
|
|
Aber das Ding, das die Noten spielt, muss irgendwoher kommen, und dieses Ding ist ein *SoundFont*:
|
|
eine Datei voller aufgenommener Instrumente -- ein Klavier, eine Trompete, eine Snare-Drum -- mit
|
|
markierten Noten und Schleifenpunkten, damit die Engine daraus jede Note jedes Instruments bauen
|
|
kann.
|
|
|
|
Singe liefert keinen mit. Ein guter SoundFont hat Dutzende Megabyte, und das wären Dutzende
|
|
Megabyte, die jede Kopie der Engine mit sich herumträgt, für ein Format, das die meisten Spiele nie
|
|
anfassen. Also nennst du einen mit `--soundfont`, oder du legst einen nach `Singe/soundfont.sf2`,
|
|
damit ein gepacktes Spiel seinen eigenen mitbringt, oder du lässt Singe den finden, den deine
|
|
Linux-Distribution installiert hat. Gibt es nirgends einen SoundFont, schlägt das Laden einer `.mid`
|
|
fehl und sagt das auch.
|
|
|
|
Darum klingt dieselbe MIDI-Datei auf zwei Rechnern verschieden, und darum klang sie auf den
|
|
Computern der Neunzigerjahre so schlecht. Die Datei ist eine Liste von Noten. Was du hörst, ist der
|
|
SoundFont, der sie gerade gespielt hat: Ein guter ist ein Raum voller Instrumente, ein billiger ist
|
|
ein Raum voller Spielzeug, und die Noten sind in beiden gleich. Wenn eine MIDI-Datei in dein Spiel
|
|
kommt, liefere den SoundFont mit. Sonst hast du nicht entschieden, wie deine Musik klingt; du hast
|
|
es dem überlassen, was auf dem Rechner des Spielers installiert ist.
|
|
|
|
[#_midi_as_a_wire]
|
|
=== MIDI als Leitung
|
|
|
|
Jetzt das zweite Ding, und das ist das interessante.
|
|
|
|
MIDI ist auch ein Kabel, und Nachrichten, die darüber laufen. Schließ ein Musik-Keyboard an deinen
|
|
Computer an, und jede Taste, die du drückst, schickt eine Nachricht: *Note On, Kanal 1, Taste 60,
|
|
Anschlagstärke 90*. Dreh an einem Regler einer Controller-Oberfläche, und sie schickt *Controller 7
|
|
auf Kanal 1 steht jetzt auf 40*. Diese Nachrichten gibt es, ob gerade Musik läuft oder nicht. Sie
|
|
sind ein weiteres Eingabegerät, wie ein Joystick mit achtundachtzig Knöpfen, die wissen, wie fest du
|
|
sie anschlägst.
|
|
|
|
Und es geht auch in die andere Richtung. Dein Spiel kann solche Nachrichten hinausschicken, an einen
|
|
Synthesizer, ein Soundmodul, eine Bühnenlichtsteuerung oder irgendetwas anderes an der Leitung.
|
|
|
|
Ports werden ab null gezählt, und Singe öffnet nichts, bis du darum bittest -- ein Spiel, das MIDI
|
|
nie erwähnt, zahlt auch nie für MIDI. Ein Eingangsport und ein Ausgangsport dürfen gleichzeitig
|
|
offen sein. Mach so eine Bestandsaufnahme des Rechners:
|
|
|
|
[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
|
|
----
|
|
|
|
Auf einem Rechner ganz ohne MIDI sind die Anzahlen null, die Aufrufe zum Öffnen antworten `false`,
|
|
und nichts schlägt fehl. Das ist wichtig: Es heißt, dass du diesen Code in ein echtes Spiel
|
|
schreiben und es ausliefern kannst, und die neunundneunzig von hundert Spielern ohne MIDI-Keyboard
|
|
merken nie, dass er da ist. Die Namen sind für einen Einstellungsbildschirm, auf dem ein Spieler den
|
|
Port auswählt, denn Port null ist selten der, den er will. `midiRescan` sucht noch einmal nach
|
|
etwas, das angeschlossen wurde, während das Spiel läuft.
|
|
|
|
[#_listening]
|
|
=== Zuhören
|
|
|
|
Jede ankommende Nachricht geht an einen Callback, genauso wie jeder Tastendruck an
|
|
`onInputPressed` geht:
|
|
|
|
[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
|
|
----
|
|
|
|
Die meisten MIDI-Nachrichten sind drei Bytes lang, und die drei Zahlen, die du bekommst, sind genau
|
|
diese Bytes, so wie das Gerät sie geschickt hat. Eine Nachricht mit weniger Bytes gibt dir für jedes,
|
|
das ihr fehlt, eine `0`.
|
|
|
|
`status` sagt, welche Art Nachricht es ist und von welchem Kanal sie kam, beides vermischt: Die Art
|
|
steht in den oberen vier Bits und der Kanal in den unteren vier. `0x90` ist Note On, und eine Note
|
|
auf Kanal 1 kommt als `0x90` an, während dieselbe Note auf Kanal 5 als `0x94` ankommt.
|
|
`status & 0xF0` behält die obere Hälfte und wirft den Kanal weg, und das willst du, außer es ist dir
|
|
wichtig, welcher Kanal es war. (`&` ist das bitweise Und von Lua, und `0x90` ist eine Zahl in
|
|
hexadezimaler Schreibweise -- zur Basis sechzehn -- weil jedes MIDI-Dokument der Welt sie so
|
|
schreibt.)
|
|
|
|
`data1` und `data2` sind die anderen beiden Bytes, und was sie bedeuten, hängt von der Art ab. Bei
|
|
einem Note On sind sie die Taste und die Anschlagstärke: Taste `60` ist das mittlere C, und die
|
|
Anschlagstärke ist, wie fest sie angeschlagen wurde, `1` bis `127`.
|
|
|
|
Die Prüfung auf `data2 == 0` ist keine Pedanterie. Ein Note On mit einer Anschlagstärke von null
|
|
bedeutet *Note Off*: So sagt fast jedes Keyboard, das je gebaut wurde, „ich habe losgelassen“, und
|
|
wenn du das nicht herausfilterst, löst dein Spiel bei jeder Taste zweimal aus.
|
|
|
|
`bytes` ist die ganze Nachricht als String; so liest du eine Nachricht, die länger als drei Bytes
|
|
ist.
|
|
|
|
[#_playing]
|
|
=== Spielen
|
|
|
|
Senden ist leichter als Empfangen.
|
|
|
|
[source,lua]
|
|
----
|
|
midiNoteOn(1, 60, 100)
|
|
midiNoteOff(1, 60)
|
|
----
|
|
|
|
Das mittlere C auf Kanal 1, mit hundert angeschlagen und dann losgelassen. Kanäle gehen hier von `1`
|
|
bis `16`, wie ein Mensch sie zählt, nicht von `0` bis `15` wie auf der Leitung; damit ist Singe nett
|
|
zu dir.
|
|
|
|
Jedes `midiNoteOn` braucht ein `midiNoteOff`. Eine Note, die nie losgelassen wird, klingt, bis etwas
|
|
sie stoppt, und der klassische erste MIDI-Bug ist ein Spiel, das abstürzt oder sich beendet, während
|
|
drei Noten gedrückt sind, und einen Akkord im Raum hängen lässt. Also bilde Paare, und zwar über
|
|
einen Timer statt von 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` leistet echte Arbeit. `timerAfter` führt seine Funktion vier Zehntelsekunden
|
|
später aus, und bis dahin kann sich `target` in eine andere Note geändert haben; wenn du es in eine
|
|
lokale Variable kopierst, lässt der Timer die Note los, die wirklich gedrückt wurde. Kopier alles,
|
|
was ein Timer später braucht.
|
|
|
|
Die anderen Sende-Aufrufe erledigen den Rest dessen, was MIDI ist: `midiProgramChange` wählt, welches
|
|
Instrument ein Kanal spielt, `midiControlChange` bewegt einen Controller (Lautstärke ist 7, Panorama
|
|
ist 10, das Haltepedal ist 64), `midiPitchBend` verbiegt die Tonhöhe eines Kanals, und `midiSend`
|
|
schickt jede beliebige Nachricht als rohe Bytes, für die Dinge, die die benannten Aufrufe nicht
|
|
abdecken.
|
|
|
|
[#_a_reason_to_care]
|
|
=== Ein Grund, sich dafür zu interessieren
|
|
|
|
Das alles ist Maschinerie. Hier ist, wofür sie da ist.
|
|
|
|
Setz die beiden Hälften zusammen, und das Spiel verlangt eine Note, der Spieler spielt sie auf einem
|
|
echten Instrument, und das Spiel weiß, ob sie richtig war:
|
|
|
|
[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
|
|
----
|
|
|
|
Elf Zeilen, und du hast das Gerüst eines Lernspiels für Musik. Füg eine Uhr hinzu, und du hast ein
|
|
Rhythmusspiel, das echtes Spielen bewertet statt vier Plastikknöpfe. Nimm `data2` in die Prüfung
|
|
auf, und es achtet darauf, wie fest du die Note anschlägst. Vergleich die Zeit, zu der die Note
|
|
ankam, mit der Zeit, zu der sie fällig war, und du bewertest das Timing auf die Millisekunde genau.
|
|
|
|
Und weil MIDI nicht nur hereinkommt, sondern auch hinausgeht, kann ein Automat Dinge, die nichts
|
|
anderes kann. Ein echtes Instrument, das an den Automaten angeschlossen ist. Ein externes Soundmodul,
|
|
das die Partitur spielt, damit die Melodie eine Tiefe hat, die ihr keine Sample-Bank gibt.
|
|
Bühnenlichter am Automaten, die im Takt blinken, weil das Spiel ihnen den Takt geschickt hat. Wenn
|
|
du zu den Leuten gehörst, die Arcade-Automaten bauen, ist das der Aufruf, mit dem der Automat ein
|
|
eigenes Ding wird statt eines Kastens um einen Computer.
|
|
|
|
Das fertige Skript ist `learn/27-midi.singe`: die Musiksteuerung, die Portliste und das Ratespiel,
|
|
in etwa hundert Zeilen. Die Leertaste schickt die Note hinaus, die linke Alt-Taste pausiert die
|
|
Musik, die linke Umschalttaste blendet sie aus, und die Pfeiltasten nach oben und unten ändern die
|
|
Lautstärke. Starte es ohne MIDI-Hardware, und die Musikhälfte funktioniert, und die MIDI-Hälfte sagt
|
|
zweimal `none`, und genau so soll es sich auf dem Rechner eines Spielers verhalten.
|
|
|
|
[#_cleaning_up]
|
|
=== Aufräumen
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
midiCloseInput()
|
|
midiCloseOutput()
|
|
musicUnload(theme)
|
|
end
|
|
----
|
|
|
|
`onShutdown` wird einmal aufgerufen, wenn das Spiel endet, und dort gibt ein Skript Dinge zurück.
|
|
Einen Port zu schließen, der nicht offen ist, bewirkt nichts, also musst du vorher nichts prüfen.
|
|
|
|
[#_try_it_27]
|
|
=== Probier es aus
|
|
|
|
. *Lass es viermal statt für immer laufen.* Ändere `musicPlay(theme, -1)` in
|
|
`musicPlay(theme, 3)` und zähl mit. Die Zahl gibt an, wie oft er sich *wiederholt*, nicht, wie
|
|
oft er spielt.
|
|
. *Blende länger aus.* Ändere das Ausblenden am Knopf, der die Musik stoppt, in
|
|
`musicStop(theme, 4000)` und hör dir an, was für einen Unterschied vier Sekunden machen. Dann
|
|
probier `musicStop(theme, 100)`.
|
|
. *Zeig den Kanal.* Rechne in `onMidiMessage` den Kanal mit `(status & 0x0F) + 1` aus und gib ihn
|
|
aus. Spiel auf deinem Keyboard und sieh nach, auf welchem Kanal es ist -- und dann such seine
|
|
Einstellungen und ändere diesen Kanal, um dir selbst zu beweisen, dass die Zahl echt ist.
|
|
. *Bewerte die Anschlagstärke.* Gib einen Punkt für die richtige Note und drei Punkte, wenn `data2`
|
|
über hundert liegt, damit das Spiel belohnt, wer die Note spielt, als meinte er es ernst.
|
|
. *Spiel die Antwort.* Wenn der Spieler eine Note richtig trifft, lass das Spiel ihm einen kleinen
|
|
Akkord zurückspielen, mit drei Aufrufen von `midiNoteOn`, ein paar Noten auseinander, und drei
|
|
Timern, die sie wieder loslassen. Dafür brauchst du einen echten Ausgangsport oder einen
|
|
Software-Synthesizer, und unter Linux ist das jedes Programm, das sich bei ALSA anmeldet.
|
|
|
|
[#_break_it_on_purpose_26]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Die beiden Lautstärkeskalen kann man leicht verwechseln, und Singe hält lieber an, als deine Musik
|
|
in einer Lautstärke abzuspielen, die du nicht wolltest. Füg unter `musicPlay(theme, -1)` eine Zeile
|
|
ein, mit der Zahl, die volle Lautstärke für einen Soundeffekt wäre, wenn du sie im Kopf verdoppelt
|
|
und dich dabei verrechnet hättest:
|
|
|
|
[source,lua]
|
|
----
|
|
musicSetVolume(200)
|
|
----
|
|
|
|
Singe hält an, bevor das Spiel startet, und nennt die Zeile in deiner Datei, in der dieser
|
|
Aufruf steht:
|
|
|
|
----
|
|
107:musicSetVolume: Invalid music volume value: 200
|
|
----
|
|
|
|
Die Zeile, der Aufruf, der sich gewehrt hat, und die Beschwerde, die dir hier die Zahl wörtlich
|
|
zurückgibt, damit es keinen Streit darüber gibt, was angekommen ist. Musik ist `0` bis `128`.
|
|
Soundeffekte sind `0` bis `63`. Wenn ein Lautstärke-Aufruf dein Spiel anhält, lautet die Frage, mit
|
|
welcher der beiden Familien du gerade sprichst.
|
|
|
|
[#_what_you_learned_27]
|
|
=== Was du gelernt hast
|
|
|
|
* Musik und Soundeffekte sind in Singe getrennt: eigene Lautstärken, keine Kanalgrenze für Musik
|
|
und eine Pause, die der Pause des Spiels folgt.
|
|
* Eine Musikdatei wird vorab in den Speicher dekodiert, also kostet ein langer Track Speicher und
|
|
sollte freigegeben werden, wenn du das Level verlässt.
|
|
* `musicPlay(track, -1)` wiederholt für immer; eine positive Zahl gibt an, wie oft sich der Track
|
|
wiederholt.
|
|
* Die Musiklautstärke geht von `0` bis `128` und die Effektlautstärke von `0` bis `63`, und der
|
|
Unterschied ist Kompatibilität mit Hypseus, kein Fehler.
|
|
* `musicStop(track, milliseconds)` blendet aus, und ein Ausblenden ist der billigste Feinschliff,
|
|
den es gibt. Es gibt keine Überblendung, weil eine einzige Verstärkung für alle Musik gilt.
|
|
* Singe zeigt Video nach der Uhr des Tons, und `singeSetAudioDelay` gleicht die Verzögerung dessen
|
|
aus, woraus der Ton kommt.
|
|
* Eine MIDI-Datei enthält Noten, keinen Klang, und braucht einen SoundFont, um überhaupt hörbar zu
|
|
sein. Singe liefert keinen mit, also liefere deinen eigenen mit, sonst ist deine Musik das, was der
|
|
Rechner des Spielers zufällig hat.
|
|
* MIDI-Ports sind etwas völlig anderes: Live-Nachrichten hinein und hinaus, auf einer Leitung, ohne
|
|
irgendetwas mit MIDI-Dateien zu tun.
|
|
* `onMidiMessage` gibt dir die rohen Bytes. Die oberen vier Bits von `status` sind die Art, die
|
|
unteren vier der Kanal, und ein Note On mit Anschlagstärke null bedeutet Note Off.
|
|
* Jedes `midiNoteOn` braucht ein `midiNoteOff`, und mit einem Timer bildest du die Paare.
|
|
* Auf einem Rechner ohne MIDI antwortet jeder Aufruf `false` oder null, und nichts schlägt fehl,
|
|
also kannst du MIDI-Unterstützung gefahrlos ausliefern.
|
|
|
|
[#_next_time_27]
|
|
=== Beim nächsten Mal
|
|
|
|
Dein Spiel hat Musik und einen Punktestand, den ein MIDI-Keyboard hochtreiben kann. Lektion
|
|
achtundzwanzig bringt diesen Punktestand vom Rechner weg: Konten, Highscore-Tabellen, die andere
|
|
Leute sehen können, und der Katalog, der dein Spiel der Welt zeigt.
|