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 }