dmusker
Published © CC BY-NC-SA

Virtual Physical Combo Organ

Creating a playable modern virtual MIDI-based Vox Continental (or any other keyboard)

IntermediateWork in progress1,173
Virtual Physical Combo Organ

Things used in this project

Hardware components

Arduino Due
Arduino Due
×1
Arduino Mega Proto Shield
Arduino Mega Proto Shield
×1
LattePanda 4GB/64GB
LattePanda 4GB/64GB
×1
Rocker Switch, Non Illuminated
Rocker Switch, Non Illuminated
×2
Drawbar Potentiometer
×6
Midi Breakout Board
×1
SparkFun Logic Level Converter - Bi-Directional
SparkFun Logic Level Converter - Bi-Directional
×1
Panel Mount USB Cable - B Female to Micro-B Male
×1
Capacitor 100 nF
Capacitor 100 nF
×6
Vishay Capacitor 1uF polyester
×3
Protek +12/-12/+5V PSU
×1
DFRobot Lattepanda 5V heatsink and fan unit
×1

Software apps and online services

Windows 10
Microsoft Windows 10
Cantabile Solo
ComboV VSTi
jBridge
Hiduino
Hairless Midi Serial Bridge
LoopMidi

Hand tools and fabrication machines

Crimp Tool, Heavy-Duty
Crimp Tool, Heavy-Duty
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Drawbar Connections

Code

Vox_Due_Decoupled_USB_midi_keyboard.ino

Arduino
This is the code that runs the Arduino Due. It (a) generates Midi note and control messages; and (b) generates a mono square wave plus gate signal. Includes some tricks to minimise latency and noise, and maximise USB throughput using queues.
#include <MIDIUSB.h>
#include <ArduinoQueue.h>
/*
Program for MIDIfying a Vox Continental organ
(c) Prof David Musker, June 2020
Free for use if source is acknowledged

NOTE: the toneDue() and noToneDue() functions are versions of the sketch posted by Jeff K07M here:
http://ko7m.blogspot.com/2015/01/arduino-due-timers-part-1.html

Intended use is:

- a 49-key single manual - can be changed with KeyboardSize (but some of the arrays will need extra elements added)

- 6 drawbars - can be changed with DrawbarMax

- a volume (expression) pedal - may be absent

- a vibrato switch

Generates:

- polyphonic MIDI note on and off messages on MidiChannel

- monophonic Low Note priority MIDI note on and off messages on LowMidiChannel

- monophonic High Note priority MIDI note on and off messages on HighMidiChannel

- MIDI CC messages based on the drawbars, volume pedal and vibrato switch

- Square wave tones for one or other of the mono signals on one pin and gate on/off signals on another, for feeding to a modular synth
*/ 

//=================================================================
const byte DrawbarMax = 6;                                     //the total number of drawbars and sliders
const byte KeyboardSize = 49;                                  //size of keyboard here 49 keys

const byte ExpPedPin = A6;                                     //analog pedal input pin
const byte StartNotePin = 2;                                   //first digital input pin for note, leaving digital pin 1 for serial TX and 0 for RX or other use
const byte VibPin  = 53;                                       //digital input pin for the vibrato switch - first free pin after 2 for serial port and 49 for keys
const byte DrawbarPin [DrawbarMax] = {A3, A1, A2, A0, A5, A4}; //array of analogue ports, extend if more
const byte AudioOutPin  = 52;                                  //in case we want to send an audio output.  Need to connect via a DC-blocking capacitor.
const byte GatePin  = 51;                                      //in case we want to send an audio output, use as gate.  Need to connect via a 1K current limiting resistor

const byte NoteOff = B10000000;
const byte NoteOn  = B10010000;
const byte CCCmd   = B10110000;
const byte AllNotesOffCC = 123;
const byte VibCC = 1;                                          //Vibrato Midi CC, default to ComboV assignment
const byte ExpPedalCC = 11;                                    //ComboV swell pedal assignment
const byte DrawbarCC [DrawbarMax] = {12,13,14,15,16,17};       //ComboV drawbar/slider CCs, extend if more - try a value other than 17???? maybe 9

const byte MidiChannel = 0;                                    //set Midi channel value here to whatever
const byte LowMidiChannel = 1;                                 //set Midi channel value here to whatever
const byte HighMidiChannel = 2;                                //set Midi channel value here to whatever
const byte LowestNote = 24;                                    //Midi note number of lowest note, set to C1

const unsigned int freq[85] = {33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186}; //table of MIDI note frequencies for use by tone()
const byte KeyPin [KeyboardSize] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,29,28,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48};

//=================================================================
volatile bool AudioOn = false;
uint32_t PulseWidthFactor = 1;                                 //1 for a square wave, higher for narrower pulse widths

unsigned long KeyReadTime = 0;
unsigned long LastADCTime = 0;                                 //a time counter, since last analogRead 
unsigned long DebounceTime = 20;                               //20mS in real life for notes - might set lower
unsigned long LastMIDISentTime;                                //track when last MIDI message sent
unsigned long MidiLag = 50;                                    //delay between MIDI messages - greater than 25uS and less than say 500uS to avoid latency

byte NoteCount = 0;                                            //notes on
byte NoteCountOld = 0;                                         //notes on at last scan
byte NotesChanged = 0;                                         //note changes from last scan
byte LowNote = 0;                                              //for generating a monophonic output
byte HighNote = 0;                                             //for generating a monophonic output
byte LowNoteOld = 0;                                           //for generating a monophonic output
byte HighNoteOld = 0;                                          //for generating a monophonic output
byte Velocity;
byte DrawbarCount = 0;                                         //a drawbar counter incrementing each read
byte ExpPedVal = 127;                                          //the volume level start on full volume
byte LastExpPedVal = 127;                                      //previous volume, start on full volume
byte VibVal = 0;                                               //Code to send for vibrato CC initialise at off

