Home Memoirs of a Gamer Movies I watched Guidebook Links

the Lobdegg's Comprehensive Guidebook to DOS Programming

Chapter 4 - Audio

Section 4: Ad Lib / OPL2 & OPL3

Initialization
Making Noise
Clean Up
Register Table
Global Registers
Channel Registers
Operator Registers
References

Introduction

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.

Note: Channels will be numbered 0-17 through out this section.

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.

To write 0x20 to index 0x01:
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.

To write 1 to index 0x105:
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);
This document will reference this opl_write() function in the given example code below.
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);
  }
}

Initialization

The official method is as follows:
  1. Reset both timers by writing 0x60 to index 0x04
  2. Set IRQReset by writing 0x80 to 0x04
  3. Read the Status port and save the result
  4. Write 0xFF to Timer 1 at index 0x02
  5. Delay for at least 80 microseconds
  6. Start Timer 1 by writing 0x21 to 0x04
  7. Read the Status port and save the result
  8. Reset both timers by writing 0x60 to index 0x04
  9. Set IRQReset by writing 0x80 to 0x04
  10. Bitwise AND both saved results by 0xE0. The first should result in 0x00, and the second should result in 0xC0. If both are true, OPL2 detected!
Note: For steps 1 & 2 keep in mind that when IRQReset is set, all other bits are ignored!
AssemblyBorland 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 0x01
    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(0x01,0xFF);
delay(80);
opl_write(0x04,0x21);
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(0x01,0xFF);
delay(80);
opl_write(0x04,0x21);
s2 = inp(0x388) & 0xE0;
opl_write(0x04,0x60);
opl_write(0x04,0x80);
opl2 = s1==0 && s2==0xC0;
To detect OPL3, after OPL2 is successfully detected read the Status port and AND it with 0x06, if the result is 0 then we have detected OPL3! Otherwise we just have bog standard OPL2.
To activate OPL3 features, once you've successfully detected OPL3, set the OPL3 flag to 1 in the index 0x105 register.
To write 1 to index 0x105:
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;
}

Making Noise

Creating sound in OPL2 mode is quite easy. All you need to do is:
  1. set the Attack / Decay register for a channel's operators
  2. set the channel's F-Number first via the channel's associated #A0 register.
  3. set the upper 2 bits of the F-Number, the octave, and set the Note On flag (= 1).
To turn on channel 3 with an F:
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);
To turn off channel 3:
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);
Getting sound going in OPL3 mode just requires one more step before setting the associated #A0-#A8 and #B0-#B8 registers:
The left or right (or both) flags of the channel's associated #C0-#C8 indexed register.
Set Channel 3 from before to play out the left speaker only:
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);

Clean up

Because the Ad Lib has no knowledge of what the rest of the computer is doing at any time, it's generally a good idea to have a program clean up after itself before closing.
The simplest way to achieve this is to simply set all registers to 0.
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);
}

Register Table

For the sake of clarity of function we will be breaking down registers into 3 categories:

  1. Global - configures chip wide features
  2. Channel - configures channel specific qualities
  3. Operator - configures a specific operator
Quick Register Reference overview:
Index D7 D6 D5 D4 D3 D2 D1 D0
Global
001WSEnableTest Register
002Timer 1 Count (80 usec resolution)
003Timer 2 Count (320 usec resolution)
004IRQResetT1 MaskT2 MaskT2 StartT1 Start
1044-OP B-E4-OP A-D4-OP 9-C4-OP 2-54-OP 1-44-OP 0-3
105OPL3 EN
008CSWNote-Sel
0BDTrem DepVibr DepPercModeBD OnSD OnTT OnCY OnHH On
Channel Wide
#A0-#A8Frequency Number (Lower 8 bits)
#B0-#B8Note OnBlock NumberF-Number (high bits)
#C0-#C8RightLeftFeedBack Modulation FactorAM/FM
Operator Specific
#20-#35TremoloVibratoSustainKSRFrequency Multiplication Factor
#40-#55Key Scale LevelOutput Level
#60-#75Attack RateDecay Rate
#80-#95Sustain LevelRelease Rate
#E0-#F5Waveform Select
table adapted from OPL3 Programmer's Guide
# - The first digit is either a 0 or a 1 depending on whether the lower 9 channels are targetted, or the higher 9 channels.
* Indices 104 and 105 are OPL3 only!

