Tuesday, March 8, 2022

KASTLE 1.5 + DRUM KASTLE




Bastl KASTLE 1.5 and DRUM KASTLE are really fun little gadget synths. I've found that by plugging them together I could make something that sounded much more decent than the sum of the parts.



Basically, it boils down to modulation sources. Each device has a stepped random voltage generator and an LFO/clock. The rates are tied through a rate control knob. I've found that multiple modulation sources with independent rates is key to making pleasant random sounds and beats.
So by sharing the two sets of modulation sources between the two units, basically one source can be used to set the tempo of a beat/rhythm, with the other source working at a much lower frequency to change the timbre, key, and energy level of the sound to reduce monotony.
The patch is as follows:
=====================================
KASTLE + KASTLE DRUM PATCH
DRUM KASTLE
DRUM MOD 1:00
PITCH MOD 10:00
TEMPO MOD MIN
DECAY 12:00
DRUM MIN
PITCH MIN
CLK 12:00


DRUMS L -> I/O L
NOISES -> I/O R
CLK -> TRIG IN
PATTERN -> DRUM MOD
PATTERN -> KASTLE TIMBRE MOD
PATTERN -> KASTLE PITCH MOD


KASTLE
PITCH MOD 9:00
TIMBRE MOD 10:00
RATE MOD MIN
WAVESHAPE 12:00
OSC PITCH MIN
OSC TIMBRE 9:00
LFO 10:00


WAVESHAPE -> LFO TRIANGLE
BIT IN -> STEPPED
STEPPED -> PITCH MOD
STEPPED -> DRUM KASTLE PITCH MOD
STEPPED -> DRUM KASTLE DECAY
LFO SAW -> WAVESHAPE
LFO SAW -> BIT IN
LFO SAW -> DRUM KASTLE FEED

So basically - the DRUM KASTLE is used as the tempo clock for both units. The clock from the DK is connected to TRIG IN, triggering the drum envelope. 
The DK's STEPPED output similarly is connected to the DRUM MOD line in on the DK, changing timbre and pitch of the drum sound on every step.  This output also goes over to the KASTLE, changing pitch and timbre on every tick. The STEPPED output on the KASTLE units is an 8 step random sequence that loops until the FEED/BIT IN lines are driven high, in which case a new set of values are loaded.
The KASTLE, on the other hand, is set to a fairly low LFO/clock frequency (several multiples of the 8 steps of the DK STEPPED output) and makes more macroscopic and undulating changes to the pitch and timbre of both units.
So - the SAW output of the KASTLE LFO slowly modulates the WAVESHAPE of the KASTLE, but also resets both units STEPPED sequencers. Similarly, the very slow rate of the STEPPED output modulates the faster STEPPED output of the DK to effectively transpose the faster STEPPED output of the KASTLE.

Sunday, February 10, 2019

BCR-2000 and Roland D-50 / D-05

Created a BC Manager-friendly BCR-2000 script to control a Roland D-50 / Roland D-05. Everything appears to be supported here (except MIDI and name entry).

NOTE: BECAUSE THIS IS ALL CUSTOM SYSEX - THE D-50/D-05 NEEDS TO BE SET TO CHANNEL 16. This is hardcoded for now, sorry.

Basic structure is:

P-1: Patch Control
P-2: Upper Common
P-3: Upper Partial 1 - Waveform and TVA
P-4: Upper Partial 1 - TVF
P-5: Upper Partial 2 - Waveform and TVA
P-6: Upper Partial 2 - TVF
P-7: Lower Common
P-8: Lower Partial 1 - Waveform and TVA
P-9: Lower Partial 1 - TVF
P-10:Lower Partial 2 - Waveform and TVA

P-11:Lower Partial 2 - TVF

Source code is at the bottom of this post.

Functions are grouped wherever possible (especially the envelopes which are all on the bottom row). This enables simplification of labeling since there are essentially 4 different functions per knob.

