1 /** 2 3 4 The implementation (and some of the documentation) is based on this specification: 5 https://www.cs.cmu.edu/~music/cmsip/readings/Standard-MIDI-file-format-updated.pdf 6 7 Authors: 8 https://github.com/w2ptr 9 */ 10 module mididi.types; 11 12 // Copyright Wout Huynen 2021. 13 // Distributed under the Boost Software License, Version 1.0. 14 // (See accompanying file LICENSE.txt or copy at 15 // https://www.boost.org/LICENSE_1_0.txt) 16 17 import mididi.def; 18 19 /** 20 `MIDI` represents the data of a complete MIDI file. 21 */ 22 struct MIDI { 23 /// 24 HeaderChunk headerChunk; 25 26 /// 27 TrackChunk[] trackChunks; 28 } 29 30 /** 31 A more general chunk type, which is essentially a tagged union of `HeaderChunk` 32 and `TrackChunk`. 33 34 Use `asHeaderChunk()` or `asTrackChunk()` to find out which one this is. 35 */ 36 struct Chunk { 37 /// 38 this(HeaderChunk chunk) @nogc nothrow pure @trusted { 39 _type = Type.headerChunk; 40 _headerChunk = chunk; 41 } 42 /// ditto 43 this(TrackChunk chunk) @nogc nothrow pure @trusted { 44 _type = Type.trackChunk; 45 _trackChunk = chunk; 46 } 47 48 /** 49 Returns: 50 a pointer to the header chunk or `null` if this chunk is not a header 51 chunk 52 */ 53 inout(HeaderChunk)* asHeaderChunk() inout return @nogc nothrow pure @safe { 54 if (_type == Type.headerChunk) { 55 return (() inout @trusted => &_headerChunk)(); 56 } 57 return null; 58 } 59 60 /** 61 Returns: 62 a pointer to the track chunk or `null` if this chunk is not a track 63 chunk 64 */ 65 inout(TrackChunk)* asTrackChunk() inout return @nogc nothrow pure @safe { 66 if (_type == Type.trackChunk) { 67 return (() inout @trusted => &_trackChunk)(); 68 } 69 return null; 70 } 71 72 private: 73 enum Type { 74 headerChunk, 75 trackChunk, 76 } 77 78 Type _type; 79 union { 80 HeaderChunk _headerChunk; 81 TrackChunk _trackChunk; 82 } 83 } 84 85 /** 86 `HeaderChunk` is the first chunk in a MIDI file. It gives information about the 87 format of the other chunks. 88 */ 89 struct HeaderChunk { 90 /** 91 The track format (the possible values are given in the enum 92 `mididi.def.TrackFormat`). 93 94 Cast this to `ushort` to get the raw value, which should be either 0, 1 or 95 2. 96 */ 97 TrackFormat trackFormat; 98 99 /** 100 The number of tracks in the data. 101 102 `nTracks` should always be 1 for if `trackFormat` is `TrackFormat.single`. 103 */ 104 ushort nTracks; 105 106 /** 107 */ 108 TimeDivision division; 109 } 110 111 /** 112 */ 113 struct TimeDivision { 114 /** 115 `fromRawValue` creates a `TimeDivision` object from the raw encoding of 116 this property in the header chunk. 117 */ 118 static TimeDivision fromRawValue(ushort rawValue) @nogc nothrow pure @safe { 119 return TimeDivision(rawValue); 120 } 121 /** 122 */ 123 static TimeDivision fromFormat0(ushort ticksPerQuarterNote) @nogc nothrow pure @safe 124 in (ticksPerQuarterNote <= 0x7FFF) { 125 return TimeDivision(ticksPerQuarterNote); 126 } 127 /** 128 */ 129 static TimeDivision fromFormat1(byte negativeSMPTEFormat, ubyte ticksPerFrame) @nogc nothrow pure @safe 130 in ( 131 negativeSMPTEFormat == -24 || negativeSMPTEFormat == -25 || 132 negativeSMPTEFormat == -29 || negativeSMPTEFormat == -30 133 ) { 134 immutable byte2 = (() @trusted => *cast(ubyte*) &negativeSMPTEFormat)(); 135 return TimeDivision( 136 (1 << 15) | (byte2 << 8) | ticksPerFrame, 137 ); 138 } 139 140 /// (read-only) 141 @property ushort rawValue() const @nogc nothrow pure @safe { 142 return _rawValue; 143 } 144 145 /** 146 Returns the time division's format. If the format is `0`, then use 147 `getTicksPerQuarterNote()`. If it is `1`, use `getNegativeSMPTEFormat()` 148 and `getTicksPerFrame()`. 149 150 Returns: 151 `0` if the division's format is how many ticks make up a quarter note 152 (bit 15 is 0) or `1` if it is ticks-per-frame (bit 15 is 1) 153 */ 154 int getFormat() const @nogc nothrow pure @safe 155 out (result; result == 0 || result == 1) { 156 return (_rawValue >> 15) & 1; 157 } 158 159 /** 160 Preconditions: 161 `getFormat()` must be `0` 162 */ 163 ushort getTicksPerQuarterNote() const @nogc nothrow pure @safe 164 in (this.getFormat() == 0) { 165 // get bits 14 through 0 166 return _rawValue; 167 } 168 169 /** 170 Returns: 171 one of the four standard SMPTE formats (-24, -25, -29, -30), 172 representing the negative of the number of frames/second 173 Preconditions: 174 `getFormat()` must be `1` 175 */ 176 byte getNegativeSMPTEFormat() const @nogc nothrow pure @safe 177 in (this.getFormat() == 1) { 178 // get bits 14 through 8 179 immutable bits = cast(ubyte) ((_rawValue >> 8) & 0b01111111); 180 // sign extend because it's in 2's complement 181 immutable value = bits | ((bits & 0b01000000) << 1); 182 // interpret as 2's complement 183 return (() @trusted => *cast(byte*) &value)(); 184 } 185 186 /** 187 Returns: 188 the number of ticks per frame 189 Preconditions: 190 `getFormat()` must be `1` 191 */ 192 ubyte getTicksPerFrame() const @nogc nothrow pure @safe 193 in (this.getFormat() == 1) { 194 // get bits 7 through 0 195 return _rawValue & 0b11111111; 196 } 197 198 private: 199 this(ushort rawValue) @nogc nothrow pure @safe { 200 _rawValue = rawValue; 201 } 202 ushort _rawValue; 203 } 204 205 /** 206 A track chunk is a chunk that stores the actual data. It contains a number of 207 track events. 208 */ 209 struct TrackChunk { 210 /// 211 TrackEvent[] events; 212 } 213 214 /** 215 A `TrackEvent` is a delta time coupled with either a MIDI event, a system 216 exclusive event or a meta event. 217 218 `TrackEvent` has an underlying union that uses the status byte to discriminate 219 (which cannot be changed after construction). Use `asMIDIEvent()`, 220 `asSysExEvent()` or `asMetaEvent()` to access the underlying values from the 221 union. 222 223 See the documentation of `mididi.def.isMIDIEvent()` to see what each type of 224 message means. Note: system exclusive events ARE also semantically system 225 common messages. The confusing thing is that all other system common messages 226 are syntactically MIDI events, but system exclusive events are not MIDI events. 227 */ 228 struct TrackEvent { 229 /** 230 These constructors initialize the object with a delta time and an event. 231 */ 232 this(int deltaTime, MIDIEvent event) @nogc nothrow pure @trusted { 233 this.deltaTime = deltaTime; 234 _statusByte = event.statusByte; 235 _midiEvent = event; 236 } 237 /// ditto 238 this(int deltaTime, SysExEvent event) @nogc nothrow pure @trusted 239 in (isSysExEvent(cast(ubyte) event.type)) { 240 this.deltaTime = deltaTime; 241 _statusByte = cast(ubyte) event.type; 242 _sysExEvent = event; 243 } 244 /// ditto 245 this(int deltaTime, MetaEvent event) @nogc nothrow pure @trusted { 246 this.deltaTime = deltaTime; 247 _statusByte = 0xFF; 248 _metaEvent = event; 249 } 250 251 /** 252 `deltaTime` is the time between the previous event and this event. 253 254 Its meaning depends on the time division in the header chunk. 255 */ 256 int deltaTime; 257 258 /** 259 Returns: 260 this event's status byte (see `TrackEvent`'s' documentation) 261 Note: 262 this property cannot be changed after construction 263 */ 264 @property ubyte statusByte() const @nogc nothrow pure @safe { 265 return _statusByte; 266 } 267 268 /** 269 Returns: 270 a pointer to the underlying `MIDIEvent`, `SysExEvent` or `MetaEvent` 271 if the object is of that variety; `null` otherwise 272 273 Note: 274 do not reassign the `TrackEvent` object while still having a reference 275 to the object in the union. 276 */ 277 inout(MIDIEvent)* asMIDIEvent() inout return @nogc nothrow pure @safe { 278 if (isMIDIEvent(_statusByte)) { 279 return (() inout @trusted => &_midiEvent)(); 280 } 281 return null; 282 } 283 /// ditto 284 inout(SysExEvent)* asSysExEvent() inout return @nogc nothrow pure @safe { 285 if (isSysExEvent(_statusByte)) { 286 return (() inout @trusted => &_sysExEvent)(); 287 } 288 return null; 289 } 290 /// ditto 291 inout(MetaEvent)* asMetaEvent() inout return @nogc nothrow pure @safe { 292 if (isMetaEvent(_statusByte)) { 293 return (() inout @trusted => &_metaEvent)(); 294 } 295 return null; 296 } 297 298 /// Equality and hash methods. 299 bool opEquals(ref const TrackEvent rhs) const @nogc nothrow pure @safe { 300 if (_statusByte != rhs._statusByte || deltaTime != rhs.deltaTime) { 301 return false; 302 } 303 304 if (auto midiEvent = asMIDIEvent()) { 305 if (auto r = rhs.asMIDIEvent()) { 306 return *midiEvent == *r; 307 } 308 return false; 309 } else if (auto sysExEvent = asSysExEvent()) { 310 if (auto r = rhs.asSysExEvent()) { 311 return *sysExEvent == *r; 312 } 313 return false; 314 } else if (auto metaEvent = asMetaEvent()) { 315 if (auto r = rhs.asMetaEvent()) { 316 return *metaEvent == *r; 317 } 318 return false; 319 } else { 320 assert(false, "unreachable"); 321 } 322 } 323 /// ditto 324 size_t toHash() const @nogc nothrow pure @safe { 325 immutable hash1 = hashOf(deltaTime); 326 if (auto midiEvent = asMIDIEvent()) { 327 return hashOf(*midiEvent, hash1); 328 } else if (auto sysExEvent = asSysExEvent()) { 329 return hashOf(*sysExEvent, hash1); 330 } else if (auto metaEvent = asMetaEvent()) { 331 return hashOf(*metaEvent, hash1); 332 } else { 333 assert(false, "unreachable"); 334 } 335 } 336 337 private: 338 ubyte _statusByte; 339 union { 340 MIDIEvent _midiEvent; 341 SysExEvent _sysExEvent; 342 MetaEvent _metaEvent; 343 } 344 } 345 346 /// Here is an example of how to use `TrackEvent`. 347 @nogc nothrow pure @safe unittest { 348 auto statusByte = cast(ubyte) ((ChannelMessageType.noteOn << 4) | 0x7); 349 auto event = TrackEvent( 350 200, 351 MIDIEvent(statusByte, [0x0F, 0x00]), 352 ); 353 354 // get underlying value 355 assert(event.asSysExEvent() is null); 356 assert(event.asMetaEvent() is null); 357 const message = event.asMIDIEvent(); 358 assert(message !is null); 359 360 // MIDI event = either a channel message or a system message 361 assert(message.isChannelMessage()); 362 assert(!message.isSystemMessage()); 363 364 // since it's a channel message, we can use these properties of the 365 // `MIDIEvent` struct: 366 assert(message.getChannelMessageType() == ChannelMessageType.noteOn); 367 assert(message.getChannelNumber() == 0x7); 368 assert(message.data == [0x0F, 0x00]); 369 } 370 371 /** 372 A MIDI event is any normal channel message or system message (except for system 373 exclusive events). 374 375 See also: 376 `mididi.def.isMIDIEvent` 377 */ 378 struct MIDIEvent { 379 /// 380 ubyte statusByte; 381 382 /** 383 The data bytes in this message. 384 385 The first bit of both bytes must be 0 and if the message type only needs 386 one byte, the second byte should be `0x00`. 387 */ 388 ubyte[2] data; 389 390 /** 391 */ 392 bool isChannelMessage() const @nogc nothrow pure @safe { 393 return .isChannelMessage(statusByte); 394 } 395 /// ditto 396 bool isSystemMessage() const @nogc nothrow pure @safe { 397 return .isSystemMessage(statusByte); 398 } 399 400 /** 401 These methods give the channel message type and the channel number (upper 402 and lower four bits, respectively). 403 404 Preconditions: 405 this message must be a channel message, so `this.isChannelMessage()` 406 must be true. 407 See also: 408 `mididi.def.ChannelMessageType` 409 */ 410 ChannelMessageType getChannelMessageType() const @nogc nothrow pure @safe 411 in (this.isChannelMessage()) { 412 immutable t = cast(ubyte) (statusByte >> 4); 413 return cast(ChannelMessageType) t; 414 } 415 /// ditto 416 ubyte getChannelNumber() const @nogc nothrow pure @safe 417 in (this.isChannelMessage()) { 418 return statusByte & 0xF; 419 } 420 421 /** 422 Preconditions: 423 this message must be a system message, so `this.isSystemMessage()` must 424 be true. 425 See also: 426 `mididi.def.SystemMessageType` 427 */ 428 SystemMessageType getSystemMessageType() const @nogc nothrow pure @safe 429 in (this.isSystemMessage()) { 430 return cast(SystemMessageType) statusByte; 431 } 432 433 /// Equality and hash methods. 434 bool opEquals(ref const MIDIEvent rhs) const @nogc nothrow pure @safe { 435 if (statusByte != rhs.statusByte) { 436 return false; 437 } 438 immutable len = getDataLength(statusByte); 439 return data[0 .. len] == rhs.data[0 .. len]; 440 } 441 /// ditto 442 size_t toHash() const @nogc nothrow pure @safe { 443 immutable len = getDataLength(statusByte); 444 return hashOf(statusByte, hashOf(data[0 .. len])); 445 } 446 } 447 448 /** 449 A system exclusive event is one unit ("packet") of a system exclusive message, 450 which can be used as an escape to emit any arbitrary bytes. 451 452 See also: 453 `mididi.def.isSysExEvent` 454 */ 455 struct SysExEvent { 456 /** 457 The kind of system exclusive event. This can be either `systemExclusive` 458 (`0xF0`) or `endOfExclusive` (`0xF7`). 459 */ 460 SystemMessageType type; 461 462 /// 463 ubyte[] data; 464 } 465 466 /** 467 A meta event is an event that is not regular MIDI message, used to send meta 468 information. Not every meta event has to be supported or acted upon. 469 470 See Also: 471 `mididi.def.isMetaEvent`; `mididi.def.MetaEventType` 472 */ 473 struct MetaEvent { 474 /// 475 MetaEventType type; 476 477 // is usually 1-6 bytes, could also be more data depending if it's a 478 // certain kind of meta event 479 /// 480 ubyte[] data; 481 }