Global Registers

Index 001 - Waveform Select
Index D7 D6 D5 D4 D3 D2 D1 D0
001WSEnableTest Register
WSEnable
Note: Test Register must be set to 0 before any further operations may be performed!
Index 002 - Timer 1 Initial Value
Index D7 D6 D5 D4 D3 D2 D1 D0
002Timer 1 Count (80 usec resolution)
Timer 1 Count
8 bit counter value that when Timer 1 Enable is active, will count upwards. Upon overflow this will trigger the associated flag in the Status Port (read from 0x388) if the Timer 1 Mask is set (not 0).
*This counter clocks on 80 microsecond intervals.
Index 003 - Timer 2 Initial Value
Index D7 D6 D5 D4 D3 D2 D1 D0
003Timer 2 Count (320 usec resolution)
Timer 2 Count
8 bit counter value that when Timer 2 Enable is active, will count upwards. Upon overflow this will trigger the associated flag in the Status Port (read from 0x388) if the Timer 2 Mask is set (not 0).
*This counter clocks on 320 microsecond intervals.
Index 004 - Timer Controls
Index D7 D6 D5 D4 D3 D2 D1 D0
004IRQResetT1 MaskT2 MaskT2 StartT1 Start
IRQReset
Resets timers if set (not 0). If set, all other bits are ignored.
T1 Mask
If set, Timer 1 will trigger the signal in the Status Port associated with when Timer 1 overflows.
T2 Mask
If set, Timer 2 will trigger the signal in the Status Port associated with when Timer 2 overflows.
T2 Start
If set, Timer 2 will count up until overflow. Upon overflow or IRQReset=1, the value written to Index 3 will be loaded into Timer 2.
T1 Start
If set, Timer 1 will count up until overflow. Upon overflow or IRQReset=1, the value written to Index 2 will be loaded into Timer 1.
Index 104 - 4 Operator Channel Switches
Index D7 D6 D5 D4 D3 D2 D1 D0
1044-OP 11-144-OP 10-134-OP 9-124-OP 2-54-OP 1-44-OP 0-3
4-OP 11-14 4-OP 10-13 4-OP 9-12 4-OP 2-5 4-OP 1-4 4-OP 0-3
Note: this register does not exist on OPL2 devices
Index 105
Index D7 D6 D5 D4 D3 D2 D1 D0
105OPL3 EN
OPL3 EN
Enables OPL3 functionality. This must be set to 1 before any OPL3 features will activate.
Note: this register does not exist on OPL2 devices
Index 008
Index D7 D6 D5 D4 D3 D2 D1 D0
008CSWNote-Sel
CSW - Composite Sinewave Mode
TODO
Note-Sel
TODO
Index 0BD
Index D7 D6 D5 D4 D3 D2 D1 D0
0BDTrem DepVibr DepPercModeBD OnSD OnTT OnCY OnHH On
Trem Dep - Tremolo Depth
If set, Tremolo oscillates by 4.8 dB.
If clear, Tremolo oscillates by 1 dB.
Vibr Dep - Vibrato Depth
If set, Vibrato oscillates by 14 cent.
If clear, Vibrato oscillates by 7 cent.
Note: a cent is a unit of measure of musical pitch equal to 1/100 of a semitone.
PercMode - Percussion Mode
When set activates the 5 percussion controls that make up the lower 5 bits of this register. Percussion uses channels 6, 7, and 8 and they must be in a note off state before this flag is set.
Note: In order to hear the percussion sounds it is necessary to set the octave and f-number via a write to A6/B6, A7/B7, and A8/B8. Just set the note values and leave the note on flag as 0.
BD On - Base Drum On
Uses both operators of Channel 6 to generate a base drum effect.
SD On - Snare Drum On
Uses Channel 7, Operator 2
TT On - Tom Tom On
Uses Channel 8, Operator 1
CY On - Cymbal On
Uses Channel 8, Operator 2
HH On - Hi Hat On
Uses Channel 7, Operator 1

