libremidi update to v5.4.3 and added virtual midi out as default

This commit is contained in:
Sebastian
2026-06-23 00:05:41 +02:00
parent dc0003232a
commit 51cd271b6f
166 changed files with 8135 additions and 2009 deletions
@@ -0,0 +1,33 @@
#pragma once
#include <libremidi/backends/android/config.hpp>
#include <libremidi/backends/android/midi_in.hpp>
#include <libremidi/backends/android/midi_out.hpp>
#include <libremidi/backends/android/observer.hpp>
NAMESPACE_LIBREMIDI
{
namespace android
{
struct backend
{
using midi_in = android::midi_in;
using midi_out = android::midi_out;
using midi_observer = android::observer;
using midi_in_configuration = android::input_configuration;
using midi_out_configuration = android::output_configuration;
using midi_observer_configuration = android::observer_configuration;
static const constexpr auto API = libremidi::API::ANDROID_AMIDI;
static const constexpr std::string_view name = "android";
static const constexpr std::string_view display_name = "Android MIDI API";
static bool available() noexcept
{
#if defined(__ANDROID__)
return true;
#else
return false;
#endif
}
};
}
}
@@ -0,0 +1,23 @@
#pragma once
#include <libremidi/config.hpp>
NAMESPACE_LIBREMIDI
{
namespace android
{
struct input_configuration
{
std::string_view client_name = "libremidi client";
};
struct output_configuration
{
std::string_view client_name = "libremidi client";
};
struct observer_configuration
{
std::string_view client_name = "libremidi client";
};
}
}
@@ -0,0 +1,239 @@
#include <libremidi/backends/android/helpers.hpp>
#include <libremidi/backends/android/midi_in.hpp>
#include <libremidi/backends/android/midi_out.hpp>
NAMESPACE_LIBREMIDI::android
{
// Main source of knowledge is RtMidi implementation
// which was done by Yellow Labrador
JNIEnv* context::get_thread_env()
{
JNIEnv* env;
JavaVM* jvm;
jsize count;
if (JNI_GetCreatedJavaVMs(&jvm, 1, &count) != JNI_OK || count < 1)
{
LOGE("No JVM found");
return nullptr;
}
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED)
{
if (jvm->AttachCurrentThread(&env, nullptr) != JNI_OK)
{
LOGE("Failed to attach thread");
return nullptr;
}
}
return env;
}
jobject context::get_context(JNIEnv* env)
{
auto activity_thread = env->FindClass("android/app/ActivityThread");
auto current_activity_thread = env->GetStaticMethodID(
activity_thread, "currentActivityThread", "()Landroid/app/ActivityThread;");
auto at = env->CallStaticObjectMethod(activity_thread, current_activity_thread);
if (!at)
{
LOGE("Failed to get ActivityThread");
return nullptr;
}
auto get_application
= env->GetMethodID(activity_thread, "getApplication", "()Landroid/app/Application;");
auto app = env->CallObjectMethod(at, get_application);
if (!app)
{
LOGE("Failed to get Application");
return nullptr;
}
return app;
}
jobject context::get_midi_manager(JNIEnv* env, jobject ctx)
{
auto context_class = env->FindClass("android/content/Context");
auto get_system_service = env->GetMethodID(
context_class, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
auto midi_service_str = env->NewStringUTF("midi");
auto service = env->CallObjectMethod(ctx, get_system_service, midi_service_str);
env->DeleteLocalRef(midi_service_str);
return service;
}
void context::refresh_midi_devices(JNIEnv* env, jobject ctx, bool is_output)
{
cleanup_devices(env);
auto midi_service = get_midi_manager(env, ctx);
if (!midi_service)
return;
auto midi_mgr_class = env->FindClass("android/media/midi/MidiManager");
auto get_devices_method
= env->GetMethodID(midi_mgr_class, "getDevices", "()[Landroid/media/midi/MidiDeviceInfo;");
auto device_array = (jobjectArray)env->CallObjectMethod(midi_service, get_devices_method);
auto device_info_class = env->FindClass("android/media/midi/MidiDeviceInfo");
auto get_input_count = env->GetMethodID(device_info_class, "getInputPortCount", "()I");
auto get_output_count = env->GetMethodID(device_info_class, "getOutputPortCount", "()I");
jsize count = env->GetArrayLength(device_array);
for (jsize i = 0; i < count; ++i)
{
auto device_info = env->GetObjectArrayElement(device_array, i);
int port_count = is_output ? env->CallIntMethod(device_info, get_input_count)
: env->CallIntMethod(device_info, get_output_count);
if (port_count > 0)
{
midi_devices.push_back(env->NewGlobalRef(device_info));
}
env->DeleteLocalRef(device_info);
}
env->DeleteLocalRef(device_array);
env->DeleteLocalRef(midi_service);
}
std::string context::port_name(JNIEnv* env, unsigned int port_number)
{
if (port_number >= midi_devices.size())
{
LOGE("Invalid port number");
return "";
}
auto device_info_class = env->FindClass("android/media/midi/MidiDeviceInfo");
auto get_props_method
= env->GetMethodID(device_info_class, "getProperties", "()Landroid/os/Bundle;");
auto bundle = env->CallObjectMethod(midi_devices[port_number], get_props_method);
auto bundle_class = env->FindClass("android/os/Bundle");
auto get_string_method
= env->GetMethodID(bundle_class, "getString", "(Ljava/lang/String;)Ljava/lang/String;");
auto key = env->NewStringUTF("name");
auto name_str = (jstring)env->CallObjectMethod(bundle, get_string_method, key);
std::string result;
if (name_str)
{
const char* name_chars = env->GetStringUTFChars(name_str, nullptr);
result = name_chars;
env->ReleaseStringUTFChars(name_str, name_chars);
env->DeleteLocalRef(name_str);
}
env->DeleteLocalRef(key);
env->DeleteLocalRef(bundle);
return result;
}
void context::cleanup_devices(JNIEnv* env)
{
for (jobject device : midi_devices)
{
env->DeleteGlobalRef(device);
}
midi_devices.clear();
}
void context::open_device(jobject device_info, void* target, bool is_output)
{
auto env = get_thread_env();
if (!env)
return;
auto ctx = get_context(env);
if (!ctx)
return;
auto midi_mgr = get_midi_manager(env, ctx);
if (!midi_mgr)
return;
// Check if we're on the main thread
auto looper_class = env->FindClass("android/os/Looper");
auto get_my_looper_method
= env->GetStaticMethodID(looper_class, "myLooper", "()Landroid/os/Looper;");
auto my_looper = env->CallStaticObjectMethod(looper_class, get_my_looper_method);
jobject handler = nullptr;
if (!my_looper)
{
LOGI("Not on a Looper thread, using main looper");
auto get_main_looper_method
= env->GetStaticMethodID(looper_class, "getMainLooper", "()Landroid/os/Looper;");
auto main_looper = env->CallStaticObjectMethod(looper_class, get_main_looper_method);
auto handler_class = env->FindClass("android/os/Handler");
auto handler_ctor = env->GetMethodID(handler_class, "<init>", "(Landroid/os/Looper;)V");
handler = env->NewObject(handler_class, handler_ctor, main_looper);
env->DeleteLocalRef(main_looper);
}
// Create the callback listener
auto callback_class = env->FindClass("dev/celtera/libremidi/MidiDeviceCallback");
if (!callback_class)
{
LOGE("MidiDeviceCallback class not found - ensure the Java class is loaded");
if (handler)
env->DeleteLocalRef(handler);
return;
}
auto callback_ctor = env->GetMethodID(callback_class, "<init>", "(JZ)V");
auto callback
= env->NewObject(callback_class, callback_ctor, (jlong)target, (jboolean)is_output);
// Open the device
auto midi_mgr_class = env->FindClass("android/media/midi/MidiManager");
auto open_device_method = env->GetMethodID(
midi_mgr_class, "openDevice",
"(Landroid/media/midi/MidiDeviceInfo;Landroid/media/midi/"
"MidiManager$OnDeviceOpenedListener;Landroid/os/Handler;)V");
env->CallVoidMethod(midi_mgr, open_device_method, device_info, callback, handler);
env->DeleteLocalRef(callback);
if (handler)
env->DeleteLocalRef(handler);
}
extern "C" JNIEXPORT void JNICALL Java_dev_celtera_libremidi_MidiDeviceCallback_onDeviceOpened(
JNIEnv* env, jobject /*thiz*/, jobject midi_device, jlong target_ptr, jboolean is_output)
{
if (!midi_device || target_ptr == 0)
{
LOGE("Invalid device or target pointer in callback");
return;
}
AMidiDevice* amidi_device = nullptr;
AMidiDevice_fromJava(env, midi_device, &amidi_device);
if (!amidi_device)
{
LOGE("Failed to convert Java MIDI device to AMidiDevice");
return;
}
if (is_output)
{
midi_out::open_callback(reinterpret_cast<midi_out*>(target_ptr), amidi_device);
}
else
{
midi_in::open_callback(reinterpret_cast<midi_in*>(target_ptr), amidi_device);
}
}
}
@@ -0,0 +1,40 @@
#pragma once
#include <libremidi/error.hpp>
#include <amidi/AMidi.h>
#include <android/log.h>
#include <jni.h>
#include <pthread.h>
#include <string>
#include <vector>
#define LOG_TAG "libremidi"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
NAMESPACE_LIBREMIDI
{
namespace android
{
struct context
{
static inline std::string client_name;
static inline std::vector<jobject> midi_devices;
static JNIEnv* get_thread_env();
static jobject get_context(JNIEnv* env);
static jobject get_midi_manager(JNIEnv* env, jobject context);
static void refresh_midi_devices(JNIEnv* env, jobject context, bool is_output);
static void open_device(jobject device_info, void* target, bool is_output);
static std::string port_name(JNIEnv* env, unsigned int port_number);
static void cleanup_devices(JNIEnv* env);
};
extern "C" JNIEXPORT void JNICALL Java_dev_celtera_libremidi_MidiDeviceCallback_onDeviceOpened(
JNIEnv* env, jobject /*thiz*/, jobject midi_device, jlong target_ptr, jboolean is_output);
}
}
@@ -0,0 +1,165 @@
#pragma once
#include <libremidi/backends/android/config.hpp>
#include <libremidi/backends/android/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
#include <atomic>
#include <chrono>
#include <thread>
NAMESPACE_LIBREMIDI
{
namespace android
{
class midi_in
: public midi1::in_api
, public error_handler
{
public:
explicit midi_in(
libremidi::input_configuration&& conf, libremidi::android::input_configuration aconf)
: configuration{std::move(conf)}
{
if (!context::client_name.empty() && context::client_name != aconf.client_name)
{
libremidi_handle_error(
configuration, "Android backend only supports one client name per process");
return;
}
context::client_name = std::string(aconf.client_name);
client_open_ = stdx::error{};
}
~midi_in() override { close_port(); }
libremidi::API get_current_api() const noexcept override
{
return libremidi::API::ANDROID_AMIDI;
}
stdx::error open_port(const input_port& port, std::string_view /*name*/) override
{
if (is_port_open())
{
libremidi_handle_error(configuration, "midi_in::open_port: a port is already open");
return std::errc{};
}
const unsigned int port_number = port.port;
auto env = context::get_thread_env();
if (!env)
{
libremidi_handle_error(configuration, "midi_in::open_port: failed to get JNI environment");
return std::errc{};
}
auto ctx = context::get_context(env);
if (!ctx)
{
libremidi_handle_error(configuration, "midi_in::open_port: failed to get Android context");
return std::errc{};
}
context::refresh_midi_devices(env, ctx, false);
if (port_number >= context::midi_devices.size())
{
libremidi_handle_error(configuration, "midi_in::open_port: invalid port number");
return std::errc{};
}
port_open = true;
running = true;
context::open_device(context::midi_devices[port_number], this, false);
return stdx::error{};
}
stdx::error close_port() override
{
if (!is_port_open())
return std::errc{};
running = false;
if (poll_thread.joinable())
{
poll_thread.join();
}
if (midi_output_port)
{
AMidiOutputPort_close(midi_output_port);
midi_output_port = nullptr;
}
if (receive_device)
{
AMidiDevice_release(receive_device);
receive_device = nullptr;
}
port_open = false;
return stdx::error{};
}
bool is_port_open() const noexcept { return port_open; }
void start_midi_thread() { poll_thread = std::thread(&midi_in::poll_midi, this); }
timestamp absolute_timestamp() const noexcept override { return system_ns(); }
void poll_midi()
{
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = false,
.absolute_is_monotonic = false,
.has_samples = false,
};
uint8_t buffer[1024];
int32_t op_code;
int64_t timestamp;
size_t num_bytes;
while (running)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
ssize_t num_messages = AMidiOutputPort_receive(
midi_output_port, &op_code, buffer, sizeof(buffer), &num_bytes, &timestamp);
if (num_messages < 0)
{
LOGE("Error receiving MIDI data: %d", (int)num_messages);
continue;
}
if (num_messages > 0 && num_bytes > 0)
{
const auto to_ns = [=] { return timestamp; };
m_processing.on_bytes_multi(
{buffer, buffer + num_bytes},
m_processing.timestamp<timestamp_info>(to_ns, timestamp));
}
}
}
static void open_callback(midi_in* self, AMidiDevice* device)
{
self->receive_device = device;
AMidiOutputPort_open(device, 0, &self->midi_output_port);
self->start_midi_thread();
}
private:
libremidi::input_configuration configuration;
AMidiDevice* receive_device = nullptr;
AMidiOutputPort* midi_output_port = nullptr;
std::thread poll_thread;
std::atomic<bool> port_open{false};
std::atomic<bool> running{false};
midi1::input_state_machine m_processing{this->configuration};
};
}
}
@@ -0,0 +1,126 @@
#pragma once
#include <libremidi/backends/android/config.hpp>
#include <libremidi/backends/android/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
NAMESPACE_LIBREMIDI
{
namespace android
{
class midi_out final
: public midi1::out_api
, public error_handler
{
public:
explicit midi_out(
libremidi::output_configuration&& conf, libremidi::android::output_configuration aconf)
: configuration{std::move(conf)}
{
if (!context::client_name.empty() && context::client_name != aconf.client_name)
{
libremidi_handle_error(
configuration, "Android backend only supports one client name per process");
return;
}
context::client_name = std::string(aconf.client_name);
client_open_ = stdx::error{};
}
~midi_out() override { close_port(); }
libremidi::API get_current_api() const noexcept override
{
return libremidi::API::ANDROID_AMIDI;
}
stdx::error open_port(const output_port& port, std::string_view /*name*/) override
{
if (is_port_open())
{
libremidi_handle_error(configuration, "midi_out::open_port: a port is already open");
return std::errc{};
}
const unsigned int port_number = port.port;
auto env = context::get_thread_env();
if (!env)
{
libremidi_handle_error(configuration, "midi_out::open_port: failed to get JNI environment");
return std::errc{};
}
auto ctx = context::get_context(env);
if (!ctx)
{
libremidi_handle_error(configuration, "midi_out::open_port: failed to get Android context");
return std::errc{};
}
context::refresh_midi_devices(env, ctx, true);
if (port_number >= context::midi_devices.size())
{
libremidi_handle_error(configuration, "midi_out::open_port: invalid port number");
return std::errc{};
}
port_open = true;
context::open_device(context::midi_devices[port_number], this, true);
return stdx::error{};
}
stdx::error close_port() override
{
if (!is_port_open())
return std::errc{};
if (midi_input_port)
{
AMidiInputPort_close(midi_input_port);
midi_input_port = nullptr;
}
if (send_device)
{
AMidiDevice_release(send_device);
send_device = nullptr;
}
port_open = false;
return stdx::error{};
}
bool is_port_open() const noexcept { return port_open; }
stdx::error send_message(const unsigned char* message, size_t size) override
{
if (!is_port_open() || !midi_input_port)
{
libremidi_handle_error(configuration, "midi_out::send_message: port is not open");
return std::errc{};
}
ssize_t result = AMidiInputPort_send(midi_input_port, message, size);
if (result < 0)
{
libremidi_handle_error(configuration, "midi_out::send_message: failed to send MIDI data");
return std::errc{};
}
return stdx::error{};
}
static void open_callback(midi_out* self, AMidiDevice* device)
{
self->send_device = device;
AMidiInputPort_open(device, 0, &self->midi_input_port);
}
private:
libremidi::output_configuration configuration;
AMidiDevice* send_device = nullptr;
AMidiInputPort* midi_input_port = nullptr;
bool port_open = false;
};
}
}
@@ -0,0 +1,93 @@
#pragma once
#include <libremidi/backends/android/config.hpp>
#include <libremidi/backends/android/helpers.hpp>
#include <libremidi/detail/observer.hpp>
NAMESPACE_LIBREMIDI
{
namespace android
{
class observer final
: public libremidi::observer_api
, public error_handler
{
public:
explicit observer(
libremidi::observer_configuration&& conf, libremidi::android::observer_configuration aconf)
: configuration{std::move(conf)}
{
if (!context::client_name.empty() && context::client_name != aconf.client_name)
{
LOGW("Android backend only supports one client name per process");
}
context::client_name = std::string(aconf.client_name);
}
~observer()
{
auto env = context::get_thread_env();
if (env)
{
context::cleanup_devices(env);
}
}
libremidi::API get_current_api() const noexcept override
{
return libremidi::API::ANDROID_AMIDI;
}
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
auto env = context::get_thread_env();
if (!env)
return {};
auto ctx = context::get_context(env);
if (!ctx)
return {};
context::refresh_midi_devices(env, ctx, false);
std::vector<libremidi::input_port> ports;
for (size_t i = 0; i < context::midi_devices.size(); ++i)
{
libremidi::input_port port;
port.api = libremidi::API::ANDROID_AMIDI;
port.port_name = context::port_name(env, i);
port.port = i;
ports.push_back(std::move(port));
}
return ports;
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
auto env = context::get_thread_env();
if (!env)
return {};
auto ctx = context::get_context(env);
if (!ctx)
return {};
context::refresh_midi_devices(env, ctx, true);
std::vector<libremidi::output_port> ports;
for (size_t i = 0; i < context::midi_devices.size(); ++i)
{
libremidi::output_port port;
port.port_name = context::port_name(env, i);
port.port = i;
ports.push_back(std::move(port));
}
return ports;
}
private:
libremidi::observer_configuration configuration;
};
}
}