Musik
Das Programmieren von Soundchips ist nicht trivial. Das habe ich damals auf dem C64 schon nicht kapiert. Mit dem Yamaha YM3812 oder auch OPL2 hat das Steckschwein einen weit komplexeren Chip als den SID, denn OPL2 kennt gleich ganze 9 Stimmen statt drei, und jede ist über eine Unzahl Parameter konfigurierbar.
Wie funktioniert der YM3812?
Wie kriegt man also einen Ton aus diesem Monstrum? Die beste Quelle zum Thema OPL2 ist wohl “Programming the AdLib/Sound Blaster FM Music Chips” von Jeffrey S. Lee. Zumindest wird einem hier schnell klar, was auf einen zukommt, will man auch nur einen einfachen Ton ausgeben. So hat der OPL2-Chip insgesamt 244 Register, die neben den Stimmen auch die integrierten Timer konfigurieren, und belegt 2 Portadressen. Konkret bedeutet das, dass man in Adresse 1 die Nummer des gewünschen Registers schreibt. Nach 3.3µs liegt an Adresse 2 das gewählte Register zum Beschreiben an. Lesen läßt sich nur das Statusregister. Hat man also das gewählte Register beschrieben, ist der Chip dann 23µs nicht ansprechbar.
Zunächst müssen wir also dafür sorgen, dass der Soundchip seine Daten mit dem richtigen Timing bekommt. Der Einfachheit halber machen wir das nicht über Timer, sondern mit NOPs. Die Länge der Nopslides zu berechnen, überlassen wir dem Assembler. Um Platz zu sparen, nutzen wir eine Nopslide mit zwei Einsprüngen.
opl2_data_delay_time = 25000 opl2_reg_delay_time = 5000
opl2_data_delay = ((opl2_data_delay_time - opl2_reg_delay_time) / (1000/clockspeed)) / 2 -12 opl2_reg_delay = (opl2_reg_delay_time / (1000/clockspeed)) / 2 -12
Und die entsprechenden Subroutinen. Die je 6 Zyklen für JSR und RTS sind ja oben schon abgezogen:
opl2_delay_data: ; 23000ns / 0 .repeat opl2_data_delay nop .endrepeat
opl2_delay_register: ; 3300 ns .repeat opl2_reg_delay nop .endrepeat rts
Das wäre also geklärt.
Futter für den Soundchip
Nachdem also schonmal klar ist, auf welch umständliche Weise der Chip mit Daten betankt werden will, bleibt nur noch die Frage: Betanken womit? FM-Synthese ist ein zu weites Feld, als dass wir dort jetzt tief einsteigen wollen. Viel naheliegender wäre ein Player für eingängige Musikfiles. Erste Experimente von Marko mit den von DosBox erzeugten DRO Files waren schon recht vielversprechend. Leider ist es etwas umständlich, mit DosBox neue Musikstücke zu konvertieren, und auch die Trefferquote für lauffähige Stücke ist nicht besonders hoch. Zudem ist der von uns verwendete Player ein ziemlicher Hack mit per NOP grob hingefummelten Timings. Dieser war ursprünglich mal für ein OPL2-Modul für den C64 geschrieben worden. Was es nicht alles gibt. MIDI-Files wollen wir uns auch noch nicht antun, weil wir hier eine Umsetzung der verwendeten MIDI-Instrumente in OPL2-Parameter hätten bauen müssen. Ideal wäre ein Dateiformat, das die OPL2-Registerwerte bereits enthält.
Zum Glück hat sich damals id-Software zu Zeiten der Commander Keen-Spiele etwas entsprechendes ausgedacht: Das IMF-Format. Dieses Format wurde für eine Reihe früher id-Software-Spiele und deren Ableger verwendet, von Commander Keen 4-6 über Duke Nukem II bis hin zu Wolfenstein 3D. Dementsprechend groß ist die Anzahl der verfügbaren Musikstücke.
IMF-Dateien sind äußerst simpel aufgebaut, jede Datei ist im Prinzip eine Abfolge von 4byte-Paketen, die Registernummer, Registerwert und die Dauer der Pause bis zum nächsten Wert enthalten:
Register (8bit) | Wert (8bit) | Pause (16bit)
Die “Pause” ist in “Ticks” angegeben, welche sich auf die Abspielfrequenz des jeweiligen Stückes bezieht. Diese ist meist entweder 560Hz oder 700Hz. Hier kommt dann ein Timer-Interrupt zum Einsatz, der 560 oder 700mal in der Sekunde ausgeführt wird. Hierzu verwenden wir Timer 1 des 6522 VIA. Der OPL2 Chip hat zwar auch Timer, aber diese basieren auf festen Intervallen von 80µs bzw 320µs, was in unserem Fall nicht so richtig aufgeht.
Der Plan ist folgender: Das IMF-File wird komplett in den Speicher geladen. Dann positionieren wir einen Zeiger auf den Anfang der im Speicher befindlichen Daten.
In der Zeropage benutzen wir 2 Bytes als unseren Delay-Zähler. Diesen setzen wir inital auf 0. In der Interrupt-Routine prüfen wir als erstes, ob der Delay-Zähler 0 ist. Wenn nicht, dekrementieren wir ihn und verlassen die Routine wieder. Ist der Zähler 0, setzen wir das Datenbyte aus unseren IMF-Daten in das vorgesehene Register. Dann rücken wir den Datenzeiger um 4 Bytes weiter, setzen den Delay-Zähler neu, und verlassen den Interrupt.
player_isr:
pha
phy
bit via1ifr ; Interrupt from VIA?
bpl @isr_end
bit via1t1cl ; Acknowledge timer interrupt by reading channel low
; delay counter zero?
lda delayh
clc
adc delayl
beq @l1
; if no, 16bit decrement and exit routine
dec16 delayh
bra @isr_end
@l1:
ldy #$00
lda (imf_ptr),y
sta opl_stat
iny
lda (imf_ptr),y
jsr opl2_delay_register
sta opl_data
iny
lda (imf_ptr),y
sta delayh
iny
lda (imf_ptr),y
sta delayl
; song data end reached? then set state to 80 so loop will terminate
lda imf_ptr_h
cmp imf_end+1
bne @l3
lda imf_ptr
cmp imf_end+0
bne @l3
lda #$80
sta state
bra @isr_end
@l3:
;advance pointer by 4 bytes
clc
lda #$04
adc imf_ptr
sta imf_ptr
bcc @isr_end
inc imf_ptr_h
@isr_end:
; jump to kernel isr
ply
pla
jmp (old_isr)
Der vollständige Player ist in unserem Github-Repository zu finden. Wir gehen jetzt den Wolfenstein 3D-Soundtrack hören.