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 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
21 define('GETID3_MIDI_MAGIC_MTHD', 'MThd'); // MIDI file header magic
22 define('GETID3_MIDI_MAGIC_MTRK', 'MTrk'); // MIDI track header magic
24 class getid3_midi
extends getid3_handler
29 public $scanwholefile = true;
34 public function Analyze() {
35 $info = &$this->getid3
->info
;
38 $info['midi']['raw'] = array();
39 $thisfile_midi = &$info['midi'];
40 $thisfile_midi_raw = &$thisfile_midi['raw'];
42 $info['fileformat'] = 'midi';
43 $info['audio']['dataformat'] = 'midi';
45 $this->fseek($info['avdataoffset']);
46 $MIDIdata = $this->fread($this->getid3
->fread_buffer_size());
48 $MIDIheaderID = substr($MIDIdata, $offset, 4); // 'MThd'
49 if ($MIDIheaderID != GETID3_MIDI_MAGIC_MTHD
) {
50 $this->error('Expecting "'.getid3_lib
::PrintHexBytes(GETID3_MIDI_MAGIC_MTHD
).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib
::PrintHexBytes($MIDIheaderID).'"');
51 unset($info['fileformat']);
55 $thisfile_midi_raw['headersize'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 4));
57 $thisfile_midi_raw['fileformat'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 2));
59 $thisfile_midi_raw['tracks'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 2));
61 $thisfile_midi_raw['ticksperqnote'] = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 2));
64 for ($i = 0; $i < $thisfile_midi_raw['tracks']; $i++
) {
65 while ((strlen($MIDIdata) - $offset) < 8) {
66 if ($buffer = $this->fread($this->getid3
->fread_buffer_size())) {
69 $this->warning('only processed '.($i - 1).' of '.$thisfile_midi_raw['tracks'].' tracks');
70 $this->error('Unabled to read more file data at '.$this->ftell().' (trying to seek to : '.$offset.'), was expecting at least 8 more bytes');
74 $trackID = substr($MIDIdata, $offset, 4);
76 if ($trackID == GETID3_MIDI_MAGIC_MTRK
) {
77 $tracksize = getid3_lib
::BigEndian2Int(substr($MIDIdata, $offset, 4));
79 //$thisfile_midi['tracks'][$i]['size'] = $tracksize;
80 $trackdataarray[$i] = substr($MIDIdata, $offset, $tracksize);
81 $offset +
= $tracksize;
83 $this->error('Expecting "'.getid3_lib
::PrintHexBytes(GETID3_MIDI_MAGIC_MTRK
).'" at '.($offset - 4).', found "'.getid3_lib
::PrintHexBytes($trackID).'" instead');
88 if (!isset($trackdataarray) ||
!is_array($trackdataarray)) {
89 $this->error('Cannot find MIDI track information');
90 unset($thisfile_midi);
91 unset($info['fileformat']);
95 if ($this->scanwholefile
) { // this can take quite a long time, so have the option to bypass it if speed is very important
96 $thisfile_midi['totalticks'] = 0;
97 $info['playtime_seconds'] = 0;
98 $CurrentMicroSecondsPerBeat = 500000; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat
99 $CurrentBeatsPerMinute = 120; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat
100 $MicroSecondsPerQuarterNoteAfter = array ();
101 $MIDIevents = array();
103 foreach ($trackdataarray as $tracknumber => $trackdata) {
106 $LastIssuedMIDIcommand = 0;
107 $LastIssuedMIDIchannel = 0;
108 $CumulativeDeltaTime = 0;
109 $TicksAtCurrentBPM = 0;
110 while ($eventsoffset < strlen($trackdata)) {
112 if (isset($MIDIevents[$tracknumber]) && is_array($MIDIevents[$tracknumber])) {
113 $eventid = count($MIDIevents[$tracknumber]);
116 for ($i = 0; $i < 4; $i++
) {
117 $deltatimebyte = ord(substr($trackdata, $eventsoffset++
, 1));
118 $deltatime = ($deltatime << 7) +
($deltatimebyte & 0x7F);
119 if ($deltatimebyte & 0x80) {
120 // another byte follows
125 $CumulativeDeltaTime +
= $deltatime;
126 $TicksAtCurrentBPM +
= $deltatime;
127 $MIDIevents[$tracknumber][$eventid]['deltatime'] = $deltatime;
128 $MIDI_event_channel = ord(substr($trackdata, $eventsoffset++
, 1));
129 if ($MIDI_event_channel & 0x80) {
130 // OK, normal event - MIDI command has MSB set
131 $LastIssuedMIDIcommand = $MIDI_event_channel >> 4;
132 $LastIssuedMIDIchannel = $MIDI_event_channel & 0x0F;
134 // running event - assume last command
137 $MIDIevents[$tracknumber][$eventid]['eventid'] = $LastIssuedMIDIcommand;
138 $MIDIevents[$tracknumber][$eventid]['channel'] = $LastIssuedMIDIchannel;
139 if ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x08) { // Note off (key is released)
141 $notenumber = ord(substr($trackdata, $eventsoffset++
, 1));
142 $velocity = ord(substr($trackdata, $eventsoffset++
, 1));
144 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x09) { // Note on (key is pressed)
146 $notenumber = ord(substr($trackdata, $eventsoffset++
, 1));
147 $velocity = ord(substr($trackdata, $eventsoffset++
, 1));
149 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0A) { // Key after-touch
151 $notenumber = ord(substr($trackdata, $eventsoffset++
, 1));
152 $velocity = ord(substr($trackdata, $eventsoffset++
, 1));
154 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0B) { // Control Change
156 $controllernum = ord(substr($trackdata, $eventsoffset++
, 1));
157 $newvalue = ord(substr($trackdata, $eventsoffset++
, 1));
159 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0C) { // Program (patch) change
161 $newprogramnum = ord(substr($trackdata, $eventsoffset++
, 1));
163 $thisfile_midi_raw['track'][$tracknumber]['instrumentid'] = $newprogramnum;
164 if ($tracknumber == 10) {
165 $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIpercussionLookup($newprogramnum);
167 $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIinstrumentLookup($newprogramnum);
170 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0D) { // Channel after-touch
172 $channelnumber = ord(substr($trackdata, $eventsoffset++
, 1));
174 } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0E) { // Pitch wheel change (2000H is normal or no change)
176 $changeLSB = ord(substr($trackdata, $eventsoffset++
, 1));
177 $changeMSB = ord(substr($trackdata, $eventsoffset++
, 1));
178 $pitchwheelchange = (($changeMSB & 0x7F) << 7) & ($changeLSB & 0x7F);
180 } elseif (($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0F) && ($MIDIevents[$tracknumber][$eventid]['channel'] == 0x0F)) {
182 $METAeventCommand = ord(substr($trackdata, $eventsoffset++
, 1));
183 $METAeventLength = ord(substr($trackdata, $eventsoffset++
, 1));
184 $METAeventData = substr($trackdata, $eventsoffset, $METAeventLength);
185 $eventsoffset +
= $METAeventLength;
186 switch ($METAeventCommand) {
187 case 0x00: // Set track sequence number
188 $track_sequence_number = getid3_lib
::BigEndian2Int(substr($METAeventData, 0, $METAeventLength));
189 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['seqno'] = $track_sequence_number;
192 case 0x01: // Text: generic
193 $text_generic = substr($METAeventData, 0, $METAeventLength);
194 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['text'] = $text_generic;
195 $thisfile_midi['comments']['comment'][] = $text_generic;
198 case 0x02: // Text: copyright
199 $text_copyright = substr($METAeventData, 0, $METAeventLength);
200 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['copyright'] = $text_copyright;
201 $thisfile_midi['comments']['copyright'][] = $text_copyright;
204 case 0x03: // Text: track name
205 $text_trackname = substr($METAeventData, 0, $METAeventLength);
206 $thisfile_midi_raw['track'][$tracknumber]['name'] = $text_trackname;
209 case 0x04: // Text: track instrument name
210 $text_instrument = substr($METAeventData, 0, $METAeventLength);
211 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['instrument'] = $text_instrument;
214 case 0x05: // Text: lyrics
215 $text_lyrics = substr($METAeventData, 0, $METAeventLength);
216 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['lyrics'] = $text_lyrics;
217 if (!isset($thisfile_midi['lyrics'])) {
218 $thisfile_midi['lyrics'] = '';
220 $thisfile_midi['lyrics'] .= $text_lyrics."\n";
223 case 0x06: // Text: marker
224 $text_marker = substr($METAeventData, 0, $METAeventLength);
225 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['marker'] = $text_marker;
228 case 0x07: // Text: cue point
229 $text_cuepoint = substr($METAeventData, 0, $METAeventLength);
230 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['cuepoint'] = $text_cuepoint;
233 case 0x2F: // End Of Track
234 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['EOT'] = $CumulativeDeltaTime;
237 case 0x51: // Tempo: microseconds / quarter note
238 $CurrentMicroSecondsPerBeat = getid3_lib
::BigEndian2Int(substr($METAeventData, 0, $METAeventLength));
239 if ($CurrentMicroSecondsPerBeat == 0) {
240 $this->error('Corrupt MIDI file: CurrentMicroSecondsPerBeat == zero');
243 $thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat;
244 $CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60;
245 $MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat;
246 $TicksAtCurrentBPM = 0;
249 case 0x58: // Time signature
250 $timesig_numerator = getid3_lib
::BigEndian2Int($METAeventData[0]);
251 $timesig_denominator = pow(2, getid3_lib
::BigEndian2Int($METAeventData[1])); // $02 -> x/4, $03 -> x/8, etc
252 $timesig_32inqnote = getid3_lib
::BigEndian2Int($METAeventData[2]); // number of 32nd notes to the quarter note
253 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_32inqnote'] = $timesig_32inqnote;
254 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_numerator'] = $timesig_numerator;
255 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_denominator'] = $timesig_denominator;
256 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_text'] = $timesig_numerator.'/'.$timesig_denominator;
257 $thisfile_midi['timesignature'][] = $timesig_numerator.'/'.$timesig_denominator;
260 case 0x59: // Keysignature
261 $keysig_sharpsflats = getid3_lib
::BigEndian2Int($METAeventData[0]);
262 if ($keysig_sharpsflats & 0x80) {
263 // (-7 -> 7 flats, 0 ->key of C, 7 -> 7 sharps)
264 $keysig_sharpsflats -= 256;
267 $keysig_majorminor = getid3_lib
::BigEndian2Int($METAeventData[1]); // 0 -> major, 1 -> minor
268 $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#');
269 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_sharps'] = (($keysig_sharpsflats > 0) ? abs($keysig_sharpsflats) : 0);
270 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_flats'] = (($keysig_sharpsflats < 0) ? abs($keysig_sharpsflats) : 0);
271 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] = (bool) $keysig_majorminor;
272 //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_text'] = $keysigs[$keysig_sharpsflats].' '.($thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] ? 'minor' : 'major');
274 // $keysigs[$keysig_sharpsflats] gets an int key (correct) - $keysigs["$keysig_sharpsflats"] gets a string key (incorrect)
275 $thisfile_midi['keysignature'][] = $keysigs[$keysig_sharpsflats].' '.((bool) $keysig_majorminor ?
'minor' : 'major');
278 case 0x7F: // Sequencer specific information
279 $custom_data = substr($METAeventData, 0, $METAeventLength);
283 $this->warning('Unhandled META Event Command: '.$METAeventCommand);
289 $this->warning('Unhandled MIDI Event ID: '.$MIDIevents[$tracknumber][$eventid]['eventid'].' + Channel ID: '.$MIDIevents[$tracknumber][$eventid]['channel']);
293 if (($tracknumber > 0) ||
(count($trackdataarray) == 1)) {
294 $thisfile_midi['totalticks'] = max($thisfile_midi['totalticks'], $CumulativeDeltaTime);
297 $previoustickoffset = null;
298 $prevmicrosecondsperbeat = null;
300 ksort($MicroSecondsPerQuarterNoteAfter);
301 foreach ($MicroSecondsPerQuarterNoteAfter as $tickoffset => $microsecondsperbeat) {
302 if (is_null($previoustickoffset)) {
303 $prevmicrosecondsperbeat = $microsecondsperbeat;
304 $previoustickoffset = $tickoffset;
307 if ($thisfile_midi['totalticks'] > $tickoffset) {
309 if ($thisfile_midi_raw['ticksperqnote'] == 0) {
310 $this->error('Corrupt MIDI file: ticksperqnote == zero');
314 $info['playtime_seconds'] +
= (($tickoffset - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
316 $prevmicrosecondsperbeat = $microsecondsperbeat;
317 $previoustickoffset = $tickoffset;
320 if ($thisfile_midi['totalticks'] > $previoustickoffset) {
322 if ($thisfile_midi_raw['ticksperqnote'] == 0) {
323 $this->error('Corrupt MIDI file: ticksperqnote == zero');
327 $info['playtime_seconds'] +
= (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
333 if (!empty($info['playtime_seconds'])) {
334 $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
337 if (!empty($thisfile_midi['lyrics'])) {
338 $thisfile_midi['comments']['lyrics'][] = $thisfile_midi['lyrics'];
345 * @param int $instrumentid
349 public function GeneralMIDIinstrumentLookup($instrumentid) {
353 /** This is not a comment!
379 24 Acoustic Guitar (nylon)
380 25 Acoustic Guitar (steel)
381 26 Electric Guitar (jazz)
382 27 Electric Guitar (clean)
383 28 Electric Guitar (muted)
388 33 Electric Bass (finger)
389 34 Electric Bass (pick)
401 46 Orchestral Strings
442 87 Lead 8 (bass + lead)
455 100 FX 5 (brightness)
475 120 Guitar Fret Noise
486 return getid3_lib
::EmbeddedLookup($instrumentid, $begin, __LINE__
, __FILE__
, 'GeneralMIDIinstrument');
490 * @param int $instrumentid
494 public function GeneralMIDIpercussionLookup($instrumentid) {
498 /** This is not a comment!
500 35 Acoustic Bass Drum
549 return getid3_lib
::EmbeddedLookup($instrumentid, $begin, __LINE__
, __FILE__
, 'GeneralMIDIpercussion');