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
98 changes: 98 additions & 0 deletions bindings/python/example_rawio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python
"""
Raw I/O backend example for pylibremidi.

Demonstrates a loopback: bytes written by MidiOut are fed directly
back into MidiIn, simulating a serial wire connection.
This is the pattern you'd use for custom transports (serial, SPI, USB HID, etc.)
"""
import pylibremidi as lm


def midi1_example():
print("=== MIDI 1 Raw I/O ===")

# The library will give us a callback to call when bytes arrive
stored_cb = [None]

# Input: receive and print parsed MIDI messages
in_config = lm.InputConfiguration()
in_config.on_message = lambda msg: print(f" Received: {msg}")
in_config.direct = True # Call Python callbacks directly (no poll needed)

rawio_in = lm.RawioInputConfiguration()
rawio_in.set_receive_callback = lambda cb: stored_cb.__setitem__(0, cb)
rawio_in.stop_receive = lambda: stored_cb.__setitem__(0, None)

midi_in = lm.MidiIn(in_config, rawio_in)
midi_in.open_virtual_port("rawio_in")

# Output: loopback written bytes into the input
out_config = lm.OutputConfiguration()

def loopback_write(data):
if stored_cb[0]:
stored_cb[0](data, 0)
return lm.Error()

rawio_out = lm.RawioOutputConfiguration()
rawio_out.write_bytes = loopback_write

midi_out = lm.MidiOut(out_config, rawio_out)
midi_out.open_virtual_port("rawio_out")

# Send some MIDI messages - they loop back through the rawio transport
print("Sending Note On C4...")
midi_out.send_message(0x90, 60, 100)

print("Sending Note Off C4...")
midi_out.send_message(0x80, 60, 0)

print("Sending CC Volume=80...")
midi_out.send_message(0xB0, 7, 80)

# close_port breaks the reference cycle: midi_out -> write -> stored_cb -> C++ callback -> midi_in
midi_in.close_port()
midi_out.close_port()


def midi2_example():
print("\n=== MIDI 2 Raw I/O (UMP) ===")

stored_cb = [None]

ump_in_config = lm.UmpInputConfiguration()
ump_in_config.on_message = lambda msg: print(f" Received UMP: {msg}")
ump_in_config.direct = True

rawio_ump_in = lm.RawioUmpInputConfiguration()
rawio_ump_in.set_receive_callback = lambda cb: stored_cb.__setitem__(0, cb)
rawio_ump_in.stop_receive = lambda: stored_cb.__setitem__(0, None)

midi_in = lm.MidiIn(ump_in_config, rawio_ump_in)
midi_in.open_virtual_port("rawio_ump_in")

ump_out_config = lm.OutputConfiguration()

def loopback_write_ump(data):
if stored_cb[0]:
stored_cb[0](data, 0)
return lm.Error()

rawio_ump_out = lm.RawioUmpOutputConfiguration()
rawio_ump_out.write_ump = loopback_write_ump

midi_out = lm.MidiOut(ump_out_config, rawio_ump_out)
midi_out.open_virtual_port("rawio_ump_out")

# Send a MIDI 2.0 Note On UMP (group 0, channel 0, note 60)
print("Sending UMP Note On...")
midi_out.send_ump(0x4090003C, 0xC0000000)

midi_in.close_port()
midi_out.close_port()


