// Scan Eminent Rocker Tab Stops and Thumb Pistons with Arduino Leonardo board // Using MIDIUSB library v.1.0.5 from Gary Grewal (install from the Arduino IDE Library Manaager) // John Harvey 21 January 2022 // www.johnharvey.uk // See these for more information: // www.arduino.cc/en/Reference/MIDIUSB // www.arduino.cc/en/Tutorial/MidiDevice // github.com/arduino-libraries/MIDIUSB // github.com/arduino/tutorials/blob/master/ArduinoZeroMidi/ArduinoZeroMidi.ino // Note that since the rocker tab stops have separate on and off contacts there is no need to use software debounce even if the contacts are poor // Inherent in the rocker design the on and off contats cannot be closed at the same time // MIDI channels allocated as follows: // Channel 01 (0x00) Swell // Channel 02 (0x01) Great // Channel 03 (0x02) Pedal // Channel 04 (0x03) Rocker tab stops and thumb pistons const int MidiChannel = 4; #include // Arduino input/output pins to interface to CD4014 shift registers scanning rocker tab contacts (16x2 diode matrix) const byte SR1 = 0; // Serial data in from 16-bit shift register (2 x 4014) const byte CK1 = 1; // Clock output const byte PS1 = 2; // Parallel/serial select output const byte PRow1 = 3; // Select pedal matrix row 1 (on and off contacts for all 7 pedal stops are in row 1) const byte SRow1 = 4; // Select swell matrix row 1 const byte SRow2 = 5; // Select swell matrix row 2 const byte GRow1 = 6; // Select great matrix row 1 const byte GRow2 = 7; // Select great matrix row 2 // Arduino input/output pins to interface to CD4014 shift registers reading 13 thumb piston contacts const byte PS2 = 8; // Parallel/serial select output const byte CK2 = 9; // Clock output const byte SR2 = 10; // Serial data in from 16-bit shift register (2 x CD4014) // Arduino input/output pins to interface to TPIC6C595 const byte CLR = A0; // Clear input shift register const byte Sin = A1; // Serial data in const byte SRCK = A2; // Shift register clock const byte RCK = A3; // Register clock const byte MidiRxSync = A4; // Set Hi for duration of MidiUSB.read() processing const byte ScopeSync = A5; // Trigger oscilloscope at start of loop boolean StopState[29]; // 29 stops with on and off contacts and LED indication; true = stop selected (Zero-based addressed as 0-28) // Stop state data stored as follows: // StopState[0] Leftmost of 7 pedal stops // to // StopState[6] Rightmost of 7 pedal stops // StopState[7] Leftmost of 11 swell stops // to // StopState[17] Rightmost of 11 swell stops // StopState[18] Leftmost of 11 great stops // to // StopState[28] Rightmost of 11 great stops boolean CurrentStopState; boolean PreviousStopState; boolean PistonState[13]; // 0 is leftmost piston; true = piston pressed (Zero-based, addressed as 0-12) boolean CurrentPistonState; boolean PreviousPistonState; void setup() { // SENSE ROCKER TAB STOPS pinMode (SR1, INPUT_PULLUP); pinMode (CK1, OUTPUT); digitalWrite(CK1, LOW); pinMode (PS1, OUTPUT); digitalWrite(PS1, LOW); pinMode (PRow1, OUTPUT); digitalWrite(PRow1, HIGH); pinMode (SRow1, OUTPUT); digitalWrite(SRow1, HIGH); pinMode (SRow2, OUTPUT); digitalWrite(SRow2, HIGH); pinMode (GRow1, OUTPUT); digitalWrite(GRow1, HIGH); pinMode (GRow2, OUTPUT); digitalWrite(GRow2, HIGH); // DRIVE LEDS pinMode (CLR, OUTPUT); digitalWrite(CLR, HIGH); // Normally high, low to clear pinMode (Sin, OUTPUT); digitalWrite(Sin, LOW); pinMode (SRCK, OUTPUT); digitalWrite(RCK, LOW); pinMode (RCK, OUTPUT); digitalWrite(RCK, LOW); // SENSE THUMB PISTONS pinMode (SR2, INPUT_PULLUP); pinMode (CK2, OUTPUT); digitalWrite(CK2, LOW); pinMode (PS2, OUTPUT); digitalWrite(PS2, LOW); for (byte stop = 0; stop <= 28; stop ++) { // 'stop' is not a reserved word even though it is highlighted in red StopState[stop] = false; // Initialise all stops off } for (byte piston = 0; piston <= 12; piston ++) { PistonState[piston] = false; // Initialise all pistons off } pinMode(LED_BUILTIN, OUTPUT); // Pin 13 is LED_BUILTIN, use for leftmost pedal rocker tab operation visual indication digitalWrite(LED_BUILTIN, LOW); // Set LED to off pinMode (MidiRxSync, OUTPUT); // See when MidiUSB.read() is processing digitalWrite(MidiRxSync, LOW); pinMode (ScopeSync, OUTPUT); // Outputs trigger pulse for oscilloscope at start of each loop digitalWrite(ScopeSync, LOW); } void loop() { // Idle loop period is 19.3 ms digitalWrite(ScopeSync, HIGH); delayMicroseconds(50); digitalWrite(ScopeSync, LOW); // ========================================================================================== // 7 PEDAL STOPS (all contained within first diode matrix row) // ========================================================================================== digitalWrite(PRow1, LOW); // Get state of all stops (parallel JAM into shift registers) digitalWrite(PS1, HIGH); // Enable parallel data entry delayMicroseconds(50); digitalWrite(CK1, HIGH); // Grabs data on rising edge of clock delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); digitalWrite(PS1, LOW); // Restore serial data entry delayMicroseconds(50); // First get on contact states for all stops for (byte stop = 0; stop <= 6; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); // Low = contact closed so invert; first stop is already available without shifting, so read first then shift // Note that 'stop + 1' produces an int in compiler so reconvert to byte value otherwise compiler outputs a narrowing conversion warning if ((PreviousStopState == false) && (CurrentStopState == true)) { // Stop on has just been pressed StopState[stop] = true; midiEventPacket_t noteOn = {0x09, byte(MidiChannel - 1 + 0x90), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); if (stop == 0) { digitalWrite(LED_BUILTIN, HIGH); // Turn Arduino onboard LED on (Leftmost pedal stop visual indication) } } delayMicroseconds(50); digitalWrite(CK1, HIGH); // Shifts data on rising edge of clock delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } digitalWrite(CK1, HIGH); // Skip 8th non-existent pedal on contact delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); // Then get off contact states for all stops for (byte stop = 0; stop <= 6; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == true) && (CurrentStopState == true)) { // Stop off has just been pressed StopState[stop] = false; midiEventPacket_t noteOff = {0x08, byte(MidiChannel - 1 + 0x80), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); if (stop == 0) { digitalWrite(LED_BUILTIN, LOW); // Turn Arduino onboard LED off } } delayMicroseconds(50); if (stop < 6) { // Last shift not needed digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } } digitalWrite(PRow1, HIGH); delayMicroseconds(100); // ========================================================================================== // 1 to 8 of 11 SWELL STOPS // ========================================================================================== digitalWrite(SRow1, LOW); digitalWrite(PS1, HIGH); delayMicroseconds(100); digitalWrite(CK1, HIGH); delayMicroseconds(100); digitalWrite(CK1, LOW); delayMicroseconds(100); digitalWrite(PS1, LOW); delayMicroseconds(100); // First get on contact states for (byte stop = 7; stop <= 14; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == false) && (CurrentStopState == true)) { StopState[stop] = true; midiEventPacket_t noteOn = {0x09, byte(MidiChannel - 1 + 0x90), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); } delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } // Then get off contact states for (byte stop = 7; stop <= 14; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == true) && (CurrentStopState == true)) { StopState[stop] = false; midiEventPacket_t noteOff = {0x08, byte(MidiChannel - 1 + 0x80), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); } delayMicroseconds(50); if (stop < 14) { digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } } digitalWrite(SRow1, HIGH); delayMicroseconds(100); // ========================================================================================== // 9 to 11 of 11 SWELL STOPS // ========================================================================================== digitalWrite(SRow2, LOW); digitalWrite(PS1, HIGH); delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); digitalWrite(PS1, LOW); delayMicroseconds(50); // First get on contact states for (byte stop = 15; stop <= 17; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == false) && (CurrentStopState == true)) { StopState[stop] = true; midiEventPacket_t noteOn = {0x09, byte(MidiChannel - 1 + 0x90), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); } delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } for (byte n = 1; n <= 5; n ++) { digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } // Then get off contact states for (byte stop = 15; stop <= 17; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == true) && (CurrentStopState == true)) { StopState[stop] = false; midiEventPacket_t noteOff = {0x08, byte(MidiChannel - 1 + 0x80), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); } delayMicroseconds(50); if (stop < 17) { digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } } digitalWrite(SRow2, HIGH); delayMicroseconds(100); // ========================================================================================== // 1 to 8 of 11 GREAT STOPS // ========================================================================================== digitalWrite(GRow1, LOW); digitalWrite(PS1, HIGH); delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); digitalWrite(PS1, LOW); delayMicroseconds(50); // First get on contact states for (byte stop = 18; stop <= 25; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == false) && (CurrentStopState == true)) { StopState[stop] = true; midiEventPacket_t noteOn = {0x09, byte(MidiChannel - 1 + 0x90), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); } delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } // Then get off contact states for (byte stop = 18; stop <= 25; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == true) && (CurrentStopState == true)) { StopState[stop] = false; midiEventPacket_t noteOff = {0x08, byte(MidiChannel - 1 + 0x80), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); } delayMicroseconds(50); if (stop < 25) { digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } } digitalWrite(GRow1, HIGH); delayMicroseconds(100); // ========================================================================================== // 9 to 11 of 11 GREAT STOPS // ========================================================================================== digitalWrite(GRow2, LOW); digitalWrite(PS1, HIGH); delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); digitalWrite(PS1, LOW); delayMicroseconds(50); // First get on contact states for (byte stop = 26; stop <= 28; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == false) && (CurrentStopState == true)) { StopState[stop] = true; midiEventPacket_t noteOn = {0x09, byte(MidiChannel - 1 + 0x90), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); } delayMicroseconds(50); digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } for (byte n = 1; n <= 5; n ++) { // Skip 5 non-existent on contacts digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } // Then get off contact states for (byte stop = 26; stop <= 28; stop ++) { PreviousStopState = StopState[stop]; CurrentStopState = !digitalRead(SR1); if ((PreviousStopState == true) && (CurrentStopState == true)) { StopState[stop] = false; midiEventPacket_t noteOff = {0x08, byte(MidiChannel - 1 + 0x80), byte(stop + 1), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); } delayMicroseconds(50); if (stop < 28) { // Last shift not needed digitalWrite(CK1, HIGH); delayMicroseconds(50); digitalWrite(CK1, LOW); delayMicroseconds(50); } } digitalWrite(GRow2, HIGH); delayMicroseconds(100); // ========================================================================================== // GET STOP STATE MIDI MESSAGES FROM HAUPTWERK // ========================================================================================== digitalWrite(MidiRxSync, HIGH); // See when we are in MidiUSB.read() code section midiEventPacket_t rx = MidiUSB.read(); // Reads data from the USB and packages it into a MIDI packet (midiEventPacket_t struct) // midiEventPacket_t struct (4 bytes for USB) // 1st byte: header // 2nd byte: message type (0..7) and channel (0..15) // 3rd byte: id (0..127) // 4th byte: value (0..127) int midiHeader = rx.header; // First parameter is the event type (0x09 = note on, 0x08 = note off) int midiID = rx.byte1; // Second parameter is note-on/note-off, combined with the channel. Channel can be 0-15, typically reported to the user as 1-16. int midiValue = rx.byte2; // Third parameter is the note number (48 = middle C) int midiVelocity = rx.byte3; // Fourth parameter is the velocity (64 = normal, 127 = fastest) int channel = midiID & 0xF; if (channel == MidiChannel - 1) { if (midiHeader == 0x9) { StopState[midiValue - 1] = true; } if (midiHeader == 0x8) { StopState[midiValue - 1] = false; } } delayMicroseconds(50); digitalWrite(MidiRxSync, LOW); // ========================================================================================== // WRITE STOP STATE DATA FROM StopState[29] TO TPIC6C595 SHIFT REGISTERS TO DRIVE LEDs // ========================================================================================== // digitalWrite(CLR, LOW); // Clear shift registers (not needed, causes flicker) // delayMicroseconds(50); // digitalWrite(CLR, HIGH); // delayMicroseconds(50); for (byte stop = 29; stop >= 1; stop --) { digitalWrite(Sin, StopState[stop - 1]); delayMicroseconds(50); digitalWrite (SRCK, HIGH); // Shift data in delayMicroseconds(50); digitalWrite(SRCK, LOW); delayMicroseconds(50); } digitalWrite (RCK, HIGH); // Transfer data to output registers delayMicroseconds(50); digitalWrite(RCK, LOW); delayMicroseconds(50); // ========================================================================================== // SCAN 13 GREAT THUMB PISTONS // ========================================================================================== // Arduino input/output pins to interface to CD4014 shift registers reading 13 thumb piston contacts // PS2 = 8; Parallel/serial select // CK2 = 9; Clock // SR2 = 10; Serial data in from 16-bit shift register (2 x CD4014) // In Hauptwerk use Input: Momentary piston: MIDI note-on for pistons; reversible pistons will toggle // Get state of all pistons (parallel JAM into shift registers) digitalWrite(PS2, HIGH); delayMicroseconds(50); digitalWrite(CK2, HIGH); delayMicroseconds(50); digitalWrite(CK2, LOW); delayMicroseconds(50); digitalWrite(PS2, LOW); delayMicroseconds(50); // First get contact states for all pistons for (byte piston = 0; piston <= 12; piston ++) { PreviousPistonState = PistonState[piston]; CurrentPistonState = !digitalRead(SR2); // Low = contact closed so invert; first piston is already available without shifting, so read first then shift if ((PreviousPistonState == false) && (CurrentPistonState == true)) { // Piston has just been pressed PistonState[piston] = true; midiEventPacket_t noteOn = {0x09, byte(MidiChannel - 1 + 0x90), byte(piston + 30), 0x40}; // Output MIDI range 030-042 MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); } if ((PreviousPistonState == true) && (CurrentPistonState == false)) { // Piston has just been released PistonState[piston] = false; // noteOff code only for testing, not needed by Hauptwerk midiEventPacket_t noteOff = {0x08, byte(MidiChannel - 1 + 0x80), byte(piston + 30), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); } delayMicroseconds(50); if (piston < 12) { // Last shift not needed digitalWrite(CK2, HIGH); delayMicroseconds(50); digitalWrite(CK2, LOW); delayMicroseconds(50); } } delayMicroseconds(50); }