A feature-complete OPL2 and OPL3 FM synthesis core in C11 with a small API and a permissive license.
Want to hear it? Try the live web demo. It runs in your browser as WebAssembly, no build needed.
Click the preview or the link above to open the player.
opal emulates the Yamaha YMF262, better known as the OPL3, together with its OPL2 predecessor. These are the chips that powered the AdLib and Sound Blaster cards, and through them most of the music in DOS games and trackers. The core was written by Shayde of Reality for Reality Adlib Tracker 2 and released into the public domain. What you find here is a C11 port derived from the OpenMPT copy and wrapped in a small C API.
Feature-complete means the full register set that affects audio. The core implements two- and four-operator FM, all eight waveforms, the tremolo and vibrato LFOs, percussion mode with its noise generator, and the timers with their status register. CSW mode is decoded and then left inert. The real YMF262 ignores it, so opal ignores it with equal care. The OPL2 waveform select enable bit receives the same treatment, because the YMF262 ignores that one too.
Stereo output matches established YMF262 emulators, including Nuked-OPL3, sample for sample at the chip's native rate of 49716 Hz. Waveforms, envelopes, LFOs, rhythm mode, two- and four-operator FM, feedback, stereo routing, and buffered register timing all follow hardware behavior. The right channel arrives one sample after the left because the YMF262 pipeline delays it. Reproducing the quirk is cheaper than explaining its absence.
You need CMake 3.23 or newer and a C11 compiler. Presets exist for Windows (MSVC, Clang, and MinGW in 32- and 64-bit), Linux (GCC), and macOS (Clang). All presets except the Visual Studio ones use Ninja Multi-Config and expect ninja on PATH. The full list lives in CMakePresets.json.
cmake --preset default
cmake --build --preset debug
On Linux or macOS use the matching presets:
cmake --preset linux # or: macos
cmake --build --preset linux-debug
The result is a static library. Link opal and include include/opal/opal.h. The example player builds by default. Set -DOPAL_BUILD_EXAMPLES=OFF if you do not want it.
Three cross presets cover platforms that cannot run the console player. The android
preset needs the NDK and ANDROID_NDK_HOME, and ios needs a macOS host. Both build the
library alone. The web preset needs an active Emscripten SDK and builds the browser
player described below.
To install and consume opal from another CMake project:
cmake --install build/msvc-x64 --prefix /path/to/prefix
# In your CMakeLists.txt:
# find_package(opal 1.0 REQUIRED)
# target_link_libraries(myapp PRIVATE opal::opal)
The public interface is eight functions, declared in include/opal/opal.h. Drive it the
way you would drive the real chip. Write registers, then read back stereo samples at
whatever rate you asked for.
#include <opal/opal.h>
Opal chip;
opalInit(&chip, 48000);
opalWriteReg(&chip, 0x20, 0x21);
/* set registers, key on, etc */
int16_t left, right;
opalSample(&chip, &left, &right);The instance holds no internal pointers: every cross reference is an index. Copy it, assign it, memcpy it, or move it freely. A copy is a complete save state and plays on from the moment it was taken.
Internally the chip always runs at its native 49716 Hz and resamples to the rate passed
to opalInit or opalSetSampleRate. opalWriteReg writes a register on the current
sample. opalWriteRegBuffered queues writes spaced a few chip samples apart for more
accurate timing. opalFlushWriteBuf applies any queue at once. opalPan adds
per-channel panning on top of the chip's own left and right enables (channel index 0 to
17, or 0 to 8 with bit 8 set for the second bank). opalReadStatus returns the status
register, which timer polling code will want.
The bundled player streams OPL music to the default audio device through miniaudio. It
reads DRO version 1, HSC, and IMF or WLF files.
player song.dro
player song.hsc
player song.imf
player --rate 700 song.wlfA few limitations are worth knowing. DRO files must be version 1, meaning the header word
at offset 8 is zero. Both the usual 24-byte v1 header and the early DOSBox 21-byte form
(single-byte hardware type, no padding) are accepted. HSC has no signature, so the player
goes by the .hsc extension. IMF and WLF are also recognized by extension, and the replay
rate defaults to 560 Hz for .imf and 700 Hz for .wlf. That rate is a property of the
game rather than of the file, so the --rate flag exists for the exceptions.
The player core is format-agnostic. Each format sits behind the small interface in
player_format.h, and support for a new one is a single format_*.c away.
The music folder holds seven public-domain HSC tracks from HSCDEMO3.EXE, a demo
Hannes Seifert released in 1992. Only his
own tracks are included, the ones the demo itself
marks independent or public domain. The track list, the attribution, and the reason the
game soundtracks stayed behind are all in music/README.md.
Point the player at any of them and quote the path, since the names contain spaces:
player "music/01 Shoot 'em up.hsc"
The examples/web directory wraps the same core and the same format decoders into a
WebAssembly build with a browser front end, styled after the trackers this music was
written in. The web preset builds it into player.js and player.wasm, and a GitHub
Actions workflow deploys it together with the demo tracks to GitHub Pages on every push
to main. The live copy is at
realbitdancer.github.io/opal.
The sources follow project conventions. Allman braces, a brace on every control body, and
no one-line bodies. .clang-format enforces all of it.
Naming is uniform across the tree. Types are PascalCase, functions and struct members
are camelCase, macros are SCREAMING_CASE, and the core's public API carries an opal
prefix (opalWriteReg, OpalChannel, egOut). Earlier releases kept the public-domain
source's names for upstream diffability. Consistency won. The full rules are in
CONTRIBUTING.md.
The project's own code is MIT, which covers the C API, the resampler, the pan helper, the example player, and the build system. See LICENSE. The Opal emulation core is public domain, courtesy of Shayde of Reality. miniaudio is public domain or MIT-0 at your option. Nothing in the library depends on LGPL code.
Full attribution and the provenance of the ROM tables are in THIRD_PARTY_LICENSES.md. Release history is in CHANGELOG.md. Language bindings: BINDINGS.md. Security reports: SECURITY.md. How to contribute: CONTRIBUTING.md.
