Skip to content

Commit 498925a

Browse files
authored
Merge pull request #76 from SLM-Audio/syl/midi
Finally get around to supporting other midi message types
2 parents 1bf255c + 9d9dc86 commit 498925a

File tree

10 files changed

+300
-18
lines changed

10 files changed

+300
-18
lines changed

include/mostly_harmless/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ set(MOSTLYHARMLESS_HEADERS
1515
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContext.h
1616
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_WebEvent.h
1717
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_ParamEvent.h
18+
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEvent.h
1819
${CMAKE_CURRENT_SOURCE_DIR}/mostlyharmless_Parameters.h
1920
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewBase.h
2021
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.h
@@ -27,6 +28,7 @@ set(MOSTLYHARMLESS_HEADERS
2728
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Proxy.h
2829
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_NoDenormals.h
2930
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Logging.h
31+
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Visitor.h
3032
${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabaseState.h
3133
${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabasePropertyWatcher.h
3234
${PLATFORM_HEADERS}

include/mostly_harmless/core/mostlyharmless_IEngine.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,56 @@ namespace mostly_harmless::core {
9898
* \param velocity The 0-1 velocity of the note event
9999
*/
100100
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) {}
101+
102+
/**
103+
* 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.
104+
* Called on the audio thread, in response to a control change event.
105+
* 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!
106+
* @param portIndex The clap port index the event originated from.
107+
* @param channel The midi channel the event was passed to
108+
* @param controlNumber The midi control that was changed
109+
* @param data The data in the midi message - see the midi spec for interpreting this.
110+
*/
111+
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) {}
112+
113+
/**
114+
* 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.
115+
* Called on the audio thread, in response to a program change event.
116+
* @param portIndex The clap port index the event originated from.
117+
* @param channel The midi channel the event was passed to
118+
* @param programNumber The program number that was set
119+
*/
120+
virtual void handleProgramChange([[maybe_unused]] std::uint16_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t programNumber) {};
121+
122+
/**
123+
* Called if the plugin receives a (polyphonic) aftertouch event (polyphonic in the sense that each note can have its own pressure data).
124+
* Not pure virtual, as this function isn't relevant if you haven't requested midi functionality.
125+
* Called on the audio thread, in response to a poly aftertouch event.
126+
* @param portIndex The clap port index the event originated from.
127+
* @param channel The midi channel the event was passed to
128+
* @param note The midi note this aftertouch event applies to
129+
* @param pressure The pressure applied to this midi note
130+
*/
131+
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) {}
132+
133+
/**
134+
* Called if the plugin receives a (channel-wide) aftertouch event. In this case, all notes within a channel get the same pressure value.
135+
* Not pure virtual, as this function isn't relevant if you haven't requested midi functionality.
136+
* Called on the audio thread, in response to a channel aftertouch event.
137+
* @param portIndex The clap port index the event originated from.
138+
* @param channel The midi channel the event was passed to
139+
* @param pressure The pressure applied to this midi note
140+
*/
141+
virtual void handleChannelAftertouch([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint8_t pressure) {}
142+
143+
/**
144+
* 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.
145+
* Called on the audio thread, in response to a pitch wheel event.
146+
* @param portIndex The clap port index the event originated from.
147+
* @param channel The midi channel the event was passed to
148+
* @param value The value, between -1.0 and 1.0
149+
*/
150+
virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] double value) {}
101151
};
102152
} // namespace mostly_harmless::core
103153
#endif // MOSTLYHARMLESS_MOSTLYHARMLESS_IENGINE_H
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#ifndef MOSTLY_HARMLESS_MIDI_EVENT
2+
#define MOSTLY_HARMLESS_MIDI_EVENT
3+
#include <variant>
4+
#include <optional>
5+
/// @internal
6+
namespace mostly_harmless::events::midi {
7+
/// @internal
8+
struct NoteOn final {
9+
std::uint8_t channel;
10+
std::uint8_t note;
11+
double velocity;
12+
};
13+
14+
/// @internal
15+
struct NoteOff final {
16+
std::uint8_t channel;
17+
std::uint8_t note;
18+
double velocity;
19+
};
20+
21+
/// @internal
22+
struct PolyAftertouch final {
23+
std::uint8_t channel;
24+
std::uint8_t note;
25+
std::uint8_t pressure;
26+
};
27+
28+
/// @internal
29+
struct ControlChange final {
30+
std::uint8_t channel;
31+
std::uint8_t controllerNumber;
32+
std::uint8_t data;
33+
};
34+
35+
/// @internal
36+
struct ProgramChange final {
37+
std::uint8_t channel;
38+
std::uint8_t programNumber;
39+
};
40+
41+
/// @internal
42+
struct ChannelAftertouch final {
43+
std::uint8_t channel;
44+
std::uint8_t pressure;
45+
};
46+
47+
/// @internal
48+
struct PitchWheel final {
49+
std::uint8_t channel;
50+
double value;
51+
};
52+
53+
/// @internal
54+
using MidiEvent = std::variant<NoteOn, NoteOff, PolyAftertouch, ControlChange, ProgramChange, ChannelAftertouch, PitchWheel>;
55+
56+
/// @internal
57+
auto parse(std::uint8_t b0, std::uint8_t b1, std::uint8_t b2) -> std::optional<MidiEvent>;
58+
} // namespace mostly_harmless::events::midi
59+
60+
#endif
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Created by Syl Morrison on 29/11/2025.
3+
//
4+
5+
#ifndef MOSTLYHARMLESS_VISITOR_H
6+
#define MOSTLYHARMLESS_VISITOR_H
7+
namespace mostly_harmless::utils {
8+
/**
9+
* \brief Util for use with `std::visit` to avoid a bunch of `if constexpr(...)`s.
10+
*
11+
* Usage::
12+
* ```
13+
* std::variant<int, double> data;
14+
* std::visit(Visitor{
15+
* [](int x) { std::cout << "Was int!\n"; },
16+
* [](double x) { std::cout << "Was double!\n"; }
17+
* }, data);
18+
* ```
19+
* @tparam Callable
20+
*/
21+
template <typename... Callable>
22+
struct Visitor : Callable... {
23+
using Callable::operator()...;
24+
};
25+
} // namespace mostly_harmless::utils
26+
#endif // GLEO_MOSTLYHARMLESS_VISITOR_H

