Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/mostly_harmless/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand Down
50 changes: 50 additions & 0 deletions include/mostly_harmless/core/mostlyharmless_IEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,56 @@ 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) {}

/**
* 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.
* 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 value, between -1.0 and 1.0
*/
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
60 changes: 60 additions & 0 deletions include/mostly_harmless/events/mostlyharmless_MidiEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#ifndef MOSTLY_HARMLESS_MIDI_EVENT
#define MOSTLY_HARMLESS_MIDI_EVENT
#include <variant>
#include <optional>
/// @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<NoteOn, NoteOff, PolyAftertouch, ControlChange, ProgramChange, ChannelAftertouch, PitchWheel>;

/// @internal
auto parse(std::uint8_t b0, std::uint8_t b1, std::uint8_t b2) -> std::optional<MidiEvent>;
} // namespace mostly_harmless::events::midi

#endif
26 changes: 26 additions & 0 deletions include/mostly_harmless/utils/mostlyharmless_Visitor.h
Original file line number Diff line number Diff line change
@@ -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<int, double> data;
* std::visit(Visitor{
* [](int x) { std::cout << "Was int!\n"; },
* [](double x) { std::cout << "Was double!\n"; }
* }, data);
* ```
* @tparam Callable
*/
template <typename... Callable>
struct Visitor : Callable... {
using Callable::operator()...;
};
} // namespace mostly_harmless::utils
#endif // GLEO_MOSTLYHARMLESS_VISITOR_H
1 change: 1 addition & 0 deletions source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions source/events/mostlyharmless_MidiEvent.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <mostly_harmless/events/mostlyharmless_MidiEvent.h>
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<MidiEvent> {
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<double>(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<double>(combined - 8192) / 8192.0;
return PitchWheel{ .channel = channel, .value = res };
}
default: return {};
}
}

} // namespace mostly_harmless::events::midi
46 changes: 30 additions & 16 deletions source/mostlyharmless_PluginBase.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
//
// Created by Syl Morrison on 20/10/2024.
//
#include "mostly_harmless/utils/mostlyharmless_Visitor.h"


#include <mostly_harmless/mostlyharmless_PluginBase.h>
#include <mostly_harmless/utils/mostlyharmless_Macros.h>
#include <mostly_harmless/audio/mostlyharmless_AudioHelpers.h>
#include <mostly_harmless/utils/mostlyharmless_NoDenormals.h>
#include <mostly_harmless/events/mostlyharmless_MidiEvent.h>
#include <clap/helpers/plugin.hxx>

namespace mostly_harmless::internal {
Expand Down Expand Up @@ -168,23 +172,33 @@ namespace mostly_harmless::internal {
// 0PPP PPPP
// 0VVV VVVV
const auto* midiEvent = reinterpret_cast<const clap_event_midi*>(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<double>(velocity) / 127.0;
switch (message) {
case 0x90: {
m_engine->handleNoteOn(midiEvent->port_index, channel, note, fpVelocity);
break;
}
case 0x80: {
m_engine->handleNoteOff(midiEvent->port_index, channel, note, fpVelocity);
break;
}
default: break; // TODO
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<const clap_event_transport_t*>(event)) {
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 1 addition & 2 deletions tests/data/mostlyharmless_DatabaseStateTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<true>(dbFile, { { "test", 0 } });
Expand Down
91 changes: 91 additions & 0 deletions tests/events/mostlyharmless_MidiEventTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Created by Syl Morrison on 29/11/2025.
//
#include <mostly_harmless/events/mostlyharmless_MidiEvent.h>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
namespace mostly_harmless::testing {
struct MidiMessage {
std::uint8_t b0;
std::uint8_t b1;
std::uint8_t b2;
};
template <typename Expected>
static auto check_result(std::optional<events::midi::MidiEvent> to_check) -> void {
REQUIRE(to_check);
REQUIRE(std::holds_alternative<Expected>(*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<events::midi::NoteOff>(res_opt);
const auto [channel, note, velocity] = std::get<events::midi::NoteOff>(*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<events::midi::NoteOn>(res_opt);
const auto [channel, note, velocity] = std::get<events::midi::NoteOn>(*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<events::midi::PolyAftertouch>(res_opt);
const auto [channel, note, pressure] = std::get<events::midi::PolyAftertouch>(*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<events::midi::ControlChange>(res_opt);
const auto [channel, controllerNumber, value] = std::get<events::midi::ControlChange>(*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<events::midi::ProgramChange>(res_opt);
const auto [channel, program] = std::get<events::midi::ProgramChange>(*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<events::midi::ChannelAftertouch>(res_opt);
const auto [channel, pressure] = std::get<events::midi::ChannelAftertouch>(*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<events::midi::PitchWheel>(res_opt);
const auto [channel, value] = std::get<events::midi::PitchWheel>(*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<events::midi::NoteOff>(res_opt);
const auto [channel, note, velocity] = std::get<events::midi::NoteOff>(*res_opt);
REQUIRE(channel == 0);
REQUIRE(note == 40);
REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(0.0));
}
}

} // namespace mostly_harmless::testing