Skip to content

Commit 6bca73f

Browse files
committed
backend: add a raw, controllable backend for embedded use-cases
1 parent b1a509d commit 6bca73f

17 files changed

Lines changed: 806 additions & 2 deletions

cmake/libremidi.examples.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_example(sysextest)
3030
add_example(minimal)
3131
add_example(midi2_echo)
3232
add_example(rawmidiin)
33+
add_example(rawio)
3334

3435
if(LIBREMIDI_HAS_STD_FLAT_SET AND LIBREMIDI_HAS_STD_PRINTLN)
3536
add_example(midi_to_pattern)

cmake/libremidi.tests.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ target_link_libraries(midi_stream_decoder_test PRIVATE libremidi Catch2::Catch2W
5252
add_executable(midi_timing_test tests/unit/midi_timing.cpp)
5353
target_link_libraries(midi_timing_test PRIVATE libremidi Catch2::Catch2WithMain)
5454

55+
add_executable(rawio_test tests/unit/rawio.cpp)
56+
target_link_libraries(rawio_test PRIVATE libremidi Catch2::Catch2WithMain)
57+
5558
include(CTest)
5659
add_test(NAME conversion_test COMMAND conversion_test)
5760
add_test(NAME error_test COMMAND error_test)
@@ -63,3 +66,4 @@ add_test(NAME midifile_write_tracks_test COMMAND midifile_write_tracks_test)
6366
add_test(NAME protocols_test COMMAND protocols_test)
6467
add_test(NAME midi_stream_decoder_test COMMAND midi_stream_decoder_test)
6568
add_test(NAME midi_timing_test COMMAND midi_timing_test)
69+
add_test(NAME rawio_test COMMAND rawio_test)

examples/rawio.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#include "utils.hpp"
2+
3+
#include <libremidi/configurations.hpp>
4+
#include <libremidi/libremidi.hpp>
5+
6+
#include <cstdlib>
7+
#include <iostream>
8+
9+
/// This example demonstrates the raw I/O backend, which allows
10+
/// the user to plug in their own byte-level transport functions.
11+
/// This is useful for Arduino, Teensy, ESP32, serial ports, SPI, USB HID, etc.
12+
///
13+
/// In this example we simulate a loopback: bytes written by the output
14+
/// are fed directly into the input, as if connected by a serial wire.
15+
int main()
16+
{
17+
// MIDI 1.0 raw I/O example
18+
{
19+
std::cout << "=== MIDI 1 Raw I/O ===" << std::endl;
20+
21+
// The receive callback will be stored here by the library
22+
libremidi::rawio_input_configuration::receive_callback feed_input;
23+
24+
// Create a MIDI input that prints received messages
25+
libremidi::midi_in midiin{
26+
libremidi::input_configuration{
27+
.on_message
28+
= [](const libremidi::message& m) {
29+
std::cout << "Received MIDI 1: " << m << std::endl;
30+
}},
31+
libremidi::rawio_input_configuration{
32+
.set_receive_callback = [&](auto cb) { feed_input = std::move(cb); },
33+
.stop_receive = [&] { feed_input = nullptr; }}};
34+
35+
// Create a MIDI output whose write function loops back to the input
36+
libremidi::midi_out midiout{
37+
libremidi::output_configuration{},
38+
libremidi::rawio_output_configuration{
39+
.write_bytes = [&](std::span<const uint8_t> bytes) -> stdx::error {
40+
// Simulate a serial loopback: bytes go directly to the input
41+
if (feed_input)
42+
feed_input(bytes, 0);
43+
return {};
44+
}}};
45+
46+
midiin.open_virtual_port("rawio_in");
47+
midiout.open_virtual_port("rawio_out");
48+
49+
// Send some MIDI messages
50+
midiout.send_message(0x90, 60, 100); // Note On: C4, velocity 100
51+
midiout.send_message(0x80, 60, 0); // Note Off: C4
52+
midiout.send_message(0xB0, 7, 80); // CC: Volume = 80
53+
}
54+
55+
// MIDI 2.0 (UMP) raw I/O example
56+
{
57+
std::cout << "\n=== MIDI 2 Raw I/O (UMP) ===" << std::endl;
58+
59+
libremidi::rawio_ump_input_configuration::receive_callback feed_input;
60+
61+
libremidi::midi_in midiin{
62+
libremidi::ump_input_configuration{.on_message = [](const libremidi::ump& m) {
63+
std::cout << "Received UMP: " << m << std::endl;
64+
}},
65+
libremidi::rawio_ump_input_configuration{
66+
.set_receive_callback = [&](auto cb) { feed_input = std::move(cb); },
67+
.stop_receive = [&] { feed_input = nullptr; }}};
68+
69+
libremidi::midi_out midiout{
70+
libremidi::output_configuration{},
71+
libremidi::rawio_ump_output_configuration{
72+
.write_ump = [&](std::span<const uint32_t> words) -> stdx::error {
73+
if (feed_input)
74+
feed_input(words, 0);
75+
return {};
76+
}}};
77+
78+
midiin.open_virtual_port("rawio_ump_in");
79+
midiout.open_virtual_port("rawio_ump_out");
80+
81+
// Send a MIDI 2.0 Note On UMP (type 4, group 0, channel 0, note 60)
82+
uint32_t ump[2] = {0x40900000 | 60, 0xC0000000};
83+
midiout.send_ump(ump, 2);
84+
}
85+
86+
return 0;
87+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../include/libremidi
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/// ESP32 Serial MIDI example using libremidi's raw I/O backend.
2+
///
3+
/// Wiring: MIDI IN on Serial2 RX (GPIO 16), MIDI OUT on Serial2 TX (GPIO 17)
4+
/// Standard MIDI baud rate is 31250.
5+
///
6+
/// This sketch receives MIDI on Serial2, parses it through libremidi
7+
/// (giving you structured messages with sysex filtering, etc.),
8+
/// sends MIDI back out on the same serial port,
9+
/// and broadcasts every received message as an OSC /midi UDP packet
10+
/// to the local network (192.168.1.255:5678).
11+
12+
#include <WiFi.h>
13+
#include <WiFiUdp.h>
14+
15+
// Note: you need to have the "include/libremidi" folder copied or
16+
// symlinked into the sketch folder for this to work, or change the
17+
// Arduino platform configuration files to add the proper include path.
18+
#define LIBREMIDI_HEADER_ONLY 1
19+
#include <libremidi/configurations.hpp>
20+
#include <libremidi/libremidi.hpp>
21+
22+
// --- Configuration ---
23+
static constexpr int MIDI_BAUD = 31250;
24+
static constexpr int MIDI_RX_PIN = 16;
25+
static constexpr int MIDI_TX_PIN = 17;
26+
27+
static const char* WIFI_SSID = "your-ssid";
28+
static const char* WIFI_PASS = "your-password";
29+
30+
static const IPAddress UDP_BROADCAST{192, 168, 1, 255};
31+
static constexpr uint16_t UDP_PORT = 49444;
32+
static const char* OSC_PATH = "/midi"; // must be 5 chars (+ padding) for the packet layout below
33+
34+
// --- Globals ---
35+
36+
// The callback that libremidi gives us to feed incoming bytes
37+
static libremidi::rawio_input_configuration::receive_callback g_feed_input;
38+
39+
// The libremidi objects need to survive across loop() calls
40+
static std::optional<libremidi::midi_in> g_midi_in;
41+
static std::optional<libremidi::midi_out> g_midi_out;
42+
43+
static WiFiUDP g_udp;
44+
45+
/// Build and send an OSC message containing a single 3-byte MIDI event.
46+
///
47+
/// OSC /midi packet layout (all 4-byte aligned):
48+
/// "/midi\0\0\0" (8 bytes: pattern + null + padding)
49+
/// ",m\0\0" (4 bytes: typetag string)
50+
/// [port, b0, b1, b2] (4 bytes: OSC MIDI atom)
51+
void osc_broadcast_midi(uint8_t status, uint8_t d1, uint8_t d2)
52+
{
53+
// Pre-built packet: only the last 3 bytes change
54+
uint8_t pkt[16] = {
55+
// OSC address pattern "/midi" + null + 2 padding zeros
56+
'/', 'm', 'i', 'd', 'i', 0, 0, 0,
57+
// OSC type tag ",m" + null + 1 padding zero
58+
',', 'm', 0, 0,
59+
// OSC MIDI data: port (0), status, data1, data2
60+
0, status, d1, d2};
61+
62+
g_udp.beginPacket(UDP_BROADCAST, UDP_PORT);
63+
g_udp.write(pkt, sizeof(pkt));
64+
g_udp.endPacket();
65+
}
66+
67+
// Called by libremidi whenever a complete MIDI message is received and parsed
68+
void on_midi_message(const libremidi::message& msg)
69+
{
70+
// Print to USB serial for debugging
71+
Serial.printf("MIDI [%02X", msg.bytes[0]);
72+
for (size_t i = 1; i < msg.bytes.size(); i++)
73+
Serial.printf(" %02X", msg.bytes[i]);
74+
Serial.println("]");
75+
76+
// Broadcast as OSC over UDP (channel messages are always 3 bytes)
77+
if (msg.bytes.size() == 3)
78+
osc_broadcast_midi(msg.bytes[0], msg.bytes[1], msg.bytes[2]);
79+
80+
// Echo note-on messages back on serial with velocity halved
81+
if (msg.get_message_type() == libremidi::message_type::NOTE_ON && g_midi_out)
82+
{
83+
uint8_t velocity = msg.bytes[2] / 2;
84+
g_midi_out->send_message(msg.bytes[0], msg.bytes[1], velocity);
85+
}
86+
}
87+
88+
void setup()
89+
{
90+
// USB serial for debug output
91+
Serial.begin(115200);
92+
93+
// Connect to WiFi
94+
WiFi.begin(WIFI_SSID, WIFI_PASS);
95+
Serial.print("Connecting to WiFi");
96+
while (WiFi.status() != WL_CONNECTED)
97+
{
98+
delay(500);
99+
Serial.print(".");
100+
}
101+
Serial.printf("\nConnected, IP: %s\n", WiFi.localIP().toString().c_str());
102+
103+
// Start UDP for OSC broadcast
104+
g_udp.begin(UDP_PORT);
105+
106+
// MIDI serial port
107+
Serial2.begin(MIDI_BAUD, SERIAL_8N1, MIDI_RX_PIN, MIDI_TX_PIN);
108+
109+
// Create MIDI input
110+
g_midi_in.emplace(
111+
libremidi::input_configuration{
112+
.on_message = on_midi_message,
113+
.ignore_sysex = true,
114+
.ignore_timing = true,
115+
.ignore_sensing = true,
116+
},
117+
libremidi::rawio_input_configuration{
118+
.set_receive_callback = [](auto cb) { g_feed_input = std::move(cb); },
119+
.stop_receive = [] { g_feed_input = nullptr; },
120+
});
121+
122+
// Create MIDI output (serial)
123+
g_midi_out.emplace(
124+
libremidi::output_configuration{},
125+
libremidi::rawio_output_configuration{
126+
.write_bytes = [](std::span<const uint8_t> bytes) -> stdx::error {
127+
Serial2.write(bytes.data(), bytes.size());
128+
return {};
129+
}});
130+
131+
// Open ports to activate the callbacks
132+
g_midi_in->open_virtual_port("esp32_in");
133+
g_midi_out->open_virtual_port("esp32_out");
134+
135+
Serial.println("MIDI ready, broadcasting OSC to " + UDP_BROADCAST.toString() + ":" + UDP_PORT);
136+
}
137+
138+
void loop()
139+
{
140+
// Read all available bytes from the MIDI serial port
141+
// and feed them into libremidi for parsing
142+
int avail = Serial2.available();
143+
if (avail > 0 && g_feed_input)
144+
{
145+
uint8_t buf[64];
146+
int n = Serial2.readBytes(buf, min(avail, (int)sizeof(buf)));
147+
g_feed_input({buf, static_cast<size_t>(n)}, 0);
148+
}
149+
}

include/libremidi/api-c.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ enum libremidi_api
2626
NETWORK, /*!< MIDI over IP */
2727
ANDROID_AMIDI, /*!< Android AMidi API */
2828
KDMAPI, /*!< OmniMIDI KDMAPI (Windows) */
29+
RAW_IO, /*!< User-provided raw byte I/O (serial, SPI, USB, etc.) */
2930

3031
// MIDI 2.0 APIs
3132
ALSA_RAW_UMP = 0x1000, /*!< Raw ALSA API for MIDI 2.0 */
@@ -36,6 +37,7 @@ enum libremidi_api
3637
NETWORK_UMP, /*!< MIDI2 over IP */
3738
JACK_UMP, /*!< MIDI2 over JACK, type "32 bit raw UMP". Requires PipeWire v1.4+. */
3839
PIPEWIRE_UMP, /*!< MIDI2 over PipeWire. Requires v1.4+. */
40+
RAW_IO_UMP, /*!< User-provided raw UMP I/O (serial, SPI, USB, etc.) */
3941

4042
DUMMY = 0xFFFF /*!< A compilable but non-functional API. */
4143
};

include/libremidi/backends.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
#include <libremidi/backends/network_ump.hpp>
7878
#endif
7979

80+
#include <libremidi/backends/rawio.hpp>
81+
#include <libremidi/backends/rawio_ump.hpp>
82+
8083
#if defined(LIBREMIDI_ANDROID)
8184
#include <libremidi/backends/android/android.hpp>
8285
#endif
@@ -144,6 +147,7 @@ LIBREMIDI_STATIC constexpr auto available_backends = make_tl(
144147
android::backend{}
145148
#endif
146149
,
150+
rawio::backend{},
147151
dummy_backend{});
148152

149153
// There should always be at least one back-end.
@@ -194,6 +198,7 @@ LIBREMIDI_STATIC constexpr auto available_backends = make_tl(
194198
pipewire_ump::backend{}
195199
#endif
196200
,
201+
rawio_ump::backend{},
197202
dummy_backend{});
198203

199204
// There should always be at least one back-end.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#pragma once
2+
#include <libremidi/backends/rawio/config.hpp>
3+
#include <libremidi/backends/rawio/midi_in.hpp>
4+
#include <libremidi/backends/rawio/midi_out.hpp>
5+
#include <libremidi/backends/rawio/observer.hpp>
6+
7+
#include <string_view>
8+
9+
NAMESPACE_LIBREMIDI::rawio
10+
{
11+
struct backend
12+
{
13+
using midi_in = rawio::midi_in;
14+
using midi_out = rawio::midi_out;
15+
using midi_observer = rawio::observer;
16+
using midi_in_configuration = rawio_input_configuration;
17+
using midi_out_configuration = rawio_output_configuration;
18+
using midi_observer_configuration = rawio_observer_configuration;
19+
static const constexpr auto API = libremidi::API::RAW_IO;
20+
static const constexpr std::string_view name = "raw_io";
21+
static const constexpr std::string_view display_name = "Raw I/O";
22+
23+
static inline bool available() noexcept { return true; }
24+
};
25+
}

0 commit comments

Comments
 (0)