Multiple virtual MIDI ports over USB with Teensy 2.0? (Or 3.0?)

Status
Not open for further replies.

MuShoo

Well-known member
I apologize in advance if any of my terminology is wrong - I'm just a hobbyist coder/arduino-head, and most of the talk about USB HID Descriptors and Endpoints and such just confuses me!

Anyway, I'm building a MIDI control surface for digital audio workstations - To interface it cleanly with one of those workstations, I need my device to show up as 3 separate MIDI ports over USB (IE, Port A in, Port A out, Port B in, etc etc). Ideally I could then write/read from each virtual port by way of something like usbMIDI_A.noteOn() or some such. From what I can tell, the usbMIDI class was written for the Teensy 1.0, (some of the comments reference AT90USB162, which has only 4 programmable endpoints, while the Teensy 2.0 uses the AT90USB1286, which has six).

Re-writing the usbMIDI class is most likely well beyond my own abilities/sanity levels - I suppose I just want to check if it's POSSIBLE to do what I need - have three different MIDI in/out pairs described to the computer, and access each one separately from within the arduino coding language.


Any help would be much appreciated!
 
Nope - I'm just emulating three different control surface MIDI protocols (they each do different things, I need different features from each one - one allows for scrubbing, another allows for better plugin interaction in the DAW, etc) at the same time. The DAW needs each protocol to be set up as it's own 'device' with it's own midi in/out pair.
 
Not sure what your hardware situation is, but unless you need more that 16 channels, you should be able to to run several control surfaces via a single MIDI. The teensy can act as a midi device. If you do need more, You may want to check this out. https://www.sparkfun.com/products/9598 I have not used it, but i understand it will convert a serial connection to MIDI.
 
Last edited:
Ah, there must be some confusion from what I wrote - I don't need to pickup MIDI signals from three devices and read them into the Teensy - I need the Teensy to act, over USB, like three different MIDI devices. So you plug the Teensy in, and the computer sees Teensy MIDI A, Teensy MIDI B, and Teensy MIDI C as separate MIDI ports it can send and receive information from - but all three of those ports lead to the Teensy, and I can then access them separately in the code. No external DIN connectors on the Teensy, different sensors on the control surface I'm building will feed to different virtual MIDI ports.

I suppose in a sense I do need 48 channels (3x16) of MIDI in and 48 of MIDI out.
 
I don't think anyone is suggesting anything different. Teensy will be recognized as a midi device. I believe the intent of the above breakout would add another
 
As far as what I can tell, for what I need that breakout board doesn't do anything for me? While yes, it would conceivably allow me to have another MIDI output - that output would be out of the 5-pin DIN connector on the MIDI Breakout Board, and NOT out of the USB cable connected to the Teensy - please correct me if I'm wrong. Using the breakout boards would require me to use six cables - one USB, two pairs of MIDI cables into another MIDI-to-USB interface that then goes out another USB cable, and then into the computer.

The Teensy 2.0 should have enough USB endpoints to allow for writing the usbMIDI library such that it can appear as 3 separate MIDI devices. From the Teensy usb_midi hardware core code, hardware/teensy/cores/usb_midi/usb.c:

Code:
       // Standard MS Interface Descriptor,
        9,                                      // bLength
        4,                                      // bDescriptorType
        MIDI_INTERFACE,                         // bInterfaceNumber
        0,                                      // bAlternateSetting
        2,                                      // bNumEndpoints
        0x01,                                   // bInterfaceClass (0x01 = Audio)
        0x03,                                   // bInterfaceSubClass (0x03 = MIDI)
        0x00,                                   // bInterfaceProtocol (unused for MIDI)
        0,                                      // iInterface

I read this as, there are 9 items in this list, the interface's number is whatever the variable 'MIDI_INTERFACE' is, no alternate setting, and takes 2 endpoints. This bit describes the way the Teensy is enumerating it's MIDI i/o to the computer, and each MIDI I/O takes 2 endpoints.

Code:
	// MIDI IN Jack Descriptor, B.4.3, Table B-7 (embedded), page 40
	6,					// bLength
	0x24,					// bDescriptorType = CS_INTERFACE
	0x02,					// bDescriptorSubtype = MIDI_IN_JACK
	0x01,					// bJackType = EMBEDDED
	1,					// bJackID, ID = 1
	0,					// iJack

