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
113 changes: 113 additions & 0 deletions .github/workflows/build_python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Python

on:
push:
paths-ignore:
- 'docs/**'
- '**.md'
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'

jobs:
build:
name: ${{ matrix.config.name }}
runs-on: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
header_only: [0, 1]
config:
- {
name: "Windows (MSVC)",
os: windows-latest,
generator: "",
cmakeflags: "-DLIBREMIDI_NO_WINUWP=0 -DBOOST_ROOT=$PWD/boost_1_86_0 -DCMAKE_GENERATOR_PLATFORM=version=10.0.22621.0",
environment: ""
}
- {
name: "Ubuntu (gcc)",
os: ubuntu-latest,
generator: "",
cmakeflags: "-DCMAKE_CXX_FLAGS='-Werror=return-type -fsanitize=address -fsanitize=undefined -D_GLIBCXX_DEBUG=1 -D_GLIBCXX_DEBUG_PEDANTIC=1 -D_GLIBCXX_ASSERTIONS=1 -D_GLIBCXX_SANITIZE_VECTOR=1'",
environment: "LD_PRELOAD=/usr/lib/gcc/x86_64-linux-gnu/13/libasan.so:/usr/lib/gcc/x86_64-linux-gnu/13/libubsan.so"
}
- {
name: "Ubuntu (clang, libstdc++)",
os: ubuntu-latest,
generator: "",
cmakeflags: "-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS='-Werror=return-type'",
environment: ""
}
- {
name: "Ubuntu (clang, libc++)",
os: ubuntu-latest,
generator: "",
cmakeflags: "-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS='-stdlib=libc++ -Werror=return-type'",
environment: ""
}
- {
name: "macOS",
os: macos-14,
generator: "",
cmakeflags: "-DCMAKE_CXX_FLAGS=-Werror=return-type -DBOOST_ROOT=$PWD/boost_1_86_0",
environment: ""
}

steps:
- uses: actions/checkout@v4

- name: Get latest release version number
id: get_version
uses: dhkatz/get-version-action@main

- uses: maxim-lobanov/setup-xcode@v1
if: runner.os == 'macOS'
with:
xcode-version: latest-stable

- name: Install dependencies
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
sudo apt update
sudo apt install cmake libboost-dev libasound-dev libjack-jackd2-dev clang libc++-dev
else
curl -L https://github.com/ossia/sdk/releases/download/sdk31/boost_1_86_0.tar.gz > boost.tar.gz
tar -xzf boost.tar.gz
rm boost.tar.gz
fi
shell: bash

- name: Configure
shell: bash
run: |
cmake -S ./bindings/python -B build \
${{ matrix.config.generator }} \
${{ matrix.config.cmakeflags }} \
-DCMAKE_BUILD_TYPE=Debug \
-DLIBREMIDI_FIND_BOOST=1 \
-DLIBREMIDI_HEADER_ONLY=${{ matrix.header_only }} \
-DLIBREMIDI_CI=1

- name: Build
run: |
cmake --build build --config Debug

- name: Test
if: runner.os == 'Windows'
shell: bash
run: |
export PYTHONPATH=$PWD/build/Debug
find . -name '*.pyd'
# ${{matrix.config.environment}} python tests/python/list_apis.py

- name: Test
if: runner.os != 'Windows'
shell: bash
run: |
export PYTHONPATH=$PWD/build
export ASAN_OPTIONS=detect_leaks=0
${{matrix.config.environment}} python tests/python/list_apis.py

3 changes: 2 additions & 1 deletion bindings/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ project(pylibremidi)

set(LIBREMIDI_HEADER_ONLY 1)
set(LIBREMIDI_NEEDS_READERWRITERQUEUE 1)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL 1)
add_subdirectory(../.. libremidi-src)
find_package(Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED)

