Sending USB MIDI to TX/RX

I have code that is sending MIDI via the default USB plug in an Arduino Micro. I was using Control_Surface library which worked fine but was too big for the board plus a display. I only need a couple features; press buttons, send program changes. I have a HobbyTronics USB Host board that works great but I can't get the MIDI messages to go out via RX/TX.

I was hoping the change was simple but I guess that's why ppl use libraries! My working code follows below.

I tried using the transit library USB-MIDI.h a couple different ways but to no avail. Code compiles fine but nothing is sent to the hardware pins. I'm only posting the changed code for MIDI_CREATE_INSTANCE, all the button handling code is the same.

Thanks for any help!

USB MIDI HARDWARE CODE:

#include "OneButton.h"
#include <USB-MIDI.h>

MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);

#define PIN_INPUT1 5
#define PIN_INPUT2 6

// tracking program change long press buttons
int counter = 0;

 // for musicians this is channel 1 (all from Grumpy Mike!)
byte channel = 0;

// MIDI program change 
void programChange(byte channel, byte patch) {
  MIDI.sendProgramChange(patch, channel);
}

// Setup a new OneButton on pin PIN_INPUT2.
OneButton button1(PIN_INPUT1, true);
// Setup a new OneButton on pin PIN_INPUT2.
OneButton button2(PIN_INPUT2, true);

// setup code here, to run once:
void setup() {

  MIDI.begin();
//... button function attachment code, same as below...
}

FULL CODE WORKS SENDING MIDI TO COMPUTER:

/*
 This is a sample sketch to show how to use the OneButtonLibrary
 to detect click events on 2 buttons in parallel.
 The library internals are explained at
 http://www.mathertel.de/Arduino/OneButtonLibrary.aspx
*/

#include "OneButton.h"
#include <MIDIUSB.h>

#define PIN_INPUT1 5
#define PIN_INPUT2 6


//MIDI CODE
int counter = 0;
byte channel = 0; // for musicians this is channel 1
// MIDI program change 
void programChange(byte channel, byte patch) {
  midiEventPacket_t event = {0x0C, 0xC0 | channel, patch};

  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
  //Serial.println("completes programChange()");
}

// Setup a new OneButton on pin PIN_INPUT2.
OneButton button1(PIN_INPUT1, true);
// Setup a new OneButton on pin PIN_INPUT2.
OneButton button2(PIN_INPUT2, true);

// setup code here, to run once:
void setup() {

  // Setup the Serial port. see http://arduino.cc/en/Serial/IfSerial
  Serial.begin(115200);

  while (!Serial) {
    ;  // wait for serial port to connect. Needed for Leonardo only
  }
  Serial.println("Starting TwoButtons...");

  // link the button 1 functions.
  button1.attachClick(click1);
  button1.attachDoubleClick(doubleclick1);
  button1.attachLongPressStart(longPressStart1);
  button1.attachLongPressStop(longPressStop1);
  button1.attachDuringLongPress(longPress1);


  // link the button 2 functions.
  button2.attachClick(click2);
  button2.attachDoubleClick(doubleclick2);
  button2.attachLongPressStart(longPressStart2);
  button2.attachLongPressStop(longPressStop2);
  button2.attachDuringLongPress(longPress2);

}  // setup


// main code here, to run repeatedly:
void loop() {
  // keep watching the push buttons:
  button1.tick();
  button2.tick();

  // You can implement other code in here or just wait a while
  delay(10);
}  // loop


// ----- button 1 callback functions

// This function will be called when the button1 was pressed 1 time (and no 2. button press followed).
void click1() {
  Serial.println("Button 1 click.");
  programChange(0, ++counter); // Increment preset

}  // click1


// This function will be called when the button1 was pressed 2 times in a short timeframe.
void doubleclick1() {
  Serial.println("Button 1 doubleclick.");
  // Reset MIDI preset to zero
  counter = 0;
  programChange(0, counter); 

}  // doubleclick1


