diff --git a/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.cpp b/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.cpp index f8512aca732c4..0b0b1c4e8241c 100644 --- a/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.cpp +++ b/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.cpp @@ -97,6 +97,11 @@ const ChannelMap& FluidSequencer::channels() const return m_channels; } +int FluidSequencer::lastStaff() const +{ + return m_lastStaff; +} + void FluidSequencer::updatePlaybackEvents(EventSequenceMap& destination, const mpe::PlaybackEventsMap& changes) { for (const auto& pair : changes) { @@ -109,6 +114,9 @@ void FluidSequencer::updatePlaybackEvents(EventSequenceMap& destination, const m timestamp_t timestampFrom = noteEvent.arrangementCtx().actualTimestamp; timestamp_t timestampTo = timestampFrom + noteEvent.arrangementCtx().actualDuration; + // Assumption: 1:1 mapping between staff and instrument and FluidSynth and FluidSequencer instances. + // So staffLayerIndex is constant. It changes only when moving staffs. + m_lastStaff = noteEvent.arrangementCtx().staffLayerIndex; channel_t channelIdx = channel(noteEvent); note_idx_t noteIdx = noteIndex(noteEvent.pitchCtx().nominalPitchLevel); @@ -285,7 +293,7 @@ tuning_t FluidSequencer::noteTuning(const mpe::NoteEvent& noteEvent, const int n velocity_t FluidSequencer::noteVelocity(const mpe::NoteEvent& noteEvent) const { - static constexpr midi::velocity_t MAX_SUPPORTED_VELOCITY = 127; + static constexpr midi::velocity_t MAX_SUPPORTED_VELOCITY = std::numeric_limits::max(); const mpe::ExpressionContext& expressionCtx = noteEvent.expressionCtx(); @@ -300,7 +308,7 @@ velocity_t FluidSequencer::noteVelocity(const mpe::NoteEvent& noteEvent) const } dynamic_level_t dynamicLevel = expressionCtx.expressionCurve.maxAmplitudeLevel(); - return expressionLevel(dynamicLevel); + return expressionLevel(dynamicLevel) << 9; // midi::Event::scaleUp(7,16) } int FluidSequencer::expressionLevel(const mpe::dynamic_level_t dynamicLevel) const diff --git a/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.h b/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.h index 00f7015fb9095..449baa1ef02a0 100644 --- a/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.h +++ b/src/framework/audio/internal/synthesizers/fluidsynth/fluidsequencer.h @@ -42,6 +42,7 @@ class FluidSequencer : public AbstractEventSequencer async::Channel channelAdded() const; const ChannelMap& channels() const; + int lastStaff() const; private: void updateOffStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::PlaybackParamList& params) override; @@ -66,6 +67,7 @@ class FluidSequencer : public AbstractEventSequencer mutable ChannelMap m_channels; bool m_useDynamicEvents = false; + int m_lastStaff = -1; }; } diff --git a/src/framework/audio/internal/synthesizers/fluidsynth/fluidsynth.cpp b/src/framework/audio/internal/synthesizers/fluidsynth/fluidsynth.cpp index 9e58178babc61..753d9f602395c 100644 --- a/src/framework/audio/internal/synthesizers/fluidsynth/fluidsynth.cpp +++ b/src/framework/audio/internal/synthesizers/fluidsynth/fluidsynth.cpp @@ -46,6 +46,8 @@ static constexpr msecs_t MIN_NOTE_LENGTH = 10; static constexpr unsigned int FLUID_AUDIO_CHANNELS_PAIR = 1; static constexpr unsigned int FLUID_AUDIO_CHANNELS_COUNT = FLUID_AUDIO_CHANNELS_PAIR * 2; +static constexpr bool STAFF_TO_MIDIOUT_CHANNEL = true; + struct muse::audio::synth::Fluid { fluid_settings_t* settings = nullptr; fluid_synth_t* synth = nullptr; @@ -160,6 +162,46 @@ void FluidSynth::allNotesOff() setControllerValue(i, midi::SUSTAIN_PEDAL_CONTROLLER, 0); setPitchBend(i, 8192); } + + auto port = midiOutPort(); + if (port->isConnected()) { + // Send all notes off to connected midi ports. + // Room for improvement: + // - We could record which groups/channels we sent something or which channels were scheduled in the sequencer. + // - We could send all events at once. + int lowerBound = 0; + int upperBound = 0; + if (STAFF_TO_MIDIOUT_CHANNEL) { + int channel = m_sequencer.lastStaff(); + if (channel < 0) { + return; + } + channel = channel % 16; + lowerBound = channel; + upperBound = channel + 1; + } else { + upperBound = min(lastChannelIdx, 16); + } + for (int i = lowerBound; i < upperBound; i++) { + muse::midi::Event e(muse::midi::Event::Opcode::ControlChange, muse::midi::Event::MessageType::ChannelVoice20); + e.setChannel(i); + e.setIndex(123); // CC#123 = All notes off + port->sendEvent(e); + } + for (int i = lowerBound; i < upperBound; i++) { + muse::midi::Event e(muse::midi::Event::Opcode::ControlChange, muse::midi::Event::MessageType::ChannelVoice20); + e.setChannel(i); + e.setIndex(midi::SUSTAIN_PEDAL_CONTROLLER); + e.setData(0); + port->sendEvent(e); + } + for (int i = lowerBound; i < upperBound; i++) { + muse::midi::Event e(muse::midi::Event::Opcode::PitchBend, muse::midi::Event::MessageType::ChannelVoice20); + e.setChannel(i); + e.setData(0x80000000); + port->sendEvent(e); + } + } } bool FluidSynth::handleEvent(const midi::Event& event) @@ -167,7 +209,8 @@ bool FluidSynth::handleEvent(const midi::Event& event) int ret = FLUID_OK; switch (event.opcode()) { case Event::Opcode::NoteOn: { - ret = fluid_synth_noteon(m_fluid->synth, event.channel(), event.note(), event.velocity()); + // fluid_synth_noteon expects 0...127 + ret = fluid_synth_noteon(m_fluid->synth, event.channel(), event.note(), event.velocity7()); m_tuning.add(event.note(), event.pitchTuningCents()); } break; case Event::Opcode::NoteOff: { @@ -176,16 +219,16 @@ bool FluidSynth::handleEvent(const midi::Event& event) } break; case Event::Opcode::ControlChange: { if (event.index() == muse::midi::EXPRESSION_CONTROLLER) { - ret = setExpressionLevel(event.data()); + ret = setExpressionLevel(event.data7()); } else { - ret = setControllerValue(event.channel(), event.index(), event.data()); + ret = setControllerValue(event.channel(), event.index(), event.data7()); } } break; case Event::Opcode::ProgramChange: { fluid_synth_program_change(m_fluid->synth, event.channel(), event.program()); } break; case Event::Opcode::PitchBend: { - ret = setPitchBend(event.channel(), event.data()); + ret = setPitchBend(event.channel(), event.pitchBend14()); } break; default: { LOGD() << "not supported event type: " << event.opcodeString(); @@ -193,7 +236,17 @@ bool FluidSynth::handleEvent(const midi::Event& event) } } - midiOutPort()->sendEvent(event); + if (STAFF_TO_MIDIOUT_CHANNEL && event.isChannelVoice()) { + int staff = m_sequencer.lastStaff(); + if (staff >= 0) { + int channel = staff % 16; + midi::Event me(event); + me.setChannel(channel); + midiOutPort()->sendEvent(me); + } + } else { + midiOutPort()->sendEvent(event); + } return ret == FLUID_OK; } diff --git a/src/framework/midi/internal/platform/osx/coremidiinport.cpp b/src/framework/midi/internal/platform/osx/coremidiinport.cpp index 7108276c0d1bb..01861dcaf309d 100644 --- a/src/framework/midi/internal/platform/osx/coremidiinport.cpp +++ b/src/framework/midi/internal/platform/osx/coremidiinport.cpp @@ -35,6 +35,15 @@ using namespace muse; using namespace muse::midi; +//#define DEBUG_COREMIDIINPORT +#ifdef DEBUG_COREMIDIINPORT +#define LOG_MIDI_D LOGD +#define LOG_MIDI_W LOGW +#else +#define LOG_MIDI_D LOGN +#define LOG_MIDI_W LOGN +#endif + struct muse::midi::CoreMidiInPort::Core { MIDIClientRef client = 0; MIDIPortRef inputPort = 0; @@ -198,34 +207,40 @@ void CoreMidiInPort::initCore() // Document Version 1.1.2 // Draft Date 2023-10-27 // Published 2023-11-10 - const uint32_t message_type_to_size_in_byte[] = { 1, // 0x0 - 1, // 0x1 - 1, // 0x2 - 2, // 0x3 - 2, // 0x4 - 4, // 0x5 - 1, // 0x6 - 1, // 0x7 - 2, // 0x8 - 2, // 0x9 - 2, // 0xA - 3, // 0xB - 3, // 0xC - 4, // 0xD - 4, // 0xE - 4 };// 0xF + // Section 2.1.4 + const uint32_t message_type_to_size_in_words[] = { + 1, // 0x0 Utility + 1, // 0x1 SystemRealTime + 1, // 0x2 ChannelVoice10 + 2, // 0x3 SystemExclusiveData + 2, // 0x4 ChannelVoice20 + 4, // 0x5 Data + 1, // 0x6 Reserved + 1, // 0x7 Reserved + 2, // 0x8 Reserved + 2, // 0x9 Reserved + 2, // 0xA Reserved + 3, // 0xB Reserved + 3, // 0xC Reserved + 4, // 0xD Reserved + 4, // 0xE Reserved + 4 // 0xF Reserved + }; + const MIDIEventPacket* packet = eventList->packet; for (UInt32 index = 0; index < eventList->numPackets; index++) { - LOGD() << "midi packet size " << packet->wordCount << " bytes"; + LOG_MIDI_D() << "Receiving MIDIEventPacket with " << packet->wordCount << " words"; // Handle packet uint32_t pos = 0; while (pos < packet->wordCount) { uint32_t most_significant_4_bit = packet->words[pos] >> 28; - uint32_t message_size = message_type_to_size_in_byte[most_significant_4_bit]; + assert(most_significant_4_bit < 6); - LOGD() << "midi message size " << message_size << " bytes"; + uint32_t message_size = message_type_to_size_in_words[most_significant_4_bit]; + LOG_MIDI_D() << "Receiving midi message with " << message_size << " words"; Event e = Event::fromRawData(&packet->words[pos], message_size); if (e) { + LOG_MIDI_D() << "Received midi message:" << e.to_string(); m_eventReceived.send((tick_t)packet->timeStamp, e); } pos += message_size; @@ -241,18 +256,30 @@ void CoreMidiInPort::initCore() { const MIDIPacket* packet = packetList->packet; for (UInt32 index = 0; index < packetList->numPackets; index++) { - if (packet->length != 0 && packet->length <= 4) { - uint32_t message(0); - memcpy(&message, packet->data, std::min(sizeof(message), sizeof(char) * packet->length)); - - auto e = Event::fromMIDI10Package(message).toMIDI20(); + auto len = packet->length; + int pos = 0; + const Byte* pointer = static_cast(&(packet->data[0])); + while (pos < len) { + Byte status = pointer[pos] >> 4; + if (status < 8 || status >= 15) { + LOG_MIDI_W() << "Unhandled status byte:" << status; + return; + } + Event::Opcode opcode = static_cast(status); + int msgLen = Event::midi10ByteCountForOpcode(opcode); + if (msgLen == 0) { + LOG_MIDI_W() << "Unhandled opcode:" << status; + return; + } + Event e = Event::fromMIDI10BytePackage(pointer + pos, msgLen); + LOG_MIDI_D() << "Received midi 1.0 message:" << e.to_string(); + e = e.toMIDI20(); if (e) { + LOG_MIDI_D() << "Converted to midi 2.0 midi message:" << e.to_string(); m_eventReceived.send((tick_t)packet->timeStamp, e); } - } else if (packet->length > 4) { - LOGW() << "unsupported midi message size " << packet->length << " bytes"; + pos += msgLen; } - packet = MIDIPacketNext(packet); } }; diff --git a/src/framework/midi/internal/platform/osx/coremidioutport.cpp b/src/framework/midi/internal/platform/osx/coremidioutport.cpp index 36202a7536da0..073b2aacc3bdf 100644 --- a/src/framework/midi/internal/platform/osx/coremidioutport.cpp +++ b/src/framework/midi/internal/platform/osx/coremidioutport.cpp @@ -35,11 +35,19 @@ using namespace muse; using namespace muse::midi; +//#define DEBUG_COREMIDIOUTPORT +#ifdef DEBUG_COREMIDIOUTPORT +#define LOG_MIDI_D LOGD +#define LOG_MIDI_W LOGW +#else +#define LOG_MIDI_D LOGN +#define LOG_MIDI_W LOGN +#endif + struct muse::midi::CoreMidiOutPort::Core { MIDIClientRef client = 0; MIDIPortRef outputPort = 0; MIDIEndpointRef destinationId = 0; - MIDIProtocolID destinationProtocolId = kMIDIProtocol_1_0; int deviceID = -1; }; @@ -72,15 +80,6 @@ void CoreMidiOutPort::deinit() } } -void CoreMidiOutPort::getDestinationProtocolId() -{ - if (__builtin_available(macOS 11.0, *)) { - SInt32 protocol = 0; - OSStatus err = MIDIObjectGetIntegerProperty(m_core->destinationId, kMIDIPropertyProtocolID, &protocol); - m_core->destinationProtocolId = err == noErr ? (MIDIProtocolID)protocol : kMIDIProtocol_1_0; - } -} - void CoreMidiOutPort::initCore() { OSStatus result; @@ -132,11 +131,6 @@ void CoreMidiOutPort::initCore() if (CFStringCompare(propertyChangeNotification->propertyName, kMIDIPropertyDisplayName, 0) == kCFCompareEqualTo || CFStringCompare(propertyChangeNotification->propertyName, kMIDIPropertyName, 0) == kCFCompareEqualTo) { self->availableDevicesChanged().notify(); - } else if (__builtin_available(macOS 11.0, *)) { - if (CFStringCompare(propertyChangeNotification->propertyName, kMIDIPropertyProtocolID, 0) == kCFCompareEqualTo - && self->isConnected() && propertyChangeNotification->object == self->m_core->destinationId) { - self->getDestinationProtocolId(); - } } } break; @@ -238,8 +232,6 @@ Ret CoreMidiOutPort::connect(const MidiDeviceID& deviceID) m_core->deviceID = std::stoi(deviceID); m_core->destinationId = (MIDIEndpointRef)obj; - - getDestinationProtocolId(); } m_deviceID = deviceID; @@ -289,18 +281,6 @@ bool CoreMidiOutPort::supportsMIDI20Output() const return false; } -static ByteCount packetListSize(const std::vector& events) -{ - if (events.empty()) { - return 0; - } - - // TODO: should be dynamic per type of event - constexpr size_t eventSize = sizeof(Event().to_MIDI10Package()); - - return offsetof(MIDIPacketList, packet) + events.size() * (offsetof(MIDIPacket, data) + eventSize); -} - Ret CoreMidiOutPort::sendEvent(const Event& e) { if (!isConnected()) { @@ -310,34 +290,54 @@ Ret CoreMidiOutPort::sendEvent(const Event& e) OSStatus result; MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); - if (__builtin_available(macOS 11.0, *)) { - MIDIProtocolID protocolId = configuration()->useMIDI20Output() ? m_core->destinationProtocolId : kMIDIProtocol_1_0; + // Note: there could be three cases: MIDI2+MIDIEventList, MIDI1+MIDIEventList, MIDI1+MIDIPacketList. + // But we only maintain 2 code paths and may drop configuration()->useMIDI20Output() and MIDIPacketList later. + + if (supportsMIDI20Output() && configuration()->useMIDI20Output()) { + if (__builtin_available(macOS 11.0, *)) { + MIDIProtocolID protocolId = kMIDIProtocol_2_0; + + ByteCount wordCount = e.midi20WordCount(); + if (wordCount == 0) { + LOG_MIDI_W() << "Failed to send message for event: " << e.to_string(); + return make_ret(Err::MidiSendError, "failed send message. unknown word count"); + } + LOG_MIDI_D() << "Sending MIDIEventList event: " << e.to_string(); - MIDIEventList eventList; - MIDIEventPacket* packet = MIDIEventListInit(&eventList, protocolId); - // TODO: Replace '4' with something specific for the type of element? - MIDIEventListAdd(&eventList, sizeof(eventList), packet, timeStamp, 4, e.rawData()); + MIDIEventList eventList; + MIDIEventPacket* packet = MIDIEventListInit(&eventList, protocolId); - result = MIDISendEventList(m_core->outputPort, m_core->destinationId, &eventList); + if (e.messageType() == Event::MessageType::ChannelVoice20 && e.opcode() == muse::midi::Event::Opcode::NoteOn + && e.velocity() < 128) { + LOG_MIDI_W() << "Detected low MIDI 2.0 ChannelVoiceMessage velocity."; + } + + MIDIEventListAdd(&eventList, sizeof(eventList), packet, timeStamp, wordCount, e.rawData()); + + result = MIDISendEventList(m_core->outputPort, m_core->destinationId, &eventList); + } } else { const std::vector events = e.toMIDI10(); - ByteCount packetListSize = ::packetListSize(events); - if (packetListSize == 0) { + if (events.empty()) { return make_ret(Err::MidiSendError, "midi 1.0 messages list was empty"); } MIDIPacketList packetList; + // packetList has one packet of 256 bytes by default, which is more than enough for one midi 2.0 event. MIDIPacket* packet = MIDIPacketListInit(&packetList); + ByteCount packetListSize = sizeof(packetList); + Byte bytesPackage[4]; for (const Event& event : events) { - uint32_t msg = event.to_MIDI10Package(); - - if (!msg) { - return make_ret(Err::MidiSendError, "message wasn't converted"); + int length = event.to_MIDI10BytesPackage(bytesPackage); + assert(length <= 3); + if (length == 0) { + LOG_MIDI_W() << "Failed Sending MIDIPacketList event: " << event.to_string(); + } else { + LOG_MIDI_D() << "Sending MIDIPacketList event: " << event.to_string(); + packet = MIDIPacketListAdd(&packetList, packetListSize, packet, timeStamp, length, bytesPackage); } - - packet = MIDIPacketListAdd(&packetList, packetListSize, packet, timeStamp, sizeof(msg), reinterpret_cast(&msg)); } result = MIDISend(m_core->outputPort, m_core->destinationId, &packetList); diff --git a/src/framework/midi/internal/platform/osx/coremidioutport.h b/src/framework/midi/internal/platform/osx/coremidioutport.h index 5f5d27e22b52b..fddb815d1cfa6 100644 --- a/src/framework/midi/internal/platform/osx/coremidioutport.h +++ b/src/framework/midi/internal/platform/osx/coremidioutport.h @@ -56,7 +56,6 @@ class CoreMidiOutPort : public IMidiOutPort private: void initCore(); - void getDestinationProtocolId(); struct Core; std::unique_ptr m_core; diff --git a/src/framework/midi/midievent.h b/src/framework/midi/midievent.h index 2d9e867c36140..e6e72511e7394 100644 --- a/src/framework/midi/midievent.h +++ b/src/framework/midi/midievent.h @@ -140,6 +140,35 @@ struct Event { return 0; } + static Event fromMIDI10BytePackage(const unsigned char* pointer, int length) + { + Event e; + uint32_t val = 0; + assert(length <= 3); + for (int i=0; i < length; i++) { + uint32_t byteVal = static_cast(pointer[i]); + val |= byteVal << ((2 - i) * 8); + } + e.m_data[0]=val; + e.setMessageType(MessageType::ChannelVoice10); + return e; + } + + int to_MIDI10BytesPackage(unsigned char* pointer) const + { + if (messageType() == MessageType::ChannelVoice10) { + auto val = m_data[0]; + int c = Event::midi10ByteCountForOpcode(opcode()); + assert(c <= 3); + for (int i=0; i < c; i++) { + auto byteVal = (val >> ((2 - i) * 8)) & 0xff; + pointer[i]=static_cast(byteVal); + } + return c; + } + return 0; + } + static Event fromRawData(const uint32_t* data, size_t count) { Event e; @@ -666,17 +695,18 @@ struct Event { basic10Event.setGroup(group()); switch (opcode()) { //D2.1 + case Opcode::PolyPressure: + case Opcode::ControlChange: case Opcode::NoteOn: case Opcode::NoteOff: { auto e = basic10Event; - auto v = scaleDown(velocity(), 16, 7); - e.setNote(note()); - if (v != 0) { - e.setVelocity(static_cast(v)); - } else { + auto v1 = (m_data[0] & 0x7F00); + auto v2 = m_data[1] >> 25; + if (opcode() == Opcode::NoteOn && v2 == 0) { //4.2.2 velocity comment - e.setVelocity(1); + v2 = 1; } + e.m_data[0] |= v1 | v2; events.push_back(e); break; } @@ -695,8 +725,8 @@ struct Event { std::vector > controlChanges = { { (opcode() == Opcode::RegisteredController ? 101 : 99), bank() }, { (opcode() == Opcode::RegisteredController ? 100 : 98), index() }, - { 6, (data() & 0x7FFFFFFF) >> 24 }, - { 38, (data() & 0x1FC0000) >> 18 } + { 6, data() >> 25 }, // first 7 bits + { 38, (data() & 0x1FC0000) >> 18 } // second 7 bits }; for (auto& c : controlChanges) { auto e = basic10Event; @@ -708,16 +738,16 @@ struct Event { break; } - //D.4 + //D2.4 case Opcode::ProgramChange: { if (isBankValid()) { auto e = basic10Event; e.setOpcode(Opcode::ControlChange); e.setIndex(0); - e.setData((bank() & 0x7F00) >> 8); + e.setData((m_data[1] & 0x7F00) >> 8); events.push_back(e); e.setIndex(0); - e.setData(bank() & 0x7F); + e.setData(m_data[1] & 0x7F); events.push_back(e); } auto e = basic10Event; @@ -728,7 +758,7 @@ struct Event { //D2.5 case Opcode::PitchBend: { auto e = basic10Event; - e.setData(data()); + e.setData(pitchBend14()); events.push_back(e); break; } @@ -978,6 +1008,96 @@ struct Event { return str; } + uint8_t velocity7() const + { + uint16_t val = velocity(); + if (isChannelVoice20()) { + return static_cast(scaleDown(val, 16, 7)); + } + return static_cast(val); + } + + void setVelocity7(uint8_t value) + { + if (isChannelVoice20()) { + uint16_t scaled = scaleUp(value, 7, 16); + setVelocity(scaled); + return; + } + setVelocity(value); + } + + uint8_t data7() const + { + uint32_t val = data(); + if (messageType() == MessageType::ChannelVoice20) { + return scaleDown(val, 32, 7); + } + return val; + } + + uint32_t data14() const + { + uint32_t val = data(); + if (messageType() == MessageType::ChannelVoice20) { + return scaleDown(val, 32, 14); + } + return val; + } + + uint32_t pitchBend14() const + { + assert(isChannelVoice() && opcode() == Opcode::PitchBend); + return data14(); + } + + int midi20WordCount() const + { + return Event::wordCountForMessageType(messageType()); + } + + static int wordCountForMessageType(MessageType messageType) + { + switch (messageType) { + case MessageType::Utility: return 1; + case MessageType::SystemRealTime: return 1; + case MessageType::ChannelVoice10: return 1; + case MessageType::SystemExclusiveData: return 2; + case MessageType::ChannelVoice20: return 2; + case MessageType::Data: return 4; + } + return 0; // reserved + } + + static int midi10ByteCountForOpcode(Opcode opcode) + { + switch (opcode) { + case Opcode::RegisteredPerNoteController: + case Opcode::AssignablePerNoteController: + case Opcode::RelativeRegisteredController: + case Opcode::RelativeAssignableController: + case Opcode::PerNotePitchBend: + case Opcode::PerNoteManagement: + //D2.8 cannot be translated to Midi 1.0 + assert(false); + case Opcode::RegisteredController: + case Opcode::AssignableController: + // Already translated to a sequence of CCs. + assert(false); + case Opcode::NoteOff: + case Opcode::NoteOn: + case Opcode::PolyPressure: + case Opcode::ControlChange: + case Opcode::PitchBend: + return 3; + case Opcode::ProgramChange: + case Opcode::ChannelPressure: + return 2; + } + assert(false); + return 0; + } + private: //!Note Temporarily disabled until the end of the investigation, looks like we're not supporting some 'custom' messages from MU3 //! v.pereverzev@wsmgroup.ru