CyberSpy

Rantings from a guy with way too much free time

Making some Noise - Teensy, TGA-Pro, and MIDI

2018-05-04 programming Rob Baruch

Making Some Noise (and maybe even Music)!

In this blog post, I’ll share my experiences using the Teensy and TGA-Pro Guitar Audio Shield to process audio - both using an audio processing codec as well as MIDI. I’ve also included the Yamaha UX16 USB-to-MIDI controller to aid in patching my MIDI channels into the inputs of the Guitar Audio shield.

Teensy - Yet antother cheap USB uController board.

At the hear of this project is the Teensy 3.6 USB Microcontroller development board. This sub-$30 board is a feature-rich micro-controller using the Cortex M4, floating-point unit, digital and analog pins, and lots of communication protocols (USB, i2C, SPI, Serial, Ethernet). What makes this little USB-attached board so useful are the accompanying software elements:

  • Teensyduino - An ardiuno-like IDE for the Teensy
  • Extensive code libraries - An Awesome collection of libraries compatible with the Teensy. Additionally, many of the arduino libraries are compatible (or are easily modified) with the Teensy.

To get started, be sure to have the latest version of the arduino IDE installed, and then visit the Teensyduino install page to get setup for Teensyduino.

TGA-Pro Guitar Audio Shield

TGA-Pro

Now that you’re all set to rock and roll with your Teensy development, let’s add the TGA Pro Guitar Audio shield to the mix. This board allows us to mate our Teensy to the shield, access MIDI and codec audio units and process audio programmatically through the Teensy!

In addition to the board, the developer also offers a Teensy library just for the board, BAGuitar Library. Before installing the library, be sure to install the dependent Dma-Spi library.

Sanity Check: First things First.

Before moving forward, it’s a good idea to verify that your setup works, so I strongly suggest running the BAGuitar Try Me First sketch. This sketch verifies that the Teensy 3.6 has been installed properly in the TGA Pro socket and can be programmed. We also test the MIDI ports on the TGA Pro. For this test you’ll need a MIDI cable so we can loop the input to the output.


#include <Wire.h>
#include <Audio.h>
#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();
using namespace midi;

AudioInputI2S            i2sIn;
AudioOutputI2S           i2sOut;

// Audio connections, just connect the I2S input directly to the I2S output.
AudioConnection      patch0(i2sIn,0, i2sOut, 0);
AudioConnection      patch1(i2sIn,1, i2sOut, 1);

AudioControlWM8731      codecControl; // needed to enable the codec

unsigned long t=0;
const int usrLED = 16; // the location of the GTA Pro user LED (not the LED on the Teensy itself)
int usrLEDState = 0;

void setup() {

  // Configure the user LED pin as output
  pinMode(usrLED, OUTPUT);
  digitalWrite(usrLED, usrLEDState);

  // Enable the MIDI to listen on all channels
  MIDI.begin(MIDI_CHANNEL_OMNI);  
  Serial.begin(57600); // Enable the serial monitor
  Wire.begin(); // Enable the I2C bus for controlling the codec
  delay(5);
  
  delay(100);
  codecControl.disable(); // Turn off the codec first (in case it was in an unknown state)
  delay(100);
  AudioMemory(24);

  Serial.println("Enabling codec...\n");
  codecControl.enable(); // Enable the codec
  delay(100);
  

}

void loop() {  
  
  ///////////////////////////////////////////////////////////////////////
  // MIDI TESTING
  // Connect a loopback cable between the MIDI IN and MIDI OUT on the
  // GTA Pro.  This test code will periodically send MIDI events which
  // will loop back and get printed in the Serial Monitor.
  ///////////////////////////////////////////////////////////////////////
  DataByte note, velocity, channel, d1, d2;

  // Send MIDI OUT
  int cc, val=0xA, channelSend = 1;
  for (cc=32; cc<40; cc++) {
    MIDI.sendControlChange(cc, val, channelSend); val++; channelSend++;
    delay(100);
    MIDI.sendNoteOn(10, 100, channelSend);
    delay(100);
  }
  
  if (MIDI.read()) {                    // Is there a MIDI message incoming ?
    MidiType type = MIDI.getType();
    Serial.println(String("MIDI IS WORKING!!!"));
    switch (type) {
      case NoteOn:
        note = MIDI.getData1();
        velocity = MIDI.getData2();
        channel = MIDI.getChannel();
        if (velocity > 0) {
          Serial.println(String("Note On:  ch=") + channel + ", note=" + note + ", velocity=" + velocity);
        } else {
          Serial.println(String("Note Off: ch=") + channel + ", note=" + note);
        }
        break;
      case NoteOff:
        note = MIDI.getData1();
        velocity = MIDI.getData2();
        channel = MIDI.getChannel();
        Serial.println(String("Note Off: ch=") + channel + ", note=" + note + ", velocity=" + velocity);
        break;
      default:
        d1 = MIDI.getData1();
        d2 = MIDI.getData2();
        Serial.println(String("Message, type=") + type + ", data = " + d1 + " " + d2);
    }
    t = millis();
  }

  // If no MIDI IN activity, print a message every 10 seconds
  if (millis() - t > 10000) {
    t += 10000;
    Serial.println("(no MIDI activity, check cables)");
  }

  // Toggle the USR LED state
  usrLEDState = ~usrLEDState;
  digitalWrite(usrLED, usrLEDState);

}

