Close

MIDI Coding

A project log for Simple Concurrent Tones for the Arduino

The Arduino tone library does not support concurrent multiple tones. Now I am sure someone has done this before but here is my take.

agpcooperagp.cooper 09/24/2017 at 08:412 Comments

MIDI Coding

I can now generate concurrent audio tones on pins A0-5.

The Loading The MIDI source into the Arduino

The good way of doing this is to just load the binary as a C array directly into the Arduino sketch. It is quite compact allowing bigger MID files. I used "srec_cat" from https://sourceforge.net/projects/srecord/files/srecord-win32/.

Using "srec_cat.exe popcorn.mid -Binary -o popcorn.hex -C-Array MidiData", this creates an array:

/* http://srecord.sourceforge.net/ */
const unsigned char MidiData[] =
{
0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x04,
...
0x0A, 0x40, 0x00, 0x5D, 0x00, 0x00, 0x07, 0x64, 0x00, 0x5B, 0x00, 0x89,
0xEB, 0x7F, 0xFF, 0x2F, 0x00,
};
const unsigned long MidiData_termination = 0x00000000;
const unsigned long MidiData_start       = 0x00000000;
const unsigned long MidiData_finish      = 0x00002A4D;
const unsigned long MidiData_length      = 0x00002A4D;
#define MIDIDATA_TERMINATION 0x00000000
#define MIDIDATA_START       0x00000000
#define MIDIDATA_FINISH      0x00002A4D
#define MIDIDATA_LENGTH      0x00002A4D

Which is stored in program space using PROGMEM:

