What is MSX-Music?
MSX-Music is a sound system based on the Yamaha YM2413 (OPLL - FM Operator Type-L Light), which is a simplification of the YM3812 (OPL2).
It should not be confused with MSX-Audio, which uses the Y8950 (OPL1) chip. A significant difference is that the Y8950 contains an ADPCM channel not found in the OPLL. The FM part is the same.
The Panasonic FS-CA1 cartridge is the only one that complied with the MSX-Audio specifications. Other commercial implementations of the time are found in the Philips NMS 1205 (Music Module) and Toshiba HX-MU900 (FM-Synthesizer Unit) cartridges, which can be expanded to be fully compatible with the format. The latest implementation of this standard is the AudioWave.
MSX-Music is found in the MSX 2+ and Turbo-R models. During the commercial era, it was also available with the Panasoft FM-Pak. Later, many more cartridges compatible with the format emerged, as indicated on the msx.org wiki. This sound system allows for generating 9 independent sound channels using FM synthesis technique, or 6 FM channels and 5 different percussions. It also enables the selection of different instruments for each channel, with 15 predefined instruments and one that can be created by the user. The chip's specifications can be found here.
What will we do in this post?
First, we will create an assembly function that writes the registers of the OPLL to configure them from the MSX. With this function, we will write a different note on each channel. We will also configure a custom instrument and change the instruments of the channels.
How do we configure the OPLL from the MSX?
The MSX configures the MSX-Music by accessing ports 0x7C and 0x7D. In the first one, the register number of the YM2413 we want to access must be written, and in the second one, we write the value we want this register to take. I found these values in some files that were passed to me in a Telegram group when I asked for information. I tried to find out if this information appeared in the MSX Data Pack, and I couldn't find it, but looking at the Grauw page, I saw that there is an excerpt from the MSX-Music Datapack where it appears.
How do we write these codes in C?
The assembly commands to perform these outs are with out(address, A). To do this with SDCC, the first thing that occurred to me is to use the assembly functions that SDCC allows, as explained by Eric Boez in his book 'FUSION-C: MSX C Library complete journey' or Konamiman on his github.
We need to pass two parameters that are of type char. In the first, we pass the register of the YM2413, and in the second parameter, we indicate the value that this register will take. If we look at Konamiman's examples, it coincides with the function SumTwoChars(char x, char y) from his example. The function to write parameters would be like this:
The __naked appendix in the function definition serves to indicate (as explained in the SDCC 3.11.3 Naked Functions documentation) that the compiler does not modify any register and that it is the programmer's responsibility to preserve the different registers. With the __asm and __endasm indicators, we mark that all the code between these two will be in SDCC assembly. This assembly has its syntactic peculiarities, such as for example, that to do (IY)+1 as it is done in most places, here it should be 1(IY).
The first thing we do is load the parameters into the D and E registers with the address we prepared before in the IY register. We wait a few cycles for the registers to stabilize with the ex af,af' command, and we load register A with the parameter we had saved in register D and perform the out. We wait for the out value to stabilize and make another output to address 0x7D with the value of the second parameter of the function that we had saved in E. We wait for some clock cycles so that the YM2413 can read the values, and we end the function.
In writing this article, I have seen that SDCC allows creating a variable and associating it with a port (explained in section 3.5.2 Z80/Z180/eZ80 intrinsic named address spaces of the manual) using the syntax __sfr__at (port_address) variable_name. So in our case, if we created __sfr__at(0x7C) register_number_YM2413, every time we did register_number_YM2413 = 0x10, the compiler would translate it with the commands:
ld A,#0x10
out (#0x7C),A
The only thing missing would be a C command that takes the same cycles as ex af,af', and we could have all the code without using assembly. But for now, let's continue with what I have already verified to work.
You may have noticed that we wait for different times for the OPLL to read each port. This is by Yamaha specification. As documented in the datapack in table 7.22.
You may have also noticed that the function is called Z80, and that's because for R800, which executes the commands faster, we have to execute more commands to have the same waiting time.
The operation of the Yamaha YM2413
As indicated in the chip's datasheet, in Table II-7, we have a map of the registers, which I'll copy here for easier reading:
The first seven registers are used to create our own instrument. There are plenty of resources that explain how to create instruments using FM synthesis, although it's a topic I've never tried myself. If anyone is interested, Moonblaster also had a synthesis part that allowed you to tinker with the presets it came with. I've looked at Furnace, and it also has a synthesis part. I've checked the web for instruments created by people, but I couldn't find any. What I do know is that you can load the ones in the instruments directory to study their presets.
Then we jump to register 0x0E, which configures the chip's operation mode. If we want it in rhythmic mode, bit 5 = 1, or in melodic mode, bit 5 = 0. In rhythmic mode, the last 3 channels are not used as instruments but configure 5 percussion instruments: Bass drum, Snare drum, Tom, Cymbal, and Hi-Hat.
Registers 10 to 18 contain the 8 least significant bits of the number F. This number F is closely related to the frequency, pitch, or note of the channel.
The next nine registers, 20-28, contain the most significant bit of number F, 3 bits to indicate the block/octave, 1 bit for whether the channel is active, and another to indicate whether it should sustain the channel's note.
The last nine registers, 30-38, each contain the type of instrument to play for that channel and its volume. The instruments are: 0-Original, 1-Violin, 2-Guitar, 3-Piano, 4-Flute, 5-Clarinet, 6-Oboe, 7-Trumpet, 8-Organ, 9-Horn, 10-Synthesizer, 11-Harpsichord, 12-Vibraphone, 13-Synthetic Bass, 14-Acoustic Bass, and 15-Electric Guitar. Regarding volume, keep in mind that 0 is maximum volume and 15 is minimum volume.
How do we indicate the note that must sound through one of the channels?
To indicate the note to be played for one of the channels, you need to modify the F-number and the Block of that channel. Each channel is defined by 3 registers. For example, for channel 0, registers 10, 20, and 30 are programmed; for channel 1, registers 11, 21, and 31 are programmed, and so on until channel 8, which is defined by registers 18, 28, and 38.
To translate the frequency of the desired note into the F-number and Block, Yamaha indicates in its manual (page 15) that F = (fmus * 2^(18) / fsam) / 2^(b-1), where F is the F-number, fmus is the frequency we want to sound, and b is the data block (octave). They also provide two tables with frequency examples (Table III-8-1 and Table III-8-2), where you can see that for different octaves, the same note changes its F-number. There is a post on msx.org discussing this issue, and one comment suggests that an easy solution is to set all F-numbers according to the frequency table in Table III-8-1 and then adjust the block as if it were the octave. This is the solution applied in the Moonblaster driver written in C. However, if we look at other implementations, such as the Moonblaster driver by bifi or the mbm player from roboplay, we see that both use a table with the following frequencies:
static const uint16_t g_frequency_table[] = { 0x00AD, 0x00B7, 0x00C2, 0x00CD, 0x00D9, 0x00E6, 0x00F4, 0x0103, 0x0112, 0x0122, 0x0134, 0x0146, 0x02AD, 0x02B7, 0x02C2, 0x02CD, 0x02D9, 0x02E6, 0x02F4, 0x0303, 0x0312, 0x0322, 0x0334, 0x0346, 0x04AD, 0x04B7, 0x04C2, 0x04CD, 0x04D9, 0x04E6, 0x04F4, 0x0503, 0x0512, 0x0522, 0x0534, 0x0546, 0x06AD, 0x06B7, 0x06C2, 0x06CD, 0x06D9, 0x06E6, 0x06F4, 0x0703, 0x0712, 0x0722, 0x0734, 0x0746, 0x08AD, 0x08B7, 0x08C2, 0x08CD, 0x08D9, 0x08E6, 0x08F4, 0x0903, 0x0912, 0x0922, 0x0934, 0x0946, 0x0AAD, 0x0AB7, 0x0AC2, 0x0ACD, 0x0AD9, 0x0AE6, 0x0AF4, 0x0B03, 0x0B12, 0x0B22, 0x0B34, 0x0B46, 0x0CAD, 0x0CB7, 0x0CC2, 0x0CCD, 0x0CD9, 0x0CE6, 0x0CF4, 0x0D03, 0x0D12, 0x0D22, 0x0D34, 0x0D46, 0x0EAD, 0x0EB7, 0x0EC2, 0x0ECD, 0x0ED9, 0x0EE6, 0x0EF4, 0x0F03, 0x0F12, 0x0F22, 0x0F34, 0x0F46 };
This table contains 8x12 elements, which are all the frequencies of the 12 notes for the 8 octaves. At first, I thought these values were the frequencies of the musical notes. However, after reopening the post mentioned earlier, it was clarified to me that these values are directly for register 10 and the 4 least significant bits of register 20 (I've mentioned 10 and 20, but they apply to all channels, from 0 to 8). Then, I was able to verify with different calculations using the previous values that they indeed use the octave as the block number, and the F-Numbers were repeating. From the previous data, I deduced that they used a fsam of 83340.57803, although I'm not exactly sure where this value comes from. An example of the table for two octaves is:
Nota | Frequency of the note | octave | Fnum | Fnum in hex |
A1 | 55.0 | 1 | 173 | AD |
A#/Bb1 | 58.27047018976124 | 1 | 183.287115324158 | B7 |
B1 | 61.735412657015516 | 1 | 194.185934357522 | C2 |
C1 | 65.40639132514966 | 1 | 205.732830895471 | CD |
C#/Db1 | 69.29565774421802 | 1 | 217.966341631813 | D9 |
D1 | 73.4161919793519 | 1 | 230.927294771416 | E6 |
D#/Eb1 | 77.78174593052023 | 1 | 244.658946290545 | F4 |
E1 | 82.4068892282175 | 1 | 259.207124299666 | 103 |
F1 | 87.30705785825097 | 1 | 274.620381990499 | 112 |
F#/Gb1 | 92.4986056779086 | 1 | 290.950159677785 | 122 |
G1 | 97.99885899543733 | 1 | 308.250956476557 | 134 |
G#/Ab1 | 103.82617439498628 | 1 | 326.580512187866 | 146 |
A2 | 110.0 | 2 | 173 | AD |
A#/Bb2 | 116.54094037952248 | 2 | 183.287115324158 | B7 |
B2 | 123.470825314031 | 2 | 194.185934357522 | C2 |
C2 | 130.8127826502993 | 2 | 205.732830895471 | CD |
C#/Db2 | 138.59131548843604 | 2 | 217.966341631813 | D9 |
D2 | 146.83238395870376 | 2 | 230.927294771416 | E6 |
D#/Eb2 | 155.56349186104046 | 2 | 244.658946290545 | F4 |
E2 | 164.813778456435 | 2 | 259.207124299666 | 103 |
F2 | 174.61411571650194 | 2 | 274.620381990499 | 112 |
F#/Gb2 | 184.9972113558172 | 2 | 290.950159677785 | 122 |
G2 | 195.99771799087466 | 2 | 308.250956476557 | 134 |
G#/Ab2 | 207.65234878997256 | 2 | 326.580512187866 | 146 |
A3 | 220.0 | 3 | 173 | AD |
Therefore, to write the note, we'll determine which note it is, its corresponding F-number, and the octave we want it to sound in. If the F-number is 9 bits, we'll use the one from register 20.
Program that plays a note
Let's make this chip sound with a simple program:
First of all, let's create the assembly functions. If the computer is a Turbo-R, we'll add more commands to wait until the registers are stable to be read, which is the WriteOPLLreg_TR function.
Let's start the main function. This time we'll use screen 0 to display messages on the screen and wait for a key to be pressed to continue with the test.
On line 59, we choose instrument 15 (electric guitar, where 15 is 0xF, and we place it in the upper part of register 0x30, which selects the instrument and volume, in this case 0, which is the maximum volume) for channel 0. Now, we need to set the note and octave to be played for channel 0. The note is an A, which according to the previous table has a value of AD. We choose octave 4 (b100), and if we shift it one position to the left since the least significant bit is the ninth bit of the F-number, we have 0x8. If we say that the channel is sounding (Key on bit of register 0x20), we set the value 0x18 to register 20. If we wanted it to sustain (gradually reduce the volume when the key is turned off), we would write the value 0x38.
We indicate that the sound is playing on the screen and wait for a key to continue.
Once a key is pressed, we change the note to a lower one, two octaves down. For this, we turn off the note by setting the Key bit to 0 in register 0x20, write the same F-number, and then activate the Key and the octave in register 0x20. At line 69, we wait for a key again.
Now, we'll change the instrument and return to the note A (A4). To do this, we turn off the note, configure the violin, set the F-number again, set the octave, and activate the channel (Key bit). All of this is done between lines 72 and 75. We indicate the new configuration on the screen and wait for a key.
Between lines 79 and 91, we change the octave of the note again and turn it off without sustaining it. Then, we configure the channel with sustain on line 93 and wait for a key to turn it off. When we press it, we'll see that the sound doesn't stop immediately like before when it didn't have sustain, but it fades out over time. Depending on the chosen instrument, the effect of sustain may be more or less noticeable. There are instruments that don't sustain the note but behave more like percussion, and in those cases, the sustain effect is hardly noticeable.
Once we press a key to continue, we'll play the C major chord, which consists of the notes G3, E3, and C3, each played with a different instrument. Between lines 101 and 103, we set the instruments: clarinet for channel 0, oboe for channel 1, and horn for channel 2. Then, we set the F-number for the corresponding notes: 0x134, 0xCD, and 0x103. Two notes have a 9-bit F-number, so when we configure the corresponding registers at 0x30, we'll need to add 1 to the octave part as seen on lines 109, 110, and 111. Channel 1, which doesn't have the ninth bit, has a value of 0x36 in register 0x21, while the others have the value 0x37.
Once the chord is played, let's change the volume of channel 1, the one for the oboe, by setting a number greater than 0 in the low byte of register 0x31 (line 114). We wait for a key and then turn off all channels and exit the application with Exit(0) to finish our test.
Conclusions
In this program, we've seen how to play different notes with the OPLL (MSX-Music) of the FM-PAC, which comes standard on MSX2+ and Turbo-R computers. The next step could be to create our own instruments, using tools like Furnace and examining the default instruments to understand how they work.
Another topic would be tempo. Here, we conducted the test sequentially and without moving any sprites. If we wanted to do so, we would need to use interrupts. In this case, since the OPLL doesn't generate any interrupts, we would need to use the VDP interrupt, which occurs at 50Hz or 60Hz (depending on whether it's PAL or NTSC), and from there, control the tempo. This is what's used in this implementation of the Moonblaster player written in C, which uses the InitVDPInterruptHandler command from Fusion-C to overwrite the VDP Hook and have our function executed every 50 (or 60) times per second, where in this case, the function checks which note should be played and configures the corresponding registers to make it sound.
Click here to see the example working.
Comments