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