Electronic – arduino – MIDI To Polyphonic Square Wave on Arduino

arduinomidi

I've been struggling with this for a few days now, and I would really like to get on with my project. Unfortunately this is holding me up. What I am trying to do is retrieve MIDI signals sent over USB to the Arduino (Uno), decode them, and then use a non-native Tone library (https://code.google.com/p/rogue-code/wiki/ToneLibraryDocumentation) to play multiple square wave tones at once. I have 6 I/O ports left on my Arduino to work with, so I expect that limits me to 6 tones. I have tried several different methods of making this work, but have never been able to finish any of them due to running into a roadblock in one way or another. My hope is to play multiple tones, whether they are from the same channel (chords) or from different channels (separate instruments), but I still cannot seem to figure out a good way to do this.

Reading in the MIDI data is not the difficult part. I have the following code, written by Greg Kennedy and posted to the Arduino forums (http://forum.arduino.cc/index.php?topic=79326.0), to retrieve data from a specific MIDI channel and play it using the native tone() function, but alas it can only play one note at a time.

// A very simple MIDI synth.
// Greg Kennedy 2011

#include <avr/pgmspace.h>

#define statusLed 13
#define tonePin 7

// MIDI channel to answer to, 0x00 - 0x0F
#define myChannel 0x00
// set to TRUE and the device will respond to all channels
#define respondAllChannels false

// midi commands
#define MIDI_CMD_NOTE_OFF 0x80
#define MIDI_CMD_NOTE_ON 0x90
#define MIDI_CMD_KEY_PRESSURE 0xA0
#define MIDI_CMD_CONTROLLER_CHANGE 0xB0
#define MIDI_CMD_PROGRAM_CHANGE 0xC0
#define MIDI_CMD_CHANNEL_PRESSURE 0xD0
#define MIDI_CMD_PITCH_BEND 0xE0

// this is a placeholder: there are
//  in fact real midi commands from F0-FF which
//  are not channel specific.
// this simple synth will just ignore those though.
#define MIDI_CMD_SYSEX 0xF0

// a dummy "ignore" state for commands which
//  we wish to ignore.
#define MIDI_IGNORE 0x00

// midi "state" - which data byte we are receiving
#define MIDI_STATE_BYTE1 0x00
#define MIDI_STATE_BYTE2 0x01

// MIDI note to frequency
//  This isn't exact and may sound a bit detuned at lower notes, because
//  the floating point values have been rounded to uint16.
//  Based on A440 tuning.

// I would prefer to use the typedef for this (prog_uint16_t), but alas that triggers a gcc bug
// and does not put anything into the flash memory.

// Also note the limitations of tone() which at 16mhz specifies a minimum frequency of 31hz - in other words, notes below
// B0 will play at the wrong frequency since the timer can't run that slowly!
uint16_t frequency[128] PROGMEM = {8, 9, 9, 10, 10, 11, 12, 12, 13, 14, 15, 15, 16, 17, 18, 19, 21, 22, 23, 24, 26, 28, 29, 31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186, 4435, 4699, 4978, 5274, 5588, 5920, 5920, 6645, 7040, 7459, 7902, 8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544};

//setup: declaring iputs and outputs and begin serial
void setup() {
  pinMode(statusLed,OUTPUT);   // declare the LED's pin as output

  pinMode(tonePin,OUTPUT);           // setup tone output pin

  //start serial with midi baudrate 31250
  // or 38400 for debugging (eg MIDI over serial from PC)
  Serial.begin(31250);

  // indicate we are ready to receive data!
  digitalWrite(statusLed,HIGH);
}

//loop: wait for serial data
void loop () {
  static byte note;
  static byte lastCommand = MIDI_IGNORE;
  static byte state;
  static byte lastByte;

  while (Serial.available()) {

    // read the incoming byte:
    byte incomingByte = Serial.read();

    // Command byte?
    if (incomingByte & 0b10000000) {
      if (respondAllChannels ||
             (incomingByte & 0x0F) == myChannel) { // See if this is our channel
        lastCommand = incomingByte & 0xF0;
      } else { // Not our channel.  Ignore command.
        lastCommand = MIDI_IGNORE;
      }
      state = MIDI_STATE_BYTE1; // Reset our state to byte1.
    } else if (state == MIDI_STATE_BYTE1) { // process first data byte
      if ( lastCommand==MIDI_CMD_NOTE_OFF )
      { // if we received a "note off", make sure that is what is currently playing
        if (note == incomingByte) noTone(tonePin);
        state = MIDI_STATE_BYTE2; // expect to receive a velocity byte
      } else if ( lastCommand == MIDI_CMD_NOTE_ON ){ // if we received a "note on", we wait for the note (databyte)
        lastByte=incomingByte;    // save the current note
        state = MIDI_STATE_BYTE2; // expect to receive a velocity byte
      }
      // implement whatever further commands you want here
    } else { // process second data byte
      if (lastCommand == MIDI_CMD_NOTE_ON) {
        if (incomingByte != 0) {
          note = lastByte;
          tone(tonePin,(unsigned int)pgm_read_word(&frequency[note]));
        } else if (note == lastByte) {
          noTone(tonePin);
        }
      }
      state = MIDI_STATE_BYTE1; // message data complete
                                 // This should be changed for SysEx
    }
  }
}

This code works great for monophonic audio, but I really need to produce polyphonic tones. I've always been more of a PIC guy, so Arduino is still fairly new to me. Thanks guys!

EDIT: Just to be clear, I want to convert a multi-tone MIDI input to a multi-tone square wave output, which I will be using as an interrupter signal for another part of the project.

Best Answer

Playing polyphonic music by using separate circuitry for each "voice" works very nicely when music is logically subdivided into a number of single-voice channels. In many cases, one not only won't care if the hardware voices aren't perfectly matched--one may even want to have some of them be a little louder than others.

When using a polyphonic instrument to play MIDI data, however, it's important to ensure that all voices behave equivalently, and the easiest way to accomplish that is to use the same circuitry to implement all of them, typically with some sort of wave-table generation.

There are many ways to perform wave-table-based music, which trade off sophistication for CPU time and memory requirements. I've written a four-voice wave-table generator for the 6502 which used twelve instructions (46 cycles) for each output sample [an average of only three per voice!] but it required almost 3K worth of data tables. I've also written an 8-voice wave-table generator for a 10MHz PIC and a 16-voice one for the PSOC. I've not done much with the Arduino, but I would guess an eight-voice wave-table synth would be workable.

There's an important thing to note, however, when doing a wave-table synth: square waves often sound bad if timing is quantized to something that isn't a multiple of their frequency. If you wish to generate clean square waves for a five-octave MIDI keyboard, you may need to use a rather high sampling rate. If you want to generate smoother waves instead you can get by with a lower sampling rate.

I've not used the Arduino, so I don't know what sorts of constructs in C would yield the best code, but a typical wave-table implementation on the ARM would look like:

uint32_t freq[8],phase[8];
int32_t volume[8];
int8_t *table[8]; // Pointer to 256-byte array
int32_t total;

total = 0;
phase[0] += freq[0]; total += table[0][phase[0] >> 8]*volume[0];
phase[1] += freq[1]; total += table[1][phase[1] >> 8]*volume[1];
...
phase[7] += freq[7]; total += table[7][phase[7] >> 8]*volume[7];

After the above code, total will hold a value which may be scaled (if needed) and either output to a DAC or used to set a PWM duty cycle.

The above code assumes that the wave table is exactly 256 bytes long; other approaches can be used if that isn't the case. If the Arduino can't efficiently perform the multiplication by volume, it's possible to use a different table for each volume level (my 6502 code used two sets of tables--one for "loud" and one for "soft").

An additional consideration if you're using a DAC (less applicable if you're using a PWM) is that rather than summing together all the individual values generated for each wave and outputting them as a group, it may be helpful have an interrupt which happens eight times as fast, and outputs the value for one wave each time. Doing that will effectively gain three more bits of ADC precision. Also, if you happen to want sine waves and your hardware doesn't support fast multiplication, generating two full-amplitude sine waves whose frequencies match but whose phases differ, is equivalent to generating one sine wave whose phase is the average of the two full-strength ones, and whose amplitude is proportional to the quantity "one plus the cosine of the phase difference". My PIC-based waveform generator used that trick.