1<?php
2
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/////////////////////////////////////////////////////////////////
10//                                                             //
11// module.audio.midi.php                                       //
12// module for Midi Audio files                                 //
13// dependencies: NONE                                          //
14//                                                            ///
15/////////////////////////////////////////////////////////////////
16
17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
18	exit;
19}
20
21define('GETID3_MIDI_MAGIC_MTHD', 'MThd'); // MIDI file header magic
22define('GETID3_MIDI_MAGIC_MTRK', 'MTrk'); // MIDI track header magic
23
24class getid3_midi extends getid3_handler
25{
26	/**
27	 * @var bool
28	 */
29	public $scanwholefile = true;
30
31	/**
32	 * @return bool
33	 */
34	public function Analyze() {
35		$info = &$this->getid3->info;
36
37		// shortcut
38		$info['midi']['raw'] = array();
39		$thisfile_midi               = &$info['midi'];
40		$thisfile_midi_raw           = &$thisfile_midi['raw'];
41
42		$info['fileformat']          = 'midi';
43		$info['audio']['dataformat'] = 'midi';
44
45		$this->fseek($info['avdataoffset']);
46		$MIDIdata = $this->fread($this->getid3->fread_buffer_size());
47		$offset = 0;
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']);
52			return false;
53		}
54		$offset += 4;
55		$thisfile_midi_raw['headersize']    = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 4));
56		$offset += 4;
57		$thisfile_midi_raw['fileformat']    = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2));
58		$offset += 2;
59		$thisfile_midi_raw['tracks']        = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2));
60		$offset += 2;
61		$thisfile_midi_raw['ticksperqnote'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2));
62		$offset += 2;
63
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())) {
67					$MIDIdata .= $buffer;
68				} else {
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');
71					return false;
72				}
73			}
74			$trackID = substr($MIDIdata, $offset, 4);
75			$offset += 4;
76			if ($trackID == GETID3_MIDI_MAGIC_MTRK) {
77				$tracksize = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 4));
78				$offset += 4;
79				//$thisfile_midi['tracks'][$i]['size'] = $tracksize;
80				$trackdataarray[$i] = substr($MIDIdata, $offset, $tracksize);
81				$offset += $tracksize;
82			} else {
83				$this->error('Expecting "'.getid3_lib::PrintHexBytes(GETID3_MIDI_MAGIC_MTRK).'" at '.($offset - 4).', found "'.getid3_lib::PrintHexBytes($trackID).'" instead');
84				return false;
85			}
86		}
87
88		if (!isset($trackdataarray) || !is_array($trackdataarray)) {
89			$this->error('Cannot find MIDI track information');
90			unset($thisfile_midi);
91			unset($info['fileformat']);
92			return false;
93		}
94
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();
102
103			foreach ($trackdataarray as $tracknumber => $trackdata) {
104
105				$eventsoffset               = 0;
106				$LastIssuedMIDIcommand      = 0;
107				$LastIssuedMIDIchannel      = 0;
108				$CumulativeDeltaTime        = 0;
109				$TicksAtCurrentBPM = 0;
110				while ($eventsoffset < strlen($trackdata)) {
111					$eventid = 0;
112					if (isset($MIDIevents[$tracknumber]) && is_array($MIDIevents[$tracknumber])) {
113						$eventid = count($MIDIevents[$tracknumber]);
114					}
115					$deltatime = 0;
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
121						} else {
122							break;
123						}
124					}
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;
133					} else {
134						// running event - assume last command
135						$eventsoffset--;
136					}
137					$MIDIevents[$tracknumber][$eventid]['eventid']   = $LastIssuedMIDIcommand;
138					$MIDIevents[$tracknumber][$eventid]['channel']   = $LastIssuedMIDIchannel;
139					if ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x08) { // Note off (key is released)
140
141						$notenumber = ord(substr($trackdata, $eventsoffset++, 1));
142						$velocity   = ord(substr($trackdata, $eventsoffset++, 1));
143
144					} elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x09) { // Note on (key is pressed)
145
146						$notenumber = ord(substr($trackdata, $eventsoffset++, 1));
147						$velocity   = ord(substr($trackdata, $eventsoffset++, 1));
148
149					} elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0A) { // Key after-touch
150
151						$notenumber = ord(substr($trackdata, $eventsoffset++, 1));
152						$velocity   = ord(substr($trackdata, $eventsoffset++, 1));
153
154					} elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0B) { // Control Change
155
156						$controllernum = ord(substr($trackdata, $eventsoffset++, 1));
157						$newvalue      = ord(substr($trackdata, $eventsoffset++, 1));
158
159					} elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0C) { // Program (patch) change
160
161						$newprogramnum = ord(substr($trackdata, $eventsoffset++, 1));
162
163						$thisfile_midi_raw['track'][$tracknumber]['instrumentid'] = $newprogramnum;
164						if ($tracknumber == 10) {
165							$thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIpercussionLookup($newprogramnum);
166						} else {
167							$thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIinstrumentLookup($newprogramnum);
168						}
169
170					} elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0D) { // Channel after-touch
171
172						$channelnumber = ord(substr($trackdata, $eventsoffset++, 1));
173
174					} elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0E) { // Pitch wheel change (2000H is normal or no change)
175
176						$changeLSB = ord(substr($trackdata, $eventsoffset++, 1));
177						$changeMSB = ord(substr($trackdata, $eventsoffset++, 1));
178						$pitchwheelchange = (($changeMSB & 0x7F) << 7) & ($changeLSB & 0x7F);
179
180					} elseif (($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0F) && ($MIDIevents[$tracknumber][$eventid]['channel'] == 0x0F)) {
181
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;
190								break;
191
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;
196								break;
197
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;
202								break;
203
204							case 0x03: // Text: track name
205								$text_trackname = substr($METAeventData, 0, $METAeventLength);
206								$thisfile_midi_raw['track'][$tracknumber]['name'] = $text_trackname;
207								break;
208
209							case 0x04: // Text: track instrument name
210								$text_instrument = substr($METAeventData, 0, $METAeventLength);
211								//$thisfile_midi_raw['events'][$tracknumber][$eventid]['instrument'] = $text_instrument;
212								break;
213
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'] = '';
219								}
220								$thisfile_midi['lyrics'] .= $text_lyrics."\n";
221								break;
222
223							case 0x06: // Text: marker
224								$text_marker = substr($METAeventData, 0, $METAeventLength);
225								//$thisfile_midi_raw['events'][$tracknumber][$eventid]['marker'] = $text_marker;
226								break;
227
228							case 0x07: // Text: cue point
229								$text_cuepoint = substr($METAeventData, 0, $METAeventLength);
230								//$thisfile_midi_raw['events'][$tracknumber][$eventid]['cuepoint'] = $text_cuepoint;
231								break;
232
233							case 0x2F: // End Of Track
234								//$thisfile_midi_raw['events'][$tracknumber][$eventid]['EOT'] = $CumulativeDeltaTime;
235								break;
236
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');
241									return false;
242								}
243								$thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat;
244								$CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60;
245								$MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat;
246								$TicksAtCurrentBPM = 0;
247								break;
248
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;
258								break;
259
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;
265								}
266
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');
273
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');
276								break;
277
278							case 0x7F: // Sequencer specific information
279								$custom_data = substr($METAeventData, 0, $METAeventLength);
280								break;
281
282							default:
283								$this->warning('Unhandled META Event Command: '.$METAeventCommand);
284								break;
285						}
286
287					} else {
288
289						$this->warning('Unhandled MIDI Event ID: '.$MIDIevents[$tracknumber][$eventid]['eventid'].' + Channel ID: '.$MIDIevents[$tracknumber][$eventid]['channel']);
290
291					}
292				}
293				if (($tracknumber > 0) || (count($trackdataarray) == 1)) {
294					$thisfile_midi['totalticks'] = max($thisfile_midi['totalticks'], $CumulativeDeltaTime);
295				}
296			}
297			$previoustickoffset      = null;
298			$prevmicrosecondsperbeat = null;
299
300			ksort($MicroSecondsPerQuarterNoteAfter);
301			foreach ($MicroSecondsPerQuarterNoteAfter as $tickoffset => $microsecondsperbeat) {
302				if (is_null($previoustickoffset)) {
303					$prevmicrosecondsperbeat = $microsecondsperbeat;
304					$previoustickoffset = $tickoffset;
305					continue;
306				}
307				if ($thisfile_midi['totalticks'] > $tickoffset) {
308
309					if ($thisfile_midi_raw['ticksperqnote'] == 0) {
310						$this->error('Corrupt MIDI file: ticksperqnote == zero');
311						return false;
312					}
313
314					$info['playtime_seconds'] += (($tickoffset - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
315
316					$prevmicrosecondsperbeat = $microsecondsperbeat;
317					$previoustickoffset = $tickoffset;
318				}
319			}
320			if ($thisfile_midi['totalticks'] > $previoustickoffset) {
321
322				if ($thisfile_midi_raw['ticksperqnote'] == 0) {
323					$this->error('Corrupt MIDI file: ticksperqnote == zero');
324					return false;
325				}
326
327				$info['playtime_seconds'] += (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000);
328
329			}
330		}
331
332
333		if (!empty($info['playtime_seconds'])) {
334			$info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
335		}
336
337		if (!empty($thisfile_midi['lyrics'])) {
338			$thisfile_midi['comments']['lyrics'][] = $thisfile_midi['lyrics'];
339		}
340
341		return true;
342	}
343
344	/**
345	 * @param int $instrumentid
346	 *
347	 * @return string
348	 */
349	public function GeneralMIDIinstrumentLookup($instrumentid) {
350
351		$begin = __LINE__;
352
353		/** This is not a comment!
354
355			0	Acoustic Grand
356			1	Bright Acoustic
357			2	Electric Grand
358			3	Honky-Tonk
359			4	Electric Piano 1
360			5	Electric Piano 2
361			6	Harpsichord
362			7	Clavier
363			8	Celesta
364			9	Glockenspiel
365			10	Music Box
366			11	Vibraphone
367			12	Marimba
368			13	Xylophone
369			14	Tubular Bells
370			15	Dulcimer
371			16	Drawbar Organ
372			17	Percussive Organ
373			18	Rock Organ
374			19	Church Organ
375			20	Reed Organ
376			21	Accordian
377			22	Harmonica
378			23	Tango Accordian
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)
384			29	Overdriven Guitar
385			30	Distortion Guitar
386			31	Guitar Harmonics
387			32	Acoustic Bass
388			33	Electric Bass (finger)
389			34	Electric Bass (pick)
390			35	Fretless Bass
391			36	Slap Bass 1
392			37	Slap Bass 2
393			38	Synth Bass 1
394			39	Synth Bass 2
395			40	Violin
396			41	Viola
397			42	Cello
398			43	Contrabass
399			44	Tremolo Strings
400			45	Pizzicato Strings
401			46	Orchestral Strings
402			47	Timpani
403			48	String Ensemble 1
404			49	String Ensemble 2
405			50	SynthStrings 1
406			51	SynthStrings 2
407			52	Choir Aahs
408			53	Voice Oohs
409			54	Synth Voice
410			55	Orchestra Hit
411			56	Trumpet
412			57	Trombone
413			58	Tuba
414			59	Muted Trumpet
415			60	French Horn
416			61	Brass Section
417			62	SynthBrass 1
418			63	SynthBrass 2
419			64	Soprano Sax
420			65	Alto Sax
421			66	Tenor Sax
422			67	Baritone Sax
423			68	Oboe
424			69	English Horn
425			70	Bassoon
426			71	Clarinet
427			72	Piccolo
428			73	Flute
429			74	Recorder
430			75	Pan Flute
431			76	Blown Bottle
432			77	Shakuhachi
433			78	Whistle
434			79	Ocarina
435			80	Lead 1 (square)
436			81	Lead 2 (sawtooth)
437			82	Lead 3 (calliope)
438			83	Lead 4 (chiff)
439			84	Lead 5 (charang)
440			85	Lead 6 (voice)
441			86	Lead 7 (fifths)
442			87	Lead 8 (bass + lead)
443			88	Pad 1 (new age)
444			89	Pad 2 (warm)
445			90	Pad 3 (polysynth)
446			91	Pad 4 (choir)
447			92	Pad 5 (bowed)
448			93	Pad 6 (metallic)
449			94	Pad 7 (halo)
450			95	Pad 8 (sweep)
451			96	FX 1 (rain)
452			97	FX 2 (soundtrack)
453			98	FX 3 (crystal)
454			99	FX 4 (atmosphere)
455			100	FX 5 (brightness)
456			101	FX 6 (goblins)
457			102	FX 7 (echoes)
458			103	FX 8 (sci-fi)
459			104	Sitar
460			105	Banjo
461			106	Shamisen
462			107	Koto
463			108	Kalimba
464			109	Bagpipe
465			110	Fiddle
466			111	Shanai
467			112	Tinkle Bell
468			113	Agogo
469			114	Steel Drums
470			115	Woodblock
471			116	Taiko Drum
472			117	Melodic Tom
473			118	Synth Drum
474			119	Reverse Cymbal
475			120	Guitar Fret Noise
476			121	Breath Noise
477			122	Seashore
478			123	Bird Tweet
479			124	Telephone Ring
480			125	Helicopter
481			126	Applause
482			127	Gunshot
483
484		*/
485
486		return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIinstrument');
487	}
488
489	/**
490	 * @param int $instrumentid
491	 *
492	 * @return string
493	 */
494	public function GeneralMIDIpercussionLookup($instrumentid) {
495
496		$begin = __LINE__;
497
498		/** This is not a comment!
499
500			35	Acoustic Bass Drum
501			36	Bass Drum 1
502			37	Side Stick
503			38	Acoustic Snare
504			39	Hand Clap
505			40	Electric Snare
506			41	Low Floor Tom
507			42	Closed Hi-Hat
508			43	High Floor Tom
509			44	Pedal Hi-Hat
510			45	Low Tom
511			46	Open Hi-Hat
512			47	Low-Mid Tom
513			48	Hi-Mid Tom
514			49	Crash Cymbal 1
515			50	High Tom
516			51	Ride Cymbal 1
517			52	Chinese Cymbal
518			53	Ride Bell
519			54	Tambourine
520			55	Splash Cymbal
521			56	Cowbell
522			57	Crash Cymbal 2
523			59	Ride Cymbal 2
524			60	Hi Bongo
525			61	Low Bongo
526			62	Mute Hi Conga
527			63	Open Hi Conga
528			64	Low Conga
529			65	High Timbale
530			66	Low Timbale
531			67	High Agogo
532			68	Low Agogo
533			69	Cabasa
534			70	Maracas
535			71	Short Whistle
536			72	Long Whistle
537			73	Short Guiro
538			74	Long Guiro
539			75	Claves
540			76	Hi Wood Block
541			77	Low Wood Block
542			78	Mute Cuica
543			79	Open Cuica
544			80	Mute Triangle
545			81	Open Triangle
546
547		*/
548
549		return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIpercussion');
550	}
551
552}
553