bool VibState = HIGH;                                          //vibrato switch state
bool LastVibState = HIGH;                                      //previous switch state
bool ExpPed = false;                                           //check expression pedal or not
bool NoPedal = false;                                          //a flag indicating whether an expression pedal is plugged in

bool KeyState[KeyboardSize] = {false};
bool NoteState[KeyboardSize] [3] = {false};                    //current and last note states and a change flag
unsigned long KeyLastTime [KeyboardSize] = {0};                //times since last key transitions
byte Drawbars [DrawbarMax] = {127};                            //drawbar ADC readings, initialise at full

//Queue creation:
ArduinoQueue<midiEventPacket_t> MidiChannelNoteQueue(20);
ArduinoQueue<midiEventPacket_t> OtherNoteQueue(10);
ArduinoQueue<midiEventPacket_t> CCQueue(10);

//=================================================================
void setup () {
delay(1000); //to deal with known Arduino Due R3 startup reset issue - I don't think it works so delete it

Serial.begin(31250);
for (int i = 0; i < KeyboardSize; i++) {
  pinMode((StartNotePin+i), INPUT_PULLUP); //Digital input pins set to input pullup
}
pinMode(VibPin, INPUT_PULLUP); //ditto
pinMode(GatePin, OUTPUT); 
pinMode(AudioOutPin, OUTPUT);
pinMode(ExpPedPin, OUTPUT);
digitalWrite(ExpPedPin, LOW);
if (analogRead(ExpPedPin) == 0) { //switch is closed so no pedal attached and pin grounded
  NoPedal = true;
}

SwitchAllNotesOff();
}

//=================================================================
void loop () {

KeyRead();

NoteRead();

NoteSend();

Mono();

AllOffCheck();

SwitchOffCheck();  

ReadSwitches();

ReadAnalogs();

TransmitMIDI();

} //main loop ends here

//=================================================================
void KeyRead() {
KeyReadTime = millis();
  for (int i = 0; i < KeyboardSize; i++) { //note reading loop
    KeyState[i] = digitalRead(KeyPin[i]+StartNotePin); //indirect addressing via keypin array
  }
}

//=================================================================
void NoteRead() {
NoteCountOld = NoteCount;
NotesChanged = 0;
 for (int i = 0; i < KeyboardSize; i++ ) { 
  if ((KeyReadTime - KeyLastTime [i]) > DebounceTime) { //only look at key state if it is more than DebounceTime since last change in note otherwise skip
    KeyLastTime [i] = KeyReadTime;
    NoteState[i][3] = false;
    NoteState[i][2] = NoteState[i][1]; //update previous note state
    if (((KeyState[i] == false) && (NoteState[i][1] == true)) || ((KeyState[i] == true) && (NoteState[i][1] == false))){ //key state has changed
        NoteState[i][1] = KeyState[i]; //update current note state
        NoteState[i][3] = true; //set a changed state flag
        NotesChanged++;
        if (NoteState[i][1] == true) { //note is on
         NoteCount++; //increment number of notes on
         }
        else {NoteCount--;} //decrement number of notes on
    }    
   }
  }
}
/*
At the end of this loop, we have:
-an array of: 
NoteState[i][1] - current note states, 
NoteState[i][2] - previous note states, and 
NoteState[i][3] - change flags 
NoteCount - a count of the number of notes on
NotesChanged - a count of the number of notes changed 
*/

//=================================================================
void NoteSend() {
if (NotesChanged > 0) {//something has changed
    for (int i = 0; i < (KeyboardSize); i++) {
      if (NoteState[i][3] == true){//note state has changed
        if (NoteState[i][1] == true){//note is ON
          sendNoteOn((i+ LowestNote), MidiChannel, 127);
          }
          else {//note is OFF
            sendNoteOff((i+ LowestNote), MidiChannel, 0);}//note is OFF
            }
      }
   }
}

//=================================================================
void Mono() {
  if (NotesChanged > 0) {
    LowNoteOld = LowNote;
    LowNote = 0;
    HighNoteOld = HighNote;
    HighNote = 0;
    if (NoteCount > 0) { //at least one note is on
      FindLowNote();
      FindHighNote();
    }
    if (LowNote == 0) {
      noToneDue(); //switch off audio out
      digitalWrite(GatePin, LOW); //Gate off

    }
    if ((LowNote != 0) && (LowNote != LowNoteOld)) {
      if (LowNoteOld != 0) {
       // delayMicroseconds(50); //this is there because of a problem of skipped messages in MIDIUSB - below 50 there may be issues
        sendNoteOff(LowNoteOld, LowMidiChannel, 0);
      }
      //delayMicroseconds(50); //this is there because of a problem of skipped messages in MIDIUSB - below 50 there may be issues
      sendNoteOn(LowNote, LowMidiChannel, 127);
      noToneDue(); //switch off audio out
      toneDue(freq[LowNote-LowestNote], PulseWidthFactor); //generate wave from low note priority mono, with stated freq and duty factor
      digitalWrite(GatePin, HIGH); //Gate on
    }
    
    if ((HighNote != 0) && (HighNote != HighNoteOld)) {
      if (HighNoteOld != 0) {
      sendNoteOff(HighNoteOld, HighMidiChannel, 0);  
      }
      sendNoteOn(HighNote, HighMidiChannel, 127);
      //could generate tone here too
    }  
  }
}

//=================================================================
void FindLowNote() {
  int i = 0;
    while (NoteState[i][1] == false){ //key is OFF
      i++;
    }
    LowNote = (i + LowestNote); //Lowest Midi note played - could drop it an octave by subtracting another 12 here
  }

