lib: midi2: new UMP Stream responder library

Add a new top-level, transport independent library to respond to UMP Stream
Discovery messages. This allows MIDI2.0 clients to discover UMP endpoints
hosted on Zephyr over the UMP protocol.

The endpoint specification can be gathered from the device tree, so that
the same information used to generate USB descriptors in usb-midi2.0
can be delivered over UMP Stream.

Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
This commit is contained in:
Titouan Christophe
2025-07-31 11:51:40 +02:00
committed by Benjamin Cabé
parent 4b04b74bcd
commit 5e83d222b6
9 changed files with 591 additions and 1 deletions

View File

@@ -1017,6 +1017,7 @@ INPUT = @ZEPHYR_BASE@/doc/_doxygen/mainpage.md \
@ZEPHYR_BASE@/include/zephyr/sys/atomic.h \
@ZEPHYR_BASE@/include/ \
@ZEPHYR_BASE@/lib/libc/minimal/include/ \
@ZEPHYR_BASE@/lib/midi2/ \
@ZEPHYR_BASE@/subsys/testsuite/include/ \
@ZEPHYR_BASE@/subsys/testsuite/ztest/include/ \
@ZEPHYR_BASE@/subsys/secure_storage/include/ \

View File

@@ -14,6 +14,10 @@ properties:
type: int
const: 1
label:
type: string
description: Name of the UMP (MIDI 2.0) endpoint
child-binding:
description: |
MIDI2 Group terminal block.
@@ -21,6 +25,10 @@ child-binding:
device exchange Universal MIDI Packets with the host.
properties:
label:
type: string
description: Name of the corresponding UMP Function block
reg:
type: array
required: true
@@ -49,3 +57,9 @@ child-binding:
- "output-only"
description: |
Type (data direction) of Group Terminals in this Block.
serial-31250bps:
type: boolean
description: |
This represent a physical MIDI1 serial port, which is limited
to a transmission speed of 31.25kb/s.

View File

