Skip to content

Commit e922d5e

Browse files
committed
python: add rawio backend, also fix an issue with send(uint32_t* ptr, int size) overload
1 parent 41530d6 commit e922d5e

3 files changed

Lines changed: 501 additions & 4 deletions

File tree

bindings/python/example_rawio.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python
2+
"""
3+
Raw I/O backend example for pylibremidi.
4+
5+
Demonstrates a loopback: bytes written by MidiOut are fed directly
6+
back into MidiIn, simulating a serial wire connection.
7+
This is the pattern you'd use for custom transports (serial, SPI, USB HID, etc.)
8+
"""
9+
import pylibremidi as lm
10+
11+
# --- MIDI 1 Raw I/O ---
12+
print("=== MIDI 1 Raw I/O ===")
13+
14+
# The library will give us a callback to call when bytes arrive
15+
stored_cb = None
16+
17+
# Input: receive and print parsed MIDI messages
18+
in_config = lm.InputConfiguration()
19+
in_config.on_message = lambda msg: print(f" Received: {msg}")
20+
in_config.direct = True # Call Python callbacks directly (no poll needed)
21+
22+
rawio_in = lm.RawioInputConfiguration()
23+
rawio_in.set_receive_callback = lambda cb: globals().update(stored_cb=cb)
24+
rawio_in.stop_receive = lambda: globals().update(stored_cb=None)
25+
26+
midi_in = lm.MidiIn(in_config, rawio_in)
27+
midi_in.open_virtual_port("rawio_in")
28+
29+
# Output: loopback written bytes into the input
30+
out_config = lm.OutputConfiguration()
31+
32+
def loopback_write(data):
33+
if stored_cb:
34+
stored_cb(data, 0)
35+
return lm.Error()
36+
37+
rawio_out = lm.RawioOutputConfiguration()
38+
rawio_out.write_bytes = loopback_write
39+
40+
midi_out = lm.MidiOut(out_config, rawio_out)
41+
midi_out.open_virtual_port("rawio_out")
42+
43+
# Send some MIDI messages - they loop back through the rawio transport
44+
print("Sending Note On C4...")
45+
midi_out.send_message(0x90, 60, 100)
46+
47+
print("Sending Note Off C4...")
48+
midi_out.send_message(0x80, 60, 0)
49+
50+
print("Sending CC Volume=80...")
51+
midi_out.send_message(0xB0, 7, 80)
52+
53+
# --- MIDI 2 Raw I/O (UMP) ---
54+
print("\n=== MIDI 2 Raw I/O (UMP) ===")
55+
56+
stored_ump_cb = None
57+
58+
ump_in_config = lm.UmpInputConfiguration()
59+
ump_in_config.on_message = lambda msg: print(f" Received UMP: {msg}")
60+
ump_in_config.direct = True
61+
62+
rawio_ump_in = lm.RawioUmpInputConfiguration()
63+
rawio_ump_in.set_receive_callback = lambda cb: globals().update(stored_ump_cb=cb)
64+
rawio_ump_in.stop_receive = lambda: globals().update(stored_ump_cb=None)
65+
66+
ump_midi_in = lm.MidiIn(ump_in_config, rawio_ump_in)
67+
ump_midi_in.open_virtual_port("rawio_ump_in")
68+
69+
ump_out_config = lm.OutputConfiguration()
70+
71+
def loopback_write_ump(data):
72+
if stored_ump_cb:
73+
stored_ump_cb(data, 0)
74+
return lm.Error()
75+
76+
rawio_ump_out = lm.RawioUmpOutputConfiguration()
77+
rawio_ump_out.write_ump = loopback_write_ump
78+
79+
ump_midi_out = lm.MidiOut(ump_out_config, rawio_ump_out)
80+
ump_midi_out.open_virtual_port("rawio_ump_out")
81+
82+
# Send a MIDI 2.0 Note On UMP (group 0, channel 0, note 60)
83+
print("Sending UMP Note On...")
84+
ump_midi_out.send_ump(0x4090003C, 0xC0000000)
85+
86+
print("\nDone.")

