Go to JSynthLib User's Guide.
Even if someone want write just a driver for his/her synth, the code will be read by others too. Maybe some changes are necessary since an API change or another developer want take a look how a special issue is solved. Following some rules makes the life easier for all who contribute to JSynthLib.
Important note: Please use an indentation level of 4 and a displayed tab width value of 8
(see FAQ for reasons).
private
or
protected
is used properly, for example, there is no need
to grep all files to see how a method or a field is used.
Especially we have to be very careful to make a public
(or package) field. If you really need to access it, consider
defining a getter or setter method (ex. getFoo() or
setFoo()) or using protected
.
Use ErrorMsg.reportError("Title", "Message", e) for error message which users need to see.
// BAD EXAMPLE try { .... } catch (Exception e) { }This makes debugging difficult. Catch a specific Exception and put a comment, if you know that it is not an error.
try { .... } catch (ASpecificException e) { // This is normal. }If it is an error or you are not sure, show a debug message. (Of course, implement proper error handling code if possible.)
try { .... } catch (ASpecificException e) { ErrorMsg.reportStatus(e); }Again catch a specific Exception. Don't catch Exception without good reason.
mkdir JSynthLib cd JSynthLib jar xf ../JSynthLib-XXX.jarYou can compile them by the following steps.
# On Windows use semicolon (;) instead of colon (:). javac -classpath '.:groovy.jar' org/jsynthlib/*/*/*.java org/jsynthlib/*/*.java javac core/*.java javac synthdrivers/*/*/*.java synthdrivers/*/*.java javac *.javaYou may also use the following method:
find . -name '*.java' | grep -v midiprovider/ > js # On Windows use semicolon (;) instead of colon (:). javac -classpath '.:groovy.jar' @jsAnd Makefile and ANT build file are included. You may compile JSynthLib by simply typing;
make allor
antIf you are using Eclipse, enable the auto-compile feature. Eclipse compiles necessary files in background every time you save a file.
Next, open up a Command Prompt or shell set to 50 row display and set it to that directory. This is used to run JSynthLib for testing (the 50 row display helps keep error messages from scrolling off the screen too fast). Make sure that the java[.exe] and javac[.exe] executables are in your path for easy access.
Important: Once you've extracted the .jar file and made changes to the resultant files, you must launch JSynthLib using;
# On Windows use semicolon (;) instead of colon (:). java -classpath '.:groovy.jar' JSynthLib -D 3instead of java -jar JSynthLib.jar. Otherwise you just run the old .jar file and none of your alterations get used. The command line option '-D 3' gives you various debug messages from ErrorMsg class. The messages help you.
Although the source code for each version of JSynthLib is included in the JSynthLib distribution, you may wish to download the latest version of the source code to get any improvements made after the latest release. We sometimes have to change API to make JSynthLib better. Your driver developed for the released JSynthLib may be required some changes for the latest version. You can downloaded the latest source code via SVN from SourceForge SVN server.
svn co https://jsynthlib.svn.sourceforge.net/svnroot/jsynthlib/trunk/JSynthLib JSynthLibFor more detail go to SVN page on JSynthLib SourceForge.net site.
To be compliant with the JSynthLib core sources it is best if you use an editor which supports our code conventions (especially concerning the tab settings).
Some of the most active developers of this project started using Eclipse (for setup have a look at the FAQ). It knows Java syntax well. I can search reference and/or definition of a member, compile files auto-magically, has integrated SVN support, etc.
/* * Copyright 20XX Your Name * * This file is part of JSynthLib. * * JSynthLib is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * * JSynthLib is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with JSynthLib; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */Don't forget editing the first line.
When you compile parts of JSynthLib, always do so from the main JSynthLib directory. For example, to compile the file synthdrivers\BossDR660Driver\BossDR660DrumkitDriver.java sit in the main JSynthLib directory and type
javac synthdrivers\BossDR660Driver\BossDR660DrumkitDriver.java. Don't enter that directory and just type;
javac BossDR660DrumkitDriver.java # does not work
javadoc -locale en_US -breakiterator -quiet -d doc/api_docs -use -author -version -overview doc/programming.html core/*.javaView doc/api_docs/index.html with your favorite browser. This document is on the top page with hyperlinks to class, interface, and member definitions.
A Single Driver extends {@link core.IPatchDriver} class and allows JSynthLib to detect a patch data for the synthesizer, to communicate with the synthesizer. Once this is written, JSynthLib will have Librarian support for your synthesizer.
A Single Driver optionally can have a Single Editor invoked by IPatchDriver.editPatch() method.
A Bank Driver extends {@link core.IPatchDriver} and allows JSynthLib to combine a single patch into a bank patch and to extract a single patch from a bank patch. While this functionality isn't strictly necessary, it is nice to have if your synth supports bulk dump patch.
A Bank Driver can have a Bank Patch Editor. But you don't have to take care of it because usually the default editor can be used.
{@link core.Device} class (required) {@link core.IDriver} interface {@link core.IPatchDriver} interface extends {@link core.IDriver} (required) {@link core.IConverter} interface (optional) {@link core.IPatch} interface {@link core.ISinglePatch} interface extends {@link core.IPatch} (required) {@link core.IBankPatch} interface extends {@link core.IPatch} (optional)All what you have to do is to implement java classes which implements one of these interfaces. Of course you don't have to write whole code from scratch. By extending existing classes, you can implement your driver with reasonable effort. Most of the current synthdrivers extend the following classes;
{@link core.Device} class {@link core.Driver} class implements {@link core.IPatchDriver} {@link core.BankDriver} class implements {@link core.IPatchDriver} (optional) {@link core.Converter} class implements {@link core.IConverter} (optional) {@link core.Patch} class implements {@link core.ISinglePatch} and {@link core.IBankPatch} classActually the interfaces described above was just introduced in the JSynthLib-0.20 release.
/** Constructor for DeviceListWriter. */ public KawaiK4Device() { super("Kawai", "K4/K4R", "F07E..0602400000040000000000f7", INFO_TEXT, "Brian Klock & Gerrit Gehnen"); } /** Constructor for for actual work. */ public KawaiK4Device(Preferences prefs) { this(); this.prefs = prefs; addDriver(new KawaiK4BulkConverter()); addDriver(new KawaiK4SingleDriver()); addDriver(new KawaiK4BankDriver()); // ... other drivers }The first constructor just defines various informations of your synth and authors of this Device class. This is used by the second constructor and DeviceListWriter described in the next section.
The second constructor is used for the actual work. It creates drivers and add them onto the driver list by using addDriver method.
java.util.prefs.Preferences
object prefs
.
By using this and overriding config() method, your driver can have
persistent parameters for your synth driver. See actual code, for
example RolandTD6Device, for more details.
java core/DeviceListWriterThis recreates the synthdrivers.properties file.
Invoke JSynthLib, go to Window->Preferences...->Synth
Driver
, and click Add Device
button. If all goes
well, you see your Device name is on the list.
Now JSynthLib knows about your synth driver. Now the time to write drivers :)
First, you change your constructor to provide the correct information to the driver. This information is used by the functions in Driver class to manipulate the data. If your synthesizer were very simple, that would be just about all there was to writing the driver. However, most synths have their unique quirks and features that are impossible to describe using just data. Therefore you will probably have to override some of the functions in Driver class to perform for your synth.
{@link core.Driver#calculateChecksum(Patch, int, int, int)} actually calculate checksum. The default method uses the most common algorithm used by YAMAHA, Roland, and etc. It adds all of the data bytes, takes the lowest 7 bits, XORs them with 127 and adds one to get the checksum. If your synth uses a different algorithm, override this method.
{@link core.Driver#calculateChecksum(Patch)} is called by some methods in {@link core.Driver} class. If your synth does not use checksum, override it by an empty method. If the patch for your synth consists of only one Sysex Message, you don't have to override the method. If your synth uses a patch which consists of multiple Sysex Messages, you need to override the method. See, for example, RolandTD6SingleDriver.java.
Looking at your driver code (which you stole from the KawaiK4 code like I told you to). You'll see that we had to implement two methods, {@link core.Driver#storePatch(Patch, int, int)} and {@link core.Driver#sendPatch(Patch)}. storePatch sends a patch to a buffer in your synth specified by a user, and sendPatch sends a patch to the editing buffer in your synth. This is common, since usually, slightly different sysex messages are used to send a patch to the editing buffer (and not overwrite a patch) or to a specific patch (and overwrite). Change these functions to match your synth. If your synth has no editing buffer, you'll need to overwrite the send method to treat a specific patch location on the synth as the edit buffer it (see the EmuProteusMPS Driver for an example of this).
One of these is the {@link core.Driver#createNewPatch} method which returns a new (blank) patch. You may use {@link core.DriverUtil#createNewPatch} for this method. See RolandTD6SingleDriver as an example.
The other is the {@link core.Driver#editPatch} method which opens an Single Editor window for the patch. You should be able to figure out how to write these by looking at the code for the KawaiK4.
There may be other functions in Driver.java that you will need to override for your synth. In general, spend time looking at that file, the drivers for all of the other synths, and API document to get a feel for how things are done.
Once your driver is working, JSynthLib now has Librarian support for your synth. Celebrate. And send in the code to us so we can include it in the next release of JSynthLib.
You may want to use your Single Driver in your Bank Driver. You can pass your Single Driver via the constructor of your Bank Driver. Here is a part of RolandTD6Device Constructor;
public RolandTD6Device(Preferences prefs) { ... // add drivers TD6SingleDriver singleDriver = new TD6SingleDriver(); addDriver(singleDriver); addDriver(new TD6BankDriver(singleDriver)); }
Writing an editor is a little bit harder than writing a driver. It is harder because you have to write a Java Swing interface. Swing is the default graphical toolkit that comes with Java. It is the graphical interface which all of JSynthLib uses. If you don't know Swing at all you might want to get a Java book, but you can get away with just the Tutorial on java.sun.com and the API guide that you can download when you download the JDK.
The job of your Single Editor class is to set up the interface for your editor by using a number of functions and widgets provided by JSynthLib and a number of Swing features. It is invoked by {@link core.IPatch#edit()} method. It returns a {@link core.JSLFrame} object. What you have to do is create a class extending {@link core.PatchEditorFrame} class which extends {@link core.JSLFrame} class. (You don't have to extend {@link core.PatchEditorFrame}, if you want to create a JSLFrame object from scratch.)
I like to set up all my sliders and CheckBoxes and figure out where they go and what there ranges are first, get a nice pretty GUI on screen and then go back and put in the numbers to make it actually work. Basically you are going to create a number of JPanes and place controls inside them using the {@link core.PatchEditorFrame#addWidget} method. Then you insert these JPanes onto scrollPane (the background of the window). scrollPane is created by JSynthLib for your convenience.
If you want Tabs and such, you can implement them the same way the other editors do using the Swing feature. For a simple synth, you can get away with just putting all the JPanes directly onto scrollPane.
You can do like with the Single Driver and start out by copying an existing editor over and renaming the file, classes, constructor, etc. I recommend the YamahaTX81z Editor since it is one of the simplest Single Editor.
Make sure you've taken a look at how all the other Single Editors are set up, the code contained within them explains all better than I could cope with mere words.
// example from the TX81z Editor addWidget(cmnPane, // pane to put the SysexWidget on new ScrollBarWidget("Feedback", patch, 0, 7, 0, new ParamModel(patch, 100), new VcedSender(53)), 1, 1, // horizontal/vertical location 3, 1, // horizontal/vertical size 18); // fader numberThe first parameter is the JComponent (pane) you wish to put the SysexWidget on. You would have created this pane previously as a JPane. Usually I use panes to break the interface into functional sections (such as LFO parameters, filter parameters, etc.).
The second parameter is the SysexWidget to create, in this case a {@link core.ScrollBarWidget}. Notice that the widget itself takes a few parameters, we'll get to that in a moment.
The next four parameters represent the location and size of the SysexWidget within the Pane (Read up on gridbaglayout in Sun's Java tutorial). They are (in order) the horizontal location, the vertical location, the horizontal size, and the vertical size).
The last parameter is the fader number. Each SysexWidget needs to have a unique fader number and they should go in order. label all SysexWidgets with positive numbers except for CheckBoxes which get negative numbers (buttons). So the sliders etc. would go 1, 2, 3, 4, 5.. and the CheckBoxes would go -1, -2, -3, -4, -5... etc.
{@link core.ScrollBarWidget} contain a label, a slider, and a numeric readout of the value of the slider. They are the most common SysexWidgets. The first parameter to the SysexWidget should be its label. the second one will always be patch. The next two values are the minimum and maximum values on the slider. The fifth value is a value offset for the slider. This is usually zero, but gets used if the parameter in the synthesizer is (for example) 0 through 127, but should be displayed as -63 to +63). The last two parameters are a Param Model and Sender for this fader (we'll get to those after we deal with some other SysexWidgets.).
{@link core.ScrollBarLookupWidget} are just like ScrollBarWidget but they let the numeric readout contain values other than numbers. They are used rarely. For example if a parameter can take the values of OFF, 1,2,3,4,5,6 or 7, you might use one. You wouldn't want to use a ScrollBarWidget because the first value should be OFF, not zero. You could also use a ComboBoxWidget in this situation. The constructor is about the same as for a combo box.
{@link core.ComboBoxWidget} are drop-down list of choices that are best used for non-numeric data (such as LFO shape). Here's an example of setting up a ComboBoxWidget:
addWidget(panel, new ComboBoxWidget("EG Shift", patch, new ParamModel(patch, 20 + i*5), new AcedSender(i*5 + 4), new String[] {"96db", "48db", "24db", "12db"}), 0, 4, 1, 1, 5);Parameters to the constructor are 1) a Label, 2) patch, 3) a Param Model, 4) a Sender, and 5) an array of all the values that it can contain.
{@link core.CheckBoxWidget} are used for parameters that can be either on or off. Heres an example:
addWidget(lfoPane, new CheckBoxWidget("1", patch, new ParamModel(patch, 55 + 3*13), new VcedSender(3*13 + 8)), 3, 6, 1, 1, -19);Parameters to the Constructor are 1) a Label, 2) patch, 3) a Param Model, and 4) a Sender.
{@link core.EnvelopeWidget}, the final SysexWidget type, are vastly more complex than the others.They represent several parameters on the synth, such as the attack, decay, sustain, and release of a VCA envelope.
Note that the fader number you give to an envelope widget represents is its first fader number, and it will take as many as it needs starting at that one to represent all of it's parameters.
The constructor for an envelope widget takes a list of {@link core.EnvelopeWidget.Node}s. Each Node is one of the movable squares on the envelope. Some of these nodes are stationary, some contain two parameters on the synth and can be moved vertically and horizontally, and others contain only one parameter and can therefore be moved in only one direction. Here is an example:
EnvelopeWidget.Node[] nodes = new EnvelopeWidget.Node[] { // origin new EnvelopeWidget.Node(0, 0, null, 0, 0, null, 0, false, null, null, null, null), // delay time new EnvelopeWidget.Node(0, 100, new K4Model(patch, 30 + i), 0, 0, null, 0, false, new K4Sender(34, i), null, "Dly", null), // atack time new EnvelopeWidget.Node(0, 100, new K4Model(patch, 62 + i), 100, 100, null, 25, false, new K4Sender(45, i), null, "A", null), // decay time and sustain level new EnvelopeWidget.Node(0, 100, new K4Model(patch, 66 + i), 0, 100, new K4Model(patch, 70 + i), 25, false, new K4Sender(46, i), new K4Sender(47, i), "D", "S"), // null node for constant length horizontal line new EnvelopeWidget.Node(100, 100, null, EnvelopeWidget.Node.SAME, 0, null, 0, false, null, null, null, null), // release time new EnvelopeWidget.Node(0, 100, new K4Model(patch, 74 + i), 0, 0, null, 0, false, new K4Sender(48, i), null, "R", null), }; addWidget(panel, new EnvelopeWidget("DCA Envelope", patch, nodes), 0, 0, 3, 5, 33);As you see, the Envelope Widget takes a label, followed by patch and then an array of EnvelopeWidget.Node objects. The parameters given to the EnvelopeWidget.Node constructor have the following meaning:
public Node(int minx, int maxx, ParamModel pmodelx, int miny, int maxy, ParamModel pmodely, int basey, boolean invertx, SysexSender senderx, SysexSender sendery, String namex, String namey)
I hope that made sense, if not, just take a look at the way EnvelopeWidget are used by various single editors.
{@link core.CheckBoxWidget}, {@link core.KnobWidget}, {@link core.KnobLookupWidget}, {@link core.LabelWidget}, {@link core.PatchNameWidget}, {@link core.ScrollBarLookupWidget}, {@link core.VertScrollBarWidget}, {@link core.SpinnerWidget}, and {@link core.TreeWidget} are also provided.
We want to keep track of the changes to the patch so that when we next call up this patch the changes are there. We also want to be able to set the SysexWidgets to the correct values for a particular patch when the Single Editor is opened. This is what the Param Model (Parameter Model) is for.
Param Model is a class object which implements {@link core.SysexWidget.IParamModel} interface. The interface provides two method, {@link core.SysexWidget.IParamModel#set} and {@link core.SysexWidget.IParamModel#get}. They just read (get) and write (set) a parameter value.
The default Param Model (constructor ParamModel(Patch patch,
int offset)
) is used by the YamahaTX81z editor. Its constructor
takes two parameters, the patch being edited and the
offset into the patch of the value.
Sometimes the default Param Model can be used, other times it is either necessary or more convenient to subclass ParamModel to make your own Model. Here is a simple example from the KawaiK5000 Editor:
class K5kSrcModel extends ParamModel { public K5kSrcModel(Patch patch, int src, int offset) { super(patch, 91 - 1 + offset + 86 * (src - 1)); } }This overrides only constructor to make the calculation of offset easier.
Here is a more complex example from the KawaiK4 Editor:
class K4Model extends ParamModel { private int bitmask; private int mult; ... public K4Model(Patch p, int offset, int bitmask) { super(p, offset + 8); this.bitmask = bitmask; if ((bitmask & 1) == 1) mult = 1; else if ((bitmask & 2) == 2) mult = 2; else if ((bitmask & 4) == 4) mult = 4; else if ((bitmask & 8) == 8) mult = 8; else if ((bitmask & 16) == 16) mult = 16; else if ((bitmask & 32) == 32) mult = 32; else if ((bitmask & 64) == 64) mult = 64; else if ((bitmask & 128) == 128) mult = 128; } public void set(int i) { patch.sysex[ofs] = (byte) ((i * mult) + (patch.sysex[ofs] & (~bitmask))); } public int get() { return ((patch.sysex[ofs] & bitmask) / mult); } }
In the case of the KawaiK4, we couldn't just use the default ParamModel class, because some Kawai K4 parameters are bitmasks stored in the same byte as other parameters. The above class K4Model takes care of deciphering the bitmasks. MIDIboxFMModel.java is another example which uses bitmasks.
You can also implement SysexWidget.IParamModel directly if you
don't need patch
or ofs
field. See
AlesisDMProModel as an exmaple.
A Sender sends a Sysex message generated by Param Model to the synth informing it of the change. It is a class object which implements {@link core.SysexWidget.ISender} interface. The interface has only one method, {@link core.SysexWidget.ISender#send}.
Here's an example of a Sender from the Kawai K4 Editor. It extends {@link core.SysexSender} class which implements {@link core.SysexWidget.ISender}:
class K4Sender extends SysexSender { private int source; private byte[] b = { (byte) 0xF0, 0x40, 0, 0x10, 0x00, 0x04, 0, 0, 0, (byte) 0xF7 }; public K4Sender(int parameter, int source) { this.source = source; b[6] = (byte) parameter; } public K4Sender(int parameter) { this.source = 0; b[6] = (byte) parameter; } public byte[] generate(int value) { b[2] = (byte) (channel - 1); b[7] = (byte) ((value / 128) + (source * 2)); b[8] = (byte) (value & 127); return b; } }This sender has two constructors. One has a parameter
source
and the other does not.
Here is another example of RolandJV80SystemSetupEditor which uses {@link core.SysexSender#SysexSender(String)} constructor.
static class JVSender extends SysexSender { int offset; // retrieve default from patch public JVSender(int offset) { super("F041@@461200000000**00F7"); this.offset = offset; } protected byte[] generate(int value) { byte[] data = super.generate(value); data[JV80Constants.ADDR4_IDX] = (byte) offset; JV80Constants.calculateChecksum(data, 1); return data; } }
The constructor for a SysexSender takes a String parameter representing which parameter a particular instance of the Sender should control and creates a sysex message containing all necessary information except for the data value to be transmitted and the channel (Device ID) to transmit on. When the SysexWidget attached to this instance of the SysexSender moves, the generate method of the SysexSender gets called with the value to send. The generate function simply puts the value and channel into the message and returns it.
RolandTD6.RolandDT1Sender is an example which implements {@link core.SysexWidget.ISender} directly without extending {@link core.SysexSender}. It is little complexed to support generic Roland DT1 (Data Transfer) message.
Often a Single Editor will have one or more Param Model and/or Sender. Sometimes more than one is used because a synth may use more than one method to transfer the data.
Note how the Param Models and Senders are used in the various editors. A SysexWidget doesn't care if you use the default Param Model or if you implement your own. This makes the SysexWidgets much more extensible. They can be used without needing to know how exactly the data is supposed to be accessed. The Param Model and Sender takes care of that.
This all probably sounds more complex than it really is, just take a look at the editors for various other synths, try changing some things maybe to see how they work. It shouldn't be too hard to figure out, but I'm not too good at explaining.
Note that the following code seems to work.
// Bad Example : does not work with jar algoIcon[0] = new ImageIcon("synthdrivers/YamahaTX81z/1.gif");But the code above does not work when the files are in a jar file. Instead use the following code.
// Good Example algoIcon[0] = new ImageIcon(getClass().getResource("1.gif"));
This is an attempt to do a test plan when writing a new driver for JSynthLib. It take little time to do and may help finding problem before the driver is available publicly.
First thing to do: you must do some single dump and some bank dump from your synth to your computer without using JSynthLib (I use Cakewalk to do that but there should be some simple freeware to do this). Save those dump in .syx format (binary). Remember, we are not using JSynthLib at his point in order to have something "clean" to refer to when testing the new driver.
One important note : It seems that some synth could have "bugs" in their factory patches. So if you resend those patch to the synth, they will be "corrected". To test this, you can simply resend the bank dump from your PC to the synth and do a new bank dump from synth to PC. If there are difference and the first bank dump and the second, this is probably the problem I mention. Normally, resending the second dump should be consistent after that. Any dump from synth to PC should be identical to the second dump since the synth has now "corrected" the original dump! I had this problem with some specific patch of my Nova after restoring original patches on the unit with the "Restore from ROM" command. I had spent some time figuring out the difference between dump so I'm warning you not to do the same mistake!
Now, redo the same thing (single dump and bank dump) but this time, change your device id number on the synth (sometime call global MIDI channel). Those new dump may have some byte different or not. In most case, single dump will be identical while bank dump may be different.
Note: I never write an editor but here are some suggestions:
Note: I suggest to move each fader at random because putting all of them to 0 or max is not a good idea since your editor may send knob info to the wrong place and you will not be able to detect it by comparing the .syx file!
Last step, send your new driver for integration in the next release of JSynthLib!
Yves Lefebvre
ivanohe@abacom.com
www.abacom.com/~ivanohe
The reason for a displayed tab width of 8 is compatibility with the following essential tools:
You need to install Subclipse, the SVN plugin for Eclipse. Follow the installation instructions at http://subclipse.tigris.org/install.html. The instructions are for Eclipse 3.0. If you have a newer version the process is slightly different:
(Note: These steps don't seem to be required using Eclipse 3.3)
Some selected documents:
Java Sound Resources is a good resource of Java Sound.
The Complete MIDI 1.0 Detailed Specification isn't
downloadable from the MMA Web site.
But you'll find a MIDI specification for example at