A few things could be improved (when there are 4 possible values per knob it requires a fair amount of twisting, some values should be not just the "one-dot" mode, etc.

Printable google doc of the 4 legend strips is here and code is available here

Friday, January 8, 2016

Sony Walkman and Playlists

Sony Walkman Playlists

Part 1 - Internal Memory

In order to get playlists to work on my Walkman, I need to put them in the MUSIC folder, and they need to contain RELATIVE path names. For example, assuming I have a file on the Walkman called (absolute path)

\MUSIC\rnb\Stevie Wonder\Talking Book\07 Big Brother.mp3

Then I have a working .m3u playlist file called (absolute path)

\MUSIC\Stevie.m3u

with the contents:

#EXTM3U
#EXTINF:213, Talking Book - 7 - Big Brother
rnb\Stevie Wonder\Talking Book\07 Big Brother.mp3

 Putting the playlist in another folder does NOT work, even if it still is relative to the MUSIC directory.

Part 2 - External Memory

(probably the same thing, haven't tested fully yet)

Thursday, December 31, 2015

Arduino X Anelace "Powers of 2" Clock

Another fun Arduino hack:

The Anelace "Powers of 2" clock is a favorite of mine. It displays code in a BCD (binary coded decimal) format.

  0   0   0 
  0 0 0 0 0 
0 0 0 0 0 0 
0 0 0 0 0 0 
H H M M S S

  0   *   0 
  0 * 0 0 0 
0 * 0 0 * 0 
* 0 * * * 0 
1 2:5 9:3 0 = 12:59 and 30 seconds


Using BCD format makes the clock easy to read, which is quite important. However, I personally am not a fan of the inefficiency of BCD. Plus, any bit-banger worth their salt should be able to read binary numbers at least up to 2^8, right?

Note that the total information conveyable by the clock is 
2^(number of leds) = 2^20 = 1,048,576 possible values.

In the specific BCD format used, the total values (going from the left-most column to the right-most column) are
4*10*8*10*8*10 = 256,000 possible values.

Now, there are (24*60*60) = 86,400 seconds in a day. So (obviously) the 24 hour time can be perfectly represented within this scheme.

However, if we go to true binary, then we know that we can represent seconds and minutes by 6 LED's (2^6=64) and the 24 hour time by 5 LED's (2^5 = 32). Conveniently enough we can use columns instead of rows and get

  0   0   0 
  0 0 0 0 0 H
0 0 0 0 0 0 M
0 0 0 0 0 0 S

so our 12:59:30 time becomes

  0   0   0 
  0 * * 0 0 12
* * * 0 * * 59
0 * * * * 0 30 = 12:59 and 30 seconds

Note that the top row isn't being used at all! Extra-conveniently: there are 7 days in a week, and there are 2^3=8 total possible values for the top row. So we can do days of the week in the top row! There are 604,800 seconds in a week, and there are 1,048,576 total possible variations of LED lighting, so we're very efficiently using the LED's now.

The Arduino sketch below uses that mode as a default. There are a few other modes though, including a random mode and a grey code mode.


// Arduino code for "Powers of 2" clock by Anelace.
// Remove microcontroller, wire lines from microcontroller pads to Arduino


/*
2 - col 1
3 - col 2
4 - col 3
5 - col 4
6 - col 5
7 - 1st row
8 - 2nd row
9 - 3rd row
10 - 4th row
11 - ?

TODO: HANDLE WRAP
*/
#define CLOCK_STATE_RUNNING (0)
#define CLOCK_STATE_SET_MINUTES (1)
#define CLOCK_STATE_SET_HOURS (2)
#define CLOCK_STATE_SET_DAYS (3)
#define CLOCK_STATE_SET_MODE (4)
#define CLOCK_NUM_STATES (5)

#define CLOCK_MODE_BINARY (0)
#define CLOCK_MODE_BCD (1)
#define CLOCK_MODE_GRAY (2)
#define CLOCK_MODE_SYMMETRIC_GRAY (3)
#define CLOCK_MODE_RANDOM (4)
#define CLOCK_NUM_MODES (5)

#define TIME_SECONDS (3)
#define TIME_MINUTES (2)
#define TIME_HOURS (1)
#define TIME_DAYS (0)
#define TIME_NUM_PARTS (4)

#define NUM_SECONDS (60)
#define NUM_MINUTES (60)
#define NUM_HOURS (24)
#define NUM_DAYS (7)

int clock_mode = CLOCK_MODE_SYMMETRIC_GRAY;
int col0 = 2;
int numcols = 5;
int row0 = 7;
int numrows = 4;
int switch0 = 12;
int last_switch0 = 0;
int last_switch1 = 0;
unsigned short time[TIME_NUM_PARTS] = {1,18,30,00}; // 3 = seconds... 0 = days
int clock_state = CLOCK_STATE_RUNNING;
unsigned long last_seconds_from_millis = 0;

#define BRIGHTNESS_DELAY_NUM (3)
unsigned int brightness_delay_index = 2;
unsigned int brightness_delay[BRIGHTNESS_DELAY_NUM] = {10,1,500};

void update_time() {
  unsigned long seconds_from_millis = millis()/1000;
  time[TIME_SECONDS] += (seconds_from_millis - last_seconds_from_millis);
  last_seconds_from_millis = seconds_from_millis;
  if (time[TIME_SECONDS] >= NUM_SECONDS) {
    time[TIME_MINUTES]++; // assume we didn't miss a whole minute w/o update
    time[TIME_SECONDS] = 0;
    if (time[TIME_MINUTES] >= NUM_MINUTES) {
      time[TIME_HOURS]++;
      time[TIME_MINUTES] = 0;
      if (time[TIME_HOURS] >= NUM_HOURS) {
        time[TIME_DAYS]++;
        time[TIME_HOURS] = 0; 
        if (time[TIME_DAYS] >= NUM_DAYS) {
          time[TIME_DAYS] = 0;
        }
      }
    }
  }  
}
int led_display_random(int row, int col) {
  return rand()&0x1;
}

int led_display_bcd(int row, int col) {
  if (row == 0) {
    switch (col) {
      case 1: // most significant hours 10's place
        return (time[TIME_HOURS]/10)&(0x1<<1);
      case 3: // least significant hours 10's place    
        return (time[TIME_HOURS]/10)&(0x1);
      case 0: // most significant hours 1's place
        return (time[TIME_HOURS]%10)&(0x1<<3);
      case 2: // most significant minutes 1's place
        return (time[TIME_MINUTES]%10)&(0x1<<3);
      case 4: // most significant seconds 1's place
        return (time[TIME_SECONDS]%10)&(0x1<<3);
    }
  }
  switch (col) {
    case 4:
      return (time[TIME_SECONDS]%10)&(0x1<<numrows-1-row);
    case 3:
      return (time[TIME_SECONDS]/10)&(0x1<<numrows-1-row);
    case 2:
      return (time[TIME_MINUTES]%10)&(0x1<<numrows-1-row);
    case 1:
      return (time[TIME_MINUTES]/10)&(0x1<<numrows-1-row);
    case 0:
      return (time[TIME_HOURS]%10)&(0x1<<numrows-1-row);
  }
}

int led_display_binary(int row, int col) {
  if (row == 0) {
      // this row also contains seconds and minutes
      switch (col) {
        case 1:
        // most significant minutes bit
          return time[2]&(0x1<<5);
        case 3:
        // most significant seconds bit
          return time[3]&(0x1<<5);
        case 2:
          return time[0]&(0x1<<1);
        case 4:
          return time[0]&(0x1);
        case 0: // most significant days bit
          return time[0]&(0x1<<2);          
      }
  }
  return time[row]&(0x1<<(numcols-1-col));    
}

int led_display_gray(int row, int col) {
  if (row == 0) {
      // this row also contains seconds and minutes
      switch (col) {
        case 1:
        // most significant minutes bit
          return binaryToGray(time[2])&(0x1<<5);
        case 3:
        // most significant seconds bit
          return binaryToGray(time[3])&(0x1<<5);
        case 2:
          return binaryToGray(time[0])&(0x1<<1);
        case 4:
          return binaryToGray(time[0])&(0x1);
        case 0: // most significant days bit
          return binaryToGray(time[0])&(0x1<<2);          
      }
  }
  return binaryToGray(time[row])&(0x1<<(numcols-1-col));    
}

int led_display_symmetric_gray(int row, int col) {
  if (row == 0) {
      // this row also contains seconds and minutes
      switch (col) {
        case 1:
        // most significant minutes bit
          return binaryToSymmetricGray(time[2],30,6)&(0x1<<5);
        case 3:
        // most significant seconds bit
          return binaryToSymmetricGray(time[3],30,6)&(0x1<<5);
        case 2:
          return binaryToGray(time[0])&(0x1<<1);
        case 4:
          return binaryToGray(time[0])&(0x1);
        case 0: // most significant days bit
          return binaryToGray(time[0])&(0x1<<2);          
      }
  }
  switch (row) {
    case 0:
      return binaryToGray(time[row])&(0x1<<(numcols-1-col));    
    case 1:
      return binaryToSymmetricGray(time[row],12,5)&(0x1<<(numcols-1-col));    
    case 2:
      return binaryToSymmetricGray(time[row],30,6)&(0x1<<(numcols-1-col));    
    case 3:
      return binaryToSymmetricGray(time[row],30,6)&(0x1<<(numcols-1-col));    
  }
}

int led_display(int row, int col) {
  switch (clock_mode) {
    case CLOCK_MODE_BINARY:
      return led_display_binary(row,col);
    case CLOCK_MODE_BCD:
      return led_display_bcd(row,col);
    case CLOCK_MODE_GRAY:
      return led_display_gray(row,col);
    case CLOCK_MODE_RANDOM:
      return led_display_random(row,col);
    case CLOCK_MODE_SYMMETRIC_GRAY:
      return led_display_symmetric_gray(row,col);
  }
}

unsigned char clock_state_led_high(int row, int col) 
// check to see if the settings wants to blank a row
{
    if (clock_mode == CLOCK_MODE_BCD) return 1;
    
    switch (clock_state) {
      case CLOCK_STATE_RUNNING:
        return 1;
      case CLOCK_STATE_SET_MINUTES:
        if (row==2) return 1; else 
        if (row==0 && col==1) return 1; else
          return 0;
      case CLOCK_STATE_SET_HOURS:
        if (row==1) return 1; else return 0;
      case CLOCK_STATE_SET_DAYS:
        if (row==0){
         if (col == 2 || col ==0 || col == 4)
           return 1; 
        }
        return 0;
      default:
        return 1;
    }
}  

void setup() {
  int ii;
  for (ii=2;ii<=11;ii++) {
    pinMode(ii,OUTPUT);
  }
  Serial.begin(9600);  
}

void loop() {
  int ii,jj;
  int input;
  update_time();
  for (ii=0;ii<numcols;ii++) {
  digitalWrite(ii+col0,HIGH);
    for (jj=0;jj<numrows;jj++) {
      if (clock_state_led_high(jj,ii)) {
        if (led_display(jj,ii)) 
        {
          digitalWrite(jj+row0,HIGH);
          delayMicroseconds(brightness_delay[brightness_delay_index]);
          digitalWrite(jj+row0,LOW);
        } 
      }
    }
  digitalWrite(ii+col0,LOW);
  }
  
  input = !digitalRead(switch0);  // inverse polarity
  if (input && last_switch0 != 1) {
    clock_state = (clock_state+1)%CLOCK_NUM_STATES;
  }
  last_switch0 = input;
  
  input = !digitalRead(switch0+1);  // inverse polarity
  if (input && last_switch1 != 1) {
    switch (clock_state) {
      case CLOCK_STATE_RUNNING:
        brightness_delay_index = (brightness_delay_index+1)%BRIGHTNESS_DELAY_NUM;
        break;
      case CLOCK_STATE_SET_MINUTES:
        time[TIME_MINUTES] = (time[TIME_MINUTES] + 1)%NUM_MINUTES;
        break;
      case CLOCK_STATE_SET_HOURS:
        time[TIME_HOURS] = (time[TIME_HOURS]+1)%NUM_HOURS;
        break;
      case CLOCK_STATE_SET_DAYS:
        time[TIME_DAYS] = (time[TIME_DAYS]+1)%NUM_DAYS;
        break;
      case CLOCK_STATE_SET_MODE:
        clock_mode = (clock_mode+1)%CLOCK_NUM_MODES;
        break;       
    }
  }
  last_switch1 = input;
} // loop();

unsigned int binaryToSymmetricGray(unsigned int num, unsigned int num_max_div2,unsigned int num_bits){

  if (num < (num_max_div2)) {
    return (num >> 1) ^ num;
  } else {
    unsigned int output = ((((num_max_div2*2) - 1 - num) >> 1) ^ ((num_max_div2*2) - 1 - num));
    output |= (0x1<<(num_bits-1));
    return output;
  }
}

unsigned int binaryToGray(unsigned int num){
        return (num >> 1) ^ num;
}

unsigned int grayToBinary(unsigned int num){
    unsigned int mask;
    for (mask = num >> 1; mask != 0; mask = mask >> 1){
        num = num ^ mask;
    }
    return num;
}

Wednesday, November 21, 2012

MiniAK/Micron/ION + BCR2000 + Arduino

I made a quick Arduino program to convert the NRPN data output from a BCR2000 (or conceivably any MIDI controller that sends NRPN's) to the MiniAK / Micron / ION's rather non-standard format. It does two things:

1 - The MiniAK expects "signed" 14-bit NRPN values. The MIDI spec (and conceivably most other synths ever made) supports 14-bit unsigned values. Thus, there is no simple way to make a standard controller (even one with the cool functionality of the BCR2000) to easily handle faking out the 2's compliment negative numbers. So - the Arduino steps in and converts a normal, positive range to the MiniAK's signed range. For example, controller #226 (Patch cable 12 level) goes from -1000 to 1000 on the MiniAK. The BCR2000 is set in the range [0,2000]. Then, the Arduino converts the message to the [-1000,1000] range that the MiniAK requests.

2 - The MiniAK expects that every NRPN is 14-bit. However, when you use a toggle button on the BCR2000, it can only send NRPN MSB data. Thus, the toggle buttons can't be used as-is to control polyphony, waveform type, etc. For NRPN ranges less than 0-127 (0x7F), the Arduino converts MSB-only signals to the LSB (bit-shifts the 14-bit data value by 7 bits.

Recipe:

Ingredients:
-  a MiniAK/Micron/ION
- a BCR2000 (others should work fine)
- a PC
- two MIDI cables
- a USB A/B cable
- an Arduino (I used an R3, others should work fine)
- a MIDI Shield (I used the LinkSprite, others should work fine)

  1. Connect the Arduino to the MIDI Shield
  2. Turn the Serial port switch on the MIDI Shield to "OFF"
  3. Download and install the Arduino environment
  4. Connect the USB cable to the PC and the Arduino
  5. Open the Arduino environment.
  6. Copy and paste the code below into the sketchpad.
  7. Go to "File"/"Upload" to compile and upload the code.
  8. Turn the Serial port switch on the MIDI Shield to "ON"
  9. Connect one MIDI cable to the BCR2000 Midi OUT and the Arduino MIDI IN
  10. Connect one MIDI cable to the MiniAK Midi IN and the Arduino MIDI OUT
  11. Power up the BCR2000 and the MiniAK.
  12. Program some NRPN's on the BCR2000 and confirm functionality!

The code is pretty straightforward. There are a few add'l var's from another project I was working on but they do not interfere with the translation code. You should just be able to copy the following source code into your Arduino sketchpad, compile, and upload. I'm positive there are optimizations to be had and code to be cleaned up but it appears to be 100% functional from my testing to date. If I (or anyone else) encounters a bug I'll edit the code in the post.

[edit #1: need to support inc/dec functionality]


Copy below this line:

// MIDI Filter
// Andrew Eloff
// This program selectively processes MIDI input data and spits to the output
// The basic motivation is to have the AKAI MiniAK be controllable using
// ordinary control surfaces and software.

// How we're going to do it for the MINIAK/Micron:
// MIDI controller outputs the NRPN ranges all from 0..MAX
// Arduino converts from 0..MAX to MIN..MIN+MAX.
// remember, MINIAK/Micron believes that the NRPN value is SIGNED
// but it's really unsigned. so -100 to 100 is really
// 16284-16383 - 0-100
//


//http://ion-micron-miniak.wikia.com/wiki/Common_FAQ

/* From http://www.midi.org/techspecs/midimessages.php#3
To set or change the value of a Registered Parameter:

1. Send two Control Change messages using Control Numbers 101 (65H) and 100 (64H) to 
select the desired Registered Parameter Number, as per the following table.

2. To set the selected Registered Parameter to a specific value, send a Control Change 
messages to the Data Entry MSB controller (Control Number 6). If the selected Registered 
Parameter requires the LSB to be set, send another Control Change message to the Data Entry 
LSB controller (Control Number 38).

3. To make a relative adjustment to the selected Registered Parameter's current value, 
use the Data Increment or Data Decrement controllers (Control Numbers 96 and 97).

*/
//  0016F35A 1 -- B0 63 00 1 --- CC: NRPN MSB
//  0016F35A 1 -- B0 62 2F 1 --- CC: NRPN LSB
//  0016F35A 1 -- B0 06 00 1 --- CC: Data Entry MSB
//  0016F35A 1 -- B0 26 0C 1 --- CC: Data Entry LSB
// this is 4 messages to be sent via the serial port
// sends a value [00 0C] to address[00 2F]

typedef struct {
  unsigned int addr;
  int min_val;
  int max_val; 
} miniak_nrpn_struct;

extern miniak_nrpn_struct miniak_nrpns[];

void setup() {
  //  Set MIDI baud rate:
  Serial.begin(31250);
  note_init();
}

long transpose = 0;
long setting_state = 0; // STATE 0 = regular play STATE 1 = setting

unsigned long last_pitch_time = 0;

unsigned char message_state = 0;

#define MESSAGE_DATA_MAX (5)
unsigned char message_data[MESSAGE_DATA_MAX];
unsigned char message_size =0;

unsigned char nrpn_addr_msb = 0;
unsigned char nrpn_addr_lsb= 0;
unsigned long nrpn_addr = 0;

unsigned char nrpn_data_msb= 0;
unsigned char nrpn_data_lsb= 0;
unsigned long nrpn_data= 0;

unsigned char nrpn_data_out_msb= 0;
unsigned char nrpn_data_out_lsb= 0;
unsigned long nrpn_data_out= 0;

long nrpn_output_data_min;
long nrpn_output_data_max;

void make_bloop(int freq1, int freq2) {
  noteOn(freq1, 0x40, 0x1); 
  delay(50);        
  noteOn(freq1, 0x0, 0x1);           
  delay(50);        
  noteOn(freq2, 0x40, 0x1); 
  delay(50);        
  noteOn(freq2, 0x0, 0x1);           
  delay(50);       
#define RIBBON_MODE_CONTINUOUS (0)
#define RIBBON_MODE_NOTE (1)

unsigned char inchar;
int current_pitch_value = 0;
unsigned char current_fret_value = 0;
//int ribbon_mode = RIBBON_MODE_CONTINUOUS;
int ribbon_mode = RIBBON_MODE_NOTE;
// pressing "2" and "A" on the rock band keytar increments/decrements


int last_program_change = 0;
#define FRET_KEY_NOT_PRESSED ((unsigned char)0xFF)
#define NUM_KEYS (0x7F)
// last note state: in the future this should be a queue for polyphony.
// for now 
unsigned char last_fret[NUM_KEYS]; // last fret val for a given key
unsigned char last_velocity[NUM_KEYS]; // last key velocity for a given key
void note_init() {
  memset(last_fret,FRET_KEY_NOT_PRESSED, sizeof(last_fret));
  memset(last_velocity,0x00, sizeof(last_velocity));
}

long nrpn_convert_range_to_miniak(unsigned long addr_in, long data_in) {
/****************************************************************

****************************************************************/
// AWE TODO: create sparse matrix so we don't have to loop through
// the messages
  long ii;
  long data_out;
  unsigned long this_addr = 0;


  this_addr = 0;
  for (ii=0;this_addr != 999 && ii<1024 ;ii++) { // ii<1024 = while loop failsafe
    this_addr = miniak_nrpns[ii].addr;
    if (this_addr > addr_in) break; // assume nrpn's are in ascending order!!
    if (this_addr == 999) break; // we got to the end of the array
    if (this_addr != addr_in) continue;

    // we have a match! first, if the range is less than
    // 7 bits then we should assume the controller is sending
    // MSB only. This is most friendly to the BCR2000 and probably
    // other devices:
    if (miniak_nrpns[ii].max_val - miniak_nrpns[ii].min_val <= 0x7F) {
      data_in = data_in >> 7; // shift MSB val to LSB
    }

    // Assume that the NRPN range given from the controller
    // is the same as the Miniak's, just always in the range
    // [0..n], even though the output may be [-n/2..n/2]
    data_out = data_in + miniak_nrpns[ii].min_val;
    if (data_out > miniak_nrpns[ii].max_val)
      data_out = miniak_nrpns[ii].max_val;
    return data_out;
  }
  
  return data_in; // means there wasn't a conversion for the MINIAK NRPN
}

void nrpn_output_converted_data(unsigned char command_byte) {
// make sure to preserve channel via "command_byte"
//  0016F35A 1 -- B0 06 00 1 --- CC: Data Entry MSB
//  0016F35A 1 -- B0 26 0C 1 --- CC: Data Entry LSB
Serial.write(command_byte); // CC
Serial.write(0x06); // MSB
Serial.write(nrpn_data_out_msb);

Serial.write(command_byte); // CC
Serial.write(0x26); // LSB
Serial.write(nrpn_data_out_lsb);
}
void nrpn_convert_message_to_miniak(unsigned char msg_data[]){
/*********************************************************
** parse the message in from MIDI and then convert it
** to MINIAK-friendly messages (crazy "negative numbers")
** address data should just be passed thru
*********************************************************/
  int ii;
  
  switch (msg_data[1]) {
    case 0x63:
      // note: if addr has changed, be sure to zero out data!
      if (nrpn_addr_msb != msg_data[2]) {
        nrpn_data = nrpn_data_msb = nrpn_data_lsb = 0;
      }
      nrpn_addr_msb = msg_data[2];
      nrpn_addr = nrpn_addr&0x007F;
      nrpn_addr |= (msg_data[2]<<7);
      for (ii=0;ii<3;ii++) { // note magic number, cc always 3 bytes
        Serial.write(msg_data[ii]);
      }    
    break;

    case 0x62:
      if (nrpn_addr_lsb != msg_data[2]) {
        nrpn_data = nrpn_data_msb = nrpn_data_lsb = 0;
      }
      nrpn_addr_lsb = msg_data[2];
      nrpn_addr = nrpn_addr&0x3F80;
      nrpn_addr |= (msg_data[2]);
      for (ii=0;ii<3;ii++) { // note magic number, cc always 3 bytes
        Serial.write(msg_data[ii]);
      }    
      break;

    case 0x06:
      nrpn_data_msb = msg_data[2];
      nrpn_data &= 0x007F;
      nrpn_data |= (msg_data[2]<<7);
      nrpn_data_out = nrpn_convert_range_to_miniak(nrpn_addr,nrpn_data);
      nrpn_data_out_lsb = (nrpn_data_out&0x7F);
      nrpn_data_out_msb = (nrpn_data_out>>7)&0x7F;
      nrpn_output_converted_data(msg_data[0]);
break;

    case 0x26:
      nrpn_data_lsb = msg_data[2];
      nrpn_data &= 0x03F80;
      nrpn_data |= (msg_data[2]);
      nrpn_data_out = nrpn_convert_range_to_miniak(nrpn_addr,nrpn_data);
      nrpn_data_out_lsb = (nrpn_data_out&0x7F);
      nrpn_data_out_msb = (nrpn_data_out>>7)&0x7F;
      nrpn_output_converted_data(msg_data[0]);
    break;

  }// switch message_data[1]
}



int message_process (unsigned char *msg_data, int msg_size) {
  int ii;
  switch (msg_data[0]&0xF0) {
    case 0xB0: // control change - including NRPN!!
      nrpn_convert_message_to_miniak(msg_data); // note this outputs data too
      return 0;  
    break;
      
    case 0x80: // note off
      // now write out the data
      for (ii=0;ii<msg_size;ii++) {
        Serial.write(msg_data[ii]);
      }       
      return 0;
    break;
    
    case 0x90: // note on
      for (ii=0;ii<msg_size;ii++) {
        Serial.write(msg_data[ii]);
      }
     return 0 ;    
    break;

    case 0xF0: // sysex
    break;    
    default:
      for (ii=0;ii<msg_size;ii++) {
        Serial.write(msg_data[ii]);
      }
      return 0;
    break;
  }
}
int get_message_size(unsigned int opcode) {
/*********************************************
** return the expected message size given the
** input opcode - return 0 if sysex (passes thru)
*********************************************/
  
  switch (opcode&0xF0) { // mask off channel # for channel-specific opcodes
    case 0x80:
    case 0x90:
    case 0xA0: // polyphonic aftertouch
    case 0xB0: // cc
    case 0xE0: // pitch
      return 3;
    break;
     
    case 0xC0: // program change
    case 0xD0: // channel aftertouch
      return 2;
     break;
     
     case 0xF0: // Sysex - 
     {
       if ((opcode&0xF8) == 0xF8) {
         // system real-time message, 1 byte, includes 0xF8, 0xFE
         return 1;
       }
       switch (opcode) {
         case 0xF0: // sysex
           return 0; // pass-thru information
         break;

         case 0xF1:
         case 0xF3:
           return 2;
         break;

         default:
           return 0; // pass-thru information
         break;
       }// switch opcode if 0xF0
     }//case 0xF0
     break;

     default:
       return 0; // pass-thru information
      break;
  }
}


void loop() {

    // simple loopback - works
if (0) {    
    while(1) {
      if (Serial.available()) {
        Serial.write(Serial.read());
        delay(1);
      }
    } // while
} //if

    // read in data and advance state machine if necessary
    if (Serial.available()) {
      inchar = Serial.read();
      // test: loopback
      // Serial.write(inchar);

      // ignore (or optionally pass-thru) "real-time" messages
      // they are allowed to interrupt the state machine
      if ((inchar&(unsigned char)0xF8) == (unsigned char)0xF8) {
        // Serial.write(inchar);
        return;
      }
      
      // otherwise automatically reset the state machine
      // if we receive bit 15 set
      if (inchar&0x80) {
        message_state = 0;
      }
      
      if (message_state == 0) { // searching for opcode
          if (inchar&0x80) {
            message_data[0] = inchar;
            message_size = get_message_size(inchar); // replace with function
            message_state = 1;
            // special case: single-byte command
            if (message_size == 1) {
              message_process(message_data,message_size);
              message_state=0;
              return;
            } else if (message_size==0) { // got an undefined message, so pass thru
              // just spit out the raw data
              Serial.write(inchar);
              message_state=0;
              return;
            } else {
              message_state = 1;
              return;
            } // if message_size
          } else { // state 0, non-command means RUNNING STATE - use last state
            // advance the state, assume that message_data[0] contains
            // the previously-used state and advance accordingly
            message_size = get_message_size(message_data[0]);
            message_data[1] = inchar;
            if (message_size <= 2) {
              message_process(message_data,message_size);
              message_state = 0;
            } else if (message_size == 0) { // undefined message, just pass thru
              Serial.write(inchar);
              message_state = 0;
              return;
            } else {
              message_state = 2;
              return;
            }
          } // if inchar&0x80
      } else { // message_state doesn't equal 0
          message_data[message_state] = inchar;
          message_state++;
          if (message_size == message_state) {
            message_process(message_data, message_size);
            message_state = 0;
            return;
          }
      }// if message_state
  }
 // just echo stuff - in to out
  
}// loop()

//  plays a MIDI note.  Doesn't check to see that
//  cmd is greater than 127, or that data values are  less than 127:
void noteOn( int pitch, int velocity, int channel) {
  if (channel <1) channel = 1; // make a warning!!
  Serial.write(0x90|((channel-1)&0xF));
  Serial.write(pitch);
  Serial.write(velocity);
}
void noteOff( int pitch, int velocity, int channel) {
  if (channel <1) channel = 1; // make a warning!!
  Serial.write(0x80|((channel-1)&0xF));
  Serial.write(pitch);
  Serial.write(velocity);
}



miniak_nrpn_struct miniak_nrpns[] = {

{ 0 , 0 , 1 }, // polyphony
{ 1 , 0 , 3 }, // unison voices
{ 2 , 0 , 100 }, // unison detune
{ 3 , 0 , 2 }, // portamento enabled
{ 4 , 0 , 3 }, // portamento type
{ 5 , 0 , 127 }, // portamento time
{ 6 , 0 , 1 }, // pitchwheel type
{ 7 , 0 , 100 }, // analog drift
{ 8 , 0 , 4 }, // oscillator sync type
{ 9 , 0 , 1000 }, // FM amount
{ 10 , 0 , 5 }, // FM type
{ 11 , 0 , 2 }, // oscillator 1 waveform
{ 12 , -100 , 100 }, // oscillator 1 wave shape
{ 13 , -3 , 3 }, // oscillator 1 octave
{ 14 , -7 , 7 }, // oscillator 1 transpose
{ 15 , -999 , 999 }, // oscillator 1 pitch
{ 16 , 0 , 12 }, // oscillator 1 pitchwheel range
{ 17 , 0 , 2 }, // oscillator 2 waveform
{ 18 , -100 , 100 }, // oscillator 2 wave shape
{ 19 , -3 , 3 }, // oscillator 2 octave
{ 20 , -7 , 7 }, // oscillator 2 transpose
{ 21 , -999 , 999 }, // oscillator 2 pitch
{ 22 , 0 , 12 }, // oscillator 2 pitchwheel range
{ 23 , 0 , 2 }, // oscillator 3 waveform
{ 24 , -100 , 100 }, // oscillator 3 wave shape
{ 25 , -3 , 3 }, // oscillator 3 octave
{ 26 , -7 , 7 }, // oscillator 3 transpose
{ 27 , -999 , 999 }, // oscillator 3 pitch
{ 28 , 0 , 12 }, // oscillator 3 pitchwheel range
{ 29 , 0 , 100 }, // Oscillator1 level
{ 30 , 0 , 100 }, // Oscillator2 level
{ 31 , 0 , 100 }, // Oscillator3 level
{ 32 , 0 , 100 }, // RingModulation level
{ 33 , 0 , 100 }, // Noise level
{ 34 , 0 , 100 }, // ExtIn level
{ 35 , -50 , 50 }, // Oscillator1 balance
{ 36 , -50 , 50 }, // Oscillator2 balance
{ 37 , -50 , 50 }, // Oscillator3 balance
{ 38 , -50 , 50 }, // RingModulation balance
{ 39 , -50 , 50 }, // Noise balance
{ 40 , -50 , 50 }, // ExtIn balance
{ 41 , 0 , 100 }, // series level???
{ 42 , 0 , 1 }, // noise type
{ 43 , 0 , 20 }, // filter 1 type
{ 44 , 0 , 1023 }, // filter 1 frequency
{ 45 , 0 , 100 }, // filter 1 resonance
{ 46 , -100 , 200 }, // filter 1 key track
{ 47 , -100 , 100 }, // filter 1 envelope amount
{ 48 , 0 , 1 }, // filter 2 relative to 1
{ 49 , 0 , 20 }, // filter 2 type
{ 50 , 0 , 1023 }, // filter 2 frequency absolute
{ 51 , 0 , 100 }, // filter 2 resonance
{ 52 , -100 , 200 }, // filter 2 key track
{ 53 , -100 , 100 }, // filter 2 envelope amount
{ 54 , 0 , 100 }, // filter 1 level
{ 55 , 0 , 100 }, // filter 2 level
{ 56 , 0 , 100 }, // pre-filter level
{ 57 , -100 , 100 }, // filter 1 pan
{ 58 , -100 , 100 }, // filter 2 pan
{ 59 , -100 , 100 }, // pre-filter pan
{ 60 , 0 , 6 }, // pre-filter source
{ 61 , 0 , 1 }, // filter 1 invert phase
{ 62 , 0 , 6 }, // drive type
{ 63 , 0 , 100 }, // drive level
{ 64 , 0 , 100 }, // program level
{ 65 , -50 , 50 }, // fx mix
{ 66 , 0 , 255 }, // envelope 1 attack time
{ 67 , 0 , 2 }, // envelope 1 attack slope
{ 68 , 0 , 255 }, // envelope 1 decay time
{ 69 , 0 , 2 }, // envelope 1 decay slope
{ 70 , 0 , 255 }, // envelope 1 sustain time
{ 71 , 0 , 100 }, // envelope 1 sustain level
{ 72 , 0 , 255 }, // envelope 1 release time
{ 73 , 0 , 2 }, // envelope 1 release slope
{ 74 , 0 , 100 }, // envelope 1 velocity to envelope
{ 75 , 0 , 1 }, // envelope 1 reset every note
{ 76 , 0 , 1 }, // envelope 1 free run
{ 77 , 0 , 3 }, // envelope 1 loop
{ 78 , 0 , 1 }, // envelope 1 sustain pedal
{ 79 , 0 , 255 }, // envelope 2 attack time
{ 80 , 0 , 2 }, // envelope 2 attack slope
{ 81 , 0 , 255 }, // envelope 2 decay time
{ 82 , 0 , 2 }, // envelope 2 decay slope
{ 83 , 0 , 255 }, // envelope 2 sustain time
{ 84 , -100 , 100 }, // envelope 2 sustain level
{ 85 , 0 , 255 }, // envelope 2 release time
{ 86 , 0 , 2 }, // envelope 2 release slope
{ 87 , 0 , 100 }, // envelope 2 velocity to envelope
{ 88 , 0 , 1 }, // envelope 2 reset every note
{ 89 , 0 , 1 }, // envelope 2 free run
{ 90 , 0 , 3 }, // envelope 2 loop
{ 91 , 0 , 1 }, // envelope 2 sustain pedal
{ 92 , 0 , 255 }, // envelope 3 attack time
{ 93 , 0 , 2 }, // envelope 3 attack slope
{ 94 , 0 , 255 }, // envelope 3 decay time
{ 95 , 0 , 2 }, // envelope 3 decay slope
{ 96 , 0 , 255 }, // envelope 3 sustain time
{ 97 , -100 , 100 }, // envelope 3 sustain level
{ 98 , 0 , 255 }, // envelope 3 release time
{ 99 , 0 , 2 }, // envelope 3 release slope
{ 100 , 0 , 100 }, // envelope 3 velocity to envelope
{ 101 , 0 , 1 }, // envelope 3 reset every note
{ 102 , 0 , 1 }, // envelope 3 free run
{ 103 , 0 , 3 }, // envelope 3 loop
{ 104 , 0 , 1 }, // envelope 3 sustain pedal
{ 105 , 0 , 1 }, // lfo 1 tempo sync
{ 106 , 0 , 1023 }, // lfo 1 rate (free)
{ 107 , 0 , 6 }, // lfo 1 reset
{ 108 , 0 , 100 }, // m1 slider influencelfo 1
{ 109 , 0 , 1 }, // lfo 2 tempo sync
{ 110 , 0 , 1023 }, // lfo 2 rate (free)
{ 111 , 0 , 6 }, // lfo 2 reset
{ 112 , 0 , 100 }, // m1 slider influencelfo 2
{ 113 , 0 , 1 }, // S&H tempo sync
{ 114 , 0 , 1023 }, // S&H rate(free)
{ 115 , 0 , 4 }, // S&H reset
{ 116 , 0 , 1 }, // S&H input
{ 117 , 0 , 100 }, // S&H smoothing
{ 118 , 0 , 111 }, // tracking input
{ 119 , 0 , 9 }, // tracking preset
{ 120 , 0 , 1 }, // tracking gridsize
{ 121 , -100 , 100 }, // tracking point -16
{ 122 , -100 , 100 }, // tracking point -15
{ 123 , -100 , 100 }, // tracking point -14
{ 124 , -100 , 100 }, // tracking point -13
{ 125 , -100 , 100 }, // tracking point -12
{ 126 , -100 , 100 }, // tracking point -11
{ 127 , -100 , 100 }, // tracking point -10
{ 128 , -100 , 100 }, // tracking point -9
{ 129 , -100 , 100 }, // tracking point -8
{ 130 , -100 , 100 }, // tracking point -7
{ 131 , -100 , 100 }, // tracking point -6
{ 132 , -100 , 100 }, // tracking point -5
{ 133 , -100 , 100 }, // tracking point -4
{ 134 , -100 , 100 }, // tracking point -3
{ 135 , -100 , 100 }, // tracking point -2
{ 136 , -100 , 100 }, // tracking point -1
{ 137 , -100 , 100 }, // tracking point 0
{ 138 , -100 , 100 }, // tracking point 1
{ 139 , -100 , 100 }, // tracking point 2
{ 140 , -100 , 100 }, // tracking point 3
{ 141 , -100 , 100 }, // tracking point 4
{ 142 , -100 , 100 }, // tracking point 5
{ 143 , -100 , 100 }, // tracking point 6
{ 144 , -100 , 100 }, // tracking point 7
{ 145 , -100 , 100 }, // tracking point 8
{ 146 , -100 , 100 }, // tracking point 9
{ 147 , -100 , 100 }, // tracking point 10
{ 148 , -100 , 100 }, // tracking point 11
{ 149 , -100 , 100 }, // tracking point 12
{ 150 , -100 , 100 }, // tracking point 13
{ 151 , -100 , 100 }, // tracking point 14
{ 152 , -100 , 100 }, // tracking point 15
{ 153 , -100 , 100 }, // tracking point 16
{ 154 , 0 , 8 }, // program category
{ 155 , 0 , 161 }, // Knob X
{ 156 , 0 , 161 }, // Knob Y
{ 157 , 0 , 161 }, // Knob Z
{ 158 , -400 , 400 }, // filter 2 frequency relative
{ 159 , 0 , 24 }, // lfo 1 rate (follow tempo)
{ 160 , 0 , 24 }, // lfo 2 rate (follow tempo)
{ 161 , 0 , 24 }, // S&H rate(synced)
{ 180 , 0 , 77 }, // patch cable 1 source
{ 181 , 0 , 77 }, // patch cable 1 destination
{ 182 , -1000 , 1000 }, // patch cable 1 level
{ 183 , -1000 , 1000 }, // patch cable 1 offset
{ 184 , 0 , 114 }, // patch cable 2 source
{ 185 , 0 , 77 }, // patch cable 2 destination
{ 186 , -1000 , 1000 }, // patch cable 2 level
{ 187 , -1000 , 1000 }, // patch cable 2 offset
{ 188 , 0 , 114 }, // patch cable 3 source
{ 189 , 0 , 77 }, // patch cable 3 destination
{ 190 , -1000 , 1000 }, // patch cable 3 level
{ 191 , -1000 , 1000 }, // patch cable 3 offset
{ 192 , 0 , 114 }, // patch cable 4 source
{ 193 , 0 , 77 }, // patch cable 4 destination
{ 194 , -1000 , 1000 }, // patch cable 4 level
{ 195 , -1000 , 1000 }, // patch cable 4 offset
{ 196 , 0 , 114 }, // patch cable 5 source
{ 197 , 0 , 77 }, // patch cable 5 destination
{ 198 , -1000 , 1000 }, // patch cable 5 level
{ 199 , -1000 , 1000 }, // patch cable 5 offset
{ 200 , 0 , 114 }, // patch cable 6 source
{ 201 , 0 , 77 }, // patch cable 6 destination
{ 202 , -1000 , 1000 }, // patch cable 6 level
{ 203 , -1000 , 1000 }, // patch cable 6 offset
{ 204 , 0 , 114 }, // patch cable 7 source
{ 205 , 0 , 77 }, // patch cable 7 destination
{ 206 , -1000 , 1000 }, // patch cable 7 level
{ 207 , -1000 , 1000 }, // patch cable 7 offset
{ 208 , 0 , 114 }, // patch cable 8 source
{ 209 , 0 , 77 }, // patch cable 8 destination
{ 210 , -1000 , 1000 }, // patch cable 8 level
{ 211 , -1000 , 1000 }, // patch cable 8 offset
{ 212 , 0 , 114 }, // patch cable 9 source
{ 213 , 0 , 77 }, // patch cable 9 destination
{ 214 , -1000 , 1000 }, // patch cable 9 level
{ 215 , -1000 , 1000 }, // patch cable 9 offset
{ 216 , 0 , 114 }, // patch cable 10 source
{ 217 , 0 , 77 }, // patch cable 10 destination
{ 218 , -1000 , 1000 }, // patch cable 10 level
{ 219 , -1000 , 1000 }, // patch cable 10 offset
{ 220 , 0 , 114 }, // patch cable 11 source
{ 221 , 0 , 77 }, // patch cable 11 destination
{ 222 , -1000 , 1000 }, // patch cable 11 level
{ 223 , -1000 , 1000 }, // patch cable 11 offset
{ 224 , 0 , 114 }, // patch cable 12 source
{ 225 , 0 , 77 }, // patch cable 12 destination
{ 226 , -1000 , 1000 }, // patch cable 12 level
{ 227 , -1000 , 1000 }, // patch cable 12 offset
{ 230 , -50 , 50 }, // fx balance
{ 231 , 0 , 6 }, // fx type 0
{ 232 , -128 , 127 }, // fx param a
{ 233 , -128 , 127 }, // fx param b
{ 234 , -128 , 127 }, // fx param c
{ 235 , -128 , 127 }, // fx param d
{ 236 , -128 , 127 }, // fx param e
{ 237 , -128 , 127 }, // fx param f
{ 238 , -128 , 127 }, // fx param g
{ 239 , 0 , 24 }, // fx param h lfo synced
{ 245 , 0 , 6 }, // fx2 type
{ 246 , -32768 , 32768 }, // fx2 param a
{ 247 , -32768 , 32768 }, // fx2 param b
{ 248 , -32768 , 32768 }, // fx2 param c
{ 249 , -32768 , 32768 }, // fx2 param d
{ 250 , 0 , 24 }, // fx param a synced
{ 512 , 0 , 31 }, // arpeggiator pattern
{ 513 , 0 , 6 }, // arpeggiator pattern multiplier
{ 514 , 0 , 14 }, // arpeggiator length
{ 515 , 0 , 4 }, // arpeggiator octave range
{ 516 , 0 , 2 }, // arpeggiator octave span
{ 517 , 0 , 5 }, // arpeggiator note order
{ 518 , 0 , 2 }, // arpeggiator enabled
{ 519 , 500 , 2500 }, // arpeggiator program tempo
{ 999 , 0 , 0 }, // end message
};