If all goes well, the Serial Monitor will display information about the MIDI messages being sent and received on the MIDI I/O ports. Assuming all’s well with the sketch above, let’s go further into the TGA Pro shield.

Code Library: Audio

As mentioned there’s an extensive collection of libraries for the Teensy. One in particular is of interest to us: Audio. Download, compress, and install the library into the Arduino IDE.

There are lots of Example sketches available with the library. Take a look at them under the File->Example->Audio menu. Speifically, let’s look at the File->Example->Audio->Synthesis->Guitar sketch. This sketch will simulate the strumming of guitar chords!!

Before looking at the code, keep in mind that the Audio library knows nothing about the TGA Pro board. Rather it’s written for one of the audio shields sold by the Teensy folks. So you are going to want to modify the code slightly to use the codec that’s on the TGA-Pro audio shield. Recall from our Try Me First sketch in the BAGuitar Library, the code instantiated our codec using the following line of code:

AudioControlWM8731      codecControl; // needed to enable the codec

In the Guitar Sketch, it’s assuming that we have a diffent codec:

AudioControlSGTL5000     sgtl5000_1;

Replacing our codec instantiations and all instances of sgl5000_1 with codeControl should make this sketch compatible with the TGA Pro Guitar shield.

Now that we’re using our codec, let’s also discuss the general concepts of the Teensy Audio library. I strongly recommend a visit the the documentation. In general, the Audio library starts by creating Audio objects and linking them in a processing chain. There’s even a GUI tool that will allow you to draw the flow of objects, link them together, and export the code for your sketch!

Using the GUI and exporting the sketch, we’d get the following code:

AudioSynthKarplusStrong  string1;
AudioSynthKarplusStrong  string2;
AudioSynthKarplusStrong  string3;
AudioSynthKarplusStrong  string4;
AudioSynthKarplusStrong  string5;
AudioSynthKarplusStrong  string6;
AudioMixer4              mixer1;
AudioMixer4              mixer2;
AudioOutputI2S           i2s1;
AudioConnection          patchCord1(string1, 0, mixer1, 0);
AudioConnection          patchCord2(string2, 0, mixer1, 1);
AudioConnection          patchCord3(string3, 0, mixer1, 2);
AudioConnection          patchCord4(string4, 0, mixer1, 3);
AudioConnection          patchCord5(mixer1, 0, mixer2, 0);
AudioConnection          patchCord6(string5, 0, mixer2, 1);
AudioConnection          patchCord7(string6, 0, mixer2, 2);
AudioConnection          patchCord8(mixer2, 0, i2s1, 0);
AudioConnection          patchCord9(mixer2, 0, i2s1, 1);

The tool also supports importing code and rendering a patch graph (although, I think that may not be working properly).

Our flow-graph looks like:

Flow

The rest of our sketch is just like any ordinary arduino sketch. We define a setup() and a loop() function and we’re done!

The setup() function allocates Audio memory, sets the mixer gains, and initializes the codec.

The loop() function is where the action is. Basically, the loop strums through a chord progression. But what’s a chord? Take a look at the chords.h header file and you’ll demystify what’s actually going on. Here’s a sample from the header file.

const float Cmajor[6] = {      0, NOTE_C3, NOTE_E3, NOTE_G3, NOTE_C4, NOTE_E4};  // C - E - G

and Notes are defined above as:

#define NOTE_E2   82.41
#define NOTE_F2   87.31
#define NOTE_Fs2  92.50
#define NOTE_G2   98.00
#define NOTE_Gs2 103.82
...

So basically we create an array of audio frequencies that comprise a note! The functions strum_up and strum_down turn on and off the notes that are defined in the cord with a given delay and velocity to synthesis the guitar strumming sound.

The full sketch is below:

#include <Audio.h>
#include <Wire.h>
#include <SD.h>
#include <SPI.h>
#include <SerialFlash.h>

#include "chords.h"

// Special thanks to Matthew Rahtz - http://amid.fish/karplus-strong/

AudioSynthKarplusStrong  string1;
AudioSynthKarplusStrong  string2;
AudioSynthKarplusStrong  string3;
AudioSynthKarplusStrong  string4;
AudioSynthKarplusStrong  string5;
AudioSynthKarplusStrong  string6;
AudioMixer4              mixer1;
AudioMixer4              mixer2;
AudioOutputI2S           i2s1;
AudioConnection          patchCord1(string1, 0, mixer1, 0);
AudioConnection          patchCord2(string2, 0, mixer1, 1);
AudioConnection          patchCord3(string3, 0, mixer1, 2);
AudioConnection          patchCord4(string4, 0, mixer1, 3);
AudioConnection          patchCord5(mixer1, 0, mixer2, 0);
AudioConnection          patchCord6(string5, 0, mixer2, 1);
AudioConnection          patchCord7(string6, 0, mixer2, 2);
AudioConnection          patchCord8(mixer2, 0, i2s1, 0);
AudioConnection          patchCord9(mixer2, 0, i2s1, 1);
//AudioControlSGTL5000     sgtl5000_1;
AudioControlWM8731      codecControl; // needed to enable the codec

const int finger_delay = 5;
const int hand_delay = 220;

int chordnum=0;

void setup() {
  AudioMemory(15);
  codecControl.enable();
  codecControl.volume(0.6);
  mixer1.gain(0, 0.15);
  mixer1.gain(1, 0.15);
  mixer1.gain(2, 0.15);
  mixer1.gain(3, 0.15);
  mixer2.gain(1, 0.15);
  mixer2.gain(2, 0.15);
  delay(700);
}

void strum_up(const float *chord, float velocity);
void strum_dn(const float *chord, float velocity);

void loop() {
  const float *chord;

  // each time through the loop, play a different chord
  if (chordnum == 0) {
    chord = Cmajor;
    Serial.println("C major");
    chordnum = 1;
  } else if (chordnum == 1) {
    chord = Gmajor;
    Serial.println("G major");
    chordnum = 2;
  } else if (chordnum == 2) {
    chord = Aminor;
    Serial.println("A minor");
    chordnum = 3;
  } else {
    chord = Eminor;
    Serial.println("E minor");
    chordnum = 0;
  }

  // then strum the 6 string several times
  strum_up(chord, 1.0);
  delay(hand_delay);
  delay(hand_delay);
  strum_up(chord, 1.0);
  delay(hand_delay);
  strum_dn(chord, 0.8);
  delay(hand_delay);
  delay(hand_delay);
  strum_dn(chord, 0.8);
  delay(hand_delay);
  strum_up(chord, 1.0);
  delay(hand_delay);
  strum_dn(chord, 0.8);
  delay(hand_delay);
  strum_up(chord, 1.0);
  delay(hand_delay);
  delay(hand_delay);
  strum_up(chord, 1.0);
  delay(hand_delay);
  strum_dn(chord, 0.7);
  delay(hand_delay);
  delay(hand_delay);
  strum_dn(chord, 0.7);
  delay(hand_delay);
  strum_up(chord, 1.0);
  delay(hand_delay);
  strum_dn(chord, 0.7);
  delay(hand_delay);

  Serial.print("Max CPU Usage = ");
  Serial.print(AudioProcessorUsageMax(), 1);
  Serial.println("%");
}

void strum_up(const float *chord, float velocity)
{
  if (chord[0] > 20.0) string1.noteOn(chord[0], velocity);
  delay(finger_delay);
  if (chord[1] > 20.0) string2.noteOn(chord[1], velocity);
  delay(finger_delay);
  if (chord[2] > 20.0) string3.noteOn(chord[2], velocity);
  delay(finger_delay);
  if (chord[3] > 20.0) string4.noteOn(chord[3], velocity);
  delay(finger_delay);
  if (chord[4] > 20.0) string5.noteOn(chord[4], velocity);
  delay(finger_delay);
  if (chord[5] > 20.0) string6.noteOn(chord[5], velocity);
  delay(finger_delay);
}

void strum_dn(const float *chord, float velocity)
{
  if (chord[5] > 20.0) string1.noteOn(chord[5], velocity);
  delay(finger_delay);
  if (chord[4] > 20.0) string2.noteOn(chord[4], velocity);
  delay(finger_delay);
  if (chord[3] > 20.0) string3.noteOn(chord[3], velocity);
  delay(finger_delay);
  if (chord[2] > 20.0) string4.noteOn(chord[2], velocity);
  delay(finger_delay);
  if (chord[1] > 20.0) string5.noteOn(chord[1], velocity);
  delay(finger_delay);
  if (chord[0] > 20.0) string6.noteOn(chord[0], velocity);
  delay(finger_delay);
}

