3 /////////////////////////////////////////////////////////////////
4 /// getID3() by James Heinrich <info@getid3.org> //
5 // available at https://github.com/JamesHeinrich/getID3 //
6 // or https://www.getid3.org //
7 // or http://getid3.sourceforge.net //
8 // see readme.txt for more details //
9 /////////////////////////////////////////////////////////////////
11 // module.audio.midi.php //
12 // module for Midi Audio files //
13 // dependencies: NONE //
15 /////////////////////////////////////////////////////////////////
17 define('GETID3_MIDI_MAGIC_MTHD', 'MThd'); // MIDI file header magic
18 define('GETID3_MIDI_MAGIC_MTRK', 'MTrk'); // MIDI track header magic
20 class getid3_midi
extends getid3_handler
25 public $scanwholefile = true;
30 public function Analyze() {
31 $info = &$this->getid3
->info
;
34 $info['midi']['raw'] = array();
35 $thisfile_midi = &$info['midi'];
36 $thisfile_midi_raw = &$thisfile_midi['raw'];
38 $info['fileformat'] = 'midi';
39 $info['audio']['dataformat'] = 'midi';
41 $this->fseek($info['avdataoffset']);
42 $MIDIdata = $this->fread($this->getid3
->fread_buffer_size());
44 $MIDIheaderID = substr($MIDIdata, $offset, 4); // 'MThd'
45 if ($MIDIheaderID != GETID3_MIDI_MAGIC_MTHD
) {
46 $this->error('Expecting "'.getid3_lib
::PrintHexBytes(GETID3_MIDI_MAGIC_MTHD
).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib
::PrintHexBytes($MIDIheaderID).'"');
47 unset($info['fileformat']);
51 $thisfile_midi_raw['headersize'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 4));
53 $thisfile_midi_raw['fileformat'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 2));
55 $thisfile_midi_raw['tracks'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 2));
57 $thisfile_midi_raw['ticksperqnote'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 2));
60 for ($i = 0; $i < $thisfile_midi_raw['tracks']; $i++
) {
61 while ((strlen($MIDIdata) - $offset) < 8) {
62 if ($buffer = $this->fread($this->getid3
->fread_buffer_size())) {
65 $this->warning('only processed '.($i - 1).' of '.$thisfile_midi_raw['tracks'].' tracks');
66 $this->error('Unabled to read more file data at '.$this->ftell().' (trying to seek to : '.$offset.'), was expecting at least 8 more bytes');
70 $trackID = substr($MIDIdata, $offset, 4);
72 if ($trackID == GETID3_MIDI_MAGIC_MTRK
) {
73 $tracksize = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 4));
75 //$thisfile_midi['tracks'][$i]['size'] = $tracksize;
76 $trackdataarray[$i] = substr($MIDIdata, $offset, $tracksize);
77 $offset +
= $tracksize;
79 $this->error('Expecting "'.getid3_lib
::PrintHexBytes(GETID3_MIDI_MAGIC_MTRK
).'" at '.($offset - 4).', found "'.getid3_lib
::PrintHexBytes($trackID).'" instead');
84 if (!isset($trackdataarray) ||
!is_array($trackdataarray)) {
85 $this->error('Cannot find MIDI track information');
86 unset($thisfile_midi);
87 unset($info['fileformat']);
91 if ($this->scanwholefile
) { // this can take quite a long time, so have the option to bypass it if speed is very important
92 $thisfile_midi['totalticks'] = 0;
93 $info['playtime_seconds'] = 0;
94 $CurrentMicroSecondsPerBeat = 500000; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat
95 $CurrentBeatsPerMinute = 120; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat
96 $MicroSecondsPerQuarterNoteAfter = array ();
97 $MIDIevents = array();
99 foreach ($trackdataarray as $tracknumber => $trackdata) {
102 $LastIssuedMIDIcommand = 0;
103 $LastIssuedMIDIchannel = 0;
104 $CumulativeDeltaTime = 0;
105 $TicksAtCurrentBPM = 0;
106 while ($eventsoffset < strlen($trackdata)) {
108 if (isset($MIDIevents[$tracknumber]) && is_array($MIDIevents[$tracknumber])) {
109 $eventid = count($MIDIevents[$tracknumber]);
112 for ($i = 0; $i < 4; $i++
) {
113 $deltatimebyte = ord(substr($trackdata, $eventsoffset++
, 1));
114 $deltatime = ($deltatime << 7) +
($deltatimebyte & 0x7F);
115 if ($deltatimebyte & 0x80) {
116 // another byte follows
121 $CumulativeDeltaTime +
= $deltatime;
122 $TicksAtCurrentBPM +
= $deltatime;
123 $MIDIevents[$tracknumber][$eventid]['deltatime'] = $deltatime;
124 $MIDI_event_channel = ord(substr($trackdata, $eventsoffset++
, 1));
125 if ($MIDI_event_channel & 0x80) {
126 // OK, normal event - MIDI command has MSB set
127 $LastIssuedMIDIcommand = $MIDI_event_channel >> 4;
128 $LastIssuedMIDIchannel = $MIDI_event_channel & 0x0F;
130 // running event - assume last command
133 $MIDIevents[$tracknumber][$eventid]['eventid'] = $LastIssuedMIDIcommand;
134 $MIDIevents[$tracknumber][$eventid]['channel'] = $LastIssuedMIDIchannel;
135 if ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x08) { // Note off (key is released)
137 $notenumber = ord(substr($trackdata, $eventsoffset++
, 1));
138 $velocity = ord(substr($trackdata, $eventsoffset++
, 1));
140 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x09) { // Note on (key is pressed)
142 $notenumber = ord(substr($trackdata, $eventsoffset++
, 1));
143 $velocity = ord(substr($trackdata, $eventsoffset++
, 1));
145 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0A) { // Key after-touch
147 $notenumber = ord(substr($trackdata, $eventsoffset++
, 1));
148 $velocity = ord(substr($trackdata, $eventsoffset++
, 1));
150 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0B) { // Control Change
152 $controllernum = ord(substr($trackdata, $eventsoffset++
, 1));
153 $newvalue = ord(substr($trackdata, $eventsoffset++
, 1));
155 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0C) { // Program (patch) change
157 $newprogramnum = ord(substr($trackdata, $eventsoffset++
, 1));
159 $thisfile_midi_raw['track'][$tracknumber]['instrumentid'] = $newprogramnum;
160 if ($tracknumber == 10) {
161 $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIpercussionLookup($newprogramnum);
163 $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIinstrumentLookup($newprogramnum);
166 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0D) { // Channel after-touch
168 $channelnumber = ord(substr($trackdata, $eventsoffset++
, 1));
170 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0E) { // Pitch wheel change (2000H is normal or no change)
172 $changeLSB = ord(substr($trackdata, $eventsoffset++
, 1));
173 $changeMSB = ord(substr($trackdata, $eventsoffset++
, 1));
174 $pitchwheelchange = (($changeMSB & 0x7F) << 7) & ($changeLSB & 0x7F);
176 } elseif (($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0F) && ($MIDIevents[$tracknumber][$eventid]['channel'] == 0x0F)) {
178 $METAeventCommand = ord(substr($trackdata, $eventsoffset++
, 1));
179 $METAeventLength = ord(substr($trackdata, $eventsoffset++
, 1));
180 $METAeventData = substr($trackdata, $eventsoffset, $METAeventLength);
181 $eventsoffset +
= $METAeventLength;
182 switch ($METAeventCommand) {
183 case 0x00: // Set track sequence number
184 $track_sequence_number = getid3_lib
::BigEndian2Int(substr($METAeventData, 0, $METAeventLength));
185 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['seqno'] = $track_sequence_number;
188 case 0x01: // Text: generic
189 $text_generic = substr($METAeventData, 0, $METAeventLength);
190 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['text'] = $text_generic;
191 $thisfile_midi['comments']['comment'][] = $text_generic;
194 case 0x02: // Text: copyright
195 $text_copyright = substr($METAeventData, 0, $METAeventLength);
196 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['copyright'] = $text_copyright;
197 $thisfile_midi['comments']['copyright'][] = $text_copyright;
200 case 0x03: // Text: track name
201 $text_trackname = substr($METAeventData, 0, $METAeventLength);
202 $thisfile_midi_raw['track'][$tracknumber]['name'] = $text_trackname;
205 case 0x04: // Text: track instrument name
206 $text_instrument = substr($METAeventData, 0, $METAeventLength);
207 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['instrument'] = $text_instrument;
210 case 0x05: // Text: lyrics
211 $text_lyrics = substr($METAeventData, 0, $METAeventLength);
212 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['lyrics'] = $text_lyrics;
213 if (!isset($thisfile_midi['lyrics'])) {
214 $thisfile_midi['lyrics'] = '';
216 $thisfile_midi['lyrics'] .= $text_lyrics."\n";
219 case 0x06: // Text: marker
220 $text_marker = substr($METAeventData, 0, $METAeventLength);
221 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['marker'] = $text_marker;
224 case 0x07: // Text: cue point
225 $text_cuepoint = substr($METAeventData, 0, $METAeventLength);
226 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['cuepoint'] = $text_cuepoint;
229 case 0x2F: // End Of Track
230 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['EOT'] = $CumulativeDeltaTime;
233 case 0x51: // Tempo: microseconds / quarter note
234 $CurrentMicroSecondsPerBeat = getid3_lib
::BigEndian2Int(substr($METAeventData, 0, $METAeventLength));
235 if ($CurrentMicroSecondsPerBeat == 0) {
236 $this->error('Corrupt MIDI file: CurrentMicroSecondsPerBeat == zero');
239 $thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat;
240 $CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60;
241 $MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat;
242 $TicksAtCurrentBPM = 0;
245 case 0x58: // Time signature
246 $timesig_numerator = getid3_lib
::BigEndian2Int($METAeventData{0});
247 $timesig_denominator = pow(2, getid3_lib
::BigEndian2Int($METAeventData{1})); // $02 -> x/4, $03 -> x/8, etc
248 $timesig_32inqnote = getid3_lib
::BigEndian2Int($METAeventData{2}); // number of 32nd notes to the quarter note
249 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_32inqnote'] = $timesig_32inqnote;
250 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_numerator'] = $timesig_numerator;
251 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_denominator'] = $timesig_denominator;
252 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_text'] = $timesig_numerator.'/'.$timesig_denominator;
253 $thisfile_midi['timesignature'][] = $timesig_numerator.'/'.$timesig_denominator;
256 case 0x59: // Keysignature
257 $keysig_sharpsflats = getid3_lib
::BigEndian2Int($METAeventData{0});
258 if ($keysig_sharpsflats & 0x80) {
259 // (-7 -> 7 flats, 0 ->key of C, 7 -> 7 sharps)
260 $keysig_sharpsflats -= 256;
263 $keysig_majorminor = getid3_lib
::BigEndian2Int($METAeventData{1}); // 0 -> major, 1 -> minor
264 $keysigs = array(-7=>'Cb', -6=>'Gb', -5=>'Db', -4=>'Ab', -3=>'Eb', -2=>'Bb', -1=>'F', 0=>'C', 1=>'G', 2=>'D', 3=>'A', 4=>'E', 5=>'B', 6=>'F#', 7=>'C#');
265 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_sharps'] = (($keysig_sharpsflats > 0) ? abs($keysig_sharpsflats) : 0);
266 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_flats'] = (($keysig_sharpsflats < 0) ? abs($keysig_sharpsflats) : 0);
267 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] = (bool) $keysig_majorminor;
268 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_text'] = $keysigs[$keysig_sharpsflats].' '.($thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] ? 'minor' : 'major');
270 // $keysigs[$keysig_sharpsflats] gets an int key (correct) - $keysigs["$keysig_sharpsflats"] gets a string key (incorrect)
271 $thisfile_midi['keysignature'][] = $keysigs[$keysig_sharpsflats].' '.((bool) $keysig_majorminor ?
'minor' : 'major');
274 case 0x7F: // Sequencer specific information
275 $custom_data = substr($METAeventData, 0, $METAeventLength);
279 $this->warning('Unhandled META Event Command: '.$METAeventCommand);
285 $this->warning('Unhandled MIDI Event ID: '.$MIDIevents[$tracknumber][$eventid]['eventid'].' + Channel ID: '.$MIDIevents[$tracknumber][$eventid]['channel']);
289 if (($tracknumber > 0) ||
(count($trackdataarray) == 1)) {
290 $thisfile_midi['totalticks'] = max($thisfile_midi['totalticks'], $CumulativeDeltaTime);
293 $previoustickoffset = null;
294 $prevmicrosecondsperbeat = null;
296 ksort($MicroSecondsPerQuarterNoteAfter);
297 foreach ($MicroSecondsPerQuarterNoteAfter as $tickoffset => $microsecondsperbeat) {
298 if (is_null($previoustickoffset)) {
299 $prevmicrosecondsperbeat = $microsecondsperbeat;
300 $previoustickoffset = $tickoffset;
303 if ($thisfile_midi['totalticks'] > $tickoffset) {
305 if ($thisfile_midi_raw['ticksperqnote'] == 0) {
306 $this->error('Corrupt MIDI file: ticksperqnote == zero');
310 $info['playtime_seconds'] +
= (($tickoffset - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
312 $prevmicrosecondsperbeat = $microsecondsperbeat;
313 $previoustickoffset = $tickoffset;
316 if ($thisfile_midi['totalticks'] > $previoustickoffset) {
318 if ($thisfile_midi_raw['ticksperqnote'] == 0) {
319 $this->error('Corrupt MIDI file: ticksperqnote == zero');
323 $info['playtime_seconds'] +
= (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
329 if (!empty($info['playtime_seconds'])) {
330 $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
333 if (!empty($thisfile_midi['lyrics'])) {
334 $thisfile_midi['comments']['lyrics'][] = $thisfile_midi['lyrics'];
341 * @param int $instrumentid
345 public function GeneralMIDIinstrumentLookup($instrumentid) {
349 /** This is not a comment!
375 24 Acoustic Guitar (nylon)
376 25 Acoustic Guitar (steel)
377 26 Electric Guitar (jazz)
378 27 Electric Guitar (clean)
379 28 Electric Guitar (muted)
384 33 Electric Bass (finger)
385 34 Electric Bass (pick)
397 46 Orchestral Strings
438 87 Lead 8 (bass + lead)
451 100 FX 5 (brightness)
471 120 Guitar Fret Noise
482 return getid3_lib
::EmbeddedLookup($instrumentid, $begin, __LINE__
, __FILE__
, 'GeneralMIDIinstrument');
486 * @param int $instrumentid
490 public function GeneralMIDIpercussionLookup($instrumentid) {
494 /** This is not a comment!
496 35 Acoustic Bass Drum
545 return getid3_lib
::EmbeddedLookup($instrumentid, $begin, __LINE__
, __FILE__
, 'GeneralMIDIpercussion');