Für mein Vektorspiel in MMBasic wollte ich zwei Dinge gleichzeitig hinbekommen: Hintergrundmusik und Soundeffekte, die einander nicht in die Quere kommen. Auf dem CMM2 geht das mit MOD-Dateien und festen Audio-Kanälen, und der Trick ist, von Anfang an zu wissen, welcher Effekt auf welchem Kanal landet.
Eine MOD-Datei für Musik, eine als Sample-Library
Eine MOD-Datei kann mehr als nur ein Lied sein. Sie enthält Patterns plus eine ganze Sample-Bibliothek, jedes Sample direkt ansteuerbar. Ich habe mir zwei MOD-Files gebaut, eines mit der Hintergrundmusik, eines, das nur die Soundeffekte als Samples enthält.
Im Spiel ist eines davon geladen. Wenn die Musik aus ist, lade ich stattdessen die Effekt-MOD, damit die Samples trotzdem zur Verfügung stehen.
CONST music_file$ = "spiel_music.mod"
CONST sounds_file$ = "spiel_sounds.mod"
IF music_on THEN
PLAY MODFILE music_file$
ELSE
PLAY MODFILE sounds_file$
END IF
Sample-Slots und Kanäle benennen
Was sich bewährt hat: Sample-Nummern und Kanal-Nummern bekommen feste Konstanten am Anfang des Programms. Damit hat man sie an einer Stelle, und der restliche Code liest sich wie eine Spielbeschreibung.
CONST SND_LASER = 12
CONST SND_TELEPORT = 13
CONST SND_UFO_SHOT = 15
CONST SND_EXPLODE_S = 16
CONST SND_EXPLODE_M = 17
CONST SND_EXPLODE_L = 18
CONST SND_THRUST = 19
CONST SND_CH_LASER = 1
CONST SND_CH_THRUST = 2
CONST SND_CH_EXPLODE = 3
CONST SND_CH_EVENT = 4
Die Kanäle sind das eigentliche Geheimnis. Ein Sample auf demselben Kanal überschreibt das vorherige, was bei manchen Effekten gewollt und bei anderen tödlich ist.
Einen Effekt abspielen
PLAY MODSAMPLE sample, channel startet ein Sample auf einem Kanal. Wenn der Kanal noch belegt ist, wird er gestoppt und überschrieben.
PLAY MODSAMPLE SND_LASER, SND_CH_LASER
PLAY MODSAMPLE SND_THRUST, SND_CH_THRUST
PLAY MODSAMPLE SND_EXPLODE_M, SND_CH_EXPLODE
Schüsse, Schub und Explosionen liegen jeweils auf eigenen Kanälen. Das heißt: ein Schuss schießt nicht den Triebwerks-Sound ab, eine Explosion stoppt nicht den Laser. Wichtig vor allem für den Schub-Sound, der so lange laufen muss, wie der Spieler beschleunigt.
Auf einem Event-Kanal wie SND_CH_EVENT lege ich Sounds, bei denen es ok ist, dass sie sich gegenseitig unterbrechen, etwa Teleport oder UFO-Alarm. Da käme ein zweiter Effekt sowieso erst an, wenn der erste schon abgeklungen wäre.
Lautstärke und Mute
Volumen passe ich global mit PLAY VOLUME an, hier mit zwei gleichen Werten für links und rechts:
DIM INTEGER volume
volume = 80
PLAY VOLUME volume, volume
Beim Umschalten zwischen Musik-MOD und Effekt-MOD ist PLAY STOP Pflicht, sonst stürzen sich die Streams aufeinander:
SUB ToggleMusic()
music_on = 1 - music_on
PLAY STOP
IF music_on THEN
PLAY MODFILE music_file$
ELSE
PLAY MODFILE sounds_file$
END IF
END SUB
Eine MOD-Datei ist Musik und Sample-Bibliothek in einem; auf zwei MODs aufgeteilt bekommt man sehr flexibles Audio mit minimalem RAM-Verbrauch. Ohne saubere Kanal-Trennung schießen sich die Sounds aber gegenseitig ab, und der Konstanten-Block oben im Programm war für mich die billigste Investition, die viele Stunden Debugging gespart hat. PLAY MODSAMPLE ist außerdem latenz-arm, weil keine WAV-Datei pro Aufruf von der Disk gelesen wird. Bei mir läuft das Spiel mit dem Muster aus zwei MODs plus festen Kanälen stabil und ohne Audio-Aussetzer.