Home
Memoirs of a Gamer
Movies I watched
Guidebook
Links
The Ad Lib sound card and subsequent derivatives are quite the step up from a simple square wave generator like the PC Speaker, and even the Tandy 3 Voice sound chip. First released with the Yamaha YM3812 chip in 1987, referred to as OPL2, it set down a fairly complex and robust 9 channel FM synthesizer standard that would be followed for some time throughout the late 80s and the 90s. In 1992, Ad Lib would follow up it's success with the Yamaha YMF262, referred to as OPL3, which expanded the standard from 9 channels to 18, and expanded from 4 sine based waveforms to 8 sine and non-sine based waveforms. Don't worry if you don't know what any of that means in any real sense, because that's exactly what this section aims to make clear.
The Ad Lib sound card is programmed by sending data, 8 bits at a time, to registers stored inside the Yamaha chips by first writing the register's index to the Index port (0x388), then writing the desired value to the Data port (0x389). Status information can also be retrieved from the Yamaha chips by reading from the Index port (0x388), during which the port is usually referred to as the Status port. Unlike MIDI or Sound Blaster, the IO addresses are thankfully fixed for most normal Ad Lib compatible devices. There is an exception in the modern era to this in the form of the OPL2LPT and OPL3LPT devices by Serdashop, and this will be discussed more later in this section.
| Assembly | Borland Turbo C++ | Open Watcom |
mov al, 0x01
mov dx, 0x388
out dx, al
mov al, 0x20
mov dx, 0x389
out dx, al |
outport(0x388,0x01); outport(0x389,0x20); |
outp(0x388,0x01); outp(0x389,0x20); |
The original OPL2 specification used almost the entirety of the 8 bit index address space, so when OPL3 was laid out, the decision was made to duplicate the index address space and use 4 IO addresses instead of 2. This takes the shape of a second Index port (0x38A) and a second Data port (0x38B). Following the trend I've often seen, this section will refer to OPL3's index space as 0x100 to 0x1FF. So understand when this text says "Write 1, to register 0x105 to activate OPL3 functionality", what is meant is that index 5 is written to 0x38A followed by a 1 to 0x38B. Many of the OPL2 registers are reflected in the OPL3's secondary index address space. If this sounds a bit wonky, don't worry, it will become clear when we start going over writing to channel and operator registers.
| Assembly | Borland Turbo C++ | Open Watcom |
mov al, 0x05
mov dx, 0x38A
out dx, al
mov al, 0x01
mov dx, 0x38B
out dx, al |
outport(0x38A,0x05); outport(0x38B,0x01); |
outp(0x38A,0x05); outp(0x38B,0x01); |
| Assembly | Borland Turbo C++ | Open Watcom |
opl_write:
push bp
mov bp, sp
push ax
push dx
mov ax, [bp+4]
cmp ax, 0x100
jge .opl3
mov dx, 0x388
out dx, al
mov ax, [bp+6]
mov dx, 0x389
out dx, al
jmp .done
.opl3:
mov dx, 0x388
out dx, al
mov ax, [bp+6]
mov dx, 0x389
out dx, al
.done:
pop dx
pop ax
mov sp, bp
pop bp
ret |
void opl_write(short i, char v) {
if (i < 0x100) {
outport(0x388,i);
outport(0x389,v);
} else {
outport(0x38A,i);
outport(0x38B,v);
}
} |
void opl_write(short i, char v) {
if (i < 0x100) {
outp(0x388,0x01);
outp(0x389,0x20);
} else {
outp(0x38A,0x01);
outp(0x38B,0x20);
}
} |
| Assembly | Borland Turbo C++ | Open Watcom |
xor ax, ax
mov [opl2], al
push 0x60
push 0x04
call opl_write
add sp, 4
push 0x80
push 0x04
call opl_write
add sp, 4
mov dx,0x388
in al, dx
and al, 0xE0
mov [s1], al
push 0xFF
push 0x02
call opl_write
add sp, 4
;TODO add 80ms delay
push 0x21
push 0x04
call opl_write
add sp, 4
mov dx,0x388
in al, dx
and al, 0xE0
mov [s2], al
push 0x60
push 0x04
call opl_write
add sp, 4
push 0x80
push 0x04
call opl_write
add sp, 4
cmp byte [s1], 0
jne .done
cmp byte [s2], 0xC0
jne .done
inc [opl2]
.done: |
char s1, s2; opl_write(0x04,0x60); opl_write(0x04,0x80); s1 = inport(0x388) & 0xE0; opl_write(0x02,0xFF); opl_write(0x04,0x21); delay(80); s2 = inport(0x388) & 0xE0; opl_write(0x04,0x60); opl_write(0x04,0x80); opl2 = s1==0 && s2==0xC0; |
char s1, s2; opl_write(0x04,0x60); opl_write(0x04,0x80); s1 = inp(0x388) & 0xE0; opl_write(0x02,0xFF); opl_write(0x04,0x21); delay(80); s2 = inp(0x388) & 0xE0; opl_write(0x04,0x60); opl_write(0x04,0x80); opl2 = s1==0 && s2==0xC0; |
| Assembly | Borland Turbo C++ | Open Watcom |
xor ax, ax
mov [opl3], al
mov dx, 0x388
in al, dx
and al, 0x06
jnz .postopl3
push 0x01
push 0x105
call opl_write
add sp, 4
inc [opl3]
.postopl3: |
opl3 = 0;
if ((inport(0x388) & 0x06)==0) {
opl_write(0x105,0x01);
opl3 = 1;
} |
opl3 = 0;
if ((inp(0x388) & 0x06)==0) {
opl_write(0x105,0x01);
opl3 = 1;
} |
| Assembly | Borland Turbo C++ | Open Watcom |
;set op1 to 75
;that's Attack of 7 and a Decay of 5
push 0x75
push 0x68
call opl_write
add sp, 4
;same again with op2
push 0x75
push 0x6B
call opl_write
add sp, 4
;set the lower 8 bits of the F-Number.
;F-Number for an F in octave 4 is 0x1CA
push 0xCA
push 0xA3
call opl_write
add sp, 4
;Set the top 2 bits of the F-Number,
;the octave to 4,
;and turn the note on by oring 0x20!
push (0x20|(4<<2)|0x01)
push 0xB3
call opl_write
add sp, 4
|
\\set op1 to 75 \\that's Attack of 7 and a Decay of 5 opl_write(0x68,0x75); \\same again with op2 opl_write(0x6B,0x75); \\set the lower 8 bits of the F-Number. \\F-Number for an F in octave 4 is 0x1CA opl_write(0xA3,0xCA); \\Set the top 2 bits of the F-Number, \\the octave to 4, \\and turn the note on by oring 0x20! opl_write(0xB3,0x20|(4<<2)|0x01); | |
| Assembly | Borland Turbo C++ | Open Watcom |
;Although we're telling the note to
;turn off we still want the octave
;and F-Number to remain intact!
;Failing to do so will create weird
;Sound effects.
push ((4<<2)|0x01)
push 0xB3
call opl_write
add sp, 4
|
\\Although we're telling the note to \\turn off we still want the octave \\and F-Number to remain intact! \\Failing to do so will create weird \\Sound effects. opl_write(0xB3,(4<<2)|0x01); |
|
| Assembly | Borland Turbo C++ | Open Watcom |
;The Left playback bit is bit 4
;which makes 0x10 in hex.
push 0x10
push 0xC3
call opl_write
add sp, 4
;Let's play that F-4 again.
push 0xCA
push 0xA3
call opl_write
add sp, 4
push (0x20|(4<<2)|0x01)
push 0xB3
call opl_write
add sp, 4
|
\\The Left playback bit is bit 4 \\which makes 0x10 in hex. opl_write(0xC3,0x10); \\Let's play that F-4 again. opl_write(0xA3,0xCA); opl_write(0xB3,0x20|(4<<2)|0x01); | |
| Assembly | Borland Turbo C++ | Open Watcom |
;Cycle through all registers
;and set them to 0!
mov cx, 1
xor ax, ax
push ax
.clean_loop:
push cx
call opl_write
add sp, 2
;Don't forget the OPL3 registers!
or cx, 0x100
push cx
call opl_write
xor cx, 0x100
add sp, 2
inc cx
cmp cx, 0xF5
jle .clean_loop
add sp, 2
|
\\Cycle through all registers
\\and set them to 0!
int c;
for (c = 0x01; c <= 0xF5; c++) {
opl_write(c,0);
\\Don't forget the OPL3 registers!
opl_write(0x100|c,0);
}
| |
For the sake of clarity of function we will be breaking down registers into 3 categories:
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| Global | ||||||||
| 001 | WSEnable | Test Register | ||||||
| 002 | Timer 1 Count (80 usec resolution) | |||||||
| 003 | Timer 2 Count (320 usec resolution) | |||||||
| 004 | IRQReset | T1 Mask | T2 Mask | T2 Start | T1 Start | |||
| 104 | 4-OP B-E | 4-OP A-D | 4-OP 9-C | 4-OP 2-5 | 4-OP 1-4 | 4-OP 0-3 | ||
| 105 | OPL3 EN | |||||||
| 008 | CSW | Note-Sel | ||||||
| 0BD | Trem Dep | Vibr Dep | PercMode | BD On | SD On | TT On | CY On | HH On |
| Channel Wide | ||||||||
| #A0-#A8 | Frequency Number (Lower 8 bits) | |||||||
| #B0-#B8 | Note On | Block Number | F-Number (high bits) | |||||
| #C0-#C8 | Right | Left | FeedBack Modulation Factor | AM/FM | ||||
| Operator Specific | ||||||||
| #20-#35 | Tremolo | Vibrato | Sustain | KSR | Frequency Multiplication Factor | |||
| #40-#55 | Key Scale Level | Output Level | ||||||
| #60-#75 | Attack Rate | Decay Rate | ||||||
| #80-#95 | Sustain Level | Release Rate | ||||||
| #E0-#F5 | Waveform Select | |||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 001 | WSEnable | Test Register | ||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 002 | Timer 1 Count (80 usec resolution) | |||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 003 | Timer 2 Count (320 usec resolution) | |||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 004 | IRQReset | T1 Mask | T2 Mask | T2 Start | T1 Start | |||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 104 | 4-OP 11-14 | 4-OP 10-13 | 4-OP 9-12 | 4-OP 2-5 | 4-OP 1-4 | 4-OP 0-3 | ||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 105 | OPL3 EN | |||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 008 | CSW | Note-Sel | ||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 0BD | Trem Dep | Vibr Dep | PercMode | BD On | SD On | TT On | CY On | HH On |
| Channel # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| Channel Offset | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 |
| Op 1 | 0x00 | 0x01 | 0x02 | 0x08 | 0x09 | 0x0A | 0x10 | 0x11 | 0x12 |
| Op 2 | 0x03 | 0x04 | 0x05 | 0x0B | 0x0C | 0x0D | 0x13 | 0x14 | 0x15 |
| Channel # | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| Channel Offset | 0x100 | 0x101 | 0x102 | 0x103 | 0x104 | 0x105 | 0x106 | 0x107 | 0x108 |
| Op 1 | 0x100 | 0x101 | 0x102 | 0x108 | 0x109 | 0x10A | 0x110 | 0x111 | 0x112 |
| Op 2 | 0x103 | 0x104 | 0x105 | 0x10B | 0x10C | 0x10D | 0x113 | 0x114 | 0x115 |
| Channel # | 0 | 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |
| Channel Offset | 0x00 | 0x01 | 0x02 | 0x06 | 0x07 | 0x08 | 0x100 | 0x101 | 0x102 | 0x106 | 0x107 | 0x108 |
| Op 1 | 0x00 | 0x01 | 0x02 | 0x10 | 0x11 | 0x12 | 0x100 | 0x101 | 0x102 | 0x110 | 0x111 | 0x112 |
| Op 2 | 0x03 | 0x04 | 0x05 | 0x13 | 0x14 | 0x15 | 0x103 | 0x104 | 0x105 | 0x113 | 0x114 | 0x115 |
| Op 3 | 0x08 | 0x09 | 0x0A | 0x108 | 0x109 | 0x10A | ||||||
| Op4 | 0x0B | 0x0C | 0x0D | 0x10B | 0x10C | 0x10D | ||||||
| F-Number | Frequency | Note |
| 16B | 277.2 | C# |
| 181 | 293.7 | D |
| 198 | 311.1 | D# |
| 1B0 | 329.6 | E |
| 1CA | 349.2 | F |
| 1E5 | 370.0 | F# |
| 202 | 392.0 | G |
| 220 | 415.3 | G# |
| 241 | 440.0 | A |
| 263 | 466.2 | A# |
| 287 | 493.9 | B |
| 2AE | 523.3 | C |
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #A0-#A8 | Frequency Number (Lower 8 bits) | |||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #B0-#B8 | Note-On | Octave | F-Number (high 2 bits) | |||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #C0-#C8 | Right | Left | FeedBack Modulation Factor | AM/FM | ||||
| AM/FM Flag | Type | Pattern |
| 0 | FM | Output = Operator2(Operator1) |
| 1 | AM | Output = Operator1 + Operator2 |
| Hi Flag | Lo Flag | Type | Pattern |
| 0 | 0 | FM-FM | Output = Operator4(Operator3(Operator2(Operator1))) |
| 0 | 1 | FM-AM | Output = Operator4(Operator3(Operator2)) + Operator1 |
| 1 | 0 | AM-FM | Output = Operator4(Operator3) + Operator2(Operator1) |
| 1 | 1 | AM-AM | Output = Operator4 + Operator3(Operator2) + Operator1 |
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #20-#35 | Tremolo | Vibrato | Sustain | KSR | Frequency Multiplication Factor | |||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #40-#55 | Key Scale Level | Output Level | ||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #60-#75 | Attack Rate | Decay Rate | ||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #80-#95 | Sustain Level | Release Rate | ||||||
| Index | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| #E0-#F5 | Waveform Select | |||||||
| Value | Description | Visual |
| 0 | Sine | ![]() |
| 1 | Half Sine | ![]() |
| 2 | Absolute Sine | ![]() |
| 3 | Pulsed Absolute Sine | ![]() |
| 4 | Sine on even periods only | ![]() |
| 5 | Absolute Sine on even periods only | ![]() |
| 6 | Square | ![]() |
| 7 | Derived Square | ![]() |