bindings/python/pylibremidi.cpp

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,79 @@ struct midi_in_poll_wrapper {
244244
}
245245
};
246246

247+
// Python-friendly wrappers for rawio configs.
248+
// std::span cannot cross the Python/C++ boundary, so we use std::vector
249+
// in the Python-facing callback signatures and convert internally.
250+
struct rawio_input_configuration_python {
251+
using py_receive_callback = std::function<void(std::vector<uint8_t>, int64_t)>;
252+
std::function<void(py_receive_callback)> set_receive_callback;
253+
std::function<void()> stop_receive = [] {};
254+
255+
rawio_input_configuration to_cpp() const {
256+
rawio_input_configuration conf;
257+
if (set_receive_callback) {
258+
conf.set_receive_callback = [py_cb = set_receive_callback](
259+
rawio_input_configuration::receive_callback cpp_cb) {
260+
py_cb([cpp_cb = std::move(cpp_cb)](std::vector<uint8_t> data, int64_t ts) {
261+
cpp_cb(std::span<const uint8_t>(data), ts);
262+
});
263+
};
264+
}
265+
conf.stop_receive = stop_receive;
266+
return conf;
267+
}
268+
};
269+
270+
struct rawio_output_configuration_python {
271+
std::function<stdx::error(std::vector<uint8_t>)> write_bytes
272+
= [](std::vector<uint8_t>) { return stdx::error{}; };
273+
274+
rawio_output_configuration to_cpp() const {
275+
rawio_output_configuration conf;
276+
if (write_bytes) {
277+
conf.write_bytes = [py_cb = write_bytes](std::span<const uint8_t> bytes) -> stdx::error {
278+
return py_cb({bytes.begin(), bytes.end()});
279+
};
280+
}
281+
return conf;
282+
}
283+
};
284+
285+
struct rawio_ump_input_configuration_python {
286+
using py_receive_callback = std::function<void(std::vector<uint32_t>, int64_t)>;
287+
std::function<void(py_receive_callback)> set_receive_callback;
288+
std::function<void()> stop_receive = [] {};
289+
290+
rawio_ump_input_configuration to_cpp() const {
291+
rawio_ump_input_configuration conf;
292+
if (set_receive_callback) {
293+
conf.set_receive_callback = [py_cb = set_receive_callback](
294+
rawio_ump_input_configuration::receive_callback cpp_cb) {
295+
py_cb([cpp_cb = std::move(cpp_cb)](std::vector<uint32_t> data, int64_t ts) {
296+
cpp_cb(std::span<const uint32_t>(data), ts);
297+
});
298+
};
299+
}
300+
conf.stop_receive = stop_receive;
301+
return conf;
302+
}
303+
};
304+
305+
struct rawio_ump_output_configuration_python {
306+
std::function<stdx::error(std::vector<uint32_t>)> write_ump
307+
= [](std::vector<uint32_t>) { return stdx::error{}; };
308+
309+
rawio_ump_output_configuration to_cpp() const {
310+
rawio_ump_output_configuration conf;
311+
if (write_ump) {
312+
conf.write_ump = [py_cb = write_ump](std::span<const uint32_t> words) -> stdx::error {
313+
return py_cb({words.begin(), words.end()});
314+
};
315+
}
316+
return conf;
317+
}
318+
};
319+
247320
struct midi_out_poll_wrapper {
248321
moodycamel::ReaderWriterQueue<poll_queue::midi_out_msg> queue{};
249322
output_configuration_wrapper python_midi1_callbacks;
@@ -294,6 +367,7 @@ NB_MODULE(pylibremidi, m) {
294367

295368
namespace nb = nanobind;
296369
nb::class_<stdx::error>(m, "Error")
370+
.def(nb::init<>())
297371
.def("__bool__", [](stdx::error e) { return e != stdx::error{}; })
298372
.def("__str__", [](stdx::error e) { return e.message().data(); })
299373
.def("__repr__", [](stdx::error e) { return e.message().data(); });
@@ -310,6 +384,7 @@ NB_MODULE(pylibremidi, m) {
310384
.value("PIPEWIRE", libremidi::API::PIPEWIRE)
311385
.value("KEYBOARD", libremidi::API::KEYBOARD)
312386
.value("NETWORK", libremidi::API::NETWORK)
387+
.value("RAW_IO", libremidi::API::RAW_IO)
313388

314389
.value("ALSA_RAW_UMP", libremidi::API::ALSA_RAW_UMP)
315390
.value("ALSA_SEQ_UMP", libremidi::API::ALSA_SEQ_UMP)
@@ -319,6 +394,7 @@ NB_MODULE(pylibremidi, m) {
319394
.value("NETWORK_UMP", libremidi::API::NETWORK_UMP)
320395
.value("JACK_UMP", libremidi::API::JACK_UMP)
321396
.value("PIPEWIRE_UMP", libremidi::API::PIPEWIRE_UMP)
397+
.value("RAW_IO_UMP", libremidi::API::RAW_IO_UMP)
322398

323399
.value("DUMMY", libremidi::API::DUMMY)
324400
.export_values();
@@ -469,6 +545,15 @@ NB_MODULE(pylibremidi, m) {
469545
nb::class_<libremidi::winmm_input_configuration>(m, "WinmmInputConfiguration").def(nb::init<>());
470546
nb::class_<libremidi::winuwp_input_configuration>(m, "WinuwpInputConfiguration").def(nb::init<>());
471547

548+
nb::class_<libremidi::rawio_input_configuration_python>(m, "RawioInputConfiguration")
549+
.def(nb::init<>())
550+
.def_rw("set_receive_callback", &libremidi::rawio_input_configuration_python::set_receive_callback)
551+
.def_rw("stop_receive", &libremidi::rawio_input_configuration_python::stop_receive);
552+
nb::class_<libremidi::rawio_ump_input_configuration_python>(m, "RawioUmpInputConfiguration")
553+
.def(nb::init<>())
554+
.def_rw("set_receive_callback", &libremidi::rawio_ump_input_configuration_python::set_receive_callback)
555+
.def_rw("stop_receive", &libremidi::rawio_ump_input_configuration_python::stop_receive);
556+
472557
nb::class_<libremidi::alsa_raw_output_configuration>(m, "AlsaRawOutputConfiguration").def(nb::init<>());
473558
nb::class_<libremidi::alsa_raw_ump::output_configuration>(m, "AlsaRawUmpOutputConfiguration").def(nb::init<>());
474559
nb::class_<libremidi::alsa_seq::output_configuration>(m, "AlsaSeqOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::alsa_seq::output_configuration::client_name);
@@ -486,6 +571,13 @@ NB_MODULE(pylibremidi, m) {
486571
nb::class_<libremidi::winmm_output_configuration>(m, "WinmmOutputConfiguration").def(nb::init<>());
487572
nb::class_<libremidi::winuwp_output_configuration>(m, "WinuwpOutputConfiguration").def(nb::init<>());
488573

574+
nb::class_<libremidi::rawio_output_configuration_python>(m, "RawioOutputConfiguration")
575+
.def(nb::init<>())
576+
.def_rw("write_bytes", &libremidi::rawio_output_configuration_python::write_bytes);
577+
nb::class_<libremidi::rawio_ump_output_configuration_python>(m, "RawioUmpOutputConfiguration")
578+
.def(nb::init<>())
579+
.def_rw("write_ump", &libremidi::rawio_ump_output_configuration_python::write_ump);
580+
489581
nb::class_<libremidi::alsa_raw_observer_configuration>(m, "AlsaRawObserverConfiguration").def(nb::init<>());
490582
nb::class_<libremidi::alsa_raw_ump::observer_configuration>(m, "AlsaRawUmpObserverConfiguration").def(nb::init<>());
491583
nb::class_<libremidi::alsa_seq::observer_configuration>(m, "AlsaSeqObserverConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::alsa_seq::observer_configuration::client_name);
@@ -511,6 +603,9 @@ NB_MODULE(pylibremidi, m) {
511603
nb::class_<libremidi::winmm_observer_configuration>(m, "WinmmObserverConfiguration").def(nb::init<>());
512604
nb::class_<libremidi::winuwp_observer_configuration>(m, "WinuwpObserverConfiguration").def(nb::init<>());
513605

606+
nb::class_<libremidi::rawio_observer_configuration>(m, "RawioObserverConfiguration").def(nb::init<>());
607+
nb::class_<libremidi::rawio_ump_observer_configuration>(m, "RawioUmpObserverConfiguration").def(nb::init<>());
608+
514609
nb::class_<libremidi::observer_poll_wrapper>(m, "Observer")
515610
.def(nb::init<>())
516611
.def(nb::init<libremidi::observer_configuration>())
@@ -525,6 +620,12 @@ NB_MODULE(pylibremidi, m) {
525620
.def(nb::init<libremidi::input_configuration_wrapper, libremidi::API>())
526621
.def(nb::init<libremidi::ump_input_configuration_wrapper>())
527622
.def(nb::init<libremidi::ump_input_configuration_wrapper, libremidi::API>())
623+
.def("__init__", [](libremidi::midi_in_poll_wrapper *self, libremidi::input_configuration_wrapper conf, libremidi::rawio_input_configuration_python apiconf) {
624+
new (self) libremidi::midi_in_poll_wrapper(std::move(conf), libremidi::input_api_configuration{apiconf.to_cpp()});
625+
})
626+
.def("__init__", [](libremidi::midi_in_poll_wrapper *self, libremidi::ump_input_configuration_wrapper conf, libremidi::rawio_ump_input_configuration_python apiconf) {
627+
new (self) libremidi::midi_in_poll_wrapper(std::move(conf), libremidi::input_api_configuration{apiconf.to_cpp()});
628+
})
528629
.def("get_current_api", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.get_current_api(); })
529630
.def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p) { return self.impl.open_port(p); })
530631
.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); })
@@ -542,6 +643,12 @@ NB_MODULE(pylibremidi, m) {
542643
.def(nb::init<>())
543644
.def(nb::init<libremidi::output_configuration_wrapper>())
544645
.def(nb::init<libremidi::output_configuration_wrapper, libremidi::API>())
646+
.def("__init__", [](libremidi::midi_out_poll_wrapper *self, libremidi::output_configuration_wrapper conf, libremidi::rawio_output_configuration_python apiconf) {
647+
new (self) libremidi::midi_out_poll_wrapper(std::move(conf), libremidi::output_api_configuration{apiconf.to_cpp()});
648+
})
649+
.def("__init__", [](libremidi::midi_out_poll_wrapper *self, libremidi::output_configuration_wrapper conf, libremidi::rawio_ump_output_configuration_python apiconf) {
650+
new (self) libremidi::midi_out_poll_wrapper(std::move(conf), libremidi::output_api_configuration{apiconf.to_cpp()});
651+
})
545652
.def("get_current_api", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.get_current_api(); })
546653
.def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p) { return self.impl.open_port(p); })
547654
.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); })
@@ -555,23 +662,21 @@ NB_MODULE(pylibremidi, m) {
555662

556663
// clang-format off
557664
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, const libremidi::message& m) { return self.impl.send_message(m); })
558-
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, const unsigned char* m, size_t size) { return self.impl.send_message(m, size); })
559665
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, std::vector<unsigned char> m) { return self.impl.send_message(m); })
560666
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0) { return self.impl.send_message(b0); })
561667
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1) { return self.impl.send_message(b0, b1); })
562668
.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); })
563669

564-
.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); })
670+
.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()); })
565671

566672
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const libremidi::ump& m) { return self.impl.send_ump(m); })
567-
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const uint32_t* ump, size_t size) { return self.impl.send_ump(ump, size); })
568673
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, std::vector<uint32_t> m) { return self.impl.send_ump(m); })
569674
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0) { return self.impl.send_ump(u0); })
570675
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1) { return self.impl.send_ump(u0, u1); })
571676
.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); })
572677
.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); })
573678

574-
.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); })
679+
.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()); })
575680
// clang-format on
576681

577682
.def("poll", &libremidi::midi_out_poll_wrapper::poll);

0 commit comments

Comments
 (0)