// This function will be called once, when the button1 is pressed for a long time.
void longPressStart1() {
  Serial.println("Button 1 longPress start");
}  // longPressStart1


// This function will be called often, while the button1 is pressed for a long time.
void longPress1() {
  Serial.println("Button 1 longPress...");
  programChange(0, ++counter); // Increment preset
  delay(250);
}  // longPress1


// This function will be called once, when the button1 is released after beeing pressed for a long time.
void longPressStop1() {
  Serial.println("Button 1 longPress stop");
}  // longPressStop1


// ... and the same for button 2:

void click2() {
  Serial.println("Button 2 click.");
  programChange(0, --counter); // Decrement preset
}  // click2


void doubleclick2() {
  Serial.println("Button 2 doubleclick.");
}  // doubleclick2


void longPressStart2() {
  Serial.println("Button 2 longPress start");
}  // longPressStart2


void longPress2() {
  Serial.println("Button 2 longPress...");
  programChange(0, --counter); // Increment preset
  delay(250);
}  // longPress2

void longPressStop2() {
  Serial.println("Button 2 longPress stop");
}  // longPressStop2
1 Like

The code you posted does not use the USB-MIDI.h library at all.
That being said, you shouldn't need it if you just want to send out MIDI messages over the TX pin.

Why does a MIDI controller need to wait for the serial port to be opened?

If you post the Control Surface code that does what you want, I may be able to suggest alternatives.

Thanks for getting back. The while (!Serial) was left in by accident.

I thought MIDI_CREATE_INSTANCE needed <USB-MIDI.h> . Obviously not.

I have a working sketch using Control_Surface.h for my C4 pedal, it's just at the end of the memory. I have the following at the top so I can either send MIDI to the computer for testing or send it to the pedal:

// Instantiate a MIDI over USB interface
 //- not for Rx/Tx
//USBMIDI_Interface midi; 
// For direct connect to USB HOST board via Rx/Tx
HardwareSerialMIDI_Interface midi {Serial1};

Thanks again.

This can be replaced by MidiUSB, like in your second sketch.

This should be equivalent to the MIDI_CREATE_INSTANCE from your first sketch. It's not clear to me why it wouldn't work. What makes you think that no data is being sent? Either way, you should post the full sketch you're actually using, not just a stripped down version.

As a side note, you can save a significant amount of memory in Control Surface by setting IGNORE_SYSEX=1, NO_SYSEX_OUTPUT=1, CS_TRUE_CONTROL_SURFACE_INSTANCE=0 and DISABLE_PIPES=1 in https://github.com/tttapa/Control-Surface/blob/main/src/Settings/Settings.hpp.

For an Arduino Micro, this saves around 6644 bytes of flash and 557 bytes of RAM.

Thanks a heap mate, I will try all those suggestions.

Hi. I took a big step backwards on this and tried to write the simplest sketch that would send MIDI to the TX pin, which didn't work.

I also implemented your memory saving suggestions and will show that at the end, along with my memory-hog sketch. As always, whatever time and help you provide is much appreciated!

First the simple sketch.

#include "MIDIUSB.h"

void programChange(byte channel, byte patch) {
  midiEventPacket_t event = {0x0C, 0xC0 | channel, patch};

  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
}

void setup() {
    // Connect to Serial1
  Serial1.begin(115200);
}


void loop() {
  int val;
  val = random(0,6);
  Serial.println(val);
  programChange(0,val);
  MidiUSB.flush();
  delay(1000);
}

The TX LED blinks when it should, every second, but the pedal does not get the messages. (To be clear my TX RX is connected to a HobbyTronics USB hub board that works great with your library.) The sketch is still sending MIDI to the computer also, which must be wrong. There must be a simple way to get the TX pin to connect. .sendMIDI(event) must not be going there. I was hoping that using Serial1 would direct the output to TX.

As to the memory configurations, I had implemented most of these already based on another post you made. Here are the results:

#define IGNORE_SYSEX 0
#define NO_SYSEX_OUTPUT 0
#define CS_TRUE_CONTROL_SURFACE_INSTANCE 1
#define DISABLE_PIPES 0

