November 17, 2017

RK-002 MIDI Processing? Just DUY it!

Up until now all firmware flavours of the RK-002 are custom built but we’ve received numerous requests for solving specific MIDI issues for sometimes pretty exotic devices…

To have a more generic (but powerful!) platform on the RK-002 without people drowning in the available options we decided to make a bootloader for the RK-002 which enables development on the free and widely supported Arduino platform

With the new /DUY firmware and our Arduino RK-002 Board extension you can use your MIDI interface to program the RK-002 with any MIDI processing software you would need.

And if you’re not that savy on programming: There’s also the RK-002 DUY Exchange Portal where you can share or download firmware sketches, for yourself or from others.

NOTE: Using the DUY firmware gives you a great amount of flexibility on the RK-002 cable but it needs a certain level of programming knowledge!

With a few simple steps you can started writing Arduino sketches for your cable.

Step 1 : download & install Arduino IDE

This is described on the Arduino site: http://www.arduino.cc

Step 2 : add RK002 Retrokits package to Arduino IDE

Open preferences and enter in the field ‘Additional Boards Manager URLs’ the URL of the Retrokits RK002 support package:

https://www.retrokits.com/rk002/arduino/package_retrokits_index.json

note that the RK002 Retrokits package is supported in Windows, MacOSX and Linux !

Step 3 : select ‘Retrokits RK002’ board

After adding our board support package in the Arduino IDE settings you can go to the Arduino ‘Board manager’ (under Tools–>Boards) and search for ‘RK002’.

Install the files via this menu and the RK-002 and after closing this, the RK-002 will be selectable from the Boards menu.
You might need to update your Java environment with these link:
java.com/download/ and/or oracle.com/technetwork/java/

Download the JDK and you are ready to write custom sketches for the RK-002!

Step 4 : start coding your custom RK002 Arduino sketch

Below is an ‘Hello World’ example of an Arduino sketch for the RK002 cable to get you started.

#include <RK002.h>

boolean RK002_onNoteOn(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOn(channel, key+7, velocity);
  return true;
}

boolean RK002_onNoteOff(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOff(channel, key+7, velocity);
  return true;
}

void setup() 
{
  RK002_setup();
}

void loop()
{
  RK002_loop();
}

PS,Goodie: we developed a quicklook plugin for OSX which allows you to quickly browse through .ino codes used by our DUY and Arduino

Step 5 : compile & upload

Arduino has to be connected via USB or serial adapter to be programmed, the RK-002 programming works via the MIDI interface.

Connect the RK002 cable to your computer’s MIDI interface input and output (black end to MIDI-out, orange end to MIDI-in), and press the ‘upload’ button inside the Arduino IDE to upload the sketch. This will start the RK002 loader tool. The first time the loader tool starts, it’s necessary to select correct MIDI INPUT and MIDI OUTPUT ports. After the ports have been selected, the upload process should start automatically.

The settings will be stored, so next time when you press ‘upload’ the upload will automatically start. The RK002 loader window can be kept open, and can be used as a general purpose MIDI monitor.

The uploader tool requires Java to be installed on your platform; www.oracle.com/technetwork/java/javase/downloads

happy coding!

[nextpage title=”Writing sketches”]
The RK002 is not an Arduino board in the traditional sense (for example: it doesn’t have LEDs or GPIO etc.). Basically we’ve implemented the relevant parts of the Arduino platform in the RK002 support package:

  • Time
  • Math
  • Trigonometry
  • Characters
  • Random Numbers
  • Bits and Bytes
  • Communication (serial)
  • EEPROM

but not: (makes no sense on the RK002)

  • Digital I/O
  • Analog I/O
  • Zero, Due or MKR Family
  • External Interrupts
  • Interrupts
  • USB

more information about these functions can be found here: https://www.arduino.cc/reference

The only possible communication with the outside world is through the MIDI input and MIDI output ports. MIDI communication is realized by means of Arduino’s serial port. In principle the RK002 support package brings you a naked Arduino-ready platform, on which you can implement user-defined sketches.

