Digital Music Programming II: key




This lab demonstrates how to timestamp notes as well as identify the musical key of the music being listened to by the computer. There are two inputs and three outputs to the key object as illustrated below:

The following Max patch demonstrates how to use the key object. In the patch, there is input from a MIDI keyboard which is echoed directly to MIDI output on the far right. On the left, the MIDI keyboard input note-ons are sent to the key object which remembers them for 7 seconds (the default memory time). The metro object causes the key object to generate an analysis every 50 milliseconds. The results of the analysis are shown in the three labeled boxes below the key object. The tonic output from the key object is then raised 7 octaves and sent MIDI output. The certainty of the analysis is used as the attack velocity of the outgoing analysis tonic note. When the key object is sure of its analysis, the outgoing notes will sound loud, but soft otherwise (such as the transition between two keys in a modulation).


Source Code

key.c

#include "ext.h"
#include <cmath>

#define BUFFERSZ 1000

typedef struct {
   long ts, note;
} NoteInfo;

typedef struct {
   t_object max_data;
   NoteInfo memory[BUFFERSZ];
   long     writeindex, windowsize, tonic, certainty, mode;
   void     *outputTonic, *outputCertainty, *outputMode;
} MyObject;

void* object_data = NULL;

void*  create_object       (long windowsizeinit);
void   InputBang           (MyObject* mo);
void   InputNote           (MyObject* mo, long value);
void   InputWindowSize     (MyObject* mo, long value);
void   MessageClear        (MyObject* mo);
long   midilimit           (long value);
void   doAnalysis          (MyObject* mo);
void   storeNoteInMemory   (MyObject* mo, long note, long timestamp);
void   makeHistogram       (double histogram[12], NoteInfo memory[BUFFERSZ],
                            long writeindex, long curtime, long windowsize);

void main(void) {
   setup((t_messlist**)&object_data, (method)create_object,
         NULL, sizeof(MyObject), NULL, A_DEFLONG, A_NOTHING);
   addbang((method)InputBang);
   addint ((method)InputNote);
   addinx ((method)InputWindowSize, 1);
}

void* create_object(long windowsizeinit) {
   MyObject* mo = (MyObject*)newobject(object_data);
   if (windowsizeinit == 0) InputWindowSize(mo, 7000);
   else InputWindowSize(mo, windowsizeinit);
   MessageClear(mo);
   mo->outputMode      = intout(mo);
   mo->outputCertainty = intout(mo);
   mo->outputTonic     = intout(mo);
   intin(mo, 1);
   return mo;
}

void InputBang(MyObject* mo) {
   doAnalysis(mo);
   outlet_int(mo->outputMode,      mo->mode);
   outlet_int(mo->outputCertainty, mo->certainty);
   outlet_int(mo->outputTonic,     mo->tonic);
}

void InputNote(MyObject* mo, long value) {
   storeNoteInMemory(mo, value, gettime());
}

void InputWindowSize(MyObject* mo, long value) {
   if (value < 10)          value = 10;
   else if (value > 600000) value = 600000;
   mo->windowsize = value;
}

void MessageClear(MyObject* mo) {
   int i;
   for (i=0; i<BUFFERSZ; i++) {
      mo->memory[i].ts = mo->memory[i].note = -1;
   }
   mo->writeindex = mo->tonic = mo->certainty = mo->mode = 0;
}

long midilimit(long value) {
   if (value < 0)     return   0;
   if (value > 127)   return 127;
   return value;
}

void storeNoteInMemory(MyObject* mo, long note, long timestamp) {
   mo->memory[mo->writeindex].note = midilimit(note);
   mo->memory[mo->writeindex].ts   = timestamp;
   mo->writeindex += 1;
   if (mo->writeindex >= BUFFERSZ) mo->writeindex = 0;
}

