usbMIDI does not send polyphonic aftertouch message when I call sendAfterTouch(note, pressure, channel);

jsnow

Member
There are, apparently, three ways to send polyphonic aftertouch using the Teensy MIDI libraries:

usbMIDI.sendAfterTouch(note, pressure, channel); ,

usbMIDI.sendAfterTouchPoly(note, pressure, channel);,

and

usbMIDI.sendPolyPressure(note, pressure, channel);.

sendPolyPressure(note, pressure, channel) works with both DIN-5 serial MIDI and usb, but I get a deprecation warning when I compile.

sendAfterTouch(note, pressure, channel) is the non-deprecated version, but if I use this one, polyphonic aftertouch messages are sent when using serial MIDI but not when using USB.

sendAfterTouchPoly(note, pressure, channel) works fine for usb but isn't defined at all for serial MIDI.

In .arduino15/packages/teensy/hardware/avr/1.59.0/libraries/MIDI/src/MIDI.hpp, sendAfterTouch(...) and sendPolyPressure(...) with the same arguments do the exact same thing, so I was a bit confused why one would work and the other wouldn't, but then I found what are apparently a separate set of definitions specifically for usb over in .arduino15/packages/teensy/hardware/avr/1.59.0/libraries/USBHost_t36/USBHost_t36.h and in .arduino15/packages/teensy/hardware/avr/1.59.0/cores/teensy4/usb_midi.h. (That latter one seems to be what gets run when in USB midi mode.)

usb_midi.h only defines the non-polyphonic variant of sendAfterTouch (the one where we omit the note number and it affects the whole channel). If the polyphonic variant of sendAfterTouch isn't even defined for usb, I don't know why I don't just get a linker error when compiling. Instead it just seems to be silently doing nothing when the function is invoked at runtime.

I was able to get sendAfterTouch working the way I expect by adding the polyphonic variant to cores/teensy4/usb_midi.h:

