Back Original

Making my own software for the RX5USB

Jacob Vosmaer's blog

2025-11-23

I've started writing my own software for the RX5USB aftermarket memory cartridge for the Yamaha RX5 drum machine.

I have previously demonstrated this software in this video.

The RX5 and the RX5USB

The Yamaha RX5 is a 1986 drum machine. It generates its sound by playing back ROM samples, either from internal memory or from an external cartridge. The RX5USB is a programmable aftermarket waveform cartridge for the RX5 made by GliGli in 2012. What is particularly nice about the RX5USB is that it is an open source design: both the PCB design files and the source code for the Windows software you need to put audio data on the cartridge are freely available.

I've owned an RX5 for a long time but I don't use it much because the built-in sounds are not that nice. A friend encouraged me to check out the RX5USB, which I was reluctant to do because I don't have a Windows computer. But then I started thinking, surely I can make something that allows me to program the cartridge from macOS?

How the RX5USB works

The RX5USB hardware consists of a circuit board board that fits in the RX5 cartridge slot. The circuit board contains a flash memory chip, which looks like ROM memory to the RX5, and a Teensy microcontroller development board which lets you write waveform data to the flash memory chip via USB.

The firmware on the Teensy runs in a loop waiting for a USB connection. When one comes in, it receives data and overwrites part of the flash memory chip. In normal use, seated in the RX5, the Teensy does nothing: the RX5 talks directly to the flash memory chip as if it is a ROM chip.

So on a hardware level, the RX5USB is a programmable flash memory that uses a custom USB protocol to store data on the flash chip. One of the functions of the Windows RX5USB software is to write data via USB using this custom protocol.

The other function of the RX5USB software is to pack WAV samples into the internal memory layout of the RX5, and to edit sample metadata.

Challenge 0: parsing the RX5 memory format

I ordered a pre-fabricated RX5USB board on Reverb.com. While waiting for the board to come in, I started writing a parser for the RX5 wave memory format, based on the RX5USB Windows software. This helped me get familiar with the project. The original software is written in Delphi, which is an object-oriented extension of Pascal. It's been a very long time since I used Pascal but it was not too difficult to make sense of the program.

The RX5 wave memory format consists of a 1KB header followed by 127KB of PCM data. The procedure TRX5Sound.ImportFromBankData converts the RX5 format to the internal representation of the RX5USB software. My C version looks like this:

void loadvoice(struct rx5voice *v, uint8_t *p) {
  memset(v, 0, sizeof(*v));
  v->octave = p[0];
  v->note = p[1];
  v->pcmformat = p[2] & 1;
  v->loop = !(p[3] & 0x40);
  v->pcmstart = getaddr(p + 3) & ~0xff;
  v->loopstart = getaddr(p + 5);
  v->loopend = getaddr(p + 8);
  v->pcmend = getaddr(p + 11);
  v->ar = p[14];
  v->d1r = p[15];
  v->rr = p[16];
  v->d2r = p[17];
  v->d1l = p[18];
  v->gt = p[19];
  v->bendrate = p[20];
  v->bendrange = p[21];
  v->unknown = p[22];
  v->level = p[23];
  v->channel = p[24];
  memmove(v->name, p + 26, 6);
  v->name[6] = 0;
}

PCM data addresses are relative to the start of the 128KB wave memory block, using 24-bit addressing. Because the pcmstart address always starts at a multiple of 256, the RX5 only stores the two high bytes.

Figuring out the parser allowed me to make rx5-ls, which lists the metadata of the sounds in an RX5 ROM, and rx5-split, which extracts the audio data as separate WAV files.

One interesting feature here is that the RX5 supports 8-bit and 12-bit PCM audio data. The 12-bit format is squeezed so that 2 12-bit words use 3 bytes of memory. Even and odd 12-bit words are stored differently.

uint8_t *p, *pcmstart = rom.data + v->pcmstart,
        *pcmend = rom.data + v->pcmend;
uint16_t *q = pcmdata; /* Write 16-bit audio to here */
if (v->pcmformat) { /* 12-bit audio */
  for (p = pcmstart + 2; p < pcmend; p++, q++) {
    if ((q - pcmdata) & 1) {
      *q = (p[0] << 8) | (p[-2] & 0xf0);
      p++;
    } else {
      *q = (p[0] << 8) | ((p[-1] & 0x0f) << 4);
    }
  }
} else { /* 8-bit audio */
  for (p = pcmstart; p < pcmend; p++, q++)
    *q = *p << 8;
}

Because I have a binary dump of the original Yamaha WRC01 cartridge, and the cartridge itself, I was able to check that I got the parser right: you can look up most of the parameters in the RX5 user interface. For the PCM audio data I checked the result by listening to the WAV files.

Challenge 1: communicating with the Teensy firmware

Once I had access to an RX5USB board I started working on talking to the Teensy. It presents itself as a "raw" HID device. After trying a few USB libraries I settled on HIDAPI. The magic numbers for connecting to the RX5USB Teensy are in the original software. Translated to C that became:

hid_device *usbopen(void) {
  struct hid_device_info *devinfo;
  for (devinfo = hid_enumerate(0x6112, 0x5550); devinfo;
       devinfo = devinfo->next)
    if (devinfo->usage_page == 0xffab && devinfo->usage == 0x200)
      return hid_open_path(devinfo->path);
  return 0;
}

This makes it sound easy but it took quite a bit of trial and error to get this working.