Channel Registers

2 Operator Mode Offsets:
Channel #012345678
Channel Offset0x000x010x020x030x040x050x060x070x08
Op 10x000x010x020x080x090x0A0x100x110x12
Op 20x030x040x050x0B0x0C0x0D0x130x140x15
Channel #91011121314151617
Channel Offset0x1000x1010x1020x1030x1040x1050x1060x1070x108
Op 10x1000x1010x1020x1080x1090x10A0x1100x1110x112
Op 20x1030x1040x1050x10B0x10C0x10D0x1130x1140x115
Note: Channels 9 through 17 are only available on OPL3
For 4 Operator Mode those offsets become:
Channel #01267891011151617
Channel Offset0x000x010x020x060x070x080x1000x1010x1020x1060x1070x108
Op 10x000x010x020x100x110x120x1000x1010x1020x1100x1110x112
Op 20x030x040x050x130x140x150x1030x1040x1050x1130x1140x115
Op 30x080x090x0A0x1080x1090x10A
Op4 0x0B0x0C0x0D0x10B0x10C0x10D
Note: This assumes all channels that support 4 Operator pairs are set to 4 Operator Mode
These registers are grouped in sets where the last digit is the channel number. Channels 0 - 8, being part of the original OPL2 specification, use 0?0 to 0?8. For the additional 9 channels added by OPL3 write to 1?0 to 1?8 for channels 9 through 17 respectively.
Note: Sound will not play until the associated operators for the desired channel have been programmed.
Generally safe values for an octave of 4 for the traditional 12 note scale are:
F-NumberFrequencyNote
16B277.2C#
181293.7D
198311.1D#
1B0329.6E
1CA349.2F
1E5370.0F#
202392.0G
220415.3G#
241440.0A
263466.2A#
287493.9B
2AE523.3C
Note: Table taken from Programming the AdLib/SoundBlaster FM Music Chips
Note: This table will not be exact owing to the chip's not operating around a clock exactly matching the western chromatic scale.
For more exact values: F-Number = Music Frequency * 2 ^ (20 - Octave) / 49716Hz
Note: Formula is from OPL3 Programmer's Guide
Index #A0-#A8
Index D7 D6 D5 D4 D3 D2 D1 D0
#A0-#A8Frequency Number (Lower 8 bits)
Frequency Number (Lower 8 bits)
The lower 8 bits of the frequency number.
Index #B0-#B8
Index D7 D6 D5 D4 D3 D2 D1 D0
#B0-#B8Note-OnOctaveF-Number (high 2 bits)
Note-On
If set to 1 the channel will immediately start playing the frequency octave combo defined by #A0-#A8 and this register. In order to play a note, this flag must first be cleared. If this field was already set and the ADSR are appropriately set, a sweep effect can be achieved.
Octave
This specifies the octave the given F-Number will be scaled to.
F-Number (high 2 bits)
The high 2 bits of the 10 bit frequency number.
Index #C0-#C8
Index D7 D6 D5 D4 D3 D2 D1 D0
#C0-#C8RightLeftFeedBack Modulation FactorAM/FM
Right
If set, this channel will produce sound for the right speaker.
Note: this only has an effect on OPL3 after the OPL3 flag in register 105 has been set to 1.
Left
If set, this channel will produce sound for the left speaker.
Note: this only has an effect on OPL3 after the OPL3 flag in register 105 has been set to 1.
FeedBack Modulation Factor
This controls how much of this channel's output is fed back into it's operators.
0 - no feedback, 7 - maximum feedback
Note: For channels paired in 4 operator mode, only the lower channel's feedback is used.
AM/FM - Additive Synthesis / Frequency Modulation
This flag dictates how the operators for this channel interact with one another. In 2 operator mode, which is the only mode available in OPL2 the flag applies as:
AM/FM FlagTypePattern
0FMOutput = Operator2(Operator1)
1AMOutput = Operator1 + Operator2
In 4 operator mode, the lower channel and upper channel combine to offer 4 algorithms:
Hi FlagLo FlagTypePattern
00FM-FMOutput = Operator4(Operator3(Operator2(Operator1)))
01FM-AMOutput = Operator4(Operator3(Operator2)) + Operator1
10AM-FMOutput = Operator4(Operator3) + Operator2(Operator1)
11AM-AMOutput = Operator4 + Operator3(Operator2) + Operator1

Operator Registers

Index #20-#35
Index D7 D6 D5 D4 D3 D2 D1 D0
#20-#35TremoloVibratoSustainKSRFrequency Multiplication Factor
Tremolo
If set, the amplitude will be modulated according to the associated flag in the BD register.
Vibrato
If set, the pitch will oscillate slightly according to the associated flag in the BD register.
Sustain
If set, the output level of the register will hold at the Sustain setting written to the associated #80-#95 register for this operator. If clear the note will decay until silent regardless of whether a note off state has been set in the associated channel's #B0-#B8 register.
KSR - Keyboard Scaling Rate
If set, higher pitched notes will pass through their ADSR states faster.
Frequency Multiplication Factor
Adjusts the note given by the associated channel's #A0-#A8 and #B0-#B8 registers by a defined amount according to the following table:
  • 0 - one octave below
  • 1 - at the voice's specified frequency
  • 2 - one octave above
  • 3 - an octave and a fifth above
  • 4 - two octaves above
  • 5 - two octaves and a major third above
  • 6 - two octaves and a fifth above
  • 7 - two octaves and a minor seventh above
  • 8 - three octaves above
  • 9 - three octaves and a major second above
  • A - three octaves and a major third above
  • B - three octaves and a major third above
  • C - three octaves and a fifth above
  • D - three octaves and a fifth above
  • E - three octaves and a major seventh above
  • F - three octaves and a major seventh above
Index #40-#45
Index D7 D6 D5 D4 D3 D2 D1 D0
#40-#55Key Scale LevelOutput Level
Key Scale Level
Applies a varied volume suppression according to the octave value specified to the associated channel's #B0-#B8 register.
  • 0 - no change
  • 1 - 1.5 dB/octave
  • 2 - 3 dB/octave
  • 3 - 6 dB/octave
Output Level
The volume suppression applied to this operator. 0 will make the operator output it's maximum volume, 63 will cause near silence.
Index #60-#75
Index D7 D6 D5 D4 D3 D2 D1 D0
#60-#75Attack RateDecay Rate
Attack Rate
The rate at which the channel will rise from silent to full volume after a note on signal is written to the associated channel's #B0-#B8 register.
0 - slowest, 15 - fastest
Decay Rate
The rate at which the channel will fall after peaking in volume.
0 - slowest, 15 - fastest
Index #80-#95
Index D7 D6 D5 D4 D3 D2 D1 D0
#80-#95Sustain LevelRelease Rate
Sustain Level
This is the fraction of the full volume level that the note will hold at, provided the Sustain flag for this operator is set in it's associated #20-#35 register.
0 - silent, 15 - loudest
Release Rate
This is the speed at which the note fades in volume after the note off state has been set on the associated channel's #B0-#B8 register.
0 - slowest, 15 - fastest
Index #E0-#F5
Index D7 D6 D5 D4 D3 D2 D1 D0
#E0-#F5Waveform Select
Waveform
This specifies the operator's waveform. In OPL2 there are only 4 possible values (0-3), but in OPL3 the options were expanded to 8. They are:
ValueDescriptionVisual
0Sine
1Half Sine
2Absolute Sine
3Pulsed Absolute Sine
4Sine on even periods only
5Absolute Sine on even periods only
6Square
7Derived Square
Infographics shamelessly stolen from OPL3 Programmer's Guide