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);
}