Ghost notes when transmitting MIDI

Hello Arduino community, this is my first post on this forum as a beginner hobbyist. I am fairly new to electronics and programming in general so I do apologise for any inefficiencies/lack of understanding/etc.
I am currently building a synthesizer with a Teensy 4.1 for the synthesis itself, and a separate Arduino Nano for the piano "keyboard" module (which is intended to function similarly to a generic MIDI keyboard, albeit without velocity sensing.)
I am facing an issue currently regarding the transmission of MIDI data. The two boards are connected via the standard MIDI circuit through an optocoupler:

Callbacks from the MIDI library by FortySevenEffects are being used to send and receive messages. Messages are received fine by the Teensy with a simple test sketch sending a noteOn and then a noteOff after a second, but my actual keyboard layout (which uses multiplexers to read simple push buttons which are normally tied to GND with a 10k pull-down resistor - it would probably be better to use shift registers but I already had these on hand) causes "ghost notes" when buttons are released, where the Teensy receives several extra noteOn's. Here is the serial output:

Note On, ch=1, note=50
//I release the button
Note On, ch=1, note=115
Note On, ch=1, note=110
Note On, ch=1, note=105
Note On, ch=1, note=103
Note On, ch=1, note=110
Note On, ch=1, note=116
Note On, ch=1, note=32
Note On, ch=1, note=102
Note On, ch=1, note=58
Note On, ch=1, note=51
Note On, ch=1, note=13
Note On, ch=1, note=36
Note Off, ch=1, note=50
//Only now is the actual noteOff message received

Some of these ghost messages are even of notes greater than what my keyboard/code is capable of (notes 36-83.) Here is the schematic of a keyboard module (I have 4 daisy-chained together) and the Nano's sketch. The pads on the edge are wired to the Nano's pins accordingly to read each multiplexer's output.



I can't add attachments as a new user so here is the gist of the Nano's code.

void readButton(int state, int prev, int pitch) {
  if (prev + 128 < state || prev - 128 > state) {
    if (state > 512) {
      Serial.print("sending note on: ");
      Serial.print(state);
      Serial.print("\t");
      Serial.println(pitch);
      MIDI.sendNoteOn(pitch, 127, 1); //Send NoteOn with pitch, vel=127, channel=1
    } else {
      Serial.print("sending note off: ");
      Serial.print(state);
      Serial.print("\t");
      Serial.println(pitch);
      MIDI.sendNoteOff(pitch, 127, 1);
    }
  }
}

This is called sequentially in the loop like such:

  readButton(mux3A, mux3Aprev, 53);
  readButton(mux3B, mux3Bprev, 54);
  readButton(mux3C, mux3Cprev, 55);
//and so forth

Any amount of help would be greatly appreciated.

Firstly, sketches can/should be added to the text (using the CODE tags, like you did for this function), not as attachments, so please post the full code.

Second, I'm a little confused about your schematic.
I'm not sure if I understand correctly or if I'm mistaken, but the buttons you added are dual (i.e. pins 1 and 2 are a button separated from pins 3 and 4, effectively implementing a dual button). In that case, the pull-down resistor connected this way is unnecessary.
Anyway the pull-down resistors shoud be connected to the mux pins, and the buttons drawn here looks kinda wrong...
Standard mini pushbuttons are like:


so if for example the mux pin is connected to pin 4, the pull-down resistor should be connected to the same "side", i.e. pin 1 or viceversa.

I don't know if this is the cause of your ghost note problem, maybe not, but it's the first thing that came to mind (along with possible bounce issues that I'm leaving aside for now).

Any further considerations can be made as soon as we have the complete code available (and please attach the actual serial output, the example you showed us here does not correspond to the serial prints of that code snippet...).

I don't see anywhere in your code where you're actually reading anything. That's why you show the complete code. I could guess what you're doing, but that's a waste of time.

And if your schematic is correct, ask yourself what your mux inputs are connected to when the pushbuttons aren't being pressed. The answer is "they're floating" if your schematic is correct.

Welcome!
If your schematic is drawn correctly simply jump pins 2 and 4 on the switches, they are drawn as a 2PST (2 Pole Single Throw). At this point I am assuming the code is correct and it is debouncing the switches.

Thanks for all the comments everyone - the button symbols were drawn incorrectly (the buttons are in fact the standard buttons which docdoc pointed out, with pins on the same side connected together); I have edited the post to feature the correct symbols.

I did not want to post the entire Teensy sketch as it is well over 2000 lines long and incredibly unoptimised (I have never used a single programming language before.) But here are the important MIDI bits:

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <MIDI.h>
#include <MIDIUSB.h>
#include <EEPROMex.h>
MIDI_CREATE_DEFAULT_INSTANCE();

void setup() {
  analogReadResolution(10);
  Serial.begin(31250);
  AudioMemory(470);
    preset = 0; //////DEBUG
  //MIDI setup
   usbMIDI.begin();
   usbMIDI.setHandleNoteOn(myNoteOn);
   usbMIDI.setHandleNoteOff(myNoteOff);
   MIDI.setHandleNoteOn(myNoteOn);
   MIDI.setHandleNoteOff(myNoteOff);
   MIDI.begin(MIDI_CHANNEL_OMNI);
   MIDI.turnThruOff();

  //Pin Setup
    //pinMode(0, INPUT); //MIDI input
    pinMode(4, OUTPUT); //Mux 1A
    pinMode(5, OUTPUT); //Mux 1B
    pinMode(6, OUTPUT); //Mux 1C
    pinMode(8, OUTPUT); //Mux 2A
    pinMode(9, OUTPUT); //Mux 2B
    pinMode(10, OUTPUT); //Mux 2C
    pinMode(11, OUTPUT); //Mux 3A
    pinMode(12, OUTPUT); //Mux 3B
    pinMode(24, OUTPUT); //Mux 3C
    pinMode(25, OUTPUT); //Mux 4A
    pinMode(26, OUTPUT); //Mux 4B
    pinMode(27, OUTPUT); //Mux 4C
    pinMode(28, OUTPUT); //Mux 5A
    pinMode(29, OUTPUT); //Mux 5B
    pinMode(30, OUTPUT); //Mux 5C
    pinMode(A4, INPUT); //Mux 1 COM
    pinMode(A3, INPUT); //Mux 2 COM
    pinMode(A2, INPUT); //Mux 3 COM
    pinMode(A1, INPUT); //Mux 4 COM
    pinMode(A0, INPUT); //Mux 5 COM
    //pinMode(7, OUTPUT); //DIN OUTPUT
    //pinMode(20, OUTPUT); //WSel
    //pinMode(21, OUTPUT); //BClk

  //Mux setup init
    digitalWrite(4, 0);
    digitalWrite(5, 0);
    digitalWrite(6, 0);
    digitalWrite(8, 0);      
    digitalWrite(9, 0);
    digitalWrite(10, 0);
    digitalWrite(11, 0);
    digitalWrite(12, 0);
    digitalWrite(24, 0);
    digitalWrite(35, 0);
    digitalWrite(34, 0);
    digitalWrite(33, 0);
    digitalWrite(36, 0);
    digitalWrite(37, 0);
    digitalWrite(38, 0);

  //vco setup
    vcoA1.begin(vcoVol, 150, WAVEFORM_SAWTOOTH);
    vcoB1.begin(vcoVol, 150, WAVEFORM_SQUARE);    
    vcoC1.begin(vcoVol * 1.5, 150, WAVEFORM_ARBITRARY);
    sub1.begin(vcoVol * 1.5, 150, WAVEFORM_TRIANGLE);
    vcoA2.begin(vcoVol, 150, WAVEFORM_SAWTOOTH);
    vcoB2.begin(vcoVol, 150, WAVEFORM_SQUARE);    
    vcoC2.begin(vcoVol * 1.5, 150, WAVEFORM_ARBITRARY);
    sub2.begin(vcoVol * 1.5, 150, WAVEFORM_TRIANGLE);
    vcoA3.begin(vcoVol, 150, WAVEFORM_SAWTOOTH);
    vcoB3.begin(vcoVol, 150, WAVEFORM_SQUARE);    
    vcoC3.begin(vcoVol * 1.5, 150, WAVEFORM_ARBITRARY);
    sub3.begin(vcoVol * 1.5, 150, WAVEFORM_TRIANGLE);
    vcoA4.begin(vcoVol, 150, WAVEFORM_SAWTOOTH);
    vcoB4.begin(vcoVol, 150, WAVEFORM_SQUARE);    
    vcoC4.begin(vcoVol * 1.5, 150, WAVEFORM_ARBITRARY);
    sub4.begin(vcoVol * 1.5, 150, WAVEFORM_TRIANGLE);
    vcoA5.begin(vcoVol, 150, WAVEFORM_SAWTOOTH);
    vcoB5.begin(vcoVol, 150, WAVEFORM_SQUARE);    
    vcoC5.begin(vcoVol * 1.5, 150, WAVEFORM_ARBITRARY);
    sub5.begin(vcoVol * 1.5, 150, WAVEFORM_TRIANGLE);
    vcoA6.begin(vcoVol, 150, WAVEFORM_SAWTOOTH);
    vcoB6.begin(vcoVol, 150, WAVEFORM_SQUARE);    
    vcoC6.begin(vcoVol * 1.5, 150, WAVEFORM_ARBITRARY);
    sub6.begin(vcoVol * 1.5, 150, WAVEFORM_TRIANGLE);
    
  //filter setup  
    filter1.octaveControl(7);
    filter2.octaveControl(7);
    filter3.octaveControl(7);
    filter4.octaveControl(7);
    filter5.octaveControl(7);
    filter6.octaveControl(7);

  //lfo A setup
    lfoA1.begin(WAVEFORM_SINE);
    lfoA2.begin(WAVEFORM_SINE);
    lfoA3.begin(WAVEFORM_SINE);
    lfoA4.begin(WAVEFORM_SINE);
    lfoA5.begin(WAVEFORM_SINE);
    lfoA6.begin(WAVEFORM_SINE);
  //lfo B setup
    lfoB1.begin(0.5, 1, WAVEFORM_TRIANGLE);
    lfoB2.begin(0.5, 1, WAVEFORM_TRIANGLE);
    lfoB3.begin(0.5, 1, WAVEFORM_TRIANGLE);
    lfoB4.begin(0.5, 1, WAVEFORM_TRIANGLE);
    lfoB5.begin(0.5, 1, WAVEFORM_TRIANGLE);
    lfoB6.begin(0.5, 1, WAVEFORM_TRIANGLE);
}

void myNoteOn(byte channel, byte note, byte velocity) {
//POLYPHONIC mode
  if (mux1F > 512) { //mux1F is the designator for the toggle switch which selects between monophonic and polyphonic modes
    switch (voices) {
      case 0 ... 5:
        if (env1on == false) {
          note1freq = note;
          env1.noteOn();
          filterEnv1.noteOn();
          env1on = true;
        } else if (env2on == false) {
          note2freq = note;
          env2.noteOn();
          filterEnv2.noteOn();
          lfoAenv2.noteOn();
          env2on = true;
        } else if (env3on == false) {
          note3freq = note;
          env3.noteOn();
          filterEnv3.noteOn();
          lfoAenv3.noteOn();
          env3on = true;
        } else if (env4on == false) {
          note4freq = note;
          env4.noteOn();
          filterEnv4.noteOn();
          lfoAenv4.noteOn();
          env4on = true;
        } else if (env5on == false) {
          note5freq = note;
          env5.noteOn();
          filterEnv5.noteOn();
          lfoAenv5.noteOn();
          env5on = true;
        } else if (env6on == false) {
          note6freq = note;
          env6.noteOn();
          filterEnv6.noteOn();
          lfoAenv6.noteOn();
          env6on = true;
        }
        voices++;
        break;
    }

//MONOPHONIC mode
  } else if (mux1F <= 512) {
    note1freq = note;
    env1.noteOn();
    filterEnv1.noteOn();
    voices++;
  }
  Serial.print("Note On, ch=");
  Serial.print(channel, DEC);
  Serial.print(", note=");
  Serial.print(note, DEC);
  Serial.print(", voices=");
  Serial.println(voices, DEC);
}

void myNoteOff(byte channel, byte note, byte velocity) {
//POLYPHONIC mode
  if (mux1F > 512) {
    switch (voices) {
      case 0 ... 6:
        if (note1freq == note) {
          env1.noteOff();
          filterEnv1.noteOff();
          env1on = false;
        } else if (note2freq == note) {
          env2.noteOff();
          filterEnv2.noteOff();
          lfoAenv2.noteOff();
          env2on = false;
        } else if (note3freq == note) {
          env3.noteOff();
          filterEnv3.noteOff();
          lfoAenv3.noteOff();
          env3on = false;
        } else if (note4freq == note) {
          env4.noteOff();
          filterEnv4.noteOff();
          lfoAenv4.noteOff();
          env4on = false;
        } else if (note5freq == note) {
          env5.noteOff();
          filterEnv5.noteOff();
          lfoAenv5.noteOff();
          env5on = false;
        } else if (note6freq == note) {
          env6.noteOff();
          filterEnv6.noteOff();
          lfoAenv6.noteOff();
          env6on = false;
        }
        voices--;
        break;

    }
//MONOPHONIC mode
  } else if (mux1F <= 512) {
    if (note1freq == note) {
      env1.noteOff();
      filterEnv1.noteOff();
      voices--;
    }
  }
  Serial.print("Note Off, ch=");
  Serial.print(channel, DEC);
  Serial.print(", note=");
  Serial.print(note, DEC);
  Serial.print(", voices=");
  Serial.println(voices, DEC);
}

void loop() {
  usbMIDI.read();
  MIDI.read();

  if (voices < 0) {
    voices = 0;
  }
//the remainder of the code here has nothing to do with MIDI processing
}

While this fragment above is not the entire Teensy sketch, it receives MIDI messages perfectly fine from the testing Nano sketch below, without generating any extra outputs:

#include <MIDI.h>
MIDI_CREATE_DEFAULT_INSTANCE();
void setup() 
{
    Serial.begin(9600);
    pinMode(LED_BUILTIN, OUTPUT);
    MIDI.begin(1);                      // Launch MIDI and listen to channel 4
}
void loop() 
{
    digitalWrite(LED_BUILTIN, HIGH);
    MIDI.sendNoteOn(42, 127, 1);    // Send pitch 42, velo 127 on channel 4
    delay(1000);
    MIDI.sendNoteOff(42, 127, 1);
    digitalWrite(LED_BUILTIN, LOW);
    delay(1000);
}

Good, so do you mean you have solved your issue or not?

However, even with this "partial" code I see a lot of room for improvement, starting with the use of arrays for many variables (it makes the code shorter, more readable and easier to debug...). In any case, even with this "partial" code, I see a lot of room for improvement, starting with the use of arrays for many variables (it makes the code shorter, more readable, and easier to debug...), and I suspect it's the cause of such a lenghty sketch...

To give an example and to start with, maybe the pin configuration would also be like this:

...
const byte TOT_MUX_PINS = 11;
const byte muxPin[11] = {9, 10, 11, 12, 24, 25, 26, 27, 28, 29, 30};
const byte TOT_COM_PINS = 5;
const byte muxCOM[5] = { A4, A3, A2, A1, A0};
const byte TOT_SETUP_PINS = 15;
const byte muxSetup[11] = {4, 5, 6, 8, 9, 10, 11, 12, 24, 35, 34, 33, 36, 37, 38};
...
void setup() {
...
  //Pin Setup
  for (int i = 0; i < TOT_MUX_PINS; ++i)
    pinMode(muxPin[i], OUTPUT);
  for (int i = 0; i < TOT_COM_PINS; ++i)
    pinMode(muxCOM[i], OUTPUT);
  for (int i = 0; i < TOT_SETUP_PINS; ++i) {
    pinMode(muxCOM[i], OUTPUT); // this was missing from the code snippet...
    digitalWrite(muxSetup[i], 0);
  }
...

It's just an example, where you have the same functionalities but with less code rows (7 instead of 35!). Did I already say that it's more compact and easier to debug?... :wink:

Just a suggestion: I would always use curly brackets with for loops etc., independent of the number of instructions that follow.

It's too easy to forget adding them later and it avoids future headaches ...

:innocent:

Sorry, I should have clarified that the correctly working Nano code was just for testing. (So no, I still haven't solved my issue.) The same setup (with the addition of the multiplexers) still results in extra NoteOn events when I release any button. I can grab some pictures of the actual wiring and setup when I get back in a few days and maybe that will help with further troubleshooting.

Thanks for the optimisation suggestions, I will implement them in the future.
My next step will probably be to debounce the button inputs and see if that improves the situation.

I had a draft suggesting that floating inputs might be an issue. Are you sure that the pin you are reading is def either HIGH or LOW, either by some assertion or a pull-up?

At this point I would use the smallest sketch you could write that simlpy demonstrates that your button reading and handling is working perfectly.

Until it does, utnagling causes and effects downstream is pointless.

a7

This is your issue, yet

I looked at your "here's enough code" post # 5. No digital reading or writing beyond initial setup.

Please post the part of your code that reads the pushbuttons using the multiplexers.

Ideally it would be in the context of a working sketch which you post in full.

Even better is what I recommended above: isolate the pushbutton/mux stuff and write a complete sketch that demonstrates the issue.

a7

Good, we'll wait for that!

The basic solution is to add a delay (50) as soon as the button is HIGH, but a hardware debounce is always the best solution.
If you search on Google you can find dozens of examples of hardware debounce with RC filter, for each button you just need to add a 100 nF capacitor between input and GND, and (if possible) a resistor in series to the input.
The most common schemes I know however use the internal pullup (INPUT_PULLUP), a bit easier because avoids the esternal resistor, so the capacitor to GND is enough.

I've returned

  • I have tried to debounce a singular button and multiplexer both with an RC filter and through code on a separate breadboard to no avail. (though debouncing doesn't seem to be the problem - I've made it print a serial output whenever it intends to send a noteOn/Off and it's clean, with no jittery outputs.)
  • All mux inputs have been tested and confirmed to be pulled LOW when their button isn't pushed.
  • I have found that the extra notes activate specifically when I release the button after a noteOn - it happens even if I don't call a noteOff when I detect the button releasing.
  • The extra notes still trigger even when I completely bypass the mux and use a simple digitalRead for the button input.
  • The test sketch using pure code to send out a periodic noteOn/Off still works completely fine.
  • I have called MIDI.turnThruOff in case it was interfering.

Here's a photo of the actual serial output:


The 2nd line and so forth only occurs when I release the button. The extra notes seem to repeat in this specific arrangement and order (78, 116, 79, 102, 10).

At this point I am completely stumped. Originally I had thought the multiplexers would be the cause.

This is the tiniest sketch for the Nano's button reading which still causes this behaviour. It prints NoteOn/Off fine and clearly, so I don't believe debouncing would have helped anyway.

#include <MIDI.h>
MIDI_CREATE_DEFAULT_INSTANCE();
float wait = 0.1;
int state = 0;
int stateprev = 0;

void setup() {
  Serial.begin(31250);
  MIDI.begin(1);
  MIDI.turnThruOff();
  analogReference(EXTERNAL);
  pinMode(7, INPUT);
}

void loop() {
  state = digitalRead(7);
  if (state != stateprev) {
    if (state == HIGH) {
      Serial.println("NoteOn");
      MIDI.sendNoteOn(42, 127, 1);
      delay(10);
      stateprev = state;
    } else if (state == LOW) {
      Serial.println("NoteOff");
      MIDI.sendNoteOff(42, 0, 1);
      delay(10);
      stateprev = state;
    }
  }
}

As for the bare minimum Teensy code:

#include <MIDI.h>
#include <MIDIUSB.h>
MIDI_CREATE_DEFAULT_INSTANCE();

void setup() {
  analogReadResolution(10);
  Serial.begin(31250);
   usbMIDI.begin();
   usbMIDI.setHandleNoteOn(myNoteOn);
   usbMIDI.setHandleNoteOff(myNoteOff);
   MIDI.setHandleNoteOn(myNoteOn);
   MIDI.setHandleNoteOff(myNoteOff);
   MIDI.begin(MIDI_CHANNEL_OMNI);
   MIDI.turnThruOff();
}
void loop() {
  usbMIDI.read();
  MIDI.read();
}
void myNoteOn(byte channel, byte note, byte velocity) {
  Serial.print("Note On, ch=");
  Serial.print(channel, DEC);
  Serial.print(", note=");
  Serial.println(note, DEC);
}
void myNoteOff(byte channel, byte note, byte velocity) {
  Serial.print("Note Off, ch=");
  Serial.print(channel, DEC);
  Serial.print(", note=");
  Serial.println(note, DEC);
}

This causes the serial output in post 12

Additionally, instead of using callbacks, I have tried to receive messages the "old fashioned" way in an attempt to debounce the receiving of MIDI signals:

  while (MIDI.read()) {
    if (MIDI.getType() == midi::NoteOn) {
      unsigned long currentNoteOnTime = millis();
      if (currentNoteOnTime - lastNoteOnTime >= debounceDelay) {
        lastNoteOnTime = currentNoteOnTime;
        byte pitch = MIDI.getData1();
        byte velocity = MIDI.getData2();
        byte channel = MIDI.getChannel();
        myNoteOn(channel, pitch, velocity);
      }
    }
    else if (MIDI.getType() == midi::NoteOff) {
      unsigned long currentNoteOffTime = millis();
      if (currentNoteOffTime - lastNoteOffTime >= debounceDelay) {
        lastNoteOffTime = currentNoteOffTime;
        byte pitch = MIDI.getData1();
        byte velocity = MIDI.getData2();
        byte channel = MIDI.getChannel();
        myNoteOff(channel, pitch, velocity);
      }
    }
  }

I have tried setting debounceDelay up to 1000 milliseconds to no improvement.

Added: Those false notes 78, 116, 79, 102, 10 are from your diagnostic printing! Translate them yourself. There's still a bit of mystery as the entire message does not get the same treatment.

Thanks. I think you have said you only get the serial printing when it is expected.

I'm not in the lab just now.

Replace your serial printing feedback with LEDs, eliminating all use of the serial line that is not for MIDI.

The easiest is this one-line toggle of an attached LED viz:

   digitalWrite(telltaleLED, digitalRead(telltaleLED) == LOW ? HIGH : LOW);

You can get more elaborate, but this will keep your minimum sketch minimal.

a7

As alto777 points out in msg #15, your problem is that the NANO sends the midi data and the serial debugging data through the same serial channel. The Teensy 4.1 has no way to tell which is which.
The solution is to not use any kind of Serial.print at all on the NANO.

I think you'd be better off using a Teensy 4.0 in place of the NANO.
In fact, unless you're doing some extremely heavy computing, the Teensy 4.1 should be able to handle the keypresses itself which are probably no more load on the CPU than handling the MIDI serial.

BTW if you're using the Arduino IDE to compile and upload code to the Teensy 4.1, you should have the USB Type (in the IDE Tools menu) set to "Serial + MIDI".

Pete

Solved! Thanks to everyone for their input. It had never occurred to me that the MIDI and serial were being transmitted on the same channel, so removing the serial.print debug output fixed it.