1 /**
2 `mididi.writer` contains functions for encoding MIDI objects to raw bytes.
3 
4 Currently, the writer 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 Note:
12     these functions do not always check if the provided data (i.e. the chunks
13     and track events) is actually correct, but if the data comes from
14     `MIDIReader` functions, it should always be complete and valid data
15 
16 Authors:
17     https://github.com/w2ptr
18 */
19 module mididi.writer;
20 
21 // Copyright Wout Huynen 2021.
22 // Distributed under the Boost Software License, Version 1.0.
23 // (See accompanying file LICENSE.txt or copy at
24 // https://www.boost.org/LICENSE_1_0.txt)
25 
26 import mididi.types;
27 import std.range.primitives : isOutputRange;
28 import std.stdio : File;
29 
30 /**
31 A utility output range that outputs bytes to a delegate.
32 */
33 struct DelegateSink {
34     ///
35     this(void delegate(scope const(ubyte)[]) sink) {
36         _sink = sink;
37     }
38 
39     ///
40     void put(ubyte x) {
41         ubyte[1] bytes = [x];
42         _sink(bytes[]);
43     }
44     ///
45     void putMultiple(const ubyte[] bytes) {
46         _sink(bytes);
47     }
48 
49 private:
50     void delegate(scope const(ubyte)[]) _sink;
51 }
52 
53 // the writeMIDIFile overloads are templates so all write* functions are not
54 // instantiated if it's never even used
55 /**
56 `writeMIDIFile` essentially does the same as `writeMIDI` to a range object that
57 writes to a `std.stdio.File`, but it is given for convenience.
58 
59 There are two overloads, one for a `std.stdio.File` object and one for a path
60 (which opens the file for you in write mode).
61 */
62 void writeMIDIFile()(File file, ref const MIDI midi) {
63     static struct FileOutputRange {
64         void put(ubyte x) {
65             ubyte[1] bytes = [x];
66             writer.put(bytes[]);
67         }
68         void putMultiple(scope const ubyte[] bytes) {
69             writer.put(bytes);
70         }
71 
72         typeof(file.lockingBinaryWriter()) writer;
73     }
74     auto output = FileOutputRange(file.lockingBinaryWriter());
75     output.writeMIDI(midi);
76 }
77 /// ditto
78 void writeMIDIFile()(string path, ref const MIDI midi) {
79     auto file = File(path, "w");
80     writeMIDIFile(file, midi);
81     file.close();
82 }
83 
84 /**
85 `writeMIDI` encodes a MIDI data object (`mididi.types.MIDI`) to binary data.
86 
87 The template parameter `T` must be an output range type defining the
88 `put(ubyte)` method. In addition, it can define the
89 `putMultiple(scope const ubyte[])` method if there is a more efficient method
90 to put several bytes at once. (This info applies to all `write*` functions in
91 this module.)
92 
93 Params:
94     T = the output range type
95     midi = the MIDI object that is encoded into `output`
96     output = the output range object
97 */
98 void writeMIDI(T)(ref T output, ref const MIDI midi)
99 if (isOutputRange!(T, ubyte)) {
100     output.writeHeaderChunk(midi.headerChunk);
101     assert(midi.headerChunk.nTracks == midi.trackChunks.length,
102         "the header's nTracks must be equal to the number of track chunks");
103     foreach (ref track; midi.trackChunks) {
104         output.writeTrackChunk(track);
105     }
106 }
107 
108 ///
109 unittest {
110     import mididi.def : MetaEventType, SystemMessageType, TrackFormat;
111 
112     const(ubyte)[] result;
113     auto sink = DelegateSink((scope const bytes) {
114         result ~= bytes;
115     });
116     auto midi = MIDI(
117         HeaderChunk(TrackFormat.single, 1, TimeDivision.fromFormat0(1000)),
118         [
119             TrackChunk([
120                 TrackEvent(
121                     0xFF,
122                     MIDIEvent(cast(ubyte) SystemMessageType.songSelect, [123, 0]),
123                 ),
124                 TrackEvent(
125                     0x0F,
126                     MetaEvent(MetaEventType.endOfTrack, []),
127                 ),
128             ]),
129         ]
130     );
131     sink.writeMIDI(midi);
132     assert(result == cast(const(ubyte)[]) [
133         'M', 'T', 'h', 'd',
134         0, 0, 0, 6,
135         0, 0,
136         0, 1,
137         0x03, 0xE8,
138 
139         'M', 'T', 'r', 'k',
140         0, 0, 0, 8,
141         0x81, 0x7F, cast(ubyte) SystemMessageType.songSelect, 123,
142         0x0F,       0xFF, cast(ubyte) MetaEventType.endOfTrack, 0x00,
143     ]);
144 }
145 
146 /**
147 */
148 void writeHeaderChunk(T)(ref T output, ref const HeaderChunk chunk)
149 if (isOutputRange!(T, ubyte)) {
150     output.write(cast(const(ubyte)[]) "MThd");
151     output.writeInt32(6); // length
152     output.writeInt16(cast(ushort) chunk.trackFormat);
153     output.writeInt16(chunk.nTracks);
154     output.writeInt16(chunk.division.rawValue);
155 }
156 
157 /// This test demonstrates header chunk writing.
158 unittest {
159     import mididi.def : TrackFormat;
160 
161     auto chunk = HeaderChunk(
162         TrackFormat.sequential,
163         ushort.max,
164         TimeDivision.fromFormat1(-25, 64),
165     );
166     auto result = "";
167     auto range = DelegateSink((scope const bytes) {
168         result ~= bytes;
169     });
170     range.writeHeaderChunk(chunk);
171     assert(result == [
172         'M', 'T', 'h', 'd', // chunk: header
173         0, 0, 0, 6, // length: 6
174         0, 2, // format: 2
175         0xFF, 0xFF, // nTracks: ushort.max
176         0b1_1100111, 64, // division: format 1; -25; 64
177     ]);
178 }
179 
180 /**
181 `writeTrackChunk` encodes a track chunk to bytes and `put`s them into `output`.
182 
183 This is guaranteed to use "running status" to encode consecutive track events
184 with the same status byte.
185 
186 Params:
187     output = the output range object
188     chunk = the track chunk to be encoded
189 */
190 void writeTrackChunk(T)(ref T output, ref const TrackChunk chunk)
191 if (isOutputRange!(T, ubyte)) {
192     output.write(cast(const(ubyte)[]) "MTrk");
193 
194     // unfortunately, we have to write everything to a buffer first so the
195     // length can be written before everything else
196     // TODO: this can be a stack buffer so it uses less heap memory
197     const(ubyte)[] buffer = [];
198     auto bufferWriter = DelegateSink((scope const bytes) {
199         buffer ~= bytes;
200     });
201     ubyte runningStatus = 0;
202     foreach (ref event; chunk.events) {
203         bufferWriter.writeTrackEvent(event, runningStatus);
204         runningStatus = event.statusByte;
205     }
206     output.writeInt32(cast(uint) buffer.length);
207     output.write(buffer);
208 }
209 
210 /// This test demonstrates track chunk writing
211 unittest {
212     import mididi.def : MetaEventType;
213 
214     const(ubyte)[] result = [];
215     auto range = DelegateSink((scope const bytes) {
216         result ~= bytes;
217     });
218     auto chunk = TrackChunk([
219         TrackEvent(
220             100, // delta time
221             // 0x93 = note on
222             MIDIEvent(0x93, [0x4C, 0x20]),
223         ),
224         TrackEvent(
225             300,
226             // 0x93 = note on (same status)
227             MIDIEvent(0x93, [0x4C, 0x00]),
228         ),
229         TrackEvent(
230             400,
231             MetaEvent(MetaEventType.endOfTrack, []),
232         ),
233     ]);
234     range.writeTrackChunk(chunk);
235     assert(result == [
236         'M', 'T', 'r', 'k',
237         0, 0, 0, 13,
238         0x64,       0x93, 0x4C, 0x20,
239         0x82, 0x2C,       0x4C, 0x00, // running status
240         0x83, 0x10, 0xFF, 0x2F, 0x00,
241     ]);
242 }
243 
244 /**
245 `writeTrackEvent` encodes a single track event to binary data and `put`s it
246 into `output`.
247 
248 If you want to encode using running status (meaning it leaves out the status
249 byte if this event's status byte is the same as the previous one), set
250 `runningStatus` to the previous event's status byte. Otherwise, set it to 0.
251 
252 Params:
253     output = the output range to send the bytes to
254     event = the track event that is encoded to bytes
255     runningStatus = the previous event's status byte; set to `0` if you don't
256         care about saving space using running status
257 */
258 void writeTrackEvent(T)(ref T output, ref const TrackEvent event, ubyte runningStatus)
259 if (isOutputRange!(T, ubyte)) {
260     import mididi.def : getDataLength;
261 
262     output.writeVariableInt(event.deltaTime);
263     if (auto midiEvent = event.asMIDIEvent()) {
264         if (event.statusByte != runningStatus) {
265             output.writeInt8(event.statusByte);
266         }
267         output.write(midiEvent.data[0 .. getDataLength(event.statusByte)]);
268     } else if (auto sysExEvent = event.asSysExEvent()) {
269         output.writeInt8(event.statusByte);
270         output.writeVariableInt(cast(uint) sysExEvent.data.length);
271         output.write(sysExEvent.data);
272     } else if (auto metaEvent = event.asMetaEvent()) {
273         assert(event.statusByte == 0xFF);
274         output.writeInt8(0xFF);
275         output.writeInt8(cast(ubyte) metaEvent.type);
276         output.writeVariableInt(cast(uint) metaEvent.data.length);
277         output.write(metaEvent.data);
278     } else {
279         assert(false, "unreachable");
280     }
281 }
282 
283 private:
284 
285 void writeVariableInt(T)(ref T output, uint x)
286 in (x < (1 << 28)) {
287     if (x < (1 << 7)) {
288         output.put(cast(ubyte) x);
289     } else if (x < (1 << 14)) {
290         ubyte[2] bytes = [
291             (1 << 7) | cast(ubyte) ((x >> 7) & 0x7F),
292             (0 << 7) | cast(ubyte) ((x >> 0) & 0x7F),
293         ];
294         output.write(bytes[]);
295     } else if (x < (1 << 21)) {
296         ubyte[3] bytes = [
297             (1 << 7) | cast(ubyte) ((x >> 14) & 0x7F),
298             (1 << 7) | cast(ubyte) ((x >> 7) & 0x7F),
299             (0 << 7) | cast(ubyte) ((x >> 0) & 0x7F),
300         ];
301         output.write(bytes[]);
302     } else {
303         ubyte[4] bytes = [
304             (1 << 7) | cast(ubyte) ((x >> 21) & 0x7F),
305             (1 << 7) | cast(ubyte) ((x >> 14) & 0x7F),
306             (1 << 7) | cast(ubyte) ((x >> 7) & 0x7F),
307             (0 << 7) | cast(ubyte) ((x >> 0) & 0x7F),
308         ];
309         output.write(bytes[]);
310     }
311 }
312 
313 // This test checks if variable integer writing works correctly.
314 unittest {
315     void test(uint x, ubyte[] result) {
316         auto local = result;
317         auto sink = DelegateSink((scope const bytes) {
318             assert(bytes.length <= local.length);
319             assert(bytes == local[0 .. bytes.length]);
320             local = local[bytes.length .. $];
321         });
322         sink.writeVariableInt(x);
323         assert(local.length == 0);
324     }
325 
326     test(0x000, [0]);
327     test(0x40, [0x40]);
328     test(0x7F, [0x7F]);
329     test(0x80, [0x81, 0x00]);
330     test(0x2000, [0xC0, 0x00]);
331     test(0x3FFF, [0xFF, 0x7F]);
332     test(0x4000, [0x81, 0x80, 0x00]);
333     test(0x100000, [0xC0, 0x80, 0x00]);
334     test(0x1FFFFF, [0xFF, 0xFF, 0x7F]);
335     test(0x200000, [0x81, 0x80, 0x80, 0x00]);
336     test(0x8000000, [0xC0, 0x80, 0x80, 0x00]);
337     test(0xFFFFFFF, [0xFF, 0xFF, 0xFF, 0x7F]);
338 }
339 
340 void writeInt32(T)(ref T output, uint x)
341 if (isOutputRange!(T, ubyte)) {
342     import std.bitmanip : nativeToBigEndian;
343 
344     ubyte[4] bytes = nativeToBigEndian(x);
345     output.write(bytes[]);
346 }
347 
348 void writeInt16(T)(ref T output, ushort x)
349 if (isOutputRange!(T, ubyte)) {
350     import std.bitmanip : nativeToBigEndian;
351 
352     ubyte[2] bytes = nativeToBigEndian(x);
353     output.write(bytes[]);
354 }
355 
356 void writeInt8(T)(ref T output, ubyte x)
357 if (isOutputRange!(T, ubyte)) {
358     ubyte[1] bytes = [x];
359     output.write(bytes[]);
360 }
361 
362 void write(T)(ref T output, scope const ubyte[] bytes)
363 if (isOutputRange!(T, ubyte)) {
364     static if (
365         __traits(hasMember, output, "putMultiple") &&
366         __traits(compiles, {
367             ubyte[1] x = [0];
368             output.putMultiple(x[]);
369         })
370     ) {
371         output.putMultiple(bytes);
372     } else {
373         foreach (b; bytes) {
374             output.put(b);
375         }
376     }
377 }