In order to actually implement useful MIDI sketches it’s needed to process MIDI input, and to generate valid MIDI output. On the RK002 cable you have 2 choices for developing your Arduino sketch:

  1. using the standard Arduino MIDI library
  2. using our RK002 support library

Writing RK002 sketches using the standard Arduino MIDI library

RK002’s serial port is directly compatible with the standard MIDI library, so all sketches using the library should work without the need to adapt. Drawback of using this library is that is could be a bit hard to learn, and the functionality of this library is not directly tailored for the typical usage inside a RK002 cable. For more information see: https://playground.arduino.cc/Main/MIDILibrary

Writing RK002 sketches using the Retrokits RK002 library

Using the general purpose MIDI library can be a bit cumbersome, because the functionality is not explicitly tailored for the typical RK002 use-case.
Therefore we’ve implemented the Retrokits RK002 library which is specifically suited for writing simple MIDI processors.

The most simple RK002 Arduino sketch is listed below:

#include <RK002.h>

void setup() 
{
  RK002_setup();  
}

// the main loop
void loop()
{
   RK002_loop();
}

The sketch basically consists of the most trivial Arduino sketch with calling the RK002 library’s setup and loop functions. This will result in-effect a MIDI-thru cable 🙂 = all MIDI data arriving at the input will be put through to the output, and no processing is done.

In order to process the MIDI stream it’s necessary to implement callback functions. These functions have to be named using the specific reserved RK002’s names:

#include <RK002.h>

boolean RK002_onNoteOn(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOn(channel, key+7, velocity);
  return true;
}

boolean RK002_onNoteOff(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOff(channel, key+7, velocity);
  return true;
}

void setup() 
{
  RK002_setup();
}

void loop()
{
  RK002_loop();
}

In the above example:

  • two callback functions (for MIDI note-on and note-off messages) are defined.
  • an example of MIDI processing is implemented here: every note-message (note on and note off) will be preceded with a note message where it’s key is transposed by 7 notes; this should give a nice half-octave doubler effect.
  • both callback functions return ‘true’ to indicate that the original received message should be sent-thru,

[nextpage title=”Declaring sketch identification”]
In order to be able to identify a sketch once it is loaded into the cable several fields of information can be embedded into the generated sketch code. For this purpose the following macro must be used:

RK002_DECLARE_INFO(app_name,app_author,app_version,app_guid)

