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 }