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 }