Sketch uses 26666 bytes (93%) of program storage space. Maximum is 28672 bytes.
Global variables use 1369 bytes (53%) of dynamic memory, leaving 1191 bytes for local variables. Maximum is 2560 bytes.

#define IGNORE_SYSEX 1
#define NO_SYSEX_OUTPUT 1
#define CS_TRUE_CONTROL_SURFACE_INSTANCE 0
#define DISABLE_PIPES 1

Sketch uses 27128 bytes (94%) of program storage space. Maximum is 28672 bytes.
Global variables use 1367 bytes (53%) of dynamic memory, leaving 1193 bytes for local variables. Maximum is 2560 bytes.

Here is the sketch. I guess it's possible that everything else like the preset names array and the display code is using up enough memory that the changes above don't make any difference.

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// For the Adafruit shield, these are the default.
#define TFT_DC 9
#define TFT_CS 10

// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);

#include <Control_Surface.h>  // Include the Control Surface library
// https://tttapa.github.io/Control-Surface-doc/Doxygen/d0/d0c/group__Selectors.html

// Instantiate a MIDI over USB interface
 //- not for Rx/Tx
//USBMIDI_Interface midi; 

// For direct connect to USB HOST board via Rx/Tx
HardwareSerialMIDI_Interface midi {Serial1};  

// Instantiate a program changer with 128 programs, max of the C4
// USING UP TO 50 FOR NOW
ProgramChanger<50> programChanger{
  { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
    21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49 },
  CHANNEL_1,  // MIDI channel to use
};


// Instantiate a selector to control which one of the three programs is active
IncrementDecrementSelector<50> selector = {
  programChanger,  // what to select
  { 5, 6 },        // push button pins (increment, decrement)
  Wrap::Wrap,      // wrap around if min/max program is reached
};

/****************************************************************
Strings representing preset names copied from Neuro Desktop
Setting up the strings is a two-step process because the Micro needs to
use the PROGMEMory to store the names. Not enough RAM!
First, define the strings.
****************************************************************/
const char string_1[] PROGMEM = "Chiptune Vibe";
const char string_2[] PROGMEM = "Wub-Wub Tre";
const char string_3[] PROGMEM = "Twin Some";
const char string_4[] PROGMEM = "Recorder/Flut";
const char string_5[] PROGMEM = "Guitar Basic C";
const char string_6[] PROGMEM = "Tom's Tremelo";
const char string_7[] PROGMEM = "DD wobble fu";
const char string_8[] PROGMEM = "SM DBSwell";
const char string_9[] PROGMEM = "SM Stro Funk";
const char string_10[] PROGMEM = "SM MonoDrtFlt";
const char string_11[] PROGMEM = "SM LoFi Chors";
const char string_12[] PROGMEM = "Rock Organ";
const char string_13[] PROGMEM = "Buzzards of G";
const char string_14[] PROGMEM = "chinese Trem";
const char string_15[] PROGMEM = "Rising Tremol";
const char string_16[] PROGMEM = "Harmonic Tre";
const char string_17[] PROGMEM = "HP42 Square ";
const char string_18[] PROGMEM = "bnz-Dirty Trem";
const char string_19[] PROGMEM = "Tremvelope";
const char string_20[] PROGMEM = "BeachyTrem";
const char string_21[] PROGMEM = "Dirty Trem";
const char string_22[] PROGMEM = "Bass PolyCho";
const char string_23[] PROGMEM = "Envelope Cho";
const char string_24[] PROGMEM = "DD fuzz choru";
const char string_25[] PROGMEM = "Sungalasses";
const char string_26[] PROGMEM = "Magic Man";
const char string_27[] PROGMEM = "ChorusMono";
const char string_28[] PROGMEM = "QM Chorus 5";
const char string_29[] PROGMEM = "Slow rotry";
const char string_30[] PROGMEM = "DD Phase Wa";
const char string_31[] PROGMEM = "Phz Slapbac";
const char string_32[] PROGMEM = "Stereo Phaser";
const char string_33[] PROGMEM = "Roy's Phase 9";
const char string_34[] PROGMEM = "EnvPhaser";
const char string_35[] PROGMEM = "3-Stage Phas";
const char string_36[] PROGMEM = "Simple Bit Cru";
const char string_37[] PROGMEM = "Da Funk";
const char string_38[] PROGMEM = "Sprinkle Donu";
const char string_39[] PROGMEM = "The Funky Ro";
const char string_40[] PROGMEM = "Everybody In ";
const char string_41[] PROGMEM = "N Bitcrshr";
const char string_42[] PROGMEM = "Two man gan";
const char string_43[] PROGMEM = "Pure Octaves";
const char string_44[] PROGMEM = "Double Bass";
const char string_45[] PROGMEM = "Toothy Twins";
const char string_46[] PROGMEM = "dual saw lead";
const char string_47[] PROGMEM = "alien vocoder";
const char string_48[] PROGMEM = "Moog Bass";
const char string_49[] PROGMEM = "Grunty";
const char string_50[] PROGMEM = "Exp";