midi1_example()
midi2_example()
print("\nDone.")
128 changes: 124 additions & 4 deletions bindings/python/pylibremidi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ struct observer_poll_wrapper {

explicit observer_poll_wrapper(observer_configuration conf, libremidi::observer_api_configuration api_conf) : conf{conf}, impl{process(std::move(conf)), std::move(api_conf)} {}

~observer_poll_wrapper() {
conf = {};
}

observer_configuration process(observer_configuration &&obs) {
if (obs.on_error)
obs.on_error = [this](std::string_view errorText, const source_location &) { queue.enqueue(poll_queue::error_message{std::string{errorText}}); };
Expand Down Expand Up @@ -128,6 +132,12 @@ struct midi_in_poll_wrapper {
explicit midi_in_poll_wrapper(ump_input_configuration_wrapper conf) noexcept : python_ump_callbacks{conf}, impl{this->process(std::move(conf))} {}
explicit midi_in_poll_wrapper(ump_input_configuration_wrapper conf, input_api_configuration api_conf) : python_ump_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}

~midi_in_poll_wrapper() {
impl.close_port();
python_midi1_callbacks = {};
python_ump_callbacks = {};
}

input_configuration process(input_configuration_wrapper obs) {
python_midi1_callbacks = obs;

Expand Down Expand Up @@ -244,6 +254,79 @@ struct midi_in_poll_wrapper {
}
};

// Python-friendly wrappers for rawio configs.
// std::span cannot cross the Python/C++ boundary, so we use std::vector
// in the Python-facing callback signatures and convert internally.
struct rawio_input_configuration_python {
using py_receive_callback = std::function<void(std::vector<uint8_t>, int64_t)>;
std::function<void(py_receive_callback)> set_receive_callback;
std::function<void()> stop_receive = [] {};

rawio_input_configuration to_cpp() const {
rawio_input_configuration conf;
if (set_receive_callback) {
conf.set_receive_callback = [py_cb = set_receive_callback](
rawio_input_configuration::receive_callback cpp_cb) {
py_cb([cpp_cb = std::move(cpp_cb)](std::vector<uint8_t> data, int64_t ts) {
cpp_cb(std::span<const uint8_t>(data), ts);
});
};
}
conf.stop_receive = stop_receive;
return conf;
}
};

struct rawio_output_configuration_python {
std::function<stdx::error(std::vector<uint8_t>)> write_bytes
= [](std::vector<uint8_t>) { return stdx::error{}; };

rawio_output_configuration to_cpp() const {
rawio_output_configuration conf;
if (write_bytes) {
conf.write_bytes = [py_cb = write_bytes](std::span<const uint8_t> bytes) -> stdx::error {
return py_cb({bytes.begin(), bytes.end()});
};
}
return conf;
}
};

struct rawio_ump_input_configuration_python {
using py_receive_callback = std::function<void(std::vector<uint32_t>, int64_t)>;
std::function<void(py_receive_callback)> set_receive_callback;
std::function<void()> stop_receive = [] {};

rawio_ump_input_configuration to_cpp() const {
rawio_ump_input_configuration conf;
if (set_receive_callback) {
conf.set_receive_callback = [py_cb = set_receive_callback](
rawio_ump_input_configuration::receive_callback cpp_cb) {
py_cb([cpp_cb = std::move(cpp_cb)](std::vector<uint32_t> data, int64_t ts) {
cpp_cb(std::span<const uint32_t>(data), ts);
});
};
}
conf.stop_receive = stop_receive;
return conf;
}
};

struct rawio_ump_output_configuration_python {
std::function<stdx::error(std::vector<uint32_t>)> write_ump
= [](std::vector<uint32_t>) { return stdx::error{}; };

rawio_ump_output_configuration to_cpp() const {
rawio_ump_output_configuration conf;
if (write_ump) {
conf.write_ump = [py_cb = write_ump](std::span<const uint32_t> words) -> stdx::error {
return py_cb({words.begin(), words.end()});
};
}
return conf;
}
};

struct midi_out_poll_wrapper {
moodycamel::ReaderWriterQueue<poll_queue::midi_out_msg> queue{};
output_configuration_wrapper python_midi1_callbacks;
Expand All @@ -253,6 +336,11 @@ struct midi_out_poll_wrapper {
explicit midi_out_poll_wrapper(const output_configuration_wrapper &conf) noexcept : python_midi1_callbacks{conf}, impl{this->process(std::move(conf))} {}
explicit midi_out_poll_wrapper(output_configuration_wrapper conf, output_api_configuration api_conf) : python_midi1_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}

~midi_out_poll_wrapper() {
impl.close_port();
python_midi1_callbacks = {};
}

output_configuration process(output_configuration_wrapper obs) {
python_midi1_callbacks = obs;

Expand Down Expand Up @@ -294,6 +382,7 @@ NB_MODULE(pylibremidi, m) {

namespace nb = nanobind;
nb::class_<stdx::error>(m, "Error")
.def(nb::init<>())
.def("__bool__", [](stdx::error e) { return e != stdx::error{}; })
.def("__str__", [](stdx::error e) { return e.message().data(); })
.def("__repr__", [](stdx::error e) { return e.message().data(); });
Expand All @@ -310,6 +399,7 @@ NB_MODULE(pylibremidi, m) {
.value("PIPEWIRE", libremidi::API::PIPEWIRE)
.value("KEYBOARD", libremidi::API::KEYBOARD)
.value("NETWORK", libremidi::API::NETWORK)
.value("RAW_IO", libremidi::API::RAW_IO)

.value("ALSA_RAW_UMP", libremidi::API::ALSA_RAW_UMP)
.value("ALSA_SEQ_UMP", libremidi::API::ALSA_SEQ_UMP)
Expand All @@ -319,6 +409,7 @@ NB_MODULE(pylibremidi, m) {
.value("NETWORK_UMP", libremidi::API::NETWORK_UMP)
.value("JACK_UMP", libremidi::API::JACK_UMP)
.value("PIPEWIRE_UMP", libremidi::API::PIPEWIRE_UMP)
.value("RAW_IO_UMP", libremidi::API::RAW_IO_UMP)

.value("DUMMY", libremidi::API::DUMMY)
.export_values();
Expand Down Expand Up @@ -469,6 +560,15 @@ NB_MODULE(pylibremidi, m) {
nb::class_<libremidi::winmm_input_configuration>(m, "WinmmInputConfiguration").def(nb::init<>());
nb::class_<libremidi::winuwp_input_configuration>(m, "WinuwpInputConfiguration").def(nb::init<>());

nb::class_<libremidi::rawio_input_configuration_python>(m, "RawioInputConfiguration")
.def(nb::init<>())
.def_rw("set_receive_callback", &libremidi::rawio_input_configuration_python::set_receive_callback)
.def_rw("stop_receive", &libremidi::rawio_input_configuration_python::stop_receive);
nb::class_<libremidi::rawio_ump_input_configuration_python>(m, "RawioUmpInputConfiguration")
.def(nb::init<>())
.def_rw("set_receive_callback", &libremidi::rawio_ump_input_configuration_python::set_receive_callback)
.def_rw("stop_receive", &libremidi::rawio_ump_input_configuration_python::stop_receive);

nb::class_<libremidi::alsa_raw_output_configuration>(m, "AlsaRawOutputConfiguration").def(nb::init<>());
nb::class_<libremidi::alsa_raw_ump::output_configuration>(m, "AlsaRawUmpOutputConfiguration").def(nb::init<>());
nb::class_<libremidi::alsa_seq::output_configuration>(m, "AlsaSeqOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::alsa_seq::output_configuration::client_name);
Expand All @@ -486,6 +586,13 @@ NB_MODULE(pylibremidi, m) {
nb::class_<libremidi::winmm_output_configuration>(m, "WinmmOutputConfiguration").def(nb::init<>());
nb::class_<libremidi::winuwp_output_configuration>(m, "WinuwpOutputConfiguration").def(nb::init<>());

nb::class_<libremidi::rawio_output_configuration_python>(m, "RawioOutputConfiguration")
.def(nb::init<>())
.def_rw("write_bytes", &libremidi::rawio_output_configuration_python::write_bytes);
nb::class_<libremidi::rawio_ump_output_configuration_python>(m, "RawioUmpOutputConfiguration")
.def(nb::init<>())
.def_rw("write_ump", &libremidi::rawio_ump_output_configuration_python::write_ump);

nb::class_<libremidi::alsa_raw_observer_configuration>(m, "AlsaRawObserverConfiguration").def(nb::init<>());
nb::class_<libremidi::alsa_raw_ump::observer_configuration>(m, "AlsaRawUmpObserverConfiguration").def(nb::init<>());
nb::class_<libremidi::alsa_seq::observer_configuration>(m, "AlsaSeqObserverConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::alsa_seq::observer_configuration::client_name);
Expand All @@ -511,6 +618,9 @@ NB_MODULE(pylibremidi, m) {
nb::class_<libremidi::winmm_observer_configuration>(m, "WinmmObserverConfiguration").def(nb::init<>());
nb::class_<libremidi::winuwp_observer_configuration>(m, "WinuwpObserverConfiguration").def(nb::init<>());

nb::class_<libremidi::rawio_observer_configuration>(m, "RawioObserverConfiguration").def(nb::init<>());
nb::class_<libremidi::rawio_ump_observer_configuration>(m, "RawioUmpObserverConfiguration").def(nb::init<>());

nb::class_<libremidi::observer_poll_wrapper>(m, "Observer")
.def(nb::init<>())
.def(nb::init<libremidi::observer_configuration>())
Expand All @@ -525,6 +635,12 @@ NB_MODULE(pylibremidi, m) {
.def(nb::init<libremidi::input_configuration_wrapper, libremidi::API>())
.def(nb::init<libremidi::ump_input_configuration_wrapper>())
.def(nb::init<libremidi::ump_input_configuration_wrapper, libremidi::API>())
.def("__init__", [](libremidi::midi_in_poll_wrapper *self, libremidi::input_configuration_wrapper conf, libremidi::rawio_input_configuration_python apiconf) {
new (self) libremidi::midi_in_poll_wrapper(std::move(conf), libremidi::input_api_configuration{apiconf.to_cpp()});
})
.def("__init__", [](libremidi::midi_in_poll_wrapper *self, libremidi::ump_input_configuration_wrapper conf, libremidi::rawio_ump_input_configuration_python apiconf) {
new (self) libremidi::midi_in_poll_wrapper(std::move(conf), libremidi::input_api_configuration{apiconf.to_cpp()});
})
.def("get_current_api", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.get_current_api(); })
.def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p) { return self.impl.open_port(p); })
.def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p, std::string_view name) { return self.impl.open_port(p, name); })
Expand All @@ -542,6 +658,12 @@ NB_MODULE(pylibremidi, m) {
.def(nb::init<>())
.def(nb::init<libremidi::output_configuration_wrapper>())
.def(nb::init<libremidi::output_configuration_wrapper, libremidi::API>())
.def("__init__", [](libremidi::midi_out_poll_wrapper *self, libremidi::output_configuration_wrapper conf, libremidi::rawio_output_configuration_python apiconf) {
new (self) libremidi::midi_out_poll_wrapper(std::move(conf), libremidi::output_api_configuration{apiconf.to_cpp()});
})
.def("__init__", [](libremidi::midi_out_poll_wrapper *self, libremidi::output_configuration_wrapper conf, libremidi::rawio_ump_output_configuration_python apiconf) {
new (self) libremidi::midi_out_poll_wrapper(std::move(conf), libremidi::output_api_configuration{apiconf.to_cpp()});
})
.def("get_current_api", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.get_current_api(); })
.def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p) { return self.impl.open_port(p); })
.def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p, std::string_view name) { return self.impl.open_port(p, name); })
Expand All @@ -555,23 +677,21 @@ NB_MODULE(pylibremidi, m) {

// clang-format off
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, const libremidi::message& m) { return self.impl.send_message(m); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, const unsigned char* m, size_t size) { return self.impl.send_message(m, size); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, std::vector<unsigned char> m) { return self.impl.send_message(m); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0) { return self.impl.send_message(b0); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1) { return self.impl.send_message(b0, b1); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1, unsigned char b2) { return self.impl.send_message(b0, b1, b2); })

.def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, const unsigned char* m, size_t size) { return self.impl.schedule_message(t, m, size); })
.def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, std::vector<unsigned char> m) { return self.impl.schedule_message(t, m.data(), m.size()); })

