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