Teensy3.6 UsbHost MIDI Performance Issues

Code:
USBHS_USBCMD = USBHS_USBCMD_ITC(1) | USBHS_USBCMD_RS |
I hadn't seen this change. Setting this to 1 improved reliability somewhat, but ultimately my device still hangs after sending a couple hundred MIDI messages.
 
Fair enough. I'll first try to confirm if there was initially anything wrong performance-wise with the original code (which I don't believe there was for T3.6 devices), and if there is, I'll send you some code to reproduce the issue.
 
So I performed several tests today using a fresh download of TeensyDuino 1.55 testing on Windows 10 using MIDIOX and macOS (Catalina). I found a couple of issues.

All my tests are using the following sketch:
Code:
//#define USE_USB_MIDI_SEND_NOW
//#define USE_UHP_MIDI_SEND_NOW

#define USE_UHP

int sendWait = 1000;
int noteLength = 1000;
bool noteIsOn = false;

uint8_t note = 0;

#ifdef USE_UHP
#include "USBHost_t36.h"

USBHost myusb;
USBHub hub1(myusb);
MIDIDevice_BigBuffer midi0(myusb);
MIDIDevice_BigBuffer midi1(myusb);
MIDIDevice_BigBuffer midi2(myusb);
MIDIDevice_BigBuffer midi3(myusb);

//POINERS TO THE UHP MIDI DEVICE SO WER CAN ENUMERATE
MIDIDevice_BigBuffer* uhpMIDI[4] = { 0 };

#endif


elapsedMicros timer;

void setup() {

  #ifdef USE_UHP
  uhpMIDI[0] = &midi0;
  uhpMIDI[1] = &midi1;
  uhpMIDI[2] = &midi2;
  uhpMIDI[3] = &midi3;
  #endif

  // put your setup code here, to run once:
  myusb.begin();

}

void loop() {
    if (!noteIsOn && timer > sendWait) {

      sendNoteOn(note, 5, 10);
      noteIsOn = true;
      timer = 0;
    }

    if (noteIsOn && timer > noteLength) {
      sendNoteOff(note, 5, 10);
      noteIsOn = false;
      timer = 0;

      note++;
      if (note > 127) note = 0;
    }

    usbMIDI.read();

    #ifdef USE_UHP
    for (int i = 0; i < 4; i++) {
       if (*uhpMIDI[i]) uhpMIDI[i]->read();
    }
    #endif
}


void sendNoteOff(uint8_t note, uint8_t vel, uint8_t chan) {
  usbMIDI.sendNoteOff(note, vel, chan);

  #ifdef USE_UHP
  for (int i = 0; i < 4; i++) {

    if (*uhpMIDI[i]) {
      uhpMIDI[i]->sendNoteOff(note, vel, chan);
    }
  }
  #endif
}

void sendNoteOn(uint8_t note, uint8_t vel, uint8_t chan) {
  usbMIDI.sendNoteOn(note, vel, chan);

  #ifdef USE_USB_MIDI_SEND_NOW
  usbMIDI.send_now();
  #endif

  #ifdef USE_UHP
  for (int i = 0; i < 4; i++) {

    if (*uhpMIDI[i]) {
      uhpMIDI[i]->sendNoteOn(note, vel, chan);
       
      #ifdef USE_UHP_MIDI_SEND_NOW
      uhpMIDI[i]->send_now();
      #endif
    }
  }
  #endif
}

Issue number 1 - Duplicate or empty usbMIDI packets

The test setup is a Teensy 3.6 device with nothing connected to the UHP (USB Host Port). Here are screenshots illustrating the problem. Of these screenshots, the double NoteOn messages are most troubling, as they result in stuck notes.

Originally I thought this issue was connected to using usbMIDI.send_now(). While that does make the issue more likely to occur, it happens even if not calling send_now(). And it's also worth noting that even if we remove all UHP code, the problem persists.

Duplicate NoteOff messages
doubleNoteOffNoGap.png

Duplicate NoteOn messages and empty MIDI packet
doubleNoteOn.png

Empty MIDI packet
gap.png

Stuck Note in Logic Pro
stuckNote.png

Issue number 2 - UHP MIDI Packets are not being sent for several milliseconds

The test setup here is a iConnectMIDI+ connected directly to the UHP and its MIDI is monitored in MIDIOX. To support the iConnectMIDI+, the descriptor buffer needs to be increased in size. I used to 2048 bytes. The TIMESTAMP in the screenshot is in milliseconds. As we can see that packets are not being sent reasonably quickly.

uhpMIDI packets - 1 uhpMIDI device (direct).png
 
Last edited:
I did another test for the second issue using a Teensy 3.2 device that forwards USB MIDI packets to its MIDI Din Out and was not able to reproduce issue #2, which leads me to believe that might be an issue with the iConnectMIDI+.
 
