y@0: function [nmat,n] = midi2nmat(filename) y@0: % nmat = midi2nmat(filename) y@0: % Read midi file FILENAME into Matlab variable NMAT (Beta) y@0: % Based on Ken Schutte's m-files (readmidi, midiInfo, getTempoChanges) y@0: % This beta might replace the mex-files used in the previous version of the toolbox as y@0: % newer versions of Matlab (7.4+) and various OS's need new compilations y@0: % of the mex files. Using the C sources and the compiled mex files provides y@0: % faster reading of midi files but because the compatibility is limited, this y@0: % simple workaround is offered. This beta version is very primitive, y@0: % though. - Tuomas Eerola y@0: % y@0: % KNOWN PROBLEMS: - Tempo changes are handled in a simple way y@0: % - Extra messages are not retained y@0: % - Channels may not be handled correctly y@0: % y@0: % For more information on Ken Schutte's functions, see y@0: % http://www.kenschutte.com/software y@0: % y@0: % CREATED ON 31.12.2007 BY TE (MATLAB 7.4 MacOSX 10.4) y@0: % y@0: y@0: %% USE KEN SCHUTTE'S FUNCTIONS y@0: m = readmidi(filename); y@0: n = midiInfo(m,0); y@0: [tempos, tempos_time] = getTempoChanges(m); y@0: y@0: %% CONVERT OUTPUT INTO MIDI TOOLBOX NMAT FORMAT y@0: nmat(:,6) = n(:,5); y@0: nmat(:,7) = n(:,6)-n(:,5); % duration y@0: nmat(:,3) = n(:,2); y@0: nmat(:,4) = n(:,3); y@0: nmat(:,5) = n(:,4); y@0: y@0: %% CHECK IF MULTIPLE TEMPI y@0: if isempty(tempos), tempos=500000;end % some files may not contain the tempo, use default in that case y@0: if length(tempos)>1 y@0: % here for diff tempi.... y@0: y@0: disp('WARNING: Multiple tempi detected, simple recoding of timing (seconds, not beats!)'); y@0: disp(num2str(60./(tempos./1000000))) y@0: y@0: y@0: tc_ind=0; y@0: starttime=0; y@0: for i=1:length(tempos_time)-1 y@0: tempo_begin = (tempos_time(i+1)-tempos_time(i))/m.ticks_per_quarter_note*tempos(i)/1000000; y@0: tempo_begin = tempo_begin+starttime; y@0: starttime = tempo_begin; y@0: tc_ind=[tc_ind tempo_begin]; y@0: end y@0: nmat2=nmat; y@0: for i=1:length(tc_ind) y@0: if i==length(tc_ind) y@0: time_index = nmat(:,6)>=tc_ind(i); y@0: else y@0: time_index = nmat(:,6)>=tc_ind(i); %& nmat(:,6)<=tc_ind(i+1); y@0: end y@0: y@0: timeratio = tempos(i)/tempos(1); y@0: y@0: % realign timing after tempo changes y@0: tmp1 = nmat2(time_index,:); y@0: tmp2 = n(time_index,5).*timeratio; tmp2=tmp2(1); y@0: realign = tmp1(1,6)-tmp2; % previous onset time y@0: nmat2(time_index,6) = (n(time_index,5).*timeratio)+realign; y@0: nmat2(time_index,7) = nmat2(time_index,7).*timeratio; y@0: end y@0: nmat(:,1) = n(:,5)/(tempos(1)/1000000); % beats are not modified according to tempi y@0: nmat(:,2) = (n(:,6)-n(:,5))/(tempos(1)/1000000); y@0: y@0: else y@0: nmat(:,1) = n(:,5)/(tempos(1)/1000000); y@0: nmat(:,2) = (n(:,6)-n(:,5))/(tempos(1)/1000000); y@0: end y@0: y@0: % postprocessing... y@0: y@0: % IF CHANNEL IS EMPTY y@0: y@0: if nmat(:,3)==0 y@0: nmat(:,3)=1; y@0: end y@0: y@0: %% FUNCTIONS y@0: y@0: y@0: function [Notes,endtime] = midiInfo(midi,outputFormat,tracklist) y@0: % [Notes,endtime] = midiInfo(midi,outputFormat,tracklist) y@0: % y@0: % Takes a midi structre and generates info on notes and messages y@0: % Can return a matrix of note parameters and/or output/display y@0: % formatted table of messages y@0: % y@0: % Inputs: y@0: % midi - Matlab structure (created by readmidi.m) y@0: % tracklist - which tracks to show ([] for all) y@0: % outputFormat y@0: % - if it's a string write the formated output to the file y@0: % - if 0, don't display or write formatted output y@0: % - if 1, just display (default) y@0: % y@0: % outputs: y@0: % Notes - a matrix containing a list of notes, ordered by start time y@0: % column values are: y@0: % 1 2 3 4 5 6 7 8 y@0: % [track chan nn vel t1 t2 msgNum1 msgNum2] y@0: % endtime - time of end of track message y@0: % y@0: %--------------------------------------------------------------- y@0: % Subversion Revision: 14 (2006-11-28) y@0: % minor alterations by TE 2.1.2008 y@0: % This software can be used freely for non-commerical use. y@0: % Visit http://www.kenschutte.com/software for more y@0: % documentation, copyright info, and updates. y@0: %--------------------------------------------------------------- y@0: y@0: y@0: if nargin<3 y@0: tracklist=[]; y@0: if nargin<2 y@0: outputFormat=1; y@0: end y@0: end y@0: if (isempty(tracklist)) y@0: tracklist = 1:length(midi.track); y@0: end y@0: y@0: [tempos, tempos_time] = getTempoChanges(midi); y@0: y@0: current_tempo = 500000; % default tempo y@0: y@0: fid = -1; y@0: if (ischar(outputFormat)) y@0: fid = fopen(outputFormat,'w'); y@0: end y@0: y@0: endtime = -1; y@0: y@0: % each row: y@0: % 1 2 3 4 5 6 7 8 y@0: % [track chan nn vel t1 t2 msgNum1 msgNum2] y@0: Notes = zeros(0,8); y@0: y@0: for i=1:length(tracklist) y@0: tracknum = tracklist(i); y@0: cumtime=0; y@0: seconds=0; y@0: y@0: Msg = cell(0); y@0: Msg{1,1} = 'chan'; y@0: Msg{1,2} = 'deltatime'; y@0: Msg{1,3} = 'time'; y@0: Msg{1,4} = 'name'; y@0: Msg{1,5} = 'data'; y@0: y@0: runnro=1; y@0: y@0: for msgNum=1:length(midi.track(tracknum).messages) y@0: y@0: currMsg = midi.track(tracknum).messages(msgNum); y@0: y@0: midimeta = currMsg.midimeta; y@0: deltatime = currMsg.deltatime; y@0: data = currMsg.data; y@0: type = currMsg.type; y@0: chan = currMsg.chan; y@0: y@0: cumtime = cumtime + deltatime; y@0: %current_tempo/midi.ticks_per_quarter_note; y@0: %debug = deltatime*1e-6*current_tempo/midi.ticks_per_quarter_note; y@0: y@0: seconds = seconds + deltatime*1e-6*current_tempo/midi.ticks_per_quarter_note; y@0: [mx ind] = max(find(cumtime >= tempos_time)); y@0: y@0: %% % ADDED BY TE 1.1.2008 y@0: if isempty(ind) y@0: % do nothing y@0: else y@0: current_tempo = tempos(ind); y@0: end y@0: %% end y@0: y@0: % find start/stop of notes: y@0: % if (strcmp(name,'Note on') && (data(2)>0)) y@0: % note on with vel>0: y@0: if (midimeta==1 && type==144 && data(2)>0) y@0: % note on: y@0: y@0: if isempty(Notes) y@0: Notes(1,:) = [tracknum chan data(1) data(2) seconds 0 runnro -1]; y@0: runnro=runnro+1; y@0: else y@0: % Notes(end+1,:) = [tracknum chan data(1) data(2) seconds 0 msgNum -1] y@0: Notes(end+1,:) = [tracknum chan data(1) data(2) seconds 0 runnro -1]; y@0: runnro=runnro+1; y@0: end y@0: % elseif ((strcmp(name,'Note on') && (data(2)==0)) || strcmp(name,'Note off')) y@0: % note on with vel==0 or note off: y@0: y@0: elseif (midimeta==1 && ( (type==144 && data(2)==0) || type==128 )) y@0: y@0: % note off: y@0: % % find index, wther tr,chan,and nn match, and not complete y@0: y@0: ind = find((... y@0: (Notes(:,1)==tracknum) + ... y@0: (Notes(:,2)==chan) + ... y@0: (Notes(:,3)==data(1)) + ... y@0: (Notes(:,8)==-1)... y@0: )==4); y@0: y@0: if (length(ind)==0) y@0: error('ending non-open note?'); y@0: elseif (length(ind)>1) y@0: disp('warning: found multiple matches in endNote, taking first...'); y@0: ind = ind(1); y@0: end y@0: y@0: % set info on ending: y@0: Notes(ind,6) = seconds; y@0: Notes(ind,8) = msgNum; y@0: y@0: % end of track: y@0: elseif (midimeta==0 && type==47) y@0: if (endtime == -1) y@0: endtime = seconds; y@0: else y@0: disp('two "end of track" messages?'); y@0: endtime(end+1) = seconds; y@0: end y@0: y@0: y@0: end y@0: y@0: % we could check to make sure it ends with y@0: % 'end of track' y@0: y@0: y@0: if (outputFormat ~= 0) y@0: % get some specific descriptions: y@0: name = num2str(type); y@0: dataStr = num2str(data); y@0: y@0: if (isempty(chan)) y@0: Msg{msgNum,1} = '-'; y@0: else y@0: Msg{msgNum,1} = num2str(chan); y@0: end y@0: y@0: Msg{msgNum,2} = num2str(deltatime); y@0: Msg{msgNum,3} = formatTime(seconds); y@0: y@0: if (midimeta==0) y@0: Msg{msgNum,4} = 'meta'; y@0: else y@0: Msg{msgNum,4} = ''; y@0: end y@0: y@0: [name,dataStr] = getMsgInfo(midimeta, type, data); y@0: Msg{msgNum,5} = name; y@0: Msg{msgNum,6} = dataStr; y@0: end y@0: y@0: y@0: end y@0: y@0: if (outputFormat ~= 0) y@0: printTrackInfo(Msg,tracknum,fid); y@0: end y@0: y@0: end y@0: y@0: % make this an option!!! y@0: % - I'm not sure why it's needed... y@0: % remove start silence: y@0: y@0: %_ removed by TE y@0: %_first_t = min(Notes(:,5)); y@0: %_Notes(:,5) = Notes(:,5) - first_t; y@0: %_Notes(:,6) = Notes(:,6) - first_t; y@0: y@0: % sort Notes by start time: y@0: [junk,ord] = sort(Notes(:,5)); y@0: Notes = Notes(ord,:); y@0: y@0: y@0: if (fid ~= -1) y@0: fclose(fid); y@0: end y@0: y@0: y@0: y@0: y@0: y@0: y@0: y@0: y@0: y@0: y@0: y@0: function printTrackInfo(Msg,tracknum,fid) y@0: y@0: y@0: % make cols same length instead of just using \t y@0: for i=1:size(Msg,2) y@0: maxLen(i)=0; y@0: for j=1:size(Msg,1) y@0: if (length(Msg{j,i})>maxLen(i)) y@0: maxLen(i) = length(Msg{j,i}); y@0: end y@0: end y@0: end y@0: y@0: y@0: s=''; y@0: s=[s sprintf('--------------------------------------------------\n')]; y@0: s=[s sprintf('Track %d\n',tracknum)]; y@0: s=[s sprintf('--------------------------------------------------\n')]; y@0: y@0: if (fid == -1) y@0: disp(s) y@0: else y@0: fprintf(fid,'%s',s); y@0: end y@0: y@0: y@0: for i=1:size(Msg,1) y@0: line=''; y@0: for j=1:size(Msg,2) y@0: sp = repmat(' ',1,5+maxLen(j)-length(Msg{i,j})); y@0: m = Msg{i,j}; y@0: m = m(:)'; % ensure column vector y@0: % line = [line Msg{i,j} sp]; y@0: line = [line m sp]; y@0: end y@0: y@0: if (fid == -1) y@0: disp(line) y@0: else y@0: fprintf(fid,'%s\n',line); y@0: end y@0: y@0: end y@0: y@0: y@0: y@0: function s=formatTime(seconds) y@0: y@0: minutes = floor(seconds/60); y@0: secs = seconds - 60*minutes; y@0: y@0: s = sprintf('%d:%2.3f',minutes,secs); y@0: y@0: y@0: y@0: function [name,dataStr]=getMsgInfo(midimeta, type, data) y@0: y@0: % meta events: y@0: if (midimeta==0) y@0: if (type==0); name = 'Sequence Number'; len=2; dataStr = num2str(data); y@0: elseif (type==1); name = 'Text Events'; len=-1; dataStr = char(data); y@0: elseif (type==2); name = 'Copyright Notice'; len=-1; dataStr = char(data); y@0: elseif (type==3); name = 'Sequence/Track Name'; len=-1; dataStr = char(data); y@0: elseif (type==4); name = 'Instrument Name'; len=-1; dataStr = char(data); y@0: elseif (type==5); name = 'Lyric'; len=-1; dataStr = char(data); y@0: elseif (type==6); name = 'Marker'; len=-1; dataStr = char(data); y@0: elseif (type==7); name = 'Cue Point'; len=-1; dataStr = char(data); y@0: elseif (type==32); name = 'MIDI Channel Prefix'; len=1; dataStr = num2str(data); y@0: elseif (type==47); name = 'End of Track'; len=0; dataStr = ''; y@0: elseif (type==81); name = 'Set Tempo'; len=3; y@0: val = data(1)*16^4+data(2)*16^2+data(3); dataStr = ['microsec per quarter note: ' num2str(val)]; y@0: elseif (type==84); name = 'SMPTE Offset'; len=5; y@0: if size(data)==5 y@0: dataStr = ['[hh mm ss fr ff]=' num2str(data)]; y@0: else y@0: dataStr = ['[hh mm ss fr ff]=' num2str(data')]; y@0: end y@0: elseif (type==88); name = 'Time Signature'; len=4; y@0: dataStr = [num2str(data(1)) '/' num2str(data(2)) ', clock ticks and notated 32nd notes=' num2str(data(3)) '/' num2str(data(4))]; y@0: elseif (type==89); name = 'Key Signature'; len=2; y@0: % num sharps/flats (flats negative) y@0: if (data(1)>=0) y@0: % 1 2 3 4 5 6 7 y@0: ss={'C','G','D', 'A', 'E','B', 'F#', 'C#'}; y@0: if data(1)>7 y@0: dataStr='C'; % ADDED BY TE 1.1.2008 y@0: else y@0: dataStr = ss{data(1)+1}; y@0: end y@0: y@0: else y@0: % 1 2 3 4 5 6 7 y@0: ss={'F','Bb','Eb','Ab','Db','Gb','Cb'}; y@0: dataStr = ss{abs(data(1))}; y@0: end y@0: if (data(2)==0) y@0: dataStr = [dataStr ' Major']; y@0: else y@0: dataStr = [dataStr ' Minor']; y@0: end y@0: y@0: elseif (type==89); name = 'Sequencer-Specific Meta-event'; len=-1; y@0: dataStr = char(data); y@0: % !! last two conflict... y@0: y@0: else y@0: name = ['UNKNOWN META EVENT: ' num2str(type)]; dataStr = num2str(data); y@0: end y@0: y@0: % meta 0x21 = MIDI port number, length 1 (? perhaps) y@0: else y@0: y@0: % channel voice messages: y@0: % (from event byte with chan removed, eg 0x8n -> 0x80 = 128 for y@0: % note off) y@0: if (type==128); name = 'Note off'; len=2; dataStr = ['nn=' num2str(data(1)) ' vel=' num2str(data(2))]; y@0: elseif (type==144); name = 'Note on'; len=2; dataStr = ['nn=' num2str(data(1)) ' vel=' num2str(data(2))]; y@0: elseif (type==160); name = 'Polyphonic Key Pressure'; len=2; dataStr = ['nn=' num2str(data(1)) ' vel=' num2str(data(2))]; y@0: elseif (type==176); name = 'Controller Change'; len=2; dataStr = ['ctrl=' controllers(data(1)) ' value=' num2str(data(2))]; y@0: elseif (type==192); name = 'Program Change'; len=1; dataStr = ['instr=' num2str(data)]; y@0: elseif (type==208); name = 'Channel Key Pressure'; len=1; dataStr = ['vel=' num2str(data)]; y@0: elseif (type==224); name = 'Pitch Bend'; len=2; y@0: val = data(1)+data(2)*256; y@0: val = base2dec('2000',16) - val; y@0: dataStr = ['change=' num2str(val) '?']; y@0: y@0: % channel mode messages: y@0: % ... unsure about data for these... (do some have a data byte and y@0: % others not?) y@0: % y@0: % 0xC1 .. 0xC8 y@0: elseif (type==193); name = 'All Sounds Off'; dataStr = num2str(data); y@0: elseif (type==194); name = 'Reset All Controllers'; dataStr = num2str(data); y@0: elseif (type==195); name = 'Local Control'; dataStr = num2str(data); y@0: elseif (type==196); name = 'All Notes Off'; dataStr = num2str(data); y@0: elseif (type==197); name = 'Omni Mode Off'; dataStr = num2str(data); y@0: elseif (type==198); name = 'Omni Mode On'; dataStr = num2str(data); y@0: elseif (type==199); name = 'Mono Mode On'; dataStr = num2str(data); y@0: elseif (type==200); name = 'Poly Mode On'; dataStr = num2str(data); y@0: y@0: % sysex, F0->F7 y@0: elseif (type==240); name = 'Sysex 0xF0'; dataStr = num2str(data); y@0: elseif (type==241); name = 'Sysex 0xF1'; dataStr = num2str(data); y@0: elseif (type==242); name = 'Sysex 0xF2'; dataStr = num2str(data); y@0: elseif (type==243); name = 'Sysex 0xF3'; dataStr = num2str(data); y@0: elseif (type==244); name = 'Sysex 0xF4'; dataStr = num2str(data); y@0: elseif (type==245); name = 'Sysex 0xF5'; dataStr = num2str(data); y@0: elseif (type==246); name = 'Sysex 0xF6'; dataStr = num2str(data); y@0: elseif (type==247); name = 'Sysex 0xF7'; dataStr = num2str(data); y@0: y@0: % realtime y@0: % (i think have no data..?) y@0: elseif (type==248); name = 'Real-time 0xF8 - Timing clock'; dataStr = num2str(data); y@0: elseif (type==249); name = 'Real-time 0xF9'; dataStr = num2str(data); y@0: elseif (type==250); name = 'Real-time 0xFA - Start a sequence'; dataStr = num2str(data); y@0: elseif (type==251); name = 'Real-time 0xFB - Continue a sequence'; dataStr = num2str(data); y@0: elseif (type==252); name = 'Real-time 0xFC - Stop a sequence'; dataStr = num2str(data); y@0: elseif (type==253); name = 'Real-time 0xFD'; dataStr = num2str(data); y@0: elseif (type==254); name = 'Real-time 0xFE'; dataStr = num2str(data); y@0: elseif (type==255); name = 'Real-time 0xFF'; dataStr = num2str(data); y@0: y@0: y@0: else y@0: name = ['UNKNOWN MIDI EVENT: ' num2str(type)]; dataStr = num2str(data); y@0: end y@0: y@0: y@0: end y@0: y@0: function s=controllers(n) y@0: if (n==1); s='Mod Wheel'; y@0: elseif (n==2); s='Breath Controllery'; y@0: elseif (n==4); s='Foot Controller'; y@0: elseif (n==5); s='Portamento Time'; y@0: elseif (n==6); s='Data Entry MSB'; y@0: elseif (n==7); s='Volume'; y@0: elseif (n==8); s='Balance'; y@0: elseif (n==10); s='Pan'; y@0: elseif (n==11); s='Expression Controller'; y@0: elseif (n==16); s='General Purpose 1'; y@0: elseif (n==17); s='General Purpose 2'; y@0: elseif (n==18); s='General Purpose 3'; y@0: elseif (n==19); s='General Purpose 4'; y@0: elseif (n==64); s='Sustain'; y@0: elseif (n==65); s='Portamento'; y@0: elseif (n==66); s='Sustenuto'; y@0: elseif (n==67); s='Soft Pedal'; y@0: elseif (n==69); s='Hold 2'; y@0: elseif (n==80); s='General Purpose 5'; y@0: elseif (n==81); s='Temp Change (General Purpose 6)'; y@0: elseif (n==82); s='General Purpose 7'; y@0: elseif (n==83); s='General Purpose 8'; y@0: elseif (n==91); s='Ext Effects Depth'; y@0: elseif (n==92); s='Tremelo Depthy'; y@0: elseif (n==93); s='Chorus Depth'; y@0: elseif (n==94); s='Detune Depth (Celeste Depth)'; y@0: elseif (n==95); s='Phaser Depth'; y@0: elseif (n==96); s='Data Increment (Data Entry +1)'; y@0: elseif (n==97); s='Data Decrement (Data Entry -1)'; y@0: elseif (n==98); s='Non-Registered Param LSB'; y@0: elseif (n==99); s='Non-Registered Param MSB'; y@0: elseif (n==100); s='Registered Param LSB'; y@0: elseif (n==101); s='Registered Param MSB'; y@0: else y@0: s='UNKNOWN CONTROLLER'; y@0: end y@0: y@0: %Channel mode message values y@0: %Reset All Controllers 79 121 Val ?? y@0: %Local Control 7A 122 Val 0 = off, 7F (127) = on y@0: %All Notes Off 7B 123 Val must be 0 y@0: %Omni Mode Off 7C 124 Val must be 0 y@0: %Omni Mode On 7D 125 Val must be 0 y@0: %Mono Mode On 7E 126 Val = # of channels, or 0 if # channels equals # voices in receiver y@0: %Poly Mode On 7F 127 Val must be 0 y@0: y@0: y@0: function [tempos,tempos_time]=getTempoChanges(midi) y@0: % [tempos,tempos_time]=getTempoChanges(midi) y@0: % y@0: % input: a midi struct from readmidi.m y@0: % output: y@0: % tempos = tempo values indexed by tempos_time y@0: % tempos_time is in units of ticks y@0: % y@0: % should tempo changes effect across tracks? across channels? y@0: % y@0: %--------------------------------------------------------------- y@0: % Subversion Revision: 14 (2006-01-24) y@0: % y@0: % This software can be used freely for non-commerical use. y@0: % Visit http://www.kenschutte.com/software for more y@0: % documentation, copyright info, and updates. y@0: %--------------------------------------------------------------- y@0: y@0: y@0: tempos = []; y@0: tempos_time = []; y@0: %tempos_index = []; y@0: for i=1:length(midi.track) y@0: cumtime=0; y@0: for j=1:length(midi.track(i).messages) y@0: cumtime = cumtime+midi.track(i).messages(j).deltatime; y@0: % if (strcmp(midi.track(i).messages(j).name,'Set Tempo')) y@0: if (midi.track(i).messages(j).midimeta==0 && midi.track(i).messages(j).type==81) y@0: y@0: tempos_time(end+1) = cumtime; y@0: d = midi.track(i).messages(j).data; y@0: tempos(end+1) = d(1)*16^4 + d(2)*16^2 + d(3); y@0: end y@0: end y@0: end y@0: y@0: y@0: function midi = readmidi(filename, rawbytes) y@0: % midi = readmidi(filename, rawbytes) y@0: % midi = readmidi(filename) y@0: % y@0: % Read MIDI file and store in a Matlab structure y@0: % (use midiInfo.m to see structure detail) y@0: % y@0: % Inputs: y@0: % filename - input MIDI file y@0: % rawbytes - 0 or 1: Include raw bytes in structure y@0: % This info is redundant, but can be y@0: % useful for debugging. default=0 y@0: % y@0: %--------------------------------------------------------------- y@0: % Subversion Revision: 14 (2006-12-03) y@0: % y@0: % This software can be used freely for non-commerical use. y@0: % Visit http://www.kenschutte.com/software for more y@0: % documentation, copyright info, and updates. y@0: %--------------------------------------------------------------- y@0: y@0: y@0: y@0: if (nargin<2) y@0: rawbytes=0; y@0: end y@0: y@0: fid = fopen(filename); y@0: %[A count] = fread(fid,'char'); y@0: [A count] = fread(fid,'uint8'); y@0: fclose(fid); y@0: y@0: midi.filename = filename; y@0: if (rawbytes) midi.rawbytes_all = A; end y@0: y@0: y@0: % realtime events: status: [F8, FF]. no data bytes y@0: %clock, undefined, start, continue, stop, undefined, active y@0: %sensing, systerm reset y@0: y@0: % file consists of "header chunk" and "track chunks" y@0: % 4B 'MThd' (header) or 'MTrk' (track) y@0: % 4B 32-bit unsigned int = number of bytes in chunk, not y@0: % counting these first 8 y@0: y@0: y@0: % HEADER CHUNK -------------------------------------------------------- y@0: % 4B 'Mthd' y@0: % 4B length y@0: % 2B file format y@0: % 0=single track, 1=multitrack synchronous, 2=multitrack asynchronous y@0: % Synchronous formats start all tracks at the same time, while asynchronous formats can start and end any track at any time during the score. y@0: % 2B track cout (must be 1 for format 0) y@0: % 2B num delta-time ticks per quarter note y@0: % y@0: y@0: if ~isequal(A(1:4)',[77 84 104 100]) % double('MThd') y@0: error('File does not begin with header ID (MThd)'); y@0: end y@0: y@0: header_len = decode_int(A(5:8)); y@0: if (header_len == 6) y@0: else y@0: error('Header length != 6 bytes.'); y@0: end y@0: y@0: format = decode_int(A(9:10)); y@0: if (format==0 || format==1 || format==2) y@0: midi.format = format; y@0: else y@0: error('Format does not equal 0,1,or 2'); y@0: end y@0: y@0: num_tracks = decode_int(A(11:12)); y@0: if (format==0 && num_tracks~=1) y@0: error('File is format 0, but num_tracks != 1'); y@0: end y@0: y@0: time_unit = decode_int(A(13:14)); y@0: if (bitand(time_unit,2^15)==0) y@0: midi.ticks_per_quarter_note = time_unit; y@0: else y@0: error('Header: SMPTE time format found - not currently supported'); y@0: end y@0: y@0: if (rawbytes), midi.rawbytes_header = A(1:14); end y@0: y@0: % end header parse ---------------------------------------------------- y@0: y@0: y@0: y@0: y@0: y@0: y@0: % BREAK INTO SEPARATE TRACKS ------------------------------------------ y@0: % midi.track(1).data = [byte byte byte ...]; y@0: % midi.track(2).date = ... y@0: % ... y@0: % y@0: % Track Chunks--------- y@0: % 4B 'MTrk' y@0: % 4B length (after first 8B) y@0: % y@0: ctr = 15; y@0: for i=1:num_tracks y@0: y@0: if ~isequal(A(ctr:ctr+3)',[77 84 114 107]) % double('MTrk') y@0: error(['Track ' num2str(i) ' does not begin with track ID=MTrk']); y@0: end y@0: ctr = ctr+4; y@0: y@0: track_len = decode_int(A(ctr:ctr+3)); y@0: ctr = ctr+4; y@0: y@0: % have track.rawbytes hold initial 8B also... y@0: track_rawbytes{i} = A((ctr-8):(ctr+track_len-1)); y@0: y@0: if (rawbytes) y@0: midi.track(i).rawbytes_header = A(ctr-8:ctr-1); y@0: end y@0: y@0: ctr = ctr+track_len; y@0: end y@0: % ---------------------------------------------------------------------- y@0: y@0: y@0: y@0: y@0: y@0: y@0: % Events: y@0: % - meta events: start with 'FF' y@0: % - MIDI events: all others y@0: y@0: % MIDI events: y@0: % optional command byte + 0,1,or 2 bytes of parameters y@0: % "running mode": command byte omitted. y@0: % y@0: % all midi command bytes have MSB=1 y@0: % all data for inside midi command have value <= 127 (ie MSB=0) y@0: % -> so can determine running mode y@0: % y@0: % meta events' data may have any values (meta events have to set y@0: % len) y@0: % y@0: y@0: y@0: y@0: % 'Fn' MIDI commands: y@0: % no chan. control the entire system y@0: %F8 Timing Clock y@0: %FA start a sequence y@0: %FB continue a sequence y@0: %FC stop a sequence y@0: y@0: % Meta events: y@0: % 1B 0xFF y@0: % 1B event type y@0: % 1B length of additional data y@0: % ?? additional data y@0: % y@0: y@0: y@0: % "channel mode messages" y@0: % have same code as "control change": 0xBn y@0: % but uses reserved controller numbers 120-127 y@0: % y@0: y@0: y@0: %Midi events consist of an optional command byte y@0: % followed by zero, one or two bytes of parameters. y@0: % In running mode, the command can be omitted, in y@0: % which case the last MIDI command specified is y@0: % assumed. The first bit of a command byte is 1, y@0: % while the first bit of a parameter is always 0. y@0: % In addition, the last 4 bits of a command y@0: % indicate the channel to which the event should y@0: % be sent; therefore, there are 6 possible y@0: % commands (really 7, but we will discuss the x'Fn' y@0: % commands later) that can be specified. They are: y@0: y@0: y@0: % parse tracks ----------------------------------------- y@0: for i=1:num_tracks y@0: y@0: track = track_rawbytes{i}; y@0: y@0: if (rawbytes); midi.track(i).rawbytes = track; end y@0: y@0: msgCtr = 1; y@0: ctr=9; % first 8B were MTrk and length y@0: while (ctr < length(track_rawbytes{i})) y@0: y@0: clear currMsg; y@0: currMsg.used_running_mode = 0; y@0: % note: y@0: % .used_running_mode is necessary only to y@0: % be able to reconstruct a file _exactly_ from y@0: % the 'midi' structure. this is helpful for y@0: % debugging since write(read(filename)) can be y@0: % tested for exact replication... y@0: % y@0: y@0: ctr_start_msg = ctr; y@0: y@0: [deltatime,ctr] = decode_var_length(track, ctr); y@0: y@0: % ? y@0: %if (rawbytes) y@0: % currMsg.rawbytes_deltatime = track(ctr_start_msg:ctr-1); y@0: %end y@0: y@0: % deltaime must be 1-4 bytes long. y@0: % could check here... y@0: y@0: y@0: % CHECK FOR META EVENTS ------------------------ y@0: % 'FF' y@0: if track(ctr)==255 y@0: y@0: type = track(ctr+1); y@0: y@0: ctr = ctr+2; y@0: y@0: % get variable length 'length' field y@0: [len,ctr] = decode_var_length(track, ctr); y@0: y@0: % note: some meta events have pre-determined lengths... y@0: % we could try verifiying they are correct here. y@0: y@0: thedata = track(ctr:ctr+len-1); y@0: chan = []; y@0: y@0: ctr = ctr + len; y@0: y@0: midimeta = 0; y@0: y@0: else y@0: midimeta = 1; y@0: % MIDI EVENT --------------------------- y@0: y@0: y@0: y@0: y@0: % check for running mode: y@0: if (track(ctr)<128) y@0: y@0: % make it re-do last command: y@0: %ctr = ctr - 1; y@0: %track(ctr) = last_byte; y@0: currMsg.used_running_mode = 1; y@0: y@0: B = last_byte; y@0: nB = track(ctr); % ? y@0: y@0: else y@0: y@0: B = track(ctr); y@0: nB = track(ctr+1); y@0: y@0: ctr = ctr + 1; y@0: y@0: end y@0: y@0: % nibbles: y@0: %B = track(ctr); y@0: %nB = track(ctr+1); y@0: y@0: y@0: Hn = bitshift(B,-4); y@0: Ln = bitand(B,15); y@0: y@0: chan = []; y@0: y@0: msg_type = midi_msg_type(B,nB); y@0: y@0: % DEBUG: y@0: if (i==2) y@0: if (msgCtr==1) y@0: disp(msg_type); y@0: end y@0: end y@0: y@0: y@0: switch msg_type y@0: y@0: case 'channel_mode' y@0: y@0: % UNSURE: if all channel mode messages have 2 data byes (?) y@0: type = bitshift(Hn,4) + (nB-120+1); y@0: thedata = track(ctr:ctr+1); y@0: chan = Ln; y@0: y@0: ctr = ctr + 2; y@0: y@0: % ---- channel voice messages: y@0: case 'channel_voice' y@0: y@0: type = bitshift(Hn,4); y@0: len = channel_voice_msg_len(type); % var length data: y@0: thedata = track(ctr:ctr+len-1); y@0: chan = Ln; y@0: y@0: % DEBUG: y@0: if (i==2) y@0: if (msgCtr==1) y@0: disp([999 Hn type]) y@0: end y@0: end y@0: y@0: ctr = ctr + len; y@0: y@0: case 'sysex' y@0: y@0: % UNSURE: do sysex events (F0-F7) have y@0: % variable length 'length' field? y@0: y@0: [len,ctr] = decode_var_length(track, ctr); y@0: y@0: type = B; y@0: thedata = track(ctr:ctr+len-1); y@0: chan = []; y@0: y@0: ctr = ctr + len; y@0: y@0: case 'sys_realtime' y@0: y@0: % UNSURE: I think these are all just one byte y@0: type = B; y@0: thedata = []; y@0: chan = []; y@0: y@0: end y@0: y@0: last_byte = Ln + bitshift(Hn,4); y@0: y@0: end % end midi event 'if' y@0: y@0: y@0: currMsg.deltatime = deltatime; y@0: currMsg.midimeta = midimeta; y@0: currMsg.type = type; y@0: currMsg.data = thedata; y@0: currMsg.chan = chan; y@0: y@0: if (rawbytes) y@0: currMsg.rawbytes = track(ctr_start_msg:ctr-1); y@0: end y@0: y@0: midi.track(i).messages(msgCtr) = currMsg; y@0: msgCtr = msgCtr + 1; y@0: y@0: y@0: end % end loop over rawbytes y@0: end % end loop over tracks y@0: y@0: y@0: function val=decode_int(A) y@0: y@0: val = 0; y@0: for i=1:length(A) y@0: val = val + bitshift(A(length(A)-i+1), 8*(i-1)); y@0: end y@0: y@0: y@0: function len=channel_voice_msg_len(type) y@0: y@0: if (type==128); len=2; y@0: elseif (type==144); len=2; y@0: elseif (type==160); len=2; y@0: elseif (type==176); len=2; y@0: elseif (type==192); len=1; y@0: elseif (type==208); len=1; y@0: elseif (type==224); len=2; y@0: else y@0: disp(type); error('bad channel voice message type'); y@0: end y@0: y@0: y@0: % y@0: % decode variable length field (often deltatime) y@0: % y@0: % return value and new position of pointer into 'bytes' y@0: % y@0: function [val,ptr] = decode_var_length(bytes, ptr) y@0: y@0: keepgoing=1; y@0: binarystring = ''; y@0: while (keepgoing) y@0: % check MSB: y@0: % if MSB=1, then delta-time continues into next byte... y@0: if(~bitand(bytes(ptr),128)) y@0: keepgoing=0; y@0: end y@0: % keep appending last 7 bits from each byte in the deltatime: y@0: binbyte = ['00000000' dec2base(bytes(ptr),2)]; y@0: binarystring = [binarystring binbyte(end-6:end)]; y@0: ptr=ptr+1; y@0: end y@0: val = base2dec(binarystring,2); y@0: y@0: y@0: y@0: y@0: % y@0: % Read first 2 bytes of msg and y@0: % determine the type y@0: % (most require only 1st byte) y@0: % y@0: % str is one of: y@0: % 'channel_mode' y@0: % 'channel_voice' y@0: % 'sysex' y@0: % 'sys_realtime' y@0: % y@0: function str=midi_msg_type(B,nB) y@0: y@0: Hn = bitshift(B,-4); y@0: Ln = bitand(B,7); y@0: y@0: % ---- channel mode messages: y@0: %if (Hn==11 && nB>=120 && nB<=127) y@0: if (Hn==11 && nB>=122 && nB<=127) y@0: str = 'channel_mode'; y@0: y@0: % ---- channel voice messages: y@0: elseif (Hn>=8 && Hn<=14) y@0: str = 'channel_voice'; y@0: y@0: % ---- sysex events: y@0: elseif (Hn==15 && Ln>=0 && Ln<=7) y@0: str = 'sysex'; y@0: y@0: % system real-time messages y@0: elseif (Hn==15 && Ln>=8 && Ln<=15) y@0: % UNSURE: how can you tell between 0xFF system real-time y@0: % message and 0xFF meta event? y@0: % (now, it will always be processed by meta) y@0: str = 'sys_realtime'; y@0: y@0: else y@0: % don't think it can get here... y@0: error('bad midi message'); y@0: end y@0: