1 : <?php
2 :
3 : /**
4 : * \Midi\Parsing\TrackParser
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\Event;
15 : use Midi\TrackHeader;
16 : use Midi\Util\Util;
17 :
18 : /**
19 : * Class for parsing MIDI tracks
20 : *
21 : * The parse state is initialized to {@link ParseState::TRACK_HEADER}.
22 : *
23 : * @package Midi
24 : * @subpackage Parsing
25 : * @since 1.0
26 : */
27 1 : class TrackParser extends Parser {
28 :
29 : /**
30 : * @var EventParser
31 : */
32 : private $eventParser;
33 :
34 : /**
35 : * @var DeltaParser
36 : */
37 : private $deltaParser;
38 :
39 : /**
40 : * The expected length of the track in bytes
41 : *
42 : * @var int
43 : */
44 : private $expectedTrackLength;
45 :
46 : /**
47 : * The number of bytes that have been parsed so far
48 : *
49 : * @var int
50 : */
51 : private $parsedTrackLength;
52 :
53 : /**
54 : * Constructor
55 : *
56 : * @since 1.0
57 : * @uses setState()
58 : *
59 : * @param EventParser $eventParser
60 : * @param DeltaParser $deltaParser
61 : */
62 : public function __construct(EventParser $eventParser = null, DeltaParser $deltaParser = null) {
63 28 : $this->eventParser = ($eventParser === null) ? new EventParser() : $eventParser;
64 28 : $this->deltaParser = ($deltaParser === null) ? new DeltaParser() : $deltaParser;
65 28 : $this->expectedTrackLength = 0;
66 28 : $this->parsedTrackLength = 0;
67 :
68 28 : $this->setState(ParseState::TRACK_HEADER);
69 28 : }
70 :
71 : /**
72 : * Gets the expected length of the track in bytes
73 : *
74 : * @since 1.0
75 : *
76 : * @return int
77 : */
78 : public function getExpectedTrackLength() {
79 1 : return $this->expectedTrackLength;
80 : }
81 :
82 : /**
83 : * Gets the total number of bytes that have already been
84 : * parsed in the track
85 : *
86 : * @since 1.0
87 : *
88 : * @return int
89 : */
90 : public function getParsedTrackLength() {
91 1 : return $this->parsedTrackLength;
92 : }
93 :
94 : /**
95 : * Gets the raw binary track header
96 : *
97 : * @since 1.0
98 : * @uses read()
99 : * @uses TrackHeader::LENGTH
100 : *
101 : * @return binary
102 : */
103 : protected function getRawTrackHeader() {
104 1 : return $this->read(TrackHeader::LENGTH, true);
105 : }
106 :
107 : /**
108 : * Parses the given track header
109 : *
110 : * @since 1.0
111 : * @uses TrackHeader::LENGTH
112 : * @uses Util::unpack()
113 : *
114 : * @param binary $header
115 : * @throws InvalidArgumentException
116 : * @throws {@link ParseException} if the header is invalid
117 : * @return TrackHeader
118 : */
119 : public function parseTrackHeader($header) {
120 3 : if (strlen($header) !== TrackHeader::LENGTH) {
121 1 : throw new \InvalidArgumentException('Track header must be ' . \Midi\TrackHeader::LENGTH . ' bytes');
122 : }
123 :
124 2 : $id = Util::unpack(substr($header, 0, 4));
125 2 : $size = array_reverse(Util::unpack(substr($header, 4)));
126 :
127 2 : if ($id !== array(0x4D, 0x54, 0x72, 0x6B)) {
128 1 : throw new ParseException('Invalid track header, expected [4D 54 72 6B]');
129 : }
130 :
131 1 : $shift = 0;
132 1 : $trackSize = 0;
133 1 : foreach ($size as $byte) {
134 1 : $trackSize |= ($byte << $shift);
135 1 : $shift += 8;
136 1 : }
137 :
138 1 : return new TrackHeader($trackSize);
139 : }
140 :
141 : /**
142 : * @since 1.0
143 : * @uses EventParser::setFile()
144 : * @uses DeltaParser::setFile()
145 : */
146 : protected function afterSetFile() {
147 1 : $this->eventParser->setFile($this->file);
148 1 : $this->deltaParser->setFile($this->file);
149 1 : }
150 :
151 : /**
152 : * @since 1.0
153 : * @uses parseTrackHeader()
154 : * @uses getRawTrackHeader()
155 : * @uses TrackHeader::getSize()
156 : * @uses DeltaParser::parse()
157 : * @uses Chunk::getLength()
158 : * @uses EventParser::parse()
159 : * @uses checkTrackLength()
160 : *
161 : * @throws {@link ParseException}
162 : * @throws {@link StateException}
163 : * @return Chunk
164 : */
165 : public function parse() {
166 7 : $state = $this->getState();
167 7 : $chunk = null;
168 : switch ($state) {
169 7 : case ParseState::TRACK_HEADER:
170 1 : $chunk = $this->parseTrackHeader($this->getRawTrackHeader());
171 1 : $this->parsedTrackLength = 0;
172 1 : $this->expectedTrackLength = $chunk->getSize();
173 1 : $this->setState(ParseState::DELTA);
174 1 : break;
175 6 : case ParseState::DELTA:
176 1 : $chunk = $this->deltaParser->parse();
177 1 : $this->setState(ParseState::EVENT);
178 1 : $this->checkTrackLength($chunk->getLength());
179 1 : break;
180 5 : case ParseState::EVENT:
181 4 : $chunk = $this->eventParser->parse();
182 4 : $this->checkTrackLength($chunk->getLength());
183 3 : if ($this->getParsedTrackLength() === $this->getExpectedTrackLength()) {
184 2 : if (!($chunk instanceof Event\EndOfTrackEvent)) {
185 1 : throw new ParseException('Expected end of track');
186 : } else {
187 1 : $this->setState(ParseState::TRACK_HEADER);
188 : }
189 1 : } else {
190 1 : $this->setState(ParseState::DELTA);
191 : }
192 2 : break;
193 1 : default:
194 1 : throw new StateException('Invalid state: ' . $state);
195 1 : }
196 :
197 4 : return $chunk;
198 : }
199 :
200 : /**
201 : * Verifies that the track length does not exceed its
202 : * expected length
203 : *
204 : * @since 1.0
205 : * @todo The parsed track length increment shouldn't happen here
206 : *
207 : * @param int $length The number of bytes to add to the parsed track bytes
208 : * @throws {@link ParseException} if the track length exceeds its expected length
209 : */
210 : protected function checkTrackLength($length) {
211 2 : $this->parsedTrackLength += $length;
212 2 : $expectedLength = $this->getExpectedTrackLength();
213 2 : $parsedLength = $this->getParsedTrackLength();
214 2 : if ($parsedLength > $expectedLength) {
215 1 : throw new ParseException('Track data exceeds expected length (' . $parsedLength . '/' . $expectedLength . ')');
216 : }
217 1 : }
218 :
219 : }
220 :
|