@@ -63,7 +63,10 @@ struct midi_ump {
#define UMP_MT_DATA_128 0x05
/** Flex Data Messages */
#define UMP_MT_FLEX_DATA 0x0d
/** UMP Stream Message */
/**
* UMP Stream Message
* @see midi_ump_stream
*/
#define UMP_MT_UMP_STREAM 0x0f
/** @} */
@@ -208,6 +211,149 @@ struct midi_ump {
#define UMP_SYS_RESET 0xff /**< Reset (no param) */
/** @} */
/**
* @defgroup midi_ump_stream UMP Stream specific fields
* @ingroup midi_ump
* @see ump112: 7.1 UMP Stream Messages
*
* @{
*/
/**
* @brief Format of a UMP Stream message
* @param[in] ump Universal MIDI Packet (containing a UMP Stream message)
* @see midi_ump_stream_format
*/
#define UMP_STREAM_FORMAT(ump) \
(((ump).data[0] >> 26) & BIT_MASK(2))
/**
* @defgroup midi_ump_stream_format UMP Stream format
* @ingroup midi_ump_stream
* @see ump112: 7.1 UMP Stream Messages: Format
* @remark When UMP_MT(x)=UMP_MT_UMP_STREAM,
* then UMP_STREAM_FORMAT(x) may be one of:
* @{
*/
/** Complete message in one UMP */
#define UMP_STREAM_FORMAT_COMPLETE 0x00
/** Start of a message which spans two or more UMPs */
#define UMP_STREAM_FORMAT_START 0x01
/** Continuing a message which spans three or more UMPs.
* There might be multiple Continue UMPs in a single message
*/
#define UMP_STREAM_FORMAT_CONTINUE 0x02
/** End of message which spans two or more UMPs */
#define UMP_STREAM_FORMAT_END 0x03
/** @} */
/**
* @brief Status field of a UMP Stream message
* @param[in] ump Universal MIDI Packet (containing a UMP Stream message)
* @see midi_ump_stream_status
*/
#define UMP_STREAM_STATUS(ump) \
(((ump).data[0] >> 16) & BIT_MASK(10))
/**
* @defgroup midi_ump_stream_status UMP Stream status
* @ingroup midi_ump_stream
* @see ump112: 7.1 UMP Stream Messages
* @remark When UMP_MT(x)=UMP_MT_UMP_STREAM,
* then UMP_STREAM_STATUS(x) may be one of:
* @{
*/
/** Endpoint Discovery Message */
#define UMP_STREAM_STATUS_EP_DISCOVERY 0x00
/** Endpoint Info Notification Message */
#define UMP_STREAM_STATUS_EP_INFO 0x01
/** Device Identity Notification Message */
#define UMP_STREAM_STATUS_DEVICE_IDENT 0x02
/** Endpoint Name Notification */
#define UMP_STREAM_STATUS_EP_NAME 0x03
/** Product Instance Id Notification Message */
#define UMP_STREAM_STATUS_PROD_ID 0x04
/** Stream Configuration Request Message */
#define UMP_STREAM_STATUS_CONF_REQ 0x05
/** Stream Configuration Notification Message */
#define UMP_STREAM_STATUS_CONF_NOTIF 0x06
/** Function Block Discovery Message */
#define UMP_STREAM_STATUS_FB_DISCOVERY 0x10
/** Function Block Info Notification */
#define UMP_STREAM_STATUS_FB_INFO 0x11
/** Function Block Name Notification */
#define UMP_STREAM_STATUS_FB_NAME 0x12
/** @} */
/**
* @brief Filter bitmap of an Endpoint Discovery message
* @param[in] ump Universal MIDI Packet (containing an Endpoint Discovery message)
* @see ump112: 7.1.1 Endpoint Discovery Message
* @see midi_ump_ep_disc
*/
#define UMP_STREAM_EP_DISCOVERY_FILTER(ump) \
((ump).data[1] & BIT_MASK(8))
/**
* @defgroup midi_ump_ep_disc UMP Stream endpoint discovery message filter bits
* @ingroup midi_ump_stream
* @see ump112: 7.1.1 Fig. 12: Endpoint Discovery Message Filter Bitmap Field
* @remark When UMP_MT(x)=UMP_MT_UMP_STREAM and
* UMP_STREAM_STATUS(x)=UMP_STREAM_STATUS_EP_DISCOVERY,
* then UMP_STREAM_EP_DISCOVERY_FILTER(x) may be an ORed combination of:
* @{
*/
/** Requesting an Endpoint Info Notification */
#define UMP_EP_DISC_FILTER_EP_INFO BIT(0)
/** Requesting a Device Identity Notification */
#define UMP_EP_DISC_FILTER_DEVICE_ID BIT(1)
/** Requesting an Endpoint Name Notification */
#define UMP_EP_DISC_FILTER_EP_NAME BIT(2)
/** Requesting a Product Instance Id Notification */
#define UMP_EP_DISC_FILTER_PRODUCT_ID BIT(3)
/** Requesting a Stream Configuration Notification */
#define UMP_EP_DISC_FILTER_STREAM_CFG BIT(4)
/** @} */
/**
* @brief Filter bitmap of a Function Block Discovery message
* @param[in] ump Universal MIDI Packet (containing a Function Block Discovery message)
* @see ump112: 7.1.7 Function Block Discovery Message
* @see midi_ump_fb_disc
*/
#define UMP_STREAM_FB_DISCOVERY_FILTER(ump) \
((ump).data[0] & BIT_MASK(8))
/**
* @brief Block number requested in a Function Block Discovery message
* @param[in] ump Universal MIDI Packet (containing a Function Block Discovery message)
* @see ump112: 7.1.7 Function Block Discovery Message
*/
#define UMP_STREAM_FB_DISCOVERY_NUM(ump) \
(((ump).data[0] >> 8) & BIT_MASK(8))
/**
* @defgroup midi_ump_fb_disc UMP Stream Function Block discovery message filter bits
* @ingroup midi_ump_stream
* @see ump112: 7.1.7 Fig. 21: Function Block Discovery Filter Bitmap Field Format
* @remark When UMP_MT(x)=UMP_MT_UMP_STREAM and
* UMP_STREAM_STATUS(x)=UMP_STREAM_STATUS_FB_DISCOVERY,
* then UMP_STREAM_FB_DISCOVERY_FILTER(x) may be an ORed combination of:
* @{
*/
/** Requesting a Function Block Info Notification */
#define UMP_FB_DISC_FILTER_INFO BIT(0)
/** Requesting a Function Block Name Notification */
#define UMP_FB_DISC_FILTER_NAME BIT(1)
/** @} */
/** @} */
/** @} */
#ifdef __cplusplus

View File

@@ -11,6 +11,7 @@ add_subdirectory_ifdef(CONFIG_CPP cpp)
add_subdirectory(hash)
add_subdirectory(heap)
add_subdirectory(mem_blocks)
add_subdirectory(midi2)
add_subdirectory_ifdef(CONFIG_NET_BUF net_buf)
add_subdirectory(os)
add_subdirectory(utils)

View File

@@ -15,6 +15,8 @@ source "lib/heap/Kconfig"
source "lib/mem_blocks/Kconfig"
source "lib/midi2/Kconfig"
source "lib/net_buf/Kconfig"
source "lib/os/Kconfig"

8
lib/midi2/CMakeLists.txt Normal file
View File

@@ -0,0 +1,8 @@
# Copyright (c) 2025 Titouan Christophe
# SPDX-License-Identifier: Apache-2.0
zephyr_include_directories(.)
if(CONFIG_MIDI2_UMP_STREAM_RESPONDER)
zephyr_sources(ump_stream_responder.c)
endif()

13
lib/midi2/Kconfig Normal file
View File

@@ -0,0 +1,13 @@
# Copyright (c) 2025 Titouan Christophe
# SPDX-License-Identifier: Apache-2.0
menu "MIDI2"
config MIDI2_UMP_STREAM_RESPONDER
bool "MIDI2 UMP Stream responder"
help
Library to respond to UMP Stream discovery messages, as specified
in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol"
version 1.1.2 section 7.1: "UMP Stream Messages"
endmenu

View File

@@ -0,0 +1,284 @@
/*
* Copyright (c) 2025 Titouan Christophe
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/sys/byteorder.h>
#include "ump_stream_responder.h"
#define BIT_IF(cond, n) ((cond) ? BIT(n) : 0)
/**
* @brief MIDI-CI version identifier for UMP v1.1 devices
* @see ump112: 7.1.8 FB Info Notification > MIDI-CI Message Version/Format
*/
#define MIDI_CI_VERSION_FORMAT_UMP_1_1 0x01
static inline bool ep_has_midi1(const struct ump_endpoint_dt_spec *ep)
{
for (size_t i = 0; i < ep->n_blocks; i++) {
if (ep->blocks[i].is_midi1) {
return true;
}
}
return false;
}
static inline bool ep_has_midi2(const struct ump_endpoint_dt_spec *ep)
{
for (size_t i = 0; i < ep->n_blocks; i++) {
if (!ep->blocks[i].is_midi1) {
return true;
}
}
return false;
}
/**
* @brief Build an Endpoint Info Notification Universal MIDI Packet
* @see ump112: 7.1.2 Endpoint Info Notification Message
*/
static inline struct midi_ump make_endpoint_info(const struct ump_endpoint_dt_spec *ep)
{
struct midi_ump res;
res.data[0] = (UMP_MT_UMP_STREAM << 28)
| (UMP_STREAM_STATUS_EP_INFO << 16)
| 0x0101; /* UMP version 1.1 */
res.data[1] = BIT(31) /* Static function blocks */
| ((ep->n_blocks) << 24)
| BIT_IF(ep_has_midi2(ep), 9)
| BIT_IF(ep_has_midi1(ep), 8);
return res;
}
/**
* @brief Build a Function Block Info Notification Universal MIDI Packet
* @see ump112: 7.1.8 Function Block Info Notification
*/
static inline struct midi_ump make_function_block_info(const struct ump_endpoint_dt_spec *ep,
size_t block_num)
{
const struct ump_block_dt_spec *block = &ep->blocks[block_num];
struct midi_ump res;
uint8_t midi1_mode = block->is_31250bps ? 2 : block->is_midi1 ? 1 : 0;
res.data[0] = (UMP_MT_UMP_STREAM << 28)
| (UMP_STREAM_STATUS_FB_INFO << 16)
| BIT(15) /* Block is active */
| (block_num << 8)
| BIT_IF(block->is_output, 5) /* UI hint Sender */
| BIT_IF(block->is_input, 4) /* UI hint Receiver */
| (midi1_mode << 2)
| BIT_IF(block->is_output, 1) /* Function block is output */
| BIT_IF(block->is_input, 0); /* Function block is input */
res.data[1] = (block->first_group << 24)
| (block->groups_spanned << 16)
| (MIDI_CI_VERSION_FORMAT_UMP_1_1 << 8) /* MIDI-CI for UMP v1.1 */
| 0xff; /* At most 255 simultaneous Sysex streams */
return res;
}
/**
* @brief Copy an ASCII string into a Universal MIDI Packet while leaving
* some most significant bytes untouched, such that the caller can
* set this prefix.
* @param ump The ump into which the string is copied
* @param[in] offset Number of bytes from the most-significant side to leave free
* @param[in] src The source string
* @param[in] len The length of the source string
* @return The number of bytes copied
*/
static inline size_t fill_str(struct midi_ump *ump, size_t offset,
const char *src, size_t len)
{
size_t i, j;
if (offset >= sizeof(struct midi_ump)) {
return 0;
}
for (i = 0; i < len && (j = i + offset) < sizeof(struct midi_ump); i++) {
ump->data[j / 4] |= src[i] << (8 * (3 - (j % 4)));
}
return i;
}
/**
* @brief Send a string as UMP Stream, possibly splitting into multiple
* packets if the string length is larger than 1 UMP
* @param[in] cfg The responder configuration
* @param[in] string The string to send
* @param[in] prefix The fixed prefix of UMP packets to send
* @param[in] offset The offset the strings starts in the packet, in bytes
*
* @return The number of packets sent
*/
static inline int send_string(const struct ump_stream_responder_cfg *cfg,
const char *string, uint32_t prefix, size_t offset)
{
struct midi_ump reply;
size_t stringlen = strlen(string);
size_t strwidth = sizeof(reply) - offset;
uint8_t format;
size_t i = 0;
int res = 0;
while (i < stringlen) {
memset(&reply, 0, sizeof(reply));
format = (i == 0)
? (stringlen - i <= strwidth)
? UMP_STREAM_FORMAT_COMPLETE
: UMP_STREAM_FORMAT_START
: (stringlen - i > strwidth)
? UMP_STREAM_FORMAT_CONTINUE
: UMP_STREAM_FORMAT_END;
reply.data[0] = (UMP_MT_UMP_STREAM << 28)
| (format << 26)
| prefix;
i += fill_str(&reply, offset, &string[i], stringlen - i);
cfg->send(cfg->dev, reply);
res++;
}
return res;
}
/**
* @brief Handle Endpoint Discovery messages
* @param[in] cfg The responder configuration
* @param[in] pkt The discovery packet to handle
* @return The number of UMP sent as reply
*/
static inline int ump_ep_discover(const struct ump_stream_responder_cfg *cfg,
const struct midi_ump pkt)
{
int res = 0;
uint8_t filter = UMP_STREAM_EP_DISCOVERY_FILTER(pkt);
/* Request for Endpoint Info Notification */
if ((filter & UMP_EP_DISC_FILTER_EP_INFO) != 0U) {
cfg->send(cfg->dev, make_endpoint_info(cfg->ep_spec));
res++;
}
/* Request for Endpoint Name Notification */
if ((filter & UMP_EP_DISC_FILTER_EP_NAME) != 0U && cfg->ep_spec->name != NULL) {
res += send_string(cfg, cfg->ep_spec->name,
UMP_STREAM_STATUS_EP_NAME << 16, 2);
}
/* Request for Product Instance ID */
if ((filter & UMP_EP_DISC_FILTER_PRODUCT_ID) != 0U && IS_ENABLED(CONFIG_HWINFO)) {
res += send_string(cfg, ump_product_instance_id(),
UMP_STREAM_STATUS_PROD_ID << 16, 2);
}
return res;
}
static inline int ump_fb_discover_block(const struct ump_stream_responder_cfg *cfg,
size_t block_num, uint8_t filter)
{
int res = 0;
const struct ump_block_dt_spec *blk = &cfg->ep_spec->blocks[block_num];
if ((filter & UMP_FB_DISC_FILTER_INFO) != 0U) {
cfg->send(cfg->dev, make_function_block_info(cfg->ep_spec, block_num));
res++;
}
if ((filter & UMP_FB_DISC_FILTER_NAME) != 0U && blk->name != NULL) {
res += send_string(cfg, blk->name,
(UMP_STREAM_STATUS_FB_NAME << 16) | (block_num << 8), 3);
}
return res;
}
/**
* @brief Handle Function Block Discovery messages
* @param[in] cfg The responder configuration
* @param[in] pkt The discovery packet to handle
* @return The number of UMP sent as reply
*/
static inline int ump_fb_discover(const struct ump_stream_responder_cfg *cfg,
const struct midi_ump pkt)
{
int res = 0;
uint8_t block_num = UMP_STREAM_FB_DISCOVERY_NUM(pkt);
uint8_t filter = UMP_STREAM_FB_DISCOVERY_FILTER(pkt);
if (block_num < cfg->ep_spec->n_blocks) {
res += ump_fb_discover_block(cfg, block_num, filter);
} else if (block_num == 0xff) {
/* Requesting information for all blocks at once */
for (block_num = 0; block_num < cfg->ep_spec->n_blocks; block_num++) {
res += ump_fb_discover_block(cfg, block_num, filter);
}
}
return res;
}
const char *ump_product_instance_id(void)
{
static char product_id[43] = "";
static const char hex[] = "0123456789ABCDEF";
if (IS_ENABLED(CONFIG_HWINFO) && product_id[0] == '\0') {
uint8_t devid[sizeof(product_id) / 2];
ssize_t len = hwinfo_get_device_id(devid, sizeof(devid));
if (len == -ENOSYS && hwinfo_get_device_eui64(devid) == 0) {
/* device id unavailable, but there is an eui64,
* which has a fixed length of 8
*/
len = 8;
} else if (len < 0) {
/* Other hwinfo driver error; mark as empty */
len = 0;
}
/* Convert to hex string */
for (ssize_t i = 0; i < len; i++) {
product_id[2 * i] = hex[devid[i] >> 4];
product_id[2 * i + 1] = hex[devid[i] & 0xf];
}
product_id[2*len] = '\0';
}
return product_id;
}
int ump_stream_respond(const struct ump_stream_responder_cfg *cfg,
const struct midi_ump pkt)
{
if (cfg->send == NULL) {
return -EINVAL;
}
if (UMP_MT(pkt) != UMP_MT_UMP_STREAM) {
return 0;
}
switch (UMP_STREAM_STATUS(pkt)) {
case UMP_STREAM_STATUS_EP_DISCOVERY:
return ump_ep_discover(cfg, pkt);
case UMP_STREAM_STATUS_FB_DISCOVERY:
return ump_fb_discover(cfg, pkt);
}
return 0;
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2025 Titouan Christophe
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_LIB_MIDI2_UMP_STREAM_RESPONDER_H_
#define ZEPHYR_LIB_MIDI2_UMP_STREAM_RESPONDER_H_
/**
* @brief Respond to UMP Stream message Endpoint or Function Block discovery
* @defgroup ump_stream_responder UMP Stream Responder
* @ingroup midi_ump
* @since 4.3
* @version 0.1.0
* @see ump112 7.1: UMP Stream Messages
* @{
*/
#include <zephyr/kernel.h>
#include <zephyr/audio/midi.h>
/**
* @brief UMP Function Block specification
* @see ump112: 6: Function Blocks
*/
struct ump_block_dt_spec {
/** Name of this function block, or NULL if unnamed */
const char *name;
/** Number of the first UMP group in this block */
uint8_t first_group;
/** Number of (contiguous) UMP groups spanned by this block */
uint8_t groups_spanned;
/** True if this function block is an input */
bool is_input;
/** True if this function block is an output */
bool is_output;
/** True if this function block carries MIDI1 data only */
bool is_midi1;
/** True if this function block is physically wired to a (MIDI1)
* serial interface, where data is transmitted at the standard
* baud rate of 31250 b/s
*/
bool is_31250bps;
};
/**
* @brief UMP endpoint specification
*/
struct ump_endpoint_dt_spec {
/** Name of this endpoint, or NULL if unnamed */
const char *name;
/** Number of function blocks in this endpoint */
size_t n_blocks;
/** Function blocks in this endpoint */
struct ump_block_dt_spec blocks[];
};
/**
* @brief Configuration for the UMP Stream responder
*/
struct ump_stream_responder_cfg {
/** The device to send reply packets */
void *dev;
/** The function to call to send a reply packet */
void (*send)(void *dev, const struct midi_ump ump);
/** The UMP endpoint specification */
const struct ump_endpoint_dt_spec *ep_spec;
};
/**
* @brief Get a Universal MIDI Packet endpoint function block from its
* device-tree representation
* @param _node The device tree node representing the midi2 block
*/
#define UMP_BLOCK_DT_SPEC_GET(_node) \
{ \
.name = DT_PROP_OR(_node, label, NULL), \
.first_group = DT_REG_ADDR(_node), \
.groups_spanned = DT_REG_SIZE(_node), \
.is_input = !DT_ENUM_HAS_VALUE(_node, terminal_type, output_only), \
.is_output = !DT_ENUM_HAS_VALUE(_node, terminal_type, input_only), \
.is_midi1 = !DT_ENUM_HAS_VALUE(_node, protocol, midi2), \
.is_31250bps = DT_PROP(_node, serial_31250bps), \
}
#define UMP_BLOCK_SEP_IF_OKAY(_node) \
COND_CODE_1(DT_NODE_HAS_STATUS_OKAY(_node), \
(UMP_BLOCK_DT_SPEC_GET(_node),), \
())
/**
* @brief Get a Universal MIDI Packet endpoint description from
* the device-tree representation of a midi2 device
* @param _node The device tree node representing a midi2 device
*/
#define UMP_ENDPOINT_DT_SPEC_GET(_node) \
{ \
.name = DT_PROP_OR(_node, label, NULL), \
.n_blocks = DT_FOREACH_CHILD_SEP(_node, DT_NODE_HAS_STATUS_OKAY, (+)), \
.blocks = {DT_FOREACH_CHILD(_node, UMP_BLOCK_SEP_IF_OKAY)}, \
}
/**
* @brief Respond to an UMP Stream message
* @param[in] cfg The responder configuration
* @param[in] pkt The message to respond to
* @return The number of UMP packets sent as reply,
* or -errno in case of error
*/
int ump_stream_respond(const struct ump_stream_responder_cfg *cfg,
const struct midi_ump pkt);
/**
* @return The UMP Product Instance ID of this device, based on the device
* hwinfo if available, otherwise an empty string
*/
const char *ump_product_instance_id(void);
/** @} */
#endif