// Then set up a string table in program memory to 
// refer to your string arrays from above.
const char *const string_table[] PROGMEM = {
  string_1,
  string_2,
  string_3,
  string_4,
  string_5,
  string_6,
  string_7,
  string_8,
  string_9,
  string_10,
  string_11,
  string_12,
  string_13,
  string_14,
  string_15,
  string_16,
  string_17,
  string_18,
  string_19,
  string_20,
  string_21,
  string_22,
  string_23,
  string_24,
  string_25,
  string_26,
  string_27,
  string_28,
  string_29,
  string_30,
  string_31,
  string_32,
  string_33,
  string_34,
  string_35,
  string_36,
  string_37,
  string_38,
  string_39,
  string_40,
  string_41,
  string_42,
  string_43,
  string_44,
  string_45,
  string_46,
  string_47,
  string_48,
  string_49,
  string_50
};

char buffer[17];  // make sure this is large enough for the largest string it must hold
uint8_t iComparator = 0;    // compare current preset to newly chosen preset

// INT to retrieve or set current preset number as index to string names array
uint8_t iCurrentPreset; 
int iPreviousPreset; 
int iNextPreset; 

void resetDisplay(uint8_t iCurrentPreset)
{
    // set previous and next preset names accounding for 
    // wrap around of 50 names list
    iNextPreset = iCurrentPreset + 1;
    if (iCurrentPreset == 49) 
      iNextPreset = 0;

    iPreviousPreset = iCurrentPreset - 1;
    if (iCurrentPreset == 0) 
      iPreviousPreset = 49;

    // get preset string from PROGMEM string table, store in RAM var 'buffer'
    // Display Current preset name
    strcpy_P(buffer, (char *)pgm_read_word(&(string_table[iCurrentPreset]))); 
    // 'clear' screen
    tft.fillScreen(ILI9341_GREENYELLOW);
    // Send it to the diplay with preset number
    tft.setTextColor(ILI9341_BLACK);
    tft.setCursor(3, 105);
    tft.setTextSize(3);
    tft.print(buffer); // Display present name
    // draw line above and below current preset name
    tft.drawLine(3, 90, 300, 90, ILI9341_BLACK);
    tft.drawLine(3, 145, 300, 145, ILI9341_BLACK);

    // Display preset number upper corner, add '1' to account for zero based arrays
    tft.setCursor(277, 8);
    tft.print(iCurrentPreset + 1); 
    
    // resize font for previous and next preset names
    tft.setTextSize(2);
    // display previous preset name
    strcpy_P(buffer, (char *)pgm_read_word(&(string_table[iPreviousPreset]))); 
    tft.setTextColor(ILI9341_BLUE);
    tft.setCursor(5, 40);
    tft.print(buffer); // Display present name

    // display next preset name
    strcpy_P(buffer, (char *)pgm_read_word(&(string_table[iNextPreset]))); 
    tft.setTextColor(ILI9341_BLUE);
    tft.setCursor(5, 185);
    tft.print(buffer); // Display present name
}