//=================================================================
void FindHighNote() {
    int i = KeyboardSize;
      while (NoteState[i-1][1] == false){ //key is OFF
        i--;
      }
          HighNote = ((i-1) + LowestNote); //Highest Midi note played
  } 

//=================================================================
void noToneDue() {
  TC_Stop(TC1, 0); //turn timer off
}

//=================================================================
void toneDue(uint32_t Rate, uint32_t PW) {
  TimerStart(TC1, 0, TC3_IRQn, Rate, PW);
}

//=================================================================
void TimerStart(Tc *tc, uint32_t channel, IRQn_Type irq, uint32_t freq, uint32_t DutyFactor)
{
   pmc_set_writeprotect(false);
   pmc_enable_periph_clk(irq);
   TC_Configure(tc, channel, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC |
                             TC_CMR_TCCLKS_TIMER_CLOCK4);
   uint32_t rc = VARIANT_MCK / 128 / freq;
   TC_SetRA(tc, channel, rc>>DutyFactor); // pulse train - DutyFactor 2 gives 50% duty cycle square wave
   TC_SetRC(tc, channel, rc);
   TC_Start(tc, channel);
   tc->TC_CHANNEL[channel].TC_IER=  TC_IER_CPCS | TC_IER_CPAS;
   tc->TC_CHANNEL[channel].TC_IDR=~(TC_IER_CPCS | TC_IER_CPAS);
   NVIC_EnableIRQ(irq);
}

//=================================================================
void TC3_Handler() //interrupt handler to generate mono wave
{
   TC_GetStatus(TC1, 0);
   digitalWrite(AudioOutPin, AudioOn = !AudioOn); //flip state of pin
}

//=================================================================
void SwitchOffCheck() {
 if (NotesChanged > 0) {//something has changed
  if (NoteCount == 2) { //only 2 notes are on
    if ((NoteState[0][1] == true) && (NoteState[(KeyboardSize-1)][1] == true)) { //the only 2 notes ON are the top and the bottom
      sendNoteOn(0, HighMidiChannel, 1); //send a signal to enable Cantabile to switch off computer
    }
  }
}
}

//=================================================================
void AllOffCheck() {
  if ((NoteCount == 0) && (NoteCountOld > 0)) { //All keys now turned off for the first time
    noToneDue(); //switch off audio out
    SwitchAllNotesOff();
  }
}

//=================================================================
void SwitchAllNotesOff() {
  sendControlChange(AllNotesOffCC, 0, MidiChannel);     //send all notes off message 
  sendControlChange(AllNotesOffCC, 0, LowMidiChannel);  //send all notes off message 
  sendControlChange(AllNotesOffCC, 0, HighMidiChannel); //send all notes off message 
  }
  
//=================================================================
void sendNoteOn (byte Pitch, byte MidiChannel, byte Velocity) {
  MIDImessage(NoteOn, MidiChannel, Pitch, Velocity);
}

//=================================================================
void sendNoteOff (byte Pitch, byte MidiChannel, byte Velocity) {
  MIDImessage(NoteOff, MidiChannel, Pitch, Velocity);
}

//=================================================================
void ReadSwitches() { 
  if ((millis() - LastADCTime) > DebounceTime) {//wait for 25 msec to debounce
    LastVibState = VibState; //update old vibrato state
    VibState = digitalRead(VibPin); //update vibrato state
    if (VibState != LastVibState) {
      if (VibState == LOW) { 
        VibVal = 127;
        }
      else { 
        VibVal = 0;
        }
    sendControlChange(VibCC, VibVal, MidiChannel); //send vibrato 
    
    } 
 }
//repeat for any other switches
}

//=================================================================
void ReadAnalogs() {
  if (NotesChanged == 0) { //only read the analog pins if no notes are changing
    if ((millis() - LastADCTime) > DebounceTime) {//wait for 25 msec before taking next ADC reading
      LastADCTime = millis(); //update the timer
      ExpPed = !ExpPed; //flip the flag
      if (ExpPed == true) { //Read the pedal every other cycle
         if (NoPedal == false) { //a pedal is connected
          ExpPedVal = handleADC(ExpPedalCC, ExpPedVal, ExpPedPin);
         }
      }
      if (ExpPed != true) { //Read the drawbars every other cycle
       //drawbar and slider readings start here
       if (NoteCount < 5) { //don't read sliders if playing.  Could alter to 5 to reflect only one hand playing 
        Drawbars[DrawbarCount] = handleADC((DrawbarCC[DrawbarCount]),(Drawbars[DrawbarCount]),(DrawbarPin[DrawbarCount]));
        DrawbarCount++;
          if (DrawbarCount == DrawbarMax){
              DrawbarCount = 0; //roll back round to first slider
          }
       }
      }
    }
  }
}

//=================================================================
byte handleADC (byte CCVal, byte lastval, byte port) {
  int newvalue = analogRead(port);
  unsigned int divided = (unsigned int)newvalue >> 3; //go from 10 bits to 7
  byte newval = (byte)divided;
  byte delta =  (newval - lastval);
 if (abs(delta) > 12) { //do nothing unless over 10 percent change
    sendControlChange(CCVal, newval, MidiChannel); //transmit
    return newval;
  }
  else {
  return lastval;
  }
}

//=================================================================
void sendControlChange (byte ControllerNo, byte Value, byte Midichannel) {
  int cmd = (CCCmd + Midichannel);
  midiEventPacket_t midiMsg = {cmd >> 4, cmd, ControllerNo, Value};
  MIDImessage(CCCmd, Midichannel, ControllerNo, Value);
}

