Midi-Instrument (not MIDI controller) with Leonardo

Hi,
I need/want to build a Midi instrument (not controller) using the Arduino Leonardo by using the MidiUSB library.

The idea is to compose music with MuseScore and play the tones on my DIY-Midi-instrument. Each GPIO will control a valve to generate a tone:

MuseScore ---- (via MIDI) ----> Arduino-Leonardo ---- (via GPIOs) -----> GPIO1==C' GPIO2==D' etc.

So far, I only found instructions to built MIDI-controllers that will connect to MIDI-Instruments, but not how to built a MIDI-instrument.

My questions are:

  1. Can the MIDI-USB library be used to built such a MIDI-Instrument ?
  2. Is there any instruction available in the internet how to do this with MIDI-USB (any link would be appreciated) ?
  3. In case the number of GPIOs of the Arduino is not sufficient (I actually want to have 32 different tones), what options do I have to expand the number of GPIOs (MUX will not work, as I want to play 2 tones at the same time ) ?

Any tip, hint, idea is welcome !

BR
HWe

Yes, MidiUSB library is full-duplex, so basically it can both receive and send Midi.

Don't know about that. I started just very simple, get a DAW to send a midi-note to the Leonardo, and simply blink the LED with noteOn and noteOff receive, as an expansion of the MIDIUSB_read example.

Well the question is more if you want to take velocity into account to begin with, and what kind of circuit you expect to use. And if you want to be able to play all notes at the same time. You could even consider using SPI DAC's though that may be a bit beyond the scope of what you need. Simple bit-shifter registers can provide you with extra output pins if that's what you need, and you can daisy chain those, so if it's just simple on-off that will work just fine.

I look into the MIDIUSB functions and found the "MIDIUSB.read()" function.
I assume I will have to decode/parse the read bytes and then switch on/off my GPIOs according to the note it played.

In case the tone/GPIO is not mapped to the output pins (due to limited number of pins) - I potentially could connect another Arduino (e.g. UNO) via UART and use my own custom protocol to expand the number of tones/GPIO to the connected UNO ?

What do you think ?

No not by itself. You need some way of generation the sounds.

Look at the Adafruit sound generators available for the Feather series's of controllers. These can turn MIDI signals into notes and have a built in range of GM sounds, but they are not brilliant quality.

The other option is to have a multi function sample player. You can get some that can handle 32 polyphony, the .wav format is the best for this as there is no delay decoding say an MP3 file. But these can be quite costly.

If you have your own sound generator like a keyboard that plugs into you computer, then you will need a MIDI host shield or some other sort of similar hardware.

yep that's the idea.

that is an option, though i think i would prefer a Bit-shifter for simplicity, or a SPI-DAC for the sake of versatility.

So far i have been going from the point of view that you have a way of making sounds. If that, then you should focus on that first. 32 tuned piezo crystal would already work, but if you intend to rely on the Arduino for the sound generation, that will complicate things significantly.

Thanks @Deva_Rishi and @Grumpy_Mike

Your inputs are very helpful.

@Grumpy_Mike
There is a DYI wodden organ with 32 keys and electro-magnetic-lifted levers to push they organ key.
It is avery cool instrument, build by my 74 year old friend - currently controlled by an old-school paper punched tape - for which the tapes were manually punched (imagine the effort to do this for a song !!!).

Now we want to replace the punched-tape-controller by Arduino+FET driver. So, yes, the sound generation is already very clearly defined !

OK so what mechanism is used to do the pulling and pushing? Is it some sort of linear actuator. If so do you know what the current draw of each actuator is. Also do you know the speed of response that is required?

How many holes are they across the paper tape? You could create them automatically with an RTTY paper punch for 5 holes, or a computer terminal for 8 holes.

He beats me by a year :slight_smile:

The simplest solution is using a 74HC596 to expand your pins. Basically you shift out the new data pin configuration and then switch the latch, so you can switch every output individually. You will need 4 of them to drive all 32 keys, and you can daisy chain them.
I imagine you are using a circuit for each of the keys that will not draw much current from the individual pins, either using a transistor or mosfet to control something bigger.

Thanks, I had a look into the 74HC596 and it seems straight forward to expand to 32 outputs.
My initial idea was a shift register as well, but the latching function was not clear.
As the 74HC596 combines both, it is fairly easy to implement it this way.

From what I see in the datasheet, the 74HC596 allows for daisy-chaining - am I correct ?
Just connect the QH' to the SER of the next device.

Regarding the MIDI protocol (which I did not use before) I need to understand the timing behind it - basically how to sync the 74HC596 SRCLK and RCLK to the actual cadence/beat note of the music played.