.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const libremidi::ump& m) { return self.impl.send_ump(m); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const uint32_t* ump, size_t size) { return self.impl.send_ump(ump, size); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, std::vector<uint32_t> m) { return self.impl.send_ump(m); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0) { return self.impl.send_ump(u0); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1) { return self.impl.send_ump(u0, u1); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2) { return self.impl.send_ump(u0, u1, u2); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2, uint32_t u3) { return self.impl.send_ump(u0, u1, u2, u3); })

.def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, const uint32_t* m, size_t size) { return self.impl.schedule_ump(t, m, size); })
.def("schedule_ump", [](libremidi::midi_out_poll_wrapper &self, int64_t t, std::vector<uint32_t> m) { return self.impl.schedule_ump(t, m.data(), m.size()); })
// clang-format on

.def("poll", &libremidi::midi_out_poll_wrapper::poll);
Expand Down
1 change: 1 addition & 0 deletions cmake/libremidi.examples.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_example(sysextest)
add_example(minimal)
add_example(midi2_echo)
add_example(rawmidiin)
add_example(rawio)

if(LIBREMIDI_HAS_STD_FLAT_SET AND LIBREMIDI_HAS_STD_PRINTLN)
add_example(midi_to_pattern)
Expand Down
4 changes: 4 additions & 0 deletions cmake/libremidi.tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ target_link_libraries(midi_stream_decoder_test PRIVATE libremidi Catch2::Catch2W
add_executable(midi_timing_test tests/unit/midi_timing.cpp)
target_link_libraries(midi_timing_test PRIVATE libremidi Catch2::Catch2WithMain)

add_executable(rawio_test tests/unit/rawio.cpp)
target_link_libraries(rawio_test PRIVATE libremidi Catch2::Catch2WithMain)

include(CTest)
add_test(NAME conversion_test COMMAND conversion_test)
add_test(NAME error_test COMMAND error_test)
Expand All @@ -63,3 +66,4 @@ add_test(NAME midifile_write_tracks_test COMMAND midifile_write_tracks_test)
add_test(NAME protocols_test COMMAND protocols_test)
add_test(NAME midi_stream_decoder_test COMMAND midi_stream_decoder_test)
add_test(NAME midi_timing_test COMMAND midi_timing_test)
add_test(NAME rawio_test COMMAND rawio_test)
Loading
Loading