//=================================================================
void MIDImessage(int byte1, int byte2, int byte3, int byte4) { //sends a 3 byte MIDI message via the serial port, TX pin 1 and queues it
  //here you could use the Arduino Midi library command instead
  byte cmd = (byte1 + byte2);
  Serial.write(cmd);//send note on or note off command
  Serial.write(byte3);//send pitch data
  Serial.write(byte4);//send velocity data
  midiEventPacket_t midiMsg = {cmd >> 4, cmd, byte3, byte4};
  if ((byte2 = MidiChannel) && ((byte1 = NoteOn) || (byte1 = NoteOff)))  { //note message on main channel
     MidiChannelNoteQueue.enqueue(midiMsg);
     }
  else if (((byte2 = HighMidiChannel) || (byte2 = LowMidiChannel)) && ((byte1 = NoteOn) || (byte1 = NoteOff))) { //other note message
     OtherNoteQueue.enqueue(midiMsg);
     }
  else { //control change message on any channel, or anything else
     CCQueue.enqueue(midiMsg);
     }
}

//=================================================================
void TransmitMIDI() {
if ((micros() - LastMIDISentTime) > MidiLag) {//only send if the delay time has expired
   if (!MidiChannelNoteQueue.isEmpty()) { //something in the main channel queue
      MidiUSB.sendMIDI(MidiChannelNoteQueue.dequeue());
      MidiUSB.flush();
      LastMIDISentTime = micros();
      }
   else if (!OtherNoteQueue.isEmpty()) { //something in the Low or High channel queue
      MidiUSB.sendMIDI(OtherNoteQueue.dequeue());
      MidiUSB.flush();
      LastMIDISentTime = micros();
      }
   else if (!CCQueue.isEmpty()) {       //something in the control channel queue
      MidiUSB.sendMIDI(CCQueue.dequeue());
      MidiUSB.flush();
      LastMIDISentTime = micros();
      }

  }
}

Vox_Mega_midi_keyboard_MIDI_Serial.ino

Arduino
#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();

/*
Program for MIDIfying a Vox Continental organ
(c) Prof David Musker, June 2020
Free for use if source is acknowledged

Intended use is:

- a 49-key single manual - can be changed with KeyboardSize

- 6 drawbars - can be changed with const byte DrawbarMax

- a volume (expression) pedal

- a vibrato switch

Generates:

- polyphonic MIDI note on and off messages on MidiChannel

- monophonic Low Note priority MIDI note on and off messages on LowMidiChannel

- monophonic High Note priority MIDI note on and off messages on HighMidiChannel

- MIDI CC messages based on the drawbars, volume pedal and vibrato switch

Can also generate square wave tones for one or other of the mono signals on one pin and gate on/off signals on another, for feeding to a modular synth


*/ 

//=================================================================

const byte DrawbarMax = 6;                                     //the total number of drawbars and sliders
const byte KeyboardSize = 49;                                  //size of keyboard here 49 keys

const byte ExpPedPin = A6;                                     //analog pedal input pin
const byte StartNotePin = 2;                                   //first digital input pin for note, leaving digital pin 1 for serial TX and 0 for RX or other use
const byte VibPin  = 53;                                       //digital input pin for the vibrato switch - first free pin after 2 for serial port and 49 for keys
const byte DrawbarPin [DrawbarMax] = {A3, A1, A2, A0, A5, A4}; //array of analogue ports, extend if more
const byte AudioOutPin  = 52;                                  //in case we want to send an audio output.  Need to connect via a DC-blocking capacitor.
const byte GatePin  = 51;                                      //in case we want to send an audio output, use as gate.  Need to connect via a 1K current limiting resistor

const byte NoteOff = B10000000;
const byte NoteOn  = B10010000;
const byte CCCmd   = B10110000;
const byte AllNotesOffCC = 123;
const byte VibCC = 1;                                          //Vibrato Midi CC, default to ComboV assignment
const byte ExpPedalCC = 11;                                    //ComboV swell pedal assignment
const byte DrawbarCC [DrawbarMax] = {12,13,14,15,16,17};       //ComboV drawbar/slider CCs, extend if more - try a value other than 17???? maybe 9

const byte MidiChannel = 0;                                    //set Midi channel value here to whatever
const byte LowMidiChannel = 1;                                 //set Midi channel value here to whatever
const byte HighMidiChannel = 2;                                //set Midi channel value here to whatever
const byte LowestNote = 24;                                    //Midi note number of lowest note, set to C1

const unsigned int freq[85] = {33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186}; //table of MIDI note frequencies for use by tone()
const byte KeyPin [KeyboardSize] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,29,28,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48};

//=================================================================

unsigned long KeyReadTime = 0;
unsigned long LastADCTime = 0;                                 //a time counter, since last analogRead 
unsigned long DebounceTime = 20;                               //20mS in real life for notes - might set lower

byte NoteCount = 0;                                            //notes on
byte NoteCountOld = 0;                                         //notes on at last scan
byte NotesChanged = 0;                                         //note changes from last scan
byte LowNote = 0;                                              //for generating a monophonic output
byte HighNote = 0;                                             //for generating a monophonic output
byte LowNoteOld = 0;                                           //for generating a monophonic output
byte HighNoteOld = 0;                                          //for generating a monophonic output
byte DrawbarCount = 0;                                         //a drawbar counter incrementing each read
byte ExpPedVal = 127;                                          //the volume level start on full volume
byte LastExpPedVal = 127;                                      //last volume, start on full volume
byte VibVal = 0;                                               //Code to send for vibrato CC initialise at off

bool VibState = HIGH;                                          //vibrato switch state
bool LastVibState = HIGH;                                      //previous switch state
bool ExpPed = false;                                           //an alternating boolean flag
bool NoPedal = false;                                          //a flag indicating whether an expression pedal is plugged in

