Hi, I'm working on a sketch for a MIDI A-B box that connects to a DSP guitar amplifier modeler. It sends one of two MIDI program change numbers depending on which switch is activated. I want to add the ability to store program change numbers read back from the modeler, but only having two buttons to work with I needed a way to detect a long press on the buttons to initiate the store process. I found a good tutorial on YouTube and the poster (Bas on Tech) had a sketch available so I used it.
The problem I'm having is that something in the code is blocking the section of the sketch that reads the incoming MIDI data. I have used the same exact code in another much simpler sketch, and it sends the incoming MIDI data to the serial monitor like it should and allows me to save the new data to a variable.
I don't see anything in the sketch that should cause the problem, but I'm by no means an Arduino expert. I would appreciate having another set of eyes look the code over.
The problem area starts at line number 75. Thanks in advance, Mark
// midi_ab-box_G
// machambers 4/25/2025
// button timing code from Bas van Dijk
// for Arduino UNO w/ Sparkfun MIDI Shield
// adding program change # storage; press & hold to store
// added MIDI Transport information, still doesn't store incoming data
// changed all "program" variables to type byte
// cut sketch in half to make it easier to troubleshoot
#include <MIDI.h>
#include <SoftwareSerial.h>
SoftwareSerial softSerial(8, 9); // RX, TX
using Transport = MIDI_NAMESPACE::SerialMIDI<SoftwareSerial>;
SoftwareSerial softSerial2 = SoftwareSerial(10, 11); // RX, TX
Transport serialMIDI(softSerial2);
MIDI_NAMESPACE::MidiInterface<Transport> MIDI((Transport&)serialMIDI);
MIDI_CREATE_INSTANCE(SoftwareSerial, softSerial, midiA); // TX
MIDI_CREATE_INSTANCE(SoftwareSerial, softSerial2, midiB); // RX
static const int buttonPinA = 4; // Rig A, left button pin
static const int redLED = 7; // red LED pin
static const int greenLED = 6; // green LED pin
static const int yellowLED = 5; // green LED pin
int buttonStatePreviousA = HIGH; // previous state of left button
unsigned long minButtonLongPressDuration = 2000; // Time we wait before we see the press as a long press
unsigned long buttonLongPressMillisA; // Time in ms when left button was pressed
bool buttonStateLongPressA = false; // True if it is a long press
const int intervalButton = 50; // Time between two readings of the button state
unsigned long previousButtonMillisA; // Timestamp of the latest reading
unsigned long buttonPressDurationA; // Time button A is pressed in ms
unsigned long currentMillis; // Variable to store the number of mS since the Arduino was started
byte programA = 18;
byte program;
void setup() {
Serial.begin(9600); // for debugging
pinMode(buttonPinA, INPUT_PULLUP); // set left button pin as input
pinMode(redLED, OUTPUT);
pinMode(greenLED, OUTPUT);
pinMode(yellowLED, OUTPUT);
digitalWrite(redLED, HIGH);
digitalWrite(greenLED, HIGH);
digitalWrite(yellowLED, HIGH);
// Serial.println("Press a button");
midiA.begin(MIDI_CHANNEL_OMNI); // listen on all channels
midiB.begin(MIDI_CHANNEL_OMNI); // listen on all channels
}
void readButtonStateA() {
// If the difference in time between the previous reading is larger than intervalButton
if (currentMillis - previousButtonMillisA > intervalButton) {
// Read the digital value of the button (LOW/HIGH)
int buttonStateA = digitalRead(buttonPinA);
// If the button has been pushed AND
// If the button wasn't pressed before AND
// If there was not already a measurement running to determine how long the button has been pressed
if (buttonStateA == LOW && buttonStatePreviousA == HIGH && !buttonStateLongPressA) {
buttonLongPressMillisA = currentMillis;
buttonStatePreviousA = LOW;
Serial.println("Button A pressed");
}
// Calculate how long the button has been pressed
buttonPressDurationA = currentMillis - buttonLongPressMillisA;
// If the button is pressed AND
// If there is no measurement running to determine how long the button is pressed AND
// If the time the button has been pressed is larger or equal to the time needed for a long press
if (buttonStateA == LOW && !buttonStateLongPressA && buttonPressDurationA >= minButtonLongPressDuration) {
buttonStateLongPressA = true;
digitalWrite(yellowLED, HIGH);
digitalWrite(greenLED, LOW);
Serial.println("Button A long pressed");
// the section below only works randomly
if (midiB.read()) {
byte type = midiB.getType();
if (type == 0xC0) { // Check for Program Change (0xC0 = 192 decimal)
programA = midiB.getData1();
Serial.print("Program Change: Program=");
Serial.println(programA);
}
}
}
// If the button is released AND
// If the button was pressed before
if (buttonStateA == HIGH && buttonStatePreviousA == LOW) {
buttonStatePreviousA = HIGH;
buttonStateLongPressA = false;
Serial.println("Button A released");
// If there is no measurement running to determine how long the button was pressed AND
// If the time the button has been pressed is smaller than the minimal time needed for a long press
if (buttonPressDurationA < minButtonLongPressDuration) {
digitalWrite(yellowLED, HIGH);
digitalWrite(greenLED, LOW);
Serial.println("Button A short pressed");
midiA.sendProgramChange(programA, 1);
}
}
// store the current timestamp in previousButtonMillis
previousButtonMillisA = currentMillis;
}
}
void loop() {
currentMillis = millis(); // store the current time
readButtonStateA(); // read the A button state
}
How frequently does your loop loop - you're pumping lots of serial out to Monitor, is it possible you've filled the queue and are blocking? Push your serial rate to 115200, see if the issue goes away. When you do that, expect Serial Monitor to scroll like crazy; other changes will be needed, this is just a test.
And yes, as observed, two software serial read activities will not work simultaneously. They can work, if one can control both sources so that listening to one only can reliably run to completion, before querying the other.
(edit, added clarification about simultaneity).
Two instances of software serial ports work just fine. I've built a number of MIDI controllers with the second port being used as an input only; a MIDI extension cord if you will. The first port is output only. One box has two software serial MIDI ports and the hardware UART is used to receive RS-232. All work with no issues. The story might be different if I was sending and receiving continuous data or clock streams, but I'm not, they just send and receive program changes that occur one every couple of minutes.
I'm using an UNO with a Sparkfun MIDI shield; like it says in the fourth line of the sketch.
I haven't timed the loop, but the DSP modeler only sends program change data when a new memory location is chosen. In normal use that's once every couple of minutes. If one spins the rotary encoder used to select presets the DSP box sends nothing until the encoder stops turning. Changing to 115200 made no change in behavior.
I'm not making two serial reads; one port reads MIDI, the other port write MIDI.
As I said in my original post, I'm using the MIDI read block of code in an earlier, simpler sketch and it works as it should. See below.
// midi_ab-box_C3
// machambers 4/25/2025
// for Arduino UNO w/ Sparkfun MIDI Shield
// added the transport information per AltPinSerial example
// adding code to use the 3rd button to store PC# to see if there is an issue using a long button press
// sketch works; it stores the incoming program change to programB
// modified the sketch to store to programA or programB; sketch works
// needs state-change debouncing
#include <MIDI.h>
#include <SoftwareSerial.h>
SoftwareSerial softSerial(8, 9);
using Transport = MIDI_NAMESPACE::SerialMIDI<SoftwareSerial>;
SoftwareSerial softSerial2 = SoftwareSerial(10, 11);
Transport serialMIDI(softSerial2);
MIDI_NAMESPACE::MidiInterface<Transport> MIDI((Transport&)serialMIDI);
MIDI_CREATE_INSTANCE(SoftwareSerial, softSerial, midiA); // TX
MIDI_CREATE_INSTANCE(SoftwareSerial, softSerial2, midiB); // RX
byte ledPin[] = {5, 6, 7}; // list of led pins
int Status = 1; // LEDs off enable
byte programA = 18;
byte programB = 76;
byte program;
void setup() {
pinMode(4, INPUT_PULLUP); // Rig A, left switch
pinMode(3, INPUT_PULLUP); // store, middle switch
pinMode(2, INPUT_PULLUP); // Rig B, right switch
pinMode(5, OUTPUT); // yellow LED
pinMode(6, OUTPUT); // green LED
pinMode(7, OUTPUT); // red LED
Serial.begin(9600); // for debugging
midiA.begin(MIDI_CHANNEL_OMNI); // listen on all channels
midiB.begin(MIDI_CHANNEL_OMNI); // listen on all channels
}
void ledOffAll() {
for (int i = 0 ; i < sizeof(ledPin); i++) {
digitalWrite(ledPin[i], HIGH);
}
}
void loop() {
if (Status == 1) {
ledOffAll();
Status = 0;
}
if (digitalRead(4) == LOW) {
ledOffAll();
digitalWrite(6, LOW);
midiA.sendProgramChange(programA, 1); // E3 (JCM-800)
Serial.println(programA);
delay (200);
}
if (digitalRead(2) == LOW) {
ledOffAll();
digitalWrite(5, LOW);
midiA.sendProgramChange(programB, 1); // T1 (modified rig)
Serial.println(programB);
delay (200);
}
if (midiB.read()) {
byte type = midiB.getType();
if (type == 0xC0) { // Check for Program Change (OxC0 = 192 decimal)
byte program = midiB.getData1();
// Serial.println(midiB.getData1());
Serial.print("Program Change: Program=");
Serial.println(program);
}
}
if (digitalRead(3) == LOW && digitalRead(4) == LOW) {
digitalWrite(7, LOW);
programA = midiB.getData1();
delay (200);
}
if (digitalRead(3) == LOW && digitalRead(2) == LOW) {
digitalWrite(7, LOW);
programB = midiB.getData1();
delay (200);
}
}
I don't know for sure, because I avoid software Serial, but I would worry that such a delay might interfere with software serial reception. Is the delay necessary, or could you use a millis() style timed response rather than delay()? For example, how large is the incoming serial buffer, and could it overflow in 200 ms?
I suggest @machambers to use a real debouncing delay-less algorithm, easy, or a button library even easier.
I like madleech's Button,
but make sure to use the constructor that lets you specify the debounce time instead of the default.
ezButton e z Button doesn't suck, but you must set a debounce time, and you must call its service method faster than the debounce period.
20 milliseconds is usually sufficient; both libraries use the fastest possible espresso se to a button press rather than waiting through the debounce interval.
@alto777 Well, the OP is clearly not our typical novice, so I'm trying to lead them to water, not throw them into the whirlpool, nor solve the whole thing for them as often happens here. In other words, trying to get the OP to think about the process.
But yes, they're debounces, and the 200 ms time is totally unnecessary, and is very likely causing software Serial problems, or other problems. MIDI runs at 31250 baud, which is at the very upper end of what is usually considered to be acceptable for software Serial, and at that rate, 3 characters per ms is possible; in a 200 ms delay, that means 600 characters could pile up, presuming MIDI has something to say, so I would suggest there's a high likelihood of pileup. But, I'm no MIDI expert.
Yes, it is the lazy programmers way to badly deal with button debouncing; note the last line of the comments in the midi_ab-box_C3 sketch I uploaded, where it says, "needs state-change debouncing".
Sadly, the ezButton library you suggested doesn't support button timing (press for X mS) so it's of limited use in my current project.
I suggested nothing of the sort, that was someone else. However, you might consider just dropping the 200 to 20, to see if life on the MIDI side improves.
Perhaps a path to success runs through solving whatever issues you are having with the sketch that show up when you try for a long press functionality.
Those issues may not be related to getting a long press to work.Trying has just exposed weaknesses in the original sketch.
That was why I created my original post.
The long press detection works; "Button A long pressed" appears in the serial monitor. The code after that doesn't run/work.
I've emailed the creator of the timing code, but have not received a reply.