// Scan Ahlborn organ manuals and thumb pistons with Arduino Leonardo board // Code is specific to Ahlborn 61-note manuals with on-board 74138 TTL 3 to 8 way decoder used for row addressing // Using MIDIUSB library v.1.0.5 from Gary Grewal (install from the Arduino IDE Library Manaager) // John Harvey 3 March 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 // THE CODE IS LAID OUT SIMPLY AND SEQUENTIALLY TO SHOW THE PROGRAM FLOW. // THE COMPILED BINARY COULD BE REDUCED IN SIZE BY BREAKING UP THE CODE // INTO FUNCTIONS TO AVOID REPETITIVE BLOCKS BUT IN THIS SIMPLE APPLICATION // ONLY A SMALL PART OF THE ARDUINO PROGRAM STORAGE SPACE IS USED. // Also keeping code inline rather than in separate functions keeps scan time // to a minimum - function calls add to overall processing 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 byte SwellMidiChannel = 1; // The channels assigned to keyboards and pistons; channels are zero-based in code, so channel 1 is 0 etc. const byte GreatMidiChannel = 2; const byte PistonMidiChannel = 4; #include // Arduino input pins to read keyboard matrix columns const byte Column1 = 0; const byte Column2 = 1; const byte Column3 = 2; const byte Column4 = 3; const byte Column5 = 4; const byte Column6 = 5; const byte Column7 = 6; const byte Column8 = 7; // Arduino output pins to drive 74138 A0-2 const byte RowSelect0 = 8; const byte RowSelect1 = 9; const byte RowSelect2 = 10; // Arduino output pins, low to select manual const byte SelectSwell = 11; const byte SelectGreat = 12; // Arduino input/output pins to interface to CD4014 shift registers scanning thumb piston contacts const byte PS1 = A0; // Parallel/serial select output const byte CK1 = A1; // Clock output const byte SR1 = A2; // Serial data in from 16-bit shift register (2 x CD4014) const byte ScopeSync = A5; // Trigger oscilloscope at start of loop // Create arrays to remember key states, true = key on, false = key off boolean SwellKeyState[64]; // Only 61 keys in full 5 octave keyboard (and some have fewer keys) but boolean SwellKeyState1[64]; // zero-based row/column scan of 8x8 keyboard matrix goes from 0 to 63 boolean SwellKeyState2[64]; boolean GreatKeyState[64]; boolean GreatKeyState1[64]; boolean GreatKeyState2[64]; //boolean SwellPreviousKeyState; //boolean GreatPreviousKeyState; boolean PistonState[32]; // 0 is leftmost piston; true = piston pressed (Zero-based addressing 0-31) boolean CurrentPistonState; boolean PreviousPistonState; // Thumb piston assignments for Hauptwerk St. Anne organ // SWELL // 0 Swell Crescendo // 1 Great Crescendo // 2 Pedal Crescendo // 3 1 // 4 2 // 5 3 // 6 4 // 7 5 // 8 6 // 9 7 // 10 8 // 11 9 // 12 Pedal Bass // 13 Swell Melody // 14 General PP // 15 General FF // GREAT // 16 Set // 17 Swell to Great // 18 Swell to Pedal // 19 Great to Pedal // 20 1 // 21 2 // 22 3 // 23 4 // 24 5 // 25 6 // 26 7 // 27 8 // 28 9 // 29 Stepper - // 30 Stepper + // 31 General Cancel void setup() { pinMode (Column1, INPUT_PULLUP); pinMode (Column2, INPUT_PULLUP); pinMode (Column3, INPUT_PULLUP); pinMode (Column4, INPUT_PULLUP); pinMode (Column5, INPUT_PULLUP); pinMode (Column6, INPUT_PULLUP); pinMode (Column7, INPUT_PULLUP); pinMode (Column8, INPUT_PULLUP); pinMode (RowSelect0, OUTPUT); // 74138 TTL 3 to 8 decoder row select pinMode (RowSelect1, OUTPUT); pinMode (RowSelect2, OUTPUT); digitalWrite(RowSelect0, LOW); digitalWrite(RowSelect1, LOW); digitalWrite(RowSelect2, LOW); pinMode (SelectSwell, OUTPUT); pinMode (SelectGreat, OUTPUT); digitalWrite(SelectSwell, HIGH); digitalWrite(SelectGreat, HIGH); pinMode(LED_BUILTIN, OUTPUT); // Pin 13 is LED_BUILTIN, use for keyboard bottom C and leftmost piston visual indication digitalWrite(LED_BUILTIN, LOW); // Set LED to off pinMode (PS1, OUTPUT); digitalWrite(PS1, LOW); pinMode (CK1, OUTPUT); digitalWrite(CK1, LOW); pinMode (SR1, INPUT_PULLUP); pinMode (A3, OUTPUT); // Set unused analog inputs as outputs so that stray noise does not trigger ADCs pinMode (A4, OUTPUT); pinMode (ScopeSync, OUTPUT); digitalWrite(ScopeSync, LOW); for (byte keyNumber = 0; keyNumber <= 63; keyNumber ++) { SwellKeyState[keyNumber] = false; // Initialise all notes off SwellKeyState1[keyNumber] = false; SwellKeyState2[keyNumber] = false; GreatKeyState[keyNumber] = false; GreatKeyState1[keyNumber] = false; GreatKeyState2[keyNumber] = false; } for (byte piston = 0; piston <= 31; piston ++) { PistonState[piston] = false; // Initialise all pistons off } } void loop() { // Loop Period 5.8ms (172 keyboard/piston scans per second) 16 pistons // Loop Period 6.95ms (144 keyboard/piston scans per second) 32 pistons digitalWrite(ScopeSync, HIGH); // Generate oscilloscope sync pulse at start of each keyboard scan for test purposes delayMicroseconds(20); digitalWrite(ScopeSync, LOW); // First read all notes into SwellKeyState1 and GreatKeyState1 arrays // Then read all notes into SwellKeyState2 and GreatKeyState2 arrays // Doing it in this sequence maximises time gap between first and // second reads of each key, maximising debounce effect // SCAN SWELL digitalWrite(SelectSwell, LOW); for (byte row = 0; row <= 7; row ++) { digitalWrite(RowSelect0, row & B00000001); digitalWrite(RowSelect1, row & B00000010); digitalWrite(RowSelect2, row & B00000100); delayMicroseconds(10); byte keyNumber; for (byte col = 0; col <= 7; col ++) { keyNumber = (row * 8) + col; SwellKeyState1[keyNumber] = !digitalRead(col); // Low = key on, so invert } } digitalWrite(SelectSwell, HIGH); // SCAN GREAT digitalWrite(SelectGreat, LOW); for (byte row = 0; row <= 7; row ++) { digitalWrite(RowSelect0, row & B00000001); digitalWrite(RowSelect1, row & B00000010); digitalWrite(RowSelect2, row & B00000100); delayMicroseconds(10); byte keyNumber; for (byte col = 0; col <= 7; col ++) { keyNumber = (row * 8) + col; GreatKeyState1[keyNumber] = !digitalRead(col); } } digitalWrite(SelectGreat, HIGH); // 2ms here results in an overall delay of 3.3ms between first and second read of each key switch // A greater delay improves effectiveness of debounce delay(2); // SCAN SWELL digitalWrite(SelectSwell, LOW); for (byte row = 0; row <= 7; row ++) { digitalWrite(RowSelect0, row & B00000001); digitalWrite(RowSelect1, row & B00000010); digitalWrite(RowSelect2, row & B00000100); delayMicroseconds(10); byte keyNumber; for (byte col = 0; col <= 7; col ++) { keyNumber = (row * 8) + col; SwellKeyState2[keyNumber] = !digitalRead(col); // Low = key on, so invert } } digitalWrite(SelectSwell, HIGH); // SCAN GREAT digitalWrite(SelectGreat, LOW); for (byte row = 0; row <= 7; row ++) { digitalWrite(RowSelect0, row & B00000001); digitalWrite(RowSelect1, row & B00000010); digitalWrite(RowSelect2, row & B00000100); delayMicroseconds(10); byte keyNumber; for (byte col = 0; col <= 7; col ++) { keyNumber = (row * 8) + col; GreatKeyState2[keyNumber] = !digitalRead(col); } } digitalWrite(SelectGreat, HIGH); // Now process collected key data, comparing time-separated data for each key boolean PreviousKeyState; for (byte keyNumber = 0; keyNumber <= 63; keyNumber ++) { PreviousKeyState = SwellKeyState[keyNumber]; // Note that "keyNumber + 0x24" etc. produces an int in compiler so reconvert to byte value otherwise compiler outputs a narrowing conversion warning if (PreviousKeyState == false && SwellKeyState1[keyNumber] == true && SwellKeyState2[keyNumber] == true) { // Note has just gone on SwellKeyState[keyNumber] = true; midiEventPacket_t noteOn = {0x09, byte(SwellMidiChannel - 1 + 0x90), byte(keyNumber + 0x24), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); if (keyNumber == 0) { digitalWrite(LED_BUILTIN, HIGH); // Turn Arduino onboard LED on (Bottom C visual indication) } } if (PreviousKeyState == true && SwellKeyState1[keyNumber] == false && SwellKeyState2[keyNumber] == false) { // Note has just gone off SwellKeyState[keyNumber] = false; midiEventPacket_t noteOn = {0x08, byte(SwellMidiChannel - 1 + 0x80), byte(keyNumber + 0x24), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); if (keyNumber == 0) { digitalWrite(LED_BUILTIN, LOW); // Turn Arduino onboard LED off } } } delayMicroseconds(10); for (byte keyNumber = 0; keyNumber <= 63; keyNumber ++) { PreviousKeyState = GreatKeyState[keyNumber]; if (PreviousKeyState == false && GreatKeyState1[keyNumber] == true && GreatKeyState2[keyNumber] == true) { // Note has just gone on GreatKeyState[keyNumber] = true; midiEventPacket_t noteOn = {0x09, byte(GreatMidiChannel - 1 + 0x90), byte(keyNumber + 0x24), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); if (keyNumber == 0) { digitalWrite(LED_BUILTIN, HIGH); } } if (PreviousKeyState == true && GreatKeyState1[keyNumber] == false && GreatKeyState2[keyNumber] == false) { // Note has just gone off GreatKeyState[keyNumber] = false; midiEventPacket_t noteOn = {0x08, byte(GreatMidiChannel - 1 + 0x80), byte(keyNumber + 0x24), 0x40}; MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); if (keyNumber == 0) { digitalWrite(LED_BUILTIN, LOW); } } } delayMicroseconds(10); // ========================================================================================== // SCAN 32 THUMB PISTONS (TRY NO DEBOUNCE FIRST - SEEMS TO BE OK WITH CHEAP CHINESE SWITCHES) // ========================================================================================== // 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(PS1, HIGH); delayMicroseconds(20); digitalWrite(CK1, HIGH); delayMicroseconds(20); digitalWrite(CK1, LOW); delayMicroseconds(20); digitalWrite(PS1, LOW); delayMicroseconds(20); // First get contact states for all pistons for (byte piston = 0; piston <= 31; piston ++) { PreviousPistonState = PistonState[piston]; CurrentPistonState = !digitalRead(SR1); // 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(PistonMidiChannel - 1 + 0x90), byte(piston + 1), 0x40}; // Output MIDI range 001-016 MidiUSB.sendMIDI(noteOn); MidiUSB.flush(); if (piston == 0) { digitalWrite(LED_BUILTIN, HIGH); // Turn Arduino onboard LED off } } 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(PistonMidiChannel - 1 + 0x80), byte(piston + 1), 0x40}; MidiUSB.sendMIDI(noteOff); MidiUSB.flush(); if (piston == 0) { digitalWrite(LED_BUILTIN, LOW); // Turn Arduino onboard LED off } } delayMicroseconds(20); if (piston < 31) { // Last shift not needed digitalWrite(CK1, HIGH); delayMicroseconds(20); digitalWrite(CK1, LOW); delayMicroseconds(20); } } delayMicroseconds(50); }