void makeHistogram(double histogram[12], NoteInfo memory[BUFFERSZ],
      long writeindex, long curtime, long windowsize) {
   int i;
   int index;
   for (i=0; i<12; i++) histogram[i] = 0.0;
   for (i=0; i<BUFFERSZ; i++) {
      index = writeindex - 1 - i;
      if (index < 0) index = (index + 2 * BUFFERSZ) % BUFFERSZ;
      if (curtime - memory[index].ts > windowsize) break;
      if (memory[index].note < 0) break;
      histogram[memory[index].note % 12] += 1.0;
   }
}

void doAnalysis(MyObject* mo) {
   double majorKey[12] =
      {6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88};
   double minorKey[12] =
      {6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17};
   double major_mean = 3.482500, minor_mean = 3.709167;
   double maj_temp, min_temp, temp, mean, r_major[12], r_minor[12];
   double maj_numerator, min_numerator, maj_denominator, min_denominator;
   double best_key, second_best_key, confidence, total = 0;
   int    subscript, i, j, sec_pitch_class, pitch_class;
   long   mode = 0;
   double histogram[12];
   makeHistogram(histogram, mo->memory, mo->writeindex, gettime(), mo->windowsize);
   for (i=0; i<12; i++) total += histogram[i];
   mean = total/12.0;
   for (i=0; i<12; i++) {
      maj_numerator   = min_numerator   = 0;
      maj_denominator = min_denominator = 0;
      maj_temp = min_temp = temp = 0;
      for (j=0; j<12; j++) {
         subscript = (i+j) % 12;
         temp += (histogram[subscript] - mean) * (histogram[subscript] - mean);
         maj_numerator += (majorKey[j]-major_mean) * (histogram[subscript]-mean);
         maj_temp += (majorKey[j] - major_mean) * (majorKey[j] - major_mean);
         min_numerator += (minorKey[j]-minor_mean) * (histogram[subscript]-mean);
         min_temp += (minorKey[j] - minor_mean) * (minorKey[j] - minor_mean);

      }
      maj_denominator = sqrt(maj_temp * temp);
      min_denominator = sqrt(min_temp * temp);
      if (maj_denominator == 0 || min_denominator == 0) {
         mo->tonic = mo->certainty = mo->mode;
         return;
      }
      r_major[i] = maj_numerator / maj_denominator;
      r_minor[i] = min_numerator / min_denominator;
   }
   best_key = 0.0;
   pitch_class = 0;
   for (i=0; i<12; i++) {
      if (r_major[i] > best_key) {
         best_key = r_major[i];
         mode = 0;
         pitch_class = i;
      }
      if (r_minor[i] > best_key) {
         best_key = r_minor[i];
         mode = 1;
         pitch_class = i;
      }
   }
   second_best_key = 0;
   sec_pitch_class = 0;
   for (i=0; i<12; i++) {
      if (r_major[i] != best_key && r_major[i] > second_best_key) {
         second_best_key = r_major[i];
         sec_pitch_class = i;
      }
      if (r_minor[i] != best_key && r_minor[i] > second_best_key) {
         second_best_key = r_minor[i];
         sec_pitch_class = i;
      }
   }
   confidence = (best_key - second_best_key) * 100 * 3.0;
   if (confidence > 100.0) confidence = 100.0;
   mo->tonic     = pitch_class;
   mo->certainty = (int)(127.0 * confidence/100.0);
   mo->mode      = mode;
}

The algorithm used to identify the key of the music is called the Krumhansl-Schmuckler key-finding algorithm which was developed by Carol Krumhansl and Mark Schmuckler in the early 1980's.

The basic principle of the algorithm is to compare a prototypical major (or minor) scale-degree profile with the input music. For example, if you are in C major, then you expect to find lots of C notes and also lots of G notes. Additionally, you do not expect to find lots of B-flats or F-sharps in C major.




Exercises

  1. Compile and test the key external object with the Max patch illustrated above.

  2. If you did extra-credit #3 for Homework #2, then send the tonic output from the key object into your happy/sad music converter and examine how music played into the happy/sad object behaves when they key of the music you play modulates. Here is an example patch to follow:



  3. Try changing the scale-degree weightings for identifying other modes or non-western modes. Use large weightings for expected notes in the scale and small weighting for notes note expected in the scale.