Expand All @@ -14,4 +15,4 @@ FetchContent_Declare(

FetchContent_MakeAvailable(nanobind)
nanobind_add_module(pylibremidi pylibremidi.cpp)
target_link_libraries(pylibremidi PUBLIC libremidi)
target_link_libraries(pylibremidi PUBLIC libremidi readerwriterqueue)
2 changes: 1 addition & 1 deletion bindings/python/test.py → bindings/python/example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python3.13
#!/usr/bin/env python
import pylibremidi as lm
observer_config = lm.ObserverConfiguration()
observer = lm.Observer(observer_config, lm.midi2_default_api())
Expand Down
106 changes: 53 additions & 53 deletions bindings/python/pylibremidi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ using midi_out_msg = boost::variant2::variant<error_message, warning_message>;
struct observer_poll_wrapper {
moodycamel::ReaderWriterQueue<poll_queue::observer_msg> queue{};
observer_configuration conf;
observer observer;
explicit observer_poll_wrapper(observer_configuration conf = {}) noexcept : conf{conf}, observer{this->process(std::move(conf))} {}
observer impl;
explicit observer_poll_wrapper(observer_configuration conf = {}) noexcept : conf{conf}, impl{this->process(std::move(conf))} {}

explicit observer_poll_wrapper(observer_configuration conf, libremidi::observer_api_configuration api_conf) : conf{conf}, observer{process(std::move(conf)), std::move(api_conf)} {}
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_configuration process(observer_configuration &&obs) {
if (obs.on_error)
Expand Down Expand Up @@ -91,12 +91,12 @@ struct midi_in_poll_wrapper {

input_configuration orig_callbacks;
ump_input_configuration ump_callbacks;
midi_in midi_in;
midi_in impl;

explicit midi_in_poll_wrapper(const input_configuration &conf) noexcept : orig_callbacks{conf}, midi_in{this->process(std::move(conf))} {}
explicit midi_in_poll_wrapper(input_configuration conf, input_api_configuration api_conf) : orig_callbacks{conf}, midi_in{this->process(std::move(conf)), std::move(api_conf)} {}
explicit midi_in_poll_wrapper(ump_input_configuration conf) noexcept : ump_callbacks{conf}, midi_in{this->process(std::move(conf))} {}
explicit midi_in_poll_wrapper(ump_input_configuration conf, input_api_configuration api_conf) : ump_callbacks{conf}, midi_in{this->process(std::move(conf)), std::move(api_conf)} {}
explicit midi_in_poll_wrapper(const input_configuration &conf) noexcept : orig_callbacks{conf}, impl{this->process(std::move(conf))} {}
explicit midi_in_poll_wrapper(input_configuration conf, input_api_configuration api_conf) : orig_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}
explicit midi_in_poll_wrapper(ump_input_configuration conf) noexcept : ump_callbacks{conf}, impl{this->process(std::move(conf))} {}
explicit midi_in_poll_wrapper(ump_input_configuration conf, input_api_configuration api_conf) : ump_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}

input_configuration process(input_configuration obs) {
orig_callbacks = obs;
Expand Down Expand Up @@ -155,11 +155,11 @@ struct midi_in_poll_wrapper {
struct midi_out_poll_wrapper {
moodycamel::ReaderWriterQueue<poll_queue::midi_out_msg> queue{};
output_configuration orig_callbacks;
midi_out midi_out;
explicit midi_out_poll_wrapper() noexcept : midi_out{} {}
midi_out impl;
explicit midi_out_poll_wrapper() noexcept : impl{} {}

explicit midi_out_poll_wrapper(const output_configuration &conf) noexcept : orig_callbacks{conf}, midi_out{this->process(std::move(conf))} {}
explicit midi_out_poll_wrapper(output_configuration conf, output_api_configuration api_conf) : orig_callbacks{conf}, midi_out{this->process(std::move(conf)), std::move(api_conf)} {}
explicit midi_out_poll_wrapper(const output_configuration &conf) noexcept : orig_callbacks{conf}, impl{this->process(std::move(conf))} {}
explicit midi_out_poll_wrapper(output_configuration conf, output_api_configuration api_conf) : orig_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}

output_configuration process(output_configuration obs) {
orig_callbacks = obs;
Expand Down Expand Up @@ -389,63 +389,63 @@ NB_MODULE(pylibremidi, m) {
.def(nb::init<>())
.def(nb::init<libremidi::observer_configuration>())
.def(nb::init<libremidi::observer_configuration, libremidi::API>())
.def("get_current_api", [](libremidi::observer_poll_wrapper &self) { return self.observer.get_current_api(); })
.def("get_input_ports", [](libremidi::observer_poll_wrapper &self) { return self.observer.get_input_ports(); })
.def("get_output_ports", [](libremidi::observer_poll_wrapper &self) { return self.observer.get_output_ports(); })
.def("get_current_api", [](libremidi::observer_poll_wrapper &self) { return self.impl.get_current_api(); })
.def("get_input_ports", [](libremidi::observer_poll_wrapper &self) { return self.impl.get_input_ports(); })
.def("get_output_ports", [](libremidi::observer_poll_wrapper &self) { return self.impl.get_output_ports(); })
.def("poll", [](libremidi::observer_poll_wrapper &self) { return self.poll(); });

nb::class_<libremidi::midi_in_poll_wrapper>(m, "MidiIn")
.def(nb::init<libremidi::input_configuration>())
.def(nb::init<libremidi::input_configuration, libremidi::API>())
.def(nb::init<libremidi::ump_input_configuration>())
.def(nb::init<libremidi::ump_input_configuration, libremidi::API>())
.def("get_current_api", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.get_current_api(); })
.def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p) { return self.midi_in.open_port(p); })
.def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p, std::string_view name) { return self.midi_in.open_port(p, name); })
.def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.open_virtual_port(); })
.def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.midi_in.open_virtual_port(name); })
.def("set_port_name", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.midi_in.set_port_name(name); })
.def("close_port", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.close_port(); })
.def("is_port_open", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.is_port_open(); })
.def("is_port_connected", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.is_port_connected(); })
.def("absolute_timestamp", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.absolute_timestamp(); })
.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); })
.def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.open_virtual_port(); })
.def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.impl.open_virtual_port(name); })
.def("set_port_name", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.impl.set_port_name(name); })
.def("close_port", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.close_port(); })
.def("is_port_open", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.is_port_open(); })
.def("is_port_connected", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.is_port_connected(); })
.def("absolute_timestamp", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.absolute_timestamp(); })
.def("poll", &libremidi::midi_in_poll_wrapper::poll);

