What is Moonsound?
Moonsound was the first cartridge created for the MSX that used the OPL4 (YMF278B) chip. It was created by Henrik Gilvad (also creator of the Graphics 9000) and distributed by Sunrise in 1995.
Moonsound in my life
Back then, you could get these hardware expansions through Hnostar. The Moonsound cost 35,000 pesetas and the Gfx9000 40,000 pesetas. Since I don't have a very good ear, I decided to buy the Gfx9000 and experiment with images, which is what has always fascinated me. I fiddled with it and looked for MSX scene programs that used it. Unfortunately, the programs that came out used Moonsound instead of Gfx9000. After that, my contact with the MSX faded: I stopped buying any fanzines (after Hnostar and SDMesxes folded) and only followed the news on msx.org. Until I decided to recover the computer at my parents' house at the end of 2022. In this period of re-engagement, I tried to get an OPL4 in any of the versions that existed (Moonsound, Wozblaster, Shockwave, DalSolRi, Monster sound Fm Blaster and MSX-Blaster) without success. Until Cristiano (the retrohacker) contacted me and told me he was making a run of Wozblaster. Finally!!! After so long chasing it, I got it.
My musical abilities remain the same as twenty years ago, practically nil. However, thanks to roboplay, I use Moonsound as a jukebox. I have an m3u file with all the songs I've collected and I make it change songs every 3 minutes. I've created 10 copies of the file with the lines in a different order (thanks to the sort -R command in Linux) and so it seems like the listening is random.
While researching for this article, I discovered that roboplay can also play VGM and I downloaded a bunch of VGMrips files for the OPL1 (YM3526), OPL2 (YM3812), OPL3 (YMF262) and, of course, the OPL4 (YMF278) chips. If you unzip them (roboplay doesn't play VGZ, only VGM, but you just need to unzip them with gunzip and you'll get the VGM) they can be played directly by roboplay. But I've also discovered vgm-conv which allows me to convert VGM files from OPL to OPL2.
And now, besides using it as a jukebox, let's study it and learn more about its capabilities.
Moonsound specifications
The OPL4 chip incorporates the identical FM synthesis component found in the OPL3, but it further includes a dedicated wave synthesis section, functioning similarly to the ADPCM found in MSX Audio (the Y8950 chip, an OPL1 variant with an added ADPCM channel).
Unlike the OPLL, which featured preconfigured instruments, the OPL1-4 series provides the flexibility to customize instrument parameters for each individual channel. Let's concisely outline the capabilities and progression of the OPL chips:
OPL1: Offers 9 FM channels or alternatively, a combination of 6 FM and 5 percussion channels. Utilizing the YM3526 chip, the Y8950 (MSX-Audio) extends its functionality by incorporating an ADPCM channel. The MSX-Music (OPLL) represents a simplified version of the OPL1.
OPL2: Maintains the OPL1's core features while introducing support for 4 distinct waveforms.
OPL3: Expands upon the OPL2 by introducing 4 additional waveforms and enabling 4-operator operations for FM channels. This enhancement allows for a maximum of 18 channels. Prior to the OPL3, chips were limited to 2-operator operations.
OPL4: Retains the FM synthesis capabilities of the OPL3 but integrates a sample-based section employing PCM technology. This approach mirrors the ADPCM found in MSX Audio.
FM Part
To fully grasp the FM section's registers, it's essential to understand the concept of a "slot" as defined in Yamaha's documentation. A slot represents a circuit designed to generate a sinusoidal waveform. These slots correspond to the previously mentioned operands, which can be grouped in pairs (2-operator) or in groups of four (4-operator). The OPL4 possesses 36 slots. When configured in the minimal 2-operator pairing, this yields the advertised 18 FM channels. However, not all slots are compatible with 4-operator configurations; only 24 slots support this mode, leaving the remaining 12 to be grouped in pairs.
To configure these slots, two banks of registers are employed: bank 0 and bank 1. While these banks are nearly identical, they differ in their first 8 registers, which serve configuration purposes. Additionally, bank 0 includes register 0xBD, dedicated to rhythm control. Bank 0 is used to program the initial 18 slots, while bank 1 handles the subsequent 18. A schematic representation of the registers is as follows:
In addition to the explanation provided in Yamaha's official documentation, there is also a very good explanation in this OPL3 programming guide.
There are two types of registers: those that affect the slots, which are organized in blocks of 16 registers from 0x20 to 0x95. These registers are used to define the waveform of the sound. And those that affect the channels, which are organized in blocks of 9 registers from 0xA0 to 0xB8. These registers are used to define the note, octave, and other effects of the sound. This applies to each register bank. But 0x16 registers are 22 slots, and there are only 18 slots per bank. How is that possible? Well, because slot registers 0x?6, 0x?7, 0x?E, and 0x?F are not used; they don't modify any slot. Here is a graph that will serve as a quick reference for the corresponding address of each slot. The "set" indicator takes the value 0 for the first bank registers and 1 for the second bank registers.
But how are the slots grouped to form channels? As I mentioned before, there are different combinations; it is not arbitrary. I cannot combine slots 3, 4, 7, and 11, for example. They can only be grouped in a specific way. In this documentation, there is a table for each combination, but I have summarized everything into one:
Here we have the 18 channels that the OPL4 allows with two operations. Each column represents the operations of 2 slots. For instance, channel 3 consists of slots 6 and 9. These slots can be combined with 4-operator operations, which are those with the same color and ordered by operands from the smallest to the largest slot. For example, the 4-op channel number 0 is made up of slots 0, 3, 6, and 9, thus channel 3 disappears when modifying the 9-register blocks, such as the F-number and octave. If I also want to configure channel 2 as 4-op, channel 5 disappears, and the slots forming channel 2 are 2, 5, 8, and 11. If we configure for FM percussion, we lose channels 6, 7, and 8, which then become the Bass Drum (12 and 15), Hi-Hat (13), Tom-Tom (14), Snare Drum (16), and Cymbal (17).
In this last scheme, I used decimal numbering to indicate the slot. If we want to modify slot 7, we need to consider its offset to modify a value in the slot configuration registers. For example, for the Key Scale Level, we should modify register 0x49 instead of 0x47 because 0x?6 and 0x?7 do not exist.
Is it worth losing 3 FM channels for percussion? In the case of Moonsound, many composers used the entire FM section and then used the wave section for percussion, as this allowed them to use their sampled sound since it was noted that the sound provided by Yamaha was very poor.
To define an instrument, there are many parameters and combinations that affect the timbre of the generated sound (more like a xylophone, piano, bell, etc.). How can I know how the variation of one of these parameters will affect the generated sound?
Nowadays, there is Furnace, an open-source tracker inspired by Deflemask (which has an older version that is free). Unlike Deflemask, Furnace has many more chips for which music can be created. In the Furnace documentation on how to generate instruments, they provide this explanatory videotutorial on how to do it in Deflemask (the interface is very similar to Furnace). There are more video tutorials for Deflemask than for Furnace, but due to their similarity, they can also help you operate Furnace. If besides this practical explanation, you are interested in the theoretical part of FM generation, you can consult this explanatory document.
And what do we have for native MSX? When the Moonsound was released, Moonblaster was adapted to have a tracker that worked with the new chip. Two versions were released: Moonblaster FM and Moonblaster Wave, which served to operate the FM part and the Wave part of Moonsound, respectively. MBFM did not configure the 3 percussion channels but had 6 Wave channels to use as percussion. I have tried to figure out the instrument generation part of MBFM but have not been able to find it.
Let's try using the instrument generation in Furnace to create new instruments (or load those already available in the application) and use them with the MSX. Furnace instruments are saved in files with the .fui extension, while Deflemask instruments have the .dmp extension. We will focus on the first one since it is open-source and also allows loading dmp instruments.
There are two types of instrument formats in Furnace: the old format, which has its specifications here, and the new format, which you can find the specifications for here. The instruments I have loaded within the Furnace installation follow the old format, but we only need to load them into Furnace and save them so that they convert to the new format. One of the advantages of the new format is that it takes up less space.
Loading .fui files
What application will we use to study the Moonsound? I thought we could create an application that loads the instrument designed with Furnace and plays a couple of notes. Additionally, it will also display the parameters extracted from the file. So, let's first see the structure of this file.
.fui format
The structure of the modern version fui file can be found on the Furnace GitHub. The documentation is in text format and is not very visually appealing. Therefore, I have made this summary:
The first 6 bytes are the header, which starts with the word FINS to identify the type of file. Then, there may be the instrument name, which is formatted with the code NA, followed by 2 bytes that indicate the length of the name string and are encoded in little endian. That means if we read the values 0x0700, the correct length is 7 bytes instead of 1792. The last byte of the string is 0x0. Next, we can find the FM command and the length of the encoding block. All the following bytes contain different parameters for configuring FM chips, but not all are used by the OPL4. I have marked in blue those that we do use.
The first configuration byte, right after the FM block length, indicates whether the configuration is for 2 operators or 4 operators. If it is for 2 operators, the first slot of the channel comes first, followed by the next slot. If it is for 4 operators, the first is for the slot 0 of the lower channel, then for the slot 0 of the other channel that merges to form the 4-operator operations, and then the slot 1 of the respective channels.
Next to the scheme, I have placed the address where we find this configuration in the file we used as an example, which you can find on the MoltSXalats GitLab. You will see that starting from position 0x10, they repeat for each of the operators, and the file does not have the NA command indicating the name.
Based on this structure, let's write the code to find the FM part of the instrument saved in fui format and configure the Moonsound:
Let's start with the includes, and then we have the definition of MS_WAIT as a macro. It is a piece of code that reads a Moonsound register until it changes value. Unlike the OPLL, which did not indicate when we could write again, and therefore we had two writing functions, one for the Z80 and one for the R800, here it is not necessary because the wait is independent of the processor, only depending on the OPL4.
To write to the registers, we will use the sdcc function __sfr __at which performs an out to the address we specify, in our case 0xC4, 0xC5, 0xC6, and 0xC7. The first two are for the first FM register bank, and the last two are for the second register bank. In the OPLL article, we used a function containing assembler code, which performed the OUT instructions and wait instructions. Here, since we don't need to measure the wait, using sfr is sufficient.
In lines 18-23, we check if a Moonsound is inserted by reading if there is information in the Moonsound register. If it returns all zeros, it means it is not connected.
Lines 26-31 define the function ms_fm1_write, which writes to the register reg of the first block the value data we pass. First, we indicate to the Moonsound which register we want to modify, wait for it to indicate that it is ready for the next command using the macro function MS_WAIT, and then we indicate the data it should store. The function ms_fm2_write does the same but for the second block of registers.
From here, we start with the functions we have used in different articles for reading data from the disk: FT_SetName, FT_errorHandler, and FT_openFile. We have slightly modified the FT_errorHandler function to write error messages that indicate there is no FINS in the file being read and that no Moonsound was found.
Finally, we have the WAIT function, which waits for the number of cycles we indicate and will be used after each note to let it sound for a certain amount of time. We could also use the WaitForKey() function from the Fusion-C library to wait for the user to press a key when they want.
After the WAIT function, we define the buffer to store the bytes we read. We will read a maximum of 5 bytes at a time, and I instructed sdcc to store it at address 0x8000. This makes it easier to debug, but in a final version, it would not be necessary, and we would have the entire block of variables together.
And we start the main function. At line 111, we detect if there is a Moonsound; if not, we throw an error and exit. At line 115, we open the file, and at line 119, we initialize the Moonsound by specifying that we will use the OPL3 (FM) and OPL4 (wave) registers.
Next, we define the channel we will use; in this case, we have chosen channel 2, which, along with channel 5, forms a 4-operator channel (red columns in the channels/slots/operators table). We could have also chosen channels 0 and 3, 1 and 4, or their equivalents in bank 1. We then define an array that will contain the distribution of the slots we will use.
We then start reading the file. First, we check if the file is of type FINS (line 126). If it is not, we exit with an error message. We then read the next 4 bytes, which are not useful for this configuration. After that, we read bytes and detect if the command is NA, which contains the instrument's name, or if it is FM. If it is NA, we read the name bytes and do nothing with them. If it is FM, we exit the byte-reading loop and start the code section to configure the FM.
We read the 2 bytes that form the size of the FM block and store them in the variable blockLength to verify that we have read all the bytes we will control with numBytesLLegitsBlockFM.
Next is the configuration byte that indicates whether it is a 2-op or 4-op instrument (two operands or four). If it is 2-op, we will use channel 1 with slots 1 and 4. If it is 4-op, we will use channels 2 and 5. Note that at line 176, the offset for channel 8 is 0x0A as indicated in the slots/offsets table, and for channel 11 it is 0x0D.
The next byte contains information about the alg and fb parameters, which we store in different variables for further processing. The alg variable holds the information about the algorithm, which defines the type of connection for the 4 operators to generate the FM sound, and fb is a configuration parameter for the channel, not for the slot like the others.
Depending on the number of operands, we will have 4 possible options or only the simple connection. Although fb only affects the first channel, looking at what Furnace did, I found that it configured this parameter in both channels.
Another feature of byte 0xC? is that it indicates which speaker it should play through: left, right, or both. This is a property I had overlooked, and the vgm2txt tool I used for debugging also did not transcribe it to text. After many different tests and checking values, I discovered that it did not activate bits 4 and 5 of register 0xC? corresponding to the channel. That is why I always set a 3 in the upper bits of this register.
After setting the bit in 0xC0 for the algorithm, we read two more bytes that are not used in the OPL4.
And finally, the configuration of the slots in registers 0x2?, 0x4?, and 0x6?. Since I have determined the channel being used, all the configuration is for block 0; if I wanted to configure block 1, I would use the ms_fm2_write command. These registers are mixed in the fui file, so I read the 5 bytes where they are located and extract each parameter (lines 234-247) to then configure the corresponding registers (lines 249-251). The SL and RR bytes are in the same format as the register and are therefore configured directly. We read another byte that we won’t use, and finally, we set the waveform. The last lines of the loop display the read values on the screen.
At line 271, I define that we will not use drums, and we only need to define the note we want to play. Just like the OPLL, it has the F-number to indicate the note and the block to indicate the octave. I used F-number 582, which corresponds to the note A, but while debugging, I saw that Furnace also used a different value for the note A. This was also the case with Moonblaster, which had different F-number values than those in the documentation.
The F-number value is an integer and is divided into two different registers, as shown in lines 277 and 278. Additionally, at line 278, we also configure the octave and activate the channel. With the note now playing, we wait a few seconds and then proceed to play the other note.
To play the next note, you must first turn off the currently playing note and configure the next one, as was done with the OPLL. This is done in lines 285 and 287. We wait for the note to sound in the new octave, turn it off, and then end the script.
Conclusions
In this article, we explored the FM part of the Moonsound (OPL4), presented the different concepts of slots and channels used in the documentation, and loaded an instrument in Furnace format (fui) to play two notes.
As a practice exercise with the code, you can finish controlling the bytes read with the numBytesLLegitsBlockFM variable and verify that they match the blockLength.
Avelino Herrera has a couple of articles in call MSX 3 and call MSX 4 magazines where he discusses the Moonsound. The first one is about the Wave part, which we have not covered yet and will see later, and the second one is about the FM part. This second article also explains the registers, and instead of loading a fui file to configure them, it uses the sbi files. Another exercise you can do is to convert the C code from the article on his website to Fusion-C library format. I have already done this, as I was investigating why it didn't sound. The result can be found on the GitLab of MoltSXalats.
For those who cannot wait for the MoltSXalats article on the Wave part, apart from Avelino's article, there is also Moai-Tech issue 5 in paper (not to be confused with the new digital format) which is more similar to what we will cover here.
I would also like to explain that for debugging, I used the vgm2txt tool from vgmtools. Furnace allows exporting to VGM, which is a format that tracks the registers being modified in a song, and you can see all the steps taken to make it sound. Additionally, openMSX also has a command to record in VGM. You just need to open the console with F10 and type vgm_rec start Moonsound to start recording, and then vgm_rec stop to finish. The console will indicate where it saved the file. Don’t forget to launch openMSX with the Moonsound extension (-ext moonsound)! The VGM format is binary, which is why I used vgm2txt, which unfortunately did not transcribe the stereo bits of register 0xC?.
If you want to try another instrument from the Furnace library, just change the file name in line 115 (and copy this file to the directory you use as dsk in openMSX). Alternatively, if you don't want to recompile the program, you can copy the file to the directory, renaming it to op4bell.fui.
So, enjoy your MSX!
Click here for to download a .dsk disk image of a working example.
Comments