bool KeyState[KeyboardSize] = {false};
bool NoteState[KeyboardSize] [3] = {false};                    //current and last note states and a change flag
unsigned long KeyLastTime [KeyboardSize] = {0};                //times since last key transitions
byte Drawbars [DrawbarMax] = {127};                            //drawbar ADC readings initialise at full

//=================================================================
void setup () {

for (int i = 0; i < KeyboardSize; i++) {
  pinMode((StartNotePin+i), INPUT_PULLUP); // Digital input pins set to input pullup
}
pinMode(VibPin, INPUT_PULLUP); //ditto
pinMode(GatePin, OUTPUT); 
pinMode(AudioOutPin, OUTPUT);
pinMode(ExpPedPin, OUTPUT);
digitalWrite(ExpPedPin, LOW);
if (analogRead(ExpPedPin) == 0) {
  NoPedal = true;
}

delay(1000*60*4); // 4 minute delay to give the computer time to set up Hairless Midi etc, change if necessary

SwitchAllNotesOff(); // just in case anything hanging
}

//=================================================================
void loop () {

KeyRead();

NoteRead();

NoteSend();

SwitchOffCheck();  

AllOffCheck();

Mono();

ReadSwitches();

ReadAnalogs();

} //main loop ends here

//=================================================================
void KeyRead() {
KeyReadTime = millis();
  for (int i = 0; i < KeyboardSize; i++) { //note reading loop
    KeyState[i] = digitalRead(KeyPin[i]+StartNotePin); //indirect addressing via keypin array
  }
}

//=================================================================
void NoteRead() {
NoteCountOld = NoteCount;
NotesChanged = 0;
 for (int i = 0; i < KeyboardSize; i++ ) { 
  if ((KeyReadTime - KeyLastTime [i]) > DebounceTime) { // only look at key state if it is more than DebounceTime since last change in note otherwise skip
    KeyLastTime [i] = KeyReadTime;
    NoteState[i][3] = false;
    NoteState[i][2] = NoteState[i][1]; // update previous note state
    if (((KeyState[i] == false) && (NoteState[i][1] == true)) || ((KeyState[i] == true) && (NoteState[i][1] == false))){ //key state has changed
        NoteState[i][1] = KeyState[i]; //update current note state
        NoteState[i][3] = true; //set a changed state flag
        NotesChanged++;
        if (NoteState[i][1] == true) { // note is on
         NoteCount++; //increment number of notes on
         }
        else {NoteCount--;} //decrement number of notes on
    }    
   }
  }
}
/*
At the end of this loop, we have:
-an array of: 
NoteState[i][1] - current note states, 
NoteState[i][2] - previous note states, and 
NoteState[i][3] - change flags 
NoteCount - a count of the number of notes on
NotesChanged - a count of the number of notes changed 
*/

//=================================================================
void NoteSend() {
if (NotesChanged > 0) {//something has changed
    for (int i = 0; i < (KeyboardSize); i++) {
      if (NoteState[i][3] == true){//note state has changed
        if (NoteState[i][1] == true){//note is ON
          MIDI.sendNoteOn((i+ LowestNote), 127, MidiChannel);
          }
          else {//note is OFF
            MIDI.sendNoteOff((i+ LowestNote), 0, MidiChannel);}//note is OFF
            }
      }
   }
}

//=================================================================
void Mono() {
  if (NotesChanged > 0) {
    LowNoteOld = LowNote;
    LowNote = 0;
    HighNoteOld = HighNote;
    HighNote = 0;
    if (NoteCount > 0) { // at least one note is on
      FindLowNote();
      FindHighNote();
    }
    if (LowNote == 0) {
      noTone(AudioOutPin); //switch off audio out
      digitalWrite(GatePin, LOW); //Gate off

    }
    if ((LowNote != 0) && (LowNote != LowNoteOld)) {
      if (LowNoteOld != 0) {
        MIDI.sendNoteOff(LowNoteOld, 0, LowMidiChannel);
      }
      MIDI.sendNoteOn(LowNote, 127, LowMidiChannel);
      noTone(AudioOutPin); //switch off audio out
      tone(AudioOutPin, freq[LowNote-LowestNote]); //generate square wave from low note priority mono
      digitalWrite(GatePin, HIGH); // Gate on
    }
    
    if ((HighNote != 0) && (HighNote != HighNoteOld)) {
      if (HighNoteOld != 0) {
      MIDI.sendNoteOff(HighNoteOld, 0, HighMidiChannel);  
      }
      MIDI.sendNoteOn(HighNote, 0, HighMidiChannel);
      //could instead generate tone here??
    }  
  }
}

//=================================================================
void FindLowNote() {
  int i = 0;
    while (NoteState[i][1] == false){ //key is OFF
      i++;
    }
    LowNote = (i + LowestNote); //lowest Midi note played - could drop it an octave by subtracting another 12 here
  }

//=================================================================
void FindHighNote() {
    int i = KeyboardSize;
      while (NoteState[i-1][1] == false){ //key is OFF
        i--;
      }
          HighNote = ((i-1) + LowestNote); //lowest Midi note played
  } 

//=================================================================
void SwitchOffCheck() {
 if (NotesChanged > 0) {//something has changed
  if (NoteCount == 2) { //only 2 notes are on
    if ((NoteState[0][1] == true) && (NoteState[(KeyboardSize-1)][1] == true)) { //the only 2 notes ON are the top and the bottom
      MIDI.sendNoteOn(0, 127, HighMidiChannel); //send a signal to enable Cantabile to switch off computer
      delay(5000);//pause so nothing else is sent until Cantabile closes
    }
  }
}
}

