summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/device/CMakeLists.txt1
-rwxr-xr-xexamples/device/audio_4_channel_mic/src/plot_audio_samples.py49
-rw-r--r--examples/device/audio_4_channel_mic/src/usb_descriptors.c4
-rwxr-xr-xexamples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py50
-rwxr-xr-xexamples/device/audio_test/src/plot_audio_samples.py50
-rw-r--r--examples/device/audio_test/src/usb_descriptors.c4
-rwxr-xr-xexamples/device/audio_test_freertos/src/plot_audio_samples.py50
-rwxr-xr-xexamples/device/audio_test_multi_rate/src/plot_audio_samples.py50
-rw-r--r--examples/device/audio_test_multi_rate/src/usb_descriptors.c4
-rw-r--r--examples/device/cdc_msc_throughput/CMakePresets.json6
-rw-r--r--examples/device/midi2_device/CMakeLists.txt33
-rw-r--r--examples/device/midi2_device/CMakePresets.json6
-rw-r--r--examples/device/midi2_device/Makefile16
-rw-r--r--examples/device/midi2_device/README.md59
-rw-r--r--examples/device/midi2_device/skip.txt1
-rw-r--r--examples/device/midi2_device/src/main.c720
-rw-r--r--examples/device/midi2_device/src/tusb_config.h88
-rw-r--r--examples/device/midi2_device/src/usb_descriptors.c142
-rw-r--r--examples/device/mtp/src/mtp_fs_example.c44
-rw-r--r--examples/device/mtp/src/tusb_config.h1
-rw-r--r--examples/device/net_lwip_webserver/src/tusb_config.h2
-rw-r--r--examples/host/CMakeLists.txt1
-rw-r--r--examples/host/midi2_host/CMakeLists.txt29
-rw-r--r--examples/host/midi2_host/CMakePresets.json6
-rw-r--r--examples/host/midi2_host/Makefile13
-rw-r--r--examples/host/midi2_host/only.txt34
-rw-r--r--examples/host/midi2_host/skip.txt1
-rw-r--r--examples/host/midi2_host/src/main.c165
-rw-r--r--examples/host/midi2_host/src/tusb_config.h106
29 files changed, 1724 insertions, 11 deletions
diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt
index 088872711..1432b36bb 100644
--- a/examples/device/CMakeLists.txt
+++ b/examples/device/CMakeLists.txt
@@ -28,6 +28,7 @@ set(EXAMPLE_LIST
hid_multiple_interface
midi_test
midi_test_freertos
+ midi2_device
msc_dual_lun
mtp
net_lwip_webserver
diff --git a/examples/device/audio_4_channel_mic/src/plot_audio_samples.py b/examples/device/audio_4_channel_mic/src/plot_audio_samples.py
index 4d61e7f5e..618745934 100755
--- a/examples/device/audio_4_channel_mic/src/plot_audio_samples.py
+++ b/examples/device/audio_4_channel_mic/src/plot_audio_samples.py
@@ -4,6 +4,51 @@ import matplotlib.pyplot as plt
import numpy as np
import platform
+
+def find_windows_input_device(name_hint, channels, preferred_apis=None):
+ """Pick a Windows input device index from query_devices() output."""
+ preferred_apis = preferred_apis or []
+ name_hint = name_hint.lower()
+ candidates = []
+
+ for index in range(len(sd.query_devices())):
+ device_info = sd.query_devices(index)
+ max_input_channels = int(device_info.get('max_input_channels', 0))
+
+ if max_input_channels < channels:
+ continue
+
+ device_name = str(device_info.get('name', '')).lower()
+ if name_hint not in device_name:
+ continue
+
+ score = 0
+
+ # Prefer exact channel matches to avoid selecting a different stream layout.
+ if max_input_channels == channels:
+ score += 100
+ else:
+ score += 10
+
+ hostapi_index = int(device_info.get('hostapi', -1))
+ api_name = ''
+ if hostapi_index >= 0:
+ api_name = str(sd.query_hostapis(hostapi_index)).lower()
+
+ for priority, api_hint in enumerate(preferred_apis):
+ if api_hint in api_name:
+ score += 50 - priority
+ break
+
+ candidates.append((score, index))
+
+ if not candidates:
+ raise ValueError(
+ 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels)
+ )
+
+ return max(candidates, key=lambda item: item[0])[1]
+
if __name__ == '__main__':
# If you got "ValueError: No input device matching", that is because your PC name example device
@@ -14,8 +59,8 @@ if __name__ == '__main__':
duration = 1 # Duration of recording
if platform.system() == 'Windows':
- # WDM-KS is needed since there are more than one MicNode device APIs (at least in Windows)
- device = 'Microphone (MicNode_4_Ch), Windows WASAPI'
+ # Match by substring to support names like "Microphone (2- MicNode_4_Ch)".
+ device = find_windows_input_device('micnode_4_ch', channels=4, preferred_apis=['wasapi', 'wdm-ks', 'mme'])
elif platform.system() == 'Darwin':
device = 'MicNode_4_Ch'
else:
diff --git a/examples/device/audio_4_channel_mic/src/usb_descriptors.c b/examples/device/audio_4_channel_mic/src/usb_descriptors.c
index 2380ea0ae..6b9a9bbae 100644
--- a/examples/device/audio_4_channel_mic/src/usb_descriptors.c
+++ b/examples/device/audio_4_channel_mic/src/usb_descriptors.c
@@ -95,6 +95,10 @@ enum
// Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering
#define EPNUM_AUDIO 0x0A
+#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V307)
+ // Only EP3 is available for ISO
+ #define EPNUM_AUDIO 0x03
+
#else
#define EPNUM_AUDIO 0x01
#endif
diff --git a/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py b/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py
index 4d5ca28d6..3b3cd0d83 100755
--- a/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py
+++ b/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py
@@ -4,6 +4,52 @@ import matplotlib.pyplot as plt
import numpy as np
import platform
+
+def find_windows_input_device(name_hint, channels, preferred_apis=None):
+ """Pick a Windows input device index from query_devices() output."""
+ preferred_apis = preferred_apis or []
+ name_hint = name_hint.lower()
+ candidates = []
+
+ for index in range(len(sd.query_devices())):
+ device_info = sd.query_devices(index)
+ max_input_channels = int(device_info.get('max_input_channels', 0))
+
+ if max_input_channels < channels:
+ continue
+
+ device_name = str(device_info.get('name', '')).lower()
+ if name_hint not in device_name:
+ continue
+
+ score = 0
+
+ # Prefer exact channel matches to avoid selecting a different stream layout.
+ if max_input_channels == channels:
+ score += 100
+ else:
+ score += 10
+
+ hostapi_index = int(device_info.get('hostapi', -1))
+ api_name = ''
+ if hostapi_index >= 0:
+ hostapi_info = sd.query_hostapis(hostapi_index)
+ api_name = str(hostapi_info.get('name', '')).lower()
+
+ for priority, api_hint in enumerate(preferred_apis):
+ if api_hint in api_name:
+ score += 50 - priority
+ break
+
+ candidates.append((score, index))
+
+ if not candidates:
+ raise ValueError(
+ 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels)
+ )
+
+ return max(candidates, key=lambda item: item[0])[1]
+
if __name__ == '__main__':
# If you got "ValueError: No input device matching", that is because your PC name example device
@@ -14,8 +60,8 @@ if __name__ == '__main__':
duration = 100e-3 # Duration of recording
if platform.system() == 'Windows':
- # WDM-KS is needed since there are more than one MicNode device APIs (at least in Windows)
- device = 'Microphone (MicNode_4_Ch), Windows WDM-KS'
+ # Match by substring to support names like "Microphone (2- MicNode_4_Ch)".
+ device = find_windows_input_device('micnode_4_ch', channels=4, preferred_apis=['wdm-ks', 'wasapi', 'mme'])
elif platform.system() == 'Darwin':
device = 'MicNode_4_Ch'
else:
diff --git a/examples/device/audio_test/src/plot_audio_samples.py b/examples/device/audio_test/src/plot_audio_samples.py
index 2be8948ea..af01b7b3e 100755
--- a/examples/device/audio_test/src/plot_audio_samples.py
+++ b/examples/device/audio_test/src/plot_audio_samples.py
@@ -5,6 +5,52 @@ import numpy as np
import platform
import csv
+
+def find_windows_input_device(name_hint, channels, preferred_apis=None):
+ """Pick a Windows input device index from query_devices() output."""
+ preferred_apis = preferred_apis or []
+ name_hint = name_hint.lower()
+ candidates = []
+
+ for index in range(len(sd.query_devices())):
+ device_info = sd.query_devices(index)
+ max_input_channels = int(device_info.get('max_input_channels', 0))
+
+ if max_input_channels < channels:
+ continue
+
+ device_name = str(device_info.get('name', '')).lower()
+ if name_hint not in device_name:
+ continue
+
+ score = 0
+
+ # Prefer exact channel matches (for example 1ch source over a 4ch source).
+ if max_input_channels == channels:
+ score += 100
+ else:
+ score += 10
+
+ hostapi_index = int(device_info.get('hostapi', -1))
+ api_name = ''
+ if hostapi_index >= 0:
+ hostapi_info = sd.query_hostapis(hostapi_index)
+ api_name = str(hostapi_info.get('name', '')).lower()
+
+ for priority, api_hint in enumerate(preferred_apis):
+ if api_hint in api_name:
+ score += 50 - priority
+ break
+
+ candidates.append((score, index))
+
+ if not candidates:
+ raise ValueError(
+ 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels)
+ )
+
+ return max(candidates, key=lambda item: item[0])[1]
+
if __name__ == '__main__':
# If you got "ValueError: No input device matching", that is because your PC name example device
@@ -15,8 +61,8 @@ if __name__ == '__main__':
duration = 3 # Duration of recording
if platform.system() == 'Windows':
- # MME is needed since there are more than one MicNode device APIs (at least in Windows)
- device = 'Microphone (MicNode), Windows WASAPI'
+ # Match by substring to support names like "Microphone (2- MicNode)".
+ device = find_windows_input_device('micnode', channels=1, preferred_apis=['wasapi', 'mme', 'wdm-ks'])
elif platform.system() == 'Darwin':
device = 'MicNode'
else:
diff --git a/examples/device/audio_test/src/usb_descriptors.c b/examples/device/audio_test/src/usb_descriptors.c
index 37ebf84d3..cea4eb8d1 100644
--- a/examples/device/audio_test/src/usb_descriptors.c
+++ b/examples/device/audio_test/src/usb_descriptors.c
@@ -95,6 +95,10 @@ enum
// Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering
#define EPNUM_AUDIO 0x0A
+#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V307)
+ // Only EP3 is available for ISO
+ #define EPNUM_AUDIO 0x03
+
#else
#define EPNUM_AUDIO 0x01
#endif
diff --git a/examples/device/audio_test_freertos/src/plot_audio_samples.py b/examples/device/audio_test_freertos/src/plot_audio_samples.py
index b6d8e824b..b6a916be4 100755
--- a/examples/device/audio_test_freertos/src/plot_audio_samples.py
+++ b/examples/device/audio_test_freertos/src/plot_audio_samples.py
@@ -4,6 +4,52 @@ import matplotlib.pyplot as plt
import numpy as np
import platform
+
+def find_windows_input_device(name_hint, channels, preferred_apis=None):
+ """Pick a Windows input device index from query_devices() output."""
+ preferred_apis = preferred_apis or []
+ name_hint = name_hint.lower()
+ candidates = []
+
+ for index in range(len(sd.query_devices())):
+ device_info = sd.query_devices(index)
+ max_input_channels = int(device_info.get('max_input_channels', 0))
+
+ if max_input_channels < channels:
+ continue
+
+ device_name = str(device_info.get('name', '')).lower()
+ if name_hint not in device_name:
+ continue
+
+ score = 0
+
+ # Prefer exact channel matches (for example 1ch source over a 4ch source).
+ if max_input_channels == channels:
+ score += 100
+ else:
+ score += 10
+
+ hostapi_index = int(device_info.get('hostapi', -1))
+ api_name = ''
+ if hostapi_index >= 0:
+ hostapi_info = sd.query_hostapis(hostapi_index)
+ api_name = str(hostapi_info.get('name', '')).lower()
+
+ for priority, api_hint in enumerate(preferred_apis):
+ if api_hint in api_name:
+ score += 50 - priority
+ break
+
+ candidates.append((score, index))
+
+ if not candidates:
+ raise ValueError(
+ 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels)
+ )
+
+ return max(candidates, key=lambda item: item[0])[1]
+
if __name__ == '__main__':
# If you got "ValueError: No input device matching", that is because your PC name example device
@@ -14,8 +60,8 @@ if __name__ == '__main__':
duration = 3 # Duration of recording
if platform.system() == 'Windows':
- # MME is needed since there are more than one MicNode device APIs (at least in Windows)
- device = 'Microphone (MicNode), Windows WASAPI'
+ # Match by substring to support names like "Microphone (2- MicNode)".
+ device = find_windows_input_device('micnode', channels=1, preferred_apis=['wasapi', 'mme', 'wdm-ks'])
elif platform.system() == 'Darwin':
device = 'MicNode'
else:
diff --git a/examples/device/audio_test_multi_rate/src/plot_audio_samples.py b/examples/device/audio_test_multi_rate/src/plot_audio_samples.py
index 1f33a003e..f35cfa311 100755
--- a/examples/device/audio_test_multi_rate/src/plot_audio_samples.py
+++ b/examples/device/audio_test_multi_rate/src/plot_audio_samples.py
@@ -5,6 +5,52 @@ import numpy as np
import platform
import csv
+
+def find_windows_input_device(name_hint, channels, preferred_apis=None):
+ """Pick a Windows input device index from query_devices() output."""
+ preferred_apis = preferred_apis or []
+ name_hint = name_hint.lower()
+ candidates = []
+
+ for index in range(len(sd.query_devices())):
+ device_info = sd.query_devices(index)
+ max_input_channels = int(device_info.get('max_input_channels', 0))
+
+ if max_input_channels < channels:
+ continue
+
+ device_name = str(device_info.get('name', '')).lower()
+ if name_hint not in device_name:
+ continue
+
+ score = 0
+
+ # Prefer exact channel matches (for example 1ch source over a 4ch source).
+ if max_input_channels == channels:
+ score += 100
+ else:
+ score += 10
+
+ hostapi_index = int(device_info.get('hostapi', -1))
+ api_name = ''
+ if hostapi_index >= 0:
+ hostapi_info = sd.query_hostapis(hostapi_index)
+ api_name = str(hostapi_info.get('name', '')).lower()
+
+ for priority, api_hint in enumerate(preferred_apis):
+ if api_hint in api_name:
+ score += 50 - priority
+ break
+
+ candidates.append((score, index))
+
+ if not candidates:
+ raise ValueError(
+ 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels)
+ )
+
+ return max(candidates, key=lambda item: item[0])[1]
+
if __name__ == '__main__':
# If you got "ValueError: No input device matching", that is because your PC name example device
@@ -15,8 +61,8 @@ if __name__ == '__main__':
duration = 100e-3 # Duration of recording
if platform.system() == 'Windows':
- # MME is needed since there are more than one MicNode device APIs (at least in Windows)
- device = 'Microphone (MicNode) MME'
+ # Match by substring to support names like "Microphone (2- MicNode)".
+ device = find_windows_input_device('micnode', channels=1, preferred_apis=['mme', 'wasapi', 'wdm-ks'])
elif platform.system() == 'Darwin':
device = 'MicNode'
else:
diff --git a/examples/device/audio_test_multi_rate/src/usb_descriptors.c b/examples/device/audio_test_multi_rate/src/usb_descriptors.c
index 31333dcd3..b1f60dd10 100644
--- a/examples/device/audio_test_multi_rate/src/usb_descriptors.c
+++ b/examples/device/audio_test_multi_rate/src/usb_descriptors.c
@@ -92,6 +92,10 @@ enum {
// Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering
#define EPNUM_AUDIO 0x0A
+#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V307)
+ // Only EP3 is available for ISO
+ #define EPNUM_AUDIO 0x03
+
#else
#define EPNUM_AUDIO 0x01
#endif
diff --git a/examples/device/cdc_msc_throughput/CMakePresets.json b/examples/device/cdc_msc_throughput/CMakePresets.json
new file mode 100644
index 000000000..5cd8971e9
--- /dev/null
+++ b/examples/device/cdc_msc_throughput/CMakePresets.json
@@ -0,0 +1,6 @@
+{
+ "version": 6,
+ "include": [
+ "../../../hw/bsp/BoardPresets.json"
+ ]
+}
diff --git a/examples/device/midi2_device/CMakeLists.txt b/examples/device/midi2_device/CMakeLists.txt
new file mode 100644
index 000000000..295af6550
--- /dev/null
+++ b/examples/device/midi2_device/CMakeLists.txt
@@ -0,0 +1,33 @@
+cmake_minimum_required(VERSION 3.20)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake)
+
+project(midi2_device C CXX ASM)
+
+# Checks this example is valid for the family and initializes the project
+family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR})
+
+# Espressif has its own cmake build system
+if(FAMILY STREQUAL "espressif")
+ return()
+endif()
+
+add_executable(${PROJECT_NAME})
+
+# Example source
+target_sources(${PROJECT_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c
+ )
+
+# Example include
+target_include_directories(${PROJECT_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ )
+
+# Configure compilation flags and libraries for the example without RTOS.
+# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
+family_configure_device_example(${PROJECT_NAME} noos)
+
+# Suppress pre-existing warning in usbd.c (uint8_t comparison always true/false)
+target_compile_options(${PROJECT_NAME} PRIVATE -Wno-type-limits)
diff --git a/examples/device/midi2_device/CMakePresets.json b/examples/device/midi2_device/CMakePresets.json
new file mode 100644
index 000000000..5cd8971e9
--- /dev/null
+++ b/examples/device/midi2_device/CMakePresets.json
@@ -0,0 +1,6 @@
+{
+ "version": 6,
+ "include": [
+ "../../../hw/bsp/BoardPresets.json"
+ ]
+}
diff --git a/examples/device/midi2_device/Makefile b/examples/device/midi2_device/Makefile
new file mode 100644
index 000000000..829d9da59
--- /dev/null
+++ b/examples/device/midi2_device/Makefile
@@ -0,0 +1,16 @@
+include ../../../hw/bsp/family_support.mk
+
+INC += \
+ src \
+
+# Example source
+EXAMPLE_SOURCE += \
+ src/main.c \
+ src/usb_descriptors.c \
+
+SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE))
+
+# Suppress pre-existing warning in usbd.c
+CFLAGS_GCC += -Wno-type-limits
+
+include ../../../hw/bsp/family_rules.mk
diff --git a/examples/device/midi2_device/README.md b/examples/device/midi2_device/README.md
new file mode 100644
index 000000000..1731dba57
--- /dev/null
+++ b/examples/device/midi2_device/README.md
@@ -0,0 +1,59 @@
+# MIDI 2.0 Song Sender
+
+USB MIDI 2.0 Device example that plays "Twinkle Twinkle Little Star" using
+native UMP (Universal MIDI Packet) format with full MIDI 2.0 expression.
+
+## MIDI 2.0 Features Demonstrated
+
+- 16-bit Velocity (vs 7-bit MIDI 1.0)
+- 32-bit Control Change values
+- 32-bit Pitch Bend (vs 14-bit MIDI 1.0)
+- 32-bit Channel Pressure (Aftertouch)
+- 32-bit Poly Pressure (Per-Note Aftertouch)
+- Per-Note Management (MIDI 2.0 exclusive)
+- Program Change with Bank Select
+- JR Timestamps
+
+## USB Descriptor
+
+The device exposes both USB-MIDI 1.0 (Alt Setting 0) and USB-MIDI 2.0 (Alt Setting 1)
+as required by the USB-MIDI 2.0 specification. A MIDI 2.0 capable host (e.g. Windows
+MIDI Services) will select Alt Setting 1 for native UMP transport. Legacy hosts use
+Alt Setting 0 with automatic MIDI 1.0 fallback.
+
+## Hardware
+
+- Any RP2040 board with USB (e.g. Raspberry Pi Pico)
+- LED on GPIO 25: steady = playing, slow blink = waiting for host
+
+## Building
+
+```bash
+mkdir build && cd build
+cmake -DBOARD=raspberry_pi_pico -DPICO_SDK_FETCH_FROM_GIT=on -G Ninja ..
+cmake --build .
+```
+
+## Flashing
+
+Hold BOOTSEL, connect USB, drag `midi2_device.uf2` to the RPI-RP2 drive.
+
+## Testing
+
+**Linux:**
+```bash
+aseqdump -p "MIDI 2.0 Device"
+```
+
+**Windows (MIDI 2.0 native):**
+```powershell
+midi endpoint list
+midi endpoint monitor
+```
+
+## Song Data
+
+Twinkle Twinkle Little Star in C major, 120 BPM. Six phrases with dynamic
+shaping (pp to ff crescendo and back), pitch bend vibrato on sustained notes,
+and channel/poly pressure for expression. All values use genuine MIDI 2.0
+resolution with no 7-bit equivalent.
diff --git a/examples/device/midi2_device/skip.txt b/examples/device/midi2_device/skip.txt
new file mode 100644
index 000000000..eadb6e74a
--- /dev/null
+++ b/examples/device/midi2_device/skip.txt
@@ -0,0 +1 @@
+mcu:SAMD11
diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c
new file mode 100644
index 000000000..62741ac41
--- /dev/null
+++ b/examples/device/midi2_device/src/main.c
@@ -0,0 +1,720 @@
+/*
+ * 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.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include "bsp/board_api.h"
+#include "tusb.h"
+#include "class/midi/midi2_device.h"
+
+//--------------------------------------------------------------------+
+// MIDI 2.0 UMP Message Type Constants (M2-104-UM, Section 4)
+//--------------------------------------------------------------------+
+// Message Type (MT) occupies bits 31-28 of Word 0
+#define UMP_MT_UTILITY 0x00000000 // 32-bit: Utility (NOOP, JR Clock, JR Timestamp)
+#define UMP_MT_SYSTEM 0x10000000 // 32-bit: System Common / Real Time
+#define UMP_MT_MIDI1_CV 0x20000000 // 32-bit: MIDI 1.0 Channel Voice
+#define UMP_MT_DATA64 0x30000000 // 64-bit: Data (SysEx 7-bit)
+#define UMP_MT_MIDI2_CV 0x40000000 // 64-bit: MIDI 2.0 Channel Voice
+#define UMP_MT_DATA128 0x50000000 // 128-bit: Data (SysEx 8-bit)
+
+// MIDI 2.0 Channel Voice status (bits 23-20 of Word 0)
+#define UMP_STATUS_NOTE_OFF 0x00800000
+#define UMP_STATUS_NOTE_ON 0x00900000
+#define UMP_STATUS_POLY_PRESSURE 0x00A00000
+#define UMP_STATUS_CC 0x00B00000
+#define UMP_STATUS_PROGRAM 0x00C00000
+#define UMP_STATUS_CHAN_PRESSURE 0x00D00000
+#define UMP_STATUS_PITCH_BEND 0x00E00000
+#define UMP_STATUS_PN_MGMT 0x00F00000 // Per-Note Management
+
+// Note Attribute Types (MIDI 2.0 spec, Section 4.2.6)
+#define UMP_ATTR_NONE 0x00
+#define UMP_ATTR_MANUFACTURER 0x01
+#define UMP_ATTR_PROFILE 0x02
+#define UMP_ATTR_PITCH_7_9 0x03 // Pitch 7.9 format
+
+//--------------------------------------------------------------------+
+// MIDI 2.0 UMP Builders - Full Spec Coverage
+//--------------------------------------------------------------------+
+
+// Helper: send a 64-bit UMP (2 words)
+static inline void ump_send_64(uint32_t w0, uint32_t w1) {
+ uint32_t words[2] = { w0, w1 };
+ tud_midi2_ump_write(words, 2);
+}
+
+// Helper: send a 32-bit UMP (1 word)
+static inline void ump_send_32(uint32_t w0) {
+ tud_midi2_ump_write(&w0, 1);
+}
+
+// -- Utility Messages (MT=0x0, 32-bit) --
+
+static inline void ump_jr_timestamp(uint16_t timestamp) {
+ // Word 0: [MT(0x0) | Group(0) | Status(0x0020) | Timestamp(16-bit)]
+ ump_send_32(UMP_MT_UTILITY | 0x00200000 | (uint32_t)timestamp);
+}
+
+// -- MIDI 2.0 Channel Voice: Note On (MT=0x4, 64-bit) --
+// Word 0: [MT(4):Group(4):Status(4):Channel(4):NoteNumber(8):AttrType(8)]
+// Word 1: [Velocity(16):Attribute(16)]
+static inline void ump_note_on(uint8_t group, uint8_t channel,
+ uint8_t pitch, uint16_t velocity,
+ uint8_t attr_type, uint16_t attr_val) {
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_NOTE_ON | ((uint32_t)(channel & 0x0F) << 16)
+ | ((uint32_t)(pitch & 0x7F) << 8)
+ | (uint32_t)(attr_type & 0xFF);
+ uint32_t w1 = ((uint32_t)(velocity & 0xFFFF) << 16)
+ | (uint32_t)(attr_val & 0xFFFF);
+ ump_send_64(w0, w1);
+}
+
+// -- MIDI 2.0 Channel Voice: Note Off (MT=0x4, 64-bit) --
+static inline void ump_note_off(uint8_t group, uint8_t channel,
+ uint8_t pitch, uint16_t velocity,
+ uint8_t attr_type, uint16_t attr_val) {
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_NOTE_OFF | ((uint32_t)(channel & 0x0F) << 16)
+ | ((uint32_t)(pitch & 0x7F) << 8)
+ | (uint32_t)(attr_type & 0xFF);
+ uint32_t w1 = ((uint32_t)(velocity & 0xFFFF) << 16)
+ | (uint32_t)(attr_val & 0xFFFF);
+ ump_send_64(w0, w1);
+}
+
+// -- MIDI 2.0 Channel Voice: Control Change (MT=0x4, 64-bit) --
+// Word 0: [MT(4):Group(4):Status(0xB):Channel(4):Index(8):Reserved(8)]
+// Word 1: [Data(32)] -- full 32-bit CC resolution (vs 7-bit MIDI 1.0)
+static inline void ump_cc(uint8_t group, uint8_t channel,
+ uint8_t index, uint32_t value) {
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_CC | ((uint32_t)(channel & 0x0F) << 16)
+ | ((uint32_t)(index & 0x7F) << 8);
+ ump_send_64(w0, value);
+}
+
+// -- MIDI 2.0 Channel Voice: Program Change (MT=0x4, 64-bit) --
+// Word 0: [MT(4):Group(4):Status(0xC):Channel(4):Reserved(8):OptionFlags(8)]
+// Word 1: [Program(8):Reserved(8):BankMSB(8):BankLSB(8)]
+// OptionFlags bit 0 = Bank Valid
+static inline void ump_program_change(uint8_t group, uint8_t channel,
+ uint8_t program,
+ bool bank_valid, uint8_t bank_msb,
+ uint8_t bank_lsb) {
+ uint8_t flags = bank_valid ? 0x01 : 0x00;
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_PROGRAM | ((uint32_t)(channel & 0x0F) << 16)
+ | (uint32_t)flags;
+ uint32_t w1 = ((uint32_t)program << 24)
+ | ((uint32_t)bank_msb << 8)
+ | (uint32_t)bank_lsb;
+ ump_send_64(w0, w1);
+}
+
+// -- MIDI 2.0 Channel Voice: Pitch Bend (MT=0x4, 64-bit) --
+// Word 0: [MT(4):Group(4):Status(0xE):Channel(4):Reserved(16)]
+// Word 1: [PitchBend(32)] -- full 32-bit (vs 14-bit MIDI 1.0!)
+// 0x80000000 = center, 0x00000000 = min, 0xFFFFFFFF = max
+static inline void ump_pitch_bend(uint8_t group, uint8_t channel,
+ uint32_t value) {
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_PITCH_BEND | ((uint32_t)(channel & 0x0F) << 16);
+ ump_send_64(w0, value);
+}
+
+// -- MIDI 2.0 Channel Voice: Channel Pressure / Aftertouch (MT=0x4, 64-bit) --
+// Word 0: [MT(4):Group(4):Status(0xD):Channel(4):Reserved(16)]
+// Word 1: [Pressure(32)] -- full 32-bit (vs 7-bit MIDI 1.0)
+static inline void ump_channel_pressure(uint8_t group, uint8_t channel,
+ uint32_t pressure) {
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_CHAN_PRESSURE | ((uint32_t)(channel & 0x0F) << 16);
+ ump_send_64(w0, pressure);
+}
+
+// -- MIDI 2.0 Channel Voice: Poly Pressure / Per-Note Aftertouch --
+// Word 0: [MT(4):Group(4):Status(0xA):Channel(4):NoteNumber(8):Reserved(8)]
+// Word 1: [Pressure(32)]
+static inline void ump_poly_pressure(uint8_t group, uint8_t channel,
+ uint8_t pitch, uint32_t pressure) {
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_POLY_PRESSURE | ((uint32_t)(channel & 0x0F) << 16)
+ | ((uint32_t)(pitch & 0x7F) << 8);
+ ump_send_64(w0, pressure);
+}
+
+// -- MIDI 2.0 Channel Voice: Per-Note Management (MT=0x4, 64-bit) --
+// Exclusive to MIDI 2.0: controls per-note behavior
+// Word 0: [MT(4):Group(4):Status(0xF):Channel(4):NoteNumber(8):Flags(8)]
+// Word 1: Reserved
+// Flags bit 1 = Reset (S), bit 0 = Detach (D)
+static inline void ump_per_note_mgmt(uint8_t group, uint8_t channel,
+ uint8_t pitch, bool detach,
+ bool reset) {
+ uint8_t flags = (reset ? 0x02 : 0x00) | (detach ? 0x01 : 0x00);
+ uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24)
+ | UMP_STATUS_PN_MGMT | ((uint32_t)(channel & 0x0F) << 16)
+ | ((uint32_t)(pitch & 0x7F) << 8)
+ | (uint32_t)flags;
+ ump_send_64(w0, 0x00000000);
+}
+
+//--------------------------------------------------------------------+
+// MIDI 1.0 Channel Voice Builders (UMP MT 0x2, 32-bit)
+//--------------------------------------------------------------------+
+// Used on Alt 1 when negotiated protocol is MIDI 1.0.
+// Word layout: [MT(0x2) | Group(4b) | Status(8b) | Data1(8b) | Data2(8b)]
+// Status nibbles: 0x8=NoteOff, 0x9=NoteOn, 0xA=PolyPress, 0xB=CC,
+// 0xC=ProgChg, 0xD=ChanPress, 0xE=PitchBend.
+
+static inline void ump_midi1_send(uint8_t group, uint8_t status,
+ uint8_t data1, uint8_t data2) {
+ uint32_t w = UMP_MT_MIDI1_CV
+ | ((uint32_t)(group & 0x0F) << 24)
+ | ((uint32_t)status << 16)
+ | ((uint32_t)data1 << 8)
+ | (uint32_t)data2;
+ ump_send_32(w);
+}
+
+static inline void ump_midi1_note_on(uint8_t group, uint8_t channel,
+ uint8_t note, uint8_t vel7) {
+ ump_midi1_send(group, 0x90 | (channel & 0x0F), note & 0x7F, vel7 & 0x7F);
+}
+
+static inline void ump_midi1_note_off(uint8_t group, uint8_t channel,
+ uint8_t note, uint8_t vel7) {
+ ump_midi1_send(group, 0x80 | (channel & 0x0F), note & 0x7F, vel7 & 0x7F);
+}
+
+static inline void ump_midi1_cc(uint8_t group, uint8_t channel,
+ uint8_t cc, uint8_t val7) {
+ ump_midi1_send(group, 0xB0 | (channel & 0x0F), cc & 0x7F, val7 & 0x7F);
+}
+
+static inline void ump_midi1_program(uint8_t group, uint8_t channel,
+ uint8_t program) {
+ ump_midi1_send(group, 0xC0 | (channel & 0x0F), program & 0x7F, 0);
+}
+
+static inline void ump_midi1_pitch_bend(uint8_t group, uint8_t channel,
+ uint16_t value14) {
+ // 14-bit pitch bend: LSB first, then MSB. Center = 0x2000.
+ ump_midi1_send(group, 0xE0 | (channel & 0x0F),
+ (uint8_t)(value14 & 0x7F),
+ (uint8_t)((value14 >> 7) & 0x7F));
+}
+
+static inline void ump_midi1_channel_pressure(uint8_t group, uint8_t channel,
+ uint8_t val7) {
+ ump_midi1_send(group, 0xD0 | (channel & 0x0F), val7 & 0x7F, 0);
+}
+
+static inline void ump_midi1_poly_pressure(uint8_t group, uint8_t channel,
+ uint8_t note, uint8_t val7) {
+ ump_midi1_send(group, 0xA0 | (channel & 0x0F), note & 0x7F, val7 & 0x7F);
+}
+
+//--------------------------------------------------------------------+
+// USB-MIDI 1.0 32-bit Event Packet Builders (Alt 0 transport)
+//--------------------------------------------------------------------+
+// Used on Alt 0 (USB-MIDI 1.0). Each packet is 4 raw bytes:
+// [(Cable << 4) | CIN] [Status] [Data1] [Data2]
+// CIN = Code Index Number. See USB-MIDI 1.0 spec Section 4.
+
+static inline void midi1_pkt_send(uint8_t cable, uint8_t cin,
+ uint8_t status, uint8_t data1,
+ uint8_t data2) {
+ uint8_t packet[4] = {
+ (uint8_t)(((cable & 0x0F) << 4) | (cin & 0x0F)),
+ status, data1, data2
+ };
+ (void) tud_midi2_packet_write(packet, 1);
+}
+
+static inline void midi1_pkt_note_on(uint8_t cable, uint8_t channel,
+ uint8_t note, uint8_t vel7) {
+ midi1_pkt_send(cable, 0x9, 0x90 | (channel & 0x0F),
+ note & 0x7F, vel7 & 0x7F);
+}
+
+static inline void midi1_pkt_note_off(uint8_t cable, uint8_t channel,
+ uint8_t note, uint8_t vel7) {
+ midi1_pkt_send(cable, 0x8, 0x80 | (channel & 0x0F),
+ note & 0x7F, vel7 & 0x7F);
+}
+
+static inline void midi1_pkt_cc(uint8_t cable, uint8_t channel,
+ uint8_t cc, uint8_t val7) {
+ midi1_pkt_send(cable, 0xB, 0xB0 | (channel & 0x0F),
+ cc & 0x7F, val7 & 0x7F);
+}
+
+static inline void midi1_pkt_program(uint8_t cable, uint8_t channel,
+ uint8_t program) {
+ midi1_pkt_send(cable, 0xC, 0xC0 | (channel & 0x0F),
+ program & 0x7F, 0);
+}
+
+static inline void midi1_pkt_pitch_bend(uint8_t cable, uint8_t channel,
+ uint16_t value14) {
+ midi1_pkt_send(cable, 0xE, 0xE0 | (channel & 0x0F),
+ (uint8_t)(value14 & 0x7F),
+ (uint8_t)((value14 >> 7) & 0x7F));
+}
+
+static inline void midi1_pkt_channel_pressure(uint8_t cable, uint8_t channel,
+ uint8_t val7) {
+ midi1_pkt_send(cable, 0xD, 0xD0 | (channel & 0x0F), val7 & 0x7F, 0);
+}
+
+static inline void midi1_pkt_poly_pressure(uint8_t cable, uint8_t channel,
+ uint8_t note, uint8_t val7) {
+ midi1_pkt_send(cable, 0xA, 0xA0 | (channel & 0x0F),
+ note & 0x7F, val7 & 0x7F);
+}
+
+//--------------------------------------------------------------------+
+// Scaling Helpers (MIDI 2.0 ↔ MIDI 1.0)
+//--------------------------------------------------------------------+
+
+static inline uint8_t scale_vel16_to_vel7(uint16_t v16) { return (uint8_t)(v16 >> 9); }
+static inline uint8_t scale_val32_to_val7(uint32_t v32) { return (uint8_t)(v32 >> 25); }
+static inline uint16_t scale_pb32_to_pb14(uint32_t pb32) { return (uint16_t)(pb32 >> 18); }
+
+//--------------------------------------------------------------------+
+// Dispatch Layer - Transport + Protocol Fallback
+//--------------------------------------------------------------------+
+// Follows the same idea as the UAC examples (`tud_descriptor_configuration_cb`
+// returning UAC1 or UAC2 based on bus speed): pick the path that matches the
+// state the host put us in. Here the decision is made per message because
+// MIDI 2.0 advertises both alts in a single config descriptor; the host
+// selects via SetInterface and UMP Stream protocol negotiation.
+
+static void send_note_on(uint8_t grp, uint8_t ch, uint8_t note, uint16_t vel16) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ midi1_pkt_note_on(grp, ch, note, scale_vel16_to_vel7(vel16));
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ ump_midi1_note_on(grp, ch, note, scale_vel16_to_vel7(vel16));
+ } else {
+ ump_note_on(grp, ch, note, vel16, UMP_ATTR_NONE, 0);
+ }
+}
+
+static void send_note_off(uint8_t grp, uint8_t ch, uint8_t note, uint16_t vel16) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ midi1_pkt_note_off(grp, ch, note, scale_vel16_to_vel7(vel16));
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ ump_midi1_note_off(grp, ch, note, scale_vel16_to_vel7(vel16));
+ } else {
+ ump_note_off(grp, ch, note, vel16, UMP_ATTR_NONE, 0);
+ }
+}
+
+static void send_cc(uint8_t grp, uint8_t ch, uint8_t cc, uint32_t val32) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ midi1_pkt_cc(grp, ch, cc, scale_val32_to_val7(val32));
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ ump_midi1_cc(grp, ch, cc, scale_val32_to_val7(val32));
+ } else {
+ ump_cc(grp, ch, cc, val32);
+ }
+}
+
+static void send_program_change(uint8_t grp, uint8_t ch, uint8_t program,
+ bool with_bank, uint8_t bank_msb,
+ uint8_t bank_lsb) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ if (with_bank) {
+ midi1_pkt_cc(grp, ch, 0x00, bank_msb);
+ midi1_pkt_cc(grp, ch, 0x20, bank_lsb);
+ }
+ midi1_pkt_program(grp, ch, program);
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ if (with_bank) {
+ ump_midi1_cc(grp, ch, 0x00, bank_msb);
+ ump_midi1_cc(grp, ch, 0x20, bank_lsb);
+ }
+ ump_midi1_program(grp, ch, program);
+ } else {
+ ump_program_change(grp, ch, program, with_bank, bank_msb, bank_lsb);
+ }
+}
+
+static void send_pitch_bend(uint8_t grp, uint8_t ch, uint32_t pb32) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ midi1_pkt_pitch_bend(grp, ch, scale_pb32_to_pb14(pb32));
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ ump_midi1_pitch_bend(grp, ch, scale_pb32_to_pb14(pb32));
+ } else {
+ ump_pitch_bend(grp, ch, pb32);
+ }
+}
+
+static void send_channel_pressure(uint8_t grp, uint8_t ch, uint32_t val32) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ midi1_pkt_channel_pressure(grp, ch, scale_val32_to_val7(val32));
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ ump_midi1_channel_pressure(grp, ch, scale_val32_to_val7(val32));
+ } else {
+ ump_channel_pressure(grp, ch, val32);
+ }
+}
+
+static void send_poly_pressure(uint8_t grp, uint8_t ch, uint8_t note,
+ uint32_t val32) {
+ uint8_t alt = tud_midi2_alt_setting();
+ if (alt == 0) {
+ midi1_pkt_poly_pressure(grp, ch, note, scale_val32_to_val7(val32));
+ } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) {
+ ump_midi1_poly_pressure(grp, ch, note, scale_val32_to_val7(val32));
+ } else {
+ ump_poly_pressure(grp, ch, note, val32);
+ }
+}
+
+//--------------------------------------------------------------------+
+// Song Data
+//--------------------------------------------------------------------+
+
+// Extended note event with MIDI 2.0 expression data
+typedef struct {
+ uint8_t pitch; // MIDI pitch (0-127, 0=rest)
+ uint16_t duration_ms; // Duration in ms
+ uint16_t velocity; // 16-bit velocity (MIDI 2.0)
+ uint32_t pressure; // 32-bit aftertouch (0 = none)
+ int16_t bend_cents; // Pitch bend in cents (0 = none, for vibrato/ornaments)
+} midi2_note_t;
+
+// 16-bit velocity (MIDI 2.0): values that have NO 7-bit equivalent.
+// MIDI 1.0 can only express 128 levels (0x0000, 0x0200, 0x0400 ... 0xFE00).
+// These use the full 16-bit range to prove genuine MIDI 2.0 resolution.
+#define V_PPP 0x0A3D // 2621 - between MIDI1 vel 5 and 6
+#define V_PP 0x1C71 // 7281 - between MIDI1 vel 14 and 15
+#define V_P 0x3219 // 12825 - between MIDI1 vel 24 and 25
+#define V_MP 0x4F5C // 20316 - between MIDI1 vel 39 and 40
+#define V_MF 0x6E93 // 28307 - between MIDI1 vel 55 and 56
+#define V_F 0x8DA5 // 36261 - between MIDI1 vel 70 and 71
+#define V_FF 0xAC37 // 44087 - between MIDI1 vel 85 and 86
+#define V_FFF 0xDEB8 // 57016 - between MIDI1 vel 111 and 112
+
+// Twinkle Twinkle Little Star - Traditional
+// Tempo: 120 BPM (500ms per quarter note)
+// Key: C major, 4/4
+// Demonstrates all MIDI 2.0 Channel Voice features:
+// 16-bit velocity, 32-bit CC, 32-bit pitch bend,
+// 32-bit channel pressure, per-note poly pressure,
+// per-note management, program change with bank select,
+// JR timestamps
+static const midi2_note_t song_data[] = {
+ // Phrase 1: "Twin-kle twin-kle lit-tle star" (C C G G A A G-)
+ // Crescendo pp -> mp, gentle entry
+ { .pitch = 60, .duration_ms = 500, .velocity = V_PP, .pressure = 0, .bend_cents = 0 }, // C4
+ { .pitch = 60, .duration_ms = 500, .velocity = V_P, .pressure = 0, .bend_cents = 0 }, // C4
+ { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 69, .duration_ms = 500, .velocity = V_MF, .pressure = 0x1A3D7E5F, .bend_cents = 0 }, // A4 (32-bit pressure)
+ { .pitch = 69, .duration_ms = 500, .velocity = V_MF, .pressure = 0x2B851EB9, .bend_cents = 0 }, // A4 (pressure swell)
+ { .pitch = 67, .duration_ms = 1000,.velocity = V_MF, .pressure = 0x3C6EF373, .bend_cents = 7 }, // G4 (half, bend 7 cents)
+
+ // Phrase 2: "How I won-der what you are" (F F E E D D C-)
+ // mf, sustained
+ { .pitch = 65, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // F4
+ { .pitch = 65, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // F4
+ { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x1E4C2B7A, .bend_cents = 0 }, // E4
+ { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x2D5A8FC1, .bend_cents = 0 }, // E4 (aftertouch swell)
+ { .pitch = 62, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // D4
+ { .pitch = 62, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // D4
+ { .pitch = 60, .duration_ms = 1000,.velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // C4 (half, resolve)
+
+ // Phrase 3: "Up a-bove the world so high" (G G F F E E D-)
+ // f, building intensity
+ { .pitch = 67, .duration_ms = 500, .velocity = V_F, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 67, .duration_ms = 500, .velocity = V_F, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 65, .duration_ms = 500, .velocity = V_F, .pressure = 0x2F8A4E13, .bend_cents = 0 }, // F4
+ { .pitch = 65, .duration_ms = 500, .velocity = V_MF, .pressure = 0x41B2C9D7, .bend_cents = 0 }, // F4 (triggers poly pressure)
+ { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // E4
+ { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // E4
+ { .pitch = 62, .duration_ms = 1000,.velocity = V_MF, .pressure = 0x537DC2A6, .bend_cents = 13 }, // D4 (half, vibrato 13 cents)
+
+ // Phrase 4: "Like a dia-mond in the sky" (G G F F E E D-)
+ // ff, expressive peak
+ { .pitch = 67, .duration_ms = 500, .velocity = V_FF, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 67, .duration_ms = 500, .velocity = V_FF, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 65, .duration_ms = 500, .velocity = V_F, .pressure = 0x44E7B8D2, .bend_cents = 0 }, // F4 (triggers poly pressure)
+ { .pitch = 65, .duration_ms = 500, .velocity = V_F, .pressure = 0x56A3F14B, .bend_cents = 0 }, // F4 (triggers poly pressure)
+ { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x2C8E1F5A, .bend_cents = 0 }, // E4
+ { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x1D73A4E8, .bend_cents = 0 }, // E4
+ { .pitch = 62, .duration_ms = 1000,.velocity = V_MF, .pressure = 0x63F5B17D, .bend_cents = 19 }, // D4 (half, vibrato 19 cents)
+
+ // Phrase 5: "Twin-kle twin-kle lit-tle star" (C C G G A A G-)
+ // Diminuendo mf -> mp
+ { .pitch = 60, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // C4
+ { .pitch = 60, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // C4
+ { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4
+ { .pitch = 69, .duration_ms = 500, .velocity = V_MP, .pressure = 0x1B4F6D83, .bend_cents = 0 }, // A4
+ { .pitch = 69, .duration_ms = 500, .velocity = V_P, .pressure = 0x0E29C5A1, .bend_cents = 0 }, // A4
+ { .pitch = 67, .duration_ms = 1000,.velocity = V_P, .pressure = 0x2A6D3B9E, .bend_cents = 5 }, // G4 (half, gentle bend 5 cents)
+
+ // Phrase 6: "How I won-der what you are" (F F E E D D C-)
+ // Dying away mp -> ppp
+ { .pitch = 65, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // F4
+ { .pitch = 65, .duration_ms = 500, .velocity = V_P, .pressure = 0, .bend_cents = 0 }, // F4
+ { .pitch = 64, .duration_ms = 500, .velocity = V_P, .pressure = 0, .bend_cents = 0 }, // E4
+ { .pitch = 64, .duration_ms = 500, .velocity = V_PP, .pressure = 0, .bend_cents = 0 }, // E4
+ { .pitch = 62, .duration_ms = 500, .velocity = V_PP, .pressure = 0, .bend_cents = 0 }, // D4
+ { .pitch = 62, .duration_ms = 500, .velocity = V_PPP, .pressure = 0, .bend_cents = 0 }, // D4
+ { .pitch = 60, .duration_ms = 2000,.velocity = V_PPP, .pressure = 0x07A1E3C9, .bend_cents = 0 }, // C4 (fermata)
+
+ // Silence before loop
+ { .pitch = 0, .duration_ms = 1000,.velocity = 0, .pressure = 0, .bend_cents = 0 },
+
+ // End marker
+ { .pitch = 0, .duration_ms = 0, .velocity = 0, .pressure = 0, .bend_cents = 0 },
+};
+
+#define SONG_LENGTH (sizeof(song_data) / sizeof(midi2_note_t))
+
+//--------------------------------------------------------------------+
+// Song Playback State Machine
+//--------------------------------------------------------------------+
+
+typedef struct {
+ uint32_t current_note_idx;
+ uint32_t note_start_ms;
+ uint8_t active_pitch;
+ bool note_is_active;
+ bool setup_sent; // Initial setup (Program Change, CC) sent?
+ uint32_t loop_count;
+} song_state_t;
+
+static song_state_t song = { 0 };
+
+// Forward declarations
+void update_song_playback(uint32_t now_ms);
+void send_initial_setup(void);
+
+//--------------------------------------------------------------------+
+// MIDI 2.0 Device Callbacks (override weak stubs from middleware)
+//--------------------------------------------------------------------+
+
+void tud_midi2_rx_cb(uint8_t itf) {
+ // Drain the RX FIFO in a loop until empty. Leaving words in the FIFO
+ // across callbacks can prevent subsequent bulk OUT transfers from landing.
+ uint32_t words[8];
+ uint32_t n;
+ while ((n = tud_midi2_n_ump_read(itf, words, TU_ARRAY_SIZE(words))) > 0) {
+ (void) n;
+ }
+}
+
+// Reset playback state and re-send setup when host switches alt setting or
+// renegotiates the UMP Stream protocol.
+void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt) {
+ (void)itf;
+ song.setup_sent = false;
+ printf("[ALT] Host selected alt=%u\r\n", (unsigned)alt);
+}
+
+//--------------------------------------------------------------------+
+// Initial Setup - Program Change, CC, Per-Note Management
+//--------------------------------------------------------------------+
+
+void send_initial_setup(void) {
+ // JR Timestamp is UMP-only (Utility MT 0x0); skipped on Alt 0 transport.
+ if (tud_midi2_alt_setting() == 1) ump_jr_timestamp(0x0001);
+
+ send_program_change(0, 0, 0, true, 0, 0);
+ send_cc(0, 0, 7, 0xCCCCCCCC); // Volume 80%
+ send_cc(0, 0, 11, 0xFFFFFFFF); // Expression 100%
+ send_cc(0, 0, 64, 0x00000000); // Sustain off
+ send_cc(0, 0, 1, 0x20000000); // Modulation
+ send_cc(0, 0, 10, 0x80000000); // Pan center
+
+ // Per-Note Management is MIDI 2.0 exclusive (MT 0x4 status 0xF).
+ // Skipped when the active path falls back to MIDI 1.0 in any form.
+ if (tud_midi2_alt_setting() == 1 &&
+ tud_midi2_protocol() == MIDI_PROTOCOL_MIDI2) {
+ ump_per_note_mgmt(0, 0, 0, false, true);
+ }
+
+ send_pitch_bend(0, 0, 0x80000000); // Center
+ send_channel_pressure(0, 0, 0x00000000);
+
+ printf("[SETUP] Piano | Vol 80%% | alt=%u proto=%u\r\n",
+ (unsigned)tud_midi2_alt_setting(),
+ (unsigned)tud_midi2_protocol());
+}
+
+//--------------------------------------------------------------------+
+// Pitch Bend Conversion: cents to 32-bit value
+//--------------------------------------------------------------------+
+
+// Convert pitch bend in cents (-200 to +200) to 32-bit UMP value
+// Center = 0x80000000, range = +/- 2 semitones (200 cents)
+static inline uint32_t cents_to_pitch_bend(int16_t cents) {
+ if (cents == 0) return 0x80000000;
+ // Scale: 200 cents = full range (0x7FFFFFFF deviation from center)
+ int32_t offset = (int32_t)(((int64_t)cents * 0x7FFFFFFF) / 200);
+ return (uint32_t)((int32_t)0x80000000 + offset);
+}
+
+//--------------------------------------------------------------------+
+// Song Playback Logic - Full MIDI 2.0 Expression
+//--------------------------------------------------------------------+
+
+void update_song_playback(uint32_t now_ms) {
+ const midi2_note_t *current = &song_data[song.current_note_idx];
+
+ if (!song.setup_sent) {
+ send_initial_setup();
+ song.setup_sent = true;
+ song.note_start_ms = now_ms;
+ }
+
+ // Note duration elapsed: send Note Off, advance
+ if (song.note_is_active && (now_ms - song.note_start_ms) >= current->duration_ms) {
+ if (song.active_pitch > 0) {
+ if (current->bend_cents != 0) send_pitch_bend(0, 0, 0x80000000);
+ if (current->pressure > 0) send_channel_pressure(0, 0, 0x00000000);
+ send_note_off(0, 0, song.active_pitch, V_P);
+ }
+
+ song.note_is_active = false;
+ song.current_note_idx++;
+
+ if (song.current_note_idx >= SONG_LENGTH) {
+ song.current_note_idx = 0;
+ song.setup_sent = false;
+ song.loop_count++;
+ printf("\r\n=== Loop %lu ===\r\n", (unsigned long)song.loop_count);
+ }
+
+ song.note_start_ms = now_ms;
+ }
+
+ // Start next note
+ if (!song.note_is_active && song.current_note_idx < SONG_LENGTH) {
+ const midi2_note_t *next = &song_data[song.current_note_idx];
+
+ if (next->duration_ms == 0) {
+ song.current_note_idx = 0;
+ song.setup_sent = false;
+ song.loop_count++;
+ printf("\r\n=== Loop %lu ===\r\n", (unsigned long)song.loop_count);
+ return;
+ }
+
+ if (next->pitch > 0) {
+ // JR Timestamp is UMP-only; skip on Alt 0 transport.
+ if (tud_midi2_alt_setting() == 1) {
+ ump_jr_timestamp((uint16_t)(now_ms & 0xFFFF));
+ }
+ if (next->bend_cents != 0) {
+ send_pitch_bend(0, 0, cents_to_pitch_bend(next->bend_cents));
+ }
+ send_note_on(0, 0, next->pitch, next->velocity);
+ if (next->pressure > 0) {
+ send_channel_pressure(0, 0, next->pressure);
+ }
+ if (next->pressure > 0x40000000 && next->duration_ms > 500) {
+ send_poly_pressure(0, 0, next->pitch, next->pressure);
+ }
+
+ song.active_pitch = next->pitch;
+ song.note_is_active = true;
+ }
+
+ // Rest: honor duration
+ if (!song.note_is_active) {
+ song.active_pitch = 0;
+ song.note_is_active = true;
+ song.note_start_ms = now_ms;
+ }
+ }
+}
+
+//--------------------------------------------------------------------+
+// Main
+//--------------------------------------------------------------------+
+
+int main(void) {
+ board_init();
+ printf("\r\n");
+ printf("===========================================\r\n");
+ printf(" TinyUSB MIDI 2.0 Device\r\n");
+ printf("===========================================\r\n");
+ printf("Tempo: 120 BPM | Song: %u notes\r\n", (unsigned)SONG_LENGTH);
+ printf("Transport + protocol fallback:\r\n");
+ printf(" Alt 0 -> USB-MIDI 1.0 32-bit Event Packets (packet_write)\r\n");
+ printf(" Alt 1 + MIDI1 -> UMP MT 0x2 MIDI 1.0 Channel Voice (ump_write)\r\n");
+ printf(" Alt 1 + MIDI2 -> UMP MT 0x4 MIDI 2.0 Channel Voice (ump_write)\r\n");
+ printf("Status: Initializing...\r\n");
+
+ tusb_rhport_init_t dev_init = {.role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_AUTO};
+ tusb_init(BOARD_TUD_RHPORT, &dev_init);
+
+ board_init_after_tusb();
+ board_led_write(true);
+
+ uint32_t last_report_ms = 0;
+
+ while (1) {
+ tud_task();
+
+ uint32_t now_ms = tusb_time_millis_api();
+
+ if (tud_midi2_mounted()) {
+ update_song_playback(now_ms);
+ board_led_write(song.active_pitch > 0);
+ } else {
+ board_led_write((now_ms / 500) & 1);
+ }
+
+ // Status report every 10 seconds
+ if (now_ms - last_report_ms > 10000) {
+ last_report_ms = now_ms;
+ if (tud_midi2_mounted()) {
+ printf("[%lums] Playing idx %lu/%u loop %lu\r\n",
+ (unsigned long)now_ms,
+ (unsigned long)song.current_note_idx,
+ (unsigned)SONG_LENGTH,
+ (unsigned long)song.loop_count);
+ } else {
+ printf("[%lums] Waiting for host...\r\n", (unsigned long)now_ms);
+ }
+ }
+ }
+
+ return 0;
+}
diff --git a/examples/device/midi2_device/src/tusb_config.h b/examples/device/midi2_device/src/tusb_config.h
new file mode 100644
index 000000000..1ada0015f
--- /dev/null
+++ b/examples/device/midi2_device/src/tusb_config.h
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#ifndef TUSB_CONFIG_H_
+#define TUSB_CONFIG_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//--------------------------------------------------------------------+
+// Board Specific Configuration
+//--------------------------------------------------------------------+
+
+#ifndef BOARD_TUD_RHPORT
+#define BOARD_TUD_RHPORT 0
+#endif
+
+#ifndef BOARD_TUD_MAX_SPEED
+#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED
+#endif
+
+//--------------------------------------------------------------------
+// COMMON CONFIGURATION
+//--------------------------------------------------------------------
+
+#ifndef CFG_TUSB_MCU
+#error CFG_TUSB_MCU must be defined
+#endif
+
+#ifndef CFG_TUSB_OS
+#define CFG_TUSB_OS OPT_OS_NONE
+#endif
+
+#ifndef CFG_TUSB_DEBUG
+#define CFG_TUSB_DEBUG 0
+#endif
+
+// Enable Device stack
+#define CFG_TUD_ENABLED 1
+
+#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED
+
+#ifndef CFG_TUSB_MEM_SECTION
+#define CFG_TUSB_MEM_SECTION
+#endif
+
+#ifndef CFG_TUSB_MEM_ALIGN
+#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))
+#endif
+
+//--------------------------------------------------------------------
+// DEVICE CONFIGURATION
+//--------------------------------------------------------------------
+
+#ifndef CFG_TUD_ENDPOINT0_SIZE
+#define CFG_TUD_ENDPOINT0_SIZE 64
+#endif
+
+//------------- CLASS -------------//
+#define CFG_TUD_MIDI2 1
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TUSB_CONFIG_H_ */
diff --git a/examples/device/midi2_device/src/usb_descriptors.c b/examples/device/midi2_device/src/usb_descriptors.c
new file mode 100644
index 000000000..19a43f2d8
--- /dev/null
+++ b/examples/device/midi2_device/src/usb_descriptors.c
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+
+#include <string.h>
+#include "bsp/board_api.h"
+#include "tusb.h"
+#include "class/audio/audio.h"
+#include "class/midi/midi.h"
+
+//--------------------------------------------------------------------+
+// Device Descriptors
+//--------------------------------------------------------------------+
+
+static tusb_desc_device_t const desc_device = {
+ .bLength = sizeof(tusb_desc_device_t),
+ .bDescriptorType = TUSB_DESC_DEVICE,
+ .bcdUSB = 0x0200,
+ .bDeviceClass = 0x00,
+ .bDeviceSubClass = 0x00,
+ .bDeviceProtocol = 0x00,
+ .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
+
+ .idVendor = 0xcafe,
+ .idProduct = 0x4062, // MIDI 2.0 Device
+ .bcdDevice = 0x0100,
+
+ .iManufacturer = 0x01,
+ .iProduct = 0x02,
+ .iSerialNumber = 0x03,
+
+ .bNumConfigurations = 0x01
+};
+
+uint8_t const * tud_descriptor_device_cb(void) {
+ return (uint8_t const *) &desc_device;
+}
+
+//--------------------------------------------------------------------+
+// Configuration Descriptor - MIDI 2.0
+//--------------------------------------------------------------------+
+
+enum {
+ ITF_NUM_MIDI2 = 0, // Audio Control interface
+ ITF_NUM_MIDI2_STREAMING, // MIDI Streaming interface (auto-created by TUD_MIDI2_DESCRIPTOR)
+ ITF_NUM_TOTAL
+};
+
+#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI2_DESC_LEN)
+
+// Endpoint addresses
+#define EPNUM_MIDI2_OUT 0x01
+#define EPNUM_MIDI2_IN 0x81
+
+static uint8_t const desc_fs_configuration[] = {
+ // Config number, interface count, string index, total length, attribute, power in mA
+ TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
+
+ // MIDI 2.0 Interface
+ TUD_MIDI2_DESCRIPTOR(ITF_NUM_MIDI2, 0, EPNUM_MIDI2_OUT, EPNUM_MIDI2_IN, 64)
+};
+
+uint8_t const * tud_descriptor_configuration_cb(uint8_t index) {
+ (void) index;
+ return desc_fs_configuration;
+}
+
+//--------------------------------------------------------------------+
+// String Descriptors
+//--------------------------------------------------------------------+
+
+enum {
+ STRID_LANGID = 0,
+ STRID_MANUFACTURER = 1,
+ STRID_PRODUCT = 2,
+ STRID_SERIAL = 3,
+};
+
+static char const *string_desc_arr[] = {
+ (const char[]) { 0x09, 0x04 }, // 0: Language
+ "TinyUSB", // 1: Manufacturer
+ "TinyUSB MIDI 2.0", // 2: Product
+ NULL, // 3: Serial
+};
+
+static uint16_t _desc_str[32 + 1];
+
+uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
+ (void) langid;
+ size_t chr_count;
+
+ switch ( index ) {
+ case STRID_LANGID:
+ memcpy(&_desc_str[1], string_desc_arr[0], 2);
+ chr_count = 1;
+ break;
+
+ case STRID_SERIAL:
+ chr_count = board_usb_get_serial(_desc_str + 1, 32);
+ break;
+
+ default:
+ if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) {
+ return NULL;
+ }
+
+ const char *str = string_desc_arr[index];
+ chr_count = strlen(str);
+ const size_t max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1;
+ if ( chr_count > max_count ) {
+ chr_count = max_count;
+ }
+
+ for ( size_t i = 0; i < chr_count; i++ ) {
+ _desc_str[1 + i] = str[i];
+ }
+ break;
+ }
+
+ _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
+ return _desc_str;
+}
diff --git a/examples/device/mtp/src/mtp_fs_example.c b/examples/device/mtp/src/mtp_fs_example.c
index 09697693e..b7d062c64 100644
--- a/examples/device/mtp/src/mtp_fs_example.c
+++ b/examples/device/mtp/src/mtp_fs_example.c
@@ -143,6 +143,7 @@ static int32_t fs_get_device_properties(tud_mtp_cb_data_t* cb_data);
static int32_t fs_get_object_handles(tud_mtp_cb_data_t* cb_data);
static int32_t fs_get_object_info(tud_mtp_cb_data_t* cb_data);
static int32_t fs_get_object(tud_mtp_cb_data_t* cb_data);
+static int32_t fs_get_partial_object(tud_mtp_cb_data_t* cb_data);
static int32_t fs_delete_object(tud_mtp_cb_data_t* cb_data);
static int32_t fs_send_object_info(tud_mtp_cb_data_t* cb_data);
static int32_t fs_send_object(tud_mtp_cb_data_t* cb_data);
@@ -164,6 +165,7 @@ fs_op_handler_dict_t fs_op_handler_dict[] = {
{ MTP_OP_GET_OBJECT_HANDLES, fs_get_object_handles },
{ MTP_OP_GET_OBJECT_INFO, fs_get_object_info },
{ MTP_OP_GET_OBJECT, fs_get_object },
+ { MTP_OP_GET_PARTIAL_OBJECT, fs_get_partial_object },
{ MTP_OP_DELETE_OBJECT, fs_delete_object },
{ MTP_OP_SEND_OBJECT_INFO, fs_send_object_info },
{ MTP_OP_SEND_OBJECT, fs_send_object },
@@ -330,6 +332,14 @@ int32_t tud_mtp_data_complete_cb(tud_mtp_cb_data_t* cb_data) {
break;
}
+ case MTP_OP_GET_PARTIAL_OBJECT: {
+ // response parameter: actual length of data sent excluding container header
+ const uint32_t len = cb_data->total_xferred_bytes - sizeof(mtp_container_header_t);
+ (void) mtp_container_add_uint32(resp, len);
+ resp->header->code = MTP_RESP_OK;
+ break;
+ }
+
default:
resp->header->code = (cb_data->xfer_result == XFER_RESULT_SUCCESS) ? MTP_RESP_OK : MTP_RESP_GENERAL_ERROR;
break;
@@ -535,6 +545,40 @@ static int32_t fs_get_object(tud_mtp_cb_data_t* cb_data) {
return 0;
}
+static int32_t fs_get_partial_object(tud_mtp_cb_data_t* cb_data) {
+ const mtp_container_command_t* command = cb_data->command_container;
+ mtp_container_info_t* io_container = &cb_data->io_container;
+ const uint32_t obj_handle = command->params[0];
+ const uint32_t req_offset = command->params[1];
+ const uint32_t req_max = command->params[2];
+ const fs_file_t* f = fs_get_file(obj_handle);
+ if (f == NULL) {
+ return MTP_RESP_INVALID_OBJECT_HANDLE;
+ }
+
+ const uint32_t avail = (req_offset >= f->size) ? 0u : (f->size - req_offset);
+ const uint32_t to_send = tu_min32(avail, req_max);
+
+ if (cb_data->phase == MTP_PHASE_COMMAND) {
+ // If file contents is larger than CFG_TUD_MTP_EP_BUFSIZE, data may only partially be added here
+ // the rest will be sent in tud_mtp_data_more_cb
+ (void) mtp_container_add_raw(io_container, f->data + req_offset, to_send);
+ tud_mtp_data_send(io_container);
+ } else if (cb_data->phase == MTP_PHASE_DATA) {
+ // continue sending remaining data: file contents offset is xferred byte minus header size
+ const uint32_t offset = cb_data->total_xferred_bytes - sizeof(mtp_container_header_t);
+ const uint32_t xact_len = tu_min32(to_send - offset, io_container->payload_bytes);
+ if (xact_len > 0) {
+ memcpy(io_container->payload, f->data + offset + req_offset, xact_len);
+ tud_mtp_data_send(io_container);
+ }
+ } else {
+ // nothing to do
+ }
+
+ return 0;
+}
+
static int32_t fs_send_object_info(tud_mtp_cb_data_t* cb_data) {
const mtp_container_command_t* command = cb_data->command_container;
mtp_container_info_t* io_container = &cb_data->io_container;
diff --git a/examples/device/mtp/src/tusb_config.h b/examples/device/mtp/src/tusb_config.h
index 95cc048ee..5224cd79b 100644
--- a/examples/device/mtp/src/tusb_config.h
+++ b/examples/device/mtp/src/tusb_config.h
@@ -106,6 +106,7 @@
MTP_OP_GET_OBJECT_HANDLES, \
MTP_OP_GET_OBJECT_INFO, \
MTP_OP_GET_OBJECT, \
+ MTP_OP_GET_PARTIAL_OBJECT, \
MTP_OP_DELETE_OBJECT, \
MTP_OP_SEND_OBJECT_INFO, \
MTP_OP_SEND_OBJECT, \
diff --git a/examples/device/net_lwip_webserver/src/tusb_config.h b/examples/device/net_lwip_webserver/src/tusb_config.h
index aff75866d..c594d1ebd 100644
--- a/examples/device/net_lwip_webserver/src/tusb_config.h
+++ b/examples/device/net_lwip_webserver/src/tusb_config.h
@@ -103,7 +103,7 @@ extern "C" {
TU_CHECK_MCU(OPT_MCU_STM32F2, OPT_MCU_STM32F4, OPT_MCU_STM32F7) || \
TU_CHECK_MCU(OPT_MCU_STM32H5, OPT_MCU_STM32H7, OPT_MCU_STM32H7RS) || \
TU_CHECK_MCU(OPT_MCU_STM32U5, OPT_MCU_STM32N6) || \
- TU_CHECK_MCU(OPT_MCU_RP2040) || \
+ TU_CHECK_MCU(OPT_MCU_RP2040, OPT_MCU_CH32V307) || \
TU_CHECK_MCU(OPT_MCU_MIMXRT1XXX) || \
TU_CHECK_MCU(OPT_MCU_NRF5X)
#define LWIP_HIGH_THROUGHPUT 1
diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt
index 70e0427ab..7c74e3c73 100644
--- a/examples/host/CMakeLists.txt
+++ b/examples/host/CMakeLists.txt
@@ -13,6 +13,7 @@ set(EXAMPLE_LIST
device_info
hid_controller
midi_rx
+ midi2_host
msc_file_explorer
msc_file_explorer_freertos
)
diff --git a/examples/host/midi2_host/CMakeLists.txt b/examples/host/midi2_host/CMakeLists.txt
new file mode 100644
index 000000000..0ec03bf5f
--- /dev/null
+++ b/examples/host/midi2_host/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.20)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake)
+
+project(midi2_host C CXX ASM)
+
+# Checks this example is valid for the family and initializes the project
+family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR})
+
+# Espressif has its own cmake build system
+if(FAMILY STREQUAL "espressif")
+ return()
+endif()
+
+add_executable(${PROJECT_NAME})
+
+# Example source
+target_sources(${PROJECT_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c
+ )
+
+# Example include
+target_include_directories(${PROJECT_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ )
+
+# Configure compilation flags and libraries for the example without RTOS.
+# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
+family_configure_host_example(${PROJECT_NAME} noos)
diff --git a/examples/host/midi2_host/CMakePresets.json b/examples/host/midi2_host/CMakePresets.json
new file mode 100644
index 000000000..5cd8971e9
--- /dev/null
+++ b/examples/host/midi2_host/CMakePresets.json
@@ -0,0 +1,6 @@
+{
+ "version": 6,
+ "include": [
+ "../../../hw/bsp/BoardPresets.json"
+ ]
+}
diff --git a/examples/host/midi2_host/Makefile b/examples/host/midi2_host/Makefile
new file mode 100644
index 000000000..f8292385e
--- /dev/null
+++ b/examples/host/midi2_host/Makefile
@@ -0,0 +1,13 @@
+include ../../../hw/bsp/family_support.mk
+
+INC += \
+ src \
+
+
+# Example source
+EXAMPLE_SOURCE += \
+ src/main.c
+
+SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE))
+
+include ../../../hw/bsp/family_rules.mk
diff --git a/examples/host/midi2_host/only.txt b/examples/host/midi2_host/only.txt
new file mode 100644
index 000000000..c71aacd87
--- /dev/null
+++ b/examples/host/midi2_host/only.txt
@@ -0,0 +1,34 @@
+family:hpmicro
+family:samd21
+family:samd5x_e5x
+mcu:CH32V20X
+mcu:ESP32P4
+mcu:ESP32S2
+mcu:ESP32S3
+mcu:KINETIS_KL
+mcu:LPC175X_6X
+mcu:LPC177X_8X
+mcu:LPC18XX
+mcu:LPC40XX
+mcu:LPC43XX
+mcu:LPC54
+mcu:LPC55
+mcu:MAX3421
+mcu:MIMXRT10XX
+mcu:MIMXRT11XX
+mcu:MIMXRT1XXX
+mcu:MSP432E4
+mcu:RAXXX
+mcu:RP2040
+mcu:RW61X
+mcu:RX65X
+mcu:STM32C0
+mcu:STM32F4
+mcu:STM32F7
+mcu:STM32G0
+mcu:STM32H5
+mcu:STM32H7
+mcu:STM32H7RS
+mcu:STM32N6
+mcu:STM32U3
+mcu:STM32U5
diff --git a/examples/host/midi2_host/skip.txt b/examples/host/midi2_host/skip.txt
new file mode 100644
index 000000000..308796869
--- /dev/null
+++ b/examples/host/midi2_host/skip.txt
@@ -0,0 +1 @@
+board:lpcxpresso54114
diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c
new file mode 100644
index 000000000..63b08318c
--- /dev/null
+++ b/examples/host/midi2_host/src/main.c
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+// Minimal USB-MIDI 2.0 host example.
+// Receives UMP from any MIDI 2.0 device, prints each packet to stdout.
+
+#include <stdio.h>
+#include <string.h>
+#include "bsp/board_api.h"
+#include "tusb.h"
+#include "class/midi/midi2_host.h"
+
+//--------------------------------------------------------------------+
+// State
+//--------------------------------------------------------------------+
+
+static uint8_t midi2_idx = 0xFF;
+
+//--------------------------------------------------------------------+
+// UMP printer - shows MT and word(s) in hex; decodes Channel Voice
+//--------------------------------------------------------------------+
+
+static void print_ump(const uint32_t* words, uint8_t wc) {
+ uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F);
+ uint8_t group = (uint8_t)((words[0] >> 24) & 0x0F);
+
+ if (mt == 0x4 && wc >= 2) {
+ // MIDI 2.0 Channel Voice
+ uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F);
+ uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F);
+ uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F);
+ switch (status) {
+ case 0x9:
+ printf("[g%u ch%u] M2 NoteOn n=%u vel=%04X attr=%04X\r\n",
+ group, channel, data1,
+ (unsigned)((words[1] >> 16) & 0xFFFF),
+ (unsigned)(words[1] & 0xFFFF));
+ break;
+ case 0x8:
+ printf("[g%u ch%u] M2 NoteOff n=%u vel=%04X\r\n",
+ group, channel, data1,
+ (unsigned)((words[1] >> 16) & 0xFFFF));
+ break;
+ case 0xB:
+ printf("[g%u ch%u] M2 CC#%u = %08lX\r\n",
+ group, channel, data1, (unsigned long)words[1]);
+ break;
+ case 0xC:
+ printf("[g%u ch%u] M2 ProgChg %u\r\n",
+ group, channel, (unsigned)((words[1] >> 24) & 0x7F));
+ break;
+ case 0xD:
+ printf("[g%u ch%u] M2 ChanPress %08lX\r\n",
+ group, channel, (unsigned long)words[1]);
+ break;
+ case 0xE:
+ printf("[g%u ch%u] M2 PitchBend %08lX\r\n",
+ group, channel, (unsigned long)words[1]);
+ break;
+ default:
+ printf("[g%u ch%u] M2 status=0x%X w0=%08lX w1=%08lX\r\n",
+ group, channel, status,
+ (unsigned long)words[0], (unsigned long)words[1]);
+ break;
+ }
+ } else if (mt == 0x2 && wc == 1) {
+ // MIDI 1.0 Channel Voice
+ uint8_t status = (uint8_t)((words[0] >> 16) & 0xFF);
+ uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F);
+ uint8_t data2 = (uint8_t)(words[0] & 0x7F);
+ printf("[g%u] M1 %02X %02X %02X\r\n", group, status, data1, data2);
+ } else {
+ printf("UMP MT=0x%X wc=%u w0=%08lX\r\n",
+ mt, wc, (unsigned long)words[0]);
+ }
+}
+
+//--------------------------------------------------------------------+
+// MIDI 2.0 Host Callbacks
+//--------------------------------------------------------------------+
+
+void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) {
+ (void)idx;
+ printf("MIDI2 descriptor: %s, RX cables=%u TX cables=%u\r\n",
+ d->protocol_version ? "MIDI 2.0" : "MIDI 1.0",
+ d->rx_cable_count, d->tx_cable_count);
+}
+
+void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) {
+ midi2_idx = idx;
+ printf("MIDI2 mounted: idx=%u protocol=%s\r\n",
+ idx, m->protocol_version ? "MIDI 2.0" : "MIDI 1.0");
+}
+
+void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) {
+ (void)xferred_bytes;
+
+ uint32_t words[16];
+ while (1) {
+ uint32_t n = tuh_midi2_ump_read(idx, words, 16);
+ if (n == 0) break;
+
+ uint32_t i = 0;
+ while (i < n) {
+ uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F);
+ uint8_t wc = midi2_ump_word_count(mt);
+ if (i + wc > n) break;
+ print_ump(&words[i], wc);
+ i += wc;
+ }
+ }
+}
+
+void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) {
+ (void)idx; (void)xferred_bytes;
+}
+
+void tuh_midi2_umount_cb(uint8_t idx) {
+ (void)idx;
+ midi2_idx = 0xFF;
+ printf("MIDI2 unmounted\r\n");
+}
+
+//--------------------------------------------------------------------+
+// Main
+//--------------------------------------------------------------------+
+
+int main(void) {
+ board_init();
+
+ printf("\r\nTinyUSB Host MIDI 2.0 Example\r\n");
+
+ tusb_rhport_init_t host_init = {
+ .role = TUSB_ROLE_HOST,
+ .speed = TUSB_SPEED_AUTO,
+ };
+ tusb_init(BOARD_TUH_RHPORT, &host_init);
+
+ while (1) {
+ tuh_task();
+ }
+
+ return 0;
+}
diff --git a/examples/host/midi2_host/src/tusb_config.h b/examples/host/midi2_host/src/tusb_config.h
new file mode 100644
index 000000000..4bd6ef2ea
--- /dev/null
+++ b/examples/host/midi2_host/src/tusb_config.h
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+#ifndef TUSB_CONFIG_H_
+#define TUSB_CONFIG_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//--------------------------------------------------------------------+
+// Common Configuration
+//--------------------------------------------------------------------+
+
+#ifndef CFG_TUSB_MCU
+#error CFG_TUSB_MCU must be defined
+#endif
+
+#ifdef ESP_PLATFORM
+#define CFG_TUSB_OS_INC_PATH freertos/
+#endif
+
+#ifndef CFG_TUSB_OS
+#define CFG_TUSB_OS OPT_OS_NONE
+#endif
+
+#ifndef CFG_TUSB_DEBUG
+#define CFG_TUSB_DEBUG 0
+#endif
+
+#ifndef CFG_TUH_MEM_SECTION
+#define CFG_TUH_MEM_SECTION
+#endif
+
+#ifndef CFG_TUH_MEM_ALIGN
+#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4)))
+#endif
+
+//--------------------------------------------------------------------+
+// Host Configuration
+//--------------------------------------------------------------------+
+
+#define CFG_TUH_ENABLED 1
+
+#if CFG_TUSB_MCU == OPT_MCU_RP2040
+ // #define CFG_TUH_RPI_PIO_USB 1 // use pio-usb as host controller
+ // #define CFG_TUH_MAX3421 1 // use max3421 as host controller
+
+ // host roothub port is 1 if using either pio-usb or max3421
+ #if (defined(CFG_TUH_RPI_PIO_USB) && CFG_TUH_RPI_PIO_USB) || (defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421)
+ #define BOARD_TUH_RHPORT 1
+ #endif
+#endif
+
+#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED
+
+//------------------------- Board Specific --------------------------+
+
+#ifndef BOARD_TUH_RHPORT
+#define BOARD_TUH_RHPORT 0
+#endif
+
+#ifndef BOARD_TUH_MAX_SPEED
+#define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED
+#endif
+
+//--------------------------------------------------------------------+
+// Driver Configuration
+//--------------------------------------------------------------------+
+
+#define CFG_TUH_ENUMERATION_BUFSIZE 256
+
+#define CFG_TUH_HUB 1
+#define CFG_TUH_DEVICE_MAX (3*CFG_TUH_HUB + 1)
+
+// USB-MIDI 2.0 Host
+#define CFG_TUH_MIDI2 CFG_TUH_DEVICE_MAX
+#define CFG_TUH_MIDI2_RX_BUFSIZE 512
+#define CFG_TUH_MIDI2_TX_BUFSIZE 512
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif