diff --git a/CHANGELOG.md b/CHANGELOG.md index 2411f0e..1cad143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,42 +5,115 @@ All notable changes to `midi2_cpp` are recorded here. Format follows adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), mirrored from the upstream midi2 C99 policy. -## [Unreleased] +## [0.2.0] -### Examples / Recipes (added) +Single source of truth for the MIDI 2.0 stack: midi2_cpp no longer +vendors the C99 core and is published as a regular Arduino / +PlatformIO library that depends on midi2 explicitly. Every recipe +under `examples/` was migrated to pull midi2 externally through the +build system that fits its host (FetchContent for Pico SDK + TinyUSB +native CMake, IDF Component Manager for ESP-IDF, lib_deps for +PlatformIO). -- `arduino-nano-esp32-midi2`, Arduino Nano ESP32 (ESP32-S3-MINI-1, PID - 0x4093). Full Showcase mirroring `esp32-s3-devkitc-usb-midi2`, single - GPIO LED on D13 / GPIO48 instead of WS2812 (Arduino Nano ESP32 has no - RMT-driven addressable LED in this recipe; the on-board RGB LED on - D14 / D15 / D16 is left untouched). +This is a breaking release. Consumers that previously vendored +`midi2_cpp/src/midi2.{h,c}` directly will break; the migration path +is documented in the manifest below and in the per-build-system +patterns shipped under `examples/`. + +### Breaking + +- **Vendored `src/midi2.{h,c}` removed.** midi2_cpp now declares + midi2 as an external dependency: + - `library.properties` carries `depends=midi2 (>=0.3.3)`. Arduino + Library Manager auto-installs midi2 when a sketch includes + midi2_cpp. + - `library.json` carries `dependencies."sauloverissimo/midi2": + "^0.3.3"`. PlatformIO resolves midi2 from its registry. + - The root `CMakeLists.txt` exposes a three-layer fallback at the + top (`if(NOT TARGET midi2)` -> `find_package(midi2 0.3.3 CONFIG)` + -> `FetchContent_Declare(midi2 GIT_TAG v0.3.3)`), then links + midi2_cpp `PUBLIC midi2::midi2` so downstream targets see the + C99 core transitively. + +### Added + +- **`midi2::Bridge` (alias `m2bridge`)**: composes Device + CI + Host + with a multi-Function-Block topology, a per-slot group rewrite + window, dynamic FB names sourced from upstream Endpoint Names, and + a USB-MIDI 1.0 byte-stream uplift path (`feedHostMidi1Bytes`) for + legacy upstream devices that arrive on alt 0. Slot lifecycle via + `slotSetActive(idx, active, alt)`. Reusable across bridge recipes; + the multi-FB Stream Discovery responder lives inside the class so + each new bridge recipe gets it for free. +- **`tests/test_midi2_bridge.cpp`**: 11 host-side sub-tests covering + m2bridge construct/destruct heap balance (50x cycle stress), topology + setter bounds and post-`begin()` lock, group rewrite formula on + slots 0/1/3, out-of-range slot rejection, and the USB-MIDI 1.0 + byte-stream uplift path. Compiles and runs clean under + `-fsanitize=address,undefined`. +- `architecture.png` referenced from the README, replacing the + previous inline ASCII layer diagram. +- **CMake entry surface for downstream consumers**: the root + `CMakeLists.txt` follows the same `find_package` -> `FetchContent` + fallback pattern that midi2 itself ships. Subprojects pulling + midi2_cpp via `add_subdirectory` or `FetchContent` skip the + `find_package` step (`if(NOT TARGET midi2)` guard). + +### Changed + +- **README tagline** drops the `zero-allocation` claim. midi2_cpp + allocates in two narrow places (`m2bridge::begin()` slot tables and + `std::function` callback storage), so the wrapper is now described + as `static-by-default`. The C99 core (midi2) remains strictly + zero-allocation. Same shift applied to the logo and to the + `.intern/decisoes.md` design heritage notes. +- **README "Manual vendor" path** rewritten: pre-v0.2 builds vendored + a single `midi2_cpp/src/midi2.{h,c}` copy; today the consumer + downloads both repositories side by side and adds `midi2/dist/` + plus `midi2_cpp/src/` to its include path. +- **`paragraph` in `library.properties`** rewritten: drops + comparisons with other libraries, focuses on what midi2_cpp itself + ships and the embedded targets validated. + +### Examples / Recipes + +#### Migrated to depend on midi2 externally (all 20 recipes) + +| Build system | Mechanism | Recipes | +|---|---|---| +| Pico SDK | `FetchContent_Declare(midi2 GIT_TAG v0.3.3)` plus `target_link_libraries(midi2_cpp PUBLIC midi2::midi2)` | `rp2040-midi2`, `waveshare-rp2040-midi2`, `sparkfun-promicro-rp2350-midi2`, `waveshare-rp2350-usb-a-midi2`, `waveshare-rp2350-usb-a-bridge-midi2`, `adafruit-feather-rp2040-host-midi2`, `adafruit-feather-rp2040-bridge-midi2`, `rp2040-promicro-ump-test-bench` | +| TinyUSB native CMake | same FetchContent pattern as Pico SDK | `xiao-samd21-midi2`, `nrf52840-promicro-midi2` | +| ESP-IDF | `idf_component.yml` declares `midi2: { git: ..., version: ">=0.3.3" }` and `idf_component_register` lists `midi2` in `REQUIRES` | `arduino-nano-esp32-midi2`, `esp32-s3-devkitc-usb-midi2`, `esp32-p4-devkit-usb-midi2`, `esp32-p4-devkit-host-midi2`, `esp32-p4-devkit-bridge-midi2`, `esp32-p4-devkit-bridge2-midi2`, `t-display-s3-midi2` | +| PlatformIO + ESP32_Host_MIDI | `lib_deps += sauloverissimo/midi2 @ ^0.3.3` | `esp32-c6-devkitc-multi-midi2`, `esp32-s3-devkitc-host-midi2`, `t-display-s3-shield-host-midi2` | + +Each recipe drops the `${MIDI2_CPP_ROOT}/src/midi2.c` (or `midi2_c99` +helper library) from its source list. Other midi2_cpp sources +(`midi2_device.cpp`, `midi2_ci.cpp`, `midi2_host.cpp`, +`midi2_bridge.cpp`) keep being compiled inline from the parent tree +via `${MIDI2_CPP_ROOT}/src` until the host helper-library shape is +finalised in a future cycle. + +#### New recipes since v0.1.0 + +- `arduino-nano-esp32-midi2`, Arduino Nano ESP32 (ESP32-S3-MINI-1, + PID 0x4093). Full Showcase mirroring `esp32-s3-devkitc-usb-midi2`; + single GPIO LED on D13 / GPIO48 instead of WS2812. - `xiao-samd21-midi2`, Seeed Studio XIAO SAMD21 (ATSAMD21G18A, PID - 0x40F0). Tier C minimal core: NoteOn/Off + CC + UMP Stream Discovery - + MIDI-CI Discovery + JR Timestamp heartbeat. Build via TinyUSB - native CMake (`hw/bsp/family_support.cmake` + FetchContent of the - PR #3571 fork at the project's pinned SHA) on top of the upstream - TinyUSB BSP `seeeduino_xiao`. ARM GNU toolchain, no Arduino IDE - involved. First recipe in the project portfolio to use this build - system path; pattern documented in `.intern/decisions.md` D-033. - Hardware validated 2026-04-30: device enumerates `cafe:40F0`, - ALSA shows `Group 1 (Main)`, chromatic walk + 32-bit CC #74 sweep - streaming live. Final size: text 34884 / 256K flash (13%), bss - 9832 / 32K SRAM (30%). + 0x40F0). Tier C minimal core; first recipe to use the TinyUSB + native CMake build system path. Hardware validated: ALSA `Group 1 + (Main)`, chromatic walk + 32-bit CC #74 sweep streaming. Final + size: text 34884 / 256K flash (13%), bss 9832 / 32K SRAM (30%). - `nrf52840-promicro-midi2`, nRF52840 Pro Micro / Nice!Nano class - boards (PID 0x40F1). Tier B standard subset: Per-Note Pitch Bend - vibrato + chromatic walk + RPN / NRPN / RelRPN / RelNRPN burst + - UMP Stream Discovery + MIDI-CI Discovery + JR Timestamp heartbeat. - Build via the same TinyUSB native CMake path used by - `xiao-samd21-midi2` (BSP `feather_nrf52840_express` upstream, Nice!Nano - shares the same nRF52840 + Adafruit UF2 bootloader region layout + - S140 v6 SoftDevice RAM reservation). Drops the earlier - Adafruit_TinyUSB_Arduino-based attempt that did not work on Seeed / - Nice!Nano cores. Hardware validated 2026-04-30 on Nice!Nano: device - enumerates `cafe:40F1`, ALSA shows `Group 1 (Main)`, full Tier B - cycle streaming live in `aseqdump`. Final size: text 38832 / 1 MB - flash (3.7%), bss 2526 / 256 KB SRAM (1%) — the chip is wildly - oversized for the recipe, leaving room for BLE-MIDI 2.0 + - application code on top. + (PID 0x40F1). Tier B subset: Per-Note Pitch Bend vibrato + + chromatic walk + RPN / NRPN / RelRPN / RelNRPN burst. Same TinyUSB + native CMake build path as the SAMD21 recipe. Hardware validated on + Nice!Nano. Final size: text 38832 / 1 MB flash (3.7%), bss 2526 / + 256 KB SRAM (1%). +- `esp32-p4-devkit-bridge2-midi2`, ESP32-P4 dual-stack bridge (PID + 0x4095) built on top of `m2bridge`. Carries the same multi-FB + topology as `esp32-p4-devkit-bridge-midi2` but consumes the + reusable Bridge class instead of an inline slot table + Stream + Discovery responder. ## [0.1.0] diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a0b587..f5e0621 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,44 @@ cmake_minimum_required(VERSION 3.14) -project(midi2_cpp VERSION 0.1.0 LANGUAGES C CXX) +project(midi2_cpp VERSION 0.2.0 LANGUAGES C CXX) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Library target +# midi2 C99 core, resolved as an external dependency. +# +# Three-layer fallback: parent project's target -> system install +# (vcpkg / conan / system package) -> FetchContent from GitHub. +# This is the same pattern the midi2 README documents for downstream +# consumers, applied to midi2_cpp itself. +if(NOT TARGET midi2) + find_package(midi2 0.3.3 QUIET CONFIG) + if(NOT midi2_FOUND) + include(FetchContent) + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.3 + ) + FetchContent_MakeAvailable(midi2) + endif() +endif() + +# Library target. +# +# midi2_cpp ships the C++ wrapper sources only; the C99 core lives in +# the external midi2 target this CMakeLists pulls above. Consumers +# linking midi2_cpp transitively see midi2::midi2 because of the +# PUBLIC link below. add_library(midi2_cpp - src/midi2.c src/midi2_device.cpp src/midi2_ci.cpp src/midi2_host.cpp src/midi2_bridge.cpp ) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) + target_include_directories(midi2_cpp PUBLIC $ diff --git a/README.md b/README.md index 5c52553..8d6bcf3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![midi2_cpp](logo_midi2_cpp.png) -*C++17, callback-first, zero-allocation, zero external dependencies, MIT.* From DIY to professional products. +*C++17, callback-first, static-by-default, zero external dependencies, MIT.* From DIY to professional products. [![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C.svg)](https://en.cppreference.com/cpp/compiler_support) [![MIDI 2.0](https://img.shields.io/badge/MIDI-2.0-blueviolet.svg)](https://midi.org/specifications/midi-2-0-specifications) @@ -22,7 +22,7 @@ midi2_cpp is the layer where a sketch meets the protocol. Plug a board into the laptop, write five lines of C++, flash, and the device appears on the bus as a USB MIDI 2.0 endpoint with full Capability Inquiry, Property Exchange, and 32-bit resolution. -Underneath, midi2 (the portable C99 core) handles parsing, dispatch, and reassembly. midi2_cpp adds the C++ ergonomics: callbacks, board glue, ready-made USB descriptors. The board does the talking; the sketch tells it what to say. +Underneath, [midi2](https://github.com/sauloverissimo/midi2) (the portable C99 core) handles parsing, dispatch, and reassembly. midi2_cpp adds the C++ ergonomics: callbacks, board glue, ready-made USB descriptors. The board does the talking; the sketch tells it what to say. ## Contents @@ -132,7 +132,7 @@ The four hooks (`setWriteFn`, `feedRx`, `setNowFn`, `setMounted` + `setAltSettin - USB MIDI 2.0 device, host, or both, depending on the board. - 49 typed UMP callbacks: notes, CCs, RPN/NRPN, per-note expression, Flex Data, Stream messages. - MIDI-CI out of the box: Discovery, Profile negotiation, Property Exchange (with Subscribe/Notify), Process Inquiry. -- Static configuration. No `malloc`, no `new`. Sized at compile time, fits a Cortex-M0+. +- Static-by-default. The hot path is allocation-free; init-time `new` only inside `m2bridge` for the per-slot tables. Fits a Cortex-M0+. - Pay-as-you-go: only the modules called by the sketch end up in the binary. ## Three shapes @@ -198,10 +198,10 @@ Library Manager: search `midi2_cpp`, click Install. The dependency on `midi2` is ```ini lib_deps = - https://github.com/sauloverissimo/midi2_cpp.git#v0.1.0 + https://github.com/sauloverissimo/midi2_cpp.git#main ``` -Pin by tag for reproducibility. Pin by commit hash when a specific point in `main` is needed. +Pin by tag for reproducibility once releases ship; until then, pin by commit hash when a specific point in `main` is needed. ### ESP-IDF component @@ -214,7 +214,7 @@ include(FetchContent) FetchContent_Declare( midi2_cpp GIT_REPOSITORY https://github.com/sauloverissimo/midi2_cpp.git - GIT_TAG v0.1.0 + GIT_TAG main ) FetchContent_MakeAvailable(midi2_cpp) ``` @@ -227,7 +227,7 @@ git submodule add https://github.com/sauloverissimo/midi2_cpp.git external/midi2 ### Manual vendor -Download the repo. Add `src/` to includes. Compile `src/midi2.c`, `src/midi2_device.cpp`, and `src/midi2_ci.cpp` alongside the project. No external links required. +Download the [midi2_cpp](https://github.com/sauloverissimo/midi2_cpp) and [midi2](https://github.com/sauloverissimo/midi2) repositories side by side. Add `midi2/dist/` and `midi2_cpp/src/` to includes. Compile `midi2/dist/midi2.c`, `midi2_cpp/src/midi2_device.cpp`, `midi2_cpp/src/midi2_ci.cpp`, and the host/bridge `.cpp` files you need alongside the project. No package manager required at build time, but the two repos must travel together. ## API at a glance @@ -257,19 +257,9 @@ Async, callback-first, copy-paste-ready. Same shape as MIDI 1.0 Arduino librarie ## Architecture -midi2_cpp is the platform layer of a 4-layer MIDI 2.0 stack: +midi2_cpp: platform layer of a 4-layer MIDI 2.0 stack: -``` -┌──────────────────────────────────────┐ -│ Sketch │ user code -├──────────────────────────────────────┤ -│ midi2_cpp │ ***this library*** -├──────────────────────────────────────┤ -│ midi2 │ portable C99 core (vendored) -├──────────────────────────────────────┤ -│ TinyUSB / Native USB / PIO-USB / BLE │ transport (caller-wired) -└──────────────────────────────────────┘ -``` +![midi2_cpp](architecture.png) The sketch touches the top. The rest is invisible until needed. diff --git a/architecture.png b/architecture.png new file mode 100644 index 0000000..36ec377 Binary files /dev/null and b/architecture.png differ diff --git a/examples/adafruit-feather-rp2040-bridge-midi2/CMakeLists.txt b/examples/adafruit-feather-rp2040-bridge-midi2/CMakeLists.txt index 3efff2c..47c444d 100644 --- a/examples/adafruit-feather-rp2040-bridge-midi2/CMakeLists.txt +++ b/examples/adafruit-feather-rp2040-bridge-midi2/CMakeLists.txt @@ -71,10 +71,23 @@ pico_sdk_init() # the single .c from the parent library tree to keep the binary lean. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") -add_library(midi2_c99 STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c -) -target_include_directories(midi2_c99 PUBLIC ${MIDI2_CPP_ROOT}/src) + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() # --------------------------------------------------------------------------- # Showcase executable. @@ -90,7 +103,7 @@ target_include_directories(adafruit-feather-rp2040-bridge-midi2-showcase PRIVATE target_link_libraries(adafruit-feather-rp2040-bridge-midi2-showcase PRIVATE - midi2_c99 + midi2::midi2 pico_stdlib hardware_i2c # SSD1306 over I2C1 tinyusb_device diff --git a/examples/adafruit-feather-rp2040-host-midi2/CMakeLists.txt b/examples/adafruit-feather-rp2040-host-midi2/CMakeLists.txt index c8d863e..726db77 100644 --- a/examples/adafruit-feather-rp2040-host-midi2/CMakeLists.txt +++ b/examples/adafruit-feather-rp2040-host-midi2/CMakeLists.txt @@ -78,13 +78,31 @@ pico_sdk_init() # copy needed. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ${MIDI2_CPP_ROOT}/src/midi2_host.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) # --------------------------------------------------------------------------- # Pico-PIO-USB sources compiled into our target. The library doesn't diff --git a/examples/arduino-nano-esp32-midi2/idf/main/CMakeLists.txt b/examples/arduino-nano-esp32-midi2/idf/main/CMakeLists.txt index b2e2171..f266e25 100644 --- a/examples/arduino-nano-esp32-midi2/idf/main/CMakeLists.txt +++ b/examples/arduino-nano-esp32-midi2/idf/main/CMakeLists.txt @@ -11,13 +11,13 @@ idf_component_register( SRCS "main.cpp" "arduino_nano_esp32_midi2.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_device.cpp" "${MIDI2_CPP_ROOT}/midi2_ci.cpp" INCLUDE_DIRS "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb driver esp_timer diff --git a/examples/arduino-nano-esp32-midi2/idf/main/idf_component.yml b/examples/arduino-nano-esp32-midi2/idf/main/idf_component.yml index f98d7cc..ed7cd23 100644 --- a/examples/arduino-nano-esp32-midi2/idf/main/idf_component.yml +++ b/examples/arduino-nano-esp32-midi2/idf/main/idf_component.yml @@ -5,3 +5,6 @@ ## ./scripts/fetch_tinyusb.sh once to clone the fork at the pinned SHA. dependencies: idf: ">=5.4" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/esp32-c6-devkitc-multi-midi2/pio/platformio.ini b/examples/esp32-c6-devkitc-multi-midi2/pio/platformio.ini index 9308f0d..97cc8bb 100644 --- a/examples/esp32-c6-devkitc-multi-midi2/pio/platformio.ini +++ b/examples/esp32-c6-devkitc-multi-midi2/pio/platformio.ini @@ -13,10 +13,10 @@ ; ; Both transports carry MIDI 1.0 byte streams on the wire (that is what ; the BLE-MIDI spec and the ESP-NOW recipe support natively). Bytes are -; uplifted to UMP MT 0x2 in firmware via midi2_conv (vendored in -; midi2_cpp/src/midi2.h) before being handed to the typed midi2::Device -; dispatch path. Outgoing UMP from the showcase loop is downgraded back -; to MIDI 1.0 bytes for the wire side. +; uplifted to UMP MT 0x2 in firmware via midi2_conv (from the midi2 C99 +; core pulled through lib_deps) before being handed to the typed +; midi2::Device dispatch path. Outgoing UMP from the showcase loop is +; downgraded back to MIDI 1.0 bytes for the wire side. ; ; No USB device interface is presented; therefore no PID is consumed ; from the project pool. Identity surface is per-transport (BLE service @@ -53,6 +53,7 @@ build_flags = lib_extra_dirs = ../../.. lib_deps = + sauloverissimo/midi2 @ ^0.3.3 https://github.com/sauloverissimo/ESP32_Host_MIDI.git#v6.0.1 ; Pre-build patch: ESP32_Host_MIDI v6.0.0 ships USB Host transport sources diff --git a/examples/esp32-p4-devkit-bridge-midi2/idf/main/CMakeLists.txt b/examples/esp32-p4-devkit-bridge-midi2/idf/main/CMakeLists.txt index df1c4ad..f945133 100644 --- a/examples/esp32-p4-devkit-bridge-midi2/idf/main/CMakeLists.txt +++ b/examples/esp32-p4-devkit-bridge-midi2/idf/main/CMakeLists.txt @@ -14,7 +14,6 @@ idf_component_register( SRCS "main.cpp" "esp32_p4_devkit_bridge.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_device.cpp" "${MIDI2_CPP_ROOT}/midi2_ci.cpp" "${MIDI2_CPP_ROOT}/midi2_host.cpp" @@ -22,6 +21,7 @@ idf_component_register( "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb driver esp_timer diff --git a/examples/esp32-p4-devkit-bridge-midi2/idf/main/idf_component.yml b/examples/esp32-p4-devkit-bridge-midi2/idf/main/idf_component.yml index d6c8ea4..33c624c 100644 --- a/examples/esp32-p4-devkit-bridge-midi2/idf/main/idf_component.yml +++ b/examples/esp32-p4-devkit-bridge-midi2/idf/main/idf_component.yml @@ -5,3 +5,6 @@ ## ./scripts/fetch_tinyusb.sh once to clone the fork at the pinned SHA. dependencies: idf: ">=5.4" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/esp32-p4-devkit-bridge2-midi2/idf/main/CMakeLists.txt b/examples/esp32-p4-devkit-bridge2-midi2/idf/main/CMakeLists.txt index 366c797..2e9c476 100644 --- a/examples/esp32-p4-devkit-bridge2-midi2/idf/main/CMakeLists.txt +++ b/examples/esp32-p4-devkit-bridge2-midi2/idf/main/CMakeLists.txt @@ -16,7 +16,6 @@ idf_component_register( SRCS "main.cpp" "esp32_p4_devkit_bridge2.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_device.cpp" "${MIDI2_CPP_ROOT}/midi2_ci.cpp" "${MIDI2_CPP_ROOT}/midi2_host.cpp" @@ -25,6 +24,7 @@ idf_component_register( "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb driver esp_timer diff --git a/examples/esp32-p4-devkit-bridge2-midi2/idf/main/idf_component.yml b/examples/esp32-p4-devkit-bridge2-midi2/idf/main/idf_component.yml index d6c8ea4..33c624c 100644 --- a/examples/esp32-p4-devkit-bridge2-midi2/idf/main/idf_component.yml +++ b/examples/esp32-p4-devkit-bridge2-midi2/idf/main/idf_component.yml @@ -5,3 +5,6 @@ ## ./scripts/fetch_tinyusb.sh once to clone the fork at the pinned SHA. dependencies: idf: ">=5.4" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/esp32-p4-devkit-host-midi2/idf/main/CMakeLists.txt b/examples/esp32-p4-devkit-host-midi2/idf/main/CMakeLists.txt index e0d86ce..eb04800 100644 --- a/examples/esp32-p4-devkit-host-midi2/idf/main/CMakeLists.txt +++ b/examples/esp32-p4-devkit-host-midi2/idf/main/CMakeLists.txt @@ -11,12 +11,12 @@ idf_component_register( SRCS "main.cpp" "esp32_p4_devkit_host.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_host.cpp" INCLUDE_DIRS "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb driver esp_timer diff --git a/examples/esp32-p4-devkit-host-midi2/idf/main/idf_component.yml b/examples/esp32-p4-devkit-host-midi2/idf/main/idf_component.yml index 6f44419..8c9186c 100644 --- a/examples/esp32-p4-devkit-host-midi2/idf/main/idf_component.yml +++ b/examples/esp32-p4-devkit-host-midi2/idf/main/idf_component.yml @@ -5,3 +5,6 @@ ## ./scripts/fetch_tinyusb.sh once to clone the fork at the pinned SHA. dependencies: idf: ">=5.4" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/esp32-p4-devkit-usb-midi2/idf/main/CMakeLists.txt b/examples/esp32-p4-devkit-usb-midi2/idf/main/CMakeLists.txt index 34222e2..ecfe576 100644 --- a/examples/esp32-p4-devkit-usb-midi2/idf/main/CMakeLists.txt +++ b/examples/esp32-p4-devkit-usb-midi2/idf/main/CMakeLists.txt @@ -11,13 +11,13 @@ idf_component_register( SRCS "main.cpp" "esp32_p4_devkit_midi2.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_device.cpp" "${MIDI2_CPP_ROOT}/midi2_ci.cpp" INCLUDE_DIRS "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb driver esp_timer diff --git a/examples/esp32-p4-devkit-usb-midi2/idf/main/idf_component.yml b/examples/esp32-p4-devkit-usb-midi2/idf/main/idf_component.yml index b34e0e8..bc0d27d 100644 --- a/examples/esp32-p4-devkit-usb-midi2/idf/main/idf_component.yml +++ b/examples/esp32-p4-devkit-usb-midi2/idf/main/idf_component.yml @@ -9,3 +9,6 @@ ## indicator is a no-op (see esp32_p4_devkit_midi2.cpp). dependencies: idf: ">=5.4" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/esp32-s3-devkitc-host-midi2/pio/platformio.ini b/examples/esp32-s3-devkitc-host-midi2/pio/platformio.ini index 80e4da7..8f973c4 100644 --- a/examples/esp32-s3-devkitc-host-midi2/pio/platformio.ini +++ b/examples/esp32-s3-devkitc-host-midi2/pio/platformio.ini @@ -36,4 +36,5 @@ build_flags = lib_extra_dirs = ../../.. lib_deps = + sauloverissimo/midi2 @ ^0.3.3 https://github.com/sauloverissimo/ESP32_Host_MIDI.git#v6.0.0 diff --git a/examples/esp32-s3-devkitc-usb-midi2/idf/main/CMakeLists.txt b/examples/esp32-s3-devkitc-usb-midi2/idf/main/CMakeLists.txt index e45dc64..ec1c7a8 100644 --- a/examples/esp32-s3-devkitc-usb-midi2/idf/main/CMakeLists.txt +++ b/examples/esp32-s3-devkitc-usb-midi2/idf/main/CMakeLists.txt @@ -11,13 +11,13 @@ idf_component_register( SRCS "main.cpp" "esp32_s3_devkitc_midi2.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_device.cpp" "${MIDI2_CPP_ROOT}/midi2_ci.cpp" INCLUDE_DIRS "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb led_strip driver diff --git a/examples/esp32-s3-devkitc-usb-midi2/idf/main/idf_component.yml b/examples/esp32-s3-devkitc-usb-midi2/idf/main/idf_component.yml index 6b91e31..1837099 100644 --- a/examples/esp32-s3-devkitc-usb-midi2/idf/main/idf_component.yml +++ b/examples/esp32-s3-devkitc-usb-midi2/idf/main/idf_component.yml @@ -8,3 +8,6 @@ dependencies: idf: ">=5.4" espressif/led_strip: "^2.5" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/nrf52840-promicro-midi2/CMakeLists.txt b/examples/nrf52840-promicro-midi2/CMakeLists.txt index 6c7c450..7863c5c 100644 --- a/examples/nrf52840-promicro-midi2/CMakeLists.txt +++ b/examples/nrf52840-promicro-midi2/CMakeLists.txt @@ -73,12 +73,30 @@ family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) # midi2_cpp parent library (consumed via ../../src, not vendored) # ---------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) target_compile_features(midi2_cpp PUBLIC cxx_std_17) target_compile_options(midi2_cpp PRIVATE -fno-rtti -fno-exceptions -fno-use-cxa-atexit) diff --git a/examples/rp2040-midi2/CMakeLists.txt b/examples/rp2040-midi2/CMakeLists.txt index 9d8d87d..2d627dd 100644 --- a/examples/rp2040-midi2/CMakeLists.txt +++ b/examples/rp2040-midi2/CMakeLists.txt @@ -54,12 +54,30 @@ pico_sdk_init() # root). No vendored copy needed. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) # --------------------------------------------------------------------------- # rp2040-midi2 — board core (Pico SDK + TinyUSB glue + 5 hooks wired) diff --git a/examples/rp2040-promicro-ump-test-bench/CMakeLists.txt b/examples/rp2040-promicro-ump-test-bench/CMakeLists.txt index c09e61e..23d16f7 100644 --- a/examples/rp2040-promicro-ump-test-bench/CMakeLists.txt +++ b/examples/rp2040-promicro-ump-test-bench/CMakeLists.txt @@ -58,12 +58,30 @@ pico_sdk_init() # library root). No vendored copy needed. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) # --------------------------------------------------------------------------- # rp2040-promicro-ump-test-bench, board core (Pico SDK + TinyUSB glue + 5 hooks diff --git a/examples/sparkfun-promicro-rp2350-midi2/CMakeLists.txt b/examples/sparkfun-promicro-rp2350-midi2/CMakeLists.txt index 8c92b8a..b6204ab 100644 --- a/examples/sparkfun-promicro-rp2350-midi2/CMakeLists.txt +++ b/examples/sparkfun-promicro-rp2350-midi2/CMakeLists.txt @@ -50,12 +50,30 @@ pico_sdk_init() # midi2_cpp, built from the parent library tree. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) # --------------------------------------------------------------------------- # sparkfun-promicro-rp2350-midi2, board core (Pico SDK + TinyUSB glue + diff --git a/examples/t-display-s3-midi2/idf/main/CMakeLists.txt b/examples/t-display-s3-midi2/idf/main/CMakeLists.txt index 235ebb1..24fbfe0 100644 --- a/examples/t-display-s3-midi2/idf/main/CMakeLists.txt +++ b/examples/t-display-s3-midi2/idf/main/CMakeLists.txt @@ -14,13 +14,13 @@ idf_component_register( SRCS "main.cpp" "t_display_s3_midi2.cpp" - "${MIDI2_CPP_ROOT}/midi2.c" "${MIDI2_CPP_ROOT}/midi2_device.cpp" "${MIDI2_CPP_ROOT}/midi2_ci.cpp" INCLUDE_DIRS "." "${MIDI2_CPP_ROOT}" REQUIRES + midi2 tinyusb piano_display driver diff --git a/examples/t-display-s3-midi2/idf/main/idf_component.yml b/examples/t-display-s3-midi2/idf/main/idf_component.yml index ad5a422..58761af 100644 --- a/examples/t-display-s3-midi2/idf/main/idf_component.yml +++ b/examples/t-display-s3-midi2/idf/main/idf_component.yml @@ -8,3 +8,6 @@ ## see ../components/piano_display/idf_component.yml. dependencies: idf: ">=5.4" + midi2: + git: https://github.com/sauloverissimo/midi2.git + version: ">=0.3.3" diff --git a/examples/t-display-s3-shield-host-midi2/pio/platformio.ini b/examples/t-display-s3-shield-host-midi2/pio/platformio.ini index 6739427..fef5fda 100644 --- a/examples/t-display-s3-shield-host-midi2/pio/platformio.ini +++ b/examples/t-display-s3-shield-host-midi2/pio/platformio.ini @@ -54,5 +54,6 @@ build_flags = lib_extra_dirs = ../../.., /home/shared/workspaces/github lib_deps = + sauloverissimo/midi2 @ ^0.3.3 ESP32_Host_MIDI lovyan03/LovyanGFX@^1.2.0 diff --git a/examples/waveshare-rp2040-midi2/CMakeLists.txt b/examples/waveshare-rp2040-midi2/CMakeLists.txt index ac892c0..c26bd38 100644 --- a/examples/waveshare-rp2040-midi2/CMakeLists.txt +++ b/examples/waveshare-rp2040-midi2/CMakeLists.txt @@ -48,12 +48,30 @@ pico_sdk_init() # midi2_cpp, built from the parent library tree. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) # --------------------------------------------------------------------------- # waveshare-rp2040-midi2, board core (Pico SDK + TinyUSB glue + diff --git a/examples/waveshare-rp2350-usb-a-bridge-midi2/CMakeLists.txt b/examples/waveshare-rp2350-usb-a-bridge-midi2/CMakeLists.txt index e9e21b4..4f12c06 100644 --- a/examples/waveshare-rp2350-usb-a-bridge-midi2/CMakeLists.txt +++ b/examples/waveshare-rp2350-usb-a-bridge-midi2/CMakeLists.txt @@ -68,10 +68,23 @@ pico_sdk_init() # midi2 (C99), only midi2_msg_word_count is used by the router. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") -add_library(midi2_c99 STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c -) -target_include_directories(midi2_c99 PUBLIC ${MIDI2_CPP_ROOT}/src) + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() # --------------------------------------------------------------------------- # Showcase executable. @@ -87,7 +100,7 @@ target_include_directories(waveshare-rp2350-usb-a-bridge-midi2-showcase PRIVATE target_link_libraries(waveshare-rp2350-usb-a-bridge-midi2-showcase PRIVATE - midi2_c99 + midi2::midi2 pico_stdlib hardware_i2c # optional SSD1306 over I2C1 tinyusb_device diff --git a/examples/waveshare-rp2350-usb-a-midi2/CMakeLists.txt b/examples/waveshare-rp2350-usb-a-midi2/CMakeLists.txt index c3fb91f..9e573e6 100644 --- a/examples/waveshare-rp2350-usb-a-midi2/CMakeLists.txt +++ b/examples/waveshare-rp2350-usb-a-midi2/CMakeLists.txt @@ -52,12 +52,30 @@ pico_sdk_init() # midi2_cpp, built from the parent library tree. # --------------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) # --------------------------------------------------------------------------- # waveshare-rp2350-usb-a-midi2, board core (Pico SDK + TinyUSB glue + diff --git a/examples/xiao-samd21-midi2/CMakeLists.txt b/examples/xiao-samd21-midi2/CMakeLists.txt index 2ee858b..087ce81 100644 --- a/examples/xiao-samd21-midi2/CMakeLists.txt +++ b/examples/xiao-samd21-midi2/CMakeLists.txt @@ -63,12 +63,30 @@ family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) # midi2_cpp parent library (consumed via ../../src, not vendored) # ---------------------------------------------------------------------- set(MIDI2_CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +# midi2 C99 core, pulled externally so the recipe shares one source +# of truth with the rest of the ecosystem. Override with +# -DMIDI2_LOCAL_PATH=/path/to/midi2 for offline builds. +include(FetchContent) +if(NOT TARGET midi2) + if(DEFINED MIDI2_LOCAL_PATH) + FetchContent_Declare(midi2 SOURCE_DIR ${MIDI2_LOCAL_PATH}) + else() + FetchContent_Declare(midi2 + GIT_REPOSITORY https://github.com/sauloverissimo/midi2.git + GIT_TAG v0.3.2 + GIT_SHALLOW TRUE + ) + endif() + FetchContent_MakeAvailable(midi2) +endif() + add_library(midi2_cpp STATIC - ${MIDI2_CPP_ROOT}/src/midi2.c ${MIDI2_CPP_ROOT}/src/midi2_device.cpp ${MIDI2_CPP_ROOT}/src/midi2_ci.cpp ) target_include_directories(midi2_cpp PUBLIC ${MIDI2_CPP_ROOT}/src) +target_link_libraries(midi2_cpp PUBLIC midi2::midi2) target_compile_features(midi2_cpp PUBLIC cxx_std_17) # midi2_cpp uses std::function / std::array; exceptions stay off (default # in arm-none-eabi-gcc with -fno-exceptions which TinyUSB build sets). diff --git a/library.json b/library.json index 6496fad..c4fc133 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "midi2_cpp", - "version": "0.1.0", - "description": "C++17 Arduino-style wrapper for MIDI 2.0 on embedded devices. Thin wrapper over the portable midi2 C99 library. Covers UMP, MIDI-CI with Appendix E, Profile, PE Subscribe/Notify, Process Inquiry, Flex Data, Bit Scaling.", + "version": "0.2.0", + "description": "C++17 Arduino-style wrapper for MIDI 2.0 on embedded devices. Thin wrapper over the portable midi2 C99 library. Covers UMP, MIDI-CI with Appendix E, Profile, PE Subscribe/Notify, Process Inquiry, Flex Data, and Bit Scaling. Targets RP2040, RP2350, Teensy, ESP32 family, nRF52, and SAMD21.", "keywords": "midi, midi2, usb, ump, capability-inquiry, property-exchange, profile", "authors": [ { @@ -18,6 +18,9 @@ "type": "git", "url": "https://github.com/sauloverissimo/midi2_cpp.git" }, + "dependencies": { + "sauloverissimo/midi2": "^0.3.3" + }, "headers": "midi2_cpp.h", "export": { "exclude": ["tests/build/*", "build/*", ".github/*"] diff --git a/library.properties b/library.properties index 1000139..564db81 100644 --- a/library.properties +++ b/library.properties @@ -1,11 +1,11 @@ name=midi2_cpp -version=0.1.0 +version=0.2.0 author=Saulo Verissimo maintainer=Saulo Verissimo sentence=C++17 Arduino-style wrapper for MIDI 2.0 on embedded devices. -paragraph=Thin C++ wrapper over the portable midi2 C99 library. Covers UMP transport (M2-104), MIDI-CI with Appendix E (M2-101), Property Exchange Subscribe/Notify, Profile Configuration, Process Inquiry, Flex Data, Bit Scaling (M2-115). For embedded (RP2040, Teensy, ESP32, nRF52). For desktop, see ni-midi2. Complementary to AM_MIDI2.0Lib (different design philosophy). +paragraph=Thin C++ wrapper over the portable midi2 C99 library. Covers UMP transport (M2-104), MIDI-CI with Appendix E (M2-101), Property Exchange Subscribe/Notify, Profile Configuration, Process Inquiry, Flex Data, and Bit Scaling (M2-115). Targets embedded boards (RP2040, RP2350, Teensy, ESP32 family, nRF52, SAMD21). category=Communication url=https://github.com/sauloverissimo/midi2_cpp architectures=* includes=midi2_cpp.h -depends= +depends=midi2 (>=0.3.3) diff --git a/logo_midi2_cpp.png b/logo_midi2_cpp.png index 0ea0c0b..b7d7b3c 100644 Binary files a/logo_midi2_cpp.png and b/logo_midi2_cpp.png differ diff --git a/src/midi2.c b/src/midi2.c deleted file mode 100644 index 70ddb6d..0000000 --- a/src/midi2.c +++ /dev/null @@ -1,36 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2026 Saulo Verissimo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* Auto-generated from midi2 v0.3.0 -- 2026-04-25 - * https://github.com/sauloverissimo/midi2 - * - * Portable MIDI 2.0 library (C99, zero dependencies) - * Specs: MIDI 2.0 UMP (M2-104-UM v1.1.2), MIDI-CI (M2-101-UM v1.2) - * - * Compile this file to get all midi2 symbols. - * Include midi2.h in your headers for declarations. - */ - -#define MIDI2_IMPLEMENTATION -#include "midi2.h" diff --git a/src/midi2.h b/src/midi2.h deleted file mode 100644 index 4814a32..0000000 --- a/src/midi2.h +++ /dev/null @@ -1,5108 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2026 Saulo Verissimo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* Auto-generated from midi2 v0.3.0 -- 2026-04-25 - * https://github.com/sauloverissimo/midi2 - * - * Portable MIDI 2.0 library (C99, zero dependencies) - * Specs: MIDI 2.0 UMP (M2-104-UM v1.1.2), MIDI-CI (M2-101-UM v1.2) - * - * Usage: - * #include "midi2.h" - * // In exactly ONE .c file: - * #define MIDI2_IMPLEMENTATION - * #include "midi2.h" - */ - -#ifndef MIDI2_H -#define MIDI2_H - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* == midi2_msg =========================================================== */ - - -/* - * midi2_msg.h - UMP message construction and parsing - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - - - -/*--------------------------------------------------------------------+ - * UMP Message Types (bits [31:28] of word 0) - *--------------------------------------------------------------------*/ -enum { - MIDI2_MT_UTILITY = 0x00, /* 1 word */ - MIDI2_MT_SYSTEM = 0x01, /* 1 word */ - MIDI2_MT_MIDI1_CV = 0x02, /* 1 word */ - MIDI2_MT_SYSEX7 = 0x03, /* 2 words */ - MIDI2_MT_MIDI2_CV = 0x04, /* 2 words */ - MIDI2_MT_DATA128 = 0x05, /* 4 words */ - MIDI2_MT_FLEX_DATA = 0x0D, /* 4 words */ - MIDI2_MT_STREAM = 0x0F, /* 4 words */ -}; - -/*--------------------------------------------------------------------+ - * MIDI 2.0 Channel Voice Status (upper nibble, MT 0x4) - *--------------------------------------------------------------------*/ -enum { - MIDI2_STATUS_NOTE_OFF = 0x80, - MIDI2_STATUS_NOTE_ON = 0x90, - MIDI2_STATUS_POLY_PRESSURE = 0xA0, - MIDI2_STATUS_CC = 0xB0, - MIDI2_STATUS_PROGRAM = 0xC0, - MIDI2_STATUS_CHAN_PRESSURE = 0xD0, - MIDI2_STATUS_PITCH_BEND = 0xE0, - MIDI2_STATUS_PER_NOTE_MGMT = 0xF0, - MIDI2_STATUS_REG_PER_NOTE = 0x00, /* Registered Per-Note Controller */ - MIDI2_STATUS_ASN_PER_NOTE = 0x10, /* Assignable Per-Note Controller */ - MIDI2_STATUS_RPN = 0x20, /* Registered Controller (RPN) */ - MIDI2_STATUS_NRPN = 0x30, /* Assignable Controller (NRPN) */ - MIDI2_STATUS_REL_RPN = 0x40, /* Relative Registered Controller */ - MIDI2_STATUS_REL_NRPN = 0x50, /* Relative Assignable Controller */ - MIDI2_STATUS_PER_NOTE_PB = 0x60, - /* Note: Per-Note CC uses the same opcode as ASN_PER_NOTE (0x10). - * Use MIDI2_STATUS_ASN_PER_NOTE for new code. */ -}; - -/*--------------------------------------------------------------------+ - * SysEx7 Status - *--------------------------------------------------------------------*/ -enum { - MIDI2_SYSEX7_COMPLETE = 0x00, - MIDI2_SYSEX7_START = 0x10, - MIDI2_SYSEX7_CONTINUE = 0x20, - MIDI2_SYSEX7_END = 0x30, -}; - -/*--------------------------------------------------------------------+ - * Flex Data Status - *--------------------------------------------------------------------*/ -enum { - MIDI2_FLEX_TEMPO = 0x00, - MIDI2_FLEX_TIME_SIG = 0x01, - MIDI2_FLEX_METRONOME = 0x02, - MIDI2_FLEX_KEY_SIG = 0x05, - MIDI2_FLEX_CHORD_NAME = 0x06, -}; - -/* Flex Data Status Banks */ -enum { - MIDI2_FLEX_BANK_SETUP = 0x00, /* Setup & Performance Events */ - MIDI2_FLEX_BANK_METADATA = 0x01, /* Metadata Text */ - MIDI2_FLEX_BANK_PERF_TEXT = 0x02, /* Performance Text Events (lyrics) */ -}; - -/* Flex Data Metadata Text status values (bank 0x01) */ -enum { - MIDI2_FLEX_TEXT_UNKNOWN = 0x00, - MIDI2_FLEX_TEXT_PROJECT_NAME = 0x01, - MIDI2_FLEX_TEXT_COMPOSITION_NAME = 0x02, - MIDI2_FLEX_TEXT_CLIP_NAME = 0x03, - MIDI2_FLEX_TEXT_COPYRIGHT = 0x04, - MIDI2_FLEX_TEXT_COMPOSER_NAME = 0x05, - MIDI2_FLEX_TEXT_LYRICIST_NAME = 0x06, - MIDI2_FLEX_TEXT_ARRANGER_NAME = 0x07, - MIDI2_FLEX_TEXT_PUBLISHER_NAME = 0x08, - MIDI2_FLEX_TEXT_PERFORMER_NAME = 0x09, - MIDI2_FLEX_TEXT_ACCOMPANY_NAME = 0x0A, - MIDI2_FLEX_TEXT_RECORDING_DATE = 0x0B, - MIDI2_FLEX_TEXT_RECORDING_LOC = 0x0C, -}; - -/* Flex Data Performance Text status values (bank 0x02) */ -enum { - MIDI2_FLEX_PERF_UNKNOWN = 0x00, - MIDI2_FLEX_PERF_LYRICS = 0x01, - MIDI2_FLEX_PERF_LYRICS_LANG = 0x02, - MIDI2_FLEX_PERF_RUBY = 0x03, - MIDI2_FLEX_PERF_RUBY_LANG = 0x04, -}; - -/*--------------------------------------------------------------------+ - * Word Count - *--------------------------------------------------------------------*/ -static inline uint8_t midi2_msg_word_count(uint8_t mt) { - switch (mt) { - case MIDI2_MT_UTILITY: - case MIDI2_MT_SYSTEM: - case MIDI2_MT_MIDI1_CV: - return 1; - case MIDI2_MT_SYSEX7: - case MIDI2_MT_MIDI2_CV: - return 2; - case MIDI2_MT_DATA128: - case MIDI2_MT_FLEX_DATA: - case MIDI2_MT_STREAM: - return 4; - default: - return 1; - } -} - -/*--------------------------------------------------------------------+ - * Parsing (field extraction from word 0) - *--------------------------------------------------------------------*/ - -/** @brief Extract message type (bits [31:28] of word 0). */ -static inline uint8_t midi2_msg_get_mt(const uint32_t *w) { return (uint8_t)((w[0] >> 28) & 0x0F); } -/** @brief Extract group number (bits [27:24] of word 0). Range: 0-15. */ -static inline uint8_t midi2_msg_get_group(const uint32_t *w) { return (uint8_t)((w[0] >> 24) & 0x0F); } -/** @brief Extract full status byte (bits [23:16] of word 0). Includes status nibble + channel. */ -static inline uint8_t midi2_msg_get_status(const uint32_t *w) { return (uint8_t)((w[0] >> 16) & 0xFF); } -/** @brief Extract channel number (bits [19:16] of word 0). Range: 0-15. */ -static inline uint8_t midi2_msg_get_channel(const uint32_t *w) { return (uint8_t)((w[0] >> 16) & 0x0F); } -/** @brief Extract note/index field (bits [15:8] of word 0). Full 8-bit field. */ -static inline uint8_t midi2_msg_get_note(const uint32_t *w) { return (uint8_t)((w[0] >> 8) & 0xFF); } -/** @brief Extract velocity (bits [31:16] of word 1). 16-bit MIDI 2.0 velocity. */ -static inline uint16_t midi2_msg_get_velocity(const uint32_t *w) { return (uint16_t)(w[1] >> 16); } -/** @brief Extract full word 1 (32-bit data payload). */ -static inline uint32_t midi2_msg_get_data(const uint32_t *w) { return w[1]; } - -/** @brief Rewrite the Group field of a UMP word in-place. - * - * Only MT 0x2 (MIDI 1.0 CV), 0x3 (SysEx7), 0x4 (MIDI 2.0 CV) and - * 0x5 (Data128/SysEx8) carry a Group field in word 0 bits [27:24]. - * Utility, System Real-Time, Flex Data and UMP Stream words have - * no Group field and are left untouched. - * - * Useful for routing pipelines that need to re-stamp the group of - * forwarded messages without rebuilding the word from scratch. - * (v0.3.0+) */ -static inline void midi2_msg_set_group(uint32_t *word0, uint8_t group) { - uint8_t mt = (uint8_t)((*word0 >> 28) & 0x0Fu); - if (mt >= 0x2u && mt <= 0x5u) { - *word0 = (*word0 & 0xF0FFFFFFu) | ((uint32_t)(group & 0x0Fu) << 24); - } -} - -/*--------------------------------------------------------------------+ - * Value Scaling (MIDI 2.0 spec section 4.2.1) - * - * Bit-replication for symmetric round-trip: scaleDown(scaleUp(x)) == x - *--------------------------------------------------------------------*/ - -/** @brief Scale 7-bit (0-127) to 16-bit (0-65535) with bit-replication. */ -static inline uint16_t midi2_msg_scale_up_7to16(uint8_t v) { - uint16_t x = (uint16_t)(v & 0x7F); - return (uint16_t)((x << 9) | (x << 2) | (x >> 5)); -} - -/** @brief Scale 7-bit (0-127) to 32-bit (0-0xFFFFFFFF) with bit-replication. */ -static inline uint32_t midi2_msg_scale_up_7to32(uint8_t v) { - uint32_t x = (uint32_t)(v & 0x7F); - return (x << 25) | (x << 18) | (x << 11) | (x << 4) | (x >> 3); -} - -/** @brief Scale 14-bit (0-16383) to 32-bit (0-0xFFFFFFFF) with bit-replication. */ -static inline uint32_t midi2_msg_scale_up_14to32(uint16_t v) { - uint32_t x = (uint32_t)(v & 0x3FFF); - return (x << 18) | (x << 4) | (x >> 10); -} - -/** @brief Scale 16-bit to 7-bit. */ -static inline uint8_t midi2_msg_scale_down_16to7(uint16_t v) { return (uint8_t)(v >> 9); } -/** @brief Scale 32-bit to 7-bit. */ -static inline uint8_t midi2_msg_scale_down_32to7(uint32_t v) { return (uint8_t)(v >> 25); } -/** @brief Scale 32-bit to 14-bit. */ -static inline uint16_t midi2_msg_scale_down_32to14(uint32_t v) { return (uint16_t)(v >> 18); } - -/*--------------------------------------------------------------------+ - * MIDI 2.0 Channel Voice Construction (MT 0x4, 2 words) - * - * All write into a uint32_t w[2] provided by caller. - *--------------------------------------------------------------------*/ - -/* Internal: build word 0 for MT 0x4 */ -static inline uint32_t midi2_msg_build_cv2_w0(uint8_t group, uint8_t status, - uint8_t channel, uint8_t b3, uint8_t b4) { - return ((uint32_t)MIDI2_MT_MIDI2_CV << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)(status & 0xF0) << 16) - | ((uint32_t)(channel & 0x0F) << 16) - | ((uint32_t)b3 << 8) - | (uint32_t)b4; -} - -/** - * @brief Build a MIDI 2.0 Note On message (MT 0x4, 2 words). - * @param w Output: uint32_t[2] provided by caller. - * @param group UMP group (0-15). - * @param channel MIDI channel (0-15). - * @param note Note number (0-127). - * @param velocity 16-bit velocity (0x0000-0xFFFF). Use midi2_msg_scale_up_7to16() for legacy values. - * @param attribute Note attribute (0 = none, or type<<8 | value). */ -static inline void midi2_msg_note_on(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint16_t velocity, uint16_t attribute) { - uint8_t attr_type = (attribute != 0) ? (uint8_t)(attribute >> 8) : 0; - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_NOTE_ON, channel, note & 0x7F, attr_type); - w[1] = ((uint32_t)velocity << 16) | (attribute & 0xFFFF); -} - -/** @brief Build a MIDI 2.0 Note Off message (MT 0x4, 2 words). See midi2_msg_note_on() for params. */ -static inline void midi2_msg_note_off(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint16_t velocity, uint16_t attribute) { - uint8_t attr_type = (attribute != 0) ? (uint8_t)(attribute >> 8) : 0; - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_NOTE_OFF, channel, note & 0x7F, attr_type); - w[1] = ((uint32_t)velocity << 16) | (attribute & 0xFFFF); -} - -/** @brief Build a MIDI 2.0 Control Change (MT 0x4). @param value 32-bit CC value. */ -static inline void midi2_msg_cc(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t index, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_CC, channel, index & 0x7F, 0); - w[1] = value; -} - -/** @brief Build a MIDI 2.0 Program Change (MT 0x4). @param bank_valid true to include bank select. */ -static inline void midi2_msg_program(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t program, bool bank_valid, - uint8_t bank_msb, uint8_t bank_lsb) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_PROGRAM, channel, 0, 0); - w[1] = (bank_valid ? (UINT32_C(1) << 31) : 0) - | ((uint32_t)(program & 0x7F) << 24) - | (bank_valid ? (((uint32_t)(bank_msb & 0x7F) << 8) | (bank_lsb & 0x7F)) : 0); -} - -/** @brief Build a MIDI 2.0 Pitch Bend (MT 0x4). @param value 32-bit, 0x80000000 = center. */ -static inline void midi2_msg_pitch_bend(uint32_t *w, uint8_t group, uint8_t channel, - uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_PITCH_BEND, channel, 0, 0); - w[1] = value; -} - -/** @brief Build a MIDI 2.0 Channel Pressure (MT 0x4). @param pressure 32-bit pressure value. */ -static inline void midi2_msg_chan_pressure(uint32_t *w, uint8_t group, uint8_t channel, - uint32_t pressure) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_CHAN_PRESSURE, channel, 0, 0); - w[1] = pressure; -} - -/** @brief Build a MIDI 2.0 Polyphonic Key Pressure (MT 0x4). */ -static inline void midi2_msg_poly_pressure(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint32_t pressure) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_POLY_PRESSURE, channel, note & 0x7F, 0); - w[1] = pressure; -} - -/** @brief Build a MIDI 2.0 RPN message (MT 0x4). @param msb RPN bank. @param lsb RPN index. */ -static inline void midi2_msg_rpn(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t msb, uint8_t lsb, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_RPN, channel, msb & 0x7F, lsb & 0x7F); - w[1] = value; -} - -/** @brief Build a MIDI 2.0 NRPN message (MT 0x4). Non-registered parameter. */ -static inline void midi2_msg_nrpn(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t msb, uint8_t lsb, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_NRPN, channel, msb & 0x7F, lsb & 0x7F); - w[1] = value; -} - -/*--------------------------------------------------------------------+ - * Per-Note Expression (MT 0x4, 2 words) - *--------------------------------------------------------------------*/ -/** @brief Build a per-note Pitch Bend (MT 0x4). Independent pitch per note. */ -static inline void midi2_msg_per_note_pb(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_PER_NOTE_PB, channel, note & 0x7F, 0); - w[1] = value; -} - -/** @brief Build a per-note CC (MT 0x4). Independent controller per note. */ -static inline void midi2_msg_per_note_cc(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint8_t index, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_ASN_PER_NOTE, channel, note & 0x7F, index); - w[1] = value; -} - -/** @brief Build a per-note Management message (MT 0x4). @param detach release from voice. @param reset reset controllers. */ -static inline void midi2_msg_per_note_mgmt(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, bool detach, bool reset) { - uint8_t flags = (detach ? 0x02 : 0) | (reset ? 0x01 : 0); - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_PER_NOTE_MGMT, channel, note & 0x7F, flags); - w[1] = 0; -} - -/*--------------------------------------------------------------------+ - * Registered Per-Note Controller (MT 0x4, status 0x0) - * 256 controllers per note, defined by MMA/AMEI. - *--------------------------------------------------------------------*/ -/** @brief Build a Registered Per-Note Controller message (MT 0x4). - * @param index Controller index (0-255). */ -static inline void midi2_msg_reg_per_note_ctrl(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint8_t index, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_REG_PER_NOTE, channel, note & 0x7F, index); - w[1] = value; -} - -/** @brief Build an Assignable Per-Note Controller message (MT 0x4). - * @param index Controller index (0-255). Application-specific. */ -static inline void midi2_msg_asn_per_note_ctrl(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t note, uint8_t index, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_ASN_PER_NOTE, channel, note & 0x7F, index); - w[1] = value; -} - -/*--------------------------------------------------------------------+ - * Relative RPN/NRPN (MT 0x4, status 0x4/0x5) - * Two's complement relative value (positive = increase, negative = decrease). - * Cannot be translated to MIDI 1.0. - *--------------------------------------------------------------------*/ -/** @brief Build a Relative Registered Controller (RPN) message. */ -static inline void midi2_msg_rel_rpn(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t msb, uint8_t lsb, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_REL_RPN, channel, msb & 0x7F, lsb & 0x7F); - w[1] = value; -} - -/** @brief Build a Relative Assignable Controller (NRPN) message. */ -static inline void midi2_msg_rel_nrpn(uint32_t *w, uint8_t group, uint8_t channel, - uint8_t msb, uint8_t lsb, uint32_t value) { - w[0] = midi2_msg_build_cv2_w0(group, MIDI2_STATUS_REL_NRPN, channel, msb & 0x7F, lsb & 0x7F); - w[1] = value; -} - -/*--------------------------------------------------------------------+ - * System Messages (MT 0x1, 1 word) - *--------------------------------------------------------------------*/ -static inline uint32_t midi2_msg_system(uint8_t group, uint8_t status) { - return ((uint32_t)MIDI2_MT_SYSTEM << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)status << 16); -} - -static inline uint32_t midi2_msg_system_2byte(uint8_t group, uint8_t status, uint8_t data1) { - return midi2_msg_system(group, status) | ((uint32_t)data1 << 8); -} - -static inline uint32_t midi2_msg_system_3byte(uint8_t group, uint8_t status, - uint8_t data1, uint8_t data2) { - return midi2_msg_system(group, status) | ((uint32_t)data1 << 8) | (uint32_t)data2; -} - -/*--------------------------------------------------------------------+ - * System Real-Time + System Common named wrappers (M2-104-UM section 4.3, - * v0.3.0+). Each calls the corresponding generic builder above with the - * canonical status byte. Useful for pattern-matching senders and for - * call sites that prefer the named shortcut over the magic-number form. - * All inline; zero ROM cost when not called. - *--------------------------------------------------------------------*/ - -/** @brief Tune Request (status 0xF6, 1-byte System Common). */ -static inline uint32_t midi2_msg_system_tune_request(uint8_t group) { - return midi2_msg_system(group, 0xF6); -} - -/** @brief Timing Clock (status 0xF8, 1-byte System Real-Time). */ -static inline uint32_t midi2_msg_system_timing_clock(uint8_t group) { - return midi2_msg_system(group, 0xF8); -} - -/** @brief Start (status 0xFA, 1-byte System Real-Time, sequencer start). */ -static inline uint32_t midi2_msg_system_start(uint8_t group) { - return midi2_msg_system(group, 0xFA); -} - -/** @brief Continue (status 0xFB, 1-byte System Real-Time). */ -static inline uint32_t midi2_msg_system_continue(uint8_t group) { - return midi2_msg_system(group, 0xFB); -} - -/** @brief Stop (status 0xFC, 1-byte System Real-Time). */ -static inline uint32_t midi2_msg_system_stop(uint8_t group) { - return midi2_msg_system(group, 0xFC); -} - -/** @brief Active Sensing (status 0xFE, 1-byte System Real-Time). */ -static inline uint32_t midi2_msg_system_active_sensing(uint8_t group) { - return midi2_msg_system(group, 0xFE); -} - -/** @brief System Reset (status 0xFF, 1-byte System Real-Time). */ -static inline uint32_t midi2_msg_system_reset(uint8_t group) { - return midi2_msg_system(group, 0xFF); -} - -/** @brief MIDI Time Code Quarter Frame (status 0xF1, 2-byte System Common). */ -static inline uint32_t midi2_msg_system_mtc(uint8_t group, uint8_t time_code) { - return midi2_msg_system_2byte(group, 0xF1, time_code & 0x7F); -} - -/** @brief Song Select (status 0xF3, 2-byte System Common). */ -static inline uint32_t midi2_msg_system_song_select(uint8_t group, uint8_t song) { - return midi2_msg_system_2byte(group, 0xF3, song & 0x7F); -} - -/** @brief Song Position Pointer (status 0xF2, 3-byte System Common). - * @param position 14-bit position; LSB stored at data1, MSB at data2. */ -static inline uint32_t midi2_msg_system_song_position(uint8_t group, uint16_t position) { - return midi2_msg_system_3byte(group, 0xF2, - (uint8_t)(position & 0x7F), - (uint8_t)((position >> 7) & 0x7F)); -} - -/*--------------------------------------------------------------------+ - * Flex Data (MT 0xD, 4 words) - * - * Word 0: [MT:4][group:4][format:2][address:2][channel:4][statusBank:8][status:8] - * Tempo, TimeSignature, KeySignature are group-level (address=0b01). - *--------------------------------------------------------------------*/ - -/* Internal: flex data word 0 builder with bank, address, channel, and format */ -static inline uint32_t midi2_msg_build_flex_w0_full(uint8_t group, uint8_t format, - uint8_t address, uint8_t channel, - uint8_t bank, uint8_t status) { - return ((uint32_t)MIDI2_MT_FLEX_DATA << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)(format & 0x03) << 22) - | ((uint32_t)(address & 0x03) << 20) - | ((uint32_t)(channel & 0x0F) << 16) - | ((uint32_t)(bank & 0xFF) << 8) - | (uint32_t)(status & 0xFF); -} - -/* Internal: flex data word 0 builder (group-level shorthand) */ -static inline uint32_t midi2_msg_build_flex_w0(uint8_t group, uint8_t status) { - return midi2_msg_build_flex_w0_full(group, 0, 0x01, 0, - MIDI2_FLEX_BANK_SETUP, status); -} - -/* Tempo: word1 = 10ns per quarter note */ -static inline void midi2_msg_tempo(uint32_t *w, uint8_t group, uint32_t ten_ns_per_qn) { - memset(w, 0, 16); - w[0] = midi2_msg_build_flex_w0(group, MIDI2_FLEX_TEMPO); - w[1] = ten_ns_per_qn; -} - -/* Time Signature: word1 = [numerator:8][denominator:8][0:16] */ -static inline void midi2_msg_time_sig(uint32_t *w, uint8_t group, - uint8_t numerator, uint8_t denominator) { - memset(w, 0, 16); - w[0] = midi2_msg_build_flex_w0(group, MIDI2_FLEX_TIME_SIG); - w[1] = ((uint32_t)numerator << 24) | ((uint32_t)denominator << 16); -} - -/* Key Signature: word1 = [sharpsFlats:4][tonicNote:4][keyType:2][0:22] - * sharpsFlats: -7 to +7 (4-bit signed). keyType: 0=major, 1=minor. - * address: 0x0a = channel, 0x01 = group. tonic: 0=unknown, 1=A..7=G. */ -static inline void midi2_msg_key_sig(uint32_t *w, uint8_t group, - int8_t sharps_flats, bool minor) { - memset(w, 0, 16); - w[0] = midi2_msg_build_flex_w0(group, MIDI2_FLEX_KEY_SIG); - uint8_t sf4 = (uint8_t)(sharps_flats & 0x0F); - uint8_t key_type = minor ? 1 : 0; - w[1] = ((uint32_t)sf4 << 28) | ((uint32_t)key_type << 22); -} - -/** @brief Build a Key Signature with tonic note and channel addressing. - * @param address 0=channel, 1=group. - * @param tonic 0=unknown, 1=A, 2=B, 3=C, 4=D, 5=E, 6=F, 7=G. - * @param key_type 0=major, 1=minor, 2=none/atonal, 3=reserved. */ -static inline void midi2_msg_key_sig_full(uint32_t *w, uint8_t group, uint8_t address, - uint8_t channel, int8_t sharps_flats, - uint8_t tonic, uint8_t key_type) { - memset(w, 0, 16); - w[0] = midi2_msg_build_flex_w0_full(group, 0, address, channel, - MIDI2_FLEX_BANK_SETUP, MIDI2_FLEX_KEY_SIG); - w[1] = ((uint32_t)(sharps_flats & 0x0F) << 28) - | ((uint32_t)(tonic & 0x0F) << 24) - | ((uint32_t)(key_type & 0x03) << 22); -} - -/* Set Metronome: group-level (address=1), format=0 (complete). - * primary_clicks: MIDI clocks per primary click - * accent_1/2/3: bar accent parts (sum = beats in bar) - * subdiv_1/2: subdivision clicks per primary click period */ -static inline void midi2_msg_metronome(uint32_t *w, uint8_t group, - uint8_t primary_clicks, - uint8_t accent_1, uint8_t accent_2, uint8_t accent_3, - uint8_t subdiv_1, uint8_t subdiv_2) { - memset(w, 0, 16); - w[0] = midi2_msg_build_flex_w0_full(group, 0, 0x01, 0, - MIDI2_FLEX_BANK_SETUP, MIDI2_FLEX_METRONOME); - w[1] = ((uint32_t)primary_clicks << 24) - | ((uint32_t)accent_1 << 16) - | ((uint32_t)accent_2 << 8) - | (uint32_t)accent_3; - w[2] = ((uint32_t)subdiv_1 << 24) - | ((uint32_t)subdiv_2 << 16); -} - -/* Set Chord Name: channel-addressable, format=0. - * Tonic: sharps_flats (4-bit signed), tonic_note (0=unknown..7=G). - * chord_type: 0x00=clear, 0x01=major, 0x07=minor, etc. (see spec Table 14) - * Alterations: up to 4 for chord, up to 2 for bass. type: 0=none, 1=add, 2=sub, 3=raise, 4=lower. - * Bass: bass_sharps_flats, bass_note, bass_chord_type. */ -static inline void midi2_msg_chord_name(uint32_t *w, uint8_t group, uint8_t address, - uint8_t channel, - int8_t tonic_sf, uint8_t tonic_note, uint8_t chord_type, - uint8_t alt1_type, uint8_t alt1_deg, - uint8_t alt2_type, uint8_t alt2_deg, - uint8_t alt3_type, uint8_t alt3_deg, - uint8_t alt4_type, uint8_t alt4_deg, - int8_t bass_sf, uint8_t bass_note, uint8_t bass_type, - uint8_t bass_alt1_type, uint8_t bass_alt1_deg, - uint8_t bass_alt2_type, uint8_t bass_alt2_deg) { - w[0] = midi2_msg_build_flex_w0_full(group, 0, address, channel, - MIDI2_FLEX_BANK_SETUP, MIDI2_FLEX_CHORD_NAME); - w[1] = ((uint32_t)(tonic_sf & 0x0F) << 28) - | ((uint32_t)(tonic_note & 0x0F) << 24) - | ((uint32_t)(chord_type & 0xFF) << 16) - | ((uint32_t)(alt1_type & 0x0F) << 12) - | ((uint32_t)(alt1_deg & 0x0F) << 8) - | ((uint32_t)(alt2_type & 0x0F) << 4) - | (uint32_t)(alt2_deg & 0x0F); - w[2] = ((uint32_t)(alt3_type & 0x0F) << 28) - | ((uint32_t)(alt3_deg & 0x0F) << 24) - | ((uint32_t)(alt4_type & 0x0F) << 20) - | ((uint32_t)(alt4_deg & 0x0F) << 16); - w[3] = ((uint32_t)(bass_sf & 0x0F) << 28) - | ((uint32_t)(bass_note & 0x0F) << 24) - | ((uint32_t)(bass_type & 0xFF) << 16) - | ((uint32_t)(bass_alt1_type & 0x0F) << 12) - | ((uint32_t)(bass_alt1_deg & 0x0F) << 8) - | ((uint32_t)(bass_alt2_type & 0x0F) << 4) - | (uint32_t)(bass_alt2_deg & 0x0F); -} - -/*--------------------------------------------------------------------+ - * Flex Data Text Messages (MT 0xD, 4 words) - * - * Bank 0x01: Metadata Text (project name, composer, copyright, etc.) - * Bank 0x02: Performance Text (lyrics, ruby, language) - * UTF-8 text, no BOM. Up to 12 bytes per UMP in words 1-3. - * Multi-UMP: format 0=complete, 1=start, 2=continue, 3=end. - *--------------------------------------------------------------------*/ - -/** @brief Build a Flex Data text message (metadata or performance text). - * @param format 0=complete, 1=start, 2=continue, 3=end. - * @param address 0=channel, 1=group. - * @param bank 0x01=metadata, 0x02=performance text. - * @param status Text subtype (see MIDI2_FLEX_TEXT_* / MIDI2_FLEX_PERF_*). - * @param text UTF-8 text data (up to 12 bytes per UMP). - * @param len Bytes of text in this UMP (0-12). */ -static inline void midi2_msg_flex_text(uint32_t *w, uint8_t group, uint8_t format, - uint8_t address, uint8_t channel, - uint8_t bank, uint8_t status, - const uint8_t *text, uint8_t len) { - if (len > 12) len = 12; - w[0] = midi2_msg_build_flex_w0_full(group, format, address, channel, bank, status); - w[1] = 0; w[2] = 0; w[3] = 0; - uint8_t i; - for (i = 0; i < len; i++) { - uint8_t wi = (uint8_t)(1 + i / 4); - uint8_t sh = (uint8_t)(24 - (i % 4) * 8); - w[wi] |= ((uint32_t)text[i] << sh); - } -} - -/*--------------------------------------------------------------------+ - * SysEx7 Single Packet (MT 0x3, 2 words) - * - * Word 0: [MT:4][group:4][status:4][numBytes:4][data0:8][data1:8] - * Word 1: [data2:8][data3:8][data4:8][data5:8] - *--------------------------------------------------------------------*/ -static inline void midi2_msg_sysex7_packet(uint32_t *w, uint8_t group, - uint8_t status, const uint8_t *data, uint8_t len) { - if (len > 6) len = 6; - w[0] = ((uint32_t)MIDI2_MT_SYSEX7 << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)(status | (len & 0x0F)) << 16); - w[1] = 0; - uint8_t i; - for (i = 0; i < len; i++) { - uint8_t wi = (i < 2) ? 0 : 1; - uint8_t sh = (i < 2) ? (uint8_t)(8 - i * 8) : (uint8_t)(24 - (i - 2) * 8); - w[wi] |= ((uint32_t)data[i] << sh); - } -} - -/*--------------------------------------------------------------------+ - * Utility Messages -- JR Timestamps (MT 0x0, 1 word) - * - * Word: [MT:4][group:4][status:4][0:4][timestamp:16] - * JR Clock status = 0x0010, JR Timestamp status = 0x0020 - * Timestamp unit: 1/31250 of a second (~32us) - *--------------------------------------------------------------------*/ -enum { - MIDI2_UTILITY_NOOP = 0x00, - MIDI2_UTILITY_JR_CLOCK = 0x01, - MIDI2_UTILITY_JR_TIMESTAMP = 0x02, - MIDI2_UTILITY_DCTPQ = 0x03, /* Delta Clockstamp Ticks Per Quarter Note */ - MIDI2_UTILITY_DC = 0x04, /* Delta Clockstamp (ticks since last event) */ -}; - -static inline uint32_t midi2_msg_jr_clock(uint8_t group, uint16_t timestamp) { - return ((uint32_t)MIDI2_MT_UTILITY << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)MIDI2_UTILITY_JR_CLOCK << 20) - | (uint32_t)timestamp; -} - -static inline uint32_t midi2_msg_jr_timestamp(uint8_t group, uint16_t timestamp) { - return ((uint32_t)MIDI2_MT_UTILITY << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)MIDI2_UTILITY_JR_TIMESTAMP << 20) - | (uint32_t)timestamp; -} - -/** @brief Build a Delta Clockstamp Ticks Per Quarter Note (DCTPQ) message. - * Declares the tick resolution for Delta Clockstamp messages in a MIDI Clip File. - * Word: [MT:4][reserved:4][0011:4][reserved:4][tpq:16] - * @param tpq Ticks per quarter note (1-65535, 0 = reserved). */ -static inline uint32_t midi2_msg_dctpq(uint16_t tpq) { - return ((uint32_t)MIDI2_MT_UTILITY << 28) - | ((uint32_t)MIDI2_UTILITY_DCTPQ << 20) - | (uint32_t)tpq; -} - -/** @brief Build a Delta Clockstamp (DC) message. - * Declares ticks since last event in a MIDI Clip File. - * Word: [MT:4][reserved:4][0100:4][20-bit ticks] - * @param ticks Ticks since last event (20-bit, 0-1048575). */ -static inline uint32_t midi2_msg_delta_clockstamp(uint32_t ticks) { - return ((uint32_t)MIDI2_MT_UTILITY << 28) - | ((uint32_t)MIDI2_UTILITY_DC << 20) - | (ticks & 0x000FFFFF); -} - -/*--------------------------------------------------------------------+ - * Stream Messages (MT 0xF, 4 words) - * - * Word 0: [MT:4][format:2][status:10][data:16] - * Words 1-3: payload (depends on status) - * - * format: 0b00 = complete, 0b01 = start, 0b10 = continue, 0b11 = end - *--------------------------------------------------------------------*/ -enum { - MIDI2_STREAM_ENDPOINT_DISCOVERY = 0x000, - MIDI2_STREAM_ENDPOINT_INFO = 0x001, - MIDI2_STREAM_DEVICE_IDENTITY = 0x002, - MIDI2_STREAM_ENDPOINT_NAME = 0x003, - MIDI2_STREAM_PRODUCT_INSTANCE_ID = 0x004, - MIDI2_STREAM_CONFIG_REQUEST = 0x005, - MIDI2_STREAM_CONFIG_NOTIFY = 0x006, - MIDI2_STREAM_FB_DISCOVERY = 0x010, - MIDI2_STREAM_FB_INFO = 0x011, - MIDI2_STREAM_FB_NAME = 0x012, - MIDI2_STREAM_START_OF_CLIP = 0x020, - MIDI2_STREAM_END_OF_CLIP = 0x021, -}; - -/* Internal: build stream word 0 */ -static inline uint32_t midi2_msg_build_stream_w0(uint8_t format, uint16_t status) { - return ((uint32_t)MIDI2_MT_STREAM << 28) - | ((uint32_t)(format & 0x03) << 26) - | ((uint32_t)(status & 0x3FF) << 16); -} - -/* Endpoint Discovery: request endpoint info from a device - * ump_ver_major/minor: UMP version we support - * filter: bitmask of what to request (bit 0=endpoint info, 1=device identity, - * 2=endpoint name, 3=product instance ID, 4=stream config) */ -static inline void midi2_msg_stream_endpoint_discovery(uint32_t *w, - uint8_t ump_ver_major, - uint8_t ump_ver_minor, - uint8_t filter) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_ENDPOINT_DISCOVERY) - | ((uint32_t)ump_ver_major << 8) - | (uint32_t)ump_ver_minor; - w[1] = (uint32_t)filter; -} - -/* Endpoint Info Reply - * static_fb: true if function blocks are static - * num_fb: number of function blocks (0-31) - * midi1_proto: supports MIDI 1.0 protocol - * midi2_proto: supports MIDI 2.0 protocol - * rx_jr/tx_jr: supports JR timestamps */ -static inline void midi2_msg_stream_endpoint_info(uint32_t *w, - uint8_t ump_ver_major, - uint8_t ump_ver_minor, - bool static_fb, uint8_t num_fb, - bool midi2_proto, bool midi1_proto, - bool rx_jr, bool tx_jr) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_ENDPOINT_INFO) - | ((uint32_t)ump_ver_major << 8) - | (uint32_t)ump_ver_minor; - w[1] = (static_fb ? (UINT32_C(1) << 31) : 0) - | ((uint32_t)(num_fb & 0x7F) << 24) - | (midi2_proto ? (UINT32_C(1) << 9) : 0) - | (midi1_proto ? (UINT32_C(1) << 8) : 0) - | (rx_jr ? (UINT32_C(1) << 1) : 0) - | (tx_jr ? UINT32_C(1) : 0); -} - -/* Device Identity Notification */ -static inline void midi2_msg_stream_device_identity(uint32_t *w, - uint32_t manufacturer_id, - uint16_t family_id, - uint16_t model_id, - uint32_t version_id) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_DEVICE_IDENTITY); - w[1] = (manufacturer_id & 0x00FFFFFF) << 8; - w[2] = ((uint32_t)family_id << 16) | (uint32_t)model_id; - w[3] = version_id; -} - -/* Stream Configuration Request/Notify - * protocol: 0x01 = MIDI 1.0, 0x02 = MIDI 2.0 */ -static inline void midi2_msg_stream_config_request(uint32_t *w, uint8_t protocol) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_CONFIG_REQUEST) - | ((uint32_t)protocol << 8); -} - -static inline void midi2_msg_stream_config_notify(uint32_t *w, uint8_t protocol) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_CONFIG_NOTIFY) - | ((uint32_t)protocol << 8); -} - -/* Function Block Discovery - * fb_num: function block number to query (0xFF = all) - * filter: bitmask (bit 0 = FB info, bit 1 = FB name) */ -static inline void midi2_msg_stream_fb_discovery(uint32_t *w, uint8_t fb_num, uint8_t filter) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_FB_DISCOVERY) - | ((uint32_t)fb_num << 8) - | (uint32_t)filter; -} - -/* Function Block Info - * active: FB is active - * fb_num: function block number - * direction: 0x00=input, 0x01=output, 0x02=bidirectional - * first_group: first group in this FB - * num_groups: number of groups - * midi_ci_ver: MIDI-CI version support (0=none, 1=1.1, 2=1.2) - * sysex8: supports SysEx8 - * protocol: 0x00=unknown, 0x01=MIDI1, 0x02=MIDI2, 0x03=both */ -static inline void midi2_msg_stream_fb_info(uint32_t *w, - bool active, uint8_t fb_num, - uint8_t direction, - uint8_t first_group, uint8_t num_groups, - uint8_t midi_ci_ver, bool sysex8, - uint8_t protocol) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_FB_INFO) - | (active ? (UINT32_C(1) << 15) : 0) - | ((uint32_t)(fb_num & 0x7F) << 8) - | (uint32_t)(direction & 0x03); - w[1] = ((uint32_t)(first_group & 0x0F) << 24) - | ((uint32_t)(num_groups & 0x0F) << 16) - | ((uint32_t)(midi_ci_ver & 0x03) << 8) - | (sysex8 ? (UINT32_C(1) << 2) : 0) - | (uint32_t)(protocol & 0x03); -} - -/* Endpoint Name Notification (multi-packet text, up to 14 bytes per UMP). - * format: 0=complete, 1=start, 2=continue, 3=end. - * name: UTF-8 text, up to 14 bytes per UMP. */ -static inline void midi2_msg_stream_endpoint_name(uint32_t *w, uint8_t format, - const uint8_t *name, uint8_t len) { - if (len > 14) len = 14; - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(format, MIDI2_STREAM_ENDPOINT_NAME); - uint8_t i; - for (i = 0; i < len; i++) { - if (i < 2) { - w[0] |= ((uint32_t)name[i] << (8 - i * 8)); - } else { - uint8_t offset = (uint8_t)(i - 2); - uint8_t dwi = (uint8_t)(1 + offset / 4); - uint8_t dsh = (uint8_t)(24 - (offset % 4) * 8); - w[dwi] |= ((uint32_t)name[i] << dsh); - } - } -} - -/* Product Instance Id Notification (multi-packet text, up to 14 bytes per UMP). */ -static inline void midi2_msg_stream_product_id(uint32_t *w, uint8_t format, - const uint8_t *id, uint8_t len) { - if (len > 14) len = 14; - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(format, MIDI2_STREAM_PRODUCT_INSTANCE_ID); - uint8_t i; - for (i = 0; i < len; i++) { - if (i < 2) { - w[0] |= ((uint32_t)id[i] << (8 - i * 8)); - } else { - uint8_t offset = (uint8_t)(i - 2); - uint8_t dwi = (uint8_t)(1 + offset / 4); - uint8_t dsh = (uint8_t)(24 - (offset % 4) * 8); - w[dwi] |= ((uint32_t)id[i] << dsh); - } - } -} - -/* Function Block Name Notification (multi-packet text, up to 13 bytes per UMP). - * fb_num goes in w[0] bits [15:8], leaving 13 bytes for name. */ -static inline void midi2_msg_stream_fb_name(uint32_t *w, uint8_t format, - uint8_t fb_num, - const uint8_t *name, uint8_t len) { - if (len > 13) len = 13; - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(format, MIDI2_STREAM_FB_NAME) - | ((uint32_t)fb_num << 8); - uint8_t i; - for (i = 0; i < len; i++) { - if (i == 0) { - w[0] |= (uint32_t)name[0]; - } else { - uint8_t offset = (uint8_t)(i - 1); - uint8_t dwi = (uint8_t)(1 + offset / 4); - uint8_t dsh = (uint8_t)(24 - (offset % 4) * 8); - w[dwi] |= ((uint32_t)name[i] << dsh); - } - } -} - -/* Start/End of Clip */ -static inline void midi2_msg_stream_start_of_clip(uint32_t *w) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_START_OF_CLIP); -} - -static inline void midi2_msg_stream_end_of_clip(uint32_t *w) { - memset(w, 0, 16); - w[0] = midi2_msg_build_stream_w0(0, MIDI2_STREAM_END_OF_CLIP); -} - -/*--------------------------------------------------------------------+ - * SysEx8 (MT 0x5, 4 words) - * - * Word 0: [MT:4][group:4][status:4][numBytes:4][streamID:8][data0:8] - * Word 1: [data1:8][data2:8][data3:8][data4:8] - * Word 2: [data5:8][data6:8][data7:8][data8:8] - * Word 3: [data9:8][data10:8][data11:8][data12:8] - * - * Max 13 data bytes per packet. 8-bit data (no 7-bit restriction). - * SysEx8 and SysEx7 coexist in the same UMP stream as different MT values. - *--------------------------------------------------------------------*/ -enum { - MIDI2_SYSEX8_COMPLETE = 0x00, - MIDI2_SYSEX8_START = 0x10, - MIDI2_SYSEX8_CONTINUE = 0x20, - MIDI2_SYSEX8_END = 0x30, -}; - -static inline void midi2_msg_sysex8_packet(uint32_t *w, uint8_t group, - uint8_t status, uint8_t stream_id, - const uint8_t *data, uint8_t len) { - if (len > 13) len = 13; - memset(w, 0, 16); - /* numBytes includes stream_id + data bytes */ - uint8_t num_bytes = (uint8_t)(len + 1); - w[0] = ((uint32_t)MIDI2_MT_DATA128 << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)(status | (num_bytes & 0x0F)) << 16) - | ((uint32_t)stream_id << 8); - - /* Pack data bytes: data[0] goes to w[0] bits [7:0], - * data[1..4] into w[1], data[5..8] into w[2], data[9..12] into w[3] */ - uint8_t i; - if (len >= 1) w[0] |= (uint32_t)data[0]; - for (i = 1; i < len; i++) { - uint8_t wi = (uint8_t)(1 + (i - 1) / 4); - uint8_t sh = (uint8_t)(24 - ((i - 1) % 4) * 8); - w[wi] |= ((uint32_t)data[i] << sh); - } -} - -/*--------------------------------------------------------------------+ - * Mixed Data Set (MT 0x5, status 0x8/0x9, 4 words) - * - * MDS carries non-MIDI payloads (firmware, XML, etc.) in chunks. - * Each chunk: 1 Header UMP + N Payload UMPs, all sharing mds_id. - * Cannot be translated to MIDI 1.0. - *--------------------------------------------------------------------*/ -enum { - MIDI2_MDS_HEADER = 0x80, - MIDI2_MDS_PAYLOAD = 0x90, -}; - -/** @brief Build a Mixed Data Set Header UMP. - * @param mds_id MDS ID (0-15), ties chunks together. - * @param num_bytes Number of valid bytes in this chunk (including header). - * @param num_chunks Total chunks in data set (0 = unknown). - * @param this_chunk This chunk number (starting from 1). - * @param mfr_id 16-bit Manufacturer ID (see spec 7.10). - * @param device_id Device ID (0xFFFF = all call). - * @param sub_id1 Sub ID #1. - * @param sub_id2 Sub ID #2. */ -static inline void midi2_msg_mds_header(uint32_t *w, uint8_t group, uint8_t mds_id, - uint16_t num_bytes, uint16_t num_chunks, - uint16_t this_chunk, uint16_t mfr_id, - uint16_t device_id, uint16_t sub_id1, - uint16_t sub_id2) { - w[0] = ((uint32_t)MIDI2_MT_DATA128 << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)(MIDI2_MDS_HEADER | (mds_id & 0x0F)) << 16) - | (uint32_t)num_bytes; - w[1] = ((uint32_t)num_chunks << 16) | (uint32_t)this_chunk; - w[2] = ((uint32_t)mfr_id << 16) | (uint32_t)device_id; - w[3] = ((uint32_t)sub_id1 << 16) | (uint32_t)sub_id2; -} - -/** @brief Build a Mixed Data Set Payload UMP. - * @param mds_id MDS ID (0-15). - * @param data Payload bytes (up to 14). - * @param len Number of payload bytes (0-14). */ -static inline void midi2_msg_mds_payload(uint32_t *w, uint8_t group, uint8_t mds_id, - const uint8_t *data, uint8_t len) { - if (len > 14) len = 14; - memset(w, 0, 16); - w[0] = ((uint32_t)MIDI2_MT_DATA128 << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)(MIDI2_MDS_PAYLOAD | (mds_id & 0x0F)) << 16); - /* Pack 14 data bytes into w[0] bits [15:0] + w[1..3] */ - uint8_t i; - for (i = 0; i < len; i++) { - uint8_t wi = (uint8_t)((i + 2) / 4); - uint8_t sh = (uint8_t)(24 - ((i + 2) % 4) * 8); - w[wi] |= ((uint32_t)data[i] << sh); - } -} - -/*--------------------------------------------------------------------+ - * MIDI 1.0 Byte Stream to UMP Conversion (stateless, single message) - * - * Converts a complete MIDI 1.0 message (1-3 bytes) to a UMP word (MT 0x2). - * Does NOT handle Running Status or SysEx -- that requires state (see midi2_conv). - * Useful when the platform already parsed the byte stream. - *--------------------------------------------------------------------*/ -static inline uint32_t midi2_msg_from_midi1(uint8_t group, - uint8_t status, uint8_t data1, uint8_t data2) { - return ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)(group & 0x0F) << 24) - | ((uint32_t)status << 16) - | ((uint32_t)(data1 & 0x7F) << 8) - | (uint32_t)(data2 & 0x7F); -} - -/*--------------------------------------------------------------------+ - * Protocol Translation: MT 0x2 (MIDI 1.0 CV) -> MT 0x4 (MIDI 2.0 CV) - * - * Translates a 1-word MT 0x2 message to a 2-word MT 0x4 message with - * proper value scaling per M2-104-UM v1.1.2, Section 7. - * - * Special cases: - * - Note On with velocity 0 becomes Note Off (velocity 0x8000) - * - Program Change: bank_valid = false (no bank info in MT 0x2) - * - Pitch Bend: combines data1 (LSB) + data2 (MSB) before scaling - * - * Returns true if the message was translated, false if mt2_word is not - * a Channel Voice message (wrong MT or unrecognized status). - *--------------------------------------------------------------------*/ -static inline bool midi2_msg_mt2_to_mt4(uint32_t mt2_word, uint32_t out[2]) { - if (midi2_msg_get_mt(&mt2_word) != MIDI2_MT_MIDI1_CV) return false; - - uint8_t group = (mt2_word >> 24) & 0x0F; - uint8_t status = (mt2_word >> 16) & 0xF0; - uint8_t channel = (mt2_word >> 16) & 0x0F; - uint8_t data1 = (mt2_word >> 8) & 0x7F; - uint8_t data2 = (mt2_word ) & 0x7F; - - switch (status) { - case 0x90: /* Note On */ - if (data2 == 0) { - /* velocity 0 means Note Off per MIDI 1.0 convention */ - midi2_msg_note_off(out, group, channel, data1, - midi2_msg_scale_up_7to16(64), 0); - } else { - midi2_msg_note_on(out, group, channel, data1, - midi2_msg_scale_up_7to16(data2), 0); - } - return true; - - case 0x80: /* Note Off */ - midi2_msg_note_off(out, group, channel, data1, - midi2_msg_scale_up_7to16(data2), 0); - return true; - - case 0xB0: /* Control Change */ - midi2_msg_cc(out, group, channel, data1, - midi2_msg_scale_up_7to32(data2)); - return true; - - case 0xC0: /* Program Change */ - midi2_msg_program(out, group, channel, data1, false, 0, 0); - return true; - - case 0xD0: /* Channel Pressure */ - midi2_msg_chan_pressure(out, group, channel, - midi2_msg_scale_up_7to32(data1)); - return true; - - case 0xE0: /* Pitch Bend */ - { - uint16_t bend14 = ((uint16_t)data2 << 7) | data1; - midi2_msg_pitch_bend(out, group, channel, - midi2_msg_scale_up_14to32(bend14)); - } - return true; - - case 0xA0: /* Poly Pressure */ - midi2_msg_poly_pressure(out, group, channel, data1, - midi2_msg_scale_up_7to32(data2)); - return true; - - default: - return false; - } -} - -/*--------------------------------------------------------------------+ - * Protocol Translation: MT 0x4 (MIDI 2.0 CV) -> MT 0x2 (MIDI 1.0 CV) - * - * Inverse of midi2_msg_mt2_to_mt4. Lossy by spec: MIDI 1.0 CV cannot - * carry RPN/NRPN/Rel/Per-Note in a single word (would require a 4-CC - * sequence). Those statuses are skipped (returns 0 words emitted). - * Caller detects skips by comparing emitted word count against the - * expected count (1 word per MT 0x4 message that is supported). - * - * Mapping per M2-115 section 4.2 / 4.3: - * Note On/Off : velocity 16-bit -> 7-bit - * CC : value 32-bit -> 7-bit - * Pitch Bend : 32-bit -> 14-bit (LSB / MSB split) - * Channel Pressure : 32-bit -> 7-bit - * Poly Pressure : 32-bit -> 7-bit - * Program Change : program byte preserved; bank dropped - * Per-Note CC/PB/Mgmt, RPN/NRPN/Rel: dropped (no MIDI 1.0 form) - * - * @return number of MT 0x2 words written (0 or 1). - * (v0.3.0+) */ -static inline uint32_t midi2_msg_mt4_to_mt2(const uint32_t mt4_words[2], - uint32_t *out_word) { - if (out_word == NULL) return 0; - uint8_t mt = (uint8_t)((mt4_words[0] >> 28) & 0x0Fu); - if (mt != MIDI2_MT_MIDI2_CV) return 0; - uint8_t grp = (uint8_t)((mt4_words[0] >> 24) & 0x0Fu); - uint8_t stat = (uint8_t)((mt4_words[0] >> 16) & 0xFFu); - uint8_t hi = (uint8_t)(stat & 0xF0u); - uint8_t ch = (uint8_t)(stat & 0x0Fu); - - switch (hi) { - case MIDI2_STATUS_NOTE_OFF: - case MIDI2_STATUS_NOTE_ON: { - uint8_t note = (uint8_t)((mt4_words[0] >> 8) & 0x7Fu); - uint16_t vel16 = (uint16_t)((mt4_words[1] >> 16) & 0xFFFFu); - uint8_t vel7 = midi2_msg_scale_down_16to7(vel16); - *out_word = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)grp << 24) - | ((uint32_t)hi << 16) - | ((uint32_t)ch << 16) - | ((uint32_t)note << 8) - | (uint32_t)vel7; - return 1; - } - case MIDI2_STATUS_POLY_PRESSURE: { - uint8_t note = (uint8_t)((mt4_words[0] >> 8) & 0x7Fu); - uint8_t v7 = midi2_msg_scale_down_32to7(mt4_words[1]); - *out_word = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)grp << 24) - | ((uint32_t)MIDI2_STATUS_POLY_PRESSURE << 16) - | ((uint32_t)ch << 16) - | ((uint32_t)note << 8) - | (uint32_t)v7; - return 1; - } - case MIDI2_STATUS_CC: { - uint8_t cc = (uint8_t)((mt4_words[0] >> 8) & 0x7Fu); - uint8_t v7 = midi2_msg_scale_down_32to7(mt4_words[1]); - *out_word = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)grp << 24) - | ((uint32_t)MIDI2_STATUS_CC << 16) - | ((uint32_t)ch << 16) - | ((uint32_t)cc << 8) - | (uint32_t)v7; - return 1; - } - case MIDI2_STATUS_PROGRAM: { - uint8_t prog = (uint8_t)((mt4_words[1] >> 24) & 0x7Fu); - *out_word = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)grp << 24) - | ((uint32_t)MIDI2_STATUS_PROGRAM << 16) - | ((uint32_t)ch << 16) - | ((uint32_t)prog << 8); - return 1; - } - case MIDI2_STATUS_CHAN_PRESSURE: { - uint8_t v7 = midi2_msg_scale_down_32to7(mt4_words[1]); - *out_word = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)grp << 24) - | ((uint32_t)MIDI2_STATUS_CHAN_PRESSURE << 16) - | ((uint32_t)ch << 16) - | ((uint32_t)v7 << 8); - return 1; - } - case MIDI2_STATUS_PITCH_BEND: { - uint16_t pb14 = midi2_msg_scale_down_32to14(mt4_words[1]); - *out_word = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)grp << 24) - | ((uint32_t)MIDI2_STATUS_PITCH_BEND << 16) - | ((uint32_t)ch << 16) - | ((uint32_t)(pb14 & 0x7Fu) << 8) - | (uint32_t)((pb14 >> 7) & 0x7Fu); - return 1; - } - default: - /* RPN/NRPN/Rel/Per-Note dropped; caller detects via count. */ - return 0; - } -} - -/*--------------------------------------------------------------------+ - * USB MIDI 1.0 cable event -> UMP MT 0x2 - * - * USB MIDI v1.0 class delivers Channel Voice and System Common - * messages as 4-byte cable events: - * byte 0 = (cable_number << 4) | CIN - * byte 1 = MIDI status byte - * byte 2 = data 1 - * byte 3 = data 2 - * Packed LSB-first into the uint32_t argument. - * - * Supported CINs: 0x2, 0x3 (System Common), 0x8-0xE (Channel Voice). - * Reserved CINs (0x0, 0x1) and SysEx fragments (0x4-0x7, 0xF) return - * false; the latter need stateful reassembly handled by midi2_conv. - * - * @return true on success, false on unsupported CIN or NULL output. - * (v0.3.0+) */ -static inline bool midi2_msg_cable_event_to_ump(uint32_t cable_event, - uint8_t group, - uint32_t *ump_out) { - if (ump_out == NULL) return false; - uint8_t b0 = (uint8_t)(cable_event & 0xFFu); - uint8_t status = (uint8_t)((cable_event >> 8) & 0xFFu); - uint8_t data1 = (uint8_t)((cable_event >> 16) & 0xFFu); - uint8_t data2 = (uint8_t)((cable_event >> 24) & 0xFFu); - uint8_t cin = (uint8_t)(b0 & 0x0Fu); - - switch (cin) { - case 0x2: case 0x3: - case 0x8: case 0x9: case 0xA: case 0xB: - case 0xC: case 0xD: case 0xE: - *ump_out = ((uint32_t)MIDI2_MT_MIDI1_CV << 28) - | ((uint32_t)(group & 0x0Fu) << 24) - | ((uint32_t)status << 16) - | ((uint32_t)(data1 & 0x7Fu) << 8) - | ((uint32_t)(data2 & 0x7Fu)); - return true; - default: - /* Reserved (0x0, 0x1) or SysEx fragment (0x4-0x7, 0xF). - * SysEx fragments are handled by midi2_conv (stateful). */ - return false; - } -} - -/* == midi2_ci_msg ======================================================== */ - - -/* - * midi2_ci_msg.h - MIDI-CI message construction and parsing - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI-CI (M2-101-UM v1.2, Jun 2023) - * Version: 0.3.0 - */ - - - - -/*====================================================================+ - * midi2_ci_msg -- MIDI-CI message construction and parsing - * - * Header-only, stateless. Builds and reads MIDI-CI SysEx payloads - * per M2-101-UM MIDI-CI Specification v1.2. - * - * All build functions write into a caller-provided buffer and return - * the number of bytes written. The buffer does NOT include F0/F7 - * delimiters (those are added by the SysEx transport layer). - * - * All parse functions extract fields from a received SysEx payload - * (also without F0/F7). - *====================================================================*/ - -/*--------------------------------------------------------------------+ - * Constants - *--------------------------------------------------------------------*/ - -#define MIDI2_CI_BROADCAST_MUID UINT32_C(0x0FFFFFFF) -#define MIDI2_CI_VERSION_1 0x01 -#define MIDI2_CI_VERSION_2 0x02 /* current: MIDI-CI v1.2 */ - -/*--------------------------------------------------------------------+ - * Sub-ID#2 values (Appendix D, M2-101-UM) - *--------------------------------------------------------------------*/ -enum { - /* Category 7: Management (0x70-0x7F) */ - MIDI2_CI_DISCOVERY = 0x70, - MIDI2_CI_DISCOVERY_REPLY = 0x71, - MIDI2_CI_ENDPOINT_INFO = 0x72, - MIDI2_CI_ENDPOINT_INFO_REPLY = 0x73, - MIDI2_CI_ACK = 0x7D, - MIDI2_CI_INVALIDATE_MUID = 0x7E, - MIDI2_CI_NAK = 0x7F, - - /* Category 2: Profile Configuration (0x20-0x2F) */ - MIDI2_CI_PROFILE_INQUIRY = 0x20, - MIDI2_CI_PROFILE_INQUIRY_REPLY = 0x21, - MIDI2_CI_SET_PROFILE_ON = 0x22, - MIDI2_CI_SET_PROFILE_OFF = 0x23, - MIDI2_CI_PROFILE_ENABLED = 0x24, - MIDI2_CI_PROFILE_DISABLED = 0x25, - MIDI2_CI_PROFILE_ADDED = 0x26, - MIDI2_CI_PROFILE_REMOVED = 0x27, - MIDI2_CI_PROFILE_DETAILS = 0x28, - MIDI2_CI_PROFILE_DETAILS_REPLY = 0x29, - MIDI2_CI_PROFILE_SPECIFIC_DATA = 0x2F, - - /* Category 3: Property Exchange (0x30-0x3F) */ - MIDI2_CI_PE_CAPABILITY = 0x30, - MIDI2_CI_PE_CAPABILITY_REPLY = 0x31, - MIDI2_CI_PE_GET = 0x34, - MIDI2_CI_PE_GET_REPLY = 0x35, - MIDI2_CI_PE_SET = 0x36, - MIDI2_CI_PE_SET_REPLY = 0x37, - MIDI2_CI_PE_SUBSCRIBE = 0x38, - MIDI2_CI_PE_SUBSCRIBE_REPLY = 0x39, - MIDI2_CI_PE_NOTIFY = 0x3F, - - /* Category 4: Process Inquiry (0x40-0x4F) */ - MIDI2_CI_PI_CAPABILITY = 0x40, - MIDI2_CI_PI_CAPABILITY_REPLY = 0x41, - MIDI2_CI_PI_MIDI_REPORT = 0x42, - MIDI2_CI_PI_MIDI_REPORT_REPLY = 0x43, - MIDI2_CI_PI_MIDI_REPORT_END = 0x44, -}; - -/*--------------------------------------------------------------------+ - * Capability Inquiry Category Supported bitmap (Table 7) - *--------------------------------------------------------------------*/ -enum { - MIDI2_CI_CAT_PROFILE_CONFIG = 0x04, /* bit 2 */ - MIDI2_CI_CAT_PROPERTY_EXCHANGE = 0x08, /* bit 3 */ - MIDI2_CI_CAT_PROCESS_INQUIRY = 0x10, /* bit 4 */ -}; - -/*--------------------------------------------------------------------+ - * NAK Status Codes (Table 16) - *--------------------------------------------------------------------*/ -enum { - MIDI2_CI_NAK_OK = 0x00, - MIDI2_CI_NAK_NOT_SUPPORTED = 0x01, - MIDI2_CI_NAK_VERSION_ERR = 0x02, - MIDI2_CI_NAK_CH_NOT_IN_USE = 0x03, - MIDI2_CI_NAK_PROFILE_NOT_SUPP = 0x04, - MIDI2_CI_NAK_TERMINATE_PE = 0x20, - MIDI2_CI_NAK_PE_OUT_OF_SEQ = 0x21, - MIDI2_CI_NAK_ERROR_RETRY = 0x40, - MIDI2_CI_NAK_MALFORMED = 0x41, - MIDI2_CI_NAK_TIMEOUT = 0x42, - MIDI2_CI_NAK_BUSY = 0x43, -}; - -/*--------------------------------------------------------------------+ - * ACK Status Codes (Table 14) - *--------------------------------------------------------------------*/ -enum { - MIDI2_CI_ACK_OK = 0x00, - MIDI2_CI_ACK_TIMEOUT_WAIT = 0x10, -}; - -/*--------------------------------------------------------------------+ - * MUID utilities - *--------------------------------------------------------------------*/ - -/** Read 28-bit MUID from 4 bytes (LSB first, 7-bit encoding). */ -static inline uint32_t midi2_ci_read_muid(const uint8_t *p) { - return (uint32_t)(p[0] & 0x7F) - | ((uint32_t)(p[1] & 0x7F) << 7) - | ((uint32_t)(p[2] & 0x7F) << 14) - | ((uint32_t)(p[3] & 0x7F) << 21); -} - -/** Write 28-bit MUID as 4 bytes (LSB first, 7-bit encoding). */ -static inline void midi2_ci_write_muid(uint8_t *p, uint32_t muid) { - p[0] = (uint8_t)((muid >> 0) & 0x7F); - p[1] = (uint8_t)((muid >> 7) & 0x7F); - p[2] = (uint8_t)((muid >> 14) & 0x7F); - p[3] = (uint8_t)((muid >> 21) & 0x7F); -} - -/*--------------------------------------------------------------------+ - * Common header: parse - * - * Standard CI SysEx layout (without F0/F7): - * [0] 0x7E Universal System Exclusive - * [1] Device ID (0x00-0x0F = channel, 0x7E = group, 0x7F = function block) - * [2] 0x0D MIDI-CI Sub-ID#1 - * [3] Sub-ID#2 (message type) - * [4] MIDI-CI Message Version/Format - * [5..8] Source MUID (4 bytes, LSB first) - * [9..12] Destination MUID (4 bytes, LSB first) - * [13..] Message-specific data - *--------------------------------------------------------------------*/ - -static inline uint8_t midi2_ci_get_device_id(const uint8_t *d) { return d[1]; } -static inline uint8_t midi2_ci_get_sub_id(const uint8_t *d) { return d[3]; } -static inline uint8_t midi2_ci_get_version(const uint8_t *d) { return d[4]; } -static inline uint32_t midi2_ci_get_src_muid(const uint8_t *d) { return midi2_ci_read_muid(&d[5]); } -static inline uint32_t midi2_ci_get_dst_muid(const uint8_t *d) { return midi2_ci_read_muid(&d[9]); } - -/** Check if SysEx payload is a MIDI-CI message (7E xx 0D ...). */ -static inline bool midi2_ci_is_ci(const uint8_t *d, uint16_t len) { - return len >= 13 && d[0] == 0x7E && d[2] == 0x0D; -} - -/*--------------------------------------------------------------------+ - * Common header: build - * - * Returns offset after header (13 bytes). Caller continues writing - * message-specific data at the returned offset. - *--------------------------------------------------------------------*/ - -static inline uint16_t midi2_ci_build_header(uint8_t *buf, uint8_t device_id, - uint8_t sub_id, uint8_t version, - uint32_t src_muid, uint32_t dst_muid) { - buf[0] = 0x7E; - buf[1] = device_id; - buf[2] = 0x0D; - buf[3] = sub_id; - buf[4] = version; - midi2_ci_write_muid(&buf[5], src_muid); - midi2_ci_write_muid(&buf[9], dst_muid); - return 13; -} - -/*--------------------------------------------------------------------+ - * 14-bit value read/write (2 bytes, LSB first, 7-bit per byte) - * - * All multi-byte fields inside SysEx use 7-bit encoding (bit 7 = 0). - * Range: 0 to 16383 (0x3FFF). Used for profile counts, data lengths, - * chunk numbers, and channel counts in MIDI-CI messages. - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_read_14(const uint8_t *p) { - return (uint16_t)(p[0] & 0x7F) | ((uint16_t)(p[1] & 0x7F) << 7); -} -static inline void midi2_ci_write_14(uint8_t *p, uint16_t v) { - p[0] = (uint8_t)(v & 0x7F); - p[1] = (uint8_t)((v >> 7) & 0x7F); -} - -/*--------------------------------------------------------------------+ - * 28-bit value read/write (4 bytes, LSB first, 7-bit each) - *--------------------------------------------------------------------*/ -static inline uint32_t midi2_ci_read_28(const uint8_t *p) { - return midi2_ci_read_muid(p); /* same encoding */ -} -static inline void midi2_ci_write_28(uint8_t *p, uint32_t v) { - midi2_ci_write_muid(p, v); -} - -/*--------------------------------------------------------------------+ - * Parse helpers for common message fields - * - * All offsets assume standard header (13 bytes). Message-specific data - * starts at byte 13. These extract fields without the caller needing - * to know byte offsets. - *--------------------------------------------------------------------*/ - -/* Discovery / Discovery Reply: device identification fields at offset 13. - * Manufacturer ID: 3 bytes, literal SysEx ID bytes (all <= 0x7F). - * Family/Model: 2 bytes each, 7-bit LSB-first (14-bit value). - * SW Revision: 4 bytes, 7-bit LSB-first (28-bit value). - * These match the "Device Inquiry" Universal SysEx format (section 5.5.1). */ -static inline uint32_t midi2_ci_get_mfr_id(const uint8_t *d) { - return (uint32_t)d[13] | ((uint32_t)d[14] << 8) | ((uint32_t)d[15] << 16); -} -static inline uint16_t midi2_ci_get_family(const uint8_t *d) { - return midi2_ci_read_14(&d[16]); -} -static inline uint16_t midi2_ci_get_model(const uint8_t *d) { - return midi2_ci_read_14(&d[18]); -} -static inline uint32_t midi2_ci_get_sw_rev(const uint8_t *d) { - return midi2_ci_read_28(&d[20]); -} -static inline uint8_t midi2_ci_get_ci_category(const uint8_t *d) { - return d[24]; -} -static inline uint32_t midi2_ci_get_max_sysex(const uint8_t *d) { - return midi2_ci_read_28(&d[25]); -} - -/* Invalidate MUID: target MUID at offset 13 */ -static inline uint32_t midi2_ci_get_target_muid(const uint8_t *d) { - return midi2_ci_read_muid(&d[13]); -} - -/* ACK/NAK (v2+): fields after header */ -static inline uint8_t midi2_ci_get_orig_sub_id(const uint8_t *d) { return d[13]; } -static inline uint8_t midi2_ci_get_nak_status_code(const uint8_t *d) { return d[14]; } -static inline uint8_t midi2_ci_get_nak_status_data(const uint8_t *d) { return d[15]; } - -/* Profile messages: profile ID at offset 13 (after header) */ -static inline const uint8_t *midi2_ci_get_profile_id(const uint8_t *d) { return &d[13]; } - -/* Profile Inquiry Reply: counts */ -static inline uint16_t midi2_ci_get_enabled_count(const uint8_t *d) { - return midi2_ci_read_14(&d[13]); -} - -/* PE data messages: request_id at offset 13 */ -static inline uint8_t midi2_ci_get_pe_request_id(const uint8_t *d) { return d[13]; } -static inline uint16_t midi2_ci_get_pe_header_len(const uint8_t *d) { - return midi2_ci_read_14(&d[14]); -} - -/* Process Inquiry MIDI Report: bitmaps */ -static inline uint8_t midi2_ci_get_pi_msg_data_control(const uint8_t *d) { return d[13]; } -static inline uint8_t midi2_ci_get_pi_system_bitmap(const uint8_t *d) { return d[14]; } -static inline uint8_t midi2_ci_get_pi_channel_ctrl_bitmap(const uint8_t *d) { return d[16]; } -static inline uint8_t midi2_ci_get_pi_note_data_bitmap(const uint8_t *d) { return d[17]; } - -/*====================================================================+ - * CATEGORY 7: MANAGEMENT MESSAGES - *====================================================================*/ - -/*--------------------------------------------------------------------+ - * Discovery (0x70) -- Table 6 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_discovery( - uint8_t *buf, uint8_t version, uint32_t src_muid, - uint32_t mfr_id, uint16_t family, uint16_t model, uint32_t sw_rev, - uint8_t ci_category, uint32_t max_sysex, uint8_t output_path_id) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_DISCOVERY, version, - src_muid, MIDI2_CI_BROADCAST_MUID); - /* Device Manufacturer (3 bytes SysEx ID) */ - buf[p++] = (uint8_t)((mfr_id >> 0) & 0x7F); - buf[p++] = (uint8_t)((mfr_id >> 8) & 0x7F); - buf[p++] = (uint8_t)((mfr_id >> 16) & 0x7F); - /* Device Family (2 bytes LSB first) */ - buf[p++] = (uint8_t)(family & 0x7F); - buf[p++] = (uint8_t)((family >> 7) & 0x7F); - /* Model Number (2 bytes LSB first) */ - buf[p++] = (uint8_t)(model & 0x7F); - buf[p++] = (uint8_t)((model >> 7) & 0x7F); - /* Software Revision (4 bytes) */ - midi2_ci_write_28(&buf[p], sw_rev); p += 4; - /* Capability Inquiry Category Supported */ - buf[p++] = ci_category; - /* Receivable Maximum SysEx Size (4 bytes LSB first) */ - midi2_ci_write_28(&buf[p], max_sysex); p += 4; - /* Output Path Id (v2+) */ - if (version >= MIDI2_CI_VERSION_2) { - buf[p++] = output_path_id; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Reply to Discovery (0x71) -- Table 8 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_discovery_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint32_t mfr_id, uint16_t family, uint16_t model, uint32_t sw_rev, - uint8_t ci_category, uint32_t max_sysex, - uint8_t output_path_id, uint8_t function_block) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_DISCOVERY_REPLY, version, - src_muid, dst_muid); - buf[p++] = (uint8_t)((mfr_id >> 0) & 0x7F); - buf[p++] = (uint8_t)((mfr_id >> 8) & 0x7F); - buf[p++] = (uint8_t)((mfr_id >> 16) & 0x7F); - buf[p++] = (uint8_t)(family & 0x7F); - buf[p++] = (uint8_t)((family >> 7) & 0x7F); - buf[p++] = (uint8_t)(model & 0x7F); - buf[p++] = (uint8_t)((model >> 7) & 0x7F); - midi2_ci_write_28(&buf[p], sw_rev); p += 4; - buf[p++] = ci_category; - midi2_ci_write_28(&buf[p], max_sysex); p += 4; - if (version >= MIDI2_CI_VERSION_2) { - buf[p++] = output_path_id; - buf[p++] = function_block; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Inquiry: Endpoint Information (0x72) -- Table 9 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_endpoint_info( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t status) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_ENDPOINT_INFO, version, - src_muid, dst_muid); - buf[p++] = status; - return p; -} - -/*--------------------------------------------------------------------+ - * Reply to Endpoint Information (0x73) -- Table 11 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_endpoint_info_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t status, const uint8_t *info_data, uint16_t info_len) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_ENDPOINT_INFO_REPLY, version, - src_muid, dst_muid); - buf[p++] = status; - midi2_ci_write_14(&buf[p], info_len); p += 2; - if (info_data && info_len > 0) { - memcpy(&buf[p], info_data, info_len); - p += info_len; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Invalidate MUID (0x7E) -- Table 12 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_invalidate_muid( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t target_muid) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_INVALIDATE_MUID, version, - src_muid, MIDI2_CI_BROADCAST_MUID); - midi2_ci_write_muid(&buf[p], target_muid); p += 4; - return p; -} - -/*--------------------------------------------------------------------+ - * ACK (0x7D) -- Table 13 (v2+) - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_ack( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, uint8_t orig_sub_id, - uint8_t status_code, uint8_t status_data, - const uint8_t *details, uint16_t msg_len, const uint8_t *msg_text) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_ACK, version, - src_muid, dst_muid); - buf[p++] = orig_sub_id; - buf[p++] = status_code; - buf[p++] = status_data; - /* 5 bytes details */ - if (details) { memcpy(&buf[p], details, 5); } else { memset(&buf[p], 0, 5); } - p += 5; - /* Message text */ - midi2_ci_write_14(&buf[p], msg_len); p += 2; - if (msg_text && msg_len > 0) { - memcpy(&buf[p], msg_text, msg_len); - p += msg_len; - } - return p; -} - -/*--------------------------------------------------------------------+ - * NAK (0x7F) -- Table 15 (v2+) - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_nak( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, uint8_t orig_sub_id, - uint8_t status_code, uint8_t status_data, - const uint8_t *details, uint16_t msg_len, const uint8_t *msg_text) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_NAK, version, - src_muid, dst_muid); - if (version >= MIDI2_CI_VERSION_2) { - buf[p++] = orig_sub_id; - buf[p++] = status_code; - buf[p++] = status_data; - if (details) { memcpy(&buf[p], details, 5); } else { memset(&buf[p], 0, 5); } - p += 5; - midi2_ci_write_14(&buf[p], msg_len); p += 2; - if (msg_text && msg_len > 0) { - memcpy(&buf[p], msg_text, msg_len); - p += msg_len; - } - } - return p; -} - -/*====================================================================+ - * CATEGORY 2: PROFILE CONFIGURATION MESSAGES - *====================================================================*/ - -/*--------------------------------------------------------------------+ - * Profile Inquiry (0x20) -- Table 17 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_inquiry( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id) { - return midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_INQUIRY, version, - src_muid, dst_muid); -} - -/*--------------------------------------------------------------------+ - * Reply to Profile Inquiry (0x21) -- Table 18 - * enabled/disabled: arrays of 5-byte profile IDs - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_inquiry_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, - const uint8_t (*enabled)[5], uint16_t enabled_count, - const uint8_t (*disabled)[5], uint16_t disabled_count) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_INQUIRY_REPLY, - version, src_muid, dst_muid); - midi2_ci_write_14(&buf[p], enabled_count); p += 2; - { uint16_t i; for (i = 0; i < enabled_count; i++) { memcpy(&buf[p], enabled[i], 5); p += 5; } } - midi2_ci_write_14(&buf[p], disabled_count); p += 2; - { uint16_t i; for (i = 0; i < disabled_count; i++) { memcpy(&buf[p], disabled[i], 5); p += 5; } } - return p; -} - -/*--------------------------------------------------------------------+ - * Set Profile On (0x22) -- Table 24 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_set_profile_on( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, const uint8_t profile_id[5], uint16_t num_channels) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_SET_PROFILE_ON, - version, src_muid, dst_muid); - memcpy(&buf[p], profile_id, 5); p += 5; - if (version >= MIDI2_CI_VERSION_2) { - midi2_ci_write_14(&buf[p], num_channels); p += 2; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Set Profile Off (0x23) -- Table 25 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_set_profile_off( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, const uint8_t profile_id[5]) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_SET_PROFILE_OFF, - version, src_muid, dst_muid); - memcpy(&buf[p], profile_id, 5); p += 5; - if (version >= MIDI2_CI_VERSION_2) { - buf[p++] = 0x00; buf[p++] = 0x00; /* reserved */ - } - return p; -} - -/*--------------------------------------------------------------------+ - * Profile Enabled Report (0x24) -- Table 26 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_enabled( - uint8_t *buf, uint8_t version, uint32_t src_muid, - uint8_t device_id, const uint8_t profile_id[5], uint16_t num_channels) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_ENABLED, - version, src_muid, MIDI2_CI_BROADCAST_MUID); - memcpy(&buf[p], profile_id, 5); p += 5; - if (version >= MIDI2_CI_VERSION_2) { - midi2_ci_write_14(&buf[p], num_channels); p += 2; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Profile Disabled Report (0x25) -- Table 27 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_disabled( - uint8_t *buf, uint8_t version, uint32_t src_muid, - uint8_t device_id, const uint8_t profile_id[5], uint16_t num_channels) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_DISABLED, - version, src_muid, MIDI2_CI_BROADCAST_MUID); - memcpy(&buf[p], profile_id, 5); p += 5; - if (version >= MIDI2_CI_VERSION_2) { - midi2_ci_write_14(&buf[p], num_channels); p += 2; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Profile Added Report (0x26) -- Table 20 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_added( - uint8_t *buf, uint8_t version, uint32_t src_muid, - uint8_t device_id, const uint8_t profile_id[5]) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_ADDED, - version, src_muid, MIDI2_CI_BROADCAST_MUID); - memcpy(&buf[p], profile_id, 5); p += 5; - return p; -} - -/*--------------------------------------------------------------------+ - * Profile Removed Report (0x27) -- Table 21 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_removed( - uint8_t *buf, uint8_t version, uint32_t src_muid, - uint8_t device_id, const uint8_t profile_id[5]) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_REMOVED, - version, src_muid, MIDI2_CI_BROADCAST_MUID); - memcpy(&buf[p], profile_id, 5); p += 5; - return p; -} - -/*--------------------------------------------------------------------+ - * Profile Details Inquiry (0x28) -- Table 22 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_details( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, const uint8_t profile_id[5], uint8_t inquiry_target) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_DETAILS, - version, src_muid, dst_muid); - memcpy(&buf[p], profile_id, 5); p += 5; - buf[p++] = inquiry_target; - return p; -} - -/*--------------------------------------------------------------------+ - * Reply to Profile Details (0x29) -- Table 23 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_details_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, const uint8_t profile_id[5], - uint8_t inquiry_target, const uint8_t *data, uint16_t data_len) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_DETAILS_REPLY, - version, src_muid, dst_muid); - memcpy(&buf[p], profile_id, 5); p += 5; - buf[p++] = inquiry_target; - midi2_ci_write_14(&buf[p], data_len); p += 2; - if (data && data_len > 0) { memcpy(&buf[p], data, data_len); p += data_len; } - return p; -} - -/*--------------------------------------------------------------------+ - * Profile Specific Data (0x2F) -- Table 28 - * - * Note: When dst_muid is Broadcast, data_len shall not exceed 512 bytes - * and the message shall use chunking if needed (per spec 7.12). - * This function does not enforce the limit -- the caller is responsible. - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_profile_specific_data( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, const uint8_t profile_id[5], - const uint8_t *data, uint32_t data_len) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PROFILE_SPECIFIC_DATA, - version, src_muid, dst_muid); - memcpy(&buf[p], profile_id, 5); p += 5; - midi2_ci_write_28(&buf[p], data_len); p += 4; - if (data && data_len > 0) { - memcpy(&buf[p], data, data_len); - p += (uint16_t)data_len; - } - return p; -} - -/*====================================================================+ - * CATEGORY 3: PROPERTY EXCHANGE MESSAGES - *====================================================================*/ - -/*--------------------------------------------------------------------+ - * PE Capabilities Inquiry (0x30) -- Table 30 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pe_capability( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t max_simultaneous, uint8_t pe_ver_major, uint8_t pe_ver_minor) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_PE_CAPABILITY, version, - src_muid, dst_muid); - buf[p++] = max_simultaneous; - if (version >= MIDI2_CI_VERSION_2) { - buf[p++] = pe_ver_major; - buf[p++] = pe_ver_minor; - } - return p; -} - -/*--------------------------------------------------------------------+ - * Reply to PE Capabilities (0x31) -- Table 32 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pe_capability_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t max_simultaneous, uint8_t pe_ver_major, uint8_t pe_ver_minor) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_PE_CAPABILITY_REPLY, version, - src_muid, dst_muid); - buf[p++] = max_simultaneous; - if (version >= MIDI2_CI_VERSION_2) { - buf[p++] = pe_ver_major; - buf[p++] = pe_ver_minor; - } - return p; -} - -/*--------------------------------------------------------------------+ - * PE Data Message (generic builder for Get/Set/Subscribe/Notify) - * - * Used by: 0x34 Get, 0x35 Get Reply, 0x36 Set, 0x37 Set Reply, - * 0x38 Subscribe, 0x39 Subscribe Reply, 0x3F Notify - * - * All PE data messages share the same structure: - * header + request_id + header_len + header_data + - * num_chunks + this_chunk + body_len + body_data - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pe_data( - uint8_t *buf, uint8_t version, uint8_t sub_id, - uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, - const uint8_t *header_data, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body_data, uint16_t body_len) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, sub_id, version, - src_muid, dst_muid); - buf[p++] = request_id; - /* Header data */ - midi2_ci_write_14(&buf[p], header_len); p += 2; - if (header_data && header_len > 0) { memcpy(&buf[p], header_data, header_len); p += header_len; } - /* Chunk info */ - midi2_ci_write_14(&buf[p], num_chunks); p += 2; - midi2_ci_write_14(&buf[p], this_chunk); p += 2; - /* Body data */ - midi2_ci_write_14(&buf[p], body_len); p += 2; - if (body_data && body_len > 0) { memcpy(&buf[p], body_data, body_len); p += body_len; } - return p; -} - -/* Convenience wrappers */ -static inline uint16_t midi2_ci_build_pe_get( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_GET, src_muid, dst_muid, - request_id, header, header_len, 1, 1, NULL, 0); -} - -static inline uint16_t midi2_ci_build_pe_get_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body, uint16_t body_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_GET_REPLY, src_muid, dst_muid, - request_id, header, header_len, num_chunks, this_chunk, - body, body_len); -} - -static inline uint16_t midi2_ci_build_pe_set( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body, uint16_t body_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_SET, src_muid, dst_muid, - request_id, header, header_len, num_chunks, this_chunk, - body, body_len); -} - -static inline uint16_t midi2_ci_build_pe_set_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_SET_REPLY, src_muid, dst_muid, - request_id, header, header_len, 1, 1, NULL, 0); -} - -static inline uint16_t midi2_ci_build_pe_subscribe( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body, uint16_t body_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_SUBSCRIBE, src_muid, dst_muid, - request_id, header, header_len, num_chunks, this_chunk, - body, body_len); -} - -static inline uint16_t midi2_ci_build_pe_subscribe_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body, uint16_t body_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_SUBSCRIBE_REPLY, src_muid, dst_muid, - request_id, header, header_len, num_chunks, this_chunk, - body, body_len); -} - -static inline uint16_t midi2_ci_build_pe_notify( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t request_id, const uint8_t *header, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body, uint16_t body_len) { - return midi2_ci_build_pe_data(buf, version, MIDI2_CI_PE_NOTIFY, src_muid, dst_muid, - request_id, header, header_len, num_chunks, this_chunk, - body, body_len); -} - -/*====================================================================+ - * CATEGORY 4: PROCESS INQUIRY MESSAGES - *====================================================================*/ - -/*--------------------------------------------------------------------+ - * PI Capabilities Inquiry (0x40) -- Table 40 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pi_capability( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid) { - return midi2_ci_build_header(buf, 0x7F, MIDI2_CI_PI_CAPABILITY, version, - src_muid, dst_muid); -} - -/*--------------------------------------------------------------------+ - * Reply to PI Capabilities (0x41) -- Table 41 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pi_capability_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t supported_features) { - uint16_t p = midi2_ci_build_header(buf, 0x7F, MIDI2_CI_PI_CAPABILITY_REPLY, version, - src_muid, dst_muid); - buf[p++] = supported_features; - return p; -} - -/*--------------------------------------------------------------------+ - * MIDI Message Report Inquiry (0x42) -- Table 43 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pi_midi_report( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, uint8_t msg_data_control, - uint8_t system_bitmap, uint8_t reserved, - uint8_t channel_ctrl_bitmap, uint8_t note_data_bitmap) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PI_MIDI_REPORT, version, - src_muid, dst_muid); - buf[p++] = msg_data_control; - buf[p++] = system_bitmap; - buf[p++] = reserved; - buf[p++] = channel_ctrl_bitmap; - buf[p++] = note_data_bitmap; - return p; -} - -/*--------------------------------------------------------------------+ - * Reply to MIDI Message Report (0x43) -- Table 45 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pi_midi_report_reply( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id, - uint8_t system_bitmap, uint8_t reserved, - uint8_t channel_ctrl_bitmap, uint8_t note_data_bitmap) { - uint16_t p = midi2_ci_build_header(buf, device_id, MIDI2_CI_PI_MIDI_REPORT_REPLY, - version, src_muid, dst_muid); - buf[p++] = system_bitmap; - buf[p++] = reserved; - buf[p++] = channel_ctrl_bitmap; - buf[p++] = note_data_bitmap; - return p; -} - -/*--------------------------------------------------------------------+ - * End of MIDI Message Report (0x44) -- Table 46 - *--------------------------------------------------------------------*/ -static inline uint16_t midi2_ci_build_pi_midi_report_end( - uint8_t *buf, uint8_t version, uint32_t src_muid, uint32_t dst_muid, - uint8_t device_id) { - return midi2_ci_build_header(buf, device_id, MIDI2_CI_PI_MIDI_REPORT_END, version, - src_muid, dst_muid); -} - -/* == midi2_dispatch ====================================================== */ - - -/* - * midi2_dispatch.h - UMP typed dispatch (42 callbacks) - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - - - -/*--------------------------------------------------------------------+ - * midi2_dispatch -- Typed UMP message dispatch - * - * Parses raw UMP words and calls granular, semantically-named callbacks - * for every message type defined in M2-104-UM v1.1.2. - * - * Usage: - * midi2_dispatch dp; - * midi2_dispatch_init(&dp); - * dp.on_note_on = my_note_on_handler; - * dp.context = my_app; - * - * // In your receive loop (or as midi2_proc on_ump callback): - * midi2_dispatch_feed(&dp, words, word_count); - * - * Any callback left NULL is silently skipped (zero overhead beyond - * the NULL check). The dispatch struct is caller-allocated. - *--------------------------------------------------------------------*/ - -/*--------------------------------------------------------------------+ - * MT 0x0: Utility - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_noop_cb)(void *context); -typedef void (*midi2_dp_jr_clock_cb)(uint8_t group, uint16_t timestamp, void *context); -typedef void (*midi2_dp_jr_timestamp_cb)(uint8_t group, uint16_t timestamp, void *context); -typedef void (*midi2_dp_dctpq_cb)(uint16_t tpq, void *context); -typedef void (*midi2_dp_dc_cb)(uint32_t ticks, void *context); - -/*--------------------------------------------------------------------+ - * MT 0x1: System Common & System Real Time - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_system_cb)(uint8_t group, uint8_t status, - uint8_t data1, uint8_t data2, void *context); - -/*--------------------------------------------------------------------+ - * MT 0x2: MIDI 1.0 Channel Voice - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_cv1_note_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint8_t velocity, void *context); -typedef void (*midi2_dp_cv1_cc_cb)(uint8_t group, uint8_t channel, - uint8_t index, uint8_t value, void *context); -typedef void (*midi2_dp_cv1_program_cb)(uint8_t group, uint8_t channel, - uint8_t program, void *context); -typedef void (*midi2_dp_cv1_pressure_cb)(uint8_t group, uint8_t channel, - uint8_t value, void *context); -typedef void (*midi2_dp_cv1_pitch_bend_cb)(uint8_t group, uint8_t channel, - uint16_t value, void *context); -typedef void (*midi2_dp_cv1_poly_pressure_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint8_t value, void *context); - -/*--------------------------------------------------------------------+ - * MT 0x4: MIDI 2.0 Channel Voice - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_note_on_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint16_t velocity, - uint8_t attr_type, uint16_t attr_data, void *context); -typedef void (*midi2_dp_note_off_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint16_t velocity, - uint8_t attr_type, uint16_t attr_data, void *context); -typedef void (*midi2_dp_poly_pressure_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint32_t value, void *context); -typedef void (*midi2_dp_cc_cb)(uint8_t group, uint8_t channel, - uint8_t index, uint32_t value, void *context); -typedef void (*midi2_dp_program_cb)(uint8_t group, uint8_t channel, - uint8_t program, bool bank_valid, - uint8_t bank_msb, uint8_t bank_lsb, void *context); -typedef void (*midi2_dp_chan_pressure_cb)(uint8_t group, uint8_t channel, - uint32_t value, void *context); -typedef void (*midi2_dp_pitch_bend_cb)(uint8_t group, uint8_t channel, - uint32_t value, void *context); -typedef void (*midi2_dp_per_note_pb_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint32_t value, void *context); -typedef void (*midi2_dp_per_note_ctrl_cb)(uint8_t group, uint8_t channel, - uint8_t note, uint8_t index, - uint32_t value, void *context); -typedef void (*midi2_dp_rpn_cb)(uint8_t group, uint8_t channel, - uint8_t bank, uint8_t index, - uint32_t value, void *context); -typedef void (*midi2_dp_per_note_mgmt_cb)(uint8_t group, uint8_t channel, - uint8_t note, bool detach, bool reset, - void *context); - -/*--------------------------------------------------------------------+ - * MT 0x3: SysEx7 (raw packet -- reassembly is in midi2_proc) - * MT 0x5: SysEx8 / Mixed Data Set - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_sysex7_cb)(uint8_t group, uint8_t status, - const uint8_t *data, uint8_t len, void *context); -typedef void (*midi2_dp_sysex8_cb)(uint8_t group, uint8_t status, - uint8_t stream_id, - const uint8_t *data, uint8_t len, void *context); -typedef void (*midi2_dp_mds_header_cb)(uint8_t group, uint8_t mds_id, - uint16_t num_bytes, uint16_t num_chunks, - uint16_t this_chunk, uint16_t mfr_id, - uint16_t device_id, uint16_t sub_id1, - uint16_t sub_id2, void *context); -/** MDS Payload callback. Always delivers 14 bytes per UMP packet. - * The actual valid byte count for the chunk is in the MDS Header's num_bytes field; - * the caller must track header state to know when payload ends. */ -typedef void (*midi2_dp_mds_payload_cb)(uint8_t group, uint8_t mds_id, - const uint8_t *data, uint8_t len, void *context); - -/*--------------------------------------------------------------------+ - * MT 0xD: Flex Data - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_tempo_cb)(uint8_t group, uint32_t ten_ns_per_qn, void *context); -typedef void (*midi2_dp_time_sig_cb)(uint8_t group, uint8_t numerator, - uint8_t denominator, uint8_t num_32nd_notes, - void *context); -typedef void (*midi2_dp_metronome_cb)(uint8_t group, uint8_t primary_clicks, - uint8_t accent_1, uint8_t accent_2, uint8_t accent_3, - uint8_t subdiv_1, uint8_t subdiv_2, void *context); -typedef void (*midi2_dp_key_sig_cb)(uint8_t group, uint8_t address, uint8_t channel, - int8_t sharps_flats, uint8_t tonic, uint8_t key_type, - void *context); -typedef void (*midi2_dp_chord_cb)(uint8_t group, uint8_t address, uint8_t channel, - int8_t tonic_sf, uint8_t tonic_note, uint8_t chord_type, - uint8_t alt1_type, uint8_t alt1_deg, - uint8_t alt2_type, uint8_t alt2_deg, - uint8_t alt3_type, uint8_t alt3_deg, - uint8_t alt4_type, uint8_t alt4_deg, - int8_t bass_sf, uint8_t bass_note, uint8_t bass_type, - uint8_t bass_alt1_type, uint8_t bass_alt1_deg, - uint8_t bass_alt2_type, uint8_t bass_alt2_deg, - void *context); -typedef void (*midi2_dp_flex_text_cb)(uint8_t group, uint8_t format, - uint8_t address, uint8_t channel, - uint8_t bank, uint8_t status, - const uint8_t *text, uint8_t len, void *context); - -/*--------------------------------------------------------------------+ - * MT 0xF: UMP Stream - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_endpoint_discovery_cb)(uint8_t ump_ver_major, uint8_t ump_ver_minor, - uint8_t filter, void *context); -typedef void (*midi2_dp_endpoint_info_cb)(uint8_t ump_ver_major, uint8_t ump_ver_minor, - bool static_fb, uint8_t num_fb, - bool midi2_proto, bool midi1_proto, - bool rx_jr, bool tx_jr, void *context); -typedef void (*midi2_dp_device_identity_cb)(uint32_t manufacturer_id, - uint16_t family_id, uint16_t model_id, - uint32_t version_id, void *context); -typedef void (*midi2_dp_stream_text_cb)(uint16_t status, uint8_t format, - const uint8_t *data, uint8_t len, void *context); -typedef void (*midi2_dp_fb_name_cb)(uint8_t format, uint8_t fb_num, - const uint8_t *name, uint8_t len, void *context); -typedef void (*midi2_dp_config_cb)(uint8_t protocol, bool rx_jr, bool tx_jr, void *context); -typedef void (*midi2_dp_fb_discovery_cb)(uint8_t fb_num, uint8_t filter, void *context); -typedef void (*midi2_dp_fb_info_cb)(bool active, uint8_t fb_num, uint8_t direction, - uint8_t first_group, uint8_t num_groups, - uint8_t midi_ci_ver, uint8_t max_sysex8_streams, - uint8_t protocol, void *context); -typedef void (*midi2_dp_clip_cb)(bool start, void *context); - -/*--------------------------------------------------------------------+ - * Fallback - *--------------------------------------------------------------------*/ -typedef void (*midi2_dp_unknown_cb)(const uint32_t *words, uint8_t word_count, void *context); - -/*--------------------------------------------------------------------+ - * Dispatch State - *--------------------------------------------------------------------*/ -typedef struct { - void *context; /**< User pointer passed to all callbacks */ - - /** When true, incoming MT 0x2 (MIDI 1.0 CV) messages are translated - * to MT 0x4 (MIDI 2.0 CV) with proper value scaling and dispatched - * through the on_note_on/on_cc/etc. callbacks. The on_cv1_* callbacks - * are NOT called when upscale is active. - * When false (default), MT 0x2 goes to on_cv1_* as before. */ - bool upscale_mt2; - - /* MT 0x0: Utility */ - midi2_dp_noop_cb on_noop; - midi2_dp_jr_clock_cb on_jr_clock; - midi2_dp_jr_timestamp_cb on_jr_timestamp; - midi2_dp_dctpq_cb on_dctpq; - midi2_dp_dc_cb on_dc; - - /* MT 0x1: System */ - midi2_dp_system_cb on_system; - - /* MT 0x2: MIDI 1.0 Channel Voice */ - midi2_dp_cv1_note_cb on_cv1_note_on; - midi2_dp_cv1_note_cb on_cv1_note_off; - midi2_dp_cv1_poly_pressure_cb on_cv1_poly_pressure; - midi2_dp_cv1_cc_cb on_cv1_cc; - midi2_dp_cv1_program_cb on_cv1_program; - midi2_dp_cv1_pressure_cb on_cv1_chan_pressure; - midi2_dp_cv1_pitch_bend_cb on_cv1_pitch_bend; - - /* MT 0x3: SysEx7 (per-packet; use midi2_proc for reassembly) */ - midi2_dp_sysex7_cb on_sysex7; - - /* MT 0x4: MIDI 2.0 Channel Voice */ - midi2_dp_note_on_cb on_note_on; - midi2_dp_note_off_cb on_note_off; - midi2_dp_poly_pressure_cb on_poly_pressure; - midi2_dp_cc_cb on_cc; - midi2_dp_program_cb on_program; - midi2_dp_chan_pressure_cb on_chan_pressure; - midi2_dp_pitch_bend_cb on_pitch_bend; - midi2_dp_per_note_pb_cb on_per_note_pb; - midi2_dp_per_note_ctrl_cb on_reg_per_note; - midi2_dp_per_note_ctrl_cb on_asn_per_note; - midi2_dp_rpn_cb on_rpn; - midi2_dp_rpn_cb on_nrpn; - midi2_dp_rpn_cb on_rel_rpn; - midi2_dp_rpn_cb on_rel_nrpn; - midi2_dp_per_note_mgmt_cb on_per_note_mgmt; - - /* MT 0x5: SysEx8 / Mixed Data Set */ - midi2_dp_sysex8_cb on_sysex8; - midi2_dp_mds_header_cb on_mds_header; - midi2_dp_mds_payload_cb on_mds_payload; - - /* MT 0xD: Flex Data */ - midi2_dp_tempo_cb on_tempo; - midi2_dp_time_sig_cb on_time_sig; - midi2_dp_metronome_cb on_metronome; - midi2_dp_key_sig_cb on_key_sig; - midi2_dp_chord_cb on_chord; - midi2_dp_flex_text_cb on_flex_text; - - /* MT 0xF: UMP Stream */ - midi2_dp_endpoint_discovery_cb on_endpoint_discovery; - midi2_dp_endpoint_info_cb on_endpoint_info; - midi2_dp_device_identity_cb on_device_identity; - midi2_dp_stream_text_cb on_stream_text; /* endpoint name, product instance id */ - midi2_dp_fb_name_cb on_fb_name; /* function block name (separate: has fb_num) */ - midi2_dp_config_cb on_config_request; - midi2_dp_config_cb on_config_notify; - midi2_dp_fb_discovery_cb on_fb_discovery; - midi2_dp_fb_info_cb on_fb_info; - midi2_dp_clip_cb on_clip; - - /* Fallback for unknown/future MTs */ - midi2_dp_unknown_cb on_unknown; -} midi2_dispatch; - -/*--------------------------------------------------------------------+ - * Functions - *--------------------------------------------------------------------*/ - -/** Initialize dispatch, zeroing all callbacks. */ -void midi2_dispatch_init(midi2_dispatch *dp); - -/** Feed one UMP message. Parses and dispatches to the appropriate callback. - * word_count must match the message size (1, 2, or 4 words). - * Can be used directly as midi2_proc on_ump callback. */ -void midi2_dispatch_feed(const uint32_t *words, uint8_t word_count, void *context); - -/* == midi2_proc ========================================================== */ - - -/* - * midi2_proc.h - UMP stream processing, group filtering, value scaling - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - - - -/*--------------------------------------------------------------------+ - * Callback types - *--------------------------------------------------------------------*/ - -/* Called for each UMP message that passes the group filter */ -typedef void (*midi2_proc_ump_cb)(const uint32_t *words, uint8_t word_count, void *context); - -/* Called when a complete SysEx7 message has been reassembled */ -typedef void (*midi2_proc_sysex7_cb)(uint8_t group, const uint8_t *data, - uint16_t length, void *context); - -/* Called when a complete SysEx8 message has been reassembled */ -typedef void (*midi2_proc_sysex8_cb)(uint8_t group, uint8_t stream_id, - const uint8_t *data, uint16_t length, void *context); - -/* Called when midi2_proc needs to send UMP words (e.g. SysEx7 fragmented send) */ -typedef uint32_t (*midi2_proc_write_fn)(const uint32_t *words, uint32_t count, void *context); - -/*--------------------------------------------------------------------+ - * State struct (user-allocated) - * - * The caller provides the SysEx reassembly buffer. This allows any - * buffer size without compile-time limits. Example: - * - * uint8_t sysex_buf[256]; - * midi2_proc_state proc; - * midi2_proc_init(&proc, sysex_buf, sizeof(sysex_buf)); - *--------------------------------------------------------------------*/ - -typedef struct { - /** Group filtering: bitmask of which groups to deliver (default 0xFFFF = all) */ - uint16_t group_mask; - - /** Group remap table for outgoing messages (default identity) */ - uint8_t group_map[16]; - - /** SysEx7 reassembly: caller-provided buffer */ - uint8_t *sysex_buf; /**< pointer to caller's buffer, or NULL to disable */ - uint16_t sysex_buf_size; /**< capacity of sysex_buf */ - uint16_t sysex_len; /**< current accumulated length */ - uint8_t sysex_group; /**< 0xFF = no active SysEx */ - - /** SysEx8 reassembly: caller-provided buffer (separate from SysEx7) */ - uint8_t *sysex8_buf; - uint16_t sysex8_buf_size; - uint16_t sysex8_len; - uint8_t sysex8_group; /**< 0xFF = no active SysEx8 */ - uint8_t sysex8_stream_id; /**< stream ID of active SysEx8 */ - - /** Callbacks */ - midi2_proc_ump_cb on_ump; - midi2_proc_sysex7_cb on_sysex7; - midi2_proc_sysex8_cb on_sysex8; - void *context; -} midi2_proc_state; - -/*--------------------------------------------------------------------+ - * Functions - *--------------------------------------------------------------------*/ - -/** Initialize state with caller-provided SysEx buffers. - * @param state State struct (caller-allocated) - * @param sysex7_buf Buffer for SysEx7 reassembly, or NULL to disable - * @param sysex7_buf_size Size of sysex7_buf in bytes - * @param sysex8_buf Buffer for SysEx8 reassembly, or NULL to disable - * @param sysex8_buf_size Size of sysex8_buf in bytes */ -void midi2_proc_init(midi2_proc_state *state, - uint8_t *sysex7_buf, uint16_t sysex7_buf_size, - uint8_t *sysex8_buf, uint16_t sysex8_buf_size); - -/* Feed UMP words from transport. Processes, filters, dispatches to callbacks. - * word_count must match the message size (1, 2, or 4 words). */ -void midi2_proc_feed(midi2_proc_state *state, const uint32_t *words, uint8_t word_count); - -/* Apply group remap to outgoing words (modifies word 0 in-place). - * Only remaps message types that have a group field (not Utility or Stream). */ -void midi2_proc_remap_group(midi2_proc_state *state, uint32_t *words); - -/* Multi-packet SysEx7 send helper. Fragments data into UMP packets (max 6 bytes each), - * calls write_fn for each 2-word packet. data does NOT include F0/F7 delimiters. */ -void midi2_proc_send_sysex7(uint8_t group, const uint8_t *data, uint16_t length, - midi2_proc_write_fn write_fn, void *context); - -/* M2-104-UM §7.1.9 Function Block Name Notification sender. - * UMP Stream MT 0xF, status 0x12. Fragments the UTF-8 name across - * Complete / Start / Continue / End 4-word packets (13 name bytes per - * UMP; total name limited to 91 bytes per spec). Remaining bytes of - * the final packet are zero-padded per spec. (v0.2.4+) */ -void midi2_proc_send_fb_name(uint8_t fb_idx, const char *name, - midi2_proc_write_fn write_fn, void *context); - -/* M2-104-UM §7.1.7 Endpoint Name Notification sender. - * UMP Stream MT 0xF, status 0x003. Fragments UTF-8 name across - * Complete / Start / Continue / End 4-word packets (14 name bytes - * per UMP). Empty name sends nothing. (v0.3.0+) */ -void midi2_proc_send_endpoint_name(const char *name, - midi2_proc_write_fn write_fn, void *context); - -/* M2-104-UM §7.1.8 Product Instance ID Notification sender. - * UMP Stream MT 0xF, status 0x004. Fragmentation identical to - * Endpoint Name (14 bytes per UMP). Empty id sends nothing. - * (v0.3.0+) */ -void midi2_proc_send_product_id(const char *id, - midi2_proc_write_fn write_fn, void *context); - -/* M2-104-UM §7.1.6 Device Identity Notification sender. - * UMP Stream MT 0xF, status 0x002. Always emits a single 4-word UMP - * (no fragmentation). Kept for callsite symmetry with the other - * Stream senders. manufacturer_id uses lower 24 bits only. - * (v0.3.0+) */ -void midi2_proc_send_device_identity(uint32_t manufacturer_id, - uint16_t family_id, uint16_t model_id, - uint32_t version_id, - midi2_proc_write_fn write_fn, void *context); - -/* M2-104-UM §7.8 SysEx8 sender. MT 0x5. Fragments raw 8-bit data - * into 4-word packets (13 data bytes per UMP; stream_id rides in - * word 0 bits [15:8]). status nibble encodes Complete / Start / - * Continue / End per M2-104-UM Table 14. Zero-length sends nothing. - * (v0.3.0+) */ -void midi2_proc_send_sysex8(uint8_t group, uint8_t stream_id, - const uint8_t *data, uint16_t length, - midi2_proc_write_fn write_fn, void *context); - -/* == midi2_conv ========================================================== */ - - -/* - * midi2_conv.h - MIDI 1.0 byte stream to UMP, protocol translation - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - - - -/*--------------------------------------------------------------------+ - * MIDI 1.0 Byte Stream to UMP Converter - * - * Converts serial MIDI 1.0 bytes (DIN-5, TRS, UART) into UMP words. - * Handles Running Status, multi-byte messages, and SysEx (F0..F7). - * - * SysEx is emitted as streaming UMP SysEx7 packets: - * - Every 6 bytes: emits START or CONTINUE (2 UMP words) - * - On F7: emits END or COMPLETE with remaining bytes - * - No caller-provided buffer needed (6-byte internal buffer) - * - * Usage: - * midi2_conv_state conv; - * midi2_conv_init(&conv, 0); // group 0 - * - * // For each incoming byte: - * if (midi2_conv_feed(&conv, byte)) { - * // conv.ump[] contains the completed UMP message - * // conv.ump_words tells how many words (1 or 2) - * process(conv.ump, conv.ump_words); - * } - *--------------------------------------------------------------------*/ - -typedef struct { - /** Configuration */ - uint8_t group; - - /** Running Status state */ - uint8_t running_status; - uint8_t data_byte_count; - uint8_t data_pos; - uint8_t data[2]; - - /** SysEx state: 6-byte internal buffer for streaming */ - uint8_t sysex_buf[6]; /**< internal buffer (one UMP packet worth) */ - uint8_t sysex_len; /**< bytes accumulated in sysex_buf (0-6) */ - bool in_sysex; /**< currently inside F0..F7 */ - bool sysex_started; /**< true after START emitted */ - - /** Output: completed UMP message */ - uint32_t ump[4]; - uint8_t ump_words; -} midi2_conv_state; - -/** Initialize converter state. - * @param state State struct (caller-allocated) - * @param group UMP group to assign to converted messages */ -void midi2_conv_init(midi2_conv_state *state, uint8_t group); - -/* Feed one MIDI 1.0 byte. Returns true when a complete UMP message is ready - * in state->ump[]. Returns false if more bytes are needed. - * - * SysEx of any length is fully supported via streaming UMP SysEx7 packets. - * Each call produces at most one UMP message (1 or 2 words). */ -bool midi2_conv_feed(midi2_conv_state *state, uint8_t byte); - -/* == midi2_ci_dispatch =================================================== */ - - -/* - * midi2_ci_dispatch.h - MIDI-CI typed dispatch (33 callbacks) - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI-CI (M2-101-UM v1.2, Jun 2023) - * Version: 0.3.0 - */ - - - - -/*====================================================================+ - * midi2_ci_dispatch -- Typed MIDI-CI message dispatch - * - * Parses reassembled SysEx payloads and calls granular, semantically- - * named callbacks for every MIDI-CI message type in M2-101-UM v1.2. - * - * Usage: - * midi2_ci_dispatch dp; - * midi2_ci_dispatch_init(&dp); - * dp.on_discovery = my_discovery_handler; - * dp.context = my_app; - * - * // When a complete CI SysEx arrives (from midi2_proc on_sysex7): - * midi2_ci_dispatch_feed(&dp, group, sysex_data, sysex_len); - * - * All callbacks receive the common CI header fields (version, src_muid, - * dst_muid, device_id) plus message-specific fields pre-parsed. - * NULL callbacks are silently skipped. - *====================================================================*/ - -/*--------------------------------------------------------------------+ - * Common header passed to all callbacks - *--------------------------------------------------------------------*/ -typedef struct { - uint8_t device_id; - uint8_t version; - uint32_t src_muid; - uint32_t dst_muid; - uint8_t group; /**< UMP group the SysEx arrived on */ -} midi2_ci_header; - -/*--------------------------------------------------------------------+ - * Callback types: Management (0x70-0x7F) - *--------------------------------------------------------------------*/ -typedef void (*midi2_ci_dp_discovery_cb)( - midi2_ci_header hdr, uint32_t mfr_id, uint16_t family, uint16_t model, - uint32_t sw_rev, uint8_t ci_category, uint32_t max_sysex, - uint8_t output_path_id, void *context); - -typedef void (*midi2_ci_dp_discovery_reply_cb)( - midi2_ci_header hdr, uint32_t mfr_id, uint16_t family, uint16_t model, - uint32_t sw_rev, uint8_t ci_category, uint32_t max_sysex, - uint8_t output_path_id, uint8_t function_block, void *context); - -typedef void (*midi2_ci_dp_endpoint_info_cb)( - midi2_ci_header hdr, uint8_t status, void *context); - -typedef void (*midi2_ci_dp_endpoint_info_reply_cb)( - midi2_ci_header hdr, uint8_t status, - const uint8_t *info_data, uint16_t info_len, void *context); - -typedef void (*midi2_ci_dp_invalidate_muid_cb)( - midi2_ci_header hdr, uint32_t target_muid, void *context); - -typedef void (*midi2_ci_dp_ack_cb)( - midi2_ci_header hdr, uint8_t orig_sub_id, - uint8_t status_code, uint8_t status_data, - const uint8_t *details, uint16_t msg_len, const uint8_t *msg_text, - void *context); - -typedef void (*midi2_ci_dp_nak_cb)( - midi2_ci_header hdr, uint8_t orig_sub_id, - uint8_t status_code, uint8_t status_data, - const uint8_t *details, uint16_t msg_len, const uint8_t *msg_text, - void *context); - -/*--------------------------------------------------------------------+ - * Callback types: Profile Configuration (0x20-0x2F) - *--------------------------------------------------------------------*/ -typedef void (*midi2_ci_dp_profile_inquiry_cb)( - midi2_ci_header hdr, void *context); - -typedef void (*midi2_ci_dp_profile_inquiry_reply_cb)( - midi2_ci_header hdr, - uint16_t enabled_count, const uint8_t *enabled_data, - uint16_t disabled_count, const uint8_t *disabled_data, - void *context); - -typedef void (*midi2_ci_dp_set_profile_cb)( - midi2_ci_header hdr, const uint8_t *profile_id, - uint16_t num_channels, void *context); - -typedef void (*midi2_ci_dp_profile_report_cb)( - midi2_ci_header hdr, const uint8_t *profile_id, - uint16_t num_channels, void *context); - -typedef void (*midi2_ci_dp_profile_added_removed_cb)( - midi2_ci_header hdr, const uint8_t *profile_id, void *context); - -typedef void (*midi2_ci_dp_profile_details_cb)( - midi2_ci_header hdr, const uint8_t *profile_id, - uint8_t inquiry_target, void *context); - -typedef void (*midi2_ci_dp_profile_details_reply_cb)( - midi2_ci_header hdr, const uint8_t *profile_id, - uint8_t inquiry_target, const uint8_t *data, uint16_t data_len, - void *context); - -typedef void (*midi2_ci_dp_profile_specific_cb)( - midi2_ci_header hdr, const uint8_t *profile_id, - const uint8_t *data, uint32_t data_len, void *context); - -/*--------------------------------------------------------------------+ - * Callback types: Property Exchange (0x30-0x3F) - *--------------------------------------------------------------------*/ -typedef void (*midi2_ci_dp_pe_caps_cb)( - midi2_ci_header hdr, uint8_t max_simultaneous, - uint8_t pe_ver_major, uint8_t pe_ver_minor, void *context); - -typedef void (*midi2_ci_dp_pe_data_cb)( - midi2_ci_header hdr, uint8_t request_id, - const uint8_t *header_data, uint16_t header_len, - uint16_t num_chunks, uint16_t this_chunk, - const uint8_t *body_data, uint16_t body_len, void *context); - -/*--------------------------------------------------------------------+ - * Callback types: Process Inquiry (0x40-0x4F) - *--------------------------------------------------------------------*/ -typedef void (*midi2_ci_dp_pi_caps_cb)( - midi2_ci_header hdr, void *context); - -typedef void (*midi2_ci_dp_pi_caps_reply_cb)( - midi2_ci_header hdr, uint8_t supported_features, void *context); - -typedef void (*midi2_ci_dp_pi_midi_report_cb)( - midi2_ci_header hdr, uint8_t msg_data_control, - uint8_t system_bitmap, uint8_t channel_ctrl_bitmap, - uint8_t note_data_bitmap, void *context); - -typedef void (*midi2_ci_dp_pi_midi_report_reply_cb)( - midi2_ci_header hdr, uint8_t system_bitmap, - uint8_t channel_ctrl_bitmap, uint8_t note_data_bitmap, - void *context); - -typedef void (*midi2_ci_dp_pi_end_cb)( - midi2_ci_header hdr, void *context); - -/*--------------------------------------------------------------------+ - * Fallback - *--------------------------------------------------------------------*/ -typedef void (*midi2_ci_dp_unknown_cb)( - midi2_ci_header hdr, uint8_t sub_id, - const uint8_t *data, uint16_t length, void *context); - -/*--------------------------------------------------------------------+ - * Dispatch state - *--------------------------------------------------------------------*/ -typedef struct { - void *context; - - /* Management */ - midi2_ci_dp_discovery_cb on_discovery; - midi2_ci_dp_discovery_reply_cb on_discovery_reply; - midi2_ci_dp_endpoint_info_cb on_endpoint_info; - midi2_ci_dp_endpoint_info_reply_cb on_endpoint_info_reply; - midi2_ci_dp_invalidate_muid_cb on_invalidate_muid; - midi2_ci_dp_ack_cb on_ack; - midi2_ci_dp_nak_cb on_nak; - - /* Profile Configuration */ - midi2_ci_dp_profile_inquiry_cb on_profile_inquiry; - midi2_ci_dp_profile_inquiry_reply_cb on_profile_inquiry_reply; - midi2_ci_dp_set_profile_cb on_set_profile_on; - midi2_ci_dp_set_profile_cb on_set_profile_off; - midi2_ci_dp_profile_report_cb on_profile_enabled; - midi2_ci_dp_profile_report_cb on_profile_disabled; - midi2_ci_dp_profile_added_removed_cb on_profile_added; - midi2_ci_dp_profile_added_removed_cb on_profile_removed; - midi2_ci_dp_profile_details_cb on_profile_details; - midi2_ci_dp_profile_details_reply_cb on_profile_details_reply; - midi2_ci_dp_profile_specific_cb on_profile_specific_data; - - /* Property Exchange */ - midi2_ci_dp_pe_caps_cb on_pe_capability; - midi2_ci_dp_pe_caps_cb on_pe_capability_reply; - midi2_ci_dp_pe_data_cb on_pe_get; - midi2_ci_dp_pe_data_cb on_pe_get_reply; - midi2_ci_dp_pe_data_cb on_pe_set; - midi2_ci_dp_pe_data_cb on_pe_set_reply; - midi2_ci_dp_pe_data_cb on_pe_subscribe; - midi2_ci_dp_pe_data_cb on_pe_subscribe_reply; - midi2_ci_dp_pe_data_cb on_pe_notify; - - /* Process Inquiry */ - midi2_ci_dp_pi_caps_cb on_pi_capability; - midi2_ci_dp_pi_caps_reply_cb on_pi_capability_reply; - midi2_ci_dp_pi_midi_report_cb on_pi_midi_report; - midi2_ci_dp_pi_midi_report_reply_cb on_pi_midi_report_reply; - midi2_ci_dp_pi_end_cb on_pi_midi_report_end; - - /* Fallback */ - midi2_ci_dp_unknown_cb on_unknown; -} midi2_ci_dispatch; - -/*--------------------------------------------------------------------+ - * Functions - *--------------------------------------------------------------------*/ - -/** Initialize dispatch, zeroing all callbacks. */ -void midi2_ci_dispatch_init(midi2_ci_dispatch *dp); - -/** Feed a reassembled SysEx payload (without F0/F7). - * Parses the CI header, dispatches to the appropriate callback. - * Returns true if the message was recognized as MIDI-CI, false otherwise. - * @param group UMP group the SysEx arrived on */ -bool midi2_ci_dispatch_feed(midi2_ci_dispatch *dp, uint8_t group, - const uint8_t *data, uint16_t length); - -/* == midi2_ci ============================================================ */ - - -/* - * midi2_ci.h - MIDI-CI convenience responder - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI-CI (M2-101-UM v1.2, Jun 2023) - * Version: 0.3.0 - */ - - - - -/*--------------------------------------------------------------------+ - * Error codes - *--------------------------------------------------------------------*/ -enum { - MIDI2_CI_OK = 0, - MIDI2_CI_ERR_FULL = -1, /**< Storage is full (profiles or properties) */ - MIDI2_CI_ERR_NOT_FOUND = -2, /**< Item not found */ - MIDI2_CI_ERR_NULL = -3, /**< NULL pointer argument */ -}; - -/* Sub-IDs and constants are now in midi2_ci_msg.h. - * Legacy alias for backward compatibility: */ -#define MIDI2_CI_DISCOVERY_REQUEST MIDI2_CI_DISCOVERY - -/*--------------------------------------------------------------------+ - * Callback types - *--------------------------------------------------------------------*/ - -/* Property Exchange getter: returns value string for given property name */ -typedef const char* (*midi2_ci_pe_getter)(const char *name, void *context); - -/* Property Exchange setter: sets value, returns true on success */ -typedef bool (*midi2_ci_pe_setter)(const char *name, const char *value, void *context); - -/*--------------------------------------------------------------------+ - * Property descriptor (used in caller-provided array) - *--------------------------------------------------------------------*/ -typedef struct { - const char *name; - const char *static_value; /**< used if getter is NULL */ - midi2_ci_pe_getter getter; - midi2_ci_pe_setter setter; - bool subscribable; /**< v0.3.0+: eligible for PE Subscribe */ -} midi2_ci_property; - -/*--------------------------------------------------------------------+ - * Subscriber registry entry (caller-provided array, v0.3.0+) - * - * name_copy holds up to 36 chars per M2-105 PE resource name limit - * plus NUL terminator, so the responder keeps a stable reference even - * if the app frees the resource name string passed to subscribe_add. - *--------------------------------------------------------------------*/ -typedef struct { - uint32_t caller_muid; - char name_copy[37]; - uint8_t in_use; /**< 0 = free slot; non-zero = active */ -} midi2_ci_subscriber; - -/*--------------------------------------------------------------------+ - * State struct (user-allocated) - * - * The caller provides storage for profiles and properties by pointing - * to pre-allocated arrays. This allows any capacity without compile-time - * limits. Example: - * - * uint8_t my_profiles[4][5]; - * midi2_ci_property my_props[2]; - * midi2_ci_state ci; - * midi2_ci_init(&ci, seed, my_profiles, 4, my_props, 2); - *--------------------------------------------------------------------*/ -/*--------------------------------------------------------------------+ - * RNG callback (v0.2.4+) - * - * Platform-specific randomness source. When set via midi2_ci_set_rng(), - * the convenience responder automatically handles MUID regeneration on - * Invalidate MUID and peer MUID collisions. Only the lower 28 bits of - * the returned value are used. Reserved values 0x00000000 and - * 0x0FFFFFFF (broadcast) are automatically avoided by midi2_ci_new_muid. - *--------------------------------------------------------------------*/ -typedef uint32_t (*midi2_ci_rng_fn)(void *context); - -typedef struct { - /* Device identity (configured by user) */ - uint32_t manufacturer_id; /**< 3-byte SysEx Manufacturer ID in lower 24 bits */ - uint16_t family_id; - uint16_t model_id; - uint32_t version_id; - - /* MUID (set at init) */ - uint32_t muid; - - /* Profiles: caller-provided storage */ - uint8_t (*profiles)[5]; /**< pointer to caller's profile array (each 5 bytes) */ - uint8_t profile_capacity; /**< max profiles (size of caller's array) */ - uint8_t profile_count; /**< current count */ - - /* Properties: caller-provided storage */ - midi2_ci_property *properties; /**< pointer to caller's property array */ - uint8_t property_capacity; - uint8_t property_count; - - /* Write function (how to send SysEx responses back) */ - midi2_proc_write_fn write_fn; - void *write_context; - - /* User context for PE callbacks */ - void *context; - - /* RNG for MUID regeneration (v0.2.4+). NULL = no auto-regen. */ - midi2_ci_rng_fn rng; - void *rng_context; - - /* When true, process_sysex replies with a NAK (Sub-ID#2 0x7F) for any - * CI sub-id not handled by the convenience responder (v0.2.4+). - * Default false to preserve v0.2.3 behavior. */ - bool nak_on_unknown; - - /* When true, the convenience responder broadcasts an Invalidate MUID - * frame for the old MUID whenever it regenerates because an inbound - * src_muid collided with ours. M2-101-UM Appendix E 2. Default: true - * (v0.3.0+). Implementation was already present in v0.2.4 but always - * on; this flag gates it. */ - bool auto_invalidate_on_collision; - - /* Subscribers: caller-provided storage (v0.3.0+). NULL when the state - * was built with the legacy midi2_ci_init (no subscribe/notify). */ - midi2_ci_subscriber *subscribers; - uint8_t subscriber_capacity; - uint8_t subscriber_count; -} midi2_ci_state; - -/*--------------------------------------------------------------------+ - * Functions - *--------------------------------------------------------------------*/ - -/** Initialize state with caller-provided storage. - * Delegates to midi2_ci_init_ex(..., NULL, 0), so the subscriber - * registry is absent and subscribe/notify APIs return ERR_FULL. - * @param state State struct (caller-allocated) - * @param muid_seed Random or unique value for MUID generation (28-bit) - * @param profiles Caller's profile array, or NULL if no profiles needed - * @param max_profiles Capacity of profiles array - * @param properties Caller's property array, or NULL if no properties needed - * @param max_properties Capacity of properties array */ -void midi2_ci_init(midi2_ci_state *state, uint32_t muid_seed, - uint8_t (*profiles)[5], uint8_t max_profiles, - midi2_ci_property *properties, uint8_t max_properties); - -/** Extended initializer that also wires a subscriber-registry array - * for PE Subscribe / Notify. Pass NULL / 0 for the subscribers - * argument to match midi2_ci_init semantics. - * (v0.3.0+) */ -void midi2_ci_init_ex(midi2_ci_state *state, uint32_t muid_seed, - uint8_t (*profiles)[5], uint8_t max_profiles, - midi2_ci_property *properties, uint8_t max_properties, - midi2_ci_subscriber *subscribers, uint8_t max_subscribers); - -/** Configure device identity */ -void midi2_ci_set_identity(midi2_ci_state *state, - uint32_t manufacturer_id, uint16_t family_id, - uint16_t model_id, uint32_t version_id); - -/** Set the write function (how CI sends SysEx responses) */ -void midi2_ci_set_write_fn(midi2_ci_state *state, - midi2_proc_write_fn write_fn, void *context); - -/** Install a platform RNG so the convenience responder can regenerate the - * MUID on Invalidate MUID messages and on peer MUID collisions. Without - * this, both situations are silently ignored (v0.2.3 behavior). - * The callback is invoked from within process_sysex; it must be re-entrant - * and should return quickly. Only the lower 28 bits matter. (v0.2.4+) */ -void midi2_ci_set_rng(midi2_ci_state *state, - midi2_ci_rng_fn rng, void *context); - -/** Enable/disable automatic NAK (Sub-ID#2 0x7F, status 0x01 NOT_SUPPORTED) - * replies for CI sub-ids the convenience responder does not handle. - * M2-101-UM Appendix E requires a device to "Be able to send a NAK message - * when appropriate". Default: false (v0.2.3 compatible). (v0.2.4+) */ -void midi2_ci_set_nak_on_unknown(midi2_ci_state *state, bool enabled); - -/** Enable/disable automatic broadcast of an Invalidate MUID frame for the - * old MUID whenever the convenience responder regenerates due to an - * inbound collision. Default: true (v0.3.0+). */ -void midi2_ci_set_auto_invalidate_on_collision(midi2_ci_state *state, bool enabled); - -/** Generate a fresh 28-bit MUID using the configured RNG, avoiding the - * reserved values 0x00000000 and 0x0FFFFFFF (broadcast). If no RNG is - * set, falls back to perturbing the current MUID. Returns the new MUID - * and also stores it into state->muid. (v0.2.4+) */ -uint32_t midi2_ci_new_muid(midi2_ci_state *state); - -/** Add a profile. Returns MIDI2_CI_OK or MIDI2_CI_ERR_FULL. */ -int midi2_ci_add_profile(midi2_ci_state *state, const uint8_t profile_id[5]); - -/** Remove a profile. Returns MIDI2_CI_OK or MIDI2_CI_ERR_NOT_FOUND. */ -int midi2_ci_remove_profile(midi2_ci_state *state, const uint8_t profile_id[5]); - -/** Add a static property. Returns MIDI2_CI_OK or MIDI2_CI_ERR_FULL. */ -int midi2_ci_add_property_static(midi2_ci_state *state, - const char *name, const char *value); - -/** Add a dynamic property with getter/setter. Returns MIDI2_CI_OK or MIDI2_CI_ERR_FULL. */ -int midi2_ci_add_property_dynamic(midi2_ci_state *state, - const char *name, - midi2_ci_pe_getter getter, - midi2_ci_pe_setter setter); - -/** Remove a property by name. Remaining properties are shifted left to - * preserve contiguous storage. Returns MIDI2_CI_OK or - * MIDI2_CI_ERR_NOT_FOUND. Symmetric with midi2_ci_remove_profile. - * (v0.3.0+) */ -int midi2_ci_remove_property(midi2_ci_state *state, const char *name); - -/** Clear all registered profiles (count-only reset; storage contents are - * left intact for caller inspection or reuse). (v0.3.0+) */ -void midi2_ci_reset_profiles(midi2_ci_state *state); - -/** Clear all registered properties (count-only reset; storage contents - * are left intact for caller inspection or reuse). (v0.3.0+) */ -void midi2_ci_reset_properties(midi2_ci_state *state); - -/** Toggle the subscribable flag on a registered property at runtime. - * Returns MIDI2_CI_OK or MIDI2_CI_ERR_NOT_FOUND. (v0.3.0+) */ -int midi2_ci_pe_set_subscribable(midi2_ci_state *state, - const char *name, bool subscribable); - -/** Register a subscriber (caller_muid) for the named PE resource. The - * property must be registered and marked subscribable. Duplicate - * (muid, name) pairs are idempotent and return OK. - * @return MIDI2_CI_OK, MIDI2_CI_ERR_NOT_FOUND (property unknown or - * not subscribable), or MIDI2_CI_ERR_FULL (no subscriber - * capacity, including the case of midi2_ci_init without a - * subscribers array). (v0.3.0+) */ -int midi2_ci_subscribe_add(midi2_ci_state *state, uint32_t caller_muid, - const char *resource_name); - -/** Remove a subscriber from the named resource. - * @return MIDI2_CI_OK or MIDI2_CI_ERR_NOT_FOUND. (v0.3.0+) */ -int midi2_ci_subscribe_remove(midi2_ci_state *state, uint32_t caller_muid, - const char *resource_name); - -/** Fan out a PE Notify frame to every subscriber of the named resource. - * Returns MIDI2_CI_OK even when the subscriber list is empty, or - * MIDI2_CI_ERR_NOT_FOUND when the property is unknown. Emission uses - * the state's write_fn (same path as Discovery / PE Reply). - * (v0.3.0+) */ -int midi2_ci_notify_property_changed(midi2_ci_state *state, - const char *resource_name); - -/** Return the current number of active subscribers across all - * resources. (v0.3.0+) */ -uint8_t midi2_ci_get_subscriber_count(const midi2_ci_state *state); - -/** Process incoming SysEx that might be MIDI-CI. - * Returns true if the message was handled (CI), false if not. - * Automatically sends Discovery Reply, Profile Inquiry Reply, PE responses. - * - * LIMITATIONS (simplified convenience responder): - * - PE Get always returns the first property with a non-NULL value, - * regardless of which property was requested (no JSON header parsing). - * - PE Set calls the first setter with an empty value string. - * - All replies use MIDI-CI Message Version 1 (no v2 extended fields). - * - For full PE/Profile control, use midi2_ci_dispatch directly. - * - * @param group UMP group the SysEx arrived on (responses go to same group) - * @param data Reassembled SysEx content (no F0/F7) - * @param length Length of data */ -bool midi2_ci_process_sysex(midi2_ci_state *state, - uint8_t group, const uint8_t *data, uint16_t length); - -#ifdef MIDI2_IMPLEMENTATION - -/* == midi2_dispatch (impl) =============================================== */ - - -/* - * midi2_dispatch.c - UMP typed dispatch implementation - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - -/*--------------------------------------------------------------------+ - * Init - *--------------------------------------------------------------------*/ -void midi2_dispatch_init(midi2_dispatch *dp) { - memset(dp, 0, sizeof(*dp)); -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0x0 Utility (1 word) - *--------------------------------------------------------------------*/ -static void dispatch_utility(midi2_dispatch *dp, uint32_t w) { - uint8_t group = (uint8_t)((w >> 24) & 0x0F); - uint8_t status = (uint8_t)((w >> 20) & 0x0F); - - switch (status) { - case MIDI2_UTILITY_NOOP: - if (dp->on_noop) dp->on_noop(dp->context); - break; - case MIDI2_UTILITY_JR_CLOCK: - if (dp->on_jr_clock) dp->on_jr_clock(group, (uint16_t)(w & 0xFFFF), dp->context); - break; - case MIDI2_UTILITY_JR_TIMESTAMP: - if (dp->on_jr_timestamp) dp->on_jr_timestamp(group, (uint16_t)(w & 0xFFFF), dp->context); - break; - case MIDI2_UTILITY_DCTPQ: - if (dp->on_dctpq) dp->on_dctpq((uint16_t)(w & 0xFFFF), dp->context); - break; - case MIDI2_UTILITY_DC: - if (dp->on_dc) dp->on_dc(w & 0x000FFFFF, dp->context); - break; - default: - if (dp->on_unknown) dp->on_unknown(&w, 1, dp->context); - break; - } -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0x1 System (1 word) - *--------------------------------------------------------------------*/ -static void dispatch_system(midi2_dispatch *dp, uint32_t w) { - if (!dp->on_system) return; - uint8_t group = (uint8_t)((w >> 24) & 0x0F); - uint8_t status = (uint8_t)((w >> 16) & 0xFF); - uint8_t data1 = (uint8_t)((w >> 8) & 0xFF); - uint8_t data2 = (uint8_t)(w & 0xFF); - dp->on_system(group, status, data1, data2, dp->context); -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0x2 MIDI 1.0 Channel Voice (1 word) - *--------------------------------------------------------------------*/ -static void dispatch_cv1(midi2_dispatch *dp, uint32_t w) { - uint8_t group = (uint8_t)((w >> 24) & 0x0F); - uint8_t status = (uint8_t)((w >> 16) & 0xF0); - uint8_t channel = (uint8_t)((w >> 16) & 0x0F); - uint8_t data1 = (uint8_t)((w >> 8) & 0x7F); - uint8_t data2 = (uint8_t)(w & 0x7F); - - switch (status) { - case 0x80: - if (dp->on_cv1_note_off) dp->on_cv1_note_off(group, channel, data1, data2, dp->context); - break; - case 0x90: - if (dp->on_cv1_note_on) dp->on_cv1_note_on(group, channel, data1, data2, dp->context); - break; - case 0xA0: - if (dp->on_cv1_poly_pressure) dp->on_cv1_poly_pressure(group, channel, data1, data2, dp->context); - break; - case 0xB0: - if (dp->on_cv1_cc) dp->on_cv1_cc(group, channel, data1, data2, dp->context); - break; - case 0xC0: - if (dp->on_cv1_program) dp->on_cv1_program(group, channel, data1, dp->context); - break; - case 0xD0: - if (dp->on_cv1_chan_pressure) dp->on_cv1_chan_pressure(group, channel, data1, dp->context); - break; - case 0xE0: - if (dp->on_cv1_pitch_bend) { - uint16_t pb = (uint16_t)((data2 << 7) | data1); - dp->on_cv1_pitch_bend(group, channel, pb, dp->context); - } - break; - default: - if (dp->on_unknown) dp->on_unknown(&w, 1, dp->context); - break; - } -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0x3 SysEx7 (2 words) - *--------------------------------------------------------------------*/ -static void dispatch_sysex7(midi2_dispatch *dp, const uint32_t *w) { - if (!dp->on_sysex7) return; - uint8_t group = (uint8_t)((w[0] >> 24) & 0x0F); - uint8_t status = (uint8_t)((w[0] >> 16) & 0xF0); /* matches MIDI2_SYSEX7_* enums */ - uint8_t num = (uint8_t)((w[0] >> 16) & 0x0F); - if (num > 6) num = 6; - - uint8_t data[6]; - /* bytes packed: w[0][15:8], w[0][7:0], w[1][31:24]..w[1][7:0] */ - data[0] = (uint8_t)((w[0] >> 8) & 0xFF); - data[1] = (uint8_t)(w[0] & 0xFF); - data[2] = (uint8_t)((w[1] >> 24) & 0xFF); - data[3] = (uint8_t)((w[1] >> 16) & 0xFF); - data[4] = (uint8_t)((w[1] >> 8) & 0xFF); - data[5] = (uint8_t)(w[1] & 0xFF); - - dp->on_sysex7(group, status, data, num, dp->context); -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0x4 MIDI 2.0 Channel Voice (2 words) - *--------------------------------------------------------------------*/ -static void dispatch_cv2(midi2_dispatch *dp, const uint32_t *w) { - uint8_t group = (uint8_t)((w[0] >> 24) & 0x0F); - uint8_t status = (uint8_t)((w[0] >> 16) & 0xF0); - uint8_t channel = (uint8_t)((w[0] >> 16) & 0x0F); - uint8_t byte3 = (uint8_t)((w[0] >> 8) & 0xFF); /* note/bank/index */ - uint8_t byte4 = (uint8_t)(w[0] & 0xFF); /* attr_type/index/lsb */ - - switch (status) { - case MIDI2_STATUS_NOTE_ON: - if (dp->on_note_on) { - uint16_t velocity = (uint16_t)(w[1] >> 16); - uint16_t attr_data = (uint16_t)(w[1] & 0xFFFF); - dp->on_note_on(group, channel, byte3 & 0x7F, velocity, byte4, attr_data, dp->context); - } - break; - - case MIDI2_STATUS_NOTE_OFF: - if (dp->on_note_off) { - uint16_t velocity = (uint16_t)(w[1] >> 16); - uint16_t attr_data = (uint16_t)(w[1] & 0xFFFF); - dp->on_note_off(group, channel, byte3 & 0x7F, velocity, byte4, attr_data, dp->context); - } - break; - - case MIDI2_STATUS_POLY_PRESSURE: - if (dp->on_poly_pressure) dp->on_poly_pressure(group, channel, byte3 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_CC: - if (dp->on_cc) dp->on_cc(group, channel, byte3 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_PROGRAM: { - if (dp->on_program) { - uint8_t program = (uint8_t)((w[1] >> 24) & 0x7F); - bool bank_valid = (w[1] & (UINT32_C(1) << 31)) != 0; - uint8_t bank_msb = (uint8_t)((w[1] >> 8) & 0x7F); - uint8_t bank_lsb = (uint8_t)(w[1] & 0x7F); - dp->on_program(group, channel, program, bank_valid, bank_msb, bank_lsb, dp->context); - } - break; - } - - case MIDI2_STATUS_CHAN_PRESSURE: - if (dp->on_chan_pressure) dp->on_chan_pressure(group, channel, w[1], dp->context); - break; - - case MIDI2_STATUS_PITCH_BEND: - if (dp->on_pitch_bend) dp->on_pitch_bend(group, channel, w[1], dp->context); - break; - - case MIDI2_STATUS_PER_NOTE_PB: - if (dp->on_per_note_pb) dp->on_per_note_pb(group, channel, byte3 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_REG_PER_NOTE: - if (dp->on_reg_per_note) dp->on_reg_per_note(group, channel, byte3 & 0x7F, byte4, w[1], dp->context); - break; - - case MIDI2_STATUS_ASN_PER_NOTE: - if (dp->on_asn_per_note) dp->on_asn_per_note(group, channel, byte3 & 0x7F, byte4, w[1], dp->context); - break; - - case MIDI2_STATUS_RPN: - if (dp->on_rpn) dp->on_rpn(group, channel, byte3 & 0x7F, byte4 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_NRPN: - if (dp->on_nrpn) dp->on_nrpn(group, channel, byte3 & 0x7F, byte4 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_REL_RPN: - if (dp->on_rel_rpn) dp->on_rel_rpn(group, channel, byte3 & 0x7F, byte4 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_REL_NRPN: - if (dp->on_rel_nrpn) dp->on_rel_nrpn(group, channel, byte3 & 0x7F, byte4 & 0x7F, w[1], dp->context); - break; - - case MIDI2_STATUS_PER_NOTE_MGMT: - if (dp->on_per_note_mgmt) { - bool detach = (byte4 & 0x02) != 0; - bool reset = (byte4 & 0x01) != 0; - dp->on_per_note_mgmt(group, channel, byte3 & 0x7F, detach, reset, dp->context); - } - break; - - default: - if (dp->on_unknown) dp->on_unknown(w, 2, dp->context); - break; - } -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0x5 Data 128-bit (4 words) - *--------------------------------------------------------------------*/ -static void dispatch_data128(midi2_dispatch *dp, const uint32_t *w) { - uint8_t group = (uint8_t)((w[0] >> 24) & 0x0F); - uint8_t status_byte = (uint8_t)((w[0] >> 16) & 0xFF); - uint8_t status_hi = status_byte & 0xF0; - uint8_t id_lo = status_byte & 0x0F; - - if (status_hi == MIDI2_MDS_HEADER) { - /* Mixed Data Set Header */ - if (dp->on_mds_header) { - uint16_t num_bytes = (uint16_t)(w[0] & 0xFFFF); - uint16_t num_chunks = (uint16_t)(w[1] >> 16); - uint16_t this_chunk = (uint16_t)(w[1] & 0xFFFF); - uint16_t mfr_id = (uint16_t)(w[2] >> 16); - uint16_t device_id = (uint16_t)(w[2] & 0xFFFF); - uint16_t sub_id1 = (uint16_t)(w[3] >> 16); - uint16_t sub_id2 = (uint16_t)(w[3] & 0xFFFF); - dp->on_mds_header(group, id_lo, num_bytes, num_chunks, this_chunk, - mfr_id, device_id, sub_id1, sub_id2, dp->context); - } - } else if (status_hi == MIDI2_MDS_PAYLOAD) { - /* Mixed Data Set Payload */ - if (dp->on_mds_payload) { - uint8_t data[14]; - uint8_t i; - for (i = 0; i < 14; i++) { - uint8_t wi = (uint8_t)((i + 2) / 4); - uint8_t sh = (uint8_t)(24 - ((i + 2) % 4) * 8); - data[i] = (uint8_t)((w[wi] >> sh) & 0xFF); - } - dp->on_mds_payload(group, id_lo, data, 14, dp->context); - } - } else { - /* SysEx8 (status 0x00..0x30) */ - uint8_t sysex_status = status_byte & 0xF0; /* matches MIDI2_SYSEX8_* enums */ - uint8_t num_bytes = status_byte & 0x0F; - - if (dp->on_sysex8) { - uint8_t stream_id = (uint8_t)((w[0] >> 8) & 0xFF); - /* data bytes: w[0][7:0], w[1][31:0], w[2][31:0], w[3][31:0] = up to 13 */ - uint8_t data[13]; - uint8_t data_len = (num_bytes > 1) ? (uint8_t)(num_bytes - 1) : 0; /* subtract stream_id */ - if (data_len > 13) data_len = 13; - uint8_t i; - if (data_len >= 1) data[0] = (uint8_t)(w[0] & 0xFF); - for (i = 1; i < data_len; i++) { - uint8_t wi = (uint8_t)(1 + (i - 1) / 4); - uint8_t sh = (uint8_t)(24 - ((i - 1) % 4) * 8); - data[i] = (uint8_t)((w[wi] >> sh) & 0xFF); - } - dp->on_sysex8(group, sysex_status, stream_id, data, data_len, dp->context); - } else if (dp->on_unknown) { - dp->on_unknown(w, 4, dp->context); - } - } -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0xD Flex Data (4 words) - *--------------------------------------------------------------------*/ -static void dispatch_flex(midi2_dispatch *dp, const uint32_t *w) { - uint8_t group = (uint8_t)((w[0] >> 24) & 0x0F); - uint8_t format = (uint8_t)((w[0] >> 22) & 0x03); - uint8_t address = (uint8_t)((w[0] >> 20) & 0x03); - uint8_t channel = (uint8_t)((w[0] >> 16) & 0x0F); - uint8_t bank = (uint8_t)((w[0] >> 8) & 0xFF); - uint8_t status = (uint8_t)(w[0] & 0xFF); - - if (bank == MIDI2_FLEX_BANK_SETUP) { - switch (status) { - case MIDI2_FLEX_TEMPO: - if (dp->on_tempo) dp->on_tempo(group, w[1], dp->context); - break; - - case MIDI2_FLEX_TIME_SIG: - if (dp->on_time_sig) { - uint8_t num = (uint8_t)((w[1] >> 24) & 0xFF); - uint8_t den = (uint8_t)((w[1] >> 16) & 0xFF); - uint8_t n32 = (uint8_t)((w[1] >> 8) & 0xFF); - dp->on_time_sig(group, num, den, n32, dp->context); - } - break; - - case MIDI2_FLEX_METRONOME: - if (dp->on_metronome) { - uint8_t clicks = (uint8_t)((w[1] >> 24) & 0xFF); - uint8_t a1 = (uint8_t)((w[1] >> 16) & 0xFF); - uint8_t a2 = (uint8_t)((w[1] >> 8) & 0xFF); - uint8_t a3 = (uint8_t)(w[1] & 0xFF); - uint8_t s1 = (uint8_t)((w[2] >> 24) & 0xFF); - uint8_t s2 = (uint8_t)((w[2] >> 16) & 0xFF); - dp->on_metronome(group, clicks, a1, a2, a3, s1, s2, dp->context); - } - break; - - case MIDI2_FLEX_KEY_SIG: - if (dp->on_key_sig) { - uint8_t sf_raw = (uint8_t)((w[1] >> 28) & 0x0F); - int8_t sf = (sf_raw & 0x08) ? (int8_t)(sf_raw | 0xF0) : (int8_t)sf_raw; - uint8_t tonic = (uint8_t)((w[1] >> 24) & 0x0F); - uint8_t keytype = (uint8_t)((w[1] >> 22) & 0x03); - dp->on_key_sig(group, address, channel, sf, tonic, keytype, dp->context); - } - break; - - case MIDI2_FLEX_CHORD_NAME: - if (dp->on_chord) { - uint8_t tsf_raw = (uint8_t)((w[1] >> 28) & 0x0F); - int8_t tsf = (tsf_raw & 0x08) ? (int8_t)(tsf_raw | 0xF0) : (int8_t)tsf_raw; - uint8_t tn = (uint8_t)((w[1] >> 24) & 0x0F); - uint8_t ct = (uint8_t)((w[1] >> 16) & 0xFF); - uint8_t a1t = (uint8_t)((w[1] >> 12) & 0x0F); - uint8_t a1d = (uint8_t)((w[1] >> 8) & 0x0F); - uint8_t a2t = (uint8_t)((w[1] >> 4) & 0x0F); - uint8_t a2d = (uint8_t)(w[1] & 0x0F); - uint8_t a3t = (uint8_t)((w[2] >> 28) & 0x0F); - uint8_t a3d = (uint8_t)((w[2] >> 24) & 0x0F); - uint8_t a4t = (uint8_t)((w[2] >> 20) & 0x0F); - uint8_t a4d = (uint8_t)((w[2] >> 16) & 0x0F); - uint8_t bsf_raw = (uint8_t)((w[3] >> 28) & 0x0F); - int8_t bsf = (bsf_raw & 0x08) ? (int8_t)(bsf_raw | 0xF0) : (int8_t)bsf_raw; - uint8_t bn = (uint8_t)((w[3] >> 24) & 0x0F); - uint8_t bt = (uint8_t)((w[3] >> 16) & 0xFF); - uint8_t b1t = (uint8_t)((w[3] >> 12) & 0x0F); - uint8_t b1d = (uint8_t)((w[3] >> 8) & 0x0F); - uint8_t b2t = (uint8_t)((w[3] >> 4) & 0x0F); - uint8_t b2d = (uint8_t)(w[3] & 0x0F); - dp->on_chord(group, address, channel, - tsf, tn, ct, - a1t, a1d, a2t, a2d, a3t, a3d, a4t, a4d, - bsf, bn, bt, b1t, b1d, b2t, b2d, - dp->context); - } - break; - - default: - if (dp->on_unknown) dp->on_unknown(w, 4, dp->context); - break; - } - } else if (bank == MIDI2_FLEX_BANK_METADATA || bank == MIDI2_FLEX_BANK_PERF_TEXT) { - /* Text messages: extract 12 bytes from words 1-3 */ - if (dp->on_flex_text) { - uint8_t text[12]; - uint8_t i; - for (i = 0; i < 12; i++) { - uint8_t wi = (uint8_t)(1 + i / 4); - uint8_t sh = (uint8_t)(24 - (i % 4) * 8); - text[i] = (uint8_t)((w[wi] >> sh) & 0xFF); - } - /* find actual length (trim trailing zeros in complete/end packets) */ - uint8_t len = 12; - if (format == 0 || format == 3) { - while (len > 0 && text[len - 1] == 0) len--; - } - dp->on_flex_text(group, format, address, channel, bank, status, text, len, dp->context); - } - } else { - if (dp->on_unknown) dp->on_unknown(w, 4, dp->context); - } -} - -/*--------------------------------------------------------------------+ - * Internal: dispatch MT 0xF UMP Stream (4 words) - *--------------------------------------------------------------------*/ -static void dispatch_stream(midi2_dispatch *dp, const uint32_t *w) { - uint8_t format = (uint8_t)((w[0] >> 26) & 0x03); - uint16_t status = (uint16_t)((w[0] >> 16) & 0x3FF); - - switch (status) { - case MIDI2_STREAM_ENDPOINT_DISCOVERY: - if (dp->on_endpoint_discovery) { - uint8_t maj = (uint8_t)((w[0] >> 8) & 0xFF); - uint8_t min = (uint8_t)(w[0] & 0xFF); - uint8_t filter = (uint8_t)(w[1] & 0xFF); - dp->on_endpoint_discovery(maj, min, filter, dp->context); - } - break; - - case MIDI2_STREAM_ENDPOINT_INFO: - if (dp->on_endpoint_info) { - uint8_t maj = (uint8_t)((w[0] >> 8) & 0xFF); - uint8_t min = (uint8_t)(w[0] & 0xFF); - bool static_fb = (w[1] & (UINT32_C(1) << 31)) != 0; - uint8_t num_fb = (uint8_t)((w[1] >> 24) & 0x7F); - bool m2 = (w[1] & (UINT32_C(1) << 9)) != 0; - bool m1 = (w[1] & (UINT32_C(1) << 8)) != 0; - bool rxjr = (w[1] & (UINT32_C(1) << 1)) != 0; - bool txjr = (w[1] & UINT32_C(1)) != 0; - dp->on_endpoint_info(maj, min, static_fb, num_fb, m2, m1, rxjr, txjr, dp->context); - } - break; - - case MIDI2_STREAM_DEVICE_IDENTITY: - if (dp->on_device_identity) { - uint32_t mfr = (w[1] >> 8) & 0x00FFFFFF; - uint16_t family = (uint16_t)(w[2] >> 16); - uint16_t model = (uint16_t)(w[2] & 0xFFFF); - uint32_t ver = w[3]; - dp->on_device_identity(mfr, family, model, ver, dp->context); - } - break; - - case MIDI2_STREAM_ENDPOINT_NAME: - case MIDI2_STREAM_PRODUCT_INSTANCE_ID: { - if (dp->on_stream_text) { - /* 2 bytes in w[0][15:0], 12 bytes in w[1..3] = 14 max */ - uint8_t data[14]; - data[0] = (uint8_t)((w[0] >> 8) & 0xFF); - data[1] = (uint8_t)(w[0] & 0xFF); - uint8_t i; - for (i = 0; i < 12; i++) { - uint8_t wi = (uint8_t)(1 + i / 4); - uint8_t sh = (uint8_t)(24 - (i % 4) * 8); - data[2 + i] = (uint8_t)((w[wi] >> sh) & 0xFF); - } - uint8_t len = 14; - if (format == 0 || format == 3) { - while (len > 0 && data[len - 1] == 0) len--; - } - dp->on_stream_text(status, format, data, len, dp->context); - } - break; - } - - case MIDI2_STREAM_FB_NAME: { - if (dp->on_fb_name) { - uint8_t fb_num = (uint8_t)((w[0] >> 8) & 0xFF); - /* 1 byte in w[0][7:0] + 12 in w[1..3] = 13 name bytes max */ - uint8_t name[13]; - name[0] = (uint8_t)(w[0] & 0xFF); - uint8_t i; - for (i = 0; i < 12; i++) { - uint8_t wi = (uint8_t)(1 + i / 4); - uint8_t sh = (uint8_t)(24 - (i % 4) * 8); - name[1 + i] = (uint8_t)((w[wi] >> sh) & 0xFF); - } - uint8_t len = 13; - if (format == 0 || format == 3) { - while (len > 0 && name[len - 1] == 0) len--; - } - dp->on_fb_name(format, fb_num, name, len, dp->context); - } - break; - } - - case MIDI2_STREAM_CONFIG_REQUEST: - if (dp->on_config_request) { - uint8_t proto = (uint8_t)((w[0] >> 8) & 0xFF); - bool rxjr = (w[0] & 0x02) != 0; - bool txjr = (w[0] & 0x01) != 0; - dp->on_config_request(proto, rxjr, txjr, dp->context); - } - break; - - case MIDI2_STREAM_CONFIG_NOTIFY: - if (dp->on_config_notify) { - uint8_t proto = (uint8_t)((w[0] >> 8) & 0xFF); - bool rxjr = (w[0] & 0x02) != 0; - bool txjr = (w[0] & 0x01) != 0; - dp->on_config_notify(proto, rxjr, txjr, dp->context); - } - break; - - case MIDI2_STREAM_FB_DISCOVERY: - if (dp->on_fb_discovery) { - uint8_t fb = (uint8_t)((w[0] >> 8) & 0xFF); - uint8_t filter = (uint8_t)(w[0] & 0xFF); - dp->on_fb_discovery(fb, filter, dp->context); - } - break; - - case MIDI2_STREAM_FB_INFO: - if (dp->on_fb_info) { - bool active = (w[0] & (UINT32_C(1) << 15)) != 0; - uint8_t fb_num = (uint8_t)((w[0] >> 8) & 0x7F); - uint8_t dir = (uint8_t)(w[0] & 0x03); - uint8_t first = (uint8_t)((w[1] >> 24) & 0x0F); - uint8_t ngrp = (uint8_t)((w[1] >> 16) & 0x0F); - uint8_t ci_ver = (uint8_t)((w[1] >> 8) & 0xFF); - uint8_t s8str = (uint8_t)((w[1] >> 2) & 0x3F); - uint8_t proto = (uint8_t)(w[1] & 0x03); - dp->on_fb_info(active, fb_num, dir, first, ngrp, ci_ver, s8str, proto, dp->context); - } - break; - - case MIDI2_STREAM_START_OF_CLIP: - if (dp->on_clip) dp->on_clip(true, dp->context); - break; - - case MIDI2_STREAM_END_OF_CLIP: - if (dp->on_clip) dp->on_clip(false, dp->context); - break; - - default: - if (dp->on_unknown) dp->on_unknown(w, 4, dp->context); - break; - } -} - -/*--------------------------------------------------------------------+ - * Public: feed one UMP message - * - * Signature matches midi2_proc_ump_cb so it can be used directly: - * proc.on_ump = midi2_dispatch_feed; - * proc.context = &dispatch; - *--------------------------------------------------------------------*/ -void midi2_dispatch_feed(const uint32_t *words, uint8_t word_count, void *context) { - midi2_dispatch *dp = (midi2_dispatch *)context; - if (!dp || !words || word_count == 0) return; - - uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); - - switch (mt) { - case MIDI2_MT_UTILITY: - dispatch_utility(dp, words[0]); - break; - case MIDI2_MT_SYSTEM: - dispatch_system(dp, words[0]); - break; - case MIDI2_MT_MIDI1_CV: - if (dp->upscale_mt2) { - uint32_t mt4[2]; - if (midi2_msg_mt2_to_mt4(words[0], mt4)) { - dispatch_cv2(dp, mt4); - } - } else { - dispatch_cv1(dp, words[0]); - } - break; - case MIDI2_MT_SYSEX7: - dispatch_sysex7(dp, words); - break; - case MIDI2_MT_MIDI2_CV: - dispatch_cv2(dp, words); - break; - case MIDI2_MT_DATA128: - dispatch_data128(dp, words); - break; - case MIDI2_MT_FLEX_DATA: - dispatch_flex(dp, words); - break; - case MIDI2_MT_STREAM: - dispatch_stream(dp, words); - break; - default: - if (dp->on_unknown) dp->on_unknown(words, word_count, dp->context); - break; - } -} - -/* == midi2_proc (impl) =================================================== */ - - -/* - * midi2_proc.c - UMP stream processing implementation - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - -/*--------------------------------------------------------------------+ - * Init - *--------------------------------------------------------------------*/ -void midi2_proc_init(midi2_proc_state *state, - uint8_t *sysex7_buf, uint16_t sysex7_buf_size, - uint8_t *sysex8_buf, uint16_t sysex8_buf_size) { - uint8_t i; - memset(state, 0, sizeof(midi2_proc_state)); - state->group_mask = 0xFFFF; - state->sysex_group = 0xFF; - state->sysex_buf = sysex7_buf; - state->sysex_buf_size = sysex7_buf_size; - state->sysex8_buf = sysex8_buf; - state->sysex8_buf_size = sysex8_buf_size; - state->sysex8_group = 0xFF; - for (i = 0; i < 16; i++) { - state->group_map[i] = i; - } -} - -/*--------------------------------------------------------------------+ - * SysEx7 reassembly (internal) - *--------------------------------------------------------------------*/ -static void sysex7_process(midi2_proc_state *state, uint8_t group, const uint32_t *words) { - if (state->sysex_buf == NULL) { - /* No buffer provided: deliver raw SysEx packets without reassembly */ - return; - } - uint8_t status_nib = (words[0] >> 16) & 0xF0; /* matches MIDI2_SYSEX7_* enums */ - uint8_t num_bytes = (words[0] >> 16) & 0x0F; - - /* Extract data bytes from SysEx7 UMP packet */ - uint8_t data[6]; - uint8_t n = 0; - if (num_bytes >= 1) data[n++] = (words[0] >> 8) & 0x7F; - if (num_bytes >= 2) data[n++] = (words[0] >> 0) & 0x7F; - if (num_bytes >= 3) data[n++] = (words[1] >> 24) & 0x7F; - if (num_bytes >= 4) data[n++] = (words[1] >> 16) & 0x7F; - if (num_bytes >= 5) data[n++] = (words[1] >> 8) & 0x7F; - if (num_bytes >= 6) data[n++] = (words[1] >> 0) & 0x7F; - - if (status_nib == MIDI2_SYSEX7_COMPLETE) { - /* Complete: single-packet SysEx */ - if (state->on_sysex7) { - state->on_sysex7(group, data, n, state->context); - } - return; - } - - if (status_nib == MIDI2_SYSEX7_START) { - /* Start */ - state->sysex_group = group; - state->sysex_len = 0; - } else if (group != state->sysex_group) { - /* Different group mid-stream: discard in-progress, restart */ - state->sysex_group = group; - state->sysex_len = 0; - if (status_nib == MIDI2_SYSEX7_CONTINUE) return; /* Continue without start: drop */ - } - - /* Append data */ - { - uint8_t i; - for (i = 0; i < n && state->sysex_len < state->sysex_buf_size; i++) { - state->sysex_buf[state->sysex_len++] = data[i]; - } - } - - if (status_nib == MIDI2_SYSEX7_END) { - /* End: deliver complete message */ - if (state->on_sysex7) { - state->on_sysex7(group, state->sysex_buf, state->sysex_len, state->context); - } - state->sysex_group = 0xFF; - state->sysex_len = 0; - } -} - -/*--------------------------------------------------------------------+ - * SysEx8 reassembly (internal) - *--------------------------------------------------------------------*/ -static void sysex8_process(midi2_proc_state *state, uint8_t group, const uint32_t *words) { - if (state->sysex8_buf == NULL) return; - - uint8_t status_nib = (words[0] >> 16) & 0xF0; /* matches MIDI2_SYSEX8_* enums */ - uint8_t num_bytes = (words[0] >> 16) & 0x0F; /* includes stream_id */ - uint8_t stream_id = (words[0] >> 8) & 0xFF; - - /* Extract data bytes (num_bytes - 1, since stream_id is counted) */ - uint8_t data[13]; - uint8_t n = 0; - uint8_t total_data = (num_bytes > 1) ? (uint8_t)(num_bytes - 1) : 0; - - if (total_data >= 1) data[n++] = words[0] & 0xFF; - if (total_data >= 2) data[n++] = (words[1] >> 24) & 0xFF; - if (total_data >= 3) data[n++] = (words[1] >> 16) & 0xFF; - if (total_data >= 4) data[n++] = (words[1] >> 8) & 0xFF; - if (total_data >= 5) data[n++] = words[1] & 0xFF; - if (total_data >= 6) data[n++] = (words[2] >> 24) & 0xFF; - if (total_data >= 7) data[n++] = (words[2] >> 16) & 0xFF; - if (total_data >= 8) data[n++] = (words[2] >> 8) & 0xFF; - if (total_data >= 9) data[n++] = words[2] & 0xFF; - if (total_data >= 10) data[n++] = (words[3] >> 24) & 0xFF; - if (total_data >= 11) data[n++] = (words[3] >> 16) & 0xFF; - if (total_data >= 12) data[n++] = (words[3] >> 8) & 0xFF; - if (total_data >= 13) data[n++] = words[3] & 0xFF; - - if (status_nib == MIDI2_SYSEX8_COMPLETE) { - /* Complete single-packet SysEx8 */ - if (state->on_sysex8) { - state->on_sysex8(group, stream_id, data, n, state->context); - } - return; - } - - if (status_nib == MIDI2_SYSEX8_START) { - /* Start */ - state->sysex8_group = group; - state->sysex8_stream_id = stream_id; - state->sysex8_len = 0; - } else if (group != state->sysex8_group || stream_id != state->sysex8_stream_id) { - /* Different group or stream mid-stream: discard */ - state->sysex8_group = group; - state->sysex8_stream_id = stream_id; - state->sysex8_len = 0; - if (status_nib == MIDI2_SYSEX8_CONTINUE) return; - } - - /* Append data */ - { - uint8_t i; - for (i = 0; i < n && state->sysex8_len < state->sysex8_buf_size; i++) { - state->sysex8_buf[state->sysex8_len++] = data[i]; - } - } - - if (status_nib == MIDI2_SYSEX8_END) { - /* End */ - if (state->on_sysex8) { - state->on_sysex8(group, state->sysex8_stream_id, - state->sysex8_buf, state->sysex8_len, state->context); - } - state->sysex8_group = 0xFF; - state->sysex8_len = 0; - } -} - -/*--------------------------------------------------------------------+ - * Feed - *--------------------------------------------------------------------*/ -void midi2_proc_feed(midi2_proc_state *state, const uint32_t *words, uint8_t word_count) { - uint8_t mt = midi2_msg_get_mt(words); - uint8_t group = midi2_msg_get_group(words); - - (void)word_count; /* caller provides for API clarity; MT determines actual size */ - - /* SysEx8: reassemble before group filtering (same rationale as SysEx7) */ - if (mt == MIDI2_MT_DATA128) { - sysex8_process(state, group, words); - } - - /* SysEx7 is processed before group filtering by design: MIDI-CI responses - * (delivered via on_sysex7) must work regardless of group filter settings. - * SysEx7 messages from filtered-out groups will still be reassembled and - * delivered via on_sysex7. This is intentional: MIDI-CI must work - * regardless of group filter settings. */ - if (mt == MIDI2_MT_SYSEX7) { - sysex7_process(state, group, words); - } - - /* Group filtering: bypass for Utility (MT 0x0) and Stream (MT 0xF) */ - if (mt != MIDI2_MT_UTILITY && mt != MIDI2_MT_STREAM) { - if (!(state->group_mask & (1u << group))) return; - } - - /* Dispatch to UMP callback (SysEx7/8 already handled above via callbacks) */ - if (mt != MIDI2_MT_SYSEX7 && mt != MIDI2_MT_DATA128 && state->on_ump) { - state->on_ump(words, midi2_msg_word_count(mt), state->context); - } -} - -/*--------------------------------------------------------------------+ - * Group remap - *--------------------------------------------------------------------*/ -void midi2_proc_remap_group(midi2_proc_state *state, uint32_t *words) { - uint8_t mt = midi2_msg_get_mt(words); - if (mt != MIDI2_MT_UTILITY && mt != MIDI2_MT_STREAM) { - uint8_t group = midi2_msg_get_group(words); - uint8_t new_group = state->group_map[group & 0x0F]; - words[0] = (words[0] & 0xF0FFFFFF) | ((uint32_t)(new_group & 0x0F) << 24); - } -} - -/*--------------------------------------------------------------------+ - * SysEx7 send (fragmentation) - *--------------------------------------------------------------------*/ -void midi2_proc_send_sysex7(uint8_t group, const uint8_t *data, uint16_t length, - midi2_proc_write_fn write_fn, void *context) { - uint32_t w[2]; - uint16_t offset = 0; - - if (length == 0) return; - - if (length <= 6) { - midi2_msg_sysex7_packet(w, group, MIDI2_SYSEX7_COMPLETE, data, (uint8_t)length); - write_fn(w, 2, context); - return; - } - - /* Start */ - midi2_msg_sysex7_packet(w, group, MIDI2_SYSEX7_START, data, 6); - write_fn(w, 2, context); - offset = 6; - - /* Continue */ - while (offset + 6 < length) { - midi2_msg_sysex7_packet(w, group, MIDI2_SYSEX7_CONTINUE, data + offset, 6); - write_fn(w, 2, context); - offset += 6; - } - - /* End */ - { - uint8_t remaining = (uint8_t)(length - offset); - midi2_msg_sysex7_packet(w, group, MIDI2_SYSEX7_END, data + offset, remaining); - write_fn(w, 2, context); - } -} - -/*--------------------------------------------------------------------+ - * Function Block Name Notification (UMP Stream MT 0xF status 0x12) - * - * M2-104-UM §7.1.9. 4-word packet; 1 name byte at byte 3 of word 0 plus - * 12 more bytes across words 1-3, so 13 bytes of name per UMP. Uses - * Form = Complete (0) for <=13 bytes, Start/Continue/End otherwise. - * Spec mandates max 91 bytes total; we silently truncate at 91. - *--------------------------------------------------------------------*/ -#define MIDI2_FB_NAME_BYTES_PER_UMP 13u -#define MIDI2_FB_NAME_MAX_BYTES 91u - -void midi2_proc_send_fb_name(uint8_t fb_idx, const char *name, - midi2_proc_write_fn write_fn, void *context) { - if (!write_fn || !name) return; - - uint16_t total = 0; - while (name[total] && total < MIDI2_FB_NAME_MAX_BYTES) total++; - if (total == 0) return; - - uint16_t offset = 0; - while (offset < total) { - uint16_t remaining = (uint16_t)(total - offset); - uint8_t n = (remaining > MIDI2_FB_NAME_BYTES_PER_UMP) - ? (uint8_t)MIDI2_FB_NAME_BYTES_PER_UMP - : (uint8_t)remaining; - uint8_t is_first = (offset == 0); - uint8_t is_last = (remaining <= MIDI2_FB_NAME_BYTES_PER_UMP); - uint8_t form = (is_first && is_last) ? 0u /* Complete */ - : is_first ? 1u /* Start */ - : is_last ? 3u /* End */ - : 2u; /* Continue */ - - uint32_t msg[4] = {0}; - msg[0] = ((uint32_t)0xFu << 28) - | ((uint32_t)form << 26) - | ((uint32_t)0x12u << 16) - | ((uint32_t)fb_idx << 8); - const uint8_t *p = (const uint8_t *)(name + offset); - if (n > 0) msg[0] |= (uint32_t)p[0]; - uint8_t i; - for (i = 1; i < n; i++) { - uint8_t widx = (uint8_t)(1u + (i - 1u) / 4u); - uint8_t shift = (uint8_t)(24u - ((i - 1u) % 4u) * 8u); - msg[widx] |= ((uint32_t)p[i] << shift); - } - write_fn(msg, 4, context); - offset += n; - } -} - -/*--------------------------------------------------------------------+ - * UMP Stream text senders: Endpoint Name (status 0x003), - * Product Instance ID (status 0x004). M2-104-UM §7.1.7 / §7.1.8. - * - * 14 payload bytes per UMP (bytes 0-1 live in word 0 bits [15:0], - * bytes 2-13 in words 1-3). Fragments into Complete / Start / - * Continue / End packets. Reuses midi2_msg_stream_endpoint_name / - * midi2_msg_stream_product_id inline builders so the word layout - * stays canonical. - *--------------------------------------------------------------------*/ -#define MIDI2_STREAM_TEXT_BYTES_PER_UMP 14u -#define MIDI2_STREAM_TEXT_MAX_BYTES 98u /* 7 UMPs cap per spec */ - -typedef void (*stream_text_builder_fn)(uint32_t *w, uint8_t format, - const uint8_t *data, uint8_t len); - -static void stream_text_emit(stream_text_builder_fn builder, - const char *text, - midi2_proc_write_fn write_fn, void *context) { - if (!write_fn || !text) return; - uint16_t total = 0; - while (text[total] && total < MIDI2_STREAM_TEXT_MAX_BYTES) total++; - if (total == 0) return; - - uint16_t offset = 0; - while (offset < total) { - uint16_t remaining = (uint16_t)(total - offset); - uint8_t n = (remaining > MIDI2_STREAM_TEXT_BYTES_PER_UMP) - ? (uint8_t)MIDI2_STREAM_TEXT_BYTES_PER_UMP - : (uint8_t)remaining; - uint8_t is_first = (offset == 0); - uint8_t is_last = (remaining <= MIDI2_STREAM_TEXT_BYTES_PER_UMP); - uint8_t form = (is_first && is_last) ? 0u /* Complete */ - : is_first ? 1u /* Start */ - : is_last ? 3u /* End */ - : 2u; /* Continue */ - uint32_t msg[4]; - builder(msg, form, (const uint8_t *)(text + offset), n); - write_fn(msg, 4, context); - offset += n; - } -} - -void midi2_proc_send_endpoint_name(const char *name, - midi2_proc_write_fn write_fn, void *context) { - stream_text_emit(midi2_msg_stream_endpoint_name, name, write_fn, context); -} - -void midi2_proc_send_product_id(const char *id, - midi2_proc_write_fn write_fn, void *context) { - stream_text_emit(midi2_msg_stream_product_id, id, write_fn, context); -} - -/*--------------------------------------------------------------------+ - * Device Identity Notification sender (M2-104-UM §7.1.6). - * Single 4-word UMP, no fragmentation. Delegates to the inline - * builder for byte layout. - *--------------------------------------------------------------------*/ -void midi2_proc_send_device_identity(uint32_t manufacturer_id, - uint16_t family_id, uint16_t model_id, - uint32_t version_id, - midi2_proc_write_fn write_fn, void *context) { - if (!write_fn) return; - uint32_t msg[4]; - midi2_msg_stream_device_identity(msg, manufacturer_id, family_id, - model_id, version_id); - write_fn(msg, 4, context); -} - -/*--------------------------------------------------------------------+ - * SysEx8 sender (M2-104-UM §7.8). - * - * 13 data bytes per UMP (word 0 low byte carries data[0], words 1-3 - * carry the remaining 12). stream_id rides word 0 bits [15:8]. The - * status nibble in bits [23:20] encodes Complete/Start/Continue/End - * per Table 14. Delegates to midi2_msg_sysex8_packet per packet so - * the status/num_bytes field stays aligned with the canonical - * builder. - *--------------------------------------------------------------------*/ -#define MIDI2_SYSEX8_BYTES_PER_UMP 13u - -void midi2_proc_send_sysex8(uint8_t group, uint8_t stream_id, - const uint8_t *data, uint16_t length, - midi2_proc_write_fn write_fn, void *context) { - if (!write_fn) return; - if (length == 0) return; - if (!data) return; - - uint16_t offset = 0; - while (offset < length) { - uint16_t remaining = (uint16_t)(length - offset); - uint8_t n = (remaining > MIDI2_SYSEX8_BYTES_PER_UMP) - ? (uint8_t)MIDI2_SYSEX8_BYTES_PER_UMP - : (uint8_t)remaining; - uint8_t is_first = (offset == 0); - uint8_t is_last = (remaining <= MIDI2_SYSEX8_BYTES_PER_UMP); - uint8_t status = (is_first && is_last) ? MIDI2_SYSEX8_COMPLETE - : is_first ? MIDI2_SYSEX8_START - : is_last ? MIDI2_SYSEX8_END - : MIDI2_SYSEX8_CONTINUE; - uint32_t msg[4]; - midi2_msg_sysex8_packet(msg, group, status, stream_id, - data + offset, n); - write_fn(msg, 4, context); - offset += n; - } -} - -/* == midi2_conv (impl) =================================================== */ - - -/* - * midi2_conv.c - MIDI 1.0 byte stream to UMP implementation - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI 2.0 UMP (M2-104-UM v1.1.2, Nov 2024) - * Version: 0.3.0 - */ - - -void midi2_conv_init(midi2_conv_state *state, uint8_t group) { - memset(state, 0, sizeof(midi2_conv_state)); - state->group = group & 0x0F; -} - -/* How many data bytes a status byte expects */ -static uint8_t expected_data_bytes(uint8_t status) { - switch (status & 0xF0) { - case 0x80: return 2; /* Note Off */ - case 0x90: return 2; /* Note On */ - case 0xA0: return 2; /* Poly Pressure */ - case 0xB0: return 2; /* CC */ - case 0xC0: return 1; /* Program Change */ - case 0xD0: return 1; /* Channel Pressure */ - case 0xE0: return 2; /* Pitch Bend */ - default: break; - } - /* System Common */ - switch (status) { - case 0xF1: return 1; /* MTC Quarter Frame */ - case 0xF2: return 2; /* Song Position Pointer */ - case 0xF3: return 1; /* Song Select */ - default: return 0; /* System Real-Time, F0, F7, etc */ - } -} - -/* Emit a completed channel voice or system common message as UMP */ -static void emit_channel_msg(midi2_conv_state *state) { - uint8_t status = state->running_status; - - if ((status & 0xF0) >= 0x80 && (status & 0xF0) <= 0xE0) { - /* Channel Voice: MT 0x2 */ - state->ump[0] = midi2_msg_from_midi1(state->group, status, - state->data[0], state->data[1]); - state->ump_words = 1; - } else if (status >= 0xF1 && status <= 0xF3) { - /* System Common: MT 0x1 */ - if (state->data_byte_count == 0) { - state->ump[0] = midi2_msg_system(state->group, status); - } else if (state->data_byte_count == 1) { - state->ump[0] = midi2_msg_system_2byte(state->group, status, state->data[0]); - } else { - state->ump[0] = midi2_msg_system_3byte(state->group, status, - state->data[0], state->data[1]); - } - state->ump_words = 1; - } -} - -/* Emit a SysEx7 UMP packet from the internal 6-byte buffer. - * Called when the buffer is full (6 bytes) or when F7 arrives. */ -static bool emit_sysex_packet(midi2_conv_state *state, bool is_end) { - uint8_t status; - - if (!state->sysex_started && is_end) { - /* Never emitted START: entire SysEx fits in one COMPLETE packet */ - status = MIDI2_SYSEX7_COMPLETE; - } else if (!state->sysex_started) { - /* First packet of a multi-packet SysEx */ - status = MIDI2_SYSEX7_START; - state->sysex_started = true; - } else if (is_end) { - /* Final packet */ - status = MIDI2_SYSEX7_END; - } else { - /* Middle packet */ - status = MIDI2_SYSEX7_CONTINUE; - } - - midi2_msg_sysex7_packet(state->ump, state->group, status, - state->sysex_buf, state->sysex_len); - state->ump_words = 2; - state->sysex_len = 0; - - if (is_end) { - state->in_sysex = false; - state->sysex_started = false; - } - - return true; -} - -bool midi2_conv_feed(midi2_conv_state *state, uint8_t byte) { - state->ump_words = 0; - - /* Real-Time messages (F8-FF) can appear anywhere, even mid-message */ - if (byte >= 0xF8) { - state->ump[0] = midi2_msg_system(state->group, byte); - state->ump_words = 1; - return true; - } - - /* SysEx handling */ - if (byte == 0xF0) { - /* SysEx Start */ - state->in_sysex = true; - state->sysex_started = false; - state->sysex_len = 0; - state->running_status = 0; /* SysEx cancels Running Status */ - return false; - } - - if (byte == 0xF7) { - /* SysEx End */ - if (state->in_sysex) { - return emit_sysex_packet(state, true); - } - return false; /* F7 without F0: ignore */ - } - - if (state->in_sysex) { - /* A non-Real-Time status byte during SysEx terminates it implicitly */ - if (byte >= 0x80) { - state->in_sysex = false; - state->sysex_started = false; - state->sysex_len = 0; - /* Fall through to process as new status byte */ - } else { - /* Accumulate SysEx data byte */ - state->sysex_buf[state->sysex_len++] = byte; - if (state->sysex_len == 6) { - /* Buffer full: emit START or CONTINUE packet */ - return emit_sysex_packet(state, false); - } - return false; - } - } - - /* Status byte */ - if (byte >= 0x80) { - /* System Common (F1-F6) cancel Running Status */ - if (byte >= 0xF1 && byte <= 0xF6) { - state->running_status = byte; - state->data_byte_count = expected_data_bytes(byte); - state->data_pos = 0; - if (state->data_byte_count == 0) { - /* Tune Request (F6) -- no data bytes */ - state->ump[0] = midi2_msg_system(state->group, byte); - state->ump_words = 1; - state->running_status = 0; - return true; - } - return false; - } - - /* Channel Voice status */ - state->running_status = byte; - state->data_byte_count = expected_data_bytes(byte); - state->data_pos = 0; - state->data[0] = 0; - state->data[1] = 0; - return false; - } - - /* Data byte (Running Status applies) */ - if (state->running_status == 0) { - return false; /* No status set: orphan data byte, ignore */ - } - - state->data[state->data_pos++] = byte; - - if (state->data_pos >= state->data_byte_count) { - emit_channel_msg(state); - state->data_pos = 0; /* Reset for Running Status (next data bytes reuse status) */ - state->data[0] = 0; - state->data[1] = 0; - return true; - } - - return false; -} - -/* == midi2_ci_dispatch (impl) ============================================ */ - - -/* - * midi2_ci_dispatch.c - MIDI-CI typed dispatch implementation - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI-CI (M2-101-UM v1.2, Jun 2023) - * Version: 0.3.0 - */ - - -void midi2_ci_dispatch_init(midi2_ci_dispatch *dp) { - memset(dp, 0, sizeof(*dp)); -} - -/*--------------------------------------------------------------------+ - * Internal: build common header struct from raw data - *--------------------------------------------------------------------*/ -static midi2_ci_header make_hdr(const uint8_t *d, uint8_t group) { - midi2_ci_header h; - h.device_id = midi2_ci_get_device_id(d); - h.version = midi2_ci_get_version(d); - h.src_muid = midi2_ci_get_src_muid(d); - h.dst_muid = midi2_ci_get_dst_muid(d); - h.group = group; - return h; -} - -/*--------------------------------------------------------------------+ - * Internal: parse PE data message (shared by Get/Set/Subscribe/Notify) - *--------------------------------------------------------------------*/ -static void dispatch_pe_data(midi2_ci_dp_pe_data_cb cb, midi2_ci_header hdr, - const uint8_t *d, uint16_t len, void *ctx) { - if (!cb) return; - if (len < 14) return; /* header(13) + request_id(1) minimum */ - uint8_t request_id = d[13]; - uint16_t p = 14; - - /* Header data */ - uint16_t hdr_len = 0; - if (p + 2 <= len) { hdr_len = midi2_ci_read_14(&d[p]); p += 2; } - const uint8_t *hdr_data = (hdr_len > 0 && p + hdr_len <= len) ? &d[p] : NULL; - if (hdr_len > 0) p += hdr_len; - - /* Chunk info */ - uint16_t num_chunks = 0, this_chunk = 0; - if (p + 2 <= len) { num_chunks = midi2_ci_read_14(&d[p]); p += 2; } - if (p + 2 <= len) { this_chunk = midi2_ci_read_14(&d[p]); p += 2; } - - /* Body data */ - uint16_t body_len = 0; - if (p + 2 <= len) { body_len = midi2_ci_read_14(&d[p]); p += 2; } - const uint8_t *body_data = (body_len > 0 && p + body_len <= len) ? &d[p] : NULL; - - cb(hdr, request_id, hdr_data, hdr_len, num_chunks, this_chunk, - body_data, body_len, ctx); -} - -/*--------------------------------------------------------------------+ - * Feed - *--------------------------------------------------------------------*/ -bool midi2_ci_dispatch_feed(midi2_ci_dispatch *dp, uint8_t group, - const uint8_t *data, uint16_t length) { - if (!dp || !data) return false; - if (!midi2_ci_is_ci(data, length)) return false; - - midi2_ci_header hdr = make_hdr(data, group); - uint8_t sub_id = midi2_ci_get_sub_id(data); - - switch (sub_id) { - - /*--- Management ---*/ - - case MIDI2_CI_DISCOVERY: { - if (!dp->on_discovery || length < 29) break; - uint8_t out_path = (hdr.version >= MIDI2_CI_VERSION_2 && length >= 30) ? data[29] : 0; - dp->on_discovery(hdr, midi2_ci_get_mfr_id(data), midi2_ci_get_family(data), - midi2_ci_get_model(data), midi2_ci_get_sw_rev(data), - midi2_ci_get_ci_category(data), midi2_ci_get_max_sysex(data), - out_path, dp->context); - return true; - } - - case MIDI2_CI_DISCOVERY_REPLY: { - if (!dp->on_discovery_reply || length < 29) break; - uint8_t out_path = 0, fb = 0x7F; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 31) { - out_path = data[29]; - fb = data[30]; - } - dp->on_discovery_reply(hdr, midi2_ci_get_mfr_id(data), midi2_ci_get_family(data), - midi2_ci_get_model(data), midi2_ci_get_sw_rev(data), - midi2_ci_get_ci_category(data), midi2_ci_get_max_sysex(data), - out_path, fb, dp->context); - return true; - } - - case MIDI2_CI_ENDPOINT_INFO: { - if (!dp->on_endpoint_info || length < 14) break; - dp->on_endpoint_info(hdr, data[13], dp->context); - return true; - } - - case MIDI2_CI_ENDPOINT_INFO_REPLY: { - if (!dp->on_endpoint_info_reply || length < 16) break; - uint8_t status = data[13]; - uint16_t info_len = midi2_ci_read_14(&data[14]); - const uint8_t *info = (info_len > 0 && length >= 16 + info_len) ? &data[16] : NULL; - dp->on_endpoint_info_reply(hdr, status, info, info_len, dp->context); - return true; - } - - case MIDI2_CI_INVALIDATE_MUID: { - if (!dp->on_invalidate_muid || length < 17) break; - dp->on_invalidate_muid(hdr, midi2_ci_get_target_muid(data), dp->context); - return true; - } - - case MIDI2_CI_ACK: { - if (!dp->on_ack || length < 13) break; - uint8_t orig = 0, sc = 0, sd = 0; - const uint8_t *det = NULL; - uint16_t ml = 0; - const uint8_t *mt = NULL; - if (length >= 23) { - orig = data[13]; sc = data[14]; sd = data[15]; - det = &data[16]; /* 5 bytes */ - ml = midi2_ci_read_14(&data[21]); - mt = (ml > 0 && length >= 23 + ml) ? &data[23] : NULL; - } - dp->on_ack(hdr, orig, sc, sd, det, ml, mt, dp->context); - return true; - } - - case MIDI2_CI_NAK: { - if (!dp->on_nak || length < 13) break; - uint8_t orig = 0, sc = 0, sd = 0; - const uint8_t *det = NULL; - uint16_t ml = 0; - const uint8_t *mt = NULL; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 23) { - orig = data[13]; sc = data[14]; sd = data[15]; - det = &data[16]; - ml = midi2_ci_read_14(&data[21]); - mt = (ml > 0 && length >= 23 + ml) ? &data[23] : NULL; - } - dp->on_nak(hdr, orig, sc, sd, det, ml, mt, dp->context); - return true; - } - - /*--- Profile Configuration ---*/ - - case MIDI2_CI_PROFILE_INQUIRY: { - if (!dp->on_profile_inquiry) break; - dp->on_profile_inquiry(hdr, dp->context); - return true; - } - - case MIDI2_CI_PROFILE_INQUIRY_REPLY: { - if (!dp->on_profile_inquiry_reply || length < 15) break; - uint16_t en_count = midi2_ci_read_14(&data[13]); - uint16_t en_bytes = (uint16_t)(en_count * 5); - if (15 + en_bytes + 2 > length) break; /* bounds check */ - const uint8_t *en_data = &data[15]; - uint16_t dis_off = (uint16_t)(15 + en_bytes); - uint16_t dis_count = 0; - const uint8_t *dis_data = NULL; - if (dis_off + 2 <= length) { - dis_count = midi2_ci_read_14(&data[dis_off]); - dis_data = &data[dis_off + 2]; - } - dp->on_profile_inquiry_reply(hdr, en_count, en_data, dis_count, dis_data, dp->context); - return true; - } - - case MIDI2_CI_SET_PROFILE_ON: { - if (!dp->on_set_profile_on || length < 18) break; - uint16_t nch = 0; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 20) { - nch = midi2_ci_read_14(&data[18]); - } - dp->on_set_profile_on(hdr, &data[13], nch, dp->context); - return true; - } - - case MIDI2_CI_SET_PROFILE_OFF: { - if (!dp->on_set_profile_off || length < 18) break; - dp->on_set_profile_off(hdr, &data[13], 0, dp->context); - return true; - } - - case MIDI2_CI_PROFILE_ENABLED: { - if (!dp->on_profile_enabled || length < 18) break; - uint16_t nch = 0; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 20) { - nch = midi2_ci_read_14(&data[18]); - } - dp->on_profile_enabled(hdr, &data[13], nch, dp->context); - return true; - } - - case MIDI2_CI_PROFILE_DISABLED: { - if (!dp->on_profile_disabled || length < 18) break; - uint16_t nch = 0; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 20) { - nch = midi2_ci_read_14(&data[18]); - } - dp->on_profile_disabled(hdr, &data[13], nch, dp->context); - return true; - } - - case MIDI2_CI_PROFILE_ADDED: { - if (!dp->on_profile_added || length < 18) break; - dp->on_profile_added(hdr, &data[13], dp->context); - return true; - } - - case MIDI2_CI_PROFILE_REMOVED: { - if (!dp->on_profile_removed || length < 18) break; - dp->on_profile_removed(hdr, &data[13], dp->context); - return true; - } - - case MIDI2_CI_PROFILE_DETAILS: { - if (!dp->on_profile_details || length < 19) break; - dp->on_profile_details(hdr, &data[13], data[18], dp->context); - return true; - } - - case MIDI2_CI_PROFILE_DETAILS_REPLY: { - if (!dp->on_profile_details_reply || length < 21) break; - uint8_t target = data[18]; - uint16_t dl = midi2_ci_read_14(&data[19]); - const uint8_t *dd = (dl > 0 && length >= 21 + dl) ? &data[21] : NULL; - dp->on_profile_details_reply(hdr, &data[13], target, dd, dl, dp->context); - return true; - } - - case MIDI2_CI_PROFILE_SPECIFIC_DATA: { - if (!dp->on_profile_specific_data || length < 22) break; - uint32_t dl = midi2_ci_read_28(&data[18]); - const uint8_t *dd = (dl > 0 && dl <= (uint32_t)(length - 22)) ? &data[22] : NULL; - dp->on_profile_specific_data(hdr, &data[13], dd, dl, dp->context); - return true; - } - - /*--- Property Exchange ---*/ - - case MIDI2_CI_PE_CAPABILITY: { - if (!dp->on_pe_capability || length < 14) break; - uint8_t max_sim = data[13]; - uint8_t maj = 0, min = 0; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 16) { - maj = data[14]; min = data[15]; - } - dp->on_pe_capability(hdr, max_sim, maj, min, dp->context); - return true; - } - - case MIDI2_CI_PE_CAPABILITY_REPLY: { - if (!dp->on_pe_capability_reply || length < 14) break; - uint8_t max_sim = data[13]; - uint8_t maj = 0, min = 0; - if (hdr.version >= MIDI2_CI_VERSION_2 && length >= 16) { - maj = data[14]; min = data[15]; - } - dp->on_pe_capability_reply(hdr, max_sim, maj, min, dp->context); - return true; - } - - case MIDI2_CI_PE_GET: - dispatch_pe_data(dp->on_pe_get, hdr, data, length, dp->context); - return dp->on_pe_get != NULL; - - case MIDI2_CI_PE_GET_REPLY: - dispatch_pe_data(dp->on_pe_get_reply, hdr, data, length, dp->context); - return dp->on_pe_get_reply != NULL; - - case MIDI2_CI_PE_SET: - dispatch_pe_data(dp->on_pe_set, hdr, data, length, dp->context); - return dp->on_pe_set != NULL; - - case MIDI2_CI_PE_SET_REPLY: - dispatch_pe_data(dp->on_pe_set_reply, hdr, data, length, dp->context); - return dp->on_pe_set_reply != NULL; - - case MIDI2_CI_PE_SUBSCRIBE: - dispatch_pe_data(dp->on_pe_subscribe, hdr, data, length, dp->context); - return dp->on_pe_subscribe != NULL; - - case MIDI2_CI_PE_SUBSCRIBE_REPLY: - dispatch_pe_data(dp->on_pe_subscribe_reply, hdr, data, length, dp->context); - return dp->on_pe_subscribe_reply != NULL; - - case MIDI2_CI_PE_NOTIFY: - dispatch_pe_data(dp->on_pe_notify, hdr, data, length, dp->context); - return dp->on_pe_notify != NULL; - - /*--- Process Inquiry ---*/ - - case MIDI2_CI_PI_CAPABILITY: { - if (!dp->on_pi_capability) break; - dp->on_pi_capability(hdr, dp->context); - return true; - } - - case MIDI2_CI_PI_CAPABILITY_REPLY: { - if (!dp->on_pi_capability_reply || length < 14) break; - dp->on_pi_capability_reply(hdr, data[13], dp->context); - return true; - } - - case MIDI2_CI_PI_MIDI_REPORT: { - if (!dp->on_pi_midi_report || length < 18) break; - dp->on_pi_midi_report(hdr, data[13], data[14], - data[16], data[17], dp->context); - return true; - } - - case MIDI2_CI_PI_MIDI_REPORT_REPLY: { - if (!dp->on_pi_midi_report_reply || length < 17) break; - dp->on_pi_midi_report_reply(hdr, data[13], data[15], data[16], dp->context); - return true; - } - - case MIDI2_CI_PI_MIDI_REPORT_END: { - if (!dp->on_pi_midi_report_end) break; - dp->on_pi_midi_report_end(hdr, dp->context); - return true; - } - - default: - break; - } - - /* Unknown or no callback registered */ - if (dp->on_unknown) { - dp->on_unknown(hdr, sub_id, data, length, dp->context); - } - return false; -} - -/* == midi2_ci (impl) ===================================================== */ - - -/* - * midi2_ci.c - MIDI-CI convenience responder implementation - * - * Part of midi2 - Portable MIDI 2.0 library (C99) - * https://github.com/sauloverissimo/midi2 - * - * Spec: MIDI-CI (M2-101-UM v1.2, Jun 2023) - * Version: 0.3.0 - */ - - -/*--------------------------------------------------------------------+ - * Init - *--------------------------------------------------------------------*/ -void midi2_ci_init_ex(midi2_ci_state *state, uint32_t muid_seed, - uint8_t (*profiles)[5], uint8_t max_profiles, - midi2_ci_property *properties, uint8_t max_properties, - midi2_ci_subscriber *subscribers, uint8_t max_subscribers) { - memset(state, 0, sizeof(midi2_ci_state)); - state->muid = muid_seed & 0x0FFFFFFF; - state->profiles = profiles; - state->profile_capacity = max_profiles; - state->properties = properties; - state->property_capacity = max_properties; - state->subscribers = subscribers; - state->subscriber_capacity = max_subscribers; - /* Clear caller-provided subscriber slots so the `in_use` sentinel starts - * zero. That field is a midi2 implementation detail, not part of the - * caller contract, so init owns it. */ - if (subscribers != NULL && max_subscribers > 0) { - memset(subscribers, 0, sizeof(midi2_ci_subscriber) * max_subscribers); - } - state->auto_invalidate_on_collision = true; /* v0.3.0+ default on */ -} - -void midi2_ci_init(midi2_ci_state *state, uint32_t muid_seed, - uint8_t (*profiles)[5], uint8_t max_profiles, - midi2_ci_property *properties, uint8_t max_properties) { - midi2_ci_init_ex(state, muid_seed, - profiles, max_profiles, - properties, max_properties, - NULL, 0); -} - -void midi2_ci_set_identity(midi2_ci_state *state, - uint32_t manufacturer_id, uint16_t family_id, - uint16_t model_id, uint32_t version_id) { - state->manufacturer_id = manufacturer_id; - state->family_id = family_id; - state->model_id = model_id; - state->version_id = version_id; -} - -void midi2_ci_set_write_fn(midi2_ci_state *state, - midi2_proc_write_fn write_fn, void *context) { - state->write_fn = write_fn; - state->write_context = context; -} - -void midi2_ci_set_rng(midi2_ci_state *state, - midi2_ci_rng_fn rng, void *context) { - state->rng = rng; - state->rng_context = context; -} - -void midi2_ci_set_nak_on_unknown(midi2_ci_state *state, bool enabled) { - state->nak_on_unknown = enabled; -} - -void midi2_ci_set_auto_invalidate_on_collision(midi2_ci_state *state, bool enabled) { - if (state == NULL) return; - state->auto_invalidate_on_collision = enabled; -} - -uint32_t midi2_ci_new_muid(midi2_ci_state *state) { - uint32_t m; - uint8_t tries = 0; - do { - if (state->rng) { - m = state->rng(state->rng_context) & 0x0FFFFFFFu; - } else { - /* Fallback: perturb current MUID. Better than returning a reserved - * value. Real devices should always install an RNG. */ - m = (state->muid * 1103515245u + 12345u) & 0x0FFFFFFFu; - } - if (++tries > 8) break; /* avoid pathological loop */ - } while (m == 0u || m == 0x0FFFFFFFu); - if (m == 0u || m == 0x0FFFFFFFu) m = 0x12345678u; /* hard fallback */ - state->muid = m; - return m; -} - -/*--------------------------------------------------------------------+ - * Profiles - *--------------------------------------------------------------------*/ -int midi2_ci_add_profile(midi2_ci_state *state, const uint8_t profile_id[5]) { - if (state->profiles == NULL) return MIDI2_CI_ERR_NULL; - if (state->profile_count >= state->profile_capacity) return MIDI2_CI_ERR_FULL; - memcpy(state->profiles[state->profile_count], profile_id, 5); - state->profile_count++; - return MIDI2_CI_OK; -} - -int midi2_ci_remove_profile(midi2_ci_state *state, const uint8_t profile_id[5]) { - uint8_t i; - for (i = 0; i < state->profile_count; i++) { - if (memcmp(state->profiles[i], profile_id, 5) == 0) { - uint8_t j; - for (j = i; j < state->profile_count - 1; j++) { - memcpy(state->profiles[j], state->profiles[j + 1], 5); - } - state->profile_count--; - return MIDI2_CI_OK; - } - } - return MIDI2_CI_ERR_NOT_FOUND; -} - -/*--------------------------------------------------------------------+ - * Properties - *--------------------------------------------------------------------*/ -int midi2_ci_add_property_static(midi2_ci_state *state, - const char *name, const char *value) { - if (state->properties == NULL) return MIDI2_CI_ERR_NULL; - if (state->property_count >= state->property_capacity) return MIDI2_CI_ERR_FULL; - state->properties[state->property_count].name = name; - state->properties[state->property_count].static_value = value; - state->properties[state->property_count].getter = NULL; - state->properties[state->property_count].setter = NULL; - state->properties[state->property_count].subscribable = false; /* v0.3.0+ */ - state->property_count++; - return MIDI2_CI_OK; -} - -int midi2_ci_add_property_dynamic(midi2_ci_state *state, - const char *name, - midi2_ci_pe_getter getter, - midi2_ci_pe_setter setter) { - if (state->properties == NULL) return MIDI2_CI_ERR_NULL; - if (state->property_count >= state->property_capacity) return MIDI2_CI_ERR_FULL; - state->properties[state->property_count].name = name; - state->properties[state->property_count].static_value = NULL; - state->properties[state->property_count].getter = getter; - state->properties[state->property_count].setter = setter; - state->properties[state->property_count].subscribable = false; /* v0.3.0+ */ - state->property_count++; - return MIDI2_CI_OK; -} - -int midi2_ci_remove_property(midi2_ci_state *state, const char *name) { - uint8_t i; - if (state == NULL || name == NULL) return MIDI2_CI_ERR_NOT_FOUND; - for (i = 0; i < state->property_count; i++) { - if (state->properties[i].name != NULL - && strcmp(state->properties[i].name, name) == 0) { - uint8_t j; - for (j = i; j + 1 < state->property_count; j++) { - state->properties[j] = state->properties[j + 1]; - } - state->property_count--; - return MIDI2_CI_OK; - } - } - return MIDI2_CI_ERR_NOT_FOUND; -} - -void midi2_ci_reset_profiles(midi2_ci_state *state) { - if (state == NULL) return; - state->profile_count = 0; -} - -void midi2_ci_reset_properties(midi2_ci_state *state) { - if (state == NULL) return; - state->property_count = 0; -} - -/*--------------------------------------------------------------------+ - * Subscribe / Notify (v0.3.0) - * - * Registry is caller-provided (state->subscribers). Each slot carries - * a stable 36-char copy of the resource name so the responder does - * not depend on app-owned string lifetimes. - *--------------------------------------------------------------------*/ -static int find_property_idx(const midi2_ci_state *state, const char *name) { - uint8_t i; - if (state == NULL || name == NULL) return -1; - for (i = 0; i < state->property_count; i++) { - if (state->properties[i].name != NULL - && strcmp(state->properties[i].name, name) == 0) { - return (int)i; - } - } - return -1; -} - -static int find_subscriber_idx(const midi2_ci_state *state, uint32_t muid, - const char *name) { - uint8_t i; - if (state == NULL || state->subscribers == NULL || name == NULL) return -1; - for (i = 0; i < state->subscriber_capacity; i++) { - if (!state->subscribers[i].in_use) continue; - if (state->subscribers[i].caller_muid != muid) continue; - if (strncmp(state->subscribers[i].name_copy, name, 36) != 0) continue; - return (int)i; - } - return -1; -} - -int midi2_ci_pe_set_subscribable(midi2_ci_state *state, const char *name, - bool subscribable) { - int idx = find_property_idx(state, name); - if (idx < 0) return MIDI2_CI_ERR_NOT_FOUND; - state->properties[idx].subscribable = subscribable; - return MIDI2_CI_OK; -} - -int midi2_ci_subscribe_add(midi2_ci_state *state, uint32_t caller_muid, - const char *resource_name) { - uint8_t i; - int pi; - size_t n; - if (state == NULL || resource_name == NULL) return MIDI2_CI_ERR_NOT_FOUND; - if (state->subscribers == NULL) return MIDI2_CI_ERR_FULL; - pi = find_property_idx(state, resource_name); - if (pi < 0) return MIDI2_CI_ERR_NOT_FOUND; - if (!state->properties[pi].subscribable) return MIDI2_CI_ERR_NOT_FOUND; - if (find_subscriber_idx(state, caller_muid, resource_name) >= 0) { - return MIDI2_CI_OK; /* idempotent duplicate */ - } - for (i = 0; i < state->subscriber_capacity; i++) { - if (state->subscribers[i].in_use) continue; - state->subscribers[i].caller_muid = caller_muid; - n = strlen(resource_name); - if (n > 36) n = 36; - memcpy(state->subscribers[i].name_copy, resource_name, n); - state->subscribers[i].name_copy[n] = '\0'; - state->subscribers[i].in_use = 1; - state->subscriber_count++; - return MIDI2_CI_OK; - } - return MIDI2_CI_ERR_FULL; -} - -int midi2_ci_subscribe_remove(midi2_ci_state *state, uint32_t caller_muid, - const char *resource_name) { - int idx = find_subscriber_idx(state, caller_muid, resource_name); - if (idx < 0) return MIDI2_CI_ERR_NOT_FOUND; - state->subscribers[idx].in_use = 0; - state->subscribers[idx].caller_muid = 0; - state->subscribers[idx].name_copy[0] = '\0'; - state->subscriber_count--; - return MIDI2_CI_OK; -} - -uint8_t midi2_ci_get_subscriber_count(const midi2_ci_state *state) { - return (state == NULL) ? 0u : state->subscriber_count; -} - -/*--------------------------------------------------------------------+ - * Internal: send SysEx via write function - *--------------------------------------------------------------------*/ -static void ci_send(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (state->write_fn) { - midi2_proc_send_sysex7(group, data, length, state->write_fn, state->write_context); - } -} - -/*--------------------------------------------------------------------+ - * PE Notify fan-out (v0.3.0) - * - * Walks the subscriber registry and, for every slot matching the - * resource name, emits a PE Notify frame carrying a minimal JSON - * header `{"resource":""}`. The actual property value is not - * embedded; consumers issue a PE Get to fetch the new value. Matches - * the common M2-103 pattern for PE where Notify signals invalidation. - *--------------------------------------------------------------------*/ -int midi2_ci_notify_property_changed(midi2_ci_state *state, - const char *resource_name) { - /* JSON header template `{"resource":""}`. is bounded by - * MIDI2_CI_RESOURCE_NAME_MAX (36 per M2-105) and stored in the - * subscriber's name_copy slot, so the worst-case header is - * 13 (prefix) + 36 (name) + 2 (suffix) = 51 bytes. Buffer of 64 is - * comfortable. No dependency. */ - static const char HDR_PREFIX[] = "{\"resource\":\""; - static const char HDR_SUFFIX[] = "\"}"; - uint8_t i; - int pi; - if (state == NULL || resource_name == NULL) return MIDI2_CI_ERR_NOT_FOUND; - pi = find_property_idx(state, resource_name); - if (pi < 0) return MIDI2_CI_ERR_NOT_FOUND; - if (state->write_fn == NULL || state->subscribers == NULL) return MIDI2_CI_OK; - - for (i = 0; i < state->subscriber_capacity; i++) { - if (!state->subscribers[i].in_use) continue; - if (strncmp(state->subscribers[i].name_copy, resource_name, 36) != 0) continue; - - uint8_t frame[128]; - uint8_t hdr[64]; - uint16_t hdr_n = 0; - size_t name_len = strlen(state->subscribers[i].name_copy); - if (name_len > 36u) name_len = 36u; - - memcpy(hdr + hdr_n, HDR_PREFIX, sizeof HDR_PREFIX - 1u); - hdr_n = (uint16_t)(hdr_n + (sizeof HDR_PREFIX - 1u)); - memcpy(hdr + hdr_n, state->subscribers[i].name_copy, name_len); - hdr_n = (uint16_t)(hdr_n + name_len); - memcpy(hdr + hdr_n, HDR_SUFFIX, sizeof HDR_SUFFIX - 1u); - hdr_n = (uint16_t)(hdr_n + (sizeof HDR_SUFFIX - 1u)); - - uint16_t frame_n = midi2_ci_build_pe_notify( - frame, MIDI2_CI_VERSION_1, - state->muid, - state->subscribers[i].caller_muid, - 0 /* request_id */, - hdr, hdr_n, - 1, 1, - NULL, 0); - if (frame_n == 0) continue; - ci_send(state, 0 /* group */, frame, frame_n); - } - return MIDI2_CI_OK; -} - -/*--------------------------------------------------------------------+ - * Discovery Reply -- uses midi2_ci_build_discovery_reply - *--------------------------------------------------------------------*/ -static bool ci_handle_discovery(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (length < 13 || state->manufacturer_id == 0) return false; - - uint32_t src_muid = midi2_ci_get_src_muid(data); - /* Declare every CI Category the convenience responder actually handles. - * The handlers for Profile Inquiry, PE, and PI Capability all live here; - * Discovery must advertise them so Initiators know to send the related - * inquiries. Bug present through v0.2.3: Process Inquiry was handled but - * not announced. */ - uint8_t ci_cat = MIDI2_CI_CAT_PROFILE_CONFIG - | MIDI2_CI_CAT_PROPERTY_EXCHANGE - | MIDI2_CI_CAT_PROCESS_INQUIRY; - - uint8_t reply[32]; - uint16_t reply_len = midi2_ci_build_discovery_reply( - reply, MIDI2_CI_VERSION_1, state->muid, src_muid, - state->manufacturer_id, state->family_id, state->model_id, - state->version_id, ci_cat, 512, 0, 0x7F); - - ci_send(state, group, reply, reply_len); - return true; -} - -/*--------------------------------------------------------------------+ - * Profile Inquiry Reply -- uses midi2_ci_build_profile_inquiry_reply - *--------------------------------------------------------------------*/ -static void ci_handle_profile_inquiry(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (length < 13) return; - - uint32_t src_muid = midi2_ci_get_src_muid(data); - - uint8_t reply[256]; - uint16_t reply_len = midi2_ci_build_profile_inquiry_reply( - reply, MIDI2_CI_VERSION_1, state->muid, src_muid, - midi2_ci_get_device_id(data), - (const uint8_t (*)[5])state->profiles, state->profile_count, - NULL, 0); - - ci_send(state, group, reply, reply_len); -} - -/*--------------------------------------------------------------------+ - * PE Capability Reply -- uses midi2_ci_build_pe_capability_reply - * - * Parallels Profile Inquiry and PI Capability: without this, an - * Initiator asking "do you do PE?" gets no answer and never tries - * PE GET/SET. Advertises max_simultaneous=1 and PE v1.0 (basic). - *--------------------------------------------------------------------*/ -static void ci_handle_pe_capability(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (length < 13) return; - - uint32_t src_muid = midi2_ci_get_src_muid(data); - - uint8_t reply[24]; - uint16_t reply_len = midi2_ci_build_pe_capability_reply( - reply, MIDI2_CI_VERSION_2, state->muid, src_muid, - /*max_simultaneous*/ 1, - /*pe_ver_major*/ 1, - /*pe_ver_minor*/ 0); - - ci_send(state, group, reply, reply_len); -} - -/*--------------------------------------------------------------------+ - * PE Get handler -- uses midi2_ci_build_pe_get_reply - *--------------------------------------------------------------------*/ -static void ci_handle_pe_get(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (length < 14 || state->property_count == 0) return; - - uint32_t src_muid = midi2_ci_get_src_muid(data); - uint8_t request_id = midi2_ci_get_pe_request_id(data); - - uint8_t i; - for (i = 0; i < state->property_count; i++) { - const char *value = NULL; - if (state->properties[i].getter) { - value = state->properties[i].getter(state->properties[i].name, state->context); - } else { - value = state->properties[i].static_value; - } - - if (value != NULL) { - uint16_t val_len = 0; - while (value[val_len] && val_len < 30) val_len++; - - uint8_t reply[64]; - uint16_t reply_len = midi2_ci_build_pe_get_reply( - reply, MIDI2_CI_VERSION_1, state->muid, src_muid, - request_id, NULL, 0, 1, 1, - (const uint8_t *)value, val_len); - - ci_send(state, group, reply, reply_len); - return; - } - } -} - -/*--------------------------------------------------------------------+ - * PE Set handler -- uses midi2_ci_build_pe_set_reply - *--------------------------------------------------------------------*/ -static void ci_handle_pe_set(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (length < 14 || state->property_count == 0) return; - - uint32_t src_muid = midi2_ci_get_src_muid(data); - uint8_t request_id = midi2_ci_get_pe_request_id(data); - uint8_t i; - - for (i = 0; i < state->property_count; i++) { - if (state->properties[i].setter) { - bool ok = state->properties[i].setter(state->properties[i].name, "", state->context); - - uint8_t reply[32]; - uint16_t reply_len = midi2_ci_build_pe_set_reply( - reply, MIDI2_CI_VERSION_1, state->muid, src_muid, - request_id, NULL, 0); - (void)ok; /* simplified: always send reply */ - - ci_send(state, group, reply, reply_len); - return; - } - } -} - -/*--------------------------------------------------------------------+ - * Invalidate MUID handler (M2-101-UM §3.5 + Appendix E) - * - * If the message's target MUID matches ours, regenerate it via the - * installed RNG. Without an RNG the request is silently ignored (v0.2.3 - * behavior preserved). - *--------------------------------------------------------------------*/ -static void ci_handle_invalidate_muid(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - (void)group; - if (length < 17) return; - if (!state->rng) return; - uint32_t target = midi2_ci_get_target_muid(data); - if (target != state->muid) return; - midi2_ci_new_muid(state); -} - -/*--------------------------------------------------------------------+ - * MUID collision detection (M2-101-UM Appendix E §2) - * - * Any inbound CI message whose src_muid matches ours means a peer is - * using our MUID. Resolution: pick a new MUID and broadcast Invalidate - * for the old value. No-op without an RNG. - *--------------------------------------------------------------------*/ -static void ci_check_muid_collision(midi2_ci_state *state, uint8_t group, - uint32_t peer_src_muid) { - if (!state->rng) return; - if (peer_src_muid != state->muid) return; - uint32_t old = state->muid; - midi2_ci_new_muid(state); - if (!state->write_fn) return; - if (!state->auto_invalidate_on_collision) return; - uint8_t buf[24]; - uint16_t len = midi2_ci_build_invalidate_muid( - buf, MIDI2_CI_VERSION_1, state->muid, old); - ci_send(state, group, buf, len); -} - -/*--------------------------------------------------------------------+ - * NAK builder (M2-101-UM Appendix E) - * - * Build a Sub-ID#2 0x7F NAK with status NOT_SUPPORTED for a given - * original sub-id. Used when nak_on_unknown is enabled. - *--------------------------------------------------------------------*/ -static void ci_send_nak_not_supported(midi2_ci_state *state, uint8_t group, - const uint8_t *data, - uint8_t orig_sub_id) { - if (!state->write_fn) return; - uint32_t src_muid = midi2_ci_get_src_muid(data); - uint8_t device_id = midi2_ci_get_device_id(data); - uint8_t buf[32]; - uint16_t len = midi2_ci_build_nak( - buf, MIDI2_CI_VERSION_2, state->muid, src_muid, device_id, - orig_sub_id, MIDI2_CI_NAK_NOT_SUPPORTED, 0, - NULL, 0, NULL); - ci_send(state, group, buf, len); -} - -/*--------------------------------------------------------------------+ - * Process Inquiry handler -- uses midi2_ci_build_pi_capability_reply - *--------------------------------------------------------------------*/ -static void ci_handle_process_inquiry(midi2_ci_state *state, uint8_t group, - const uint8_t *data, uint16_t length) { - if (length < 13) return; - - uint32_t src_muid = midi2_ci_get_src_muid(data); - - uint8_t reply[16]; - uint16_t reply_len = midi2_ci_build_pi_capability_reply( - reply, MIDI2_CI_VERSION_1, state->muid, src_muid, 0x01); - - ci_send(state, group, reply, reply_len); -} - -/*--------------------------------------------------------------------+ - * Process incoming SysEx - *--------------------------------------------------------------------*/ -bool midi2_ci_process_sysex(midi2_ci_state *state, - uint8_t group, const uint8_t *data, uint16_t length) { - if (!midi2_ci_is_ci(data, length)) return false; - - /* Every inbound CI message is an opportunity to detect MUID collisions. - * Do this before dispatching so a reply (if any) carries the new MUID. */ - ci_check_muid_collision(state, group, midi2_ci_get_src_muid(data)); - - uint8_t sub_id = midi2_ci_get_sub_id(data); - switch (sub_id) { - case MIDI2_CI_DISCOVERY: - return ci_handle_discovery(state, group, data, length); - - case MIDI2_CI_INVALIDATE_MUID: - ci_handle_invalidate_muid(state, group, data, length); - return true; - - case MIDI2_CI_PROFILE_INQUIRY: - ci_handle_profile_inquiry(state, group, data, length); - return true; - - case MIDI2_CI_PE_CAPABILITY: - ci_handle_pe_capability(state, group, data, length); - return true; - - case MIDI2_CI_PE_GET: - ci_handle_pe_get(state, group, data, length); - return true; - - case MIDI2_CI_PE_SET: - ci_handle_pe_set(state, group, data, length); - return true; - - case MIDI2_CI_PI_CAPABILITY: - ci_handle_process_inquiry(state, group, data, length); - return true; - - default: - /* Appendix E: "Be able to send a NAK message when appropriate." - * When nak_on_unknown is set, reply with Sub-ID#2 0x7F - * status NOT_SUPPORTED. Otherwise the v0.2.3 behavior (silent - * fall-through to return false) is preserved. */ - if (state->nak_on_unknown) { - ci_send_nak_not_supported(state, group, data, sub_id); - return true; - } - return false; - } -} - -#endif /* MIDI2_IMPLEMENTATION */ - -#ifdef __cplusplus -} -#endif - -#endif /* MIDI2_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b6e944d..9415b33 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,3 +19,4 @@ add_midi2_cpp_test(test_midi2_flex) add_midi2_cpp_test(test_midi2_conversion) add_midi2_cpp_test(test_midi2_ci) add_midi2_cpp_test(test_midi2_host) +add_midi2_cpp_test(test_midi2_bridge) diff --git a/tests/test_midi2_bridge.cpp b/tests/test_midi2_bridge.cpp new file mode 100644 index 0000000..e264128 --- /dev/null +++ b/tests/test_midi2_bridge.cpp @@ -0,0 +1,320 @@ +// tests/test_midi2_bridge.cpp -- m2bridge smoke + group rewrite + heap balance. +// +// Bridge is the only midi2_cpp class that allocates state on the heap +// (BridgeState in begin's predecessor; ByteStreamConverter slots inside +// begin). Running this suite under ASan + UBSan is what catches the +// allocate-without-free regressions m2bridge could otherwise hide. +#include "test_common.h" +#include "midi2_cpp.h" + +#include +#include + +uint32_t g_captured_tx[CAPTURE_MAX] = {0}; +size_t g_captured_tx_len = 0; +uint32_t g_test_now_ms = 0; + +using namespace midi2; + +// Bridge has its own write-fn signatures (size_t return; upstream is +// slot-aware). Local capture wrappers feed test_common's TX buffer. +static size_t bridge_capture_downstream(const uint32_t* w, size_t n) { + capture_write(w, n); + return n; +} + +static uint8_t g_last_upstream_idx = 0xFF; +static uint32_t g_upstream_tx[CAPTURE_MAX] = {0}; +static size_t g_upstream_tx_len = 0; + +static size_t bridge_capture_upstream(uint8_t idx, const uint32_t* w, size_t n) { + g_last_upstream_idx = idx; + for (size_t i = 0; i < n && g_upstream_tx_len < CAPTURE_MAX; ++i) + g_upstream_tx[g_upstream_tx_len++] = w[i]; + return n; +} + +static void upstream_reset() { + g_last_upstream_idx = 0xFF; + g_upstream_tx_len = 0; + std::memset(g_upstream_tx, 0, sizeof(g_upstream_tx)); +} + +// Helper: build an MT 0x4 NoteOn UMP into a 2-word buffer. +// word0 = 0x4|G|0x9|CH | NN | 0 +// word1 = VEL16 << 16 +static void make_note_on(uint32_t* w, uint8_t group, uint8_t ch, + uint8_t note, uint16_t vel16) { + w[0] = (uint32_t)(0x4u << 28) + | ((uint32_t)(group & 0x0F) << 24) + | (uint32_t)(0x9u << 20) + | ((uint32_t)(ch & 0x0F) << 16) + | ((uint32_t)note << 8); + w[1] = (uint32_t)vel16 << 16; +} + +// --------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------- + +static void test_bridge_constructs_clean(void) { + TEST("Bridge default-constructs with sane topology defaults"); + m2bridge br; + CHECK_EQ(br.numSlots(), m2bridge::MAX_SLOTS, "numSlots starts at MAX_SLOTS"); + CHECK_EQ(br.groupsPerSlot(), 4u, "groupsPerSlot defaults to 4"); + PASS(); +} + +static void test_bridge_destruct_balanced(void) { + TEST("Bridge construct + begin + destruct is heap-balanced"); + { + m2bridge br; + br.setNumSlots(4); + br.setGroupsPerSlot(4); + br.setDownstreamWriteFn(bridge_capture_downstream); + br.setUpstreamWriteFn(bridge_capture_upstream); + br.setNowFn(test_now_fn); + br.setRngFn([] { return 0xDEADBEEFu; }); + br.begin(); + br.task(); + } // dtor must release BridgeState + 4 ByteStreamConverters. + PASS(); +} + +static void test_bridge_topology_setters_respect_bounds(void) { + TEST("setNumSlots / setGroupsPerSlot reject out-of-range"); + m2bridge br; + uint8_t before_slots = br.numSlots(); + uint8_t before_gps = br.groupsPerSlot(); + + br.setNumSlots(0); // rejected + CHECK_EQ(br.numSlots(), before_slots, "numSlots(0) ignored"); + br.setNumSlots(m2bridge::MAX_SLOTS + 1); // rejected + CHECK_EQ(br.numSlots(), before_slots, "numSlots(>MAX) ignored"); + br.setNumSlots(2); + CHECK_EQ(br.numSlots(), 2u, "numSlots(2) accepted"); + + br.setGroupsPerSlot(0); // rejected + CHECK_EQ(br.groupsPerSlot(), before_gps, "groupsPerSlot(0) ignored"); + br.setGroupsPerSlot(17); // rejected + CHECK_EQ(br.groupsPerSlot(), before_gps, "groupsPerSlot(17) ignored"); + br.setGroupsPerSlot(8); + CHECK_EQ(br.groupsPerSlot(), 8u, "groupsPerSlot(8) accepted"); + PASS(); +} + +static void test_bridge_begin_requires_write_fns(void) { + TEST("Bridge::begin is a no-op when write fns are missing"); + m2bridge br; + // Intentionally do NOT call setDownstreamWriteFn / setUpstreamWriteFn. + br.begin(); + // Topology must remain mutable post-no-op begin (begun stayed false). + br.setNumSlots(3); + CHECK_EQ(br.numSlots(), 3u, "numSlots still mutable after no-op begin"); + PASS(); +} + +static void test_bridge_topology_locks_after_begin(void) { + TEST("Topology setters are locked once begin() succeeds"); + m2bridge br; + br.setNumSlots(4); + br.setGroupsPerSlot(4); + br.setDownstreamWriteFn(bridge_capture_downstream); + br.setUpstreamWriteFn(bridge_capture_upstream); + br.setNowFn(test_now_fn); + br.begin(); + br.setNumSlots(2); // ignored + CHECK_EQ(br.numSlots(), 4u, "numSlots locked at 4 after begin"); + br.setGroupsPerSlot(8); // ignored + CHECK_EQ(br.groupsPerSlot(), 4u, "groupsPerSlot locked at 4 after begin"); + PASS(); +} + +// --------------------------------------------------------------------- +// Group rewrite +// --------------------------------------------------------------------- +// +// Implementation rule (forward_ump_to_pc): +// base = idx * groupsPerSlot +// out_group = base + (in_group % groupsPerSlot) +// +// MT 0x0 / 0xE / 0xF are skipped (utility, reserved, stream). + +static void make_bridge(m2bridge& br) { + br.setNumSlots(4); + br.setGroupsPerSlot(4); + br.setDownstreamWriteFn(bridge_capture_downstream); + br.setUpstreamWriteFn(bridge_capture_upstream); + br.setNowFn(test_now_fn); + br.setRngFn([] { return 0xCAFEBABEu; }); + br.begin(); + br.setDeviceMounted(true); + br.setDeviceAltSetting(1); +} + +// Find a NoteOn (MT 0x4, status 0x9X) in the captured TX. Stream/JR +// traffic from begin/slotSetActive can also land in the buffer, so the +// test scans rather than peeking [0]. +static bool find_first_note_on(uint8_t* out_group) { + for (size_t i = 0; i < g_captured_tx_len; ++i) { + uint8_t mt = (uint8_t)((g_captured_tx[i] >> 28) & 0x0F); + uint8_t status = (uint8_t)((g_captured_tx[i] >> 20) & 0x0F); + if (mt == 0x4 && status == 0x9) { + *out_group = (uint8_t)((g_captured_tx[i] >> 24) & 0x0F); + return true; + } + } + return false; +} + +static void test_bridge_group_rewrite_slot0(void) { + TEST("Group rewrite slot 0 (base=0): in_group 7 -> out_group 3"); + capture_reset(); + upstream_reset(); + m2bridge br; + make_bridge(br); + br.slotSetActive(0, true, 1); + capture_reset(); // discard FB Info / FB Name from slotSetActive + + uint32_t note_on[2]; + make_note_on(note_on, /*group*/ 7, /*ch*/ 0, /*note*/ 60, /*vel16*/ 0xFFFF); + br.feedHostRx(0, note_on, 2); + + uint8_t fwd_group = 0xFF; + CHECK(find_first_note_on(&fwd_group), "NoteOn appears on downstream"); + CHECK_EQ(fwd_group, 3u, "group 7 % 4 = 3, base 0 -> 3"); + PASS(); +} + +static void test_bridge_group_rewrite_slot1(void) { + TEST("Group rewrite slot 1 (base=4): in_group 0 -> out_group 4"); + capture_reset(); + upstream_reset(); + m2bridge br; + make_bridge(br); + br.slotSetActive(1, true, 1); + capture_reset(); + + uint32_t note_on[2]; + make_note_on(note_on, /*group*/ 0, /*ch*/ 5, /*note*/ 64, /*vel16*/ 0x8000); + br.feedHostRx(1, note_on, 2); + + uint8_t fwd_group = 0xFF; + CHECK(find_first_note_on(&fwd_group), "NoteOn appears on downstream"); + CHECK_EQ(fwd_group, 4u, "base 4 + (0 % 4) = 4"); + PASS(); +} + +static void test_bridge_group_rewrite_slot3_max(void) { + TEST("Group rewrite slot 3 (base=12): in_group 11 -> out_group 15"); + capture_reset(); + upstream_reset(); + m2bridge br; + make_bridge(br); + br.slotSetActive(3, true, 1); + capture_reset(); + + uint32_t note_on[2]; + make_note_on(note_on, /*group*/ 11, /*ch*/ 0, /*note*/ 36, /*vel16*/ 0xC000); + br.feedHostRx(3, note_on, 2); + + uint8_t fwd_group = 0xFF; + CHECK(find_first_note_on(&fwd_group), "NoteOn appears on downstream"); + CHECK_EQ(fwd_group, 15u, "base 12 + (11 % 4) = 15"); + PASS(); +} + +static void test_bridge_drops_out_of_range_slot(void) { + TEST("feedHostRx with idx >= numSlots is a no-op"); + capture_reset(); + upstream_reset(); + m2bridge br; + make_bridge(br); + capture_reset(); + + uint32_t note_on[2]; + make_note_on(note_on, 0, 0, 60, 0xFFFF); + br.feedHostRx(/*idx*/ 99, note_on, 2); + + uint8_t fwd_group = 0xFF; + CHECK(!find_first_note_on(&fwd_group), "no NoteOn forwarded for idx 99"); + PASS(); +} + +// --------------------------------------------------------------------- +// MIDI 1.0 byte-stream uplift (alt 0) +// --------------------------------------------------------------------- + +static void test_bridge_midi1_bytes_become_mt2_in_slot_window(void) { + TEST("feedHostMidi1Bytes uplifts USB-MIDI 1.0 packets to MT 0x2 UMPs"); + capture_reset(); + upstream_reset(); + m2bridge br; + make_bridge(br); + br.slotSetActive(2, true, /*alt*/ 0); // legacy upstream + capture_reset(); + + // USB-MIDI 1.0 packet: CIN 0x9 (NoteOn), CN 0, status/data triple. + // base for slot 2 with groupsPerSlot=4 is 8, so the resulting MT 0x2 + // UMP must land in group 8. + const uint8_t pkt[4] = {0x09, 0x90, 0x3C, 0x40}; // ch=0 note=60 vel=64 + br.feedHostMidi1Bytes(2, pkt, sizeof(pkt)); + + bool found = false; + uint8_t group = 0xFF; + for (size_t i = 0; i < g_captured_tx_len; ++i) { + uint8_t mt = (uint8_t)((g_captured_tx[i] >> 28) & 0x0F); + uint8_t status = (uint8_t)((g_captured_tx[i] >> 20) & 0x0F); + if (mt == 0x2 && status == 0x9) { + group = (uint8_t)((g_captured_tx[i] >> 24) & 0x0F); + found = true; + break; + } + } + CHECK(found, "MT 0x2 NoteOn emitted by ByteStreamConverter"); + CHECK_EQ(group, 8u, "MT 0x2 lands in slot 2's first group (= 8)"); + PASS(); +} + +// --------------------------------------------------------------------- +// Stress: heap balance under repeated lifecycles +// --------------------------------------------------------------------- + +static void test_bridge_repeated_construct_destroy(void) { + TEST("50x construct/begin/destroy cycles are heap-balanced"); + for (int i = 0; i < 50; ++i) { + m2bridge br; + br.setNumSlots(4); + br.setGroupsPerSlot(4); + br.setDownstreamWriteFn(bridge_capture_downstream); + br.setUpstreamWriteFn(bridge_capture_upstream); + br.setNowFn(test_now_fn); + br.setRngFn([] { return 0xAAAAAAAAu; }); + br.begin(); + br.task(); + } + PASS(); +} + +// --------------------------------------------------------------------- + +int main(void) { + std::printf("\n[m2bridge]\n"); + + test_bridge_constructs_clean(); + test_bridge_destruct_balanced(); + test_bridge_topology_setters_respect_bounds(); + test_bridge_begin_requires_write_fns(); + test_bridge_topology_locks_after_begin(); + + test_bridge_group_rewrite_slot0(); + test_bridge_group_rewrite_slot1(); + test_bridge_group_rewrite_slot3_max(); + test_bridge_drops_out_of_range_slot(); + + test_bridge_midi1_bytes_become_mt2_in_slot_window(); + + test_bridge_repeated_construct_destroy(); + + REPORT_AND_EXIT(); +}