As can be seen from the above definition, the macro takes 4 parameters:

  1. app_name = short text-string identifying the app (for example: “TESTAPP”)
  2. app_author = text-string identifying the author (for example “Retrokits, ([email protected])”)
  3. app_version = text-string identifying the version of the app (for example “1.0.0”)
  4. app_guid = text-string containing a globally unique identifier which can be used to uniquely identifying this app
    (generate any GUID form here: https://www.guidgenerator.com)

The RK002 loader program is able to fetch & display this information from a running sketch.

example:

#include <RK002.h>

RK002_DECLARE_INFO("NOTE DOUBLER","[email protected]","1.0","7b47d3dd-991a-4197-ac02-d340cdabfb59")

boolean RK002_onNoteOn(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOn(channel, key+7, velocity);
  return true;
}

boolean RK002_onNoteOff(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOff(channel, key+7, velocity);
  return true;
}

void setup() 
{
  RK002_setup();
}

void loop()
{
  RK002_loop();
}

[nextpage title=”Declaring sketch parameters”]
The definition of ‘parameters’ makes it possible to parameterize the sketch. These parameters can be stored either in RAM only, or in (emulated) EEPROM.

In order to declare parameters to following macro must be used:

RK002_DECLARE_PARAM(name,flags,min,max,def)

As can be seen from the above definition, the macro takes 5 parameters:

  1. name = the name of the parameter
  2. flags = flags for the creation of the parameter: currently only the following values are supported: (0=default, 1=store in EEPROM)
  3. min = minimal value of the parameter
  4. max = maximal value of the parameter
  5. def = default value of the parameter

Our example sketch can be extended by using a parameter definition.

#include <RK002.h>

RK002_DECLARE_PARAM(TRANSPOSE,1,-12,12,0);

boolean RK002_onNoteOn(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOn(channel, key + RK002_paramGet(TRANSPOSE), velocity);
  return true;
}

boolean RK002_onNoteOff(byte channel, byte key, byte velocity)
{
  RK002_sendNoteOff(channel, key + RK002_paramGet(TRANSPOSE), velocity);
  return true;
}

void setup() 
{
  RK002_setup();
}

void loop()
{
  RK002_loop();
}

There is a lot of under-water-magic going on when defining parameters this way. During the compilation of the sketch all occurrences of the RK002_DECLARE_PARAM macro will be replaced by internally used code.

  • Every occurrence of the RK002_DECLARE_PARAM macro will create 1 parameter.
  • A parameter can internally hold a 16-bit value, either signed or unsigned.
  • Per default a parameter is treated internally as a unsigned value. When the minimal value of a parameter is below zero the parameter will be treated as signed.
  • In total there can be defined max. 32 parameters.
  • Parameters can be inquired by MIDI sysex : the RK002 loader uses this to display the parameter names and ranges.
  • Parameters can be read & written by MIDI sysex : the RK002 library will handle this transparently : the sketch can be notified when a parameter has been written by means of callback function
  • Parameters can be read & written by the sketch itself : the outside world will be notified when a parameter has been written by means of MIDI sysex.
  • Parameters can be stored in (emulated) EEPROM so that they will be available even when the cable is not powered.
  • Parameters can be referenced by their name or index inside the sketch.
  • Parameters have to be referenced by their index when accessing by MIDI.

The sketch can access parameters by means of the following 2 functions:

void RK002_paramSet(unsigned param_nr, int val)

This will set the parameter to the requested value. The value will be bound-checked, possible call-back functions will be called, and a MIDI sysex message will be generated to indicate parameter change.

Note that the parameter value is defined as an “int” On the Arduino platform an “int” is acutally a 32-bit value. Inside the RK002 library however a parameter is a 16-bit value (signed or unsigned) This means that the actual range of a parameter is -32768..32767 or 0..65535 depending on the min argument during declaration (!)

int RK002_paramGet(unsigned param_nr)

This will fetch a parameter value.

Whenever a paramer is written by MIDI (sysex) the RK002 library will handle the actual communication. In order to be notified of a parameter change the sketch can implement the following callback:

void RK002_onParamSet(unsigned param_nr, int param_val)

this callback will be called on every parameter write, regardless whether the param_val has changed or not.

and / or

void RK002_onParamChange(unsigned param_nr, int param_val)

this callback will be called only when the parameter_val really changes.
[nextpage title=”Debugging”]
There is not much debugging-love going on inside the Arduino IDE. The idea is that a developer writes a sketch and just tests it directly on the target. Sometimes it can be very handy to have some more information abou twhat is going on inside your sketch. For this purpose the following function is implemented.

void RK002_printf(const char *fmt, ...)

This function will send the formatted string encoded as a MIDI sysex packet. The RK002 loader will catch this message and display it as a regular console string.

an example:

#include <RK002.h>

void setup()
{
  RK002_setup();
}

void loop()
{
  RK002_loop();
}

boolean RK002_onNoteOn(byte chn, byte key, byte vel)
{
  RK002_printf("received note-on message (chn=%d, key=%d, vel=%d)",chn,key,vel);
  return true;
}

This will cause a textstring to be sent-out by MIDI whenever a MIDI note-on command arrives at the input. The RK002 loader will display these messages.
[nextpage title=”Summary”]

  1. call RK002_setup() and RK002_loop() from your setup() and loop() respectively
  2. for reception of messages: implement callback functions
  3. for transmission of messages: call the send functions
  4. declare sketch id by means of RK002_DECLARE_INFO
  5. declare parameters by menas of RK002_DECLARE_PARAM

In the next part we include some more examples.

[nextpage title=”Examples”]

Tempo Clock intern/extern/tap

In this example operation of the clock generator is shown. The sketch sets the internal tempo to 150.0 bpm, and displays the measured tempo on each beat (=24 clocks).

#include <RK002.h>

int beatctr = 0;

// clock tick handler: this is called 24x per quarter note: 
// either from the internal clock generator
//   or 
// when a MIDI clock message (=0xF8) is received 
bool RK002_onClock()
{
  beatctr = (beatctr + 1) % 24;
  if (beatctr == 0)
  {
    RK002_printf("tempo=%d (%s)",RK002_clockGetTempo(),RK002_clockIsExtern()?"EXTERNAL":"INTERNAL");
  }

  return true;
}

#define TAPMIDICHN 5

bool RK002_onNoteOn(byte channel, byte note, byte velocity)
{
  // receive any note-on on the TAP channel: tap the internal clock generator
  if (channel == (TAPMIDICHN-1))
  {
    RK002_clockTap();    
  }
}

// initialize
void setup()
{
  RK002_setup();

  // set internal clock generator tempo to 150.0 bpm:
  RK002_clockSetTempo(1500);
}

// the main loop
void loop()
{
  RK002_loop();
}

Novation Circuit

In this example the cable can be used to play the drum channels on the Novation Circuit.

#include <RK002.h>

#define APP_NAME "CIRCUIT example"
#define APP_AUTHOR "www.retrokits.com"
#define APP_VERSION "0.0.1"
#define APP_GUID "15fead90-5085-4ca0-b7a4-a93fe6c95686"

RK002_DECLARE_INFO(APP_NAME,APP_AUTHOR,APP_VERSION,APP_GUID);
RK002_DECLARE_PARAM(ENABLECHROMATIC,1,0,4,0)

// CC values for changing pitch
byte pitchtable[]={
  0,1,2,4,5,7,9,10,12,14,17,19,21,23,26,29,
  32,35,39,42,46,50,55,59,64,69,74,80,86,93,99,107,115,122,127
};

//CC numbers for drum pitch
byte drumPitchCC[] ={0,14,46,34,55};

// note numbers mapping from lowest control octave
byte controlKeys[] ={0,60,64,62,65};

//handler triggered on note on message:
bool RK002_onNoteOn(byte channel, byte note, byte velocity)
{
    byte enableChromaticParam = RK002_paramGet(ENABLECHROMATIC);

    if (channel == 1)
    {
        // switch to lowest Circuit octave to control replay:
        note = min(note, 40); // pitch table is limited to three octaves (+ 5 control notes)
        if (note > 4)
        {
            // pressed note not lowest control octave:
            if (controlKeys[enableChromaticParam] != 0)
            {
                RK002_sendControlChange(10,drumPitchCC[enableChromaticParam], pitchtable[note - 5]);
                RK002_sendNoteOn(10,controlKeys[enableChromaticParam], velocity);
            }
        }
        else
        {
            //  we are in control octave, set control index pointer
            RK002_paramSet(ENABLECHROMATIC, note);
        }
    }

    return true;

}

bool RK002_onNoteOff(byte channel, byte note, byte velocity)
{
    byte enableChromaticParam = RK002_paramGet(ENABLECHROMATIC);

    if (channel == 1)
    {
        if (note > 4)
        {
            if (controlKeys[enableChromaticParam] != 0)
            {
                RK002_sendNoteOff(10,controlKeys[enableChromaticParam], velocity);
            }
        }
    }

    return true;
}
// disable clock for loopback cable mode
bool RK002_onClock()
{
    return false;
}
bool RK002_onStart()
{
 
    return false;
}
bool RK002_onContinue()
{
 
    return false;
}

bool RK002_onStop()
{
 
    return false;
}

// initialize
void setup()
{
    RK002_setup();
}

// the main loop
void loop()
{
    RK002_loop();
}

[nextpage title=”Library reference”]

Functions for sending MIDI data

void RK002_sendNoteOff(byte channel, byte key, byte velocity)

MIDI note-off event.
channel = MIDI channel number 0..15
key = MIDI key number 0..127
velocity = MIDI velocity 0..127

void RK002_sendNoteOn(byte channel, byte key, byte velocity)

Sends a MIDI note-on event.
channel = MIDI channel number 0..15
key = MIDI key number 0..127
velocity = MIDI velocity 0..127

void RK002_sendPolyPressure(byte channel, byte key, byte val)

Sends a MIDI polyphonic keypressure event.
channel = MIDI channel number 0..15
key = MIDI key number 0..127
val = key-pressure value 0..127

void RK002_sendControlChange(byte channel, byte nr, byte val)

Sends a MIDI control change.
channel = MIDI channel number 0..15
nr = MIDI controller number 0..127
val = controller value 0..127

void RK002_sendProgramChange(byte channel, byte nr)

Sends a MIDI program change.
channel = MIDI channel number 0..15
nr = program change number 0..127

void RK002_sendChannelPressure(byte channel, byte val)

Sends a MIDI channel pressure (=’aftertouch’).
channel = MIDI channel number 0..15
val = pressure value 0..127

void RK002_sendPitchBend(byte channel, int position)

Sends a MIDI channel pressure (=’aftertouch’).
channel = MIDI channel number 0..15
position = pitchbend position -8192 .. 8191

void RK002_sendSystemExclusive(unsigned n, const byte *data)

Sends a MIDI system exclusive message.
n = number of bytes to send
data = data to send

void RK002_sendTimeCodeQuarterFrame(byte mtc)

Sends a MIDI timecode message.
mtc = timecode

void RK002_sendSongSelect(byte songsel)

Sends a MIDI song select message.
songsel = selected song

void RK002_sendSongPosition(unsigned songpos)

Sends a MIDI song position message.
songpos = song position

RK002_sendTuneRequest()

Sends MIDI tune request message.

RK002_sendClock()

Sends MIDI clock message.

RK002_sendStart()

Sends MIDI start message.

RK002_sendContinue()

Sends MIDI continue message.

RK002_sendStop()

Sends MIDI stop message.

RK002_sendActiveSensing()

Sends MIDI active sensing message.

RK002_sendReset()

Sends MIDI active sensing message.

Call-back functions for receiving MIDI data

boolean RK002_onNoteOff(byte channel, byte key, byte velocity)

Callback function which is called when a MIDI note off message is received.
channel=MIDI channel 0..15
key=key number 0..127
velocity=velocity 0..127
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onNoteOn(byte channel, byte key, byte velocity)

Callback function which is called when a MIDI note on message is received.
channel=MIDI channel 0..15
key=key number 0..127
velocity=velocity 0..127
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onPolyPressure(byte channel, byte key, byte value)

Callback function which is called when a MIDI poly-pressure message is received.
channel=MIDI channel 0..15
key=key number 0..127
value=pressure value 0..127
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onControlChange(byte channel, byte nr, byte value)

Callback function which is called when a MIDI control change message is received.
channel=MIDI channel 0..15
nr=controller number 0..127
value=pressure value 0..127
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onProgramChange(byte channel, byte nr)

Callback function which is called when a MIDI program change message is received.
channel=MIDI channel 0..15
nr=program change value 0..127
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onChannelPressure(byte channel, byte val)

Callback function which is called when a MIDI program channel pressure (=’aftertouch’) message is received.
channel=MIDI channel 0..15
nr=program change value 0..127
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onPitchBend(byte channel, int val)

Callback function which is called when a MIDI pitchbend message is received.
channel=MIDI channel 0..15
val=value -8192..8191
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onSystemExclusive(unsigned n, const byte *data)

Callback function which is called when a MIDI sysex message is received.
n=number of bytes (inclusive F0 and F7)
data=sysex data (inclusive F0 and F7)
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onTimeCodeQuarterFrame(byte mtc)

Callback function which is called when a MIDI timecode message is received.
mtc=midi timecode
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onSongSelect(byte songsel)

Callback function which is called when a MIDI songselect message is received.
songsel=selected song
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onSongPosition(unsigned pos)

Callback function which is called when a MIDI song position message is received.
pos=song position
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onTuneRequest()

Callback function which is called when a MIDI tune request message is received.
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onClock()

Callback function which is called when a MIDI clock message is received OR a tick from the internal MIDI clock generator has passed.
should return ‘true’ to indicate that the MIDI clock message should be sent.

boolean RK002_onStart()

Callback function which is called when a MIDI start message is received.
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onContinue()

Callback function which is called when a MIDI continue message is received.
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onStop()

Callback function which is called when a MIDI stop message is received.
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onActiveSensing()

Callback function which is called when a MIDI active-sensing message is received.
should return ‘true’ to indicate that the original message must be sent thru

boolean RK002_onReset()

Callback function which is called when a MIDI reset message is received.
should return ‘true’ to indicate that the original message must be sent thru

Function & callbacks for clock generator

void RK002_clockSetTempo(unsigned int bpm_x10)

Set internal clock generator tempo.
bpm_x10=beats per minute x 10 (so 120.0 should be passed as 1200)

unsigned int RK002_clockGetTempo()

Get the current clock tempo (either internal, or measured from external).
returns beats per minute x 10

void RK002_clockSetMode(unsigned int mode)

Set the clock generator mode:
mode=0 : automatic
mode=1 : internal only
mode=2 : external only

boolean RK002_clockIsExtern()

Check whether the clockgenerator is running externally synced.
returns TRUE : clock is external
returns FALSE: clock is generated internally

void RK002_clockTap()

Tap the clock generator (makes sense only when internally clocked).
This can be used to implement a tap-tempo mechanism.

void RK002_onHeartBeat()

This function is called every “heartbeat”. The heartbeat timer is a fixed-rate timer running at 10ms interval (so the callback function is called at 100Hz). This callback should be used to implement time-based delays (in stead of the delay() function)

#define RK002_HEARTBEAT_IN_MS

This symbol can be used to implement real-time intervals. The value of this symbol contains the number of milliseconds of 1 heartbeat (currently 10 ms).

For example:

int longtimer = 0;

void RK002_onHeartBeat()
{
  // is timer running ?
  if (longtimer != 0)
  {
    // count 1 timer tick:
    longtimer--;

    // is timer timed-out?
    if (longtimer == 0)
    {
      // .. do something useful here:
      // .....
    }
  }
}

void RK002_ControlChange(byte chn, byte nr)
{
  if ((chn == 5) && (nr == 55))
  {
    // start the 1-shot timer:
    // note: in this example we time 4 seconds = 4000ms, 
    // so we have to count '4000 / RK002_HEARTBEAT_IN_MS' callbacks
    longtimer = 4000 / RK002_HEARTBEAT_IN_MS;
  }
}


<h1>Functions &amp; callbacks for parameters</h1>

void RK002_paramSet(unsigned param_nr, int val)

Set parameter value.
param_nr=parameter number
val=parameter value (will be bound-checked)

int RK002_paramGet(unsigned param_nr)

Get parameter value.
param_nr=parameter number
returns parameter value (bound-checked)

void RK002_onParamSet(unsigned param_nr, int val)

Callback function which is called whenever a parameter is set (regardless of value).
param_nr=parameter number
val=parameter value

void RK002_onParamChange(unsigned param_nr, int val)

Callback function which is called only when a parameter is changed.
param_nr=parameter number
val=parameter value

Functions for debugging

RK002_printf(const char *fmt, ...)

Sends debug text message through MIDI sysex.

Retrokits, P.O.Box 36334 | 1050MH Amsterdam, the Netherlands - | Phone:+31 6 54 278 478 | email us here