void sendPolyPressure(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0) __attribute__((always_inline)) { send(0xA0, note, pressure, channel, cable); } void sendAfterTouchPoly(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0) __attribute__((always_inline)) { send(0xA0, note, pressure, channel, cable); } void sendAfterTouch(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0) __attribute__((always_inline)) { // <----- NEW send(0xA0, note, pressure, channel, cable); } void sendControlChange(uint8_t control, uint8_t value, uint8_t channel, uint8_t cable=0) __attribute__((always_inline)) { send(0xB0, control, value, channel, cable); } void sendProgramChange(uint8_t program, uint8_t channel, uint8_t cable=0) __attribute__((always_inline)) { send(0xC0, program, 0, channel, cable); } void sendAfterTouch(uint8_t pressure, uint8_t channel) __attribute__((always_inline)) { // <---- MODIFIED send(0xD0, pressure, 0, channel, 0); }

Though, I had to remove the "cable=0" default argument from the non-polyphonic variant of sendAfterTouch because the compiler couldn't distinguish which version you meant if you supply three arguments. I'm not sure how important it is.

For what it's worth, this is my MIDI wrapper I use to let me use serial and usb MIDI at the same time:

bool useUsbMidi = true; bool useDinMidi = false; MIDI_CREATE_INSTANCE(HardwareSerial, Serial5, dinMidi); uint64_t midiMsgsSent = 0; uint64_t midiMsgsReceived = 0; #define doMidi(func, ...) { \ midiMsgsSent++; \ if (useUsbMidi) { \ usbMIDI.func(__VA_ARGS__); \ } \ if (useDinMidi) { \ dinMidi.func(__VA_ARGS__); \ } \ } void midiNoteOn(uint8_t note, uint8_t velocity, uint8_t channel) { doMidi(sendNoteOn, note, velocity, channel); } void midiNoteOff(uint8_t note, uint8_t velocity, uint8_t channel) { doMidi(sendNoteOff, note, velocity, channel); } void midiPitchBend(int16_t pb, uint8_t channel) { doMidi(sendPitchBend, pb, channel); } void midiAfterTouch(uint8_t volume, uint8_t channel) { doMidi(sendAfterTouch, volume, channel); } void midiPolyAfterTouch(uint8_t note, uint8_t pressure, uint8_t channel) { doMidi(sendAfterTouch, note, pressure, channel); /* no message is sent on usb */ // doMidi(sendPolyPressure, note, pressure, channel); /* works on serial and usb, but causes deprecation warning */ } void midiControlChange(uint8_t cc, uint8_t value, uint8_t channel) { doMidi(sendControlChange, cc, value, channel); } void midiProgramChange(uint8_t bank, uint8_t channel) { doMidi(sendProgramChange, bank, channel); } void midiRPN14Bit(uint8_t pmsb, uint8_t plsb, uint8_t vmsb, uint8_t vlsb, uint8_t channel) { midiControlChange(0x65, pmsb, channel); midiControlChange(0x64, plsb, channel); midiControlChange(0x6, vmsb, channel); midiControlChange(0x26, vlsb, channel); midiControlChange(0x65, 127, channel); midiControlChange(0x64, 127, channel); } void midiRPN(uint8_t pmsb, uint8_t plsb, uint8_t v, uint8_t channel) { midiControlChange(0x65, pmsb, channel); midiControlChange(0x64, plsb, channel); midiControlChange(0x6, v, channel); midiControlChange(0x65, 127, channel); midiControlChange(0x64, 127, channel); } int midiBufferSize = 0; void midiSetup(){ Serial.println("serial fifo size " + String(Serial5.availableForWrite())); dinMidi.begin(); midiBufferSize = Serial5.availableForWrite(); Serial.println("midiBufferSize set to " + String(midiBufferSize)); /* the default size appears to be 39 bytes */ }

I could work around this by calling sendAfterTouch(...) on serial MIDI and sendAfterTouchPoly(...) on usb or just use sendPolyPressure(...) on both and ignore the deprecation warning, but I thought I'd post it here just in case other people are running into the same thing.

(I've been using midisnoop under Xubuntu to monitor the MIDI stream.)
 
Last edited:
I just realized something that in retrospect is kind of obvious given what I wrote in the previous post:

The reason the Teensy is dropping aftertouch messages when I call sendAfterTouch(note, pressure, channel) is that it interprets it as the channel aftertouch command with the extra "cable" argument that normally defaults to zero. Or, in other words, it thinks I'm calling sendAfterTouch(pressure, channel, cable).

That's why I don't get any linker errors -- I'm calling a real function, but the arguments are all wrong.

This is kind of an easy mistake to make if you're going off of the generic Arduino midi library documentation like I was.
 
From the Teensy usb MIDI documentation

C++:
void sendAfterTouch(uint8_t pressure, uint8_t channel, uint8_t cable=0) {
        send(0xD0, pressure, 0, channel, cable);

and

C++:
void sendAfterTouchPoly(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0) {
        send(0xA0, note, pressure, channel, cable);
 
This differs from the Arduino MIDI library, which overloads the function name (and thus, can't be extended to allow an optional cable number)

C++:
void MidiInterface< Transport, Settings, Platform >::sendAfterTouch     (     DataByte      inNoteNumber,
        DataByte      inPressure,
        Channel      inChannel
    )

and

C++:
void MidiInterface< Transport, Settings, Platform >::sendAfterTouch     (     DataByte      inPressure,
        Channel      inChannel
    )
 
Right, that's the problem -- calling sendAfterTouch with 3 arguments does completely different things depending on if you're using usb or serial MIDI. If you're already aware of that, it's possible to work around, but it's a very easy mistake to make since the usb and serial midi APIs are otherwise almost identical.

As far as I can tell, there also doesn't seem to be any separate documentation for the usb midi API aside from https://www.pjrc.com/teensy/td_midi.html. That page does mention sendAfterTouchPoly and doesn't advertise a polyphonic variant of sendAfterTouch, but it also doesn't mention anything about the cable=0 default argument that all of those functions take. If someone converts their serial midi code over to use usb, if they're really attentive they might wonder why the polyphonic variant of the sendAfterTouch command compiles despite not being in the "official" list of functions, but then might assume the page is out of date, and be confused that no poly aftertouch messages are being sent when they run their code.

Polyphonic aftertouch is not much used, which makes it an even easier mistake to make; if it was a well-known pitfall, there might be some folk knowledge about it in the community.

As it is, I don't see an obvious simple right way to make the APIs consistent. Some possible options include: we could convince the Arduino serial midi developers to un-deprecate sendPolyPressure() or add a sendAfterTouchPoly function to serial midi, and recommend people use that. The teensy usb API could make the sendAfterTouch 3 argument variant be the polyphonic AT version, and then add another function like sendAfterTouchWithCable(pressure, channel, cable) that is the channel AT variant that takes a cable argument. The usb midi library could be re-designed to determine the cable number in some other way (e.g. usb3.noteOn(60, 127, 1) instead of noteOn(60, 127, 1, 3) to send a note-on command on cable 3).

If there is no practical fix, then maybe https://www.pjrc.com/teensy/td_midi.html could at least include a note to be careful not to use sendAfterTouch() for poly AT even though it's allowed by the serial midi API.
 
@Paul Here is a replacement USB MIDI web page, derived from the current page and adding guidance on this aftertouch issue. Feel free to use this, if you would like. I didn't see the website on GitHub, so could not send a pull request.
 

Attachments

  • td_midi.zip
    10.8 KB · Views: 58
Back
Top