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 }