// // Programmer: Craig Stuart Sapp <craig@ccrma.stanford.edu> // Creation Date: Sun May 16 12:54:28 PDT 1999 // Last Modified: Sun May 31 19:49:53 EDT 2009 (added alternative weights) // Filename: .../improv/examples/synthImprov/rtkey/rtkey.cpp // Syntax: C++; synthImprov 2.1 // // Description: analyze the key of musical input and output the determined // key as a note on the tonic of the key. The certainty of // the measurement is mapped to the loudness on the analysis // note. // // References: Key analysis is done using the Krumhansl-Schmuckler // key-finding algorithm which measures Pearson correlation // of the musical data against prototype pitch distributions // of major and minor key profiles. The key which produces // the highest correlation is determined to be the key. // #include "synthImprov.h" #include <math.h> #ifndef OLDCPP #include <iostream> #include <iomanip> using namespace std; #else #include <iostream.h> #include <iomanip.h> #endif /*----------------- beginning of improvization algorithms ---------------*/ MidiMessage message; // for extracting notes from the synthesizer CircularBuffer<char> notes; // storage for notes being played CircularBuffer<long> times; // storage for note times being played double occurrences[12] = {0}; // number of notes occurrences int fadeNote = 0; // next to to go out of scope int fadeTime = 0; // next time to go out of scope int keyoctave = 7; // the analysis key performance octave int displayKey2 = 0; // display the second key possibility int octave = 4; // used for keyboard keyboard int freezeQ = 0; // used with 'f' keyboard command SigTimer metronome; // for display period of key analysis double analysisDuration; // duration in seconds of analysis window double tempo; // tempo of the rtkeyalysis // key profile weights: weights repeated twice to make calculations // simpler for input to pearsonCorrelation(). // Aarden 2003 double AardenEssenMajor[24] = {17.7661, 0.145624, 14.9265, 0.160186, 19.8049, 11.3587, 0.291248, 22.062, 0.145624, 8.15494, 0.232998, 4.95122, 17.7661, 0.145624, 14.9265, 0.160186, 19.8049, 11.3587, 0.291248, 22.062, 0.145624, 8.15494, 0.232998, 4.95122}; double AardenEssenMinor[24] = {18.2648, 0.737619, 14.0499, 16.8599, 0.702494, 14.4362, 0.702494, 18.6161, 4.56621, 1.93186, 7.37619, 1.75623, 18.2648, 0.737619, 14.0499, 16.8599, 0.702494, 14.4362, 0.702494, 18.6161, 4.56621, 1.93186, 7.37619, 1.75623}; // Bellman 2005 double BellmanBudgeMajor[24] = {16.80, 0.86, 12.95, 1.41, 13.49, 11.93, 1.25, 20.28, 1.80, 8.04, 0.62, 10.57, 16.80, 0.86, 12.95, 1.41, 13.49, 11.93, 1.25, 20.28, 1.80, 8.04, 0.62, 10.57}; double BellmanBudgeMinor[24] = {18.16, 0.69, 12.99, 13.34, 1.07, 11.15, 1.38, 21.07, 7.49, 1.53, 0.92, 10.21, 18.16, 0.69, 12.99, 13.34, 1.07, 11.15, 1.38, 21.07, 7.49, 1.53, 0.92, 10.21}; // Krumhansl & Kessler (1982) weightings double KrumhanslKesslerMajor[24] = {6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88, 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 KrumhanslKesslerMinor[24] = {6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17, 6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17}; // Craig 2003 double SimpleMajor[24] = {2, 0, 1, 0, 1, 1, 0, 2, 0, 1, 0, 1, 2, 0, 1, 0, 1, 1, 0, 2, 0, 1, 0, 1}; double SimpleMinor[24] = {2, 0, 1, 1, 0, 1, 0, 2, 1, 0, 1, 0, 2, 0, 1, 1, 0, 1, 0, 2, 1, 0, 1, 0}; // Temperley 2007 double TemperleyMajor[24] = {0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400, 0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400}; double TemperleyMinor[24] = {0.712, 0.084, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330, 0.712, 0.084, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330}; double* majorKey = TemperleyMajor; // weights for analysis of major keys double* minorKey = TemperleyMinor; // weights for analysis of minor keys Voice firstVoice; // for primary key voice Voice secondVoice; // for secondary key voice double offFraction = 0.75; // for turning off next keyvoice. // function declarations: void analyzekey(void); void storeNote(char storeNote, long storeTime); void keyboard(int key); const char* name(int pitch_class); double pearsonCorrelation(int size, double* x, double* y); /*--------------------- maintenance algorithms --------------------------*/ ////////////////////////////// // // description -- this function is called by the improv interface // whenever a capital "D" is pressed on the computer keyboard. // Put a description of the program and how to use it here. // void description(void) { printboxtop(); psl("Keyan -- analyze musical input for key"); psl(""); psl(" < = Slow down analysis note tempo > = Speed up analysis note tempo"); psl(" @ = Toggle playing of secondary choice - = Clear note memory"); psl(" A = Use Aarden-Essen key profiles B = Use Bellman-Budge key profiles"); psl(" P = Use Simple key profiles T = Use Temperley key profiles"); psl(" K = Use Krumhansl-Kessler key profiles"); psl(" "); psl( " \"0\"-\"9\" = octave number of computer keyboard notes"); psl(" Notes: s d g h j "); psl(" z x c v b n m "); printboxbottom(); } ////////////////////////////// // // initialization -- this function is called by the improv // intervace once at the start of the program. Put items // here which need to be initialized at the beginning of // the program. // void initialization(void) { analysisDuration = 7.0; // duration in seconds of analysis window tempo = 60.0; // tempo of the rtkeyalysis metronome.setTempo(tempo); firstVoice.setChannel(0); secondVoice.setChannel(0); notes.setSize(10000); times.setSize(10000); } ////////////////////////////// // // finishup -- this function is called by the improv interface // whenever the program is exited. Put items here which // need to be taken care of when the program is finished. // void finishup(void) { firstVoice.off(); secondVoice.off(); } /*-------------------- main loop algorithms -----------------------------*/ ////////////////////////////// // // mainloopalgorithms -- this function is called by the improv interface // continuously while the program is running. The global variable t_time // which stores the current time is set just before this function is // called and remains constant while in this functions. // void mainloopalgorithms(void) { while (synth.getNoteCount() > 0) { message = synth.extractNote(); if (message.p2() != 0) { storeNote(message.p1(), t_time); } } if (metronome.expired()) { analyzekey(); metronome.reset(); } if (metronome.getPeriodCount() > offFraction) { firstVoice.off(); secondVoice.off(); } } /*-------------------- triggered algorithms -----------------------------*/ ////////////////////////////// // // keyboard -- simulate a MIDI keyboard on the computer keyboard. // void keyboard(int key) { static int keyboardnote = -1; // computer keyboard note if (keyboardnote < -1) { synth.play(0, keyboardnote, 0); } switch (key) { case 'z': keyboardnote = 12 * octave + 0; break; // C case 's': keyboardnote = 12 * octave + 1; break; // C# case 'x': keyboardnote = 12 * octave + 2; break; // D case 'd': keyboardnote = 12 * octave + 3; break; // D# case 'c': keyboardnote = 12 * octave + 4; break; // E case 'v': keyboardnote = 12 * octave + 5; break; // F case 'g': keyboardnote = 12 * octave + 6; break; // F# case 'b': keyboardnote = 12 * octave + 7; break; // G case 'h': keyboardnote = 12 * octave + 8; break; // G# case 'n': keyboardnote = 12 * octave + 9; break; // A case 'j': keyboardnote = 12 * octave + 10; break; // A# case 'm': keyboardnote = 12 * octave + 11; break; // B case ',': keyboardnote = 12 * octave + 12; break; // C default: return; } if (keyboardnote < 0) keyboardnote = 0; else if (keyboardnote > 127) keyboardnote = 127; synth.play(0, keyboardnote, 100); storeNote(keyboardnote, t_time); } /////////////////////////////// // // keyboardchar -- this function is called by the improv interface // whenever a key is pressed on the computer keyboard. // Put commands here which will be executed when a key is // pressed on the computer keyboard. // void keyboardchar(int key) { switch (key) { case 'A': // Use Aarden-Essen key profiles majorKey = AardenEssenMajor; minorKey = AardenEssenMinor; cout << "Aarden-Essen weights" << endl; break; case 'B': // Use Bellman-Budge key profiles majorKey = BellmanBudgeMajor; minorKey = BellmanBudgeMinor; cout << "Bellman-Budge weights" << endl; break; case 'K': // Use Krumhansl-Kessler key profiles majorKey = KrumhanslKesslerMajor; minorKey = KrumhanslKesslerMinor; cout << "Krumhansl-Kessler weights" << endl; break; case 'P': // Use simPle key profiles majorKey = SimpleMajor; minorKey = SimpleMinor; cout << "Simple weights" << endl; break; case 'T': // Use Temperley key profiles majorKey = TemperleyMajor; minorKey = TemperleyMinor; cout << "Temperley weights" << endl; break; case '-': // Clear note memory notes.reset(); times.reset(); fadeTime = 0; fadeNote = 0; { for (int i=0; i<12; i++) { occurrences[i] = 0; } } cout << "Note memory cleared" << endl; break; case 'f': // freeze histogram (no deleteing of notes in buffer) freezeQ = !freezeQ; if (freezeQ) { cout << "ANALYSIS HISTOGRAM IS FROZEN" << endl; } else { cout << "ANALYSIS HISTOGRAM IS UNFROZEN" << endl; } break; case '@': // for playing the secondary key possibility displayKey2 = !displayKey2; if (displayKey2) { cout << "Secondary key output turned ON" << endl; } else { cout << "Secondary key output turned OFF" << endl; } break; case '<': // slow down the tempo which will be tempo /= 1.05; // activated at the next note to be played if (tempo < 20) { tempo = 20; } metronome.setTempo(tempo); cout << "Tempo is: " << tempo << endl; break; case '>': // speed up the tempo which will be tempo *= 1.05; // activated at the next note to be played if (tempo < 240) { tempo = 240; } metronome.setTempo(tempo); cout << "Tempo is: " << tempo << endl; break; case '{': case '[': // shorten analysis window by one second analysisDuration -= 1.0; if (analysisDuration < 1.0) { analysisDuration = 1.0; } cout << "ANALYSIS HISTORY: " << analysisDuration << " sec." << endl; break; case '}': case ']': // lengthen analysis window by one second analysisDuration += 1.0; cout << "ANALYSIS HISTORY: " << analysisDuration << " sec." << endl; break; default: keyboard(key); } if (isdigit(key)) { keyoctave = key - '0'; cout << "ANALYSIS OCTAVE: " << keyoctave << endl; } } /*------------------ begining of assistant functions --------------------*/ ////////////////////////////// // // analyzekey -- figure out the key possibilities and then play the // best two choices. // void analyzekey(void) { int i; // first, adjust the number of notes being analyzed if (!freezeQ) { while ((times.getCount() > 0) && (fadeTime < t_time - analysisDuration * 1000)) { if (fadeTime > 0) { occurrences[fadeNote%12]--; } fadeTime = times.extract(); fadeNote = notes.extract(); } if ((fadeTime > 0) && (fadeTime < t_time - analysisDuration * 1000)) { occurrences[fadeNote%12]--; fadeTime = 0; fadeNote = 0; } } // now analyze the keys double total = 0; cout << "Count:"; for (i=0; i<12; i++) { cout.width(3); cout << occurrences[i]; total += occurrences[i]; } cout << flush; if (total <= 0) { cout << endl; return; } double r_major[12]; double r_minor[12]; for (i=0; i<12; i++) { r_major[i] = pearsonCorrelation(12, occurrences, majorKey + 12 - i); r_minor[i] = pearsonCorrelation(12, occurrences, minorKey + 12 - i); } // Now determine which correlation is the greatest. // Start off with the assumption that C major is the best key. double best_key = 0.0; const char* mode = "unknown"; int pitch_class = 0; // tonic note of best key // Compare all the remaining key correlations. for (i=0; i<12; i++) { if (r_major[i] > best_key) { best_key = r_major[i]; mode = "major"; pitch_class = i; } if (r_minor[i] > best_key) { best_key = r_minor[i]; mode = "minor"; pitch_class = i; } } // A confidence measure can be determined by taking the difference // between the correlation for the "best key" and subtracting the // correlation for the "second best key". The maximum confidence // score is 100; the minimum is zero. // First, having found the "best key", find the "second best key." double second_best_key = 0; const char* secondmode = "unknown"; int 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]; secondmode = "major"; sec_pitch_class = i; } if (r_minor[i] != best_key && r_minor[i] > second_best_key) { second_best_key = r_minor[i]; secondmode = "minor"; sec_pitch_class = i; } } // The value 3.0 below is a scaling factor. double confidence = (best_key - second_best_key) * 100 * 3.0; if (confidence > 100.0) confidence = 100.0; // Print the analysis results: cout << " Key: " << name(pitch_class) << " " << mode << " ("; cout.width(3); cout << (int)confidence << "%), or " << name(sec_pitch_class) << " " << secondmode; cout << endl; // finally, play the determined keys according to confidence int velocity = (int)(127.0 * confidence/100.0); firstVoice.play(pitch_class + 12 * keyoctave, velocity); if (displayKey2) { secondVoice.play(sec_pitch_class+12*keyoctave, int(velocity*0.75 + 0.5)); } } ////////////////////////////// // // pearsonCorrelation -- // double pearsonCorrelation(int size, double* x, double* y) { double sumx = 0.0; double sumy = 0.0; double sumco = 0.0; double meanx = x[0]; double meany = y[0]; double sweep; double deltax; double deltay; int i; for (i=2; i<=size; i++) { sweep = (i-1.0) / i; deltax = x[i-1] - meanx; deltay = y[i-1] - meany; sumx += deltax * deltax * sweep; sumy += deltay * deltay * sweep; sumco += deltax * deltay * sweep; meanx += deltax / i; meany += deltay / i; } double popsdx = sqrt(sumx / size); double popsdy = sqrt(sumy / size); double covxy = sumco / size; return covxy / (popsdx * popsdy); } ////////////////////////////// // // name -- return the name of the input pitch class // const char* name(int pitch_class) { pitch_class = pitch_class % 12; switch (pitch_class) { case 0: return "C "; case 1: return "C#"; case 2: return "D "; case 3: return "Eb"; case 4: return "E "; case 5: return "F "; case 6: return "F#"; case 7: return "G "; case 8: return "Ab"; case 9: return "A "; case 10: return "Bb"; case 11: return "B "; } return "unknown"; } ////////////////////////////// // // storeNote -- store the next note // void storeNote(char storeNote, long storeTime) { notes.insert(storeNote); times.insert(storeTime); occurrences[storeNote%12]++; } /*------------------ end improvization algorithms -----------------------*/ // md5sum: 37e8b26f4e2bda279991a5247715ff18 rtkey.cpp [20090626]