The communication protocol of the RX5USB is a series of 64-byte messages. The host sends a message to the Teensy, the Teensy responds. Repeat many times. The first message is a handshake and it also tells the Teensy what address range of the flash chip the host wants to overwrite. Then the new contents of the flash chip are sent from the host to the Teensy 64 bytes at a time. The Teensy receive 64 bytes, writes them to flash, reads them back and sends the result back to the host.

It took me a frustrating amount of time to get this working because I was sending an incorrect address range in the first message. I misread the source code of the original software: the order of the fields in the handshake message is not the order that the original software sets the fields.

On top of that it turns out that the RX5USB board does not always make good electrical contact with the RX5, and then the RX5 will complain about a checksum error. I kept thinking this was because of a mistake in my software but it had nothing to do with it.

In spite of these difficulties I got rx5-program working, and I was able to test it by writing known-good RX5 ROM images to the flash chip and reading them in the RX5 itself.

Challenge 2: loading my own sounds into the RX5

Now that I was able to load existing ROM images onto the RX5USB I started working on creating new ROM images myself. To keep things simple, I started out by writing a program rx5-build that takes a list of WAV files as command line arguments and tries to pack them into a 128KB ROM file using default playback parameters.

This worked but I had a few more occasions to doubt myself because of the dodgy electrical contacts I mentioned earlier: sometimes garbled sounds would come out. I also encountered a bug that I still have not been able to fix. The RX5 has a reverse playback function where you can play any of the drum sounds backwards. When I load my samples, reverse playback works for some of them but not all: some sounds come out garbled in reverse mode. I hope to fix this one day because reversing sounds is nice.

Challenge 3: editing parameters

Because editing sounds on the RX5 is tedious I wanted to have a way to edit sound parameters on my computer before I put them on the RX5USB. I came up with a simple text file format to describe a bank layout, and I modified rx5-build to read that file format instead of taking a list of WAV files as command line arguments.

The file format uses key value pairs, one per line, with a space after the key. A bank description is a series of sound descriptions. Each sound starts with a file key that refers to a WAV file. Lines that start with # are ignored. For example:

# Add kick.wav with custom envelope settings and name
file kick.wav
name Kick!!
decay1rate 50
decay1level 0
# Add rim.wav with default settings
file rim.wav
# Add snare.wave with custom level
file snare.wav
level 30

Each sound must be assigned to one of the 12 RX5 output channels. If the description does not specify a channel for a sound, the ROM builder uses round-robin assignment to assign the sound to a new channel. Sounds must also have names. If you omit the name key, the ROM builder derives a sound name from the WAV file name.

I dreaded the complexity of making a file format for a while but it turned not to be complicated or a lot of work. I used a table to handle all the numerical parameters.

struct {
  ptrdiff_t offset;
  char *field;
  int min, max;
} params[] =
    {
        {offsetof(struct rx5voice, octave), fOCTAVE, 0, 4},
        {offsetof(struct rx5voice, note), fNOTE, 0, 120},
        {offsetof(struct rx5voice, ar), fATTACKRATE, 0, 99},
        {offsetof(struct rx5voice, d1r), fDECAY1RATE, 0, 99},
        {offsetof(struct rx5voice, d1l), fDECAY1LEVEL, 0, 60},
        {offsetof(struct rx5voice, d2r), fDECAY2RATE, 0, 99},
        {offsetof(struct rx5voice, rr), fRELEASERATE, 0, 99},
        {offsetof(struct rx5voice, gt), fGATETIME, 0, 255},
        {offsetof(struct rx5voice, bendrate), fBENDRATE, 0, 255},
        {offsetof(struct rx5voice, bendrange), fBENDRANGE, 0, 255},
        {offsetof(struct rx5voice, unknown), fUNKNOWN, 0, 255},
        {offsetof(struct rx5voice, level), fLEVEL, 0, 31},
        {offsetof(struct rx5voice, channel), fCHANNEL, 0, 11},
    },
  *pp;
for (pp = params; pp < params + nelem(params); pp++) {
  if (s = matchfield(line, pp->field), s) {
    int x = atoi(s);
    if (!rom.nvoice)
      errx(-1, "%s before file statement", pp->field);
    if (x < pp->min || x > pp->max)
      errx(-1, "%s out of range: %s", pp->field, s);
    ((u8 *)v)[pp->offset] = x;
    break;
  }
}

So far the main thing I'm using the bank description file format for is assigning channels. The RX5 channels have hard-wired pan positions in the stereo field and I like putting kick drums in the middle. Now that I write this I realize configuring this in the ROM parameters is unnecessary because the RX5 has individual outputs and for sounds where I care about the pan position, I usually bypass the internal RX5 mixer anyway.

Further challenges

I'd like to fix the sound reversing bug I mentioned earlier. It would also be nice to know what the "unknown" field is for; I copied this field name from the original RX5USB software.

Finally it would be nice to get looping working. I would like to be able to load AdventureKid Waveforms into the RX5 and use it as a synthesizer. I know looping is possible because the original RX5USB software can do it. I just haven't gotten around to implementing it in my bank description file format.

Conclusion

I would now normally say "this was a nice challenge" but it wasn't a challenge really, I knew this would work. I still find it satisfying that I got to make my own tools. Even though there are still bugs and missing features, the tools work and I have been using them to make music!

If you would like to know more you can also watch this demonstration video I made.

Thank you GliGli for creating the RX5USB and sharing it as open source!

Tags: music c

IndexContact