nb::class_<libremidi::midi_out>(m, "MidiOutBase");
nb::class_<libremidi::midi_out_poll_wrapper>(m, "MidiOut")
.def(nb::init<>())
.def(nb::init<libremidi::output_configuration>())
.def(nb::init<libremidi::output_configuration, libremidi::API>())
.def("get_current_api", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.get_current_api(); })
.def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p) { return self.midi_out.open_port(p); })
.def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p, std::string_view name) { return self.midi_out.open_port(p, name); })
.def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.open_virtual_port(); })
.def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.midi_out.open_virtual_port(name); })
.def("set_port_name", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.midi_out.set_port_name(name); })
.def("close_port", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.close_port(); })
.def("is_port_open", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.is_port_open(); })
.def("is_port_connected", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.is_port_connected(); })
.def("absolute_timestamp", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.current_time(); })
.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); })
.def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.open_virtual_port(); })
.def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.impl.open_virtual_port(name); })
.def("set_port_name", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.impl.set_port_name(name); })
.def("close_port", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.close_port(); })
.def("is_port_open", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.is_port_open(); })
.def("is_port_connected", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.is_port_connected(); })
.def("absolute_timestamp", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.current_time(); })

// clang-format off
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, const libremidi::message& m) { return self.midi_out.send_message(m); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, const unsigned char* m, size_t size) { return self.midi_out.send_message(m, size); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, std::span<const unsigned char> m) { return self.midi_out.send_message(m); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0) { return self.midi_out.send_message(b0); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1) { return self.midi_out.send_message(b0, b1); })
.def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1, unsigned char b2) { return self.midi_out.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.midi_out.schedule_message(t, m, size); })

.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const libremidi::ump& m) { return self.midi_out.send_ump(m); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const uint32_t* ump, size_t size) { return self.midi_out.send_ump(ump, size); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, std::span<const uint32_t> m) { return self.midi_out.send_ump(m); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0) { return self.midi_out.send_ump(u0); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1) { return self.midi_out.send_ump(u0, u1); })
.def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2) { return self.midi_out.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.midi_out.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.midi_out.schedule_ump(t, m, size); })
.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::span<const 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("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::span<const 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); })
// clang-format on

.def("poll", &libremidi::midi_out_poll_wrapper::poll);
Expand Down
7 changes: 7 additions & 0 deletions tests/python/list_apis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env python
import pylibremidi as lm
lm.available_apis()
lm.available_ump_apis()
lm.get_version()
lm.midi1_default_api()
lm.midi2_default_api()