Skip to content

Commit 01cb08c

Browse files
Syl MorrisonSyl Morrison
authored andcommitted
Midi unit tests, and refactor internal handling to std::variant
1 parent 4c30695 commit 01cb08c

File tree

10 files changed

+351
-158
lines changed

10 files changed

+351
-158
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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,12 @@ namespace mostly_harmless::core {
142142

143143
/**
144144
* 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-
* 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.
146145
* Called on the audio thread, in response to a pitch wheel event.
147146
* @param portIndex The clap port index the event originated from.
148147
* @param channel The midi channel the event was passed to
149-
* @param value The 14-bit value representing pitch wheel pos between 0x0 and 0x3FFF.
148+
* @param value The value, between -1.0 and 1.0
150149
*/
151-
virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] std::uint16_t value) {}
150+
virtual void handlePitchWheel([[maybe_unused]] std::uint8_t portIndex, [[maybe_unused]] std::uint8_t channel, [[maybe_unused]] double value) {}
152151
};
153152
} // namespace mostly_harmless::core
154153
#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 & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
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 {
11-
namespace midi_headers {
12-
constexpr static auto s_note_off{ 0x80 };
13-
constexpr static auto s_note_on{ 0x90 };
14-
constexpr static auto s_poly_aftertouch{ 0xA0 };
15-
constexpr static auto s_control_change{ 0xB0 };
16-
constexpr static auto s_program_change{ 0xC0 };
17-
constexpr static auto s_channel_aftertouch{ 0xD0 };
18-
constexpr static auto s_pitch_wheel{ 0xE0 };
19-
} // namespace midi_headers
2015
PluginBase::PluginBase(const clap_host* host) : clap::helpers::Plugin<clap::helpers::MisbehaviourHandler::Ignore, clap::helpers::CheckingLevel::Maximal>(&getDescriptor(), host) {
2116
MH_LOG("PROC: Creating plugin instance...");
2217
m_pluginEntry = core::createPluginEntry();
@@ -177,53 +172,33 @@ namespace mostly_harmless::internal {
177172
// 0PPP PPPP
178173
// 0VVV VVVV
179174
const auto* midiEvent = reinterpret_cast<const clap_event_midi*>(event);
180-
const std::uint8_t message = midiEvent->data[0] & 0xF0; // SSSS 0000 - Message will be << 4
181-
const std::uint8_t channel = midiEvent->data[0] & 0x0F; // 0000 CCCC
182-
switch (message) {
183-
case midi_headers::s_note_on: [[fallthrough]];
184-
case midi_headers::s_note_off: {
185-
const std::uint8_t note = midiEvent->data[1]; // 0PPP PPPP
186-
const std::uint8_t velocity = midiEvent->data[2]; // 0VVV VVVV
187-
const auto fpVelocity = static_cast<double>(velocity) / 127.0;
188-
if (message == midi_headers::s_note_on) {
189-
m_engine->handleNoteOn(midiEvent->port_index, channel, note, fpVelocity);
190-
} else {
191-
m_engine->handleNoteOff(midiEvent->port_index, channel, note, fpVelocity);
192-
}
193-
break;
194-
}
195-
case midi_headers::s_poly_aftertouch: {
196-
const std::uint8_t note = midiEvent->data[1];
197-
const std::uint8_t pressure = midiEvent->data[2];
198-
m_engine->handlePolyAftertouch(midiEvent->port_index, channel, note, pressure);
199-
break;
200-
}
201-
case midi_headers::s_control_change: {
202-
const std::uint8_t controllerNumber = midiEvent->data[1];
203-
const std::uint8_t data = midiEvent->data[2];
204-
m_engine->handleControlChange(midiEvent->port_index, channel, controllerNumber, data);
205-
break;
206-
}
207-
case midi_headers::s_program_change: {
208-
const std::uint8_t programNumber = midiEvent->data[1];
209-
m_engine->handleProgramChange(midiEvent->port_index, channel, programNumber);
210-
break;
211-
}
212-
case midi_headers::s_channel_aftertouch: {
213-
const std::uint8_t pressure = midiEvent->data[1];
214-
m_engine->handleChannelAftertouch(midiEvent->port_index, channel, pressure);
215-
break;
216-
}
217-
case midi_headers::s_pitch_wheel: {
218-
const std::uint8_t lsb = midiEvent->data[1];
219-
const std::uint8_t msb = midiEvent->data[2];
220-
const std::uint16_t combined = lsb & (msb << 7);
221-
m_engine->handlePitchWheel(midiEvent->port_index, channel, combined);
222-
break;
223-
}
224-
default: break;
175+
auto res = mostly_harmless::events::midi::parse(midiEvent->data[0], midiEvent->data[1], midiEvent->data[2]);
176+
if (!res) {
177+
return;
225178
}
226-
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);
227202
}
228203
case CLAP_EVENT_TRANSPORT: {
229204
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)

0 commit comments

Comments
 (0)