diff --git a/doc/zephyr.doxyfile.in b/doc/zephyr.doxyfile.in index 1dbc5ecd0d2..cea0300cfb0 100644 --- a/doc/zephyr.doxyfile.in +++ b/doc/zephyr.doxyfile.in @@ -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/ \ diff --git a/dts/bindings/usb/zephyr,midi2-device.yaml b/dts/bindings/usb/zephyr,midi2-device.yaml index 851916e7e87..1113c1886ef 100644 --- a/dts/bindings/usb/zephyr,midi2-device.yaml +++ b/dts/bindings/usb/zephyr,midi2-device.yaml @@ -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. diff --git a/include/zephyr/audio/midi.h b/include/zephyr/audio/midi.h index acc7d51338f..5e59dd444a1 100644 --- a/include/zephyr/audio/midi.h +++ b/include/zephyr/audio/midi.h @@ -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 diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index fad0e383c90..f960141b7af 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -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) diff --git a/lib/Kconfig b/lib/Kconfig index 149c1317e49..7f2a5b2a15b 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -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" diff --git a/lib/midi2/CMakeLists.txt b/lib/midi2/CMakeLists.txt new file mode 100644 index 00000000000..49a11c2ed85 --- /dev/null +++ b/lib/midi2/CMakeLists.txt @@ -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() diff --git a/lib/midi2/Kconfig b/lib/midi2/Kconfig new file mode 100644 index 00000000000..d4ab789c303 --- /dev/null +++ b/lib/midi2/Kconfig @@ -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 diff --git a/lib/midi2/ump_stream_responder.c b/lib/midi2/ump_stream_responder.c new file mode 100644 index 00000000000..7ffffbf8732 --- /dev/null +++ b/lib/midi2/ump_stream_responder.c @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#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; +} diff --git a/lib/midi2/ump_stream_responder.h b/lib/midi2/ump_stream_responder.h new file mode 100644 index 00000000000..420e206de38 --- /dev/null +++ b/lib/midi2/ump_stream_responder.h @@ -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 +#include + +/** + * @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