ILIAS  release_4-3 Revision
 All Data Structures Namespaces Files Functions Variables Groups Pages
module.audio.midi.php
Go to the documentation of this file.
1 <?php
4 // available at http://getid3.sourceforge.net //
5 // or http://www.getid3.org //
7 // See readme.txt for more details //
9 // //
10 // module.audio.midi.php //
11 // module for Midi Audio files //
12 // dependencies: NONE //
13 // ///
15 
16 
18 {
19 
20  function getid3_midi(&$fd, &$ThisFileInfo, $scanwholefile=true) {
21 
22  // shortcut
23  $ThisFileInfo['midi']['raw'] = array();
24  $thisfile_midi = &$ThisFileInfo['midi'];
25  $thisfile_midi_raw = &$thisfile_midi['raw'];
26 
27  $ThisFileInfo['fileformat'] = 'midi';
28  $ThisFileInfo['audio']['dataformat'] = 'midi';
29 
30  fseek($fd, $ThisFileInfo['avdataoffset'], SEEK_SET);
31  $MIDIdata = fread($fd, GETID3_FREAD_BUFFER_SIZE);
32  $offset = 0;
33  $MIDIheaderID = substr($MIDIdata, $offset, 4); // 'MThd'
34  if ($MIDIheaderID != 'MThd') {
35  $ThisFileInfo['error'][] = 'Expecting "MThd" at offset '.$ThisFileInfo['avdataoffset'].', found "'.$MIDIheaderID.'"';
36  unset($ThisFileInfo['fileformat']);
37  return false;
38  }
39  $offset += 4;
40  $thisfile_midi_raw['headersize'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 4));
41  $offset += 4;
42  $thisfile_midi_raw['fileformat'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2));
43  $offset += 2;
44  $thisfile_midi_raw['tracks'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2));
45  $offset += 2;
46  $thisfile_midi_raw['ticksperqnote'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2));
47  $offset += 2;
48 
49  for ($i = 0; $i < $thisfile_midi_raw['tracks']; $i++) {
50  if ((strlen($MIDIdata) - $offset) < 8) {
51  $MIDIdata .= fread($fd, GETID3_FREAD_BUFFER_SIZE);
52  }
53  $trackID = substr($MIDIdata, $offset, 4);
54  $offset += 4;
55  if ($trackID == 'MTrk') {
56  $tracksize = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 4));
57  $offset += 4;
58  // $thisfile_midi['tracks'][$i]['size'] = $tracksize;
59  $trackdataarray[$i] = substr($MIDIdata, $offset, $tracksize);
60  $offset += $tracksize;
61  } else {
62  $ThisFileInfo['error'][] = 'Expecting "MTrk" at '.$offset.', found '.$trackID.' instead';
63  return false;
64  }
65  }
66 
67  if (!isset($trackdataarray) || !is_array($trackdataarray)) {
68  $ThisFileInfo['error'][] = 'Cannot find MIDI track information';
69  unset($thisfile_midi);
70  unset($ThisFileInfo['fileformat']);
71  return false;
72  }
73 
74  if ($scanwholefile) { // this can take quite a long time, so have the option to bypass it if speed is very important
75  $thisfile_midi['totalticks'] = 0;
76  $ThisFileInfo['playtime_seconds'] = 0;
77  $CurrentMicroSecondsPerBeat = 500000; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat
78  $CurrentBeatsPerMinute = 120; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat
79 
80  foreach ($trackdataarray as $tracknumber => $trackdata) {
81 
82  $eventsoffset = 0;
83  $LastIssuedMIDIcommand = 0;
84  $LastIssuedMIDIchannel = 0;
85  $CumulativeDeltaTime = 0;
86  $TicksAtCurrentBPM = 0;
87  while ($eventsoffset < strlen($trackdata)) {
88  $eventid = 0;
89  if (isset($MIDIevents[$tracknumber]) && is_array($MIDIevents[$tracknumber])) {
90  $eventid = count($MIDIevents[$tracknumber]);
91  }
92  $deltatime = 0;
93  for ($i = 0; $i < 4; $i++) {
94  $deltatimebyte = ord(substr($trackdata, $eventsoffset++, 1));
95  $deltatime = ($deltatime << 7) + ($deltatimebyte & 0x7F);
96  if ($deltatimebyte & 0x80) {
97  // another byte follows
98  } else {
99  break;
100  }
101  }
102  $CumulativeDeltaTime += $deltatime;
103  $TicksAtCurrentBPM += $deltatime;
104  $MIDIevents[$tracknumber][$eventid]['deltatime'] = $deltatime;
105  $MIDI_event_channel = ord(substr($trackdata, $eventsoffset++, 1));
106  if ($MIDI_event_channel & 0x80) {
107  // OK, normal event - MIDI command has MSB set
108  $LastIssuedMIDIcommand = $MIDI_event_channel >> 4;
109  $LastIssuedMIDIchannel = $MIDI_event_channel & 0x0F;
110  } else {
111  // running event - assume last command
112  $eventsoffset--;
113  }
114  $MIDIevents[$tracknumber][$eventid]['eventid'] = $LastIssuedMIDIcommand;
115  $MIDIevents[$tracknumber][$eventid]['channel'] = $LastIssuedMIDIchannel;
116  if ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x08) { // Note off (key is released)
117 
118  $notenumber = ord(substr($trackdata, $eventsoffset++, 1));
119  $velocity = ord(substr($trackdata, $eventsoffset++, 1));
120 
121  } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x09) { // Note on (key is pressed)
122 
123  $notenumber = ord(substr($trackdata, $eventsoffset++, 1));
124  $velocity = ord(substr($trackdata, $eventsoffset++, 1));
125 
126  } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0A) { // Key after-touch
127 
128  $notenumber = ord(substr($trackdata, $eventsoffset++, 1));
129  $velocity = ord(substr($trackdata, $eventsoffset++, 1));
130 
131  } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0B) { // Control Change
132 
133  $controllernum = ord(substr($trackdata, $eventsoffset++, 1));
134  $newvalue = ord(substr($trackdata, $eventsoffset++, 1));
135 
136  } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0C) { // Program (patch) change
137 
138  $newprogramnum = ord(substr($trackdata, $eventsoffset++, 1));
139 
140  $thisfile_midi_raw['track'][$tracknumber]['instrumentid'] = $newprogramnum;
141  if ($tracknumber == 10) {
142  $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIpercussionLookup($newprogramnum);
143  } else {
144  $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIinstrumentLookup($newprogramnum);
145  }
146 
147  } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0D) { // Channel after-touch
148 
149  $channelnumber = ord(substr($trackdata, $eventsoffset++, 1));
150 
151  } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0E) { // Pitch wheel change (2000H is normal or no change)
152 
153  $changeLSB = ord(substr($trackdata, $eventsoffset++, 1));
154  $changeMSB = ord(substr($trackdata, $eventsoffset++, 1));
155  $pitchwheelchange = (($changeMSB & 0x7F) << 7) & ($changeLSB & 0x7F);
156 
157  } elseif (($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0F) && ($MIDIevents[$tracknumber][$eventid]['channel'] == 0x0F)) {
158 
159  $METAeventCommand = ord(substr($trackdata, $eventsoffset++, 1));
160  $METAeventLength = ord(substr($trackdata, $eventsoffset++, 1));
161  $METAeventData = substr($trackdata, $eventsoffset, $METAeventLength);
162  $eventsoffset += $METAeventLength;
163  switch ($METAeventCommand) {
164  case 0x00: // Set track sequence number
165  $track_sequence_number = getid3_lib::BigEndian2Int(substr($METAeventData, 0, $METAeventLength));
166  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['seqno'] = $track_sequence_number;
167  break;
168 
169  case 0x01: // Text: generic
170  $text_generic = substr($METAeventData, 0, $METAeventLength);
171  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['text'] = $text_generic;
172  $thisfile_midi['comments']['comment'][] = $text_generic;
173  break;
174 
175  case 0x02: // Text: copyright
176  $text_copyright = substr($METAeventData, 0, $METAeventLength);
177  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['copyright'] = $text_copyright;
178  $thisfile_midi['comments']['copyright'][] = $text_copyright;
179  break;
180 
181  case 0x03: // Text: track name
182  $text_trackname = substr($METAeventData, 0, $METAeventLength);
183  $thisfile_midi_raw['track'][$tracknumber]['name'] = $text_trackname;
184  break;
185 
186  case 0x04: // Text: track instrument name
187  $text_instrument = substr($METAeventData, 0, $METAeventLength);
188  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['instrument'] = $text_instrument;
189  break;
190 
191  case 0x05: // Text: lyrics
192  $text_lyrics = substr($METAeventData, 0, $METAeventLength);
193  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['lyrics'] = $text_lyrics;
194  if (!isset($thisfile_midi['lyrics'])) {
195  $thisfile_midi['lyrics'] = '';
196  }
197  $thisfile_midi['lyrics'] .= $text_lyrics."\n";
198  break;
199 
200  case 0x06: // Text: marker
201  $text_marker = substr($METAeventData, 0, $METAeventLength);
202  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['marker'] = $text_marker;
203  break;
204 
205  case 0x07: // Text: cue point
206  $text_cuepoint = substr($METAeventData, 0, $METAeventLength);
207  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['cuepoint'] = $text_cuepoint;
208  break;
209 
210  case 0x2F: // End Of Track
211  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['EOT'] = $CumulativeDeltaTime;
212  break;
213 
214  case 0x51: // Tempo: microseconds / quarter note
215  $CurrentMicroSecondsPerBeat = getid3_lib::BigEndian2Int(substr($METAeventData, 0, $METAeventLength));
216  if ($CurrentMicroSecondsPerBeat == 0) {
217  $ThisFileInfo['error'][] = 'Corrupt MIDI file: CurrentMicroSecondsPerBeat == zero';
218  return false;
219  }
220  $thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat;
221  $CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60;
222  $MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat;
223  $TicksAtCurrentBPM = 0;
224  break;
225 
226  case 0x58: // Time signature
227  $timesig_numerator = getid3_lib::BigEndian2Int($METAeventData{0});
228  $timesig_denominator = pow(2, getid3_lib::BigEndian2Int($METAeventData{1})); // $02 -> x/4, $03 -> x/8, etc
229  $timesig_32inqnote = getid3_lib::BigEndian2Int($METAeventData{2}); // number of 32nd notes to the quarter note
230  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_32inqnote'] = $timesig_32inqnote;
231  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_numerator'] = $timesig_numerator;
232  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_denominator'] = $timesig_denominator;
233  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_text'] = $timesig_numerator.'/'.$timesig_denominator;
234  $thisfile_midi['timesignature'][] = $timesig_numerator.'/'.$timesig_denominator;
235  break;
236 
237  case 0x59: // Keysignature
238  $keysig_sharpsflats = getid3_lib::BigEndian2Int($METAeventData{0});
239  if ($keysig_sharpsflats & 0x80) {
240  // (-7 -> 7 flats, 0 ->key of C, 7 -> 7 sharps)
241  $keysig_sharpsflats -= 256;
242  }
243 
244  $keysig_majorminor = getid3_lib::BigEndian2Int($METAeventData{1}); // 0 -> major, 1 -> minor
245  $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#');
246  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_sharps'] = (($keysig_sharpsflats > 0) ? abs($keysig_sharpsflats) : 0);
247  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_flats'] = (($keysig_sharpsflats < 0) ? abs($keysig_sharpsflats) : 0);
248  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] = (bool) $keysig_majorminor;
249  //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_text'] = $keysigs[$keysig_sharpsflats].' '.($thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] ? 'minor' : 'major');
250 
251  // $keysigs[$keysig_sharpsflats] gets an int key (correct) - $keysigs["$keysig_sharpsflats"] gets a string key (incorrect)
252  $thisfile_midi['keysignature'][] = $keysigs[$keysig_sharpsflats].' '.((bool) $keysig_majorminor ? 'minor' : 'major');
253  break;
254 
255  case 0x7F: // Sequencer specific information
256  $custom_data = substr($METAeventData, 0, $METAeventLength);
257  break;
258 
259  default:
260  $ThisFileInfo['warning'][] = 'Unhandled META Event Command: '.$METAeventCommand;
261  break;
262  }
263 
264  } else {
265 
266  $ThisFileInfo['warning'][] = 'Unhandled MIDI Event ID: '.$MIDIevents[$tracknumber][$eventid]['eventid'].' + Channel ID: '.$MIDIevents[$tracknumber][$eventid]['channel'];
267 
268  }
269  }
270  if (($tracknumber > 0) || (count($trackdataarray) == 1)) {
271  $thisfile_midi['totalticks'] = max($thisfile_midi['totalticks'], $CumulativeDeltaTime);
272  }
273  }
274  $previoustickoffset = null;
275 
276  ksort($MicroSecondsPerQuarterNoteAfter);
277  foreach ($MicroSecondsPerQuarterNoteAfter as $tickoffset => $microsecondsperbeat) {
278  if (is_null($previoustickoffset)) {
279  $prevmicrosecondsperbeat = $microsecondsperbeat;
280  $previoustickoffset = $tickoffset;
281  continue;
282  }
283  if ($thisfile_midi['totalticks'] > $tickoffset) {
284 
285  if ($thisfile_midi_raw['ticksperqnote'] == 0) {
286  $ThisFileInfo['error'][] = 'Corrupt MIDI file: ticksperqnote == zero';
287  return false;
288  }
289 
290  $ThisFileInfo['playtime_seconds'] += (($tickoffset - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
291 
292  $prevmicrosecondsperbeat = $microsecondsperbeat;
293  $previoustickoffset = $tickoffset;
294  }
295  }
296  if ($thisfile_midi['totalticks'] > $previoustickoffset) {
297 
298  if ($thisfile_midi_raw['ticksperqnote'] == 0) {
299  $ThisFileInfo['error'][] = 'Corrupt MIDI file: ticksperqnote == zero';
300  return false;
301  }
302 
303  $ThisFileInfo['playtime_seconds'] += (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($microsecondsperbeat / 1000000);
304 
305  }
306  }
307 
308  if ($ThisFileInfo['playtime_seconds'] > 0) {
309  $ThisFileInfo['bitrate'] = (($ThisFileInfo['avdataend'] - $ThisFileInfo['avdataoffset']) * 8) / $ThisFileInfo['playtime_seconds'];
310  }
311 
312  if (!empty($thisfile_midi['lyrics'])) {
313  $thisfile_midi['comments']['lyrics'][] = $thisfile_midi['lyrics'];
314  }
315 
316  return true;
317  }
318 
319  function GeneralMIDIinstrumentLookup($instrumentid) {
320 
321  $begin = __LINE__;
322 
456  return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIinstrument');
457  }
458 
459  function GeneralMIDIpercussionLookup($instrumentid) {
460 
461  $begin = __LINE__;
462 
514  return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIpercussion');
515  }
516 
517 }
518 
519 
520 ?>