A while back, built a multi-cable usbMidi Clock source and had issues with the T3x <>usbMIDI.send_now(); relationship.It would crash some random time after startup. For compatibility with other gear it was necessary use a T4x which has never crashed but have been working on it recently adding Sysex handlers etc. and ears pricked up here so started digging - might learn something.

Firstly, I can confirm that I ran the above code with and without #define USE_USB_MIDI_SEND_NOW and managed to repro the double NoteOn and here's the evidence:-

DSC_0002.jpg

Don't have any hubs and other goodies handy so did not do anything with usbHOST.

Here's a stripped version of my Clock generator, run on a T3.6.

Code:
// Tested on bare T3.2, 3.6 and 4.0, usbTYPE = MIDIx16.

#define USE_SEND_NOW

int NumClocks = 6; // 0 - 15

struct ClockData {
  bool Started;
  bool Paused;
  unsigned long CurrentMicros;
  unsigned long PreviousMicros;
  unsigned long ClockInterval;
  uint16_t ClockTempo;
  int LedState;
  int LedPinNum;
};
struct ClockData ClockData[16] = {
  {false, false, 0, 0, 0, 60, HIGH, 13},
  {false, false, 0, 0, 0, 60, HIGH, 14},
  {false, false, 0, 0, 0, 60, HIGH, 15},
  {false, false, 0, 0, 0, 60, HIGH, 15},
  {false, false, 0, 0, 0, 60, HIGH, 17},
  {false, false, 0, 0, 0, 60, HIGH, 18},
  {false, false, 0, 0, 0, 60, HIGH, 19},
  {false, false, 0, 0, 0, 60, HIGH, 20},

  {false, false, 0, 0, 0, 60, HIGH, 21},
  {false, false, 0, 0, 0, 60, HIGH, 22},
  {false, false, 0, 0, 0, 60, HIGH, 23},
  {false, false, 0, 0, 0, 60, HIGH,  0},
  {false, false, 0, 0, 0, 60, HIGH,  1},
  {false, false, 0, 0, 0, 60, HIGH,  2},
  {false, false, 0, 0, 0, 60, HIGH,  3},
  {false, false, 0, 0, 0, 60, HIGH,  4}
};

//**********************************************************

void setup()
{
  delay(500);
  for (int i = 0; i <= NumClocks; i++) {
    pinMode(ClockData[i].LedPinNum, OUTPUT);
    digitalWrite(ClockData[i].LedPinNum, LOW);
    ClockData[i].Started = true;
  }
}
//*********************************************************

void loop()
{
  while (usbMIDI.read()) {
  }
  for (int i = 0; i <= NumClocks; i++) {
    myBPM(i);
  }
}
//****************************************************
void myBPM(byte clockNumber) {
  {
    if ((ClockData[clockNumber].Started) == (false) || (ClockData[clockNumber].ClockTempo) == (0)) {
      ClockData[clockNumber].ClockInterval = -1;
    }
    else
      ClockData[clockNumber].ClockInterval = (2500000 / ClockData[clockNumber].ClockTempo);
  }
  ClockData[clockNumber].CurrentMicros = micros();
  if (ClockData[clockNumber].CurrentMicros - ClockData[clockNumber].PreviousMicros > ClockData[clockNumber].ClockInterval)
  {
    ClockData[clockNumber].PreviousMicros = ClockData[clockNumber].CurrentMicros;
    if (ClockData[clockNumber].LedState == LOW)
      ClockData[clockNumber].LedState = HIGH;
    else
      ClockData[clockNumber].LedState = LOW;
    digitalWrite(ClockData[clockNumber].LedPinNum, ClockData[clockNumber].LedState);
    usbMIDI.sendRealTime(usbMIDI.Clock, clockNumber);
#if defined USE_SEND_NOW
    usbMIDI.send_now();
#endif
  }
}
And here's a shot of where it crashed:-

DSC_0003.jpg


After it's crashed you need to press the Prog button to upload fresh code.

Note the three extra Clock messages on Cable 4.
 
I noticed that for my issue, if I increase the length of notes and the time between them to 2000us, both issues seem to go away (provided I don't call send_now()).

Code:
unsigned long ClockInterval

Code:
ClockData[clockNumber].ClockInterval = -1;

Probably not going to crash anything, but its not quite right either.
 
Ok, thanks for pointing out my next homework assignment.

Back to duplicate messages and playing around with your example above, found that any Midi message can be duplicated. Seems to be the first symptom of calling usbMIDISendNow() too frequently and if you increase that frequency further the Windows10 <>Teensy connection gets trashed and takes a reboot and a some extra time to sort itself out.
 
Ok, thanks for pointing out my next homework assignment.

Back to duplicate messages and playing around with your example above, found that any Midi message can be duplicated. Seems to be the first symptom of calling usbMIDISendNow() too frequently and if you increase that frequency further the Windows10 <>Teensy connection gets trashed and takes a reboot and a some extra time to sort itself out.

Something very strange is going on. I modified the sketch to send 3 notes at a time. Look at the timestamps. I'm having a hard time explaining that one.
backwardsInTime.png
 
I think the issue is with the way MIDI drivers work. I'm thinking the best solution is to add some sort of timer or check to prevent calling usb_midi_flush_output multiple times between between usb polling calls by the driver.
 
1 Ms, jitter? Interesting is that I've seen 14Ms between a particular message and it's duplicate. Easier to spot if you send a mix of messages - use 'em all.
Another interesting avenue is to send messages on individual cables, like use all 16. Duplicates seem to pop up more frequently on cables 7 and 8.
Same code's output from a T4x can look headscratchingly different...
 
Perhaps I should start a new thread because the title of this one is unrelated to this particular issue.
 
1ms of jitter is not remarkable, but when it goes backwards in time, it certainly is. Here's a clearer image.
View attachment 26575

Okay. I feel stupid. Here's a lesson. Don't ever take a screenshot while the window is repainting its view. So the timestamp wierdness is a red herring.

Anyways, I think I've solved the mystery.

Don't call send_now(). This function calls usb_midi_flush_output() which is already periodically called in usb_dev.c. I'm guessing that if the USB controller calls the function while the user code is already calling it, you get the chance of a duplicate packet being queued. I can't be 100% that this is what's causing the duplicate packets, but it's a very plausible explanation.

And another important tip, if you're sending MIDI to devices connected via MIDI DIN, or USB HOST devices that support sending DIN MIDI, then you'll want to be careful about sending MIDI messages too quickly (less than 2 ms apart), as it can result in dropped messages or undefined behavior (iConnectMIDI4+).
 
Furthermore, it looks like the problem with calling send_now() from user code can be resolved by changing the usb_flush_output() slightly.

Basically, I'm just using the tx_noautoflush as a sort of lock.

Code:
void usb_midi_flush_output(void)
{
	if (tx_noautoflush == 0) {
		tx_noautoflush = 1;

		if (tx_packet && tx_packet->index > 0) {
			tx_packet->len = tx_packet->index * 4;
			usb_tx(MIDI_TX_ENDPOINT, tx_packet);
			tx_packet = NULL;
		}

		tx_noautoflush = 0;
	}
}
 
Furthermore, it looks like the problem with calling send_now() from user code can be resolved by changing the usb_flush_output() slightly.

Basically, I'm just using the tx_noautoflush as a sort of lock.

Code:
void usb_midi_flush_output(void)
{
	if (tx_noautoflush == 0) {
		tx_noautoflush = 1;

		if (tx_packet && tx_packet->index > 0) {
			tx_packet->len = tx_packet->index * 4;
			usb_tx(MIDI_TX_ENDPOINT, tx_packet);
			tx_packet = NULL;
		}

		tx_noautoflush = 0;
	}
}

Great detective work done by yeahtuna. Is this change with usb_midi_flush already implemented or is this something I’d have to change manually somewhere? I’m guessing it’s somewhere in the midi library?
 
@yeahtuna did you ever find a good solution for this?

I am facing the exact same problem:
I am using Teensy 3.6 with USB Host, with two MIDI devices connected. I am trying to send MIDI notes to both devices. I am basically using your code from above.

I get stuck notes and skipped notes – but only for the second MIDIDevice. It seems, for the first MIDIDevice everything works fine.
Also, if I define only one MIDIDevice, everything works fine, too.

The behaviour fits the assumption that the problem arises when messages are sent too quickly: With the first message it works fine, with the second message there are problems.


Code:
USBHost myusb;                                        
USBHub hub1(myusb);                                   
MIDIDevice_BigBuffer midiDevice(myusb);       
MIDIDevice_BigBuffer midiDevice2(myusb);     
MIDIDevice_BigBuffer* midiDeviceList[2] = { 0 };

...

midiDeviceList[0] = &midiDevice;
midiDeviceList[1] = &midiDevice2;
myusb.begin();

...

void sendMidiNoteOn(int channel, int noteNumber, int velocity) {
  for (int i = 0; i < 2; i++) {
    if (* midiDeviceList[i]) {
      midiDeviceList[i]->sendNoteOn(noteNumber, velocity, channel);  
    }
  }
}

void sendMidiNoteOff(int channel, int noteNumber, int velocity) {
  for (int i = 0; i < 2; i++) {
    if (* midiDeviceList[i]) {
      midiDeviceList[i]->sendNoteOff(noteNumber, 0, channel);  
    }
  }
}
 
Back
Top