This is describing the virtual MIDI port that Teensy shows up as when you connect it to a computer in USB_MIDI HID mode. In particular, it's the 'In' jack (IE, Teensy MIDI In) as it pops up on my computer. It's an 'embedded' type of jack - not one that references a physical input.

From hardware/teensy/cores/usb_midi/usb_private.h:

Code:
// These buffer sizes are best for most applications, but perhaps if you
// want more buffering on some endpoint at the expense of others, this
// is where you can make such changes.  The [B]AT90USB162[/B] has only 176 bytes
// of DPRAM (USB buffers) and only endpoints 3 & 4 can double buffer.


// 0: control
// 1: debug IN
// 2: debug OUT
// 3: midi IN
// 4: midi OUT

The comment in this piece of the hardware core, as well as the description of what each endpoint is doing, leads me to believe that the Teensy USB MIDI Library/Core was written for the old Teensy 1.0 (Which had an AT90USB162. That chip only has 4 programmable endpoints (and one non-programmable). However, the Teensy 2.0, which is an AT90USB1286, has 6 programmable endpoints.

So, my questions are these: Can the USB HID descriptor for the Teensy usb_midi hardware core be rewritten to have 3 pairs of "midi IN" and "midi OUT"?

And can the rest of the core usb_midi_class class (hardware/teensy/cores/usb_midi/usb_api.h) be written so that you can have 3 different instances of the usbMIDI object, each referencing it's own pair of USB endpoints?

I'm not asking if anyone out there is willing to do this for me, I just want to know if it's feasible/possible before I (probably badly) try and attempt it. Also note that I would prefer for this to not need any additional software/drivers (that I'd then have to write and distribute) to, say, parse a serial signal from the teensy and convert it to multiple MIDI outs. That is my backup plan, but an entirely 'in the box' solution fits my needs better.
 
A small amount of my last post was incorrect - the Teensy 2.0++ uses the AT90USB1286, the Teensy 2.0 uses the ATMega32u4 - both chips have 6 programmable endpoints, however, so the point still stands.
 
Last edited:
Yes, I suspect that a new descriptor could be writen that has six MIDI endpoints (3 in and 3 out). You might find that some of the other descriptors which declare multipe devices (like keyboard+mouse+joystick) to be helpful as examples.

What I don't know is whether the computer OS will recognise the device without a driver (i.e. whether it will be class compliant).

(And yes, adding an Arduino DIN-MIDI shield would not help you here).
 
The USB MIDI protocol can support up to 16 "jacks" or "cables" (both terms are used in the specification). The idea is to represent up to 16 virtual MIDI cables. I have personally never used this. I do not know if Mac, Windows and Linux are compatible with this feature, or if they are, how the virtual cables are presented to the user. This is all based only on the USB MIDI spec, as published at www.usb.org (and which I used to write Teensy's USB MIDI code).

In USB MIDI, messages are packed into 32 bits. The first byte uses 4 bits to specify which type of message is in the other 3 bytes, and the other 4 bits specify which virtual cable. Teensy's USB MIDI implementation always sets the cable number to 0. Likewise, the Teensyduino functions to send do not have fields to specify which virtual cable, and the receive callbacks and functions do not provide the virtual cable. I considered this. I talked with numerous people interested in USB MIDI. Nobody really wanted this feature, and several people were confused by the question (talk of the 16 MIDI channels was the common response, similar to messages in this thread). I quickly came to the conclusion that adding virtual cable parameters would add more complexity than it was worth. So far, you're the very first person to ask for this virtual cable feature!

If you want to implement 3 virtual USB MIDI cables, you certainly can. You'll simply have some work to do. Since you mentioned building a controller, I'll only mention the sending functions....

First, you'll need to edit hardware/teensy/cores/usb_midi/usb.c. Here's the descriptors that currently exist:

Code:
        // Standard MS Interface Descriptor,
        9,                                      // bLength
        4,                                      // bDescriptorType
        MIDI_INTERFACE,                         // bInterfaceNumber
        0,                                      // bAlternateSetting
        2,                                      // bNumEndpoints
        0x01,                                   // bInterfaceClass (0x01 = Audio)
        0x03,                                   // bInterfaceSubClass (0x03 = MIDI)
        0x00,                                   // bInterfaceProtocol (unused for MIDI)
        0,                                      // iInterface

        // MIDI MS Interface Header, USB MIDI 6.1.2.1, page 21, Table 6-2
        7,                                      // bLength
        0x24,                                   // bDescriptorType = CS_INTERFACE
        0x01,                                   // bDescriptorSubtype = MS_HEADER 
        0x00, 0x01,                             // bcdMSC = revision 01.00
        0x41, 0x00,                             // wTotalLength

        // MIDI IN Jack Descriptor, B.4.3, Table B-7 (embedded), page 40
        6,                                      // bLength
        0x24,                                   // bDescriptorType = CS_INTERFACE
        0x02,                                   // bDescriptorSubtype = MIDI_IN_JACK
        0x01,                                   // bJackType = EMBEDDED
        1,                                      // bJackID, ID = 1
        0,                                      // iJack

        // MIDI IN Jack Descriptor, B.4.3, Table B-8 (external), page 40
        6,                                      // bLength
        0x24,                                   // bDescriptorType = CS_INTERFACE
        0x02,                                   // bDescriptorSubtype = MIDI_IN_JACK
        0x02,                                   // bJackType = EXTERNAL
        2,                                      // bJackID, ID = 2
        0,                                      // iJack

        // MIDI OUT Jack Descriptor, B.4.4, Table B-9, page 41
        9,
        0x24,                                   // bDescriptorType = CS_INTERFACE
        0x03,                                   // bDescriptorSubtype = MIDI_OUT_JACK
        0x01,                                   // bJackType = EMBEDDED
        3,                                      // bJackID, ID = 3
        1,                                      // bNrInputPins = 1 pin
        2,                                      // BaSourceID(1) = 2
        1,                                      // BaSourcePin(1) = first pin
        0,                                      // iJack

        // MIDI OUT Jack Descriptor, B.4.4, Table B-10, page 41
        9,
        0x24,                                   // bDescriptorType = CS_INTERFACE
        0x03,                                   // bDescriptorSubtype = MIDI_OUT_JACK
        0x02,                                   // bJackType = EXTERNAL
        4,                                      // bJackID, ID = 4
        1,                                      // bNrInputPins = 1 pin
        1,                                      // BaSourceID(1) = 1
        1,                                      // BaSourcePin(1) = first pin
        0,                                      // iJack

        // Standard Bulk OUT Endpoint Descriptor, B.5.1, Table B-11, pae 42
        9,                                      // bLength
        5,                                      // bDescriptorType = ENDPOINT 
        MIDI_RX_ENDPOINT,                       // bEndpointAddress
        0x02,                                   // bmAttributes (0x02=bulk)
        MIDI_RX_SIZE, 0,                        // wMaxPacketSize
        0,                                      // bInterval
        0,                                      // bRefresh
        0,                                      // bSynchAddress

        // Class-specific MS Bulk OUT Endpoint Descriptor, B.5.2, Table B-12, page 42
        5,                                      // bLength
        0x25,                                   // bDescriptorSubtype = CS_ENDPOINT
        0x01,                                   // bJackType = MS_GENERAL
        1,                                      // bNumEmbMIDIJack = 1 jack
        1,                                      // BaAssocJackID(1) = jack ID #1

        // Standard Bulk IN Endpoint Descriptor, B.5.1, Table B-11, pae 42
        9,                                      // bLength
        5,                                      // bDescriptorType = ENDPOINT 
        MIDI_TX_ENDPOINT | 0x80,                // bEndpointAddress
        0x02,                                   // bmAttributes (0x02=bulk)
        MIDI_TX_SIZE, 0,                        // wMaxPacketSize
        0,                                      // bInterval
        0,                                      // bRefresh
        0,                                      // bSynchAddress

        // Class-specific MS Bulk IN Endpoint Descriptor, B.5.2, Table B-12, page 42
        5,                                      // bLength
        0x25,                                   // bDescriptorSubtype = CS_ENDPOINT
        0x01,                                   // bJackType = MS_GENERAL
        1,                                      // bNumEmbMIDIJack = 1 jack
        3,                                      // BaAssocJackID(1) = jack ID #3

You'll need to add more "jacks". I can't help you with the exact bytes to add. You'll need to get the "Universal Serial Bus Device Class Definition for MIDI Devices" from www.usb.org which is probably in the audio class section. At least those comments in the code can point you to the relevant sections. Honestly, all that virtual cable and jacks stuff seems confusing to me, not being a MIDI expert. Hopefully it'll make more sense for you... or you can try stuff until you find settings that work? Just remember, the USB descriptors don't actually "do" anything, they're only binary data which your computer reads to learn what type of USB device you've implemented.

Once you've created descriptors that tell your PC about the other virtual cables, then all you need to do is modify the code in hardware/teensy/cores/usb_midi/usb_api.cpp to send the virtual cable numbers, instead of putting zeros into those fields. You'll see all the send functions look like this:

Code:
void usb_midi_class::sendNoteOff(uint8_t note, uint8_t velocity, uint8_t channel)
{
        send_raw(0x08, 0x80 | ((channel - 1) & 0x0F), note & 0x7F, velocity & 0x7F);
}
void usb_midi_class::sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel)
{
        send_raw(0x09, 0x90 | ((channel - 1) & 0x0F), note & 0x7F, velocity & 0x7F);
}

They all call send_raw() to transmit 4 bytes. That first byte is the one with the cable number and message type. Just put your cable number into the upper 4 bits. Easy, right?

Of course, with USB MIDI, the hardest part is finding what is actually supported on the 3 operating systems. The drivers are remarkably poor, especially Microsoft's (big surprise, right?!)

Good luck. If you get it to work, I hope you'll share the modified code with some details about which system you tested and how the virtual cables appear?
 
Last edited:
It would be nice to see this added and tested. I suggest that a '4 cable MIDI' set would be good; as an example, the Kontakt sampler (very popular, on Windows and Mac) supports 4 groups of 16 channels so hardware could be tested with that. (I have Kontakt).
 
If there are descriptors that work on all 3 operating systems, I'd be happy to include it in a future Teensyduino release.
 
Thanks Paul! I'm glad I was finally able to make sense. I've been digging into that USB Class Definition doc (was screwing around with it yesterday but with little success).

I think I ought to be able to pull this off - if I do I'll try and post my code. One last question! Is there a way to make a new usb core for Teensy? IE, "midi_usb_3ports" that would show up in the "Tools:USB Type" submenu in Teensyduino? Currently I've just been editing the usb_midi files from within teensy/cores directly, but that seems... dangerous.

And I knew MIDI well before I knew coding (going on... wow, like 16 years now. Makin' me feel old...) so all that jargon about jacks and cables makes perfect sense to me :D
 
Oh, also - can I safely remove all code related to the DEBUG_INTERFACE? From my limited understanding, it's using two endpoints, and I'll need those to make other MIDI jacks out of.
 
It's easiest to just edit the exiting MIDI type. You can add more types, but it requires getting a number of things to match between boards.txt and several files. I'd suggest just editing the MIDI type, at least for getting started. If you want to use the original, just install another copy of Arduino in a different directory.

Using the virtual cables feature, you can have up to 16 cables all running from the same pair of endpoints normally used for only a single cable. Maybe that wasn't clear from my message?

Don't mess with the DEBUG_INTERFACE stuff. If you ever want to use Serial.print() to the Arduino Serial Monitor, you'll need it. If your changes will ever get merged into a future version of Teensyduino, that stuff must be preserved. Besides, you don't need more endpoints. The virtual cables is a 4 bit number in each 32 bit data field, all running within the same 2-endpoint interface.
 
Hello! This thread is very interesting to me. The original poster mentioned that he is using a DAW (Digital Audio Workstation) which supports 3 different protocols over MIDI which require separate endpoints for each protocol. I see an additional significant benefit.... the ability to talk with more than one Midi Application at a time! Recently I did a big show where I had my MacBookPro running a DAW application (Reaper) for triggering audio samples via midi. The same MBP was running the main lighting control software for the event (QLC). When you use a standard USB Midi Controller like the Kenton Killamix Mini (http://www.kentonuk.com/products/items/midicontrol/kmix-mini.shtml), then whichever software "opens" the midi device first takes control of it and you cannot "open" the midi device in other software. I see a significant benefit using the Teensy2 or Teensy3 as a platform for developing new MIDI controllers which show up as multiple ports and can control different applications simultaneously. Perhaps this is specialty stuff here, but I see value in it.... of course we have to design the new Midi Controllers in a way that routes the various controller data to the appropriate ports in a useful and intuitive way.
Good thread! -frenchy

(ps-I would love to send OSC data (much higher resolution and flexible naming) using a Teensy over USB, but that is for another thread...)
 
Last edited:
Update! I have successfully gotten Teensy to show up with 2 pairs of MIDI in/out jacks. Eventually I'll update it to 3 pairs (I'm just happy to have two working now!)

In Mac OSX 10.7.5, using MaxMSP as my MIDI host, they show up as 'Teensy Midi Port 1' and 'Teensy MIDI Port 2' (which is perfectly fine by me).

I had to tweak some things, I was using this forum post http://www.microchip.com/forums/m493322-print.aspx as a reference for the descriptor types (it does a few things differently). I still have to NOT include the AC Interface Descriptor stuff though (#if 0 section of the MIDI descriptor code).

Keeping track of the bLengths and wTotalLengths of all these descriptors is a huge pain in the ass. :)
 
Gotten it up to 3 virtual in/out ports, which is my required limit (I think I understand it well enough that I can write up an explanation of what you need to change if you want to add more - there's a LOT of variables that are related to adding/removing ports.

Currently I'm now attacking the bitwise math/logic I need to make the usb_midi_class output to multiple ports (I am not good with bitwise math or logic). My ideal for using this would actually be to construct 3 instances of the class, usbMIDI1, usbMIDI2, and usbMIDI3, and have the constructor for each one assign it's Jack/Cable data there - instead of having to do it each time I call usbMIDI.sendControlChange(), I'd just call usbMIDI1.sendControlChange() or usbMIDI2.sendControlChange().

Is that feasible?

Also, I'm having some trouble parsing usb_midi_class::read():

Code:
if (type1 == [B]0x09[/B] && type2 == 0x90) {
			if (b3) {
				msg_type = 1;			// Note on
				if (handleNoteOn) (*handleNoteOn)(c, b2, b3);
			} else {
				msg_type = 0;			// Note off
				if (handleNoteOff) (*handleNoteOff)(c, b2, b3);

That bold 0x09 is where I'd insert my jack ID nibble, correct?
 
Alrighty, I've got everything _mostly_ working the way I want it to now. Except for reading in MIDI data on any of the ports, that seems to be acting very strange, and I'm not sure why. It's all still pretty rough and I'm sure there's some cleanup/efficiency stuff that could be done to it.

I've uploaded the current state of it to: *See a later post! Page 2!*

That .zip contains the current state of my 3-port USB files, as well as what appears to me to be fully functional MIDI sends on any of the three ports (oddly, it wouldn't let me instantiate my multiple instances of the usb_midi_class class unless I did it within the sketch! definitely look at the example/test sketch included.)

After instantiating your usb_midi objects (up to three) like so:

Code:
usb_midi_class		usbMIDIa = usb_midi_class();
usb_midi_class		usbMIDIb = usb_midi_class();
usb_midi_class		usbMIDIc = usb_midi_class();

You'll need to put something like this:

Code:
  usbMIDIa.setPortNum(0);
  usbMIDIb.setPortNum(1);
  usbMIDIc.setPortNum(2);

inside of your setup() function. I'm guessing there's an easier way to do this ("usb_midi_class usbMIDIb = usb_midi_class(1);" perhaps?) but I didn't really want to muck about with constructors too much.

Anyway, now you can use all the usbMIDI.sendWhatever() functions to any of the three virtual MIDI out ports that are set up.

However, as should hopefully be apparent with the Test sketch, reading doesn't work that well, and I'm not sure why. My completely uneducated guess is that the way pointers are being passed around to reference the various functions (such as OnNoteOn, which I've attempted to rectify by making OnNoteOnA and OnNoteOnB) isn't actually allowing for multiple handler functions.

Last note, there's a quick, rough MaxMSP-based standalone application included in the .zip file, for Mac (I don't have a windows machine to test this on with, and so I can't build windows MaxMSP apps) - it'll let you easily verify that all three in/out ports are appearing, and sending a MIDI note to one of the first two ports will make the third port spit back out some information. If you don't have a Mac, you can still see the problem by sending a note to one of the first two Teensy MIDI ports. A note sent to the first MIDI port should ONLY turn the LED ON, while a note to the second port should ONLY turn the LED OFF. Sadly that's not the case.

Any thoughts?
 
Last edited:
Oh, another note - not sure if it's my fault or not, but Serial.print() and so forth don't seem to work anymore. Not really sure what I did, I don't think I touched the debug settings at all.
 
*See later posts!*

SUCCESS. Though, it's still only been tested on a Mac - here's what I was doing wrong in the read() before:

By calling read(); for each of the three ports, I was inadvertently dumping the buffer data (UEDATX) _every_ time - so sometimes (50% of the time, when reading from two ports) I'd get buffer data that matches the channel I needed, and the other half I wouldn't. So now there's a (possibly not supposed to do this) *global* array that holds the buffer data (MIDIdata[]) - you read() only once for any loop, and it can be done from any of the 'ports' - speaking of which, I fixed the issue about having to define the usbMIDI objects in the sketch - so now you get usbMIDIa, usbMIDIb, and usbMIDIc just by having your Teensy type defined as MIDI.

Because of the changes to the way read() works, you now call something like usbMIDIa.read(); (it can be any of the three MIDI ports, but only call it ONCE per loop) to grab the buffer data into the MIDIdata array.

After you've read() from the buffer, you can use

Code:
usbMIDIa.parse(channel);
usbMIDIb.parse(channel);
usbMIDIc.parse(channel);

which functions the SAME as the previous usbMIDI implementation's 'read(channel);' function - ie:


Code:
const int ledPin = 11;   // Teensy has LED on 11, Teensy++ on 6

int pinStatus = 0;

void setup()
{
  
  pinMode(ledPin, OUTPUT);
  usbMIDIa.setPortNum(0);
  usbMIDIb.setPortNum(1);
  usbMIDIc.setPortNum(2);

  usbMIDIa.setHandleNoteOn(OnNoteOnA);
  
  usbMIDIb.setHandleNoteOn(OnNoteOnB);
  
  usbMIDIc.setHandleNoteOn(OnNoteOnC);

}

void loop(){
usbMIDIa.read();    // download MIDI data from
usbMIDIa.parse();   // parse midi data on port a
usbMIDIb.parse();   // parse midi data on port b
usbMIDIc.parse();   // parse midi data on port c
}

void OnNoteOnA(byte channel, byte note, byte velocity) {
  pinStatus = 1;
  digitalWrite(ledPin, HIGH);
}

void OnNoteOnB(byte channel, byte note, byte velocity) {
  pinStatus = 0;
  digitalWrite(ledPin, LOW);
}

void OnNoteOnC(byte channel, byte note, byte velocity) {
  if (pinStatus)
    digitalWrite(ledPin, LOW);
  else
    digitalWrite(ledPin, HIGH);
}

Writing MIDI to the various ports is simply of matter of using the identifier for the port you want to use, and using the same functions as Paul's MIDI api. For example, to send a midi note out of port B, and then a control change out of port C:

Code:
usbMIDIb.sendNoteOn(64, 127, 1);  //send a note on out of port b
usbMIDIc.sendControlChange(37, 65, 15);  //send control change #37 value 65, on channel 15, out port c

Anyway, like I mentioned earlier - I've only been able to test this on a Mac. I have no idea if the Ports will show up correctly on Windows or Linux - if anyone is feeling adventurous and wants to test those, let me know how it goes! (I honestly have no idea how midi is implemented within windows or linux, I'm mainly a mac audio geek)
 
Last edited:
Me too.

Any chance you can update your link? I need multiple USB MIDI ports for my project also.

- Peter
 
Status
Not open for further replies.
Back
Top