void setup() {
  Control_Surface.begin();  // Initialize Control Surface

  // set up for display
  Serial.begin(9600);
  tft.begin();
  tft.setRotation(3);
  tft.setTextWrap(false);    
  tft.setTextSize(3);
  // 'clear' screen
  //tft.fillScreen(ILI9341_GREENYELLOW);

  // starting display
  resetDisplay(0);
}

void loop() {
  Control_Surface.loop(); // Update the Control Surface

  /* Using the string table in program memory requires the use of special functions to retrieve the data.
     The strcpy_P function copies a string from program space to a string in RAM ("buffer").
     Make sure your receiving string in RAM is large enough to hold whatever
     you are retrieving from program space. 
  */

  // access the Control_Surface object 'selector' and return the preset number it sent to the pedal
  iCurrentPreset = selector.get();

  // Only update the display when the preset choice changes
  if (iComparator != iCurrentPreset)
  {
    // set comparator 'iComparator'
    iComparator = iCurrentPreset;
    resetDisplay(iCurrentPreset);
  }
}

What makes you think that this sketch should send MIDI to the TX pin? The MidiUSB library only sends MIDI over the USB connection of the Arduino, never over a serial port.

You need a different library, e.g. the FortySevenEffects MIDI library, and explicitly tell it to use Serial1.

The TX LED only indicates transmissions over the USB connection, it is completely independent from the TX pin.

That doesn't seem right. IGNORE_SYSEX=1 on its own should save at least 128 bytes of RAM.
Which file did you edit? Which version of Control Surface are you using?

I was hoping that if I I used Serial1.begin as I have seen elsewhere it might set the environment correctly. I was winging it. BTW I have searched for basic examples of sending USB MIDI commands to TX/RX clearly to no avail.

I looked at the FortySevenEffects library somewhat previously but I didn't understand it well enough to know that it would help. I'll be perfectly honest I've done a lot of DB and web server coding in my life and my goal was not to learn more coding; it was to create something for my guitar rig. In any case thanks for the help, I'll dig into '47'.

The file I edited for the various settings changes was Settings.hpp, as you asked. It resides on my Mac at ../Arduino/libraries/Control_Surface/src/Settings, so I assume it the right one. I see the that version github was released on Sep 8, 2020. I only started doing this in 2023, BUT I deleted all the library files I had and got the latest, and a Christmas miracle happened, Sketch uses 24798 bytes (86%) of program storage space. No idea why that should have made any difference but it recouped 7%.

BTW the Settings.hpp file in the Control_Surface.zip and the same file on github are different. You probably already know that. Also I couldn't set IGNORE_SYSEX=1 because the compiler choked on HardwareSerialMIDI_Interface midi {Serial1};

/Users/michaelg3933/Documents/Arduino/libraries/Control-Surface-1.2.0/src/MIDI_Parsers/SerialMIDI_Parser.cpp: In member function 'CS::MIDIReadEvent CS::SerialMIDI_Parser::parse(uint8_t)':
/Users/michaelg3933/Documents/Arduino/libraries/Control-Surface-1.2.0/src/MIDI_Parsers/SerialMIDI_Parser.cpp:38:56: error: 'SysExStart' was not declared in this scope
             bool unterminatedSysEx = midimsg.header == SysExStart;

You probably aren't surprised by that either.

However with the memory storage changes in place, except for IGNORE_SYSEX, it doesn't change the usage. The usage for the sketch dropped 7% by using 'new' files but the settings changes didn't make a difference.

So this is still great for me. My last piece of the project is getting a rotary control in place, hopefully using Control_Surface. If that will fit into the memory I have left I'll happily live with the ugly fonts and call this a win. I've also come to the conclusion that this is definitely a prototype. I learned a lot about where I need to go and I'll be looking at other, beefier boards for the next iteration.

Thanks!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.