Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 105 additions & 32 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
30 changes: 27 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
Expand Down
28 changes: 9 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
```
Expand All @@ -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

Expand Down Expand Up @@ -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.

Expand Down
Binary file added architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 18 additions & 5 deletions examples/adafruit-feather-rp2040-bridge-midi2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
20 changes: 19 additions & 1 deletion examples/adafruit-feather-rp2040-host-midi2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/arduino-nano-esp32-midi2/idf/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions examples/arduino-nano-esp32-midi2/idf/main/idf_component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 5 additions & 4 deletions examples/esp32-c6-devkitc-multi-midi2/pio/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading