From 0b89d574c50a42fc5a07f812e7ff5760e201d30b Mon Sep 17 00:00:00 2001 From: Syl Morrison Date: Sun, 23 Nov 2025 23:07:42 +0000 Subject: [PATCH 1/3] handlers for midi event types --- .../core/mostlyharmless_IEngine.h | 7 +++ source/mostlyharmless_PluginBase.cpp | 61 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/include/mostly_harmless/core/mostlyharmless_IEngine.h b/include/mostly_harmless/core/mostlyharmless_IEngine.h index 54fc6b6..70ebd36 100644 --- a/include/mostly_harmless/core/mostlyharmless_IEngine.h +++ b/include/mostly_harmless/core/mostlyharmless_IEngine.h @@ -98,6 +98,13 @@ namespace mostly_harmless::core { * \param velocity The 0-1 velocity of the note event */ virtual void handleNoteOff([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t note, [[maybe_unused]] double velocity) {} + + // TODO: Document + virtual void handleControlChange([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t controlNumber, [[maybe_unused]] std::uint8_t data) {} + virtual void handleProgramChange([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t programNumber) {}; + virtual void handlePolyAftertouch([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t note, [[maybe_unused]] std::uint8_t pressure) {} + virtual void handleChannelAftertouch([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t pressure) {} + virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint16_t value) {} }; } // namespace mostly_harmless::core #endif // MOSTLYHARMLESS_MOSTLYHARMLESS_IENGINE_H diff --git a/source/mostlyharmless_PluginBase.cpp b/source/mostlyharmless_PluginBase.cpp index 12d900f..073d071 100644 --- a/source/mostlyharmless_PluginBase.cpp +++ b/source/mostlyharmless_PluginBase.cpp @@ -8,6 +8,15 @@ #include namespace mostly_harmless::internal { + namespace midi_headers { + constexpr static auto s_note_off{ 0x80 }; + constexpr static auto s_note_on{ 0x90 }; + constexpr static auto s_poly_aftertouch{ 0xA0 }; + constexpr static auto s_control_change{ 0xB0 }; + constexpr static auto s_program_change{ 0xC0 }; + constexpr static auto s_channel_aftertouch{ 0xD0 }; + constexpr static auto s_pitch_wheel{ 0xE0 }; + } // namespace midi_headers PluginBase::PluginBase(const clap_host* host) : clap::helpers::Plugin(&getDescriptor(), host) { MH_LOG("PROC: Creating plugin instance..."); m_pluginEntry = core::createPluginEntry(); @@ -168,21 +177,53 @@ namespace mostly_harmless::internal { // 0PPP PPPP // 0VVV VVVV const auto* midiEvent = reinterpret_cast(event); - std::uint8_t message = midiEvent->data[0] & 0xF0; // SSSS 0000 - Message will be << 4 - std::uint8_t channel = midiEvent->data[0] & 0x0F; // 0000 CCCC - std::uint8_t note = midiEvent->data[1]; // 0PPP PPPP - std::uint8_t velocity = midiEvent->data[2]; // 0VVV VVVV - const auto fpVelocity = static_cast(velocity) / 127.0; + const auto handle_note_on_or_off = [this](const clap_event_midi* ev, bool on) -> void { + }; + const std::uint8_t message = midiEvent->data[0] & 0xF0; // SSSS 0000 - Message will be << 4 + const std::uint8_t channel = midiEvent->data[0] & 0x0F; // 0000 CCCC switch (message) { - case 0x90: { - m_engine->handleNoteOn(midiEvent->port_index, channel, note, fpVelocity); + case midi_headers::s_note_on: [[fallthrough]]; + case midi_headers::s_note_off: { + const std::uint8_t note = midiEvent->data[1]; // 0PPP PPPP + const std::uint8_t velocity = midiEvent->data[2]; // 0VVV VVVV + const auto fpVelocity = static_cast(velocity) / 127.0; + if (message == midi_headers::s_note_on) { + m_engine->handleNoteOn(midiEvent->port_index, channel, note, fpVelocity); + } else { + m_engine->handleNoteOff(midiEvent->port_index, channel, note, fpVelocity); + } + break; + } + case midi_headers::s_poly_aftertouch: { + const std::uint8_t note = midiEvent->data[1]; + const std::uint8_t pressure = midiEvent->data[2]; + m_engine->handlePolyAftertouch(midiEvent->port_index, channel, note, pressure); + break; + } + case midi_headers::s_control_change: { + const std::uint8_t controllerNumber = midiEvent->data[1]; + const std::uint8_t data = midiEvent->data[2]; + m_engine->handleControlChange(midiEvent->port_index, channel, controllerNumber, data); + break; + } + case midi_headers::s_program_change: { + const std::uint8_t programNumber = midiEvent->data[1]; + m_engine->handleProgramChange(midiEvent->port_index, channel, programNumber); + break; + } + case midi_headers::s_channel_aftertouch: { + const std::uint8_t pressure = midiEvent->data[1]; + m_engine->handleChannelAftertouch(midiEvent->port_index, channel, pressure); break; } - case 0x80: { - m_engine->handleNoteOff(midiEvent->port_index, channel, note, fpVelocity); + case midi_headers::s_pitch_wheel: { + const std::uint8_t lsb = midiEvent->data[1]; + const std::uint8_t msb = midiEvent->data[2]; + const std::uint16_t combined = lsb & (msb << 7); + m_engine->handlePitchWheel(midiEvent->port_index, channel, combined); break; } - default: break; // TODO + default: break; } break; } From 4c30695fa1835f0b4371c64a0451519845a2ae3c Mon Sep 17 00:00:00 2001 From: Syl Morrison Date: Sun, 23 Nov 2025 23:20:21 +0000 Subject: [PATCH 2/3] document new midi related functions --- .../core/mostlyharmless_IEngine.h | 46 ++++++++++++++++++- source/mostlyharmless_PluginBase.cpp | 2 - 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/include/mostly_harmless/core/mostlyharmless_IEngine.h b/include/mostly_harmless/core/mostlyharmless_IEngine.h index 70ebd36..4415914 100644 --- a/include/mostly_harmless/core/mostlyharmless_IEngine.h +++ b/include/mostly_harmless/core/mostlyharmless_IEngine.h @@ -99,11 +99,55 @@ namespace mostly_harmless::core { */ virtual void handleNoteOff([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t note, [[maybe_unused]] double velocity) {} - // TODO: Document + /** + * Called if the plugin receives a midi control change event - not pure virtual, as this function isn't relevant if you haven't requested midi functionality. + * Called on the audio thread, in response to a control change event. + * Some of the identifiers here are reserved for special controls (mod wheel etc) - we don't do any handling of this framework side, so have a google if you need this info! + * @param portIndex The clap port index the event originated from. + * @param channel The midi channel the event was passed to + * @param controlNumber The midi control that was changed + * @param data The data in the midi message - see the midi spec for interpreting this. + */ virtual void handleControlChange([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t controlNumber, [[maybe_unused]] std::uint8_t data) {} + + /** + * Called if the plugin receives a program change event - not pure virtual, as this function isn't relevant if you haven't requested midi functionality. + * Called on the audio thread, in response to a program change event. + * @param portIndex The clap port index the event originated from. + * @param channel The midi channel the event was passed to + * @param programNumber The program number that was set + */ virtual void handleProgramChange([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t programNumber) {}; + + /** + * Called if the plugin receives a (polyphonic) aftertouch event (polyphonic in the sense that each note can have its own pressure data). + * Not pure virtual, as this function isn't relevant if you haven't requested midi functionality. + * Called on the audio thread, in response to a poly aftertouch event. + * @param portIndex The clap port index the event originated from. + * @param channel The midi channel the event was passed to + * @param note The midi note this aftertouch event applies to + * @param pressure The pressure applied to this midi note + */ virtual void handlePolyAftertouch([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t note, [[maybe_unused]] std::uint8_t pressure) {} + + /** + * Called if the plugin receives a (channel-wide) aftertouch event. In this case, all notes within a channel get the same pressure value. + * Not pure virtual, as this function isn't relevant if you haven't requested midi functionality. + * Called on the audio thread, in response to a channel aftertouch event. + * @param portIndex The clap port index the event originated from. + * @param channel The midi channel the event was passed to + * @param pressure The pressure applied to this midi note + */ virtual void handleChannelAftertouch([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t pressure) {} + + /** + * Called if the plugin receives a pitch wheel event - not pure virtual, as this function isn't relevant if you haven't requested midi functionality. + * Value here is a 14-bit value representing the pitch wheel's position, in the range 0x0 to 0x3FFF. Generally, 0x2000 is treated as the centre, negative pitch wheel pos is below 0x2000, and positive is above etc etc etc. + * Called on the audio thread, in response to a pitch wheel event. + * @param portIndex The clap port index the event originated from. + * @param channel The midi channel the event was passed to + * @param value The 14-bit value representing pitch wheel pos between 0x0 and 0x3FFF. + */ virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint16_t value) {} }; } // namespace mostly_harmless::core diff --git a/source/mostlyharmless_PluginBase.cpp b/source/mostlyharmless_PluginBase.cpp index 073d071..17a2399 100644 --- a/source/mostlyharmless_PluginBase.cpp +++ b/source/mostlyharmless_PluginBase.cpp @@ -177,8 +177,6 @@ namespace mostly_harmless::internal { // 0PPP PPPP // 0VVV VVVV const auto* midiEvent = reinterpret_cast(event); - const auto handle_note_on_or_off = [this](const clap_event_midi* ev, bool on) -> void { - }; const std::uint8_t message = midiEvent->data[0] & 0xF0; // SSSS 0000 - Message will be << 4 const std::uint8_t channel = midiEvent->data[0] & 0x0F; // 0000 CCCC switch (message) { From 9d9dc864272ce86c315b67b066bd88618ccd9c33 Mon Sep 17 00:00:00 2001 From: Syl Morrison Date: Sat, 29 Nov 2025 15:00:21 +0000 Subject: [PATCH 3/3] Midi unit tests, and refactor internal handling to std::variant --- include/mostly_harmless/CMakeLists.txt | 2 + .../core/mostlyharmless_IEngine.h | 5 +- .../events/mostlyharmless_MidiEvent.h | 60 ++++++++++++ .../utils/mostlyharmless_Visitor.h | 26 ++++++ source/CMakeLists.txt | 1 + source/events/mostlyharmless_MidiEvent.cpp | 38 ++++++++ source/mostlyharmless_PluginBase.cpp | 85 ++++++----------- tests/CMakeLists.txt | 1 + .../mostlyharmless_DatabaseStateTests.cpp | 3 +- .../events/mostlyharmless_MidiEventTests.cpp | 91 +++++++++++++++++++ 10 files changed, 252 insertions(+), 60 deletions(-) create mode 100644 include/mostly_harmless/events/mostlyharmless_MidiEvent.h create mode 100644 include/mostly_harmless/utils/mostlyharmless_Visitor.h create mode 100644 source/events/mostlyharmless_MidiEvent.cpp create mode 100644 tests/events/mostlyharmless_MidiEventTests.cpp diff --git a/include/mostly_harmless/CMakeLists.txt b/include/mostly_harmless/CMakeLists.txt index 1ec8912..1efffb8 100644 --- a/include/mostly_harmless/CMakeLists.txt +++ b/include/mostly_harmless/CMakeLists.txt @@ -15,6 +15,7 @@ set(MOSTLYHARMLESS_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContext.h ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_WebEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_ParamEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/mostlyharmless_Parameters.h ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewBase.h ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.h @@ -27,6 +28,7 @@ set(MOSTLYHARMLESS_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Proxy.h ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_NoDenormals.h ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Logging.h + ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Visitor.h ${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabaseState.h ${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabasePropertyWatcher.h ${PLATFORM_HEADERS} diff --git a/include/mostly_harmless/core/mostlyharmless_IEngine.h b/include/mostly_harmless/core/mostlyharmless_IEngine.h index 4415914..3fd9d81 100644 --- a/include/mostly_harmless/core/mostlyharmless_IEngine.h +++ b/include/mostly_harmless/core/mostlyharmless_IEngine.h @@ -142,13 +142,12 @@ namespace mostly_harmless::core { /** * Called if the plugin receives a pitch wheel event - not pure virtual, as this function isn't relevant if you haven't requested midi functionality. - * Value here is a 14-bit value representing the pitch wheel's position, in the range 0x0 to 0x3FFF. Generally, 0x2000 is treated as the centre, negative pitch wheel pos is below 0x2000, and positive is above etc etc etc. * Called on the audio thread, in response to a pitch wheel event. * @param portIndex The clap port index the event originated from. * @param channel The midi channel the event was passed to - * @param value The 14-bit value representing pitch wheel pos between 0x0 and 0x3FFF. + * @param value The value, between -1.0 and 1.0 */ - virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint16_t value) {} + virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] double value) {} }; } // namespace mostly_harmless::core #endif // MOSTLYHARMLESS_MOSTLYHARMLESS_IENGINE_H diff --git a/include/mostly_harmless/events/mostlyharmless_MidiEvent.h b/include/mostly_harmless/events/mostlyharmless_MidiEvent.h new file mode 100644 index 0000000..78bdd72 --- /dev/null +++ b/include/mostly_harmless/events/mostlyharmless_MidiEvent.h @@ -0,0 +1,60 @@ +#ifndef MOSTLY_HARMLESS_MIDI_EVENT +#define MOSTLY_HARMLESS_MIDI_EVENT +#include +#include +/// @internal +namespace mostly_harmless::events::midi { + /// @internal + struct NoteOn final { + std::uint8_t channel; + std::uint8_t note; + double velocity; + }; + + /// @internal + struct NoteOff final { + std::uint8_t channel; + std::uint8_t note; + double velocity; + }; + + /// @internal + struct PolyAftertouch final { + std::uint8_t channel; + std::uint8_t note; + std::uint8_t pressure; + }; + + /// @internal + struct ControlChange final { + std::uint8_t channel; + std::uint8_t controllerNumber; + std::uint8_t data; + }; + + /// @internal + struct ProgramChange final { + std::uint8_t channel; + std::uint8_t programNumber; + }; + + /// @internal + struct ChannelAftertouch final { + std::uint8_t channel; + std::uint8_t pressure; + }; + + /// @internal + struct PitchWheel final { + std::uint8_t channel; + double value; + }; + + /// @internal + using MidiEvent = std::variant; + + /// @internal + auto parse(std::uint8_t b0, std::uint8_t b1, std::uint8_t b2) -> std::optional; +} // namespace mostly_harmless::events::midi + +#endif \ No newline at end of file diff --git a/include/mostly_harmless/utils/mostlyharmless_Visitor.h b/include/mostly_harmless/utils/mostlyharmless_Visitor.h new file mode 100644 index 0000000..3e4cb41 --- /dev/null +++ b/include/mostly_harmless/utils/mostlyharmless_Visitor.h @@ -0,0 +1,26 @@ +// +// Created by Syl Morrison on 29/11/2025. +// + +#ifndef MOSTLYHARMLESS_VISITOR_H +#define MOSTLYHARMLESS_VISITOR_H +namespace mostly_harmless::utils { + /** + * \brief Util for use with `std::visit` to avoid a bunch of `if constexpr(...)`s. + * + * Usage:: + * ``` + * std::variant data; + * std::visit(Visitor{ + * [](int x) { std::cout << "Was int!\n"; }, + * [](double x) { std::cout << "Was double!\n"; } + * }, data); + * ``` + * @tparam Callable + */ + template + struct Visitor : Callable... { + using Callable::operator()...; + }; +} // namespace mostly_harmless::utils +#endif // GLEO_MOSTLYHARMLESS_VISITOR_H diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 7b835c1..d889b6c 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -23,6 +23,7 @@ set(MOSTLYHARMLESS_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContext.cpp ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_ParamEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_WebEvent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewBase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Colour.cpp diff --git a/source/events/mostlyharmless_MidiEvent.cpp b/source/events/mostlyharmless_MidiEvent.cpp new file mode 100644 index 0000000..cd9fd03 --- /dev/null +++ b/source/events/mostlyharmless_MidiEvent.cpp @@ -0,0 +1,38 @@ +#include +namespace mostly_harmless::events::midi { + constexpr static auto s_note_off{ 0x80 }; + constexpr static auto s_note_on{ 0x90 }; + constexpr static auto s_poly_aftertouch{ 0xA0 }; + constexpr static auto s_control_change{ 0xB0 }; + constexpr static auto s_program_change{ 0xC0 }; + constexpr static auto s_channel_aftertouch{ 0xD0 }; + constexpr static auto s_pitch_wheel{ 0xE0 }; + + auto parse(std::uint8_t b0, std::uint8_t b1, std::uint8_t b2) -> std::optional { + const std::uint8_t message = b0 & 0xF0; + const std::uint8_t channel = b0 & 0x0F; + switch (message) { + case s_note_on: [[fallthrough]]; + case s_note_off: { + const std::uint8_t note = b1; + const std::uint8_t velocity = b2; + const auto floatVel = static_cast(velocity) / 127.0; + if (message == s_note_on && velocity != 0) { + return NoteOn{ .channel = channel, .note = note, .velocity = floatVel }; + } + return NoteOff{ .channel = channel, .note = note, .velocity = floatVel }; + } + case s_poly_aftertouch: return PolyAftertouch{ .channel = channel, .note = b1, .pressure = b2 }; + case s_control_change: return ControlChange{ .channel = channel, .controllerNumber = b1, .data = b2 }; + case s_program_change: return ProgramChange{ .channel = channel, .programNumber = b1 }; + case s_channel_aftertouch: return ChannelAftertouch{ .channel = channel, .pressure = b1 }; + case s_pitch_wheel: { + const std::int16_t combined = b1 | (b2 << 7); + double res = static_cast(combined - 8192) / 8192.0; + return PitchWheel{ .channel = channel, .value = res }; + } + default: return {}; + } + } + +} // namespace mostly_harmless::events::midi \ No newline at end of file diff --git a/source/mostlyharmless_PluginBase.cpp b/source/mostlyharmless_PluginBase.cpp index 17a2399..e21a1c9 100644 --- a/source/mostlyharmless_PluginBase.cpp +++ b/source/mostlyharmless_PluginBase.cpp @@ -1,22 +1,17 @@ // // Created by Syl Morrison on 20/10/2024. // +#include "mostly_harmless/utils/mostlyharmless_Visitor.h" + + #include #include #include #include +#include #include namespace mostly_harmless::internal { - namespace midi_headers { - constexpr static auto s_note_off{ 0x80 }; - constexpr static auto s_note_on{ 0x90 }; - constexpr static auto s_poly_aftertouch{ 0xA0 }; - constexpr static auto s_control_change{ 0xB0 }; - constexpr static auto s_program_change{ 0xC0 }; - constexpr static auto s_channel_aftertouch{ 0xD0 }; - constexpr static auto s_pitch_wheel{ 0xE0 }; - } // namespace midi_headers PluginBase::PluginBase(const clap_host* host) : clap::helpers::Plugin(&getDescriptor(), host) { MH_LOG("PROC: Creating plugin instance..."); m_pluginEntry = core::createPluginEntry(); @@ -177,53 +172,33 @@ namespace mostly_harmless::internal { // 0PPP PPPP // 0VVV VVVV const auto* midiEvent = reinterpret_cast(event); - const std::uint8_t message = midiEvent->data[0] & 0xF0; // SSSS 0000 - Message will be << 4 - const std::uint8_t channel = midiEvent->data[0] & 0x0F; // 0000 CCCC - switch (message) { - case midi_headers::s_note_on: [[fallthrough]]; - case midi_headers::s_note_off: { - const std::uint8_t note = midiEvent->data[1]; // 0PPP PPPP - const std::uint8_t velocity = midiEvent->data[2]; // 0VVV VVVV - const auto fpVelocity = static_cast(velocity) / 127.0; - if (message == midi_headers::s_note_on) { - m_engine->handleNoteOn(midiEvent->port_index, channel, note, fpVelocity); - } else { - m_engine->handleNoteOff(midiEvent->port_index, channel, note, fpVelocity); - } - break; - } - case midi_headers::s_poly_aftertouch: { - const std::uint8_t note = midiEvent->data[1]; - const std::uint8_t pressure = midiEvent->data[2]; - m_engine->handlePolyAftertouch(midiEvent->port_index, channel, note, pressure); - break; - } - case midi_headers::s_control_change: { - const std::uint8_t controllerNumber = midiEvent->data[1]; - const std::uint8_t data = midiEvent->data[2]; - m_engine->handleControlChange(midiEvent->port_index, channel, controllerNumber, data); - break; - } - case midi_headers::s_program_change: { - const std::uint8_t programNumber = midiEvent->data[1]; - m_engine->handleProgramChange(midiEvent->port_index, channel, programNumber); - break; - } - case midi_headers::s_channel_aftertouch: { - const std::uint8_t pressure = midiEvent->data[1]; - m_engine->handleChannelAftertouch(midiEvent->port_index, channel, pressure); - break; - } - case midi_headers::s_pitch_wheel: { - const std::uint8_t lsb = midiEvent->data[1]; - const std::uint8_t msb = midiEvent->data[2]; - const std::uint16_t combined = lsb & (msb << 7); - m_engine->handlePitchWheel(midiEvent->port_index, channel, combined); - break; - } - default: break; + auto res = mostly_harmless::events::midi::parse(midiEvent->data[0], midiEvent->data[1], midiEvent->data[2]); + if (!res) { + return; } - break; + std::visit(utils::Visitor{ + [this, &midiEvent](events::midi::NoteOff x) { + m_engine->handleNoteOff(midiEvent->port_index, x.channel, x.note, x.velocity); + }, + [this, &midiEvent](events::midi::NoteOn x) { + m_engine->handleNoteOn(midiEvent->port_index, x.channel, x.note, x.velocity); + }, + [this, &midiEvent](events::midi::PolyAftertouch x) { + m_engine->handlePolyAftertouch(midiEvent->port_index, x.channel, x.note, x.pressure); + }, + [this, &midiEvent](events::midi::ControlChange x) { + m_engine->handleControlChange(midiEvent->port_index, x.channel, x.controllerNumber, x.data); + }, + [this, &midiEvent](events::midi::ProgramChange x) { + m_engine->handleProgramChange(midiEvent->port_index, x.channel, x.programNumber); + }, + [this, &midiEvent](events::midi::ChannelAftertouch x) { + m_engine->handleChannelAftertouch(midiEvent->port_index, x.channel, x.pressure); + }, + [this, &midiEvent](events::midi::PitchWheel x) { + m_engine->handlePitchWheel(midiEvent->port_index, x.channel, x.value); + } }, + *res); } case CLAP_EVENT_TRANSPORT: { if (const auto* transportEvent = reinterpret_cast(event)) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4a52f07..ae4ceef 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,4 +5,5 @@ set(MOSTLYHARMLESS_TEST_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TimerTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContextTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabaseStateTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEventTests.cpp PARENT_SCOPE) \ No newline at end of file diff --git a/tests/data/mostlyharmless_DatabaseStateTests.cpp b/tests/data/mostlyharmless_DatabaseStateTests.cpp index 001744d..026f1a6 100644 --- a/tests/data/mostlyharmless_DatabaseStateTests.cpp +++ b/tests/data/mostlyharmless_DatabaseStateTests.cpp @@ -79,8 +79,7 @@ namespace mostly_harmless::testing { } std::filesystem::remove(dbFile); } - - SECTION("Test DatabasePropertyWatcher") { + SECTION("Test DatabasePropertyWatcher") { for (auto i = 0; i < 100; ++i) { { auto databaseOpt = tryCreateDatabase(dbFile, { { "test", 0 } }); diff --git a/tests/events/mostlyharmless_MidiEventTests.cpp b/tests/events/mostlyharmless_MidiEventTests.cpp new file mode 100644 index 0000000..03b7d81 --- /dev/null +++ b/tests/events/mostlyharmless_MidiEventTests.cpp @@ -0,0 +1,91 @@ +// +// Created by Syl Morrison on 29/11/2025. +// +#include +#include +#include +namespace mostly_harmless::testing { + struct MidiMessage { + std::uint8_t b0; + std::uint8_t b1; + std::uint8_t b2; + }; + template + static auto check_result(std::optional to_check) -> void { + REQUIRE(to_check); + REQUIRE(std::holds_alternative(*to_check)); + } + TEST_CASE("Test Midi Status Bytes") { + SECTION("Test NoteOff") { + const MidiMessage msg{ 0b10000000, 36, 0 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, note, velocity] = std::get(*res_opt); + REQUIRE(channel == 0); + REQUIRE(note == 36); + REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(0.0)); + } + SECTION("Test NoteOn") { + const MidiMessage msg{ 0b10010000, 36, 127 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, note, velocity] = std::get(*res_opt); + REQUIRE(channel == 0); + REQUIRE(note == 36); + REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(1.0f)); + } + SECTION("Test PolyAftertouch") { + const MidiMessage msg{ 0b10100001, 60, 127 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, note, pressure] = std::get(*res_opt); + REQUIRE(channel == 1); + REQUIRE(note == 60); + REQUIRE(pressure == 127); + } + SECTION("Test ControlChange") { + const MidiMessage msg{ 0b10110001, 0, 127 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, controllerNumber, value] = std::get(*res_opt); + REQUIRE(channel == 1); + REQUIRE(controllerNumber == 0); + REQUIRE(value == 127); + } + SECTION("Test ProgramChange") { + const MidiMessage msg{ 0b11000000, 2, 0 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, program] = std::get(*res_opt); + REQUIRE(channel == 0); + REQUIRE(program == 2); + } + SECTION("Test ChannelAftertouch") { + const MidiMessage msg{ 0b11010010, 8, 0 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, pressure] = std::get(*res_opt); + REQUIRE(channel == 2); + REQUIRE(pressure == 8); + } + SECTION("Test PitchWheel") { + const MidiMessage msg{ 0b11100000, 0x00, 0x40 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, value] = std::get(*res_opt); + REQUIRE(channel == 0); + REQUIRE_THAT(value, Catch::Matchers::WithinRel(0.0)); + } + + SECTION("Test 0 Velocity Note-On") { + const MidiMessage msg{ 0b10010000, 40, 0 }; + const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2); + check_result(res_opt); + const auto [channel, note, velocity] = std::get(*res_opt); + REQUIRE(channel == 0); + REQUIRE(note == 40); + REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(0.0)); + } + } + +} // namespace mostly_harmless::testing