3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Wavetable synthesis is so much easier on low-end hardware. Not even kidding. \$\endgroup\$ – Ignacio Vazquez-Abrams Jul 24 '14 at 0:05
  • \$\begingroup\$ I am not familiar with MIDI + Wavetable Synth with the Arduino. Would you happen to have a link to a good source of data and examples? I googled it but haven't found any extensive documentation on it, just a few small projects here and there \$\endgroup\$ – DerStrom8 Jul 24 '14 at 0:27
  • \$\begingroup\$ Haven't seen one. But it can be done from scratch more easily than mashing square waves together. \$\endgroup\$ – Ignacio Vazquez-Abrams Jul 24 '14 at 0:30
  • \$\begingroup\$ The output must be a square wave, as it will serve as an interrupter for the next part of the project. I guess I'm not familiar enough with wavetable synthesis to understand how it could be applied to this project. Thanks for the suggestion though, I'll look into it. \$\endgroup\$ – DerStrom8 Jul 24 '14 at 0:36
  • \$\begingroup\$ Oh. I thought you were using the Arduino to generate audio. Never mind then. \$\endgroup\$ – Ignacio Vazquez-Abrams Jul 24 '14 at 0:39
2
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

I'm on a tablet so I'll have to keep this kinda brief. To have multiple tones, you'll want a structure for each voice in your polyphonic instrument that holds the tone MIDI note number, a constant which holds the output pin, a "phase accumulator" for the tone, and an angle increment for the phase accumulator. You then initialize these structures with the output pins you want and other fields empty and store them in some kind of data structure (a stack would be good). This is then the "unused note stack."

When a MIDI Note On message comes in, you call a function to pop an unused note off the stack, and u se the note number and a lookup table to find the appropriate angle increment for the phase accumulator. Then the filled out structure will be put in an empty slot in an array called the "playing notes array." When a Note Off message comes i n, a function is called that looks through that array to find the note that was turned off, and removes the structure from the array and places it back in the unused note stack.

There will also be a timer interrupt called at some rate depending upon the highest output frequency square wave you need. The phase accumulator increments will be calculated based on this rate. In the interrupt the array of playing notes will be scanned through, and all the phase accumulators updated. To generate the square waves the output pins will be set low when the top byte of the respective phase accumulator is less than 128, high when greater.

Edit: to make things faster, you could just malloc a bunch of structs for each voice at runtime. Then use the pointers instead of moving around the structs themselves.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ What you describe with the structures is exactly how the Tone library works. You initialize a "Tone" with a pin number (which means I'm limited to 6 tones, since I have 6 available pins), and then you can send a note to the tone structure, and it turns it on by using Tone.play(). You can then turn off the tone by using Tone.stop(). However, the difficult part is indexing the correct tone to turn off the specified note. That is one of the roadblocks I kept running into. I can turn on a tone, but when I receive a note off message, I don't know how to tell it which tone to stop \$\endgroup\$ – DerStrom8 Jul 24 '14 at 14:35
  • \$\begingroup\$ @derstrom8 The documentation for the tone library states that only one tone can be played at a time - "Only one tone can be generated at a time. If a tone is already playing on a different pin, the call to tone() will have no effect. If the tone is playing on the same pin, the call will set its frequency." \$\endgroup\$ – Bitrex Jul 24 '14 at 14:50
  • \$\begingroup\$ If you re-read my original post you will see that I am not using the standard tone library. I am using a completely different one, which does support polyphonic sounds. I even linked to it in the post. \$\endgroup\$ – DerStrom8 Jul 24 '14 at 15:21
  • \$\begingroup\$ @derstrom8 Sorry about that! I somehow missed that you were using the non native library. Well then it should be simpler. I'll try to edit this answer when I get a chance... \$\endgroup\$ – Bitrex Jul 24 '14 at 15:50
  • \$\begingroup\$ @derstrom8 Basically the idea with the stack of structure pointers should work - youd just store a constant pin number in each structure, along with an initially empty note number. Then when a note on comes in you turn the note on, associate a note number with a pin by storing the note number in the struct, and move the structure pointer into the "playing notes" array. Then when a note off comes in you scan through the structs pointed to by the pointers in the array and find the struct that has the correct note, use the associated pin to turn it off, then move the pointer back to the stack. \$\endgroup\$ – Bitrex Jul 24 '14 at 16:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.