/* PopCorn */
#include <avr/pgmspace.h>
const byte PROGMEM MidiData[] = {
0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x04,
...

Magic now I just have to process the code.

The Processing Code

Now I need to consider the MIDI processing system.

It may seem simple, just turn on or off tones with duration but the MIDI code has to be queued and executed at the right time (the event). To do that I will need a master or global "tick counter" and a "next event counter" for each channel.

The masker tick counter would be handled by the ISR. Instead of a frequency, a duration would be set. The ISR would increment a global or master tick counter upon timeout.

The next event counter would be incremented with duration as channel data is processed. Then no new data would be processed until the master tick equals or exceeds the next event counter.

Decoding MIDI Files

The MIDI code format is not that difficult but it will take time to write the decoder.

The webpage describes the format:

     https://www.csie.ntu.edu.tw/~r92092/ref/midi/

The above MIDI file description is really good! As you read the description it answers the questions in your mind.

At first I was horrified by the variable data format but once I understood how simple it was I am converted!

Reading the Header

Here is the code so far, it reads the header and exits (I have clipped the MIDI data array to keep it short):

/*
   A Concurrent Tone Sketch
   Six channels: A0-A5
   Maximum frequency is 5957 Hz
   Procedure is SetTone(Pin,Freq);
   Set Freq to zero to turn off tone
   
   Author: agp.cooper@gmail.com
*/
/* PopCorn */
#include <avr/pgmspace.h>
const byte PROGMEM MidiData[] = {
0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x04,
...
0x0A, 0x40, 0x00, 0x5D, 0x00, 0x00, 0x07, 0x64, 0x00, 0x5B, 0x00, 0x89,
0xEB, 0x7F, 0xFF, 0x2F, 0x00,
};
const unsigned long MidiData_termination = 0x00000000;
const unsigned long MidiData_start       = 0x00000000;
const unsigned long MidiData_finish      = 0x00002A4D;
const unsigned long MidiData_length      = 0x00002A4D;
#define MIDIDATA_TERMINATION 0x00000000
#define MIDIDATA_START       0x00000000
#define MIDIDATA_FINISH      0x00002A4D
#define MIDIDATA_LENGTH      0x00002A4D
// Define various ADC prescaler
const byte PS_16=(1<<ADPS2);
const byte PS_32=(1<<ADPS2)|(1<<ADPS0);
const byte PS_64=(1<<ADPS2)|(1<<ADPS1);
const byte PS_128=(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);
/* ISR: Tone on A0-5 */
#define TickDuration 1 // 1 ms 
volatile unsigned int magic=0;
volatile unsigned int magic0=0;
volatile unsigned int magic1=0;
volatile unsigned int magic2=0;
volatile unsigned int magic3=0;
volatile unsigned int magic4=0;
volatile unsigned int magic5=0;
volatile unsigned int phase=0;
volatile unsigned int phase0=0;
volatile unsigned int phase1=0;
volatile unsigned int phase2=0;
volatile unsigned int phase3=0;
volatile unsigned int phase4=0;
volatile unsigned int phase5=0;
volatile unsigned long master=0;
ISR(TIMER2_OVF_vect) {
  if (phase<0x8000) {
    phase+=magic;
    if (phase>=0x8000) master++;
  } else {
    phase+=magic;    
  }
  phase0+=magic0;
  phase1+=magic1;
  phase2+=magic2;
  phase3+=magic3;
  phase4+=magic4;
  phase5+=magic5;
  if (phase0<0x8000) PORTC&=B11111110; else PORTC|=B00000001;
  if (phase1<0x8000) PORTC&=B11111101; else PORTC|=B00000010;
  if (phase2<0x8000) PORTC&=B11111011; else PORTC|=B00000100;
  if (phase3<0x8000) PORTC&=B11110111; else PORTC|=B00001000;
  if (phase4<0x8000) PORTC&=B11101111; else PORTC|=B00010000;
  if (phase5<0x8000) PORTC&=B11011111; else PORTC|=B00100000;
}
void SetTone(unsigned int Pin,unsigned int Freq) {
  // Max Frequency is 5957 Hz
  if (Pin==0) magic0=4*(Freq*11/21);
  if (Pin==1) magic1=4*(Freq*11/21);
  if (Pin==2) magic2=4*(Freq*11/21);
  if (Pin==3) magic3=4*(Freq*11/21);
  if (Pin==4) magic4=4*(Freq*11/21);
  if (Pin==5) magic5=4*(Freq*11/21);
  // Turn it off
  if (Freq==0) {
    if (Pin==0) phase0=0;
    if (Pin==1) phase1=0;
    if (Pin==2) phase2=0;
    if (Pin==3) phase3=0;
    if (Pin==4) phase4=0;
    if (Pin==5) phase5=0;
  }
}
void setup() {
  // Header Variables
  unsigned int format=0;
  unsigned int nTrks=0;
  unsigned int trk;
  unsigned int division=0;
  bool BadHeader=false;
  bool BadTrack=false;
  // Temporary data variables
  byte Data0;
  byte Data1;
  byte Data2;
  byte Data3;
  // Decoding varaibles
  unsigned long DeltaTick=0;
  unsigned long TrackPtr[6];
  
  pinMode(LED_BUILTIN,OUTPUT);
  pinMode(A0,OUTPUT);
  pinMode(A1,OUTPUT);
  pinMode(A2,OUTPUT);
  pinMode(A3,OUTPUT);
  pinMode(A4,OUTPUT);
  pinMode(A5,OUTPUT);
  pinMode(11,OUTPUT); // Used as a frequency check
  // Set ADC prescaler (assume a 16 MHz clock)
  ADCSRA&=~PS_128;                                 // Remove bits set by Arduino library
  ADCSRA|=PS_16;                                   // 16 prescaler (1 MHz)
  // Disable interrupts
  cli();
  
  // Use Timer 2 for ISR (Output on D11 for frequency check)
  // Good for ATmega48A/PA/88A/PA/168A/PA/328/P
  TIMSK2 = 0;                                      // Timer interrupts off
  TCCR2A = (2 << COM2A0)|(1 << WGM20);             // Phase correct PWM (31275.5 Hz), toggle output on OC2A (PB3/D11)
  TCCR2B = (0 << WGM22)|(1 << CS20);               // 16 MHz clock (no pre-scaler)
  OCR2A = 128;                                     // Set 50% duty
  TIMSK2 = (1<<TOIE2);                             // Set interrupt on overflow (=BOTTOM)
  // Enable interrupts 
  sei();   
  // Set tick duration (ms)
  magic=2095*TickDuration;
  /* Read MIDI Headers */
  // Header Id
  Data0=pgm_read_byte(MidiData+3);
  Data1=pgm_read_byte(MidiData+2);
  Data2=pgm_read_byte(MidiData+1);
  Data3=pgm_read_byte(MidiData+0);
  if ((Data3!='M')&&(Data2!='T')&&(Data1!='h')&&(Data0!='d')) BadHeader=true;
  // Header Records
  Data0=pgm_read_byte(MidiData+7);
  Data1=pgm_read_byte(MidiData+6);
  Data2=pgm_read_byte(MidiData+5);
  Data3=pgm_read_byte(MidiData+4);
  // Set the first track pointer
  TrackPtr[0]=Data0+8;
  // MIDI format
  Data0=pgm_read_byte(MidiData+9);
  Data1=pgm_read_byte(MidiData+8);
  format=((unsigned int)Data1<<8)+Data0;
  if ((format!=1)&&(format!=2)&&(format==3)) BadHeader=true;
  // MIDI tracks
  Data0=pgm_read_byte(MidiData+11);
  Data1=pgm_read_byte(MidiData+10);
  nTrks=((unsigned int)Data1<<8)+Data0;
  // MIDI division (ticks per quarter staff)
  Data0=pgm_read_byte(MidiData+13);
  Data1=pgm_read_byte(MidiData+12);
  division=((unsigned int)Data1<<8)+Data0;
  // Only ticks per quarter staff accepted
  if (division&0x80==0x80) BadHeader=true;
  if (!BadHeader) {
    // Set remaining track pointers
    for (trk=0;trk<6;trk++) {
      if (trk<nTrks) {
        Data0=pgm_read_byte(MidiData+TrackPtr[trk]+3);
        Data1=pgm_read_byte(MidiData+TrackPtr[trk]+2);
        Data2=pgm_read_byte(MidiData+TrackPtr[trk]+1);
        Data3=pgm_read_byte(MidiData+TrackPtr[trk]+0);
        if ((Data3!='M')&&(Data2!='T')&&(Data1!='r')&&(Data0!='k')) BadTrack=true;
        if (trk+1<6) {
          Data0=pgm_read_byte(MidiData+TrackPtr[trk]+7);
          Data1=pgm_read_byte(MidiData+TrackPtr[trk]+6);
          Data2=pgm_read_byte(MidiData+TrackPtr[trk]+5);
          Data3=pgm_read_byte(MidiData+TrackPtr[trk]+4);
          TrackPtr[trk+1]=TrackPtr[trk]+((unsigned int)Data3<<24)+((unsigned int)Data2<<16)+((unsigned int)Data1<<8)+Data0+8;
        }
      }
    }
  }
  Serial.begin(9600);
  delay(100);
  if (!BadHeader) {
    Serial.println("Header Okay:");
  } else {
    Serial.println("Header Bad?:");     
  }
  Serial.print("  Format ");
  Serial.println(format);
  Serial.print("  Tracks ");
  Serial.println(nTrks);
  Serial.print("  Division ");
  Serial.println(division);
  delay(100);
  if (!BadTrack) {
    Serial.println("Track Headers Okay:");
  } else {
    Serial.println("Track Headers Bad?:");     
  }
  for (trk=0;trk<6;trk++) {
    if (trk<nTrks) {
      Serial.print("  TrackPtr");
      Serial.print(trk);
      Serial.print(" ");
      Serial.println(TrackPtr[trk]);
    }
  }   
  Serial.end();
}
void loop() {
  delay(1000);
  digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
}

And this is what I get:

Header Okay:
  Format 1
  Tracks 4
  Division 480
Track Headers Okay:
  TrackPtr0 14
  TrackPtr1 715
  TrackPtr2 6511
  TrackPtr3 10781

 Magic.

I should probably point out that the MIDI standard for format "1" is that Track 1 contains the "Tempo Map", so there is only three music tracks in this file (Tracks 2-4).

The MIDI Standard

The track "data" format is:

  [<Variable Length "Delta Time"><Variable Length "Event">]*

The MIDI Standard describes lots of interesting "Events" that can be done but I am only interested in tones, so I need to write code to recognise and skip non-tone events.

Variable Length Delta Time

Rather than describe it, here is a short code fragment:

{
  unsigned long DeltaTime;
  byte Data;
  Data=NextByte();
  DeltaTime=Data&0x7F;
  while (Data>=0x80) {
    Data=NextByte();
    DeltaTime=DeltaTime<<7;
    DeltaTime+=Data&0x7F;
  }
}

TBC ...

AlanX

Discussions

agp.cooper wrote 09/25/2017 at 15:38 point

Hi Stuart,

I had a good look at you suggestion (to be fair). I ran the calculations, built two sketches for both versions. Your version works fine except at high frequencies (above 1k Hz when using a 31275.5Hz ISR clock), it can't discriminate between adjacent keys (keys as in piano keys). But the sound quality is better (no sub-harmonics). Yes, the ISR clock frequency can be increased, this pushes the key problem into higher frequencies, but then you will have less time between interrupts to do work.

I posted a log if your interest.

Anyway, I do appreciate the suggestion, regards AlanX

  Are you sure? yes | no

agp.cooper wrote 09/25/2017 at 03:10 point

Yes but ...

The method used is pretty standard for digital signal processing. Its call Direct Digital Synthesis (DDS).

It is a counter but in "magic" steps to match the "phase" word width (16 bit in this case).

Why use it?

1) It is just as simple to code.

2) The long term frequency accuracy is much much better than simple counter and can be increased by using a wider phase word width.

3) The actual interrupt frequency (which there are very limited choices available) is not important for long term frequency accuracy.

4) The high order bits of the phase register can be used with an ADC to generate a sine wave! Yes you can generate sine waves directly, I just took the MSB for a square in this application.

Trust me, its the way to go!

Regards AlanX

  Are you sure? yes | no