MIDI Mania!

All of the processing above was done using the codec. But what if we want to receive or send MIDI messages? How can we use the TGA-Pro to interact with the MIDI protocol?

First you have to understand that MIDI is nothing more than a serial bus protocol that defines messages that describe musical objects and properties. To actually make sound, we still need to drive frequencies through and audio codec!

Basic MIDI with the TGA Pro Shield

The most basic MIDI processing to consider using the TGA Pro shield is receiving MIDI messages on the shield through the MIDI input port

Let’s take a look at how we might write a sketch to display incoming MIDI messages.

Before we describe our setup and loop functions, we need to first include the MIDI.h header file for the MIDI library, and then create a MIDI instance. MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); For more information in the parameters, see the documentation in the MIDI Library.

Now that we’ve established our MIDI instance, we can setup that instance by calling MIDI.begin(MIDI_CHANNEL_OMNI); We’re all set to process MIDI on our instance. So let’s jump to our loop function. Here we check to see if there’s a MIDI message ready to be read, and if so we get the message type and extract the MIDI object and property information so we can write it to the Serial Monitor screen.

So all this does is demonstrate that we’re getting MIDI messages from our MIDI device. But wait, where’s the sound? There is no sound. That only occurs when you render the messages and process it through and audio processor (like a codec). More on that next.

But first, even getting this sketch to properly receive MIDI messages may be a challenge depending on how you connect your MIDI devices to the input. If you’re using a Mac for example, you may need to use the Audio MIDI Setup application. Or, you might be able to simplify things by using an application like MidiKeys that allows you to select where to send the MIDI output.

Midi-Keys

#include <MIDI.h>

MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);

void setup() {
  MIDI.begin(MIDI_CHANNEL_OMNI);
  Serial.begin(57600);
  Serial.println("MIDI Input Test");
}

unsigned long t=0;

void loop() {
  int type, note, velocity, channel, d1, d2;
  //Serial.println("waiting for MIDI msg...");
  if (MIDI.read()) {                    // Is there a MIDI message incoming ?
    Serial.println("got a MIDI msg...");
    byte type = MIDI.getType();
    switch (type) {
      case midi::NoteOn:
        note = MIDI.getData1();
        velocity = MIDI.getData2();
        channel = MIDI.getChannel();
        if (velocity > 0) {
          Serial.println(String("Note On:  ch=") + channel + ", note=" + note + ", velocity=" + velocity);
        } else {
          Serial.println(String("Note Off: ch=") + channel + ", note=" + note);
        }
        break;
      case midi::NoteOff:
        note = MIDI.getData1();
        velocity = MIDI.getData2();
        channel = MIDI.getChannel();
        Serial.println(String("Note Off: ch=") + channel + ", note=" + note + ", velocity=" + velocity);
        break;
      default:
        d1 = MIDI.getData1();
        d2 = MIDI.getData2();
        Serial.println(String("Message, type=") + type + ", data = " + d1 + " " + d2);
    }
    t = millis();
  }
  if (millis() - t > 10000) {
    t += 10000;
    Serial.println("(inactivity)");
  }
}

MIDI + Sound

We demonstrated how to receive MIDI messages and print the objects and properties of those messages to the Serial Monitor, but that’s not too exciting. Let’s next look at how we might make some sound from those messages.

In order to make our MIDI messages render, we’ll need to look at yet another library - Wavetable-Synthesis. This library does a lot of cool things that result in producing sound from our MIDI messages. There are two key features of the library:

  • Soundfont decoding.
  • Wavetable class object.

What’s a soundfont? Just as the name implies, it’s a collection of sound samples and properties that describe an instrument and its corresponding sounds at certain notes in the MIDI protocol. We can think of a soundfont no differently than a character font for printing. Depending on the style of the writing, we can select the font that best conveys the tone of our printing (okay, pun intended!!).

Next, the library defines a Wavetable class library that manipulates these objects and lets us render them to an audio processor. In order to understand how to use the Wavetable class, it’s imperative that you review the documentation. It’s extensive and well worth the reading!

Assuming you’ve read the docs, take a look at the repo I wrote that modifies an example sketch to use it with the TGA-Pro. Program the Teensy with this sketch, connect your MidiKeys application to the input of the TGA-Pro shield, and listen to the sound rendered out of the TGA-Pro OUTPUT port!

comments powered by Disqus