MIDIFileAnalyse { classvar major, minor; var <>midifile, <>normalform; *initClass { major= [0,2,4,5,7,9,11]; minor=[0,2,3,5,7,8,11]; //harmonic minor, more complicated logic for melodic (based on not just note now but direction to next note), and Aoelian mode like major scale! } *new {arg midifile, bps=1; ^super.new.midifile_(midifile).normalForm(bps) } //merge all tracks (assumes MIDI file type 1 for now), calculate absolute time positions, //get durations from matched on and off events //MIDI note data form starts as (times in ticks= this.divisions) [ioi,channel,pitch, velocity,starttime] //will make it into a single track with [ioi, channel, pitch, vel, time, dur] normalForm {arg bps=1; var onoffarray, outputarray, inputarray, tmp; onoffarray=Array.fill(128,{nil}); //when a note is on, store in this array- when off occurs or new note of same pitch, remove old but add duration outputarray= List.new; //assumes Type 1 MIDIFile ((midifile.ntrks)-1).do {arg i; inputarray= midifile.scores[i+1]; inputarray.do {arg val,j; //no need to check if velocity 0 because will also require previous note //and sometimes note off might be missing if (onoffarray[val[2]].notNil,{ //add previous to output, also adding on a duration value outputarray.add((onoffarray[val[2]])++([val[4]-(onoffarray[val[2]][4])])); onoffarray[val[2]]=nil; }); if(val[3]!=0, { //add to onoffarray onoffarray[val[2]]= val; }); }; }; "hello".postln; outputarray.postln; outputarray=outputarray.asArray; //now sort outputarray by absolutestarttimes outputarray= outputarray.sort({arg v1,v2; v1[4]then,{continue=false; firstindex= max(0,count-1)}); if(count>=normalform.size,{continue=false; ^[]}); count=count+1; }); continue= true; if (count==(normalform.size-1),{^[normalform[firstindex]]}); count= firstindex+1; while ({continue},{ tmp=normalform[count]; if(tmp[4]>now,{continue=false; lastindex= max(0,count-1)}); if(count>=normalform.size,{continue=false; lastindex=normalform.size-1;}); count=count+1; }); ^normalform.copyRange(firstindex,lastindex); } //Analyse MIDI file in normal form [ioi, channel, pitch, velocity, starttime, dur] //default analysis over last 2 seconds analyse {arg timenow, window=2; var involved; if (normalform.isNil,{^nil}); involved= this.getNormalNow(timenow, timenow-window); if(involved.isEmpty,{^nil}); //now can do analysis using all the events in the window ^((this.findDensity(involved,window,timenow))++(this.findKey(involved,window))++(this.findMelodicParams(involved,window))) } //density and energy params (potentially on a number of timescales), average velocity, can weight (to most recent) findDensity {arg array, window, timenow; var notespersec, chordspersec, averageioi, averagevelocity, weightedenergy; var tmp, tmp2, count; notespersec= array.size/window; //chords are single struck objects where ioi from last note under 30 msec- this is a hard resolution, //fine for metronomic midifiles, but things become more interesting/difficult with expressive playing //and this also doesn't look at the whole spread but just successive iois...a gliss would confuse it tmp= 0.0; count=0; (array.size-1).do {arg i; var val; val= array[i+1]; if(val[0]>0.03,{count=count+1;});}; chordspersec= count/window; //sum up all nonzero iois, excluding first ioi tmp= 0.0; count=0; (array.size-1).do {arg i; var val; val= array[i+1]; if(val[0]>0.001,{count=count+1; tmp=tmp+val[0];});}; averageioi= if(count>0,{tmp/count},0.0); tmp=0.0; array.do {arg val; tmp= tmp+ val[3]}; averagevelocity= tmp/array.size; tmp= 0.0; tmp2=0.0; //weighting most recent more array.do {arg val; var calc; calc= timenow-val[4]; calc= (1.0-(calc/window)); tmp=tmp+(calc*val[3]); tmp2=tmp2+calc; }; weightedenergy= if(tmp2>0.001,{tmp/tmp2},0.0); ^[\notespersec, notespersec,\chordspersec, chordspersec, \averageioi, averageioi, \averagevelocity, averagevelocity, \weightedenergy, weightedenergy]; } //various methods possible, histograms, statistics, music theoretic constructions (proximity on cycle of fifths etc) //I want to find best fitting major or minor key to current pitch content //P(key|observed pitches) is proportional to P(observed pitches| key)P(key) //could train a neural net or similar to have these probabilities from a large corpus //Could use the Krumhansl Kessler profile to weight situations from experimental data //but let's just do a quick hack for now //given a key array and pitches array. Judge match by making a score 1 for every pitch in that key, -1 for every pitch out of key. Creates a profile over key. No attempt to weight different pitches by importance in a key (ie tonic over leading note etc) findKey {arg array; var keyscores, tmp, best; keyscores=Array.fill(24,{0}); 24.do{arg i; var testkey, score, results; testkey= (i.div(2)+(if(i.odd,{major},{minor})))%12; results=Array.fill(12,{-1}); results.put(testkey,1); score=0; array.do({arg val; var pitch; //modulo 12 pitch= (val[2])%12; score=score+results[pitch]; }); keyscores[i]=score; }; //highest score wins! tmp=0; best=(1000.neg); keyscores.do{arg val,j; if(val>best,{best=val; tmp=j});}; ^[\key, (["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"].at(tmp.div(2)))+(if(tmp.odd,"major","minor"))]; } //low note, high note, tessitura, mean pitch, pitch variance findMelodicParams {arg array; var lownote, highnote, meanpitch, variance; var tmp, tmp2, count; highnote=0; lownote=127; tmp=0; array.do{arg val; var pitch= val[2]; if(pitchhighnote,{highnote=pitch}); tmp=tmp+pitch; }; meanpitch=tmp/(array.size); variance=0.0; array.do{arg val; variance= variance+((val[2]-meanpitch)**2);}; ^[\lownote, lownote, \highnote, highnote, \tessitura, highnote-lownote+1,\meanpitch, meanpitch, \stddev, (variance/(array.size)).sqrt]; } }