//=================================================================
void AllOffCheck() {
  if ((NoteCount == 0) && (NoteCountOld > 0)) { //All keys now turned off for the first time
    noTone(AudioOutPin); //switch off audio out
    SwitchAllNotesOff();
  }
}

//=================================================================
void SwitchAllNotesOff() {
  MIDI.sendControlChange(AllNotesOffCC, 0, MidiChannel); //send all notes off message 
  MIDI.sendControlChange(AllNotesOffCC, 0, LowMidiChannel);  //send all notes off message
  MIDI.sendControlChange(AllNotesOffCC, 0, HighMidiChannel); //send all notes off message 
  }

//================================================================================================
void ReadSwitches() { //read switches
  if ((millis() - LastADCTime) > DebounceTime) {//wait for 25 msec to debounce
    LastVibState = VibState; // update old vibrato state
    VibState = digitalRead(VibPin); // update vibrato state
    if (VibState != LastVibState) {
      if (VibState == LOW) { 
        VibVal = 127;
        }
      else { 
        VibVal = 0;
        }
    MIDI.sendControlChange(VibCC, VibVal, MidiChannel); //send vibrato 
    
    } 
 }
//repeat for any other switches
}
//=================================================================
void ReadAnalogs() {
  if (NotesChanged == 0) { // only read the analog pins if no notes are changing
    if ((millis() - LastADCTime) > DebounceTime) {//wait for 25 msec before taking next ADC reading
      LastADCTime = millis(); //update the timer
      ExpPed = !ExpPed; //flip the flag
      if (ExpPed == true) { // Read the pedal every other cycle
         if (NoPedal == false) { // a pedal is connected
          ExpPedVal = handleADC(ExpPedalCC, ExpPedVal, ExpPedPin);
         }
      }
      if (ExpPed != true) { // Read the drawbars every other cycle
       //drawbar and slider readings start here
       if (NoteCount < 5) { //don't read sliders if playing.  Could alter to 5 to reflect only one hand playing 
        Drawbars[DrawbarCount] = handleADC((DrawbarCC[DrawbarCount]),(Drawbars[DrawbarCount]),(DrawbarPin[DrawbarCount]));
        DrawbarCount++;
          if (DrawbarCount == DrawbarMax){
              DrawbarCount = 0; //roll back round to first slider
          }
       }
      }
    }
  }
}

//=================================================================
byte handleADC (byte CCVal, byte lastval, byte port) {
  int newvalue = analogRead(port);
  unsigned int divided = (unsigned int)newvalue >> 3; // go from 10 bits to 7
  byte newval = (byte)divided;
  byte delta =  (newval - lastval);
 if (abs(delta) > 12) { // do nothing unless over 10 percent change
    MIDI.sendControlChange(CCVal, newval, MidiChannel); //transmit
    return newval;
  }
  else {
  return lastval;
  }
}s
//=================================================================

Vox_Mega_midi_keyboard_USB_Serial.ino

Arduino
/*
//#include <MIDIUSB.h>
#include <MIDI.h>
//#include <analogReadFast.h>
MIDI_CREATE_DEFAULT_INSTANCE();

 */


/*
Program for MIDIfying a Vox Continental organ
(c) Prof David Musker, June 2020
Free for use if source is acknowledged

Intended use is:

- a 49-key single manual - can be changed with KeyboardSize

- 6 drawbars - can be changed with const byte DrawbarMax

- a volume (expression) pedal

- a vibrato switch

Generates:

- polyphonic MIDI note on and off messages on MidiChannel

- monophonic Low Note priority MIDI note on and off messages on LowMidiChannel

- monophonic High Note priority MIDI note on and off messages on HighMidiChannel

- MIDI CC messages based on the drawbars, volume pedal and vibrato switch

Can also generate square wave tones for one or other of the mono signals on one pin and gate on/off signals on another, for feeding to a modular synth


*/ 

//=================================================================

const byte DrawbarMax = 6;                                     //the total number of drawbars and sliders
const byte KeyboardSize = 49;                                  //size of keyboard here 49 keys

const byte ExpPedPin = A6;                                     //analog pedal input pin
const byte StartNotePin = 2;                                   //first digital input pin for note, leaving digital pin 1 for serial TX and 0 for RX or other use
const byte VibPin  = 53;                                       //digital input pin for the vibrato switch - first free pin after 2 for serial port and 49 for keys
const byte DrawbarPin [DrawbarMax] = {A3, A1, A2, A0, A5, A4}; //array of analogue ports, extend if more
const byte AudioOutPin  = 52;                                  //in case we want to send an audio output.  Need to connect via a DC-blocking capacitor.
const byte GatePin  = 51;                                      //in case we want to send an audio output, use as gate.  Need to connect via a 1K current limiting resistor

const byte NoteOff = B10000000;
const byte NoteOn  = B10010000;
const byte CCCmd   = B10110000;
const byte AllNotesOffCC = 123;
const byte VibCC = 1;                                          //Vibrato Midi CC, default to ComboV assignment
const byte ExpPedalCC = 11;                                    //ComboV swell pedal assignment
const byte DrawbarCC [DrawbarMax] = {12,13,14,15,16,17};       //ComboV drawbar/slider CCs, extend if more - try a value other than 17???? maybe 9

const byte MidiChannel = 0;                                    //set Midi channel value here to whatever
const byte LowMidiChannel = 1;                                 //set Midi channel value here to whatever
const byte HighMidiChannel = 2;                                //set Midi channel value here to whatever
const byte LowestNote = 24;                                    //Midi note number of lowest note, set to C1

const unsigned int freq[85] = {33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186}; //table of MIDI note frequencies for use by tone()
const byte KeyPin [KeyboardSize] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,29,28,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48};

