Intelligent Assistant
Chat with our virtual assistant to get answers promptly.
OH_MIDI is a native MIDI API provided by the system, used for implementing MIDI application development at the C/C++ layer starting from API version 24. When an application needs to interact with external MIDI devices (such as USB MIDI keyboards, Bluetooth MIDI devices) for data exchange, OH_MIDI can be used. Typical use scenarios include:
With OH_MIDI, you can implement the following functions:
Before developing with MIDI, call the canIUse API to check whether the current device supports MIDI capabilities. When canIUse("SystemCapability.Multimedia.Audio.MIDI") returns true, it indicates that the MIDI capability is available.
The main APIs of OH_MIDI include:
Before using OH_MIDI APIs, you need to complete the following preparations:
Link the dynamic library in the CMake script.
target_link_libraries(entry PUBLIC
libace_napi.z.so
libohmidi.so
libhilog_ndk.z.so
)Add header files.
#include "native_midi.h" #include "native_midi_base.h" #include <hilog/log.h>
The permission requirements for MIDI functionality vary depending on the usage scenario.
Scenario 1: Using only USB MIDI devices.
No additional permission declaration is required.
Scenario 2: Discovering and connecting BLE MIDI devices.
Declare the Bluetooth permission in module.json5:
"requestPermissions": [
{
"name": "ohos.permission.ACCESS_BLUETOOTH",
"reason": "$string:bluetooth_permission_reason"
}
] Creating a MIDI client is the first step in using the MIDI API.
The client serves as the connection entry point between the application and the MIDI system service, responsible for managing all interactions with the MIDI service. Before creating the client, you need to prepare the callback structure:
The system has defined the OH_MIDICallbacks structure. You need to implement the callback functions within it:
Create a MIDI client instance by calling the OH_MIDIClient_Create API, passing in the callback structure and user data.
// Create the MIDI client.
static napi_value CreateMIDIClient(napi_env env, napi_callback_info info)
{
// ...
std::lock_guard<std::mutex> lock(g_midiMutex);
// ...
OH_MIDIStatusCode status = OH_MIDIClient_Create(&g_midiClient, g_midiCallbacks, nullptr);
// ...
}When the MIDI functionality is no longer needed, the client should be destroyed to release resources. Before destroying, all opened devices must be closed first.
Destroy the MIDI client instance by calling OH_MIDIClient_Destroy to release all associated resources.
static napi_value DestroyMIDIClient(napi_env env, napi_callback_info info)
{
// ...
std::lock_guard<std::mutex> lock(g_midiMutex);
if (g_midiClient != nullptr) {
CloseAllOpenedDevices();
OH_MIDIClient_Destroy(g_midiClient);
g_midiClient = nullptr;
}
CleanupAllPortContexts();
// ...
}After creating the client, you can enumerate the MIDI devices currently available in the system. Device enumeration involves two steps:
Get the device count: Call OH_MIDIClient_GetDeviceCount to obtain the number of currently connected devices.
Get device information: Allocate a sufficiently large buffer and call OH_MIDIClient_GetDeviceInfos to populate detailed device information.
Obtain the device count through OH_MIDIClient_GetDeviceCount, and then obtain detailed device information through OH_MIDIClient_GetDeviceInfos.
static napi_value GetDeviceInfos(napi_env env, napi_callback_info info)
{
std::lock_guard<std::mutex> lock(g_midiMutex);
// ...
size_t count = 0;
OH_MIDIStatusCode status = OH_MIDIClient_GetDeviceCount(g_midiClient, &count);
// ...
std::vector<OH_MIDIDeviceInformation> devices(count);
size_t actualCount = 0;
status = OH_MIDIClient_GetDeviceInfos(g_midiClient, devices.data(), count, &actualCount);
// ...
}A device must be opened before data transmission can occur. The opening method varies depending on the device type: USB MIDI devices are opened synchronously through OH_MIDIClient_OpenDevice, while BLE MIDI devices are opened asynchronously through OH_MIDIClient_OpenBLEDevice.
Opening a USB MIDI Device (Synchronous)
Synchronously open a USB MIDI device through OH_MIDIClient_OpenDevice, passing in the device ID to obtain the device handle.
static napi_value OpenDevice(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
std::lock_guard<std::mutex> lock(g_midiMutex);
// ...
OH_MIDIDevice *device = nullptr;
OH_MIDIStatusCode status = OH_MIDIClient_OpenDevice(g_midiClient, deviceId, &device);
if (status == OH_MIDI_STATUS_OK && device != nullptr) {
g_openedDevices[deviceId] = device;
OH_LOG_INFO(LOG_APP, "[OpenDevice] device stored, total opened devices=%{public}zu", g_openedDevices.size());
}
// ...
}Open a BLE MIDI Device (Asynchronous)
Opening a BLE MIDI device is an asynchronous operation using OH_MIDIClient_OpenBLEDevice.
The BLE device address can be obtained through the following methods:
BLE scan discovery: Use the Bluetooth API to scan for MIDI BLE devices.
History: Load previously connected device addresses from persistent storage.
User input: Allow the user to manually enter a MAC address.
MIDI BLE devices can be filtered and identified by the service UUID 03B80E5A-EDE8-4B33-A751-6CE34EC4C700.
Asynchronously open a BLE MIDI device through OH_MIDIClient_OpenBLEDevice, passing in the device MAC address and a result callback.
// Asynchronously open the BLE device.
static napi_value OpenBLEDevice(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// Obtain the device address.
size_t addrLen = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &addrLen);
std::string deviceAddr(addrLen, '\0');
napi_get_value_string_utf8(env, args[0], &deviceAddr[0], addrLen + 1, &addrLen);
// ...
std::lock_guard<std::mutex> lock(g_midiMutex);
// ...
OH_MIDIStatusCode status = OH_MIDIClient_OpenBLEDevice(g_midiClient, deviceAddr.c_str(),
OnBLEDeviceOpened, nullptr);
// ...
}Receive the BLE device open result through the OH_MIDIClient_OnDeviceOpened callback, and obtain the device handle and device information in the callback.
static void OnBLEDeviceOpened(void *userData, bool opened, OH_MIDIDevice *device, OH_MIDIDeviceInformation info)
{
std::string deviceAddr = info.deviceAddress;
// ...
}Close an opened MIDI device and release device resources throughOH_MIDIClient_CloseDevice.
static napi_value CloseDevice(napi_env env, napi_callback_info info)
{
// ...
std::lock_guard<std::mutex> lock(g_midiMutex);
// ...
auto it = g_openedDevices.find(deviceId);
if (it != g_openedDevices.end()) {
// Clean up all InputPortContext instances for the device.
CleanupInputPortContextsForDevice(deviceId);
OH_MIDIStatusCode status = OH_MIDIClient_CloseDevice(g_midiClient, it->second);
g_openedDevices.erase(it);
// ...
} else {
// ...
}
// ...
return result;
}After opening the device, you need to obtain its port information and open the input or output port to send or receive MIDI data.
Each MIDI device may provide multiple ports, and each port has a clear direction (input or output). You need to enumerate all ports first, find the corresponding input port and output port based on application requirements, and then open them separately.
Obtain the port count through OH_MIDIClient_GetPortCount, and then obtain detailed port information through OH_MIDIClient_GetPortInfos.
static napi_value GetPortInfos(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
// ...
std::lock_guard<std::mutex> lock(g_midiMutex);
// ...
size_t count = 0;
OH_MIDIStatusCode status = OH_MIDIClient_GetPortCount(g_midiClient, deviceId, &count);
// ...
std::vector<OH_MIDIPortInformation> ports(count);
size_t actualCount = 0;
status = OH_MIDIClient_GetPortInfos(g_midiClient, deviceId, ports.data(), count, &actualCount);
// ...
}Open input port.
To receive MIDI messages, you need to open an input port and register a receiving callback function. When the device sends MIDI data, the callback function will be invoked.
The callback function should be executed in an independent thread, requiring the use of mutex locks or other synchronization mechanisms to ensure thread safety. The events data in the callback is valid only during this callback and must be copied if retention is needed.
Receive MIDI data through the OH_MIDIDevice_OnReceived callback, and obtain the event array and its data within the callback.
static void OnMIDIReceived(void *userData, const OH_MIDIEvent *events, size_t eventCount)
{
// userData points to InputPortContext.
InputPortContext* context = static_cast<InputPortContext*>(userData);
// ...
}Open the input port through OH_MIDIDevice_OpenInputPort, pass in the OH_MIDIPortDescriptor structure to configure port parameters, and register the data reception callback.
static napi_value OpenInputPort(napi_env env, napi_callback_info info)
{
InputPortArgs args = ParseInputPortArgs(env, info);
std::lock_guard<std::mutex> lock(g_midiMutex);
napi_value result;
auto it = g_openedDevices.find(args.deviceId);
if (g_midiClient == nullptr || it == g_openedDevices.end()) {
OH_LOG_ERROR(LOG_APP, "[OpenInputPort] client is null or device not opened");
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_INVALID_DEVICE_HANDLE), &result);
return result;
}
// Construct the port descriptor.
OH_MIDIPortDescriptor descriptor;
descriptor.portIndex = args.portIndex;
descriptor.protocol = static_cast<OH_MIDIProtocol>(args.protocol);
// Create an input port context for thread-safe callback processing.
auto context = std::make_shared<InputPortContext>(args.deviceId, args.portIndex);
OH_MIDIStatusCode status = OH_MIDIDevice_OpenInputPort(it->second, descriptor, OnMIDIReceived, context.get());
// ...
}Close the input port.
Use OH_MIDIDevice_CloseInputPort to close an opened input port. After the port is closed, it will no longer receive MIDI messages, and the registered callback function will no longer be called.
// Close the input port.
static napi_value CloseInputPort(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
uint32_t portIndex = 0;
napi_get_value_uint32(env, args[1], &portIndex);
OH_LOG_INFO(LOG_APP, "[CloseInputPort] ++enter, deviceId=%{public}lld, portIndex=%{public}u",
(long long)deviceId, portIndex);
std::lock_guard<std::mutex> lock(g_midiMutex);
napi_value result;
auto it = g_openedDevices.find(deviceId);
if (g_midiClient == nullptr || it == g_openedDevices.end()) {
OH_LOG_ERROR(LOG_APP, "[CloseInputPort] client is null or device not opened");
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_INVALID_DEVICE_HANDLE), &result);
return result;
}
OH_MIDIStatusCode status = OH_MIDIDevice_CloseInputPort(it->second, portIndex);
// Clean up the input port context.
auto key = std::make_pair(deviceId, portIndex);
auto contextIt = g_inputPortContexts.find(key);
if (contextIt != g_inputPortContexts.end()) {
if (contextIt->second != nullptr) {
contextIt->second->Stop();
}
g_inputPortContexts.erase(contextIt);
}
// ...
}Open the output port.
Open the output port through OH_MIDIDevice_OpenOutputPort, passing in the OH_MIDIPortDescriptor structure to configure port parameters and protocols.
static napi_value OpenOutputPort(napi_env env, napi_callback_info info)
{
size_t argc = 3;
napi_value args[3] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
uint32_t portIndex = 0;
napi_get_value_uint32(env, args[1], &portIndex);
int32_t protocol = static_cast<int32_t>(MIDI_PROTOCOL_1_0); // Use the MIDI 1.0 protocol by default.
napi_get_value_int32(env, args[MIDI_ARG_INDEX_2], &protocol);
OH_LOG_INFO(LOG_APP, "[OpenOutputPort] ++enter, deviceId=%{public}lld, portIndex=%{public}u, protocol=%{public}d",
(long long)deviceId, portIndex, protocol);
std::lock_guard<std::mutex> lock(g_midiMutex);
napi_value result;
auto it = g_openedDevices.find(deviceId);
if (g_midiClient == nullptr || it == g_openedDevices.end()) {
OH_LOG_ERROR(LOG_APP, "[OpenOutputPort] client is null or device not opened");
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_INVALID_DEVICE_HANDLE), &result);
return result;
}
OH_MIDIPortDescriptor descriptor;
descriptor.portIndex = portIndex;
descriptor.protocol = static_cast<OH_MIDIProtocol>(protocol);
OH_MIDIStatusCode status = OH_MIDIDevice_OpenOutputPort(it->second, descriptor);
// ...
}Close the output port.
Use the OH_MIDIDevice_CloseOutputPort API to close an opened output port. After the port is closed, MIDI messages can no longer be sent through this port.
// Close the output port.
static napi_value CloseOutputPort(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
uint32_t portIndex = 0;
napi_get_value_uint32(env, args[1], &portIndex);
OH_LOG_INFO(LOG_APP, "[CloseOutputPort] ++enter, deviceId=%{public}lld, portIndex=%{public}u",
(long long)deviceId, portIndex);
std::lock_guard<std::mutex> lock(g_midiMutex);
napi_value result;
auto it = g_openedDevices.find(deviceId);
if (g_midiClient == nullptr || it == g_openedDevices.end()) {
OH_LOG_ERROR(LOG_APP, "[CloseOutputPort] client is null or device not opened");
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_INVALID_DEVICE_HANDLE), &result);
return result;
}
OH_MIDIStatusCode status = OH_MIDIDevice_CloseOutputPort(it->second, portIndex);
// Remove the protocol information of this output port.
if (status == OH_MIDI_STATUS_OK) {
auto key = std::make_pair(deviceId, portIndex);
g_outputPortProtocols.erase(key);
}
// ...
}To send MIDI messages, you need to first construct data in the Universal MIDI Packet (UMP) format, and then send it through OH_MIDIDevice_Send. UMP is the abbreviation of universal MIDI packet, which is the unified packet format used by the MIDI 2.0 standard.
Send custom MIDI messages.
Construct an array of OH_MIDIEvent events, and send MIDI messages through the OH_MIDIDevice_Send API.
static napi_value SendMIDI(napi_env env, napi_callback_info info)
{
size_t argc = 3;
napi_value args[3] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
uint32_t portIndex = 0;
napi_get_value_uint32(env, args[1], &portIndex);
OH_LOG_DEBUG(LOG_APP, "[SendMIDI] deviceId=%{public}lld, portIndex=%{public}u", (long long)deviceId, portIndex);
bool isArray = false;
napi_is_array(env, args[MIDI_ARG_INDEX_2], &isArray);
if (!isArray) {
napi_value result;
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_GENERIC_INVALID_ARGUMENT), &result);
return result;
}
std::vector<OH_MIDIEvent> events;
std::vector<std::vector<uint32_t>> eventDataBuffers;
ParseMIDIEventsFromArray(env, args[MIDI_ARG_INDEX_2], events, eventDataBuffers);
uint32_t eventCount = static_cast<uint32_t>(events.size());
std::lock_guard<std::mutex> lock(g_midiMutex);
napi_value result;
auto it = g_openedDevices.find(deviceId);
if (g_midiClient == nullptr || it == g_openedDevices.end()) {
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_INVALID_DEVICE_HANDLE), &result);
return result;
}
uint32_t eventsWritten = 0;
OH_MIDIStatusCode status = OH_MIDIDevice_Send(it->second, portIndex, events.data(), eventCount, &eventsWritten);
napi_create_object(env, &result);
napi_value statusValue;
napi_create_int32(env, static_cast<int32_t>(status), &statusValue);
napi_set_named_property(env, result, "status", statusValue);
napi_value writtenValue;
napi_create_uint32(env, eventsWritten, &writtenValue);
napi_set_named_property(env, result, "eventsWritten", writtenValue);
return result;UMP format description
OH_MIDI mandates the use of the UMP format. Common MIDI 1.0 channel messages (MT=0x2, 32-bit) are constructed as follows:
| Bit | Field | Description |
|---|---|---|
| 31-28 | MT | Message type. For MIDI 1.0 Channel Voice messages, this is 0x2. |
| 27-24 | Group | Reserved. Set to 0. |
| 23-20 | Status | Status code (for example, 0x9 = Note On, 0x8 = Note Off). |
| 19-16 | Channel | Channel number (0–15). |
| 15-8 | Data1 | First data byte (for example, note number). |
| 7-0 | Data2 | Second data byte (for example, velocity). |
Common MIDI message UMP construction examples
// Build a MIDI 1.0 Note On UMP (a 32-bit unsigned integer).
static void BuildMIDI1NoteOn(uint32_t channel, uint32_t note, uint32_t velocity, uint32_t* umpData)
{
// UMP format (MIDI 1.0 Channel Voice - Note On): Bits 28–31 are the message type (MT=0x2), bits 24–27 are the group number (Group=0x0),
// bits 20–23 are the status code (Status=0x9), bits 16–19 are the channel number (Channel), bits 8–15 are the note number (Note), and bits 0–7 are the velocity value (Velocity).
umpData[0] = (MIDI_UMP_MT_1_0 << MIDI_UMP_WORDS_28) | (0x0 << MIDI_UMP_SHIFT_24) |
(MIDI_UMP_STATUS_NOTE_ON << MIDI_UMP_SHIFT_20) |
((channel & MIDI_CHANNEL_MASK) << MIDI_UMP_WORDS_16) |
((note & MIDI_NOTE_MASK) << MIDI_UMP_SHIFT_8) | (velocity & MIDI_NOTE_MASK);
}
// Build a MIDI 1.0 Note Off UMP (a 32-bit unsigned integer).
static void BuildMIDI1NoteOff(uint32_t channel, uint32_t note, uint32_t velocity, uint32_t* umpData)
{
// UMP format (MIDI 1.0 Channel Voice - Note Off): Bits 28-31 are the message type (MT=0x2), bits 24-27 are the group number (Group=0x0),
// bits 20-23 are the status code (Status=0x8), bits 16-19 are the channel number (Channel), bits 8-15 are the note number (Note), and bits 0-7 are the velocity value (Velocity).
umpData[0] = (MIDI_UMP_MT_1_0 << MIDI_UMP_WORDS_28) | (0x0 << MIDI_UMP_SHIFT_24) |
(MIDI_UMP_STATUS_NOTE_OFF << MIDI_UMP_SHIFT_20) |
((channel & MIDI_CHANNEL_MASK) << MIDI_UMP_WORDS_16) |
((note & MIDI_NOTE_MASK) << MIDI_UMP_SHIFT_8) | (velocity & MIDI_NOTE_MASK);
}Common CC controller numbers
| Number | Name | Usage |
|---|---|---|
| 1 | Modulation Wheel | Vibrato depth |
| 7 | Volume | Volume |
| 10 | Pan | Pan |
| 11 | Expression | Expression |
| 64 | Sustain Pedal | Sustain pedal |
| 65 | Portamento | Portamento |
| 71 | Resonance | Resonance |
| 74 | Filter Cutoff | Filter cutoff |
Sending System Exclusive Messages (SysEx)
System exclusive messages are used to transmit manufacturer-specific data. Use OH_MIDIDevice_SendSysEx to send SysEx messages that exceed the length of regular MIDI messages.
// Send a large SysEx message.
void SendSysExExample(OH_MIDIDevice *device, uint32_t outputPortIndex)
{
// Construct SysEx data.
std::vector<uint8_t> sysexData;
sysexData.push_back(0xF0); // SysEx start flag.
// Add vendor ID and data.
sysexData.push_back(0x43); // Vendor ID example.
sysexData.push_back(0x10);
sysexData.push_back(0x4C);
sysexData.push_back(0x00);
sysexData.push_back(0x00);
sysexData.push_back(0x7E);
sysexData.push_back(0xF7); // SysEx end flag.
OH_MIDIStatusCode status = OH_MIDIDevice_SendSysEx(
device, outputPortIndex, sysexData.data(), sysexData.size());
if (status != OH_MIDI_STATUS_OK) {
OH_LOG_ERROR(LOG_APP, "[SendSysEx] Failed: %{public}d", (int)status);
}
} Use the OH_MIDIDevice_FlushOutputPort API to flush the output buffer of a specified port, discarding all pending messages.
static napi_value FlushOutputPort(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t deviceId = 0;
napi_get_value_int64(env, args[0], &deviceId);
uint32_t portIndex = 0;
napi_get_value_uint32(env, args[1], &portIndex);
std::lock_guard<std::mutex> lock(g_midiMutex);
napi_value result;
auto it = g_openedDevices.find(deviceId);
if (g_midiClient == nullptr || it == g_openedDevices.end()) {
OH_LOG_ERROR(LOG_APP, "[FlushOutputPort] client is null or device not opened");
napi_create_int32(env, static_cast<int32_t>(OH_MIDI_STATUS_INVALID_DEVICE_HANDLE), &result);
return result;
}
OH_MIDIStatusCode status = OH_MIDIDevice_FlushOutputPort(it->second, portIndex);
// ...
}The OH_MIDI APIs provide userData parameters at three levels: client-level, BLE connection-level, and port connection-level, which are passed to callbacks in different scopes respectively.
| API | userData Scope | Callbacks Passed To | Typical Usage |
|---|---|---|---|
| OH_MIDIClient_Create | Client-level | onDeviceChange, onError | Global app state, device management. |
| OH_MIDIClient_OpenBLEDevice | BLE connection level | OnBLEDeviceOpened | Distinguishing concurrent connection requests, carrying connection context. |
| OH_MIDIDevice_OpenInputPort | Port-level | OnMIDIReceived | Port-specific processing logic. |
The three userData parameters are independent and can pass different context objects. After a port is closed, the corresponding callback is no longer invoked, and it is safe to release the port-level userData at this point.
Comparison of userData Usage Scenarios
| Scenario | Recommended Use | Example Data |
|---|---|---|
| Device hot-swap handling | Client-level | Device list, connection status. |
| Error log recording | Client-level | Log file handle, error count. |
| Port event statistics | Port-level | Event count, timestamp recording. |
| Multi-port differentiation | Port-level | Port ID, port name. |
| Cross-callback state sharing | Client-level | App configuration, global state. |
Resource management order: Closing a client automatically closes all devices and ports. It is recommended that applications close resources in the order of port -> device -> client to ensure clear code logic.
Thread safety: MIDI callback functions (OnMIDIReceived, OnDeviceChange, OnError) are executed in independent threads. Pay attention to thread safety, meaning synchronization mechanisms such as mutex locks should be used when accessing shared resources.
Memory safety: The events array and all data pointers within it in the OnMIDIReceived callback are temporary and valid only during this callback. Any data that needs to be retained must be copied before the callback returns. Accessing expired pointers can lead to undefined behavior (crashes, memory corruption).
Error handling: All MIDI API calls should check the return value and handle error conditions properly.
Performance optimization: Operations exceeding 1 millisecond or blocking I/O are prohibited in callback functions to ensure real-time processing of MIDI messages is not affected. Performing time-consuming operations or blocking I/O in callbacks will severely impact the real-time processing of MIDI messages, potentially causing audio latency or frame drops.
UMP format: The SDK always uses the UMP format for data transmission, regardless of whether the MIDI 1.0 or MIDI 2.0 protocol is selected. Understanding the UMP message format is required to correctly construct and parse MIDI messages.
Buffer management: The send API is non-blocking and returns OH_MIDI_STATUS_WOULD_BLOCK when the buffer is full. The application should check the return value and retry sending the unfinished data when the buffer becomes available.
Protocol compatibility: When requesting the MIDI 2.0 protocol but the device only supports MIDI 1.0, the service automatically performs a downgrade conversion, which will result in the loss of data precision or specific message types (such as SysEx).
Permission request: Accessing Bluetooth MIDI devices requires the ohos.permission.ACCESS_BLUETOOTH permission.
Device hot-swap: Applications should handle device connection and disconnection events, update internal state promptly, and release related resources.
Resource release order: OH_MIDIClient_Destroy() automatically closes all opened devices and ports. However, if an error occurs during initialization and the function needs to exit early, resources should be released manually in the following order:
Step 1: Close opened ports (OH_MIDIDevice_CloseInputPort/OH_MIDIDevice_CloseOutputPort). See the examples of closing input ports and closing output ports in MIDI Port Management.
Step 2: Close opened devices (OH_MIDIClient_CloseDevice). See the example in Closing a MIDI Device.
Step 3: Destroy the client (OH_MIDIClient_Destroy). For details, see the Destroying the MIDI Client example.
Use the following methods to verify that MIDI devices are correctly enumerated:
The UMP message format follows the MIDI 2.0 standard. Common UMP message types:
During debugging, you can output raw UMP data for verification:
OH_LOG_DEBUG(LOG_APP, "UMP Data: 0x%{public}08X", umpData[0]); Log output: Use OH_LOG series macros to output detailed debugging information.
During the development phase, it is recommended to add detailed logs containing timestamps, method names, parameter values, and return values at API calls, error handling, and key business logic state changes:
#define MIDI_LOG_TAG "[MIDI]"
#define MIDI_LOGI(fmt, ...) OH_LOG_INFO(LOG_APP, MIDI_LOG_TAG "[INFO] " fmt, ##__VA_ARGS__)
#define MIDI_LOGE(fmt, ...) OH_LOG_ERROR(LOG_APP, MIDI_LOG_TAG "[ERROR] " fmt, ##__VA_ARGS__)
#define MIDI_LOGD(fmt, ...) OH_LOG_DEBUG(LOG_APP, MIDI_LOG_TAG "[DEBUG] " fmt, ##__VA_ARGS__)
// Usage example.
MIDI_LOGI("Device opened: ID=%{public}lld", targetDeviceId);
MIDI_LOGE("Failed to send message: %{public}d", result);
MIDI_LOGD("UMP Data: 0x%{public}08X", umpData[0]); Indicates that the send buffer is full and the message cannot be sent immediately. The causes and handling methods are as follows:
The sending speed exceeds the buffer processing capacity: Reduce the sending frequency (usually in scenarios involving sending a large number of SysEx messages).
The buffer is not processed in time: Consider using the OH_MIDIDevice_FlushOutputPort API to clear the buffer.
Handle partial sending: Check the eventsWritten parameter to understand how many events were actually sent.
Example:
uint32_t eventsWritten = 0;
OH_MIDIStatusCode result = OH_MIDIDevice_Send(device, portIndex, events, count, &eventsWritten);
if (result == OH_MIDI_STATUS_WOULD_BLOCK) {
OH_LOG_WARN(LOG_APP, "Buffer full, partial send: %{public}u/%{public}u", eventsWritten, count);
// Option 1: Retry the remaining events later.
OH_MIDIEvent *remaining = &events[eventsWritten];
size_t remainingCount = count - eventsWritten;
// Option 2: Discard the unsent events.
// OH_LOG_WARN(LOG_APP, "Dropped %{public}zu events", remainingCount);
} Proper device hot-swap handling process:
Common issues and solutions for Bluetooth MIDI device connections:
This is usually caused by thread safety or memory lifecycle issues.
Error example:
// Error: Saving the pointer for later use.
static const OH_MIDIEvent *g_savedEvents;
static void OnMIDIReceived(void *userData, const OH_MIDIEvent *events, size_t eventCount) {
g_savedEvents = events; // Error! events become invalid after the callback ends.
}
// Accessing g_savedEvents elsewhere causes a crash. Correct example:
// Correct approach: copy the data.
struct MIDIMessage {
std::vector<uint32_t> umpData;
};
// Use a thread-safe queue to store messages.
static void OnMIDIReceived(void *userData, const OH_MIDIEvent *events, size_t eventCount) {
for (size_t i = 0; i < eventCount; i++) {
// Allocate memory and copy the data.
MIDIMessage msg;
msg.umpData.assign(events[i].data, events[i].data + events[i].length);
// Add the copied data to the queue (locking required).
// enqueue_message(msg);
}
} The complete sample code can be viewed in the sample project:
The sample project demonstrates: