Skip to content
FreeDOSDeep Dive Published Updated 8 min readViews unavailable

Programming the AdLib YM3812: OPL2 Registers, Operators, and Key-On

Build a deterministic AdLib OPL2 voice by respecting address/data timing, operator register groups, channel frequency fields, and key-on cleanup.

The AdLib Music Synthesizer Card exposes a programmable Yamaha YM3812, commonly called OPL2, through an indexed address/data interface. A DOS application selects a register, observes the interface’s required timing interval, writes a value, and then lets the chip’s operators generate a voice. Writing one frequency value is not enough to produce a useful note: operator envelopes, multiplier, waveform, feedback, channel algorithm, and key-on state all participate in the sound.

This article is specifically about the original AdLib-style OPL2 register model. It does not describe the Sound Blaster DSP used for digitized samples, nor does it assume OPL3’s second register bank, stereo routing, or four-operator modes. Many later sound cards and emulators implement compatibility, but an implementation should identify the actual interface and follow its documented behavior.

Address and data are separate transactions

The classic AdLib address port is 388h and its data port is 389h, though a compatible device or emulator may expose another base. A write first places the register index on the address port, then places the value on the data port. The card’s bus interface imposes a minimum delay between these operations and before another transaction. The AdLib programming guide describes minimum delays and uses repeated status-port reads as a conservative delay sequence; its status register reports timer and IRQ flags, not a general-purpose ready/busy signal. Do not remove the documented wait merely because a modern emulator tolerates back-to-back writes.

An indexed write is conceptually:

wait the documented interval after the preceding transaction
write register number to base + 0
wait the documented address-to-data interval
write register value to base + 1
wait the documented post-data interval before the next operation

The exact wait method must match the adapter and execution environment. Historical programs use repeated status-port reads as a time delay or another calibrated delay; do not treat the timer flags as a busy bit. An instruction-count loop is not a unit of time across processors. A protected-mode extender may also restrict port I/O. Keep the register write behind one tested routine so timing fixes and emulator workarounds do not diverge between the synthesizer code paths.

Map operator registers instead of guessing offsets

OPL2 organizes sound as nine two-operator channels. One operator acts as the modulator and the other as the carrier in the common two-operator algorithm. Operator registers are stored in groups with hardware-defined offsets; they are not laid out as “operator 1, operator 2, next channel” in a simple contiguous array. The AdLib Programmer’s Manual includes the mapping between a channel and its two operator register numbers. Build a table from that documented map and test all nine channels before relying on it.

The operator register groups configure characteristics such as tremolo/vibrato and multiplier, key-scale level and total level, attack/decay, sustain/release, and waveform selection. The global waveform-enable control must be set before non-default waveforms are relied on, and waveform availability can depend on the device revision or compatible implementation. Reserved bits should be written as specified, not filled with arbitrary “convenient” values.

A good initialization routine starts from a known state. Silence active channels, clear rhythm mode unless the application uses it, configure waveform and timer controls deliberately, then program each operator’s parameters before keying on a voice. A partial initialization can inherit a previous program’s envelope or rhythm state if the machine was not reset. This is particularly important in DOS, where a program may return to a shell without power-cycling the card.

Frequency, block, and key state

Each melodic channel has a frequency low register and a channel register containing the high frequency bits, octave-like block field, and key-on bit. The frequency number is not a direct Hertz value. The block changes the scale, and conversion from a musical pitch to the chip’s frequency number follows the YM3812 reference formula for the chip clock. A program must use the formula and clock documented for its hardware rather than treating the raw F-number as MIDI note number.

Key-on is a state transition: set the key-on bit only after the channel’s operators and frequency registers are ready. Key-off clears that bit, allowing the programmed release envelope to run. If a program leaves key-on set during exit, the chip can continue sounding after DOS regains control. Cleanup should silence every channel the program touched and restore any global mode it changed.

The following is schematic pseudocode, not a complete OPL2 driver. opl_write must perform the tested status and timing sequence described earlier, and the values need to come from a validated instrument definition:

opl_write(0x20 + modulator_offset, operator_flags); /* multiplier/flags */
opl_write(0x40 + modulator_offset, modulator_level);
opl_write(0x60 + modulator_offset, modulator_attack_decay);
opl_write(0x80 + modulator_offset, modulator_sustain_release);
/* Program the carrier through its mapped register offsets as well. */
opl_write(0xA0 + channel, f_number_low);
opl_write(0xC0 + channel, feedback_and_connection);
opl_write(0xB0 + channel, (block_and_f_number_high) | 0x20); /* key on */

This illustrates register groups only. It intentionally does not supply an arbitrary “best” instrument or claim that these writes are sufficient for every tone. A carrier’s total-level field controls attenuation: zero adds no total-level attenuation, while the maximum field value can make an otherwise correctly programmed voice effectively silent. An unsuitable modulator level or envelope can produce a click or harsh transient. Use a known AdLib instrument definition and verify the operator offset mapping against the manual.

Two-operator algorithm and audible output

The connection bit in the channel control register chooses how the operators contribute to the output. In the common modulator-to-carrier configuration, the modulator shapes the carrier’s phase and therefore its spectrum. In additive mode, both operators contribute audible output. The feedback field affects the modulator feedback path. These settings change timbre, not the channel’s allocated pitch range.

Instrument data is a parameter set, not a self-contained sound file. It must be translated into the hardware’s operator register groups and scheduled with appropriate note-on/note-off events. Many games shipped instrument banks or register streams that assumed an AdLib-compatible device. A format that contains timed register writes needs both the data and its timing; replaying the values as fast as possible changes the music.

Rhythm mode repurposes channels and operator combinations for percussion. Enabling it changes how certain voices are interpreted. A melodic driver and a rhythm driver should not independently toggle the global rhythm bit without shared ownership. Reserve the necessary channels, initialize the percussion state, and clear rhythm mode during teardown if the card is handed back to another application.

Timing, status, and emulation differences

The YM3812 is an external peripheral on a PC I/O bus. CPU speed does not change its internal oscillator rate, but it changes how quickly software can issue register transactions and how long a delay loop lasts. Some clone chips accept writes faster than the original card, while another emulator may model transaction timing differently. The portable behavior is the documented minimum delay, not “the fastest loop that worked on my machine.”

The OPL timer/status registers are a distinct feature from a DOS wall clock. If a driver uses them for internal timing or diagnostics, it must understand timer start/reset bits and status flags as described by the chip documentation. Do not conflate OPL timer overflow with the PC PIT, BIOS tick, or RTC alarm. Those are different clock sources with different interrupt and ownership paths.

Failure diagnosis and state discipline

Silence can result from the wrong port base, no compatible OPL device, operator levels that mute the carrier, a missing key-on transition, or a mode that disables the waveform selection. A note that never stops suggests missing key-off or an exit path that did not silence the channel. A harsh click can be an envelope transition or a note started before operator setup finished. A melody that is too fast may be a scheduling error in the register stream rather than an OPL hardware fault.

Use a minimal one-channel test first: establish a documented base, test status behavior, initialize global state, program one known instrument, set a conservative note, then key off and verify release. Capture the exact register/value sequence and elapsed timing. Repeat on each claimed hardware target and emulator. Do not use “it sounds close” as proof of register compatibility when the software depends on all nine channels or percussion mode.

Version boundaries and safe cleanup

OPL2 and OPL3 share a family resemblance, not an identical feature set. The OPL3 adds a second register bank and additional operating modes. An OPL2 driver should neither write the second bank nor assume stereo/four-operator features. A Sound Blaster card’s digital audio path and its FM synthesizer are separate resources: successful OPL output says nothing about DSP DMA/IRQ playback, and vice versa.

At startup, document the port range, detected/assumed chip, and emulation mode. At shutdown, key off all active channels, clear rhythm or waveform options only if the driver changed them, stop timers it started, and restore the caller’s sound state when the hardware and API permit. Keep a single owner for shared registers. A second resident music driver can otherwise change channel or rhythm state while a program is rendering.

Acceptance checklist

Validate a known instrument on one channel, all intended operator offsets, pitch conversion at low and high notes, modulation and additive algorithms, and note release. Test that the timing wait is bounded and that the program does not hang when no device responds. Exercise every global mode the driver uses, including rhythm if claimed, then confirm cleanup leaves no stuck voice. Keep OPL2 and OPL3 behavior in separate tests and avoid claiming support for an undocumented clone.

The useful abstraction is a timed indexed-register writer plus a small voice state machine. That keeps hardware timing, sound synthesis, and music scheduling separate. Once those boundaries are explicit, AdLib programming becomes a reproducible hardware integration task rather than a sequence of unexplained port writes.

Related:

Sources:

Comments