Combination Action

/*

SAM Interface Control Module:

Note: This is an updated version (Mar 13) that incorporates provision for pistons and daisy chaining

==========================

This module scans 16 inputs from Stop Action Magnet switches and outputs MIDI messages on Channel 1,

via serial port 0. These data, when they arrive at the MIDI shield are output via the OUT connector

to the virtual organ. Messages coming from the virtual organ are received via the IN connector on the MIDI shield. This in

turn connects to the Receive pin of port 0.

The module also has 16 outputs which, via suitable drivers, operate the ON magnets. An additional 16 operate

the OFF magnets. When stop change messages arrive from the virtual organ, the module switches the state

of the SAM to correspond.

The module outputs turn ON/OFF pulses of typically one second duration (maximum pulse count: adjustable).

If the SAM switches state before this time expires, the pulse is shortened accordingly. The length of the

"follow-through" pulse, typically 30 milliseconds, may also be defined. With freely operating drawknobs,

the "follow-through" pulse won't be needed and the minimum pulse count may be set to one.

There are 16 additional inputs. These pins can be connected to pistons, to trigger combinations,

bank select, general cancel, etc. Pin 13, as usual is best not used. The attached LED makes it

unreliable as input.

The "note number" output is the pin number.

There is now provision for daisy chaining these modules. First assign each one a different channel number in this line:

const byte status9n = 0x90, status8n = 0x80; //Ch 1: for Ch2: use 0x91, 0x81 etc.

Then make sure the boards share a common ground and connect Tx0 and Rx0 on the first board to Rx1 and Tx1 resepectively

on the second board (the output board with the MIDI shield.

Pin assignments and scan order:

38 - 53 OFF magnets; out

22 - 37 ON magnets; out

54 - 69 stop switch inputs. Normally high using internal pullups. Switch to ground to activate.

The stop switch pin numbers are also the "note number" associated with that switch

2 - 17 Input pins connected to pistons

Equipment: Arduino Mega and MIDI shield

created 2019 Mar 7

modified 2019 Mar 13

by John Coenraads

*/

// Declarations==========================================

//In this partially recycled code, note number now refers to stop tablet or drawknob number

byte i, j, k; //old Fortran habit (yes, I did punched cards)

byte inputBit; //input data bit

byte statusBuffer; //keeps track of MIDI status during running status

const byte debounceCount = 4; //increase if switches are electrically noisy

const int maxPulseCount = 4000; //maximum number of scan cycles (~0.25 mS) magnet is pulsed on

const int minPulseCount = 300; //number of scan cycles for magnet pulse "follow through." Min value = 1

//Set channel number, for this module

const byte status9n = 0x90, status8n = 0x80; //Ch 1: for Ch2: use 0x91, 0x81 etc.

//the following arrays are excessive in size so that any pin number (0 to 69)can be used as index into array

byte switchOn [75]; //tracks which switches are on or off (both stop switches and pistons)

byte switchDebounce [75]; //switch ON if count reaches 4, OFF if count reaches 0

int onMagnetCount [75]; //length of time (scan cycles) before ON magnet coil loses power

int offMagnetCount [75]; //length of time (scan cycles) before OFF magnet coil loses power

//MIDI message Receive variables

byte noteStatusRx;

byte noteNumberRx;

byte noteVelocityRx;

//Initialize ======================================================

void setup()

{

// Set MIDI baud rate:

Serial.begin(31250);

Serial1.begin(31250);

//Clear serial input buffers

while (Serial.available() > 0) {

Serial.read();

}

while (Serial1.available() > 0) {

Serial1.read();

}

//Initialize 16 pins for input from switches. Normally high (via internal pullups)

for (i = 54; i <= 69; i++)

{

pinMode (i, INPUT_PULLUP);

}

//Initialize 16 pins for input from switches. Normally high (via internal pullups)

for (i = 2; i <= 17; i++)

{

pinMode (i, INPUT_PULLUP);

}

//Initialize 16 pins for output to ON magnets and turn off

for (i = 22; i <= 37; i++)

{

pinMode (i, OUTPUT);

digitalWrite (i, LOW);

}

//Initialize 16 pins for output to OFF magnets and turn off

for (i = 38; i <= 53; i++)

{

pinMode (i, OUTPUT);

digitalWrite (i, LOW);

}

//Initialize debounce count array to zero

for (i = 1; i <= 70; i++)

{

switchOn [i] = 0; //initialize all switches to Off

switchDebounce [i] = 0; //initialize debounce counts to zero

onMagnetCount [i] = 0; //length of time before ON magnet coil loses power

offMagnetCount [i] = 0; //length of time before OFF magnet coil loses power

}

}

//Main Loop ===========================================================

void loop()

{

scanInputs(); //Scan tab rail switches, output via port 0

if (Serial.available ()) //Process messages received from virtual organ via port 1

{

processSerialIn();

}

if (Serial1.available ()) //transmit data received via Port1 from preceding module

{

processSerialReceived();

}

checkMagnetsPulseCount (); //Check if ON/OFF magnets have switched or timed out and can be turned OFF

Serial.flush();

}

//Scan tab rail switches, convert to MIDI and output via port 0, channel 1

void scanInputs ()

