1 /* 2 Cockos WDL License 3 4 Copyright (C) 2005 - 2015 Cockos Incorporated 5 Copyright (C) 2015 - 2017 Auburn Sounds 6 7 Portions copyright other contributors, see each source file for more information 8 9 This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. 10 11 Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 14 1. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 15 1. This notice may not be removed or altered from any source distribution. 16 */ 17 /** 18 MIDI messages definitions. 19 */ 20 module dplug.client.midi; 21 22 import std.algorithm.mutation; 23 import dplug.core.vec; 24 25 /// This abstraction is similar to the one in IPlug. 26 /// For VST raw MIDI messages are passed. 27 /// For AU MIDI messages gets synthesized. 28 struct MidiMessage 29 { 30 pure: 31 nothrow: 32 @nogc: 33 34 this( int offset, ubyte statusByte, ubyte data1, ubyte data2) 35 { 36 _offset = offset; 37 _statusByte = statusByte; 38 _data1 = data1; 39 _data2 = data2; 40 } 41 42 int offset() const 43 { 44 return _offset; 45 } 46 47 /// Midi channels 1 .. 16 48 /// 49 /// Returns: [0 .. 15] 50 int channel() const 51 { 52 return _statusByte & 0x0F; 53 } 54 55 /// Status Type 56 /// 57 /// See_Also: dplug.client.midi : MidiStatus 58 deprecated("Use statusType instead") alias status = statusType; 59 int statusType() const 60 { 61 return _statusByte >> 4; 62 } 63 64 // Status type distinction properties 65 66 bool isChannelAftertouch() const 67 { 68 return statusType() == MidiStatus.channelAftertouch; 69 } 70 71 bool isControlChange() const 72 { 73 return statusType() == MidiStatus.controlChange; 74 } 75 76 /// Checks whether the status type of the message is 'Note On' 77 /// _and the velocity value is actually greater than zero_. 78 /// 79 /// IMPORTANT: As per MIDI 1.0, a 'Note On' event with a velocity 80 /// of zero should be treated like a 'Note Off' event. 81 /// Which is why this function checks velocity > 0. 82 /// 83 /// See_Also: 84 /// isNoteOff() 85 bool isNoteOn() const 86 { 87 return (statusType() == MidiStatus.noteOn) && (noteVelocity() > 0); 88 } 89 90 /// Checks whether the status type of the message is 'Note Off' 91 /// or 'Note On' with a velocity of 0. 92 /// 93 /// IMPORTANT: As per MIDI 1.0, a 'Note On' event with a velocity 94 /// of zero should be treated like a 'Note Off' event. 95 /// Which is why this function checks velocity == 0. 96 /// 97 /// Some devices send a 'Note On' message with a velocity 98 /// value of zero instead of a real 'Note Off' message. 99 /// Many DAWs will automatically convert such ones to 100 /// explicit ones, but one cannot rely on this. 101 /// 102 /// See_Also: 103 /// isNoteOn() 104 bool isNoteOff() const 105 { 106 return (statusType() == MidiStatus.noteOff) 107 || 108 ( (statusType() == MidiStatus.noteOn) && (noteVelocity() == 0) ); 109 } 110 111 bool isPitchBend() const 112 { 113 return statusType() == MidiStatus.pitchWheel; 114 } 115 116 alias isPitchWheel = isPitchBend; 117 118 bool isPolyAftertouch() const 119 { 120 return statusType() == MidiStatus.polyAftertouch; 121 } 122 123 bool isProgramChange() const 124 { 125 return statusType() == MidiStatus.programChange; 126 } 127 128 // Data value properties 129 130 /// Channel pressure 131 int channelAftertouch() const 132 { 133 assert(isChannelAftertouch()); 134 return _data1; 135 } 136 137 /// Number of the changed controller 138 MidiControlChange controlChangeControl() const 139 { 140 assert(isControlChange()); 141 return cast(MidiControlChange)(_data1); 142 } 143 144 /// Controller's new value 145 /// 146 /// Returns: [1 .. 127] 147 int controlChangeValue() const 148 { 149 assert(isControlChange()); 150 return _data2; 151 } 152 153 /// Controller's new value 154 /// 155 /// Returns: [0.0 .. 1.0] 156 float controlChangeValue0to1() const 157 { 158 return cast(float)(controlChangeValue()) / 127.0f; 159 } 160 161 /// Returns: true = on 162 bool controlChangeOnOff() const 163 { 164 return controlChangeValue() >= 64; 165 } 166 167 int noteNumber() const 168 { 169 assert(isNoteOn() || isNoteOff() || isPolyAftertouch()); 170 return _data1; 171 } 172 173 int noteVelocity() const 174 { 175 return _data2; 176 } 177 178 /// Returns: [-1.0 .. 1.0[ 179 float pitchBend() const 180 { 181 assert(isPitchBend()); 182 immutable int iVal = (_data2 << 7) + _data1; 183 return cast(float) (iVal - 8192) / 8192.0f; 184 } 185 186 alias pitchWheel = pitchBend; 187 188 /// Amount of pressure 189 int polyAftertouch() const 190 { 191 assert(isPolyAftertouch()); 192 return _data2; 193 } 194 195 /// Program number 196 int program() const 197 { 198 assert(isProgramChange()); 199 return _data1; 200 } 201 202 private: 203 int _offset = 0; 204 205 ubyte _statusByte = 0; 206 207 ubyte _data1 = 0; 208 209 ubyte _data2 = 0; 210 } 211 212 213 enum MidiStatus : ubyte 214 { 215 none = 0, 216 noteOff = 8, 217 noteOn = 9, 218 polyAftertouch = 10, 219 controlChange = 11, 220 programChange = 12, 221 channelAftertouch = 13, 222 pitchBend = 14, 223 pitchWheel = pitchBend 224 }; 225 226 enum MidiControlChange : ubyte 227 { 228 modWheel = 1, 229 breathController = 2, 230 undefined003 = 3, 231 footController = 4, 232 portamentoTime = 5, 233 channelVolume = 7, 234 balance = 8, 235 undefined009 = 9, 236 pan = 10, 237 expressionController = 11, 238 effectControl1 = 12, 239 effectControl2 = 13, 240 generalPurposeController1 = 16, 241 generalPurposeController2 = 17, 242 generalPurposeController3 = 18, 243 generalPurposeController4 = 19, 244 sustainOnOff = 64, 245 portamentoOnOff = 65, 246 sustenutoOnOff = 66, 247 softPedalOnOff = 67, 248 legatoOnOff = 68, 249 hold2OnOff = 69, 250 soundVariation = 70, 251 resonance = 71, 252 releaseTime = 72, 253 attackTime = 73, 254 cutoffFrequency = 74, 255 decayTime = 75, 256 vibratoRate = 76, 257 vibratoDepth = 77, 258 vibratoDelay = 78, 259 soundControllerUndefined = 79, 260 tremoloDepth = 92, 261 chorusDepth = 93, 262 phaserDepth = 95, 263 allNotesOff = 123 264 } 265 266 nothrow @nogc: 267 268 MidiMessage makeMidiMessage(int offset, int channel, MidiStatus statusType, int data1, int data2) 269 { 270 assert(channel >= 0 && channel <= 15); 271 assert(statusType >= 0 && statusType <= 15); 272 assert(data1 >= 0 && data2 <= 255); 273 assert(data1 >= 0 && data2 <= 255); 274 MidiMessage msg; 275 msg._offset = offset; 276 msg._statusByte = cast(ubyte)( channel | (statusType << 4) ); 277 msg._data1 = cast(ubyte)data1; 278 msg._data2 = cast(ubyte)data2; 279 return msg; 280 } 281 282 MidiMessage makeMidiMessageNoteOn(int offset, int channel, int noteNumber, int velocity) 283 { 284 return makeMidiMessage(offset, channel, MidiStatus.noteOn, noteNumber, velocity); 285 } 286 287 MidiMessage makeMidiMessageNoteOff(int offset, int channel, int noteNumber) 288 { 289 return makeMidiMessage(offset, channel, MidiStatus.noteOff, noteNumber, 0); 290 } 291 292 MidiMessage makeMidiMessagePitchWheel(int offset, int channel, float value) 293 { 294 int ivalue = 8192 + cast(int)(value * 8192.0); 295 if (ivalue < 0) 296 ivalue = 0; 297 if (ivalue > 16383) 298 ivalue = 16383; 299 return makeMidiMessage(offset, channel, MidiStatus.pitchWheel, ivalue & 0x7F, ivalue >> 7); 300 } 301 302 MidiMessage makeMidiMessageControlChange(int offset, int channel, MidiControlChange index, float value) 303 { 304 // MAYDO: mapping is a bit strange here, not sure it can make +127 except exactly for 1.0f 305 return makeMidiMessage(offset, channel, MidiStatus.controlChange, index, cast(int)(value * 127.0f) ); 306 } 307 308 309 MidiQueue makeMidiQueue() 310 { 311 return MidiQueue(42); 312 } 313 314 /// Queue for MIDI messages 315 /// TODO: use a priority queue 316 struct MidiQueue 317 { 318 nothrow: 319 @nogc: 320 321 enum QueueCapacity = 511; 322 323 private this(int dummy) 324 { 325 _outMessages = makeVec!MidiMessage(QueueCapacity); 326 } 327 328 @disable this(this); 329 330 ~this() 331 { 332 } 333 334 void initialize() 335 { 336 // Clears all pending MIDI messages 337 _framesElapsed = 0; 338 _numElements = 0; 339 } 340 341 /// Enqueue a message in the priority queue. 342 void enqueue(MidiMessage message) 343 { 344 // Tweak message to mark it with current stamp 345 // This allows to have an absolute offset for MIDI messages. 346 assert(message._offset >= 0); 347 message._offset += _framesElapsed; 348 insertElement(message); 349 } 350 351 /// Gets all the MIDI messages for the next `frames` samples. 352 /// It is guaranteed to be in order relative to time. 353 /// These messages are valid until the next call to `getNextMidiMessages`. 354 const(MidiMessage)[] getNextMidiMessages(int frames) 355 { 356 _outMessages.clearContents(); 357 358 int framesLimit = _framesElapsed; 359 360 if (!empty()) 361 { 362 MidiMessage m = minElement(); 363 364 while(m._offset < (_framesElapsed + frames)) 365 { 366 // Subtract the timestamp again 367 m._offset -= _framesElapsed; 368 assert(m._offset >= 0); 369 assert(m._offset < frames); 370 _outMessages.pushBack(m); 371 popMinElement(); 372 373 if (empty()) 374 break; 375 376 m = minElement(); 377 } 378 } 379 _framesElapsed += frames; 380 return _outMessages[]; 381 } 382 383 private: 384 385 // Frames elapsed since the beginning 386 int _framesElapsed = 0; 387 388 // Scratch buffer to return slices of messages on demand (messages are copied there). 389 Vec!MidiMessage _outMessages; 390 391 // 392 // Min heap implementation below. 393 // 394 395 int _numElements = 0; 396 397 // Useful slots are in 1..QueueCapacity+1 398 // means it can contain QueueCapacity items at most 399 MidiMessage[QueueCapacity+1] _heap; 400 401 void insertElement(MidiMessage message) 402 { 403 // Insert the element at the next available bottom level slot 404 int slot = _numElements + 1; 405 406 _numElements++; 407 408 if (slot >= _heap.length) 409 { 410 // TODO: limitation here, we can't accept more than QueueCapacity MIDI messages in the queue 411 debug 412 { 413 // MIDI messages heap is on full capacity. 414 // That can happen because you have forgotten to call `getNextMidiMessages` 415 // with the necessary number of frames. 416 assert(false); 417 } 418 else 419 { 420 return; // dropping excessive message 421 } 422 } 423 424 _heap[slot] = message; 425 426 // Bubble up 427 while (slot > 1 && _heap[parentOf(slot)]._offset > _heap[slot]._offset) 428 { 429 // swap with parent if larger 430 swap(_heap[slot], _heap[parentOf(slot)]); 431 slot = parentOf(slot); 432 } 433 } 434 435 bool empty() 436 { 437 assert(_numElements >= 0); 438 return _numElements == 0; 439 } 440 441 MidiMessage minElement() 442 { 443 assert(!empty); 444 return _heap[1]; 445 } 446 447 void popMinElement() 448 { 449 assert(!empty); 450 451 // Put the last element into root 452 _heap[1] = _heap[_numElements]; 453 454 _numElements = _numElements-1; 455 456 int slot = 1; 457 458 if (slot >= _heap.length) 459 { 460 debug assert(false); // dropping excessive message 461 } 462 463 while (1) 464 { 465 // Looking for the minimum of self and children 466 467 int left = leftChildOf(slot); 468 int right = rightChildOf(slot); 469 int best = slot; 470 if (left <= _numElements && _heap[left]._offset < _heap[best]._offset) 471 best = left; 472 if (right <= _numElements && _heap[right]._offset < _heap[best]._offset) 473 best = right; 474 475 if (best == slot) // no swap, final position 476 { 477 // both children (if any) are larger, everything good 478 break; 479 } 480 else 481 { 482 // swap with smallest of children 483 swap(_heap[slot], _heap[best]); 484 485 // continue to bubble down 486 slot = best; 487 } 488 } 489 } 490 491 static int parentOf(int index) 492 { 493 return index >> 1; 494 } 495 496 static int leftChildOf(int index) 497 { 498 return 2*index; 499 } 500 501 static int rightChildOf(int index) 502 { 503 return 2*index+1; 504 } 505 } 506 507 unittest 508 { 509 MidiQueue queue = makeMidiQueue(); 510 511 foreach (k; 0..2) 512 { 513 // Enqueue QueueCapacity messages with decreasing timestamps 514 foreach(i; 0..MidiQueue.QueueCapacity) 515 { 516 queue.enqueue( makeMidiMessageNoteOn(MidiQueue.QueueCapacity-1-i, 0, 60, 100) ); 517 assert(queue._numElements == i+1); 518 } 519 520 const(MidiMessage)[] messages = queue.getNextMidiMessages(1024); 521 foreach(int i, m; messages) 522 { 523 import core.stdc.stdio; 524 // each should be in order 525 assert(m.offset == i); 526 assert(m.isNoteOn); 527 } 528 } 529 }