iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
MidiSynth.cpp
1/*
2 ==============================================================================
3
4 This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5
6 See LICENSE.txt for more info.
7
8 ==============================================================================
9 */
10
11#include "MidiSynth.h"
12
13using namespace iplug;
14
15MidiSynth::MidiSynth(VoiceAllocator::EPolyMode mode, int blockSize)
16: mBlockSize(blockSize)
17{
18 SetPolyMode(mode);
19
20 for(int i=0; i<128; i++)
21 {
22 mVelocityLUT[i] = i / 127.f;
23 mAfterTouchLUT[i] = i / 127.f;
24 }
25
26 // initialize Channel states
27 for(int i=0; i<16; ++i)
28 {
29 mChannelStates[i] = ChannelState{0};
30 mChannelStates[i].pitchBendRange = kDefaultPitchBendRange;
31 }
32}
33
34MidiSynth::~MidiSynth()
35{
36}
37
38VoiceInputEvent MidiSynth::MidiMessageToEventBasic(const IMidiMsg& msg)
39{
40 VoiceInputEvent event{};
41
42 IMidiMsg::EStatusMsg status = msg.StatusMsg();
43 event.mSampleOffset = msg.mOffset;
44 event.mAddress.mChannel = msg.Channel();
45 event.mAddress.mKey = msg.NoteNumber();
46
47 switch (status)
48 {
49 case IMidiMsg::kNoteOn:
50 {
51 int v = Clip(msg.Velocity(), 0, 127);
52 event.mAction = (v == 0) ? kNoteOffAction : kNoteOnAction;
53 event.mValue = mVelocityLUT[v];
54 break;
55 }
56 case IMidiMsg::kNoteOff:
57 {
58 int v = Clip(msg.Velocity(), 0, 127);
59 event.mAction = kNoteOffAction;
60 event.mValue = mVelocityLUT[v];
61 break;
62 }
63 case IMidiMsg::kPolyAftertouch:
64 {
65 event.mAction = kPressureAction;
66 event.mValue = mAfterTouchLUT[msg.PolyAfterTouch()];
67 break;
68 }
69 case IMidiMsg::kChannelAftertouch:
70 {
71 event.mAction = kPressureAction;
72 event.mValue = mAfterTouchLUT[msg.ChannelAfterTouch()];
73 break;
74 }
75 case IMidiMsg::kPitchWheel:
76 {
77 event.mAction = kPitchBendAction;
78 float bendRange = mChannelStates[event.mAddress.mChannel].pitchBendRange;
79 event.mValue = static_cast<float>(msg.PitchWheel()) * bendRange / 12.f;
80 break;
81 }
82 case IMidiMsg::kControlChange:
83 {
84 event.mControllerNumber = msg.ControlChangeIdx();
85 event.mValue = static_cast<float>(msg.ControlChange(msg.ControlChangeIdx()));
86 switch(event.mControllerNumber)
87 {
88 // handle special controllers
89 case IMidiMsg::kCutoffFrequency:
90 {
91 event.mAction = kTimbreAction;
92 break;
93 }
94 case IMidiMsg::kSustainOnOff:
95 {
96 event.mAction = kSustainAction;
97 break;
98 }
99 case IMidiMsg::kAllNotesOff:
100 {
101 event.mAddress.mFlags = kVoicesAll;
102 event.mAction = kNoteOffAction;
103 break;
104 }
105 // handle all other controllers
106 default:
107 {
108 event.mAction = kControllerAction;
109 break;
110 }
111 }
112 break;
113 }
114 case IMidiMsg::kProgramChange:
115 {
116 event.mAction = kProgramChangeAction;
117 event.mControllerNumber = msg.Program();
118 break;
119 }
120 default:
121 {
122 break;
123 }
124 }
125
126 return event;
127}
128
129// Here we handle the MIDI messages used by MPE as listed in the MPE spec, page 7
130VoiceInputEvent MidiSynth::MidiMessageToEventMPE(const IMidiMsg& msg)
131{
132 VoiceInputEvent event{};
133 IMidiMsg::EStatusMsg status = msg.StatusMsg();
134 event.mSampleOffset = msg.mOffset;
135 event.mAddress.mChannel = msg.Channel();
136 event.mAddress.mKey = msg.NoteNumber();
137 event.mAddress.mZone = MasterZoneFor(event.mAddress.mChannel);
138
139 // handle pitch bend, channel pressure and CC#74 in the same way:
140 // sum main and member channel values
141 bool isPitchBend = status == IMidiMsg::kPitchWheel;
142 bool isChannelPressure = status == IMidiMsg::kChannelAftertouch;
143 bool isTimbre = (status == IMidiMsg::kControlChange) && (msg.ControlChangeIdx() == IMidiMsg::kCutoffFrequency);
144 if(isPitchBend || isChannelPressure || isTimbre)
145 {
146 float* pChannelDestValue{};
147 float masterChannelStoredValue{};
148 if(isPitchBend)
149 {
150 event.mAction = kPitchBendAction;
151 float bendRange = mChannelStates[event.mAddress.mChannel].pitchBendRange;
152 event.mValue = static_cast<float>(msg.PitchWheel()) * bendRange / 12.f;
153
154 pChannelDestValue = &(mChannelStates[event.mAddress.mChannel].pitchBend);
155 masterChannelStoredValue = mChannelStates[MasterChannelFor(event.mAddress.mChannel)].pitchBend;
156 }
157 else if(isChannelPressure)
158 {
159 event.mAction = kPressureAction;
160 event.mValue = mAfterTouchLUT[msg.ChannelAfterTouch()];
161 pChannelDestValue = &(mChannelStates[event.mAddress.mChannel].pressure);
162 masterChannelStoredValue = mChannelStates[MasterChannelFor(event.mAddress.mChannel)].pressure;
163 }
164 else if(isTimbre)
165 {
166 event.mAction = kTimbreAction;
167 event.mValue = static_cast<float>(msg.ControlChange(msg.ControlChangeIdx()));
168 pChannelDestValue = &(mChannelStates[event.mAddress.mChannel].timbre);
169 masterChannelStoredValue = mChannelStates[MasterChannelFor(event.mAddress.mChannel)].timbre;
170 }
171
172 if(IsMasterChannel(event.mAddress.mChannel))
173 {
174 // store value in master channel
175 *pChannelDestValue = event.mValue;
176
177 // no action needed
178 event.mAction = kNullAction;
179 }
180 else
181 {
182 // add stored master channel value to event value
183 event.mValue += masterChannelStoredValue;
184
185 // store sum in member channel
186 *pChannelDestValue = event.mValue;
187 }
188 return event;
189 }
190
191 // pitch bend sensitiity (RPN 0) is handled in HandleRPN()
192
193 // poly key pressure is ignored in MPE.
194
195 switch (status)
196 {
197 // program change:
198 // we are using MIDI mode 3. A program change sent to a master channel
199 // affects all voices within the zone. Program changes sent to member channels are ignored.
200 case IMidiMsg::kProgramChange:
201 {
202 if(IsMasterChannel(event.mAddress.mChannel))
203 {
204 event.mAction = kProgramChangeAction;
205 event.mControllerNumber = msg.Program();
206 break;
207 }
208 else
209 {
210 event.mAction = kNullAction;
211 }
212 }
213 case IMidiMsg::kNoteOn:
214 {
215 int v = Clip(msg.Velocity(), 0, 127);
216 event.mAction = (v == 0) ? kNoteOffAction : kNoteOnAction;
217 event.mValue = mVelocityLUT[v];
218 break;
219 }
220 case IMidiMsg::kNoteOff:
221 {
222 int v = Clip(msg.Velocity(), 0, 127);
223 event.mAction = kNoteOffAction;
224 event.mValue = mVelocityLUT[v];
225 break;
226 }
227 case IMidiMsg::kControlChange:
228 {
229 event.mControllerNumber = msg.ControlChangeIdx();
230 switch(event.mControllerNumber)
231 {
232 case IMidiMsg::kAllNotesOff:
233 {
234 event.mAddress.mFlags = kVoicesAll;
235 event.mAction = kNoteOffAction;
236 break;
237 }
238
239 default:
240 // send all other controllers to matching channels using the generic control action
241 // note: according to the MPE spec these messages should be sent to all channels in the zone,
242 // but that is less useful IMO - to do so just add the line
243 // event.mAddress.mChannel = kAllChannels;
244 event.mAction = kControllerAction;
245 break;
246 }
247 event.mValue = static_cast<float>(msg.ControlChange(msg.ControlChangeIdx()));
248 break;
249 }
250 default:
251 {
252 break;
253 }
254 }
255
256 return event;
257}
258
259VoiceInputEvent MidiSynth::MidiMessageToEvent(const IMidiMsg& msg)
260{
261 return (mMPEMode ? MidiMessageToEventMPE(msg) : MidiMessageToEventBasic(msg));
262}
263
264// sets the number of channels in the lo or hi MPE zones.
265void MidiSynth::SetMPEZones(int channel, int nChans)
266{
267 // total channels = member channels + the master channel, or 0 if there is no Zone.
268 // totalChannels is never 1.
269 int memberChannels = Clip(nChans, 0, 15);
270 int totalChannels = memberChannels ? (memberChannels + 1) : 0;
271 if(channel == 0)
272 {
273 mMPELowerZoneChannels = totalChannels;
274 mMPEUpperZoneChannels = Clip(mMPEUpperZoneChannels, 0, 16 - mMPELowerZoneChannels);
275 }
276 else if (channel == 15)
277 {
278 mMPEUpperZoneChannels = totalChannels;
279 mMPELowerZoneChannels = Clip(mMPELowerZoneChannels, 0, 16 - mMPEUpperZoneChannels);
280 }
281
282 // activate / deactivate MPE mode if needed
283 bool anyMPEChannelsActive = (mMPELowerZoneChannels || mMPEUpperZoneChannels);
284 if(anyMPEChannelsActive && (!mMPEMode))
285 {
286 mMPEMode = true;
287 }
288 else if ((!anyMPEChannelsActive) && (mMPEMode))
289 {
290 mMPEMode = false;
291 }
292
293 // reset pitch bend ranges as per MPE spec
294 if(mMPEMode)
295 {
296 if(channel == 0)
297 {
298 SetChannelPitchBendRange(kMPELowerZoneMasterChannel, 2);
299 SetChannelPitchBendRange(kMPELowerZoneMasterChannel + 1, 48);
300 }
301 else if (channel == 15)
302 {
303 SetChannelPitchBendRange(kMPEUpperZoneMasterChannel, 2);
304 SetChannelPitchBendRange(kMPEUpperZoneMasterChannel - 1, 48);
305 }
306 }
307 else
308 SetPitchBendRange(mNonMPEPitchBendRange);
309
310 std::cout << "MPE mode: " << (mMPEMode ? "ON" : "OFF") << "\n";
311 std::cout << "MPE channels: \n lo: " << mMPELowerZoneChannels << " hi " << mMPEUpperZoneChannels << "\n";
312}
313
314void MidiSynth::SetChannelPitchBendRange(int channelParam, int rangeParam)
315{
316 int channelLo, channelHi;
317 if(IsInLowerZone(channelParam))
318 {
319 channelLo = LowerZoneStart();
320 channelHi = LowerZoneEnd();
321 }
322 else if (IsInUpperZone(channelParam))
323 {
324 channelLo = UpperZoneStart();
325 channelHi = UpperZoneEnd();
326 }
327 else
328 {
329 channelLo = channelHi = Clip(channelParam, 0, 15);
330 }
331
332 int range = Clip(rangeParam, 0, 96);
333
334 for(int i=channelLo; i <= channelHi; ++i)
335 {
336 mChannelStates[i].pitchBendRange = range;
337 }
338}
339
340bool IsRPNMessage(IMidiMsg msg)
341{
342 if(msg.StatusMsg() != IMidiMsg::kControlChange) return false;
343 int cc = msg.mData1;
344 return(cc == 0x64)||(cc == 0x65)||(cc == 0x26)||(cc == 0x06);
345}
346
347void MidiSynth::HandleRPN(IMidiMsg msg)
348{
349 int channel = msg.Channel();
350 ChannelState& state = mChannelStates[channel];
351
352 uint8_t valueByte = msg.mData2;
353 int param, value;
354 switch (msg.mData1)
355 {
356 case 0x64:
357 state.paramLSB = valueByte;
358 state.valueMSB = state.valueLSB = 0xff;
359 break;
360 case 0x65:
361 state.paramMSB = valueByte;
362 state.valueMSB = state.valueLSB = 0xff;
363 break;
364 case 0x26:
365 state.valueLSB = valueByte;
366 break;
367 case 0x06:
368 // whenever the value MSB byte is received we constuct the value and take action on the RPN.
369 // if only the MSB has been received, it is used as the entire value so the maximum possible value is 127.
370 state.valueMSB = valueByte;
371 param = ((state.paramMSB&0xFF) << 7) + (state.paramLSB&0xFF);
372 if(state.valueLSB != 0xff)
373 {
374 value = ((state.valueMSB&0xFF) << 7) + (state.valueLSB&0xFF);
375 }
376 else
377 {
378 value = state.valueMSB&0xFF;
379 }
380 std::cout << "RPN received: channel " << channel << ", param " << param << ", value " << value << "\n";
381 switch(param)
382 {
383 case 0: // RPN 0 : pitch bend range
384 SetChannelPitchBendRange(channel, value);
385 break;
386 case 6: // RPN 6 : MPE zone configuration. These messages may turn MPE mode on or off.
387 if(IsMasterChannel(channel))
388 {
389 SetMPEZones(channel, value);
390 }
391 break;
392 default:
393 break;
394 }
395 break;
396
397 default:
398 break;
399 }
400}
401
402bool MidiSynth::ProcessBlock(sample** inputs, sample** outputs, int nInputs, int nOutputs, int nFrames)
403{
404 assert(NVoices());
405
406 if (mVoicesAreActive | !mMidiQueue.Empty())
407 {
408 int blockSize = mBlockSize;
409 int samplesRemaining = nFrames;
410 int startIndex = 0;
411
412 while(samplesRemaining > 0)
413 {
414 if(samplesRemaining < blockSize)
415 blockSize = samplesRemaining;
416
417 while (!mMidiQueue.Empty())
418 {
419 IMidiMsg msg = mMidiQueue.Peek();
420
421 // we assume the messages are in chronological order. If we find one later than the current block we are done.
422 if (msg.mOffset > startIndex + blockSize) break;
423
424 if(IsRPNMessage(msg))
425 {
426 HandleRPN(msg);
427 }
428 else
429 {
430 // send performance messages to the voice allocator
431 // message offset is relative to the start of this processSamples() block
432 msg.mOffset -= startIndex;
433 mVoiceAllocator.AddEvent(MidiMessageToEvent(msg));
434 }
435 mMidiQueue.Remove();
436 }
437
438 mVoiceAllocator.ProcessEvents(blockSize, mSampleTime);
439 mVoiceAllocator.ProcessVoices(inputs, outputs, nInputs, nOutputs, startIndex, blockSize);
440
441 samplesRemaining -= blockSize;
442 startIndex += blockSize;
443 mSampleTime += blockSize;
444 }
445
446 bool voicesbusy = false;
447 int activeCount = 0;
448
449 for(int v = 0; v < NVoices(); v++)
450 {
451 bool busy = GetVoice(v)->GetBusy();
452 voicesbusy |= busy;
453
454 activeCount += (busy==true);
455#if DEBUG_VOICE_COUNT
456 if(GetVoice(v)->GetBusy()) printf("X");
457 else DBGMSG("_");
458 }
459 DBGMSG("\n");
460 DBGMSG("Num Voices busy %i\n", activeCount);
461#else
462 }
463#endif
464
465 mVoicesAreActive = voicesbusy;
466
467 mMidiQueue.Flush(nFrames);
468 }
469 else // empty block
470 {
471 return true;
472 }
473
474 return false; // made some noise
475}
476
477void MidiSynth::SetSampleRateAndBlockSize(double sampleRate, int blockSize)
478{
479 Reset();
480
481 mSampleRate = sampleRate;
482 mMidiQueue.Resize(blockSize);
483 mVoiceAllocator.SetSampleRateAndBlockSize(sampleRate, blockSize);
484
485 for(int v = 0; v < NVoices(); v++)
486 {
487 GetVoice(v)->SetSampleRateAndBlockSize(sampleRate, blockSize);
488 }
489}
A VoiceInputEvent describes a change in input to be applied to one more more voices.
bool ProcessBlock(sample **inputs, sample **outputs, int nInputs, int nOutputs, int nFrames)
Processes a block of audio samples.
Definition: MidiSynth.cpp:402
void SetPitchBendRange(int pitchBendRange)
Set the pitch bend range for non-MPE mode.
Definition: MidiSynth.h:74
virtual bool GetBusy() const =0
virtual void SetSampleRateAndBlockSize(double sampleRate, int blockSize)
Implement this if you need to do work when the sample rate or block size changes.
Definition: SynthVoice.h:92
void AddEvent(VoiceInputEvent e)
Add a single event to the input queue for the current processing block.
void ProcessEvents(int samples, int64_t sampleTime)
Process all input events and generate voice outputs.
BEGIN_IPLUG_NAMESPACE T Clip(T x, T lo, T hi)
Clips the value x between lo and hi.
Encapsulates a MIDI message and provides helper functions.
Definition: IPlugMidi.h:31
double ControlChange(EControlChangeMsg idx) const
Get the value of a CC message.
Definition: IPlugMidi.h:340
int Channel() const
Gets the channel of a MIDI message.
Definition: IPlugMidi.h:236
int Velocity() const
Get the velocity value of a NoteOn/NoteOff message.
Definition: IPlugMidi.h:270
int Program() const
Get the program index from a Program Change message.
Definition: IPlugMidi.h:310
EStatusMsg
Constants for the status byte of a MIDI message.
Definition: IPlugMidi.h:37
int ChannelAfterTouch() const
Get the Pressure value from an AfterTouch message.
Definition: IPlugMidi.h:297
EControlChangeMsg ControlChangeIdx() const
Gets the controller index of a CC message.
Definition: IPlugMidi.h:333
int PolyAfterTouch() const
Get the Pressure value from a PolyAfterTouch message.
Definition: IPlugMidi.h:284
double PitchWheel() const
Get the value from a Pitchwheel message.
Definition: IPlugMidi.h:321
int NoteNumber() const
Gets the MIDI note number.
Definition: IPlugMidi.h:255
EStatusMsg StatusMsg() const
Gets the MIDI Status message.
Definition: IPlugMidi.h:243