- Summary
- Download
- Installation and Usage
- Tape Dump Analysis
- Patch Data Format
- Coding Process
- License and Thanks
- Changelog
Summary
In 1983, Roland produced a digitally-controlled analog synthesizer called the JX-3P. It was the first Roland synth to support the then-new MIDI standard. I picked this synth up a few years ago because I was looking for an analog-styled synth and I liked the sound. The JX-3P has 4 banks of 16 patches; banks A and B are read-only factory presets, but banks C and D can be configured by the user and are saved in battery-backed memory.
Since the MIDI standard was still new, Roland did not include a way to dump and restore the patch memory through MIDI SysEx messages (like synths after this era). Instead, Roland went the path of contemporary home computers and provided a pair of audio ports on the unit so the user could connect the synth to their tape deck to store the patch parameters and sequencer data to magnetic tape (most musicians, it’s assumed, would have tape decks).
Using this functionality is well and good if all you want to do is back up the memory, but if you want to save individual patches or merge a pair of patch dumps, you’re out of luck. And if, like me, you find a great patch programmed by a previous owner and want to write down the settings and save them with your project, again, out of luck. The 16 LEDs on the front-panel display don’t provide enough resolution to give you the exact value of some of the continuous parameters, some of which have 256 levels. They’re good enough to get you in the ballpark, but you’re not going to get the more nuanced levels of precision in order to fully replicate the patch.
So what to do? Well, you write a program to listen to the tape dump, decode it, and put the patch parameters into a spreadsheet, of course. (Jump to Download section) Then, if you have installed the MIDI Expansion Kit produced by Organix, like I have, you can program your MIDI workstation software to send Continuous Controller messages to the JX-3P to set up the patch when you load your project.
Download:
Prebuilt utility executable (you’ll probably need only this):
JX-3P_tape_decoder-20130505 (462KB)
Unzip and read USAGE.txt for guidance on installation and usage.
Source code files (for experimentation):
JX-3P_tape_decoder_src-20130505 (1.5MB)
Find the included README.txt for more detailed info on compilation, installation and usage.
Installation and Usage
Installation: To install the utility, simply download the precompiled binary package and unzip it into a folder of your choice. If you want to look at the source code and compile the utility yourself, download the source archive, unzip it, and have fun.
Usage: This utility is intended for use as a command-line tool, meaning the user will need to execute the program in a command shell window (see the documentation for your operating system for how to do this). Open a command window, change to the directory where the executable file lives, and execute with the following command:
decode_patches <wavfilename>
Some operating systems (like some versions of Windows, for instance) will allow the user to drag-and-drop the tape dump audio file onto the executable icon. A command window will pop up, the tool will do its work, and if there are any errors in decoding, the window will remain open so you can read the error messages. On successful completion, the patch parameters will be in a spreadsheet in the file “patchdump.csv”.
Audio Preparation: for best results, you will need to take some care in preparing your tape audio file. Record your tape dump in the audio editor of your choice (directly from the JX-3P tape jack, if possible), trim any noise and silence before the beginning and after the end of the dump signal, amplify/normalize to the proper amplitude (between -12dB and -1dB), and save the file.


