Last week, I posted about the ApOPL3xy reaching the version 1.0 milestone. This time, I want to discuss the emulator I wrote for it. I think it’s pretty cool. The emulator was initially conceived as a way to run the code on a computer with all the debugging tools that I’m used to having at my disposal. It was certainly useful for that, but it grew into a project of its own.
Demo Videos
Before I get into some of the more technical details, I want to show some video captures of the emulator running. And if you happen to find yourself thinking that it looks like something you’d want to play with, I’m releasing it all as open source. There will be some links later in the post where you can download the emulator, and for the ambitious, all the files needed to build your own hardware ApOPL3xy are available as well.
VGM Player
One of the most iconic pieces composed for the OPL3 is “At Doom’s Gate,” the music for the first level (Episode 1 Mission1, or E1M1) of Doom. vgmrips.net has the VGM capture of this song available for download, and I can’t think of a better song to demonstrate the VGM player feature. Note that most, if not all, VGM files only use the first two output channels, since no 4-channel OPL3 sound cards were commonly available.
The numbered buttons below the display correspond to the colored buttons on the hardware, and the two groups of three buttons, labeled ↺, ↻, and ☟, represent the rotary encoders being turned counter-clockwise, turned clockwise, and pressed like a button, respectively. At some point, I may improve these to be a more skeuomorphic control, but the buttons get the job done.
MIDI Player
My last post showed the new MIDI file player feature on the hardware ApOPL3xy, but due to difficulties filming the backlit LCD screen, it wasn’t easy to see what was on the display. The next video shows the emulator playing a MIDI version of the Doctor Who theme music. As before, I arbitrarily assigned MIDI channels to output channels to make the VU meters a bit more interesting.
Loop Controls
Between the last post and this one, I added loop controls to both the VGM and MIDI players. It’s a small feature, but potentially useful. The loop indicator is just to the left of the total time in the player interface. It starts out as a right-pointing arrow, meaning no loop. The other loop modes are single loop, represented by an arrow that curves from the right to the left, and continual loop, represented by two arrows pointing at each other’s tail. The 5 button cycles through these modes.
This video demonstrates the single loop mode being used for a MIDI file with a version of the Pac-Man music. It also shows the page down menu navigation function, which is mapped to button 9.
MIDI Input
My primary motivation for building the ApOPL3xy was to enable MIDI input to control an OPL3 chip. So far, I’ve demonstrated playback functionality, but I want to show the live MIDI capability as well. Unfortunately, I’m not very skilled at the piano, so I just play a few scales in the following video. The piano keyboard in the video is an open source tool called VMPK (Virtual MIDI Piano Keyboard), which I did not write, but am simply using.
The video shows selecting a MIDI input source for the emulator and using the Channel Editor feature to show the patches assigned to MIDI channels. Then, a few scales are played using a few different patches.
MIDI Output
The ApOPL3xy also has a MIDI output port, which sends MIDI messages from the MIDI player to an external device. It also optionally echoes messages received from the MIDI input port, but that echoing is not demonstrated here. What is demonstrated is using VMPK as an external device to visualize the notes being played. In this video, VMPK is configured to display each MIDI channel with a different color. The MIDI file being played is the battle music from Final Fantasy VII.
Implementation Details
The ApOPL3xy firmware interacts with several pieces of hardware, all of which need to be emulated to be able to run the firmware purely in software.
Microcontroller
The hardware ApOPL3xy has an ATmega1284p microcontroller to run the firmware. Because the main goal of the emulator is to support debugging, this microcontroller is not emulated. Instead of running the compiled AVR firmware on an emulated ATmega1284p, the firmware is recompiled to native code for the computer running the emulator, with some conditionally-compiled hooks in a few places to tie in the other emulated hardware.
OPL3
This is probably the most important piece of hardware to emulate, and also the most daunting. Fortunately, the open source project DOSBox has code to emulate the OPL2 and OPL3 chips, so I adapted it for my use. This code exposes a function to set registers on the emulated OPL chip, which I used instead of the SPI interface I built for the hardware ApOPL3xy for that purpose. The DOSBox code also exposes a function to get output audio samples, which I used to populate the audio buffers (more on this later).
The OPL3 supports up to 4 channels of audio output, but sound cards incorporating the chip rarely if ever used more than 2. DOSBox didn’t bother implementing support for channels 3 and 4, so I modified it to add that support. It was actually easier than I expected. Since they had already gone to the trouble to implement 2 channels, most of the work involved increasing some array sizes and loop bounds from 2 to 4.
LCD Character Display
The standard 20×4 LCD character display used by the ApOPL3xy is based on a driver chip called the HD44780. I broke the emulation of this device up into two parts: the HD44780 emulation and the drawing of the LCD panel itself.
The HD44780 supports several commands which are sent by writing to its control and data registers. To emulate this chip, I wrote a C++ class to represent all of its internal registers, RAM, and ROM, and then implemented each command as a separate function that operates on the internal state. I then implemented a wrapper function which accepts commands or data, and dispatches to the correct underlying function(s). This is called by the emulated firmware instead of using SPI, similar to the OPL3.
For the HD44780 emulation, I also wrote a function to retrieve the output state, e.g., which pixels are on and which are off. This is used by the LCD panel emulation to actually display the pixels. I used the open-source, cross-platform GUI library wxWidgets to implement the emulator’s GUI, and the LCD panel is implemented as a custom widget that does its own drawing with the graphics routines provided by the library.
Audio Output
To implement audio output for the emulator, I used the open-source, cross-platform library PortAudio. This library abstracts all the platform-specific audio APIs and presents a unified interface to interact with the audio system. With it, the emulator is able to select audio output devices, sample rate, latency, and number of channels. (These options are exposed to the user via the Audio Settings dialog window.) To play the synthesized audio, the emulator extracts audio samples from the emulated OPL3 and sends them to the host audio system using PortAudio’s interface.
VU Meters and Gain Controls
Each output channel on the physical ApOPL3xy has a VU meter (well, not a true VU meter, just a linear amplitude display) and a gain control. Emulating this in wxWidgets was pretty straightforward. These are in separate tool windows that the user can display and dismiss.
The gain controls are slider widgets. To implement gain, the emulator reads the values from the sliders and uses them to adjust the audio samples before sending them to PortAudio for playback.
The VU meters are custom widgets that draw themselves based on a given amplitude. The emulator determines the current amplitude of each channel from the audio samples before sending them to PortAudio, and sends the amplitude values to their respective VU meter for display.
MIDI Input and Output
To emulate the MIDI input and output ports, I used the open-source, cross-platform library RtMidi. This library abstracts the host computer’s MIDI system, and enables enumeration of, reading from, and writing to system MIDI ports. The MIDI In and MIDI Out dialogs allow the user to select which, if any, MIDI ports to read from and write to.
If a MIDI input port is selected, MIDI events are read from it and injected into the firmware’s MIDI queue, instead of reading from the microcontroller’s serial interface. Similarly, instead of writing output MIDI events to the microcontroller’s serial interface, the emulator writes them to the host MIDI port selected for output.
MicroSD Card
The hardware ApOPL3xy includes a MicroSD card reader, which is read by the firmware using the open-source SdFat library. This library abstracts both the SPI messaging needed to interact with the card itself, as well as handling the FAT filesystem that keeps track of which files are where in the card’s storage.
To emulate this, I wrote a mock implementation of the subset of SdFat’s interface that the firmware uses, but instead of accessing an SD card, it uses the standard C++ std::filesystem library to access the host’s filesystem through the host OS. The user can “insert” a virtual SD card by using the Mount SD Card dialog window to select the directory to serve as the root directory of the virtual card. The user can “eject” a virtual SD card by using the Unmount SD Card menu option.
SRAM and EEPROM
The ATmega1284p microcontroller has 16 kiB of RAM and 4 kiB of EEPROM storage built in. This isn’t enough for everything I wanted the ApOPL3xy to be able to do, so I added SPI SRAM and EEPROM chips (128 kiB each) to the design. The SRAM on the microcontroller itself doesn’t need to be emulated; the emulator just uses the host’s system RAM for that. The microcontroller’s EEPROM isn’t used by the firmware at all, so that doesn’t need to be emulated either. The add-on chips, however, do need to be emulated.
The SRAM chip was very simple to emulate. I simply allocated an array of the right size and provided functions to read from and write to it. The emulator uses these functions instead of sending SPI commands like the firmware does on physical hardware. The EEPROM was very similar, except that a file is used instead an in-memory array, since EEPROMs are nonvolatile.
If the emulator finds no EEPROM file in the expected location on startup, it generates a default one and uses that. This has turned out to be useful for setting up a hardware ApOPL3xy. Since the format of the EEPROM is identical between the emulator and the hardware, one can copy the EEPROM file from the emulator to a MicroSD card and load it onto the hardware.
Input Controls
The ApOPL3xy has 10 buttons and 2 rotary encoders (which can also act as buttons) which are used to navigate menus and issue commands. The hardware version of these are connected to a shift register, which the firmware reads via SPI, interprets, debounces, and adds to an input event queue. The emulator instead provides GUI buttons for these, and pressing a button injects an input event directly into the queue. These buttons are also mapped to (hopefully) intuitive keyboard shortcuts.
Miscellaneous
There were a few other things that needed to be replaced or mocked to be able to compile and run the firmware in an emulated context.
- The microcontroller framework used by the ApOPL3xy’s firmware provides functions for timing that use the microcontroller’s on-chip timers. These were replaced with versions that use the standard C++
std::chronolibrary instead. - The AVR architecture used by the ATmega1284p requires the use of special functions to read data out of program code (e.g. constant string data) instead of SRAM. These were replaced with simple pass-through functions, since the host machine for the emulator doesn’t have this restriction.
- Various utility functions like
min()andmax()provided by the framework needed to be reimplemented.
Download Links
Emulator
If you’re interested in trying out the ApOPL3xy emulator, you can download pre-compiled binaries here:
- ApOPL3xy Emulator v1.0.1 (Windows)
- ApOPL3xy Emulator v1.0.1 (MacOS)
- ApOPL3xy Emulator v1.0.1 (Ubuntu 24.04)
- ApOPL3xy Emulator v1.0.1 (Fedora 42)
Note that on Windows and MacOS, the fact that this binary isn’t signed may cause the operating system to complain. To get past this on MacOS, see https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac. Microsoft doesn’t seem to have a good page on this, but here’s a potentially helpful google search.
Music Files
Here are some music files that you can play using the ApOPL3xy:
Source Code
The combined source code for the emulator and firmware is available below. It’s released under the terms of the MIT License, except for the OPL emulation code from DOSBox, which is released under the LGPL 2.1 license. I’ll probably post this to github at some point, but the code is a bit messy at the moment, and I might like to clean it up first.
Hardware Design Files
If you’re interested and brave enough to want to build your own hardware ApOPL3xy, here are the files you’ll need. Note that the documentation is rather lacking at the moment, but I think someone determined enough could figure it out. If you try and have difficulties, let me know and I’ll be happy to help.
- ApOPL3xy PCB rev3 (Gerbers)
- ApOPL3xy PCB rev3 (Bill of Materials)
- ApOPL3xy PCB rev3 (Schematic)
- ApOPL3xy PCB rev3 (KiCad Design Files)
- ApOPL3xy Firmware v1.0.1 (HEX File)
- ApOPL3xy EEPROM Image v1.0.1 (BIN File)
- ApOPL3xy User Manual v1.0.1 (PDF File)
Closing Thoughts
I’m quite pleased with how this project turned out. What began as an attempt to make debugging easier grew into something kinda cool on its own. It also enables me to share the ApOPL3xy with more people than I would have otherwise. If you do try it out, I’d love to hear what you think. I have my own ideas of what could be improved, but there’s no substitute for real user feedback.
Leave a Reply