Do you have any tutorial/documentation/recommendation where to find more details ?

Yes that does the trick.

The beats / notes played come in as commands, real-time and in a first come first go sort of sequence. With the time required to send a command using the hardware baudrate of 31250bps a 3 byte command takes about 1ms to transmit, and the USB midi is faster. A command consists of 3 bytes (mostly) defining the type of command and 2 data bytes, which for a noteOn data1 is the note, and data2 is the velocity that it is played at. Although noteOff is a separate command, a noteOn with velocity 0 is considered to be the same as noteOff. If you send a noteOn but never send a noteOff, the note will just keep playing.

The normal way to process this in your case would be to read any incoming packets, set or clear any bits on the output variable (if using a 32 tone output, it can simply be a 32-bit variable) if there are no incoming packets left, shift out the bits to the shift registers, and flick the latch, and repeat the process. You could test to see if there is any change on the output variable compared to before, but the shifting out can be done so fast that there will hardly be any need for that.

Midi.org, but you should be able to find a lot online all over the place and usually more explanatory than midi.org

Rather than use a 74HC596 you will be much better off using a TIPC6B595. They are pin compatible and can provide much more current than the 74HC version. There are current restrictions to the whole package and you will not be able to use them at the maximum current for all the outputs.

They both pull down only, that is they switch the output to ground or turn the output off. In other words the can't source power only sink it.

Like any shift register they can simply be chained. Don't forget decoupling capacitors on each chip in the chain.

Great support @Deva_Rishi and @Grumpy_Mike

Now I feel well prepared for the implementation - thx a lot!

I compiled the first simple code (without any Arduino connected - but selected the Leonardo) and received this (sorry for the german) - it means no bootloader file found.
I am on Ubuntu 24 and installed Arduino 1.8.19 from the Ubuntu repos.
The "arduino-core-avr" package is installed.

Any idea ?

This is what I get when searching:

hwe@hwe-T580:/usr$ find . -type f -name *eonardo*
./share/arduino/hardware/arduino/avr/bootloaders/caterina/Caterina-Leonardo.txt
./share/arduino/hardware/arduino/avr/bootloaders/caterina/Leonardo-prod-firmware-2012-12-10.txt
./share/arduino/hardware/arduino/avr/bootloaders/caterina/Leonardo-prod-firmware-2012-04-26.txt
hwe@hwe-T580:/usr$ 

This is the error:


Build-Optionen wurden verändert, alles wird neu kompiliert
Bootloader-Datei angegeben, aber nicht vorhanden: /usr/share/arduino/hardware/arduino/avr/bootloaders/caterina/Caterina-Leonardo.hex
Der Sketch verwendet 4896 Bytes (17%) des Programmspeicherplatzes. Das Maximum sind 28672 Bytes.
Globale Variablen verwenden 457 Bytes (17%) des dynamischen Speichers, 2103 Bytes für lokale Variablen verbleiben. Das Maximum sind 2560 Bytes.

UPDATE:
I learned that I have to download the *.hex files and others from GitHub - arduino/ArduinoCore-avr: The Official Arduino AVR core.
After copying the required file, it is working !

Problem solved !

1 Like

No not really. MIDIUSB_read example compiles for me without issue (other than the warning about the literal 0x80 / 0x90 being narrowed from int to byte, but that's fine.)

This should be useful: Control Surface: Getting Started

It receives MIDI Note messages over USB and turns on/off the pins of a *595 shift register accordingly.

Perfect !

First steps with the Leonardo in progress ... first issue detected:

MuseScore2 (Ubuntu 24) allows to select "Arduino Leonardo Midi1" as output device --> good
Leonardo is programmed to blink the onboard LED on a received MIDI byte --> bad

I tried different settings in Musescore and I am not even sure it is connected properly.
Any idea why the LED does not blink - or why no MIDI byte is received ?
Any hint how to debug ?

The LED blinking on its own is working properly.

[Update: Deactivating the serial port commands did the trick --> issue solved]

This is the code:


#include "MIDIUSB.h"

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

  pinMode(LED_BUILTIN, OUTPUT);

}

void loop() {
  // put your main code here, to run repeatedly:

  midiEventPacket_t rx;
  do {
    rx = MidiUSB.read();
    if (rx.header != 0) {
      Serial.print("Received: ");
      Serial.print(rx.header, HEX);
      Serial.print("-");
      Serial.print(rx.byte1, HEX);
      Serial.print("-");
      Serial.print(rx.byte2, HEX);
      Serial.print("-");
      Serial.println(rx.byte3, HEX);
      digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
      delay(300);                      // wait for a second
      digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
      }
  } while (rx.header != 0);

}