- Bit depth: 8, 16, 24 bits are supported (16 bits is optimal)
- Sample rate: 44.1 KHz is most suitable, but 22.1 KHz should work, too.
- Channels: The tool works best with mono only. If a stereo file is used, the tool will use only the left channel and ignore the rest.
- Format: Any format supported by LibSndFile is supported; this includes Microsoft PCM/WAV, Apple AIFF, and Sun AU.
- Volume: for best results, amplify or normalize the dump audio to an average amplitude of between -12dB and -1dB. Do not clip the audio. If it is too quiet, the logic won’t detect the signal.
- Source recording: best results are obtained by directly recording the dump audio; connect the JX-3P tape out jack to your workstation’s audio interface. Decoding audio previously stored on magnetic tape is currently untested and not guaranteed to work due to the excess noise, dropouts, jitter, stretch, and pre-emphasis compression inherent in tape recording and playback.
Tape Dump Analysis:
The tape dump audio signal is your typical Frequency Shift Key modulation containing two symbols: 0 and 1. Each symbol is one cycle of audio (two phase changes or zero-crossings). The long symbol (~882 Hz) is a binary 1, the short symbol (~3675 Hz) is a binary 0. The dump starts with a pilot tone of about five seconds of 1 symbols (4096 bits) so the user can set the record/playback volume; this also serves to train the synth (and the decoder utility) on the length of the 1 symbol. The patch data begins on the first detected 0 symbol. Each patch is repeated twice; on load, the synth picks the first one that decodes correctly. The two banks (C & D) are separated by another five second pilot tone.

