1 : <?php
2 :
3 : /**
4 : * \Midi\Parsing\FileParser
5 : *
6 : * @package Midi
7 : * @subpackage Parsing
8 : * @copyright © 2009 Tommy Montgomery <http://phpmidiparser.com/>
9 : * @since 1.0
10 : */
11 :
12 : namespace Midi\Parsing;
13 :
14 : use \Midi\FileHeader;
15 : use \Midi\Util\Util;
16 :
17 : /**
18 : * Class for parsing MIDI files
19 : *
20 : * @package Midi
21 : * @subpackage Parsing
22 : * @since 1.0
23 : */
24 1 : class FileParser extends Parser {
25 :
26 : /**
27 : * @var TrackParser
28 : */
29 : private $trackParser;
30 :
31 : /**
32 : * The number of tracks parsed thus far
33 : *
34 : * @var int
35 : */
36 : private $tracksParsed;
37 :
38 : /**
39 : * The number of tracks expected to be parsed
40 : *
41 : * @var int
42 : */
43 : private $tracksExpected;
44 :
45 : /**
46 : * Constructor
47 : *
48 : * The parse state is initialized to {@link ParseState::FILE_HEADER}.
49 : *
50 : * @since 1.0
51 : * @uses setState()
52 : *
53 : * @param TrackParser $trackParser
54 : */
55 : public function __construct(TrackParser $trackParser = null) {
56 15 : parent::__construct();
57 15 : $this->trackParser = ($trackParser === null) ? new TrackParser() : $trackParser;
58 15 : $this->tracksParsed = 0;
59 15 : $this->tracksExpected = 0;
60 :
61 15 : $this->setState(ParseState::FILE_HEADER);
62 15 : }
63 :
64 : /**
65 : * Gets the number of tracks that have been parsed so far
66 : *
67 : * @since 1.0
68 : *
69 : * @return int
70 : */
71 : public function getTracksParsed() {
72 1 : return $this->tracksParsed;
73 : }
74 :
75 : /**
76 : * Gets the number of tracks that should be in the MIDI file
77 : *
78 : * @since 1.0
79 : *
80 : * @return int
81 : */
82 : public function getTracksExpected() {
83 1 : return $this->tracksExpected;
84 : }
85 :
86 : /**
87 : * @since 1.0
88 : * @uses TrackParser::setBuffer()
89 : */
90 : protected function afterLoad() {
91 1 : $this->trackParser->setFile($this->file);
92 1 : }
93 :
94 : /**
95 : * Gets the raw binary file header
96 : *
97 : * @since 1.0
98 : * @uses read()
99 : * @uses FileHeader::LENGTH
100 : *
101 : * @return binary
102 : */
103 : protected function getRawFileHeader() {
104 1 : return $this->read(FileHeader::LENGTH, true);
105 : }
106 :
107 : /**
108 : * Parses the given file header
109 : *
110 : * @since 1.0
111 : * @uses FileHeader::LENGTH
112 : * @uses Util::unpack()
113 : *
114 : * @param binary $header
115 : * @throws InvalidArgumentException
116 : * @throws {@link ParseException} if the file header is not a valid MIDI file header
117 : * @return FileHeader
118 : */
119 : public function parseFileHeader($header) {
120 5 : if (strlen($header) !== FileHeader::LENGTH) {
121 1 : throw new \InvalidArgumentException('MIDI file header must be ' . FileHeader::LENGTH . ' bytes');
122 : }
123 :
124 4 : $id = Util::unpack(substr($header, 0, 4));
125 4 : $chunkSize = Util::unpack(substr($header, 4, 4));
126 4 : $format = Util::unpack(substr($header, 8, 2));
127 4 : $tracks = Util::unpack(substr($header, 10, 2));
128 4 : $timeDivision = Util::unpack(substr($header, 12, 2));
129 :
130 4 : if ($id !== array(0x4D, 0x54, 0x68, 0x64)) {
131 1 : throw new ParseException('Invalid file header, expected byte sequence [4D 54 68 64]');
132 : }
133 3 : if ($chunkSize !== array(0x00, 0x00, 0x00, 0x06)) {
134 1 : throw new ParseException('File header chunk size must be [00 00 00 06]');
135 : }
136 :
137 2 : $format = ($format[0] << 8) | $format[1];
138 2 : $timeDivision = ($timeDivision[0] << 8) | $timeDivision[1];
139 2 : $tracks = ($tracks[0] << 8) | $tracks[1];
140 :
141 2 : if ($format !== 0 && $format !== 1 && $format !== 2) {
142 1 : throw new ParseException('MIDI file format must be 0, 1 or 2 (got ' . $format . ')');
143 : }
144 :
145 1 : return new FileHeader($format, $tracks, $timeDivision);
146 : }
147 :
148 : /**
149 : * @since 1.0
150 : * @uses getState()
151 : * @uses parseFileHeader()
152 : * @uses getRawFileHeader()
153 : * @uses Chunk::getData()
154 : * @uses TrackParser::parse()
155 : * @uses TrackParser::getState()
156 : *
157 : * @throws {@link ParseException}
158 : * @return Chunk
159 : */
160 : public function parse() {
161 7 : $chunk = null;
162 7 : $state = $this->getState();
163 : switch ($state) {
164 7 : case ParseState::FILE_HEADER:
165 1 : $chunk = $this->parseFileHeader($this->getRawFileHeader());
166 1 : list(, $numTracks, ) = $chunk->getData();
167 1 : $this->tracksExpected = $numTracks;
168 1 : $this->setState(ParseState::TRACK_HEADER);
169 1 : break;
170 6 : case ParseState::TRACK_HEADER:
171 6 : case ParseState::EVENT:
172 6 : case ParseState::DELTA:
173 3 : $chunk = $this->trackParser->parse();
174 3 : $newState = $this->trackParser->getState();
175 :
176 3 : if ($newState === ParseState::TRACK_HEADER) {
177 1 : $this->tracksParsed++;
178 1 : if ($this->getTracksParsed() >= $this->getTracksExpected()) {
179 1 : $newState = ParseState::EOF;
180 1 : }
181 1 : }
182 :
183 3 : $this->setState($newState);
184 3 : break;
185 3 : case ParseState::EOF:
186 : //the pointer is not beyond the buffer length at this point
187 : //one more should push it over the edge
188 2 : $this->file->fgetc();
189 2 : if (!$this->file->eof()) {
190 : //$this->file->fseek(-1, SEEK_CUR);
191 1 : throw new ParseException('Expected EOF');
192 : }
193 1 : break;
194 1 : default:
195 1 : throw new StateException('Unknown parse state: ' . $state);
196 1 : }
197 :
198 5 : return $chunk;
199 : }
200 :
201 : }
202 :
|