Yeah i was wondering about that, i mean that is the example ? When i saw it i was already confused. The example i kept for myself was this.

#include "MIDIUSB.h"

#define LED 3

// First parameter is the event type (0x09 = note on, 0x08 = note off).
// Second parameter is note-on/note-off, combined with the channel.
// Channel can be anything between 0-15. Typically reported to the user as 1-16.
// Third parameter is the note number (48 = middle C).
// Fourth parameter is the velocity (64 = normal, 127 = fastest).

void noteOn(byte channel, byte pitch, byte velocity) {
  midiEventPacket_t noteOn = {0x09, 0x90 | channel, pitch, velocity};
  MidiUSB.sendMIDI(noteOn);
  MidiUSB.flush();
}

void noteOff(byte channel, byte pitch, byte velocity) {
  midiEventPacket_t noteOff = {0x08, 0x80 | channel, pitch, velocity};
  MidiUSB.sendMIDI(noteOff);
  MidiUSB.flush();
}

void setup() {
  //Serial.begin(115200);
  pinMode(LED, OUTPUT);
  analogWrite(LED, 20);
  delay(1000);
  analogWrite(LED, 0);
}

// First parameter is the event type (0x0B = control change).
// Second parameter is the event type, combined with the channel.
// Third parameter is the control number number (0-119).
// Fourth parameter is the control value (0-127).

void controlChange(byte channel, byte control, byte value) {
  midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value};
  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
}


void loop() {
  static uint32_t moment = millis();
  static bool noteon = true;

  midiEventPacket_t rx;

  if (millis() - moment > 2000) {
    moment = millis();
    if (noteon) noteOn(0, 48, 100);  // pin C2
    else noteOff(0, 48, 64);
    noteon = !noteon;
  }


  do {
    rx = MidiUSB.read();
    if (rx.header != 0) {
      uint8_t type = rx.byte1 >> 4;
      uint8_t channel = rx.byte1 & 0xF;
      uint8_t value1 = rx.byte2;
      uint8_t value2 = rx.byte3;
      if (type == 0x9) analogWrite(LED, value2 * 2);
      if (type == 0x8) analogWrite(LED, 0);
      }
  } while (rx.header != 0);

  // controlChange(0, 10, 65); // Set the value of controller 10 on channel 0 to 65
}

I puit a LED + resistor on pin 3 and used it's PWM ability.
Did some sending in the opposite way just to confirm. Of course the Serial.port is used for the MIDI, and setting the baud rate and writing BS data (as in not MIDI) is probably not such a great idea.
Mind you the Leonardo has Serial1 connected to it's 0 & 1 pins i think just like the micro. You could connect to that if you really want debug information.

I do recommend you look into the Control-Surface library. It is really great and well supported.

Just to keep going on the current path will also get your where you want i think.

Please elaborate more on the USB and the Serial TX/RX on Pins 1 and 2.
I am not sure if I understand this correctly, and so far I do not have an idea how the USB is implemented in Leonardo.

I see from the Leonardo schematic, that the USB is connected to the pins D+ and D-.
Same as for an Arduino UNO.

Why should the MIDI use the serial TX/RX on pins 1 and 2 ?
And why is the integrated USB required anyway for MIDIUSB.h ?

In my simple mind, any USB2SERIAL converter (like FT232) connected to the serial TX/RX (e.g. on an UNO) could be selected as MIDI device in MuseScore and transmit data to the uC.
What am I missing here ?

Bthw. there is progress. Now 4 LEDs for C,D,E,F are blinking accoring to the MuseScore music :wink:

USB 1.0 and 2.0 are half-duplex :slight_smile:

No, the Arduino UNO is very different: its main microcontroller doesn't have D+/- pins at all. Instead, it is connected to a secondary microcontroller using the TX/RX pins.
The Leonardo doesn't need a secondary microcontroller, and its TX/RX pins are free.

If you want MIDI over USB, you can just ignore the serial TX/RX pins, you don't need them.

Because a MIDI device needs specific USB descriptors and a dedicated bulk endpoint, and the MIDIUSB.h library configures those for you. If the USB interface/firmware is on a secondary microcontroller, the library cannot configure these descriptors and endpoints from the main microcontroller.

In theory, you could indeed hack MuseScore in a way that allows this. But in practice, the operating system will load a serial port/modem driver for the Arduino, and MuseScore has no idea how to send MIDI to such a device. You need the correct USB descriptors so the operating system detects your Arduino as a MIDI device and loads the appropriate driver, which MuseScore is then able to communicate with as a proper MIDI device.