{

for (i = 54; i <= 69; i++)

{

inputBit = digitalRead(i); //read bit

if (inputBit == HIGH) //bit HIGH, switch open

{

turnNoteOFF(i);

}

else //bit LOW, switch closed

{

turnNoteON(i);

}

}

for (i = 2; i <= 17; i++)

{

inputBit = digitalRead(i); //read bit

if (inputBit == HIGH) //bit HIGH, switch open

{

turnNoteOFF(i);

}

else //bit LOW, switch closed

{

turnNoteON(i);

}

}

}

//Turn note on. Debouncing is achieved by requiring that several turnNoteON requests

//are received before sending out noteOn MIDI message

void turnNoteON (byte i) //i is index into array

{

if (switchDebounce[i] < debounceCount)

{

switchDebounce[i] = switchDebounce[i] + 1;

if ((switchDebounce[i] == debounceCount) && ( !switchOn[i]))

{

Serial.write (status9n); //note ON, channel n,

Serial.write (i); //send out pin number as note number

Serial.write (0x7f); //medium velocity

switchOn[i] = 1; //note now ON

}

}

}

//Turn note off. Debouncing is achieved by requiring that several turnNoteOFF requests

//are received before sending out noteOff MIDI message

void turnNoteOFF (byte i) //i is index into array

{

if (switchDebounce[i] > 0)

{

switchDebounce[i] = switchDebounce[i] - 1;

if ((switchDebounce[i] == 0) && (switchOn[i]))

{

Serial.write (status9n); //note ON, channel n,

Serial.write (i); //send out pin number as note number

Serial.write (0); //zero velocity = turn OFF note

switchOn[i] = 0; //note now OFF

}

}

}

//Process data received for serial port 0: status, note number, velocity

void processSerialIn ()

{

noteStatusRx = Serial.read ();

//transmit all data received through Port 0 to next module via Port 1

//in subsequent statements

Serial1.write (noteStatusRx);

if (noteStatusRx > 0x7F) //is status byte

{

if ((noteStatusRx == status9n) || //channel n, note ON, OR

(noteStatusRx == status8n)) //channel n, note OFF

{

statusBuffer = noteStatusRx;

while (!Serial.available()) {} //wait for serial data, port 0

noteNumberRx = Serial.read ();

Serial1.write (noteNumberRx);

while (!Serial.available()) {} //wait for serial data, port 0

noteVelocityRx = Serial.read ();

Serial1.write (noteVelocityRx);

if (noteStatusRx == status8n) {

noteVelocityRx = 0;

}

processMessageReceived ();

}

else //not status9n or status8n

{

statusBuffer = 0;

}

}

else //is data byte

{

if (statusBuffer != 0)

{

noteNumberRx = noteStatusRx;

noteStatusRx = statusBuffer;

while (!Serial.available()) {} //wait for serial data

noteVelocityRx = Serial.read ();

Serial1.write (noteVelocityRx);

if (noteStatusRx == status8n) {

noteVelocityRx = 0;

}

processMessageReceived ();

}

}

}

void processMessageReceived ()

{

if ((noteVelocityRx) //non-zero note velocity

&& (!switchOn [noteNumberRx])) //AND switch not already ON

{

onMagnetCount [noteNumberRx - 32] = maxPulseCount; // start count down

digitalWrite (noteNumberRx - 32, HIGH); //turn ONmagnet ON

}

if ((!noteVelocityRx) //zero note velocity

&& (switchOn [noteNumberRx])) //AND switch not already OFF

{

offMagnetCount [noteNumberRx - 16] = maxPulseCount; //start count down

digitalWrite (noteNumberRx - 16, HIGH); //turn OFFmagnet ON

}

}

void checkMagnetsPulseCount () //count down for magnets pulse length

{

for (i = 22; i <= 37; i++)

{

if (onMagnetCount [i] > 0)

{

if ((onMagnetCount [i] > minPulseCount) && (switchOn [i + 32]))

{

onMagnetCount [i] = minPulseCount;

}

if (onMagnetCount [i] == 1)

{

digitalWrite (i, LOW);

}

onMagnetCount [i] = onMagnetCount [i] - 1;

}

}

for (i = 38; i <= 53; i++)

{

if (offMagnetCount [i] > 0)

{

if ((offMagnetCount [i] > minPulseCount) && (!switchOn [i + 16]))

{

offMagnetCount [i] = minPulseCount;

}

if (offMagnetCount [i] == 1)

{

digitalWrite (i, LOW);

}

offMagnetCount [i] = offMagnetCount [i] - 1;

}

}

}

//Process data received via serial port 1 (from preceding Arduino): status, note number, velocity

//Note: this bare bones version does not handle running status.

void processSerialReceived ()

{

noteStatusRx = Serial1.read ();

if (noteStatusRx > 0x7F) //is status byte

{

while (!Serial1.available()) {} //wait for serial data, port 1

noteNumberRx = Serial1.read ();

while (!Serial1.available()) {} //wait for serial data, port 1

noteVelocityRx = Serial1.read ();

Serial.write (noteStatusRx ); //output on port 0

Serial.write (noteNumberRx);

Serial.write (noteVelocityRx);

}

}

void trace (byte num, byte info) //used during debugging

{

Serial.write (0xB3);

Serial.write (num);

Serial.write (info);

}