1 /**
2 `mididi.reader` contains functions for reading the MIDI format.
3 
4 Currently, the reader is compatible with the standard MIDI format 1.0 and
5 forward compatible with newer formats (in which case newer features will be
6 ignored, as the specification demands).
7 
8 The implementation (and some of the documentation) is based on this specification:
9 https://www.cs.cmu.edu/~music/cmsip/readings/Standard-MIDI-file-format-updated.pdf
10 
11 Authors:
12     https://github.com/w2ptr
13 */
14 module mididi.reader;
15 
16 // Copyright Wout Huynen 2021.
17 // Distributed under the Boost Software License, Version 1.0.
18 // (See accompanying file LICENSE.txt or copy at
19 // https://www.boost.org/LICENSE_1_0.txt)
20 
21 import mididi.types;
22 import std.range.primitives : isInputRange, ElementType;
23 import std.stdio : File;
24 
25 /**
26 `readMIDIFile()` reads a MIDI file, of course.
27 
28 This essentially does the same as `MIDIReader!(ubyte[]).readMIDI()`, but is
29 given for convenience.
30 
31 It has two overloads, one for `std.stdio.File` objects, and one if you have the
32 file path only.
33 */
34 MIDI readMIDIFile()(File file) {
35     // TODO: could also do this byChunk() to potentially use less memory
36     auto bytes = file.rawRead(new ubyte[cast(size_t) file.size]);
37     auto reader = MIDIReader!(ubyte[])(bytes);
38     return reader.readMIDI();
39 }
40 /// ditto
41 MIDI readMIDIFile()(string path) {
42     return readMIDIFile(File(path));
43 }
44 
45 /**
46 `MIDIReader` is the raw MIDI reader object.
47 
48 This can be used to read the standard MIDI chunk-by-chunk or event-by-event.
49 
50 Constraints:
51     `T` must be an input range of `ubyte`
52 */
53 struct MIDIReader(T)
54 if (isInputRange!T && is(ElementType!T : const(ubyte))) {
55     import std.range.primitives : empty, front, popFront;
56 
57     ///
58     this(T range) {
59         _input = range;
60     }
61 
62     /**
63     Reads the entire input and returns the data in a `MIDI` object.
64 
65     Throws:
66         `Exception` if all chunks are read and the input is not empty yet;
67         `Exception` if the header chunk or any of the track chunks is invalid
68     */
69     MIDI readMIDI() {
70         import std.conv : text;
71 
72         auto headerChunk = readHeaderChunk();
73         auto trackChunks = new TrackChunk[headerChunk.nTracks];
74         foreach (i; 0 .. headerChunk.nTracks) {
75             trackChunks[i] = readTrackChunk();
76         }
77 
78         if (!isFinished()) { // file not empty yet
79             throw new Exception(text(
80                 "Expected ",
81                 headerChunk.nTracks,
82                 " track chunks from the header, but the file contains more",
83             ));
84         }
85 
86         return MIDI(headerChunk, trackChunks);
87     }
88 
89     /**
90     Reads a chunk from the input (meaning, it can either read a header chunk or
91     a track chunk).
92 
93     Throws:
94         `Exception` if the chunk is invalid
95     */
96     Chunk readChunk() {
97         const chunkType = cast(char[]) read(4);
98         if (chunkType == "MTrk") {
99             return Chunk(readHeaderChunk());
100         } else if (chunkType == "MThd") {
101             return Chunk(readTrackChunk());
102         } else {
103             throw new Exception("Invalid chunk type: " ~ chunkType.idup);
104         }
105     }
106 
107     /**
108     */
109     HeaderChunk readHeaderChunk() {
110         import mididi.def : TrackFormat;
111         import std.conv : text;
112 
113         auto chunkType = cast(const(char[])) read(4);
114         skip(4, "chunk type MThd");
115         if (chunkType != "MThd") {
116             throw new Exception("Not a header chunk: " ~ chunkType.idup);
117         }
118 
119         immutable length = readInt32();
120         if (length < 6) {
121             throw new Exception("Header chunk must have length 6");
122         }
123 
124         // see mididi.def.TrackFormat for the definitions
125         immutable format = readInt16();
126         if (format > TrackFormat.max) {
127             throw new Exception("Header chunk format must be between 0 and " ~
128                 TrackFormat.max.stringof);
129         }
130 
131         immutable tracks = readInt16();
132         if (format == 0 && tracks != 1) {
133             throw new Exception("If the header chunk's format is 0, the " ~
134                 "number of tracks given in the header chunk must be 1");
135         }
136 
137         // bits: 15 | 14-8                  | 7-0             |
138         //       ---+-----------------------+-----------------+
139         //       0  |        ticks-per-quarter-note           |
140         //       ---+-----------------------+-----------------+
141         //       1  | negative SMPTE format | ticks-per-frame |
142         immutable division = TimeDivision.fromRawValue(readInt16());
143         if (division.getFormat() == 1) {
144             immutable smpteFormat = division.getNegativeSMPTEFormat();
145             if (
146                 smpteFormat != -24 && smpteFormat != -25 &&
147                 smpteFormat != -29 && smpteFormat != -30
148             ) {
149                 throw new Exception(text(
150                     "If the division's format is 1, bits 14 through 8 must " ~
151                         "be -24, -25, -29 or -30 in 2's complement, not ",
152                     smpteFormat,
153                 ));
154             }
155         }
156 
157         skip(length - 6, "remaining header chunk bytes");
158 
159         return HeaderChunk(cast(TrackFormat) format, tracks, division);
160     }
161 
162     /**
163     */
164     TrackChunk readTrackChunk() {
165         import std.conv : text;
166 
167         auto chunkType = cast(const(char[])) read(4);
168         skip(4, "chunk type MTrk");
169         if (chunkType != "MTrk") {
170             throw new Exception("Not a track chunk: " ~ chunkType.idup);
171         }
172 
173         immutable length = readInt32();
174         immutable endIndex = _index + length;
175 
176         TrackEvent[] events;
177         ubyte previousStatus = 0;
178         bool inSysExPacket = false;
179         while (_index != endIndex) {
180             if (_index > endIndex) {
181                 throw new Exception(text(
182                     "Expected track events for ", length, " bytes, but event ",
183                     events.length, " stopped at byte ", _index + 1,
184                 ));
185             }
186 
187             auto event = readTrackEvent(previousStatus, inSysExPacket);
188             previousStatus = event.statusByte;
189             events ~= event;
190         }
191 
192         if (events.length == 0) {
193             throw new Exception("At least one track event must be present " ~
194                 "in a track chunk");
195         }
196 
197         if (inSysExPacket) {
198             throw new Exception("Unterminated system exclusive packet");
199         }
200 
201         return TrackChunk(events);
202     }
203 
204     // TODO: should probably be public?
205     private TrackEvent readTrackEvent(ubyte prevStatus, ref bool inSysExPacket) {
206         import mididi.def : getDataLength, isMIDIEvent, isMetaEvent,
207             isSysExEvent, MetaEventType, SystemMessageType;
208         import std.conv : text;
209 
210         // <MTrk event> = <delta-time><event>
211         // <event> = <MIDI event> | <sysex event> | <meta event>
212         
213         // <MIDI event> = [1tttnnnn] 0xxxxxxx 0yyyyyyy
214         // if the status byte starts with a 0, use the previous one.
215         // <sysex event> = F0 <length> <data> | F7 <length> <data>
216         // <meta event> = FF <type> <length> <data>
217 
218         immutable deltaTime = readVariableInt();
219         const nextByte = read(1);
220         if (nextByte.length != 1) {
221             throw new Exception("Input data ended at start of track event");
222         }
223 
224         ubyte statusByte;
225         if (((nextByte[0] >> 7) & 1) == 0) {
226             if (isMIDIEvent(prevStatus)) { // running status
227                 statusByte = prevStatus;
228             } else {
229                 throw new Exception(text(
230                     "Running status does not work for non-midi events, but " ~
231                     "the previous status byte is one: ", prevStatus,
232                 ));
233             }
234         } else {
235             skip(1, "status byte");
236             statusByte = nextByte[0];
237         }
238 
239         if (!isSysExEvent(statusByte) && inSysExPacket) { // invalid
240             throw new Exception(
241                 "No non-system-exclusive event may be emitted until the " ~
242                 "last system exclusive event",
243             );
244         }
245 
246         if (isMIDIEvent(statusByte)) {
247             immutable dataLength = getDataLength(statusByte);
248             auto varData = read(dataLength);
249             skip(dataLength, "midi event data bytes");
250             assert(varData.length == dataLength);
251             ubyte[2] data = [0, 0];
252             data[0 .. dataLength] = varData[];
253             return TrackEvent(
254                 deltaTime,
255                 MIDIEvent(statusByte, data),
256             );
257         } else if (isSysExEvent(statusByte)) {
258             immutable type = cast(SystemMessageType) statusByte;
259             immutable length = readVariableInt();
260             auto data = read(cast(size_t) length);
261             skip(cast(size_t) length, "system message data bytes");
262             assert(data.length == length);
263 
264             if (inSysExPacket && type == SystemMessageType.systemExclusive) {
265                 throw new Exception(
266                     "Cannot start a system exclusive packet while another " ~
267                     "one is not terminated yet",
268                 );
269             }
270 
271             if (type == SystemMessageType.systemExclusive) {
272                 // start of a packet
273                 inSysExPacket = true;
274             }
275             if (data.length > 0 && data[$ - 1] == 0xF7) {
276                 // end of a packet
277                 inSysExPacket = false;
278             }
279 
280             return TrackEvent(
281                 deltaTime,
282                 SysExEvent(type, data),
283             );
284         } else if (isMetaEvent(statusByte)) {
285             immutable metaEventType = cast(MetaEventType) readInt8();
286             immutable length = readVariableInt();
287             auto data = read(cast(size_t) length);
288             skip(cast(size_t) length, "meta event data bytes");
289             assert(data.length == length);
290 
291             return TrackEvent(
292                 deltaTime,
293                 MetaEvent(metaEventType, data),
294             );
295         } else {
296             throw new Exception(text(
297                 "Unrecognized status byte: ", statusByte,
298             ));
299         }
300     }
301 
302     /**
303     Returns:
304         whether the reader has no more content to read.
305     */
306     bool isFinished() const @nogc nothrow pure @safe {
307         return _peeked.length == 0;
308     }
309 
310 private:
311     int readVariableInt() {
312         auto bytes = read(4);
313         size_t byteCount = 0;
314         immutable result = .readVariableInt(bytes, byteCount);
315         if (byteCount == 0) {
316             throw new Exception("Expected variable integer");
317         }
318         skip(byteCount, "[impossible]");
319         return result;
320     }
321     uint readInt32() {
322         import std.bitmanip : bigEndianToNative;
323 
324         auto bytes = read(4);
325         skip(4, "32-bit integer");
326         assert(bytes.length == 4);
327         ubyte[4] b = bytes[0 .. 4];
328         return bigEndianToNative!uint(b);
329     }
330     ushort readInt16() {
331         import std.bitmanip : bigEndianToNative;
332 
333         auto bytes = read(2);
334         skip(2, "16-bit integer");
335         assert(bytes.length == 2);
336         ubyte[2] b = bytes[0 .. 2];
337         return bigEndianToNative!ushort(b);
338     }
339     ubyte readInt8() {
340         auto b = read(1);
341         skip(1, "8-bit integer");
342         return b[0];
343     }
344 
345     // consumes n bytes, throws if not possible
346     void skip(size_t n, string msg = "more bytes") {
347         if (_peeked.length < n) {
348             throw new Exception("Unexpected end of input, expected: " ~ msg);
349         }
350         _peeked = _peeked[n .. $];
351         _index += n;
352     }
353     // reads at most n bytes without consuming
354     ubyte[] read(size_t n)
355     out (result; result.length <= n) {
356         import std.algorithm.comparison : min;
357 
358         while (_peeked.length < n && !_input.empty) {
359             _peeked ~= _input.front;
360             _input.popFront();
361         }
362         return _peeked[0 .. min($, n)];
363     }
364 
365     size_t _index = 0;
366     T _input;
367     ubyte[] _peeked;
368 }
369 
370 /// Reads a complete file with a single track of a single event
371 unittest {
372     import mididi.def : MetaEventType, TrackFormat;
373 
374     auto reader = MIDIReader!(ubyte[])([
375         'M', 'T', 'h', 'd', // header chunk
376         0x00, 0x00, 0x00, 0x06, // length
377         0x00, 0x01, // format 1
378         0x00, 0x02, // 2 tracks
379         0b0_0000000, 0x60, // time division: 0, 0, 0x60
380 
381         'M', 'T', 'r', 'k', // track chunk 1
382         0, 0, 0, 4, // length = 4
383         0x00, 0xFF, 0x2F, 0x00, // end of track
384 
385         'M', 'T', 'r', 'k', // track chunk 2
386         0, 0, 0, 4, // length = 4
387         0x00, 0xFF, 0x2F, 0x00, // end of track
388     ]);
389     auto midi = reader.readMIDI();
390 
391     assert(midi.headerChunk.trackFormat == TrackFormat.simultaneous);
392     assert(midi.headerChunk.nTracks == 2);
393     assert(midi.headerChunk.division.getFormat() == 0);
394     assert(midi.headerChunk.division.getTicksPerQuarterNote() == 0x0060);
395 
396     assert(midi.trackChunks.length == 2);
397     assert(midi.trackChunks[0].events.length == 1);
398     const event = midi.trackChunks[0].events[0];
399     assert(event.asMetaEvent().type == MetaEventType.endOfTrack);
400 }
401 
402 /// Reads a file that has bytes past the last chunk, which is disallowed.
403 unittest {
404     auto reader = MIDIReader!(ubyte[])([
405         'M', 'T', 'h', 'd',
406         0x00, 0x00, 0x00, 0x06, // length
407         0x00, 0x00, // format: 0
408         0x00, 0x01, // nTracks: 1
409         0b0_0000000, 0x60, // time division: 0, 0, 0x60
410 
411         'M', 'T', 'r', 'k',
412         0, 0, 0, 4,
413         0x00, 0xFF, 0x2F, 0x00,
414 
415         'M', 'T', 'r', 'k', // too many chunks
416         0, 0, 0, 4,
417         0x00, 0xFF, 0x2F, 0x00,
418     ]);
419 
420     try {
421         const _ = reader.readMIDI();
422         assert(false, "File is invalid, but no exception was thrown");
423     } catch (Exception e) {}
424 }
425 
426 private:
427 
428 // This test checks if the reader works for a file with multiple tracks
429 // (example taken from paper, see module comment).
430 unittest {
431     import mididi.def : ChannelMessageType, MetaEventType, TrackFormat;
432 
433     auto reader = MIDIReader!(ubyte[])([
434         0x4D, 0x54, 0x68, 0x64, // MThd
435         0x00, 0x00, 0x00, 0x06, // chunk length
436         0x00, 0x01, // format 1
437         0x00, 0x04, // four tracks
438         0x00, 0x60, // 96 per quarter note
439 
440         // First, the track chunk for the time signature/tempo track. Its
441         // header, followed by the events:
442         0x4D, 0x54, 0x72, 0x6B, // MTrk
443         0x00, 0x00, 0x00, 0x14, // chunk length (20)
444         // Delta-Time Event Comments
445         0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08, // time signature
446         0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20, // tempo
447         0x83, 0x00, 0xFF, 0x2F, 0x00, // end of track
448 
449         // Then, the track chunk for the first music track. The MIDI convention
450         // for note on/off running status is used in this example:
451         0x4D, 0x54, 0x72, 0x6B, // MTrk
452         0x00, 0x00, 0x00, 0x10, // chunk length (16)
453         // Delta-Time Event Comments
454         0x00, 0xC0, 0x05,
455         0x81, 0x40, 0x90, 0x4C, 0x20,
456         0x81, 0x40, 0x4C, 0x00, // Running status: note on, vel=0
457         0x00, 0xFF, 0x2F, 0x00, // end of track
458 
459         // Then, the track chunk for the second music track:
460         0x4D, 0x54, 0x72, 0x6B, // MTrk
461         0x00, 0x00, 0x00, 0x0F, // chunk length (15)
462         // Delta-Time Event Comments
463         0x00, 0xC1, 0x2E,
464         0x60, 0x91, 0x43, 0x40,
465         0x82, 0x20, 0x43, 0x00, // running status
466         0x00, 0xFF, 0x2F, 0x00, // end of track
467 
468         // Then, the track chunk for the third music track:
469         0x4D, 0x54, 0x72, 0x6B, // MTrk
470         0x00, 0x00, 0x00, 0x15, // chunk length (21)
471         // Delta-Time Event Comments
472         0x00, 0xC2, 0x46,
473         0x00, 0x92, 0x30, 0x60,
474         0x00, 0x3C, 0x60, // running status
475         0x83, 0x00, 0x30, 0x00, // two-byte delta-time, running status
476         0x00, 0x3C, 0x00, // running status
477         0x00, 0xFF, 0x2F, 0x00, // end of track
478     ]);
479     auto midi = reader.readMIDI();
480 
481     const headerChunk = midi.headerChunk;
482     assert(headerChunk.nTracks == 4);
483     assert(headerChunk.trackFormat == TrackFormat.simultaneous);
484     assert(headerChunk.division.getFormat() == 0);
485     assert(midi.trackChunks.length == headerChunk.nTracks);
486 
487     const track1 = midi.trackChunks[0];
488     assert(track1.events.length == 3);
489     assert(track1.events[2].asMetaEvent() !is null);
490     assert(track1.events[2].asMetaEvent().type == MetaEventType.endOfTrack);
491 
492     const track2 = midi.trackChunks[1];
493     assert(track2.events.length == 4);
494 
495     const track3 = midi.trackChunks[2];
496     assert(track3.events.length == 4);
497 
498     const track4 = midi.trackChunks[3];
499     assert(track4.events.length == 6);
500 
501     const event43 = track4.events[2].asMIDIEvent();
502     assert(event43 !is null);
503     assert(event43.isChannelMessage());
504     assert(event43.getChannelMessageType() == ChannelMessageType.noteOn);
505 
506     assert(track4.events[3].deltaTime == 384);
507     const event44 = track4.events[3].asMIDIEvent();
508     assert(event44 !is null);
509     assert(event44.isChannelMessage());
510     assert(event44.getChannelMessageType() == ChannelMessageType.noteOn);
511 }
512 
513 // This test checks if the header chunk parser works properly.
514 unittest {
515     import mididi.def : TrackFormat;
516 
517     static MIDIReader!(ubyte[]) create(ubyte[] info) {
518         ubyte[8] prefix = [
519             'M', 'T', 'h', 'd',
520             0x00, 0x00, 0x00, 0x06, // length 6 (as always)
521         ];
522         return MIDIReader!(ubyte[])(prefix ~ info);
523     }
524 
525     auto r1 = create([
526         0x00, 0x00, // format 0
527         0x00, 0x01, // 1 track
528         0x00, 0x60, // division: 0, 0, 0x60
529     ]);
530     auto h1 = r1.readHeaderChunk();
531     assert(h1.trackFormat == TrackFormat.single);
532     assert(h1.nTracks == 1);
533     assert(h1.division.getFormat() == 0);
534     assert(h1.division.rawValue == 0x0060);
535     assert(h1.division.getTicksPerQuarterNote() == 0x60);
536 
537     auto r2 = create([
538         0x00, 0x01, // format 1
539         0xFF, 0xFF, // 65536 tracks
540         0b1_1100111, 40, // division: 1, -25, 40
541     ]);
542     auto h2 = r2.readHeaderChunk();
543     assert(h2.trackFormat == TrackFormat.simultaneous);
544     assert(h2.nTracks == ushort.max);
545     assert(h2.division.getFormat() == 1);
546     assert(h2.division.getNegativeSMPTEFormat() == -25);
547     assert(h2.division.getTicksPerFrame() == 40);
548 }
549 
550 // This test checks if the header chunk parser errors correctly on invalid
551 // cases.
552 unittest {
553     static void test(E = Exception)(ubyte[] bytes) {
554         try {
555             auto reader = MIDIReader!(ubyte[])(bytes);
556             cast(void) reader.readHeaderChunk();
557             assert(false, "Invalid header chunk but no exception thrown");
558         } catch (E e) {}
559     }
560 
561     // incomplete
562     test([]);
563     test(['M']);
564     test(['M', 'T', 'h', 'd']);
565     test(['M', 'T', 'h', 'd', 0x50]);
566     test(['M', 'T', 'h', 'd', 0x00, 0x06]);
567 
568     // invalid length
569     test(['M', 'T', 'h', 'd', 0x00, 0x05]);
570 
571     // invalid track format or number of tracks
572     test([
573         'M', 'T', 'h', 'd',
574         0x00, 0x00, 0x00, 0x06, // length
575         0x00, 0x03, // format
576         0x00, 0x01, // tracks
577         0x00, 0x60, // time division
578     ]);
579     test([
580         'M', 'T', 'h', 'd',
581         0x00, 0x00, 0x00, 0x06,
582         0x02, 0x02,
583         0x00, 0x01,
584         0x00, 0x60,
585     ]);
586     test([
587         'M', 'T', 'h', 'd',
588         0x00, 0x00, 0x00, 0x06,
589         0x00, 0x00,
590         0x00, 0x02,
591         0x00, 0x60,
592     ]);
593 
594     // invalid time division
595     test([
596         'M', 'T', 'h', 'd',
597         0x00, 0x00, 0x00, 0x06,
598         0x00, 0x01,
599         0xFF, 0xFF,
600         0b1_1100110, 40,
601     ]);
602 }
603 
604 // This test checks if the track chunk parser works properly (example taken
605 // from paper, see module comment).
606 unittest {
607     import mididi.def : ChannelMessageType, MetaEventType;
608 
609     auto reader = MIDIReader!(ubyte[])([
610         'M', 'T', 'r', 'k', // chunk type
611         0x00, 0x00, 0x00, 0x3B, // length (59)
612 
613         0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08, // time signature (0)
614         0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20, // tempo
615         0x00, 0xC0, 0x05,
616         0x00, 0xC1, 0x2E,
617         0x00, 0xC2, 0x46, // (4)
618         0x00, 0x92, 0x30, 0x60, // (5)
619         0x40, 0x3C, 0x60, // running status (6)
620         0x60, 0x91, 0x43, 0x40,
621         0x60, 0x90, 0x4C, 0x20,
622         0x81, 0x40, 0x82, 0x30, 0x40, // two-byte delta-time (9)
623         0x00, 0x3C, 0x40, // running status
624         0x00, 0x81, 0x43, 0x40,
625         0x00, 0x80, 0x4C, 0x40,
626         0x00, 0xFF, 0x2F, 0x00, // end of track
627     ]);
628     auto track = reader.readTrackChunk();
629 
630     assert(reader.isFinished());
631     assert(track.events.length == 14);
632 
633     const event1 = track.events[0];
634     assert(event1.deltaTime == 0x00);
635     assert(event1.asMIDIEvent() is null);
636     assert(event1.asSysExEvent() is null);
637     const metaEvent1 = event1.asMetaEvent();
638     assert(metaEvent1 !is null);
639     assert(metaEvent1.type == MetaEventType.timeSignature);
640     assert(metaEvent1.data == [0x04, 0x02, 0x18, 0x08]);
641     
642     const event2 = track.events[4];
643     assert(event2.deltaTime == 0x00);
644     const midiEvent2 = event2.asMIDIEvent();
645     assert(midiEvent2 !is null);
646     assert(midiEvent2.getChannelMessageType() == ChannelMessageType.programChange);
647     assert(midiEvent2.getChannelNumber() == 2);
648 
649     const event3 = track.events[5];
650     assert(event3.deltaTime == 0x00);
651     const midiEvent3 = event3.asMIDIEvent();
652     assert(midiEvent3 !is null);
653     assert(midiEvent3.getChannelMessageType() == ChannelMessageType.noteOn);
654     assert(midiEvent3.getChannelNumber() == 2);
655 
656     const event4 = track.events[6];
657     assert(event4.deltaTime == 0x40);
658     const midiEvent4 = event4.asMIDIEvent();
659     assert(midiEvent4 !is null);
660 
661     assert(midiEvent4.getChannelMessageType() == midiEvent3.getChannelMessageType());
662     assert(midiEvent4.getChannelNumber() == midiEvent3.getChannelNumber());
663 
664     const event5 = track.events[9];
665     assert(event5.deltaTime == 192);
666 }
667 
668 // This test checks system exclusive messages.
669 // See the test below for invalid system exclusive messages.
670 unittest {
671     static void testNoErrors(ubyte[] bytes) {
672         ubyte[4] start = [
673             'M', 'T', 'r', 'k'
674         ];
675         auto reader = MIDIReader!(ubyte[])(start ~ bytes);
676         cast(void) reader.readTrackChunk();
677         assert(reader.isFinished());
678     }
679 
680     testNoErrors([
681         0, 0, 0, 13, // length
682         0x00, 0xF0, 0x01, 0xFF,
683         0x00, 0xF7, 0x02, 0xFF, 0xF7,
684         0x00, 0xFF, 0x2F, 0x00, // end of track
685     ]);
686     testNoErrors([
687         0, 0, 0, 8,
688         0x00, 0xF0, 0x01, 0xF7,
689         0x00, 0xFF, 0x2F, 0x00,
690     ]);
691     
692     // system messages
693     testNoErrors([
694         0, 0, 0, 6,
695         0x00, 0xFA,
696         0x00, 0xFF, 0x2F, 0x00, // end of track
697     ]);
698 }
699 
700 // This test checks whether the functions correctly differentiate between
701 // control change messages and channel mode messages.
702 // (Arguably, this test could also be in `mididi.def`.)
703 unittest {
704     import mididi.def : isChannelModeMessage, isChannelVoiceMessage;
705 
706     auto reader = MIDIReader!(ubyte[])([
707         'M', 'T', 'r', 'k',
708         0, 0, 0, 12,
709         0x00, 0xB4, 0x77, 0x7F, // channel voice message
710         0x00, 0xB4, 0x78, 0x00, // channel mode message
711         0x00, 0xFF, 0x2F, 0x00,
712     ]);
713     auto track = reader.readTrackChunk();
714 
715     const midiEvent1 = track.events[0].asMIDIEvent();
716     assert(midiEvent1.isChannelMessage());
717     assert(isChannelVoiceMessage(midiEvent1.statusByte, midiEvent1.data));
718     assert(!isChannelModeMessage(midiEvent1.statusByte, midiEvent1.data));
719     assert(midiEvent1.getChannelNumber() == 4);
720 
721     const midiEvent2 = track.events[1].asMIDIEvent();
722     assert(midiEvent2.isChannelMessage());
723     assert(!isChannelVoiceMessage(midiEvent2.statusByte, midiEvent2.data));
724     assert(isChannelModeMessage(midiEvent2.statusByte, midiEvent2.data));
725     assert(midiEvent2.getChannelNumber() == 4);
726 }
727 
728 // This test checks if the track chunk parser errors correctly on invalid
729 // cases when parsing individual track chunks.
730 unittest {
731     static void test(E = Exception)(ubyte[] bytes) {
732         auto reader = MIDIReader!(ubyte[])(bytes);
733 
734         try {
735             cast(void) reader.readTrackChunk();
736             assert(false, "Invalid track chunk but no exception thrown");
737         } catch (Exception e) {}
738     }
739 
740     // incomplete
741     test([]);
742     test(['M', 'T', 'r', 'k']);
743     test(['M', 'T', 'r', 'k', 0x00]);
744 
745     // there must be >= 1 event
746     test(['M', 'T', 'r', 'k', 0, 0, 0, 0]);
747 
748     // fallthrough only works for midi events, not for other events
749     test([
750         'M', 'T', 'r', 'k',
751         0, 0, 0, 5,
752         0xFF, 0x01, 0x00, // e.g. for a meta event
753         0x00, 0x00,
754     ]);
755 
756     // fail on invalid sysex messages
757     test([
758         'M', 'T', 'r', 'k',
759         0, 0, 0, 13,
760         0x00, 0xF0, 0x01, 0xFF,
761         0x00, 0xF7, 0x02, 0xFF, 0xFF,
762         0x00, 0xFF, 0x2F, 0x00, // end of track
763     ]);
764     test([
765         'M', 'T', 'r', 'k',
766         0, 0, 0, 4,
767         0x00, 0xF0, 0x01, 0xFF,
768     ]);
769     test([
770         'M', 'T', 'r', 'k',
771         0, 0, 0, 12,
772         0x00, 0xF0, 0x01, 0xFF,
773         0x00, 0xF0, 0x01, 0xF7,
774         0x00, 0xFF, 0x2F, 0x00, // end of track
775     ]);
776 
777     // wrong length for chunk (too long)
778     test([
779         'M', 'T', 'r', 'k',
780         0x00, 0x00, 0x00, 20, // (1 too long)
781         0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08,
782         0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20,
783         0x00, 0xFF, 0x2F, 0x00, // end of track
784     ]);
785 
786     // wrong length for certain messages
787     test([
788         'M', 'T', 'r', 'k',
789         0x00, 0x00, 0x00, 0x04,
790         0x00, 0xF2, 0x00, // "song position pointer"
791     ]);
792 }
793 
794 /*
795 Returns the variable that was encoded in `bytes`.
796 
797 If the variable-length int was invalid, then byteCount = 0.
798 */
799 int readVariableInt(const ubyte[] bytes, out size_t byteCount)
800 out (result; result >= 0 && result <= 0x0FFFFFFF)
801 out (result; byteCount <= 4) {
802     import std.algorithm.comparison : min;
803 
804     // read at most 4 bytes of data, until the most significant bit (bit 7)
805     // of the current byte is unset (=0)
806     int result = 0;
807     size_t i = 0;
808     immutable count = min(4, bytes.length);
809     while (i < count) {
810         immutable b = cast(int) bytes[i];
811         i++;
812         result <<= 7;
813         result += b & 0x7F;
814         if ((b & 0x80) == 0) { // 7th bit == 0
815             byteCount = i;
816             return result;
817         }
818     }
819 
820     // not valid: int too large or bytes's length too small
821     byteCount = 0;
822     return 0;
823 }
824 
825 // This test checks valid variable-length integer decoding.
826 unittest {
827     size_t byteCount = 0;
828 
829     immutable x1 = readVariableInt([0x00], byteCount);
830     assert(x1 == 0x00 && byteCount == 1);
831 
832     immutable x2 = readVariableInt([0x40], byteCount);
833     assert(x2 == 0x40 && byteCount == 1);
834 
835     immutable x3 = readVariableInt([0x7F, 0x5E], byteCount);
836     assert(x3 == 0x7F && byteCount == 1);
837 
838     immutable x4 = readVariableInt([0x81, 0x00], byteCount);
839     assert(x4 == 0x80 && byteCount == 2);
840 
841     immutable x5 = readVariableInt([0xC0, 0x00, 0x80], byteCount);
842     assert(x5 == 0x2000 && byteCount == 2);
843 
844     immutable x6 = readVariableInt([0xFF, 0x7F], byteCount);
845     assert(x6 == 0x3FFF && byteCount == 2);
846 
847     immutable x7 = readVariableInt([0x81, 0x80, 0x00], byteCount);
848     assert(x7 == 0x4000 && byteCount == 3);
849 
850     immutable x8 = readVariableInt([0xC0, 0x80, 0x00, 0xFF, 0x00, 0xFF], byteCount);
851     assert(x8 == 0x100000 && byteCount == 3);
852 
853     immutable x9 = readVariableInt([0xFF, 0xFF, 0x7F, 0x00], byteCount);
854     assert(x9 == 0x1FFFFF && byteCount == 3);
855 
856     immutable x10 = readVariableInt([0x81, 0x80, 0x80, 0x00, 0x80], byteCount);
857     assert(x10 == 0x200000 && byteCount == 4);
858 
859     immutable x11 = readVariableInt([0xC0, 0x80, 0x80, 0x00], byteCount);
860     assert(x11 == 0x8000000 && byteCount == 4);
861 
862     immutable x12 = readVariableInt([0xFF, 0xFF, 0xFF, 0x7F, 0x00], byteCount);
863     assert(x12 == 0xFFFFFFF && byteCount == 4);
864 }
865 
866 // This test checks invalid variable-length integer decoding.
867 unittest {
868     size_t byteCount = 0;
869 
870     // too long
871     readVariableInt([0xFF, 0xFF, 0xFF, 0xFF, 0x00], byteCount);
872     assert(byteCount == 0);
873     readVariableInt([0xFF, 0xFF, 0xFF, 0xFF], byteCount);
874     assert(byteCount == 0);
875 
876     // too short (keeps going)
877     readVariableInt([], byteCount);
878     assert(byteCount == 0);
879     readVariableInt([0x80], byteCount);
880     assert(byteCount == 0);
881     readVariableInt([0xCF, 0xFF], byteCount);
882     assert(byteCount == 0);
883     readVariableInt([0xFF, 0xFF, 0xFF], byteCount);
884     assert(byteCount == 0);
885 }