//=================================================================

unsigned long KeyReadTime = 0;
unsigned long LastADCTime = 0;                                 //a time counter, since last analogRead 
unsigned long DebounceTime = 20;                               //20mS in real life for notes - might set lower

byte NoteCount = 0;                                            //notes on
byte NoteCountOld = 0;                                         //notes on at last scan
byte NotesChanged = 0;                                         //note changes from last scan
byte LowNote = 0;                                              //for generating a monophonic output
byte HighNote = 0;                                             //for generating a monophonic output
byte LowNoteOld = 0;                                           //for generating a monophonic output
byte HighNoteOld = 0;                                          //for generating a monophonic output
byte DrawbarCount = 0;                                         //a drawbar counter incrementing each read
byte ExpPedVal = 127;                                          //the volume level start on full volume
byte LastExpPedVal = 127;                                      //last volume, start on full volume
byte VibVal = 0;                                               //Code to send for vibrato CC initialise at off

bool VibState = HIGH;                                          //vibrato switch state
bool LastVibState = HIGH;                                      //previous switch state
bool ExpPed = false;                                           //an alternating boolean flag
bool NoPedal = false;                                          //a flag indicating whether an expression pedal is plugged in

bool KeyState[KeyboardSize] = {false};
bool NoteState[KeyboardSize] [3] = {false};                    //current and last note states and a change flag
unsigned long KeyLastTime [KeyboardSize] = {0};                //times since last key transitions
byte Drawbars [DrawbarMax] = {127};                            //drawbar ADC readings initialise at full

//=================================================================
void setup () {
Serial.begin(115200);//fastest rate for Hairless Serial - change if necessary
for (int i = 0; i < KeyboardSize; i++) {
  pinMode((StartNotePin+i), INPUT_PULLUP); // Digital input pins set to input pullup
}
pinMode(VibPin, INPUT_PULLUP); //ditto
pinMode(GatePin, OUTPUT); 
pinMode(AudioOutPin, OUTPUT);
pinMode(ExpPedPin, OUTPUT);
digitalWrite(ExpPedPin, LOW);
if (analogRead(ExpPedPin) == 0) {
  NoPedal = true;
}

delay(1000*60*4); // 4 minute delay to give the computer time to set up Hairless Midi etc, change if necessary

SwitchAllNotesOff(); // just in case anything hanging
}

//=================================================================
void loop () {

KeyRead();

NoteRead();

NoteSend();

SwitchOffCheck();  

AllOffCheck();

Mono();

ReadSwitches();

ReadAnalogs();

} //main loop ends here

//=================================================================
void KeyRead() {
KeyReadTime = millis();
  for (int i = 0; i < KeyboardSize; i++) { //note reading loop
    KeyState[i] = digitalRead(KeyPin[i]+StartNotePin); //indirect addressing via keypin array
  }
}

//=================================================================
void NoteRead() {
NoteCountOld = NoteCount;
NotesChanged = 0;
 for (int i = 0; i < KeyboardSize; i++ ) { 
  if ((KeyReadTime - KeyLastTime [i]) > DebounceTime) { // only look at key state if it is more than DebounceTime since last change in note otherwise skip
    KeyLastTime [i] = KeyReadTime;
    NoteState[i][3] = false;
    NoteState[i][2] = NoteState[i][1]; // update previous note state
    if (((KeyState[i] == false) && (NoteState[i][1] == true)) || ((KeyState[i] == true) && (NoteState[i][1] == false))){ //key state has changed
        NoteState[i][1] = KeyState[i]; //update current note state
        NoteState[i][3] = true; //set a changed state flag
        NotesChanged++;
        if (NoteState[i][1] == true) { // note is on
         NoteCount++; //increment number of notes on
         }
        else {NoteCount--;} //decrement number of notes on
    }    
   }
  }
}
/*
At the end of this loop, we have:
-an array of: 
NoteState[i][1] - current note states, 
NoteState[i][2] - previous note states, and 
NoteState[i][3] - change flags 
NoteCount - a count of the number of notes on
NotesChanged - a count of the number of notes changed 
*/

//=================================================================
void NoteSend() {
if (NotesChanged > 0) {//something has changed
    for (int i = 0; i < (KeyboardSize); i++) {
      if (NoteState[i][3] == true){//note state has changed
        if (NoteState[i][1] == true){//note is ON
          sendNoteOn((i+ LowestNote), MidiChannel);
          }
          else {//note is OFF
            sendNoteOff((i+ LowestNote), MidiChannel);}//note is OFF
            }
      }
   }
}

//=================================================================
void Mono() {
  if (NotesChanged > 0) {
    LowNoteOld = LowNote;
    LowNote = 0;
    HighNoteOld = HighNote;
    HighNote = 0;
    if (NoteCount > 0) { // at least one note is on
      FindLowNote();
      FindHighNote();
    }
    if (LowNote == 0) {
      noTone(AudioOutPin); //switch off audio out
      digitalWrite(GatePin, LOW); //Gate off

    }
    if ((LowNote != 0) && (LowNote != LowNoteOld)) {
      if (LowNoteOld != 0) {
        sendNoteOff(LowNoteOld, LowMidiChannel);
      }
      sendNoteOn(LowNote, LowMidiChannel);
      noTone(AudioOutPin); //switch off audio out
      tone(AudioOutPin, freq[LowNote-LowestNote]); //generate square wave from low note priority mono
      digitalWrite(GatePin, HIGH); // Gate on
    }
    
    if ((HighNote != 0) && (HighNote != HighNoteOld)) {
      if (HighNoteOld != 0) {
      sendNoteOff(HighNoteOld, HighMidiChannel);  
      }
      sendNoteOn(HighNote, HighMidiChannel);
      //could instead generate tone here??
    }  
  }
}