Each patch record is 26 bytes long (recorded twice end-to-end to cover problems with tape crinkle, dropouts, noise, etc). Each 8-bit byte, stored Little-Endian, is enclosed in an 11-bit serial frame, generated by the onboard Intel P8051 microcontroller’s built-in UART, with one start bit of 0 and two stop bits of 1. There is no parity bit, since it’s useless to have parity on an asynchronous media like tapes which can’t be asked to retransmit if an error is detected. The last byte (25) is an additive checksum of all other bytes (0-24) and allows the synth to detect errors; if an error is found on the first copy the synth (and the decode utility) will attempt to decode the second copy.
Patch Data Format:
Byte 00 [0-5] unused [6-7] Data type (2=patch, 3=seq) Byte 01 [0-1] A/B bank (ignore) [2-7] unused Byte 02 [0-1] C/D bank indication (C=2, D=3) [2-7] unused Byte 03 [0-3] patch number (0~15) [4-7] unused Byte 04 [0-1] A01 DCO-1 range (16', 8', 4') [2-3] A02 DCO-1 waveform (saw, pulse, square) [4-5] A05 DCO-2 range (16', 8', 4') [6-7] A06 DCO-2 waveform (saw, pulse, square, noise) Byte 05 [0-1] A07 DCO-2 cross modulation (0=off 1=sync 2=metal) [2] B06 VCF Env polarity (0=negative, 1=positive) [3] B07 VCA Mode (0=gate, 1=envelope) [4] A11 DCO-2 ENV Mod (off/on) [5] A10 DCO-2 LFO Mod (off/on) [6] A04 DCO-1 ENV Mod (off/on) [7] A03 DCO-1 LFO Mod (off/on) Byte 06 [0-1] B10 LFO Waveform (sine, square, random, fast random) [2] A14 DCO ENV Polarity (0=negative, 1=positive) [3] B09 Chorus (off/on) [4-7] unused Byte 07 [0-7] A09 DCO-2 Fine Tune Byte 08 [0-7] A08 DCO-2 Tune Byte 09 [0-7] A13 DCO ENV Mod amount Byte 10 [0-7] A12 DCO LFO Mod amount Byte 11 [0-7] A15 VCF Mix Byte 12 [0-7] A16 VCF HPF Byte 13 [0-7] B04 VCF Resonance Byte 14 [0-7] B01 VCF Cutoff Freq Byte 15 [0-7] B05 VCF ENV Mod Byte 16 [0-7] B02 VCF LFO Mod Byte 17 [0-7] B03 VCF Pitch Follow Byte 18 [0-7] B08 VCA Level Byte 19 [0-7] B12 LFO Rate Byte 20 [0-7] B11 LFO Delay Byte 21 [0-7] B13 ENV Attack Byte 22 [0-7] B14 ENV Decay Byte 23 [0-7] B15 ENV Sustain Byte 24 [0-7] B16 ENV Release Byte 25 [0-7] checksum (unsigned sum of all bytes 00 to 24)
Coding Process:
The code I wrote includes a series of modules to break the work into functional parts: 1) detector, to detect phase changes and produce a list of zero-crossings; 2) demodulator, to read the list of crossings and generate a stream of bits, and 3) decoder, to read the bit stream and populate a data structure for reading and manipulation by client code.
I wrote several standalone binaries along the way to check my work and help me analyze the signal and format. The first binary was defined in analyzer.c, built to test the detector module. It opens the audio file, reads through it and performs some statistical analysis like min/max sample values, average value (DC bias), creates a histogram of distances between zero crossings (essential for determining symbol sizes), builds a list of crossings and dumps them to an external file for checking against the audio file in an audio editor.
The second standalone binary is defined in bitstream.c, built to test the demodulator. It listens to the audio, generates a list of zero crossings, and demodulates it into a stream of bits for output. It was at this stage that I was able to look at the bit stream for patterns; luckily, the JX-3P gave them up easily, and I was able to determine the bit position and length of each of the patch records.
The binary c1diff.c was instrumental in helping me, with a good bit of work, to sniff out where in this stream of bits each of the patch parameters lived. I did this by sitting at the synth and tweaking one parameter of patch C01 at a time (for all 32 parameters) and saving a dump after each change. The c1diff.exe binary was then used to load the baseline dump and one of the 32 modified dumps and indicate the differences. I then used that knowledge to build a data structure of bitfields for direct access to the packed parameters.
The final binary, decode_patches.c, is the culmination of all this work. It takes the filename of the audio file and outputs a spreadsheet of patch parameters in CSV format.
Since beginning my voyage of learning the C language, I’ve been looking for a real-world project, a real reason to dig deep into the language; that’s the only way to truly learn it. I’ve learned a lot in the month or so I’ve been hacking on this, and I’m glad for it. An earlier C project, an attempt at building a binary to decode cassette dumps from my Texas Instruments TI-99/4A computer from high school, was largely failed for several amateurish reasons, some of which were involved in coding style and my incomplete understanding of the problem space and the tools C provides. I used select pieces of that failed project as the basis for this, and in the process figured out where I went wrong on the first attempt. In this attempt I learned how to refactor the code into standalone functional blocks that could be used by multiple binaries. This is also the first project I’ve done that involved a library written by another author; it uses the fine libsndfile library for manipulating the audio file. And since multiple binaries and component source files were being used, it would’ve been madness to build each of the object files by hand with the proper GCC command, so I bothered to learn the miraculous, magical wonder of makefiles. I am now a real programmer!
Now that I’ve gotten to the point where I can read an audio file, get a data structure of banks and patches, and dump this into CSV format, I’m pretty-much done with this project. I could, if I had a real need, build a binary to dump the parameters into standalone MIDI files for pushing to the synth, or build a binary to take a collection of patches and generate a dump audio file for loading into the synth (an advanced exercise, but I have more important things to do). I might delve into decoding the sequencer tape dump, but since I don’t really use the onboard sequencer (it doesn’t sync to MIDI messages, and requires a CV converter to lock against another MIDI timepiece), I don’t see a real need to build the binary there, either. If someone has a need (but not the skill), I might be prodded to look into it.
Special note: the continuous controls are stored internally in synth RAM in 8-bit format, providing values between 0-255. These are the values exported to the tape dump. If you are using these values to construct MIDI CC messages to send to the Organix MIDI expansion module, you will have to convert them. The CC format allows for 7-bit values only, which gives you a range between 0-127. To convert, take the value from the spreadsheet and divide it by 2, dropping anything after the decimal. It’s half as precise as the native format, but that’s the nature of the MIDI beast. For calculating the CC values of the remaining non-continuous parameters, follow the MIDI support chart included with the MIDI expansion module.
License and Thanks:
Written by Shawn Thomas, January 2013. This code is provided as-is with no warranty expressed or implied. Use at your own risk. Author is not responsible for data loss.
Due to the difficulty of copyright enforcement once the source code is released to the public, the segments of this work written by me are released to the public domain. Do with it as you wish. If I can figure it out, you can figure it out. No magical skills required.
This work includes a dynamically-linked binary copy of the libsndfile library written by Erik de Castro Lopo for audio file I/O. It is covered by the LGPL license and is copyrighted by its author. If compiling on Linux, it is best to use your package management system to install the library and any ancillary development packages. Otherwise, download the library package from the libsndfile site and place a copy of the .dll/.so into the project’s root folder.
Special thanks go to Laurens (Organix) at Inque for the technical insight and feedback, and for designing the JX-3P MIDI Expansion Module. He’s a real stand-up guy.
Special thanks also go to Paul for being an early tester and providing real-world feedback.
Changelog:
- 2013-03-08: fixed field output values for some DCO-2 params. Thanks to Paul.
- 2013-04-03: fixed field output value for LFO waveform, was “saw”, changed to “sine”. Added LFO waveform “fast-random” to support later ROM updates which added the new waveform. Fixed DCO2 labeling to include “noise” as a waveform. Thanks to Paul.
- 2013-04-07: updated documentation to include a usage and installation guide. Tool now waits for user input if there are error messages (to help drag-drop users debug issues). Tool properly supports multi-channel audio files, using only the first channel. Thanks to Kenny. Also, split distribution into a precompiled package and a source package.
- 2013-05-05: Major reliability upgrades in the decoder module. It’s now better suited to withstand dropouts and noise in the dump signal. Fixed some faulty assumptions I made in the original releases that prevented the code from rebounding from a single bad bit detection in the stream. If both copies of a patch are unreadable, the code willl simply place a series of dashes in the spreadsheet. All users are advised to upgrade to this version; sorry for the mix-up. Thanks to Laurens at Organix for providing the one corner case that caused me to rework and vastly simplify my design into something more resilient.
Shawn, regarding my earlier comment that the patch decoder wasn’t decoding correctly, I found the error. The third fprintf statement is decoding dco1_range instead of dco2_range. Same with dco1_waveform, dco1_fmod_lfo and dco1_fmod_env. They should all point to dco2. First rate coding otherwise.
If you could update the code when you read this, it would be great. I’d fix myself but I haven’t installed gcc environment on PC before. I’m more of a hardware engineer but wrote some C years back.
Thanks,
Paul
Super good catch, Paul. Easy to overlook mistakes when the project’s end is in sight, like I’m rushing for the schoolroom door at the end of the school year.
I’ve made the bugfixes, rebuilt and tested the binary, and have posted an updated distribution zip file. Have another go at it.
Thanks for paying attention!
Shawn, thanks for fixing DCO2 references in the code, I checked it and the results are good. However, I have a few patches that use the DCO2 noise source and they don’t show correctly but decode as ‘saw’. Also, the LFO Waveform is type ‘sine’ instead of ‘saw’. The decoder_patch.c should have the following code change:
char * dco_waveform(int n)
{ static char *label[] = {“saw”,”pulse”,”square”,”noise”};
return label[n%3];
}
char * lfo_waveform(int n)
{ static char *label[] = {“sine”,”square”,”random”,”fast random”};
return label[n%4];
}
Everything else looks correct to me.
Hey Paul. Thanks for the feedback.
It didn’t occur to me to include and test for fast-random as an LFO waveform. As I understand, later ROM updates provide the waveform, and none of my custom patches used it, so it never popped up. After making a test patch with it, I love the waveform – makes a really dirty sound. I’ve updated the code to add this. I was also under the assumption that “saw” was one of the LFO waveforms; some of the docs I found are unclear, and it required a look at the silkscreen on the synth to be certain that the waveform was actually “sine”, so I’ve updated that label. Finally, DCO2 has its own label interpreter which correctly states “noise” as the DCO2 waveform. Thanks again for your eagle eye.