source/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ set(MOSTLYHARMLESS_SOURCES
2323
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContext.cpp
2424
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_ParamEvent.cpp
2525
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_WebEvent.cpp
26+
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEvent.cpp
2627
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewBase.cpp
2728
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.cpp
2829
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Colour.cpp
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include <mostly_harmless/events/mostlyharmless_MidiEvent.h>
2+
namespace mostly_harmless::events::midi {
3+
constexpr static auto s_note_off{ 0x80 };
4+
constexpr static auto s_note_on{ 0x90 };
5+
constexpr static auto s_poly_aftertouch{ 0xA0 };
6+
constexpr static auto s_control_change{ 0xB0 };
7+
constexpr static auto s_program_change{ 0xC0 };
8+
constexpr static auto s_channel_aftertouch{ 0xD0 };
9+
constexpr static auto s_pitch_wheel{ 0xE0 };
10+
11+
auto parse(std::uint8_t b0, std::uint8_t b1, std::uint8_t b2) -> std::optional<MidiEvent> {
12+
const std::uint8_t message = b0 & 0xF0;
13+
const std::uint8_t channel = b0 & 0x0F;
14+
switch (message) {
15+
case s_note_on: [[fallthrough]];
16+
case s_note_off: {
17+
const std::uint8_t note = b1;
18+
const std::uint8_t velocity = b2;
19+
const auto floatVel = static_cast<double>(velocity) / 127.0;
20+
if (message == s_note_on && velocity != 0) {
21+
return NoteOn{ .channel = channel, .note = note, .velocity = floatVel };
22+
}
23+
return NoteOff{ .channel = channel, .note = note, .velocity = floatVel };
24+
}
25+
case s_poly_aftertouch: return PolyAftertouch{ .channel = channel, .note = b1, .pressure = b2 };
26+
case s_control_change: return ControlChange{ .channel = channel, .controllerNumber = b1, .data = b2 };
27+
case s_program_change: return ProgramChange{ .channel = channel, .programNumber = b1 };
28+
case s_channel_aftertouch: return ChannelAftertouch{ .channel = channel, .pressure = b1 };
29+
case s_pitch_wheel: {
30+
const std::int16_t combined = b1 | (b2 << 7);
31+
double res = static_cast<double>(combined - 8192) / 8192.0;
32+
return PitchWheel{ .channel = channel, .value = res };
33+
}
34+
default: return {};
35+
}
36+
}
37+
38+
} // namespace mostly_harmless::events::midi

source/mostlyharmless_PluginBase.cpp

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
//
22
// Created by Syl Morrison on 20/10/2024.
33
//
4+
#include "mostly_harmless/utils/mostlyharmless_Visitor.h"
5+
6+
47
#include <mostly_harmless/mostlyharmless_PluginBase.h>
58
#include <mostly_harmless/utils/mostlyharmless_Macros.h>
69
#include <mostly_harmless/audio/mostlyharmless_AudioHelpers.h>
710
#include <mostly_harmless/utils/mostlyharmless_NoDenormals.h>
11+
#include <mostly_harmless/events/mostlyharmless_MidiEvent.h>
812
#include <clap/helpers/plugin.hxx>
913

1014
namespace mostly_harmless::internal {
@@ -168,23 +172,33 @@ namespace mostly_harmless::internal {
168172
// 0PPP PPPP
169173
// 0VVV VVVV
170174
const auto* midiEvent = reinterpret_cast<const clap_event_midi*>(event);
171-
std::uint8_t message = midiEvent->data[0] & 0xF0; // SSSS 0000 - Message will be << 4
172-
std::uint8_t channel = midiEvent->data[0] & 0x0F; // 0000 CCCC
173-
std::uint8_t note = midiEvent->data[1]; // 0PPP PPPP
174-
std::uint8_t velocity = midiEvent->data[2]; // 0VVV VVVV
175-
const auto fpVelocity = static_cast<double>(velocity) / 127.0;
176-
switch (message) {
177-
case 0x90: {
178-
m_engine->handleNoteOn(midiEvent->port_index, channel, note, fpVelocity);
179-
break;
180-
}
181-
case 0x80: {
182-
m_engine->handleNoteOff(midiEvent->port_index, channel, note, fpVelocity);
183-
break;
184-
}
185-
default: break; // TODO
175+
auto res = mostly_harmless::events::midi::parse(midiEvent->data[0], midiEvent->data[1], midiEvent->data[2]);
176+
if (!res) {
177+
return;
186178
}
187-
break;
179+
std::visit(utils::Visitor{
180+
[this, &midiEvent](events::midi::NoteOff x) {
181+
m_engine->handleNoteOff(midiEvent->port_index, x.channel, x.note, x.velocity);
182+
},
183+
[this, &midiEvent](events::midi::NoteOn x) {
184+
m_engine->handleNoteOn(midiEvent->port_index, x.channel, x.note, x.velocity);
185+
},
186+
[this, &midiEvent](events::midi::PolyAftertouch x) {
187+
m_engine->handlePolyAftertouch(midiEvent->port_index, x.channel, x.note, x.pressure);
188+
},
189+
[this, &midiEvent](events::midi::ControlChange x) {
190+
m_engine->handleControlChange(midiEvent->port_index, x.channel, x.controllerNumber, x.data);
191+
},
192+
[this, &midiEvent](events::midi::ProgramChange x) {
193+
m_engine->handleProgramChange(midiEvent->port_index, x.channel, x.programNumber);
194+
},
195+
[this, &midiEvent](events::midi::ChannelAftertouch x) {
196+
m_engine->handleChannelAftertouch(midiEvent->port_index, x.channel, x.pressure);
197+
},
198+
[this, &midiEvent](events::midi::PitchWheel x) {
199+
m_engine->handlePitchWheel(midiEvent->port_index, x.channel, x.value);
200+
} },
201+
*res);
188202
}
189203
case CLAP_EVENT_TRANSPORT: {
190204
if (const auto* transportEvent = reinterpret_cast<const clap_event_transport_t*>(event)) {

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ set(MOSTLYHARMLESS_TEST_SOURCE
55
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TimerTests.cpp
66
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContextTests.cpp
77
${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabaseStateTests.cpp
8+
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEventTests.cpp
89
PARENT_SCOPE)

tests/data/mostlyharmless_DatabaseStateTests.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ namespace mostly_harmless::testing {
7979
}
8080
std::filesystem::remove(dbFile);
8181
}
82-
83-
SECTION("Test DatabasePropertyWatcher") {
82+
SECTION("Test DatabasePropertyWatcher") {
8483
for (auto i = 0; i < 100; ++i) {
8584
{
8685
auto databaseOpt = tryCreateDatabase<true>(dbFile, { { "test", 0 } });
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//
2+
// Created by Syl Morrison on 29/11/2025.
3+
//
4+
#include <mostly_harmless/events/mostlyharmless_MidiEvent.h>
5+
#include <catch2/catch_test_macros.hpp>
6+
#include <catch2/matchers/catch_matchers_floating_point.hpp>
7+
namespace mostly_harmless::testing {
8+
struct MidiMessage {
9+
std::uint8_t b0;
10+
std::uint8_t b1;
11+
std::uint8_t b2;
12+
};
13+
template <typename Expected>
14+
static auto check_result(std::optional<events::midi::MidiEvent> to_check) -> void {
15+
REQUIRE(to_check);
16+
REQUIRE(std::holds_alternative<Expected>(*to_check));
17+
}
18+
TEST_CASE("Test Midi Status Bytes") {
19+
SECTION("Test NoteOff") {
20+
const MidiMessage msg{ 0b10000000, 36, 0 };
21+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
22+
check_result<events::midi::NoteOff>(res_opt);
23+
const auto [channel, note, velocity] = std::get<events::midi::NoteOff>(*res_opt);
24+
REQUIRE(channel == 0);
25+
REQUIRE(note == 36);
26+
REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(0.0));
27+
}
28+
SECTION("Test NoteOn") {
29+
const MidiMessage msg{ 0b10010000, 36, 127 };
30+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
31+
check_result<events::midi::NoteOn>(res_opt);
32+
const auto [channel, note, velocity] = std::get<events::midi::NoteOn>(*res_opt);
33+
REQUIRE(channel == 0);
34+
REQUIRE(note == 36);
35+
REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(1.0f));
36+
}
37+
SECTION("Test PolyAftertouch") {
38+
const MidiMessage msg{ 0b10100001, 60, 127 };
39+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
40+
check_result<events::midi::PolyAftertouch>(res_opt);
41+
const auto [channel, note, pressure] = std::get<events::midi::PolyAftertouch>(*res_opt);
42+
REQUIRE(channel == 1);
43+
REQUIRE(note == 60);
44+
REQUIRE(pressure == 127);
45+
}
46+
SECTION("Test ControlChange") {
47+
const MidiMessage msg{ 0b10110001, 0, 127 };
48+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
49+
check_result<events::midi::ControlChange>(res_opt);
50+
const auto [channel, controllerNumber, value] = std::get<events::midi::ControlChange>(*res_opt);
51+
REQUIRE(channel == 1);
52+
REQUIRE(controllerNumber == 0);
53+
REQUIRE(value == 127);
54+
}
55+
SECTION("Test ProgramChange") {
56+
const MidiMessage msg{ 0b11000000, 2, 0 };
57+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
58+
check_result<events::midi::ProgramChange>(res_opt);
59+
const auto [channel, program] = std::get<events::midi::ProgramChange>(*res_opt);
60+
REQUIRE(channel == 0);
61+
REQUIRE(program == 2);
62+
}
63+
SECTION("Test ChannelAftertouch") {
64+
const MidiMessage msg{ 0b11010010, 8, 0 };
65+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
66+
check_result<events::midi::ChannelAftertouch>(res_opt);
67+
const auto [channel, pressure] = std::get<events::midi::ChannelAftertouch>(*res_opt);
68+
REQUIRE(channel == 2);
69+
REQUIRE(pressure == 8);
70+
}
71+
SECTION("Test PitchWheel") {
72+
const MidiMessage msg{ 0b11100000, 0x00, 0x40 };
73+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
74+
check_result<events::midi::PitchWheel>(res_opt);
75+
const auto [channel, value] = std::get<events::midi::PitchWheel>(*res_opt);
76+
REQUIRE(channel == 0);
77+
REQUIRE_THAT(value, Catch::Matchers::WithinRel(0.0));
78+
}
79+
80+
SECTION("Test 0 Velocity Note-On") {
81+
const MidiMessage msg{ 0b10010000, 40, 0 };
82+
const auto res_opt = events::midi::parse(msg.b0, msg.b1, msg.b2);
83+
check_result<events::midi::NoteOff>(res_opt);
84+
const auto [channel, note, velocity] = std::get<events::midi::NoteOff>(*res_opt);
85+
REQUIRE(channel == 0);
86+
REQUIRE(note == 40);
87+
REQUIRE_THAT(velocity, Catch::Matchers::WithinRel(0.0));
88+
}
89+
}
90+
91+
} // namespace mostly_harmless::testing

0 commit comments

Comments
 (0)