//=================================================================
void FindLowNote() {
  int i = 0;
    while (NoteState[i][1] == false){ //key is OFF
      i++;
    }
    LowNote = (i + LowestNote); //lowest Midi note played - could drop it an octave by subtracting another 12 here
  }

//=================================================================
void FindHighNote() {
    int i = KeyboardSize;
      while (NoteState[i-1][1] == false){ //key is OFF
        i--;
      }
          HighNote = ((i-1) + LowestNote); //lowest Midi note played
  } 

//=================================================================
void SwitchOffCheck() {
 if (NotesChanged > 0) {//something has changed
  if (NoteCount == 2) { //only 2 notes are on
    if ((NoteState[0][1] == true) && (NoteState[(KeyboardSize-1)][1] == true)) { //the only 2 notes ON are the top and the bottom
      sendNoteOn(0, HighMidiChannel); //send a signal to enable Cantabile to switch off computer
      delay(5000);//pause so nothing else is sent until Cantabile closes
    }
  }
}
}

//=================================================================
void AllOffCheck() {
  if ((NoteCount == 0) && (NoteCountOld > 0)) { //All keys now turned off for the first time
    noTone(AudioOutPin); //switch off audio out
    SwitchAllNotesOff();
  }
}

//=================================================================
void SwitchAllNotesOff() {
  sendControlChange(AllNotesOffCC, 0, MidiChannel); //send all notes off message 
  //sendControlChange(AllNotesOffCC, 0, LowMidiChannel);  //send all notes off message - put this in here if you take out the send from the sendControlChange() function
  //sendControlChange(AllNotesOffCC, 0, HighMidiChannel); //send all notes off message - put this in here if you take out the send from the sendControlChange() function 
  }

//=================================================================
void sendNoteOn (byte Pitch, byte MidiChannel) {
  byte Velocity = 127;
  MIDImessage(NoteOn, MidiChannel, Pitch, Velocity);
}

//=================================================================
void sendNoteOff (byte Pitch, byte MidiChannel) {
  byte Velocity = 0;
  MIDImessage(NoteOff, MidiChannel, Pitch, Velocity);
}

//================================================================================================
void ReadSwitches() { //read switches
  if ((millis() - LastADCTime) > DebounceTime) {//wait for 25 msec to debounce
    LastVibState = VibState; // update old vibrato state
    VibState = digitalRead(VibPin); // update vibrato state
    if (VibState != LastVibState) {
      if (VibState == LOW) { 
        VibVal = 127;
        }
      else { 
        VibVal = 0;
        }
    sendControlChange(VibCC, VibVal, MidiChannel); //send vibrato 
    
    } 
 }
//repeat for any other switches
}
//=================================================================
void ReadAnalogs() {
  if (NotesChanged == 0) { // only read the analog pins if no notes are changing
    if ((millis() - LastADCTime) > DebounceTime) {//wait for 25 msec before taking next ADC reading
      LastADCTime = millis(); //update the timer
      ExpPed = !ExpPed; //flip the flag
      if (ExpPed == true) { // Read the pedal every other cycle
         if (NoPedal == false) { // a pedal is connected
          ExpPedVal = handleADC(ExpPedalCC, ExpPedVal, ExpPedPin);
         }
      }
      if (ExpPed != true) { // Read the drawbars every other cycle
       //drawbar and slider readings start here
       if (NoteCount < 5) { //don't read sliders if playing.  Could alter to 5 to reflect only one hand playing 
        Drawbars[DrawbarCount] = handleADC((DrawbarCC[DrawbarCount]),(Drawbars[DrawbarCount]),(DrawbarPin[DrawbarCount]));
        DrawbarCount++;
          if (DrawbarCount == DrawbarMax){
              DrawbarCount = 0; //roll back round to first slider
          }
       }
      }
    }
  }
}

//=================================================================
byte handleADC (byte CCVal, byte lastval, byte port) {
  int newvalue = analogRead(port);
  unsigned int divided = (unsigned int)newvalue >> 3; // go from 10 bits to 7
  byte newval = (byte)divided;
  byte delta =  (newval - lastval);
 if (abs(delta) > 12) { // do nothing unless over 10 percent change
    sendControlChange(CCVal, newval, MidiChannel); //transmit
    return newval;
  }
  else {
  return lastval;
  }
}

//=================================================================
void sendControlChange (byte Cmd, byte Value, byte Midichannel) {
  MIDImessage(CCCmd, Midichannel, Cmd, Value);
  MIDImessage(CCCmd, (LowMidiChannel), Cmd, Value);  //to deal with a known issue which sometimes makes Hairless Serial Midi crash - any channel except MidiChannel will do
  MIDImessage(CCCmd, (HighMidiChannel), Cmd, Value); //to deal with a known issue which sometimes makes Hairless Serial Midi crash - any channel except the previous 2 will do - this one possible unneccessary 
}  

//=================================================================
void MIDImessage(byte byte1, byte byte2, byte byte3, byte byte4) {
  byte Cmd = (byte1 + byte2);
  Serial.write(Cmd);//send note on or note off command 
  Serial.write(byte3);//send pitch data
  Serial.write(byte4);//send velocity data
}

//=================================================================

Windows Shutdown Script

BatchFile
This script shuts down Cantabile and then Windows. It is called from Cantabile on arrival of a Midi message sent by the Arduino. That allows a nice clean shutdown.
@echo off 
Taskkill /im Cantabile.exe
TIMEOUT /T 5 /NOBREAK
shutdown.exe /f /p

Credits

dmusker
4 projects • 3 followers
Contact

Comments

Please log in or sign up to comment.