Added tests and updated Documentation

This commit is contained in:
pszsh 2026-03-04 06:02:26 -08:00
parent 782ff440f0
commit 517cbbb318
18 changed files with 858 additions and 40 deletions

View File

@ -1,6 +1,12 @@
cmake_minimum_required(VERSION 3.13)
add_library(cs_midi STATIC
# Interface selection consumers set these before add_subdirectory()
option(CS_MIDI_BLE "Enable BLE MIDI interface (BTstack)" ON)
option(CS_MIDI_USB "Enable USB MIDI interface (TinyUSB)" OFF)
option(CS_MIDI_SERIAL "Enable Serial MIDI interface (UART)" OFF)
# Core sources always compiled
set(CS_MIDI_CORE_SOURCES
AH/Debug/Debug.cpp
AH/Error/Exit.cpp
AH/PrintStream/PrintStream.cpp
@ -21,10 +27,32 @@ add_library(cs_midi STATIC
MIDI_Parsers/MIDI_MessageTypes.cpp
MIDI_Interfaces/MIDI_Interface.cpp
MIDI_Interfaces/MIDI_Pipes.cpp
)
set(CS_MIDI_SOURCES ${CS_MIDI_CORE_SOURCES})
if(CS_MIDI_BLE)
list(APPEND CS_MIDI_SOURCES
MIDI_Interfaces/BLEMIDI/BLEMIDIPacketBuilder.cpp
MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp
MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp
)
)
endif()
if(CS_MIDI_USB)
list(APPEND CS_MIDI_SOURCES
MIDI_Parsers/USBMIDI_Parser.cpp
MIDI_Interfaces/USBMIDI_Interface.cpp
)
endif()
if(CS_MIDI_SERIAL)
list(APPEND CS_MIDI_SOURCES
MIDI_Interfaces/SerialMIDI_Interface.cpp
)
endif()
add_library(cs_midi STATIC ${CS_MIDI_SOURCES})
target_include_directories(cs_midi PUBLIC
${CMAKE_CURRENT_LIST_DIR}
@ -32,16 +60,32 @@ target_include_directories(cs_midi PUBLIC
target_compile_definitions(cs_midi PUBLIC
MIDI_NUM_CABLES=1
$<$<BOOL:${CS_MIDI_BLE}>:CS_MIDI_BLE=1>
$<$<BOOL:${CS_MIDI_USB}>:CS_MIDI_USB=1>
$<$<BOOL:${CS_MIDI_SERIAL}>:CS_MIDI_SERIAL=1>
)
# pico_stdlib: needed by the library for millis/micros/gpio
# pico_btstack_ble: BTstack BLE headers + objects (linked PRIVATE to avoid
# propagating the full btstack source build to consumers)
# hardware_sync: save_and_disable_interrupts used by BTstackBackgroundBackend
target_link_libraries(cs_midi
PUBLIC pico_stdlib hardware_sync hardware_adc
PRIVATE pico_btstack_ble pico_btstack_cyw43
pico_cyw43_arch_lwip_threadsafe_background
)
if(CS_MIDI_BLE)
target_link_libraries(cs_midi
PRIVATE pico_btstack_ble pico_btstack_cyw43
pico_cyw43_arch_lwip_threadsafe_background
)
endif()
if(CS_MIDI_SERIAL)
target_link_libraries(cs_midi
PRIVATE hardware_uart
)
endif()
if(CS_MIDI_USB)
target_link_libraries(cs_midi
PRIVATE tinyusb_device tinyusb_board
)
endif()
target_compile_features(cs_midi PUBLIC cxx_std_17)

View File

@ -0,0 +1,67 @@
#include "SerialMIDI_Interface.hpp"
#include "hardware/gpio.h"
BEGIN_CS_NAMESPACE
void HardwareSerialMIDI_Interface::begin() {
uart_init(uart, MIDI_BAUD);
gpio_set_function(txPin, GPIO_FUNC_UART);
gpio_set_function(rxPin, GPIO_FUNC_UART);
}
void HardwareSerialMIDI_Interface::update() {
MIDI_Interface::updateIncoming(this);
}
MIDIReadEvent HardwareSerialMIDI_Interface::read() {
return parser.pull(UARTPuller{uart});
}
ChannelMessage HardwareSerialMIDI_Interface::getChannelMessage() const {
return parser.getChannelMessage();
}
SysCommonMessage HardwareSerialMIDI_Interface::getSysCommonMessage() const {
return parser.getSysCommonMessage();
}
RealTimeMessage HardwareSerialMIDI_Interface::getRealTimeMessage() const {
return parser.getRealTimeMessage();
}
SysExMessage HardwareSerialMIDI_Interface::getSysExMessage() const {
return parser.getSysExMessage();
}
void HardwareSerialMIDI_Interface::sendChannelMessageImpl(ChannelMessage msg) {
if (msg.hasTwoDataBytes()) {
uint8_t buf[3] = {msg.header, msg.data1, msg.data2};
uart_write_blocking(uart, buf, 3);
} else {
uint8_t buf[2] = {msg.header, msg.data1};
uart_write_blocking(uart, buf, 2);
}
}
void HardwareSerialMIDI_Interface::sendSysCommonImpl(SysCommonMessage msg) {
uint8_t ndata = msg.getNumberOfDataBytes();
if (ndata == 2) {
uint8_t buf[3] = {msg.header, msg.data1, msg.data2};
uart_write_blocking(uart, buf, 3);
} else if (ndata == 1) {
uint8_t buf[2] = {msg.header, msg.data1};
uart_write_blocking(uart, buf, 2);
} else {
uart_write_blocking(uart, &msg.header, 1);
}
}
void HardwareSerialMIDI_Interface::sendSysExImpl(SysExMessage msg) {
uart_write_blocking(uart, msg.data, msg.length);
}
void HardwareSerialMIDI_Interface::sendRealTimeImpl(RealTimeMessage msg) {
uart_write_blocking(uart, &msg.message, 1);
}
END_CS_NAMESPACE

View File

@ -0,0 +1,55 @@
#pragma once
#include "MIDI_Interface.hpp"
#include <MIDI_Parsers/SerialMIDI_Parser.hpp>
#include "hardware/uart.h"
BEGIN_CS_NAMESPACE
struct UARTPuller {
uart_inst_t *uart;
bool pull(uint8_t &byte) {
if (!uart_is_readable(uart))
return false;
byte = uart_getc(uart);
return true;
}
};
class HardwareSerialMIDI_Interface : public MIDI_Interface {
public:
HardwareSerialMIDI_Interface(uart_inst_t *uart, uint tx_pin, uint rx_pin)
: uart(uart), txPin(tx_pin), rxPin(rx_pin) {}
void begin() override;
void update() override;
MIDIReadEvent read();
ChannelMessage getChannelMessage() const;
SysCommonMessage getSysCommonMessage() const;
RealTimeMessage getRealTimeMessage() const;
SysExMessage getSysExMessage() const;
protected:
void sendChannelMessageImpl(ChannelMessage msg) override;
void sendSysCommonImpl(SysCommonMessage msg) override;
void sendSysExImpl(SysExMessage msg) override;
void sendRealTimeImpl(RealTimeMessage msg) override;
void sendNowImpl() override {}
private:
#if !DISABLE_PIPES
void handleStall() override { MIDI_Interface::handleStall(this); }
#ifdef DEBUG_OUT
const char *getName() const override { return "serial"; }
#endif
#endif
SerialMIDI_Parser parser;
uart_inst_t *uart;
uint txPin;
uint rxPin;
};
END_CS_NAMESPACE

View File

@ -0,0 +1,112 @@
#include "USBMIDI_Interface.hpp"
BEGIN_CS_NAMESPACE
void USBMIDI_Interface::update() {
tud_task();
MIDI_Interface::updateIncoming(this);
}
MIDIReadEvent USBMIDI_Interface::read() {
return parser.pull(TinyUSBPuller{cableNum});
}
ChannelMessage USBMIDI_Interface::getChannelMessage() const {
return parser.getChannelMessage();
}
SysCommonMessage USBMIDI_Interface::getSysCommonMessage() const {
return parser.getSysCommonMessage();
}
RealTimeMessage USBMIDI_Interface::getRealTimeMessage() const {
return parser.getRealTimeMessage();
}
SysExMessage USBMIDI_Interface::getSysExMessage() const {
return parser.getSysExMessage();
}
// Map MIDI status high nibble to USB MIDI Code Index Number
MIDICodeIndexNumber USBMIDI_Interface::CIN(uint8_t status) {
if (status >= 0x80 && status < 0xF0)
return static_cast<MIDICodeIndexNumber>((status >> 4) & 0x0F);
switch (status) {
case 0xF1: // MTC Quarter Frame
case 0xF3: // Song Select
return MIDICodeIndexNumber::SystemCommon2B;
case 0xF2: // Song Position Pointer
return MIDICodeIndexNumber::SystemCommon3B;
case 0xF6: // Tune Request
case 0xF7: // SysEx End (bare)
return MIDICodeIndexNumber::SystemCommon1B;
default:
if (status >= 0xF8)
return MIDICodeIndexNumber::SingleByte;
return MIDICodeIndexNumber::MiscFunctionCodes;
}
}
void USBMIDI_Interface::writePacket(uint8_t cin, uint8_t b0,
uint8_t b1, uint8_t b2) {
uint8_t packet[4] = {
static_cast<uint8_t>((cableNum << 4) | (cin & 0x0F)),
b0, b1, b2
};
tud_midi_n_packet_write(cableNum, packet);
}
void USBMIDI_Interface::sendChannelMessageImpl(ChannelMessage msg) {
auto cin = CIN(msg.header);
if (msg.hasTwoDataBytes())
writePacket(uint8_t(cin), msg.header, msg.data1, msg.data2);
else
writePacket(uint8_t(cin), msg.header, msg.data1, 0);
}
void USBMIDI_Interface::sendSysCommonImpl(SysCommonMessage msg) {
auto cin = CIN(msg.header);
uint8_t ndata = msg.getNumberOfDataBytes();
if (ndata == 2)
writePacket(uint8_t(cin), msg.header, msg.data1, msg.data2);
else if (ndata == 1)
writePacket(uint8_t(cin), msg.header, msg.data1, 0);
else
writePacket(uint8_t(cin), msg.header, 0, 0);
}
void USBMIDI_Interface::sendSysExImpl(SysExMessage msg) {
const uint8_t *data = msg.data;
uint16_t remaining = msg.length;
while (remaining > 3) {
writePacket(uint8_t(MIDICodeIndexNumber::SysExStartCont),
data[0], data[1], data[2]);
data += 3;
remaining -= 3;
}
switch (remaining) {
case 3:
writePacket(uint8_t(MIDICodeIndexNumber::SysExEnd3B),
data[0], data[1], data[2]);
break;
case 2:
writePacket(uint8_t(MIDICodeIndexNumber::SysExEnd2B),
data[0], data[1], 0);
break;
case 1:
writePacket(uint8_t(MIDICodeIndexNumber::SysExEnd1B),
data[0], 0, 0);
break;
default:
break;
}
}
void USBMIDI_Interface::sendRealTimeImpl(RealTimeMessage msg) {
writePacket(uint8_t(MIDICodeIndexNumber::SingleByte),
msg.message, 0, 0);
}
END_CS_NAMESPACE

View File

@ -0,0 +1,53 @@
#pragma once
#include "MIDI_Interface.hpp"
#include <MIDI_Parsers/USBMIDI_Parser.hpp>
#include "tusb.h"
BEGIN_CS_NAMESPACE
struct TinyUSBPuller {
uint8_t itf;
bool pull(USBMIDI_Parser::MIDIUSBPacket_t &packet) {
return tud_midi_n_packet_read(itf, packet.data);
}
};
class USBMIDI_Interface : public MIDI_Interface {
public:
explicit USBMIDI_Interface(uint8_t cable_num = 0)
: cableNum(cable_num) {}
void begin() override {}
void update() override;
MIDIReadEvent read();
ChannelMessage getChannelMessage() const;
SysCommonMessage getSysCommonMessage() const;
RealTimeMessage getRealTimeMessage() const;
SysExMessage getSysExMessage() const;
protected:
void sendChannelMessageImpl(ChannelMessage msg) override;
void sendSysCommonImpl(SysCommonMessage msg) override;
void sendSysExImpl(SysExMessage msg) override;
void sendRealTimeImpl(RealTimeMessage msg) override;
void sendNowImpl() override {}
private:
#if !DISABLE_PIPES
void handleStall() override { MIDI_Interface::handleStall(this); }
#ifdef DEBUG_OUT
const char *getName() const override { return "usb"; }
#endif
#endif
static MIDICodeIndexNumber CIN(uint8_t status);
void writePacket(uint8_t cin, uint8_t b0, uint8_t b1, uint8_t b2);
USBMIDI_Parser parser;
uint8_t cableNum;
};
END_CS_NAMESPACE

View File

@ -0,0 +1,166 @@
#include "USBMIDI_Parser.hpp"
#include <Settings/SettingsWrapper.hpp>
BEGIN_CS_NAMESPACE
MIDIReadEvent USBMIDI_Parser::handleChannelMessage(MIDIUSBPacket_t packet,
Cable cable) {
midimsg.header = packet[1];
midimsg.data1 = packet[2];
midimsg.data2 = packet[3];
midimsg.cable = cable;
return MIDIReadEvent::CHANNEL_MESSAGE;
}
MIDIReadEvent USBMIDI_Parser::handleSysExStartCont(MIDIUSBPacket_t packet,
Cable cable) {
#if !IGNORE_SYSEX
if (packet[1] == uint8_t(MIDIMessageType::SysExStart)) {
startSysEx(cable);
} else if (!receivingSysEx(cable)) {
DEBUGREF(F("No SysExStart received"));
return MIDIReadEvent::NO_MESSAGE;
}
if (!hasSysExSpace(cable, 3)) {
storePacket(packet);
endSysExChunk(cable);
return MIDIReadEvent::SYSEX_CHUNK;
}
addSysExBytes(cable, &packet[1], 3);
#else
(void)packet;
(void)cable;
#endif
return MIDIReadEvent::NO_MESSAGE;
}
template <uint8_t NumBytes>
MIDIReadEvent USBMIDI_Parser::handleSysExEnd(MIDIUSBPacket_t packet,
Cable cable) {
static_assert(NumBytes == 2 || NumBytes == 3,
"Only 2- or 3-byte SysEx packets are supported");
#if !IGNORE_SYSEX
if (packet[1] == uint8_t(MIDIMessageType::SysExStart)) {
startSysEx(cable);
} else if (!receivingSysEx(cable)) {
DEBUGFN(F("No SysExStart received"));
return MIDIReadEvent::NO_MESSAGE;
}
if (!hasSysExSpace(cable, NumBytes)) {
storePacket(packet);
endSysExChunk(cable);
return MIDIReadEvent::SYSEX_CHUNK;
}
addSysExBytes(cable, &packet[1], NumBytes);
endSysEx(cable);
return MIDIReadEvent::SYSEX_MESSAGE;
#else
(void)packet;
(void)cable;
return MIDIReadEvent::NO_MESSAGE;
#endif
}
template <>
MIDIReadEvent USBMIDI_Parser::handleSysExEnd<1>(MIDIUSBPacket_t packet,
Cable cable) {
if (packet[1] != uint8_t(MIDIMessageType::SysExEnd)) {
midimsg.header = packet[1];
midimsg.cable = cable;
return MIDIReadEvent::SYSCOMMON_MESSAGE;
}
#if !IGNORE_SYSEX
else {
if (!receivingSysEx(cable)) {
DEBUGREF(F("No SysExStart received"));
return MIDIReadEvent::NO_MESSAGE;
}
if (!hasSysExSpace(cable, 1)) {
storePacket(packet);
endSysExChunk(cable);
return MIDIReadEvent::SYSEX_CHUNK;
}
addSysExByte(cable, packet[1]);
endSysEx(cable);
return MIDIReadEvent::SYSEX_MESSAGE;
}
#else
(void)packet;
(void)cable;
return MIDIReadEvent::NO_MESSAGE;
#endif
}
MIDIReadEvent USBMIDI_Parser::handleSysCommon(MIDIUSBPacket_t packet,
Cable cable) {
midimsg.header = packet[1];
midimsg.data1 = packet[2];
midimsg.data2 = packet[3];
midimsg.cable = cable;
return MIDIReadEvent::SYSCOMMON_MESSAGE;
}
MIDIReadEvent USBMIDI_Parser::handleSingleByte(MIDIUSBPacket_t packet,
Cable cable) {
rtmsg.message = packet[1];
rtmsg.cable = cable;
return MIDIReadEvent::REALTIME_MESSAGE;
}
MIDIReadEvent USBMIDI_Parser::feed(MIDIUSBPacket_t packet) {
Cable cable = Cable(packet[0] >> 4);
MIDICodeIndexNumber CIN = MIDICodeIndexNumber(packet[0] & 0xF);
if (cable.getRaw() >= USB_MIDI_NUMBER_OF_CABLES)
return MIDIReadEvent::NO_MESSAGE;
using M = MIDICodeIndexNumber;
switch (CIN) {
case M::MiscFunctionCodes: break;
case M::CableEvents: break;
case M::SystemCommon2B:
case M::SystemCommon3B: return handleSysCommon(packet, cable);
case M::SysExStartCont: return handleSysExStartCont(packet, cable);
case M::SysExEnd1B: return handleSysExEnd<1>(packet, cable);
case M::SysExEnd2B: return handleSysExEnd<2>(packet, cable);
case M::SysExEnd3B: return handleSysExEnd<3>(packet, cable);
case M::NoteOff:
case M::NoteOn:
case M::KeyPressure:
case M::ControlChange:
case M::ProgramChange:
case M::ChannelPressure:
case M::PitchBend: return handleChannelMessage(packet, cable);
case M::SingleByte: return handleSingleByte(packet, cable);
default: break;
}
return MIDIReadEvent::NO_MESSAGE;
}
MIDIReadEvent USBMIDI_Parser::resume() {
#if !IGNORE_SYSEX
if (!hasStoredPacket())
return MIDIReadEvent::NO_MESSAGE;
MIDIUSBPacket_t packet = popStoredPacket();
if (receivingSysEx(activeCable)) {
startSysEx(activeCable);
}
return feed(packet);
#else
return MIDIReadEvent::NO_MESSAGE;
#endif
}
END_CS_NAMESPACE

View File

@ -1,14 +1,22 @@
BUILD_DIR = tests/build
BUILD_DIR = build
TEST_BUILD_DIR = tests/build
dist-tests: $(BUILD_DIR)/Makefile
@$(MAKE) -C $(BUILD_DIR) examples
@echo "All examples compiled successfully."
all: $(BUILD_DIR)/Makefile
@$(MAKE) -C $(BUILD_DIR)
$(BUILD_DIR)/Makefile: tests/CMakeLists.txt tests/examples/CMakeLists.txt
$(BUILD_DIR)/Makefile: CMakeLists.txt
@mkdir -p $(BUILD_DIR)
@cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake ..
clean:
@rm -rf $(BUILD_DIR)
tests: $(TEST_BUILD_DIR)/Makefile
@$(MAKE) -C $(TEST_BUILD_DIR)
@echo "All examples compiled successfully."
.PHONY: dist-tests clean
$(TEST_BUILD_DIR)/Makefile: tests/CMakeLists.txt tests/examples/CMakeLists.txt CMakeLists.txt
@mkdir -p $(TEST_BUILD_DIR)
@cd $(TEST_BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake ..
clean:
@rm -rf $(BUILD_DIR) $(TEST_BUILD_DIR)
.PHONY: all tests clean

View File

@ -21,10 +21,22 @@
#include <MIDI_Constants/Notes.hpp>
#include <MIDI_Constants/Program_Change.hpp>
// MIDI interfaces
// MIDI interfaces — common
#include <MIDI_Interfaces/MIDI_Pipes.hpp>
// MIDI interfaces — transport-specific
#ifdef CS_MIDI_BLE
#include <MIDI_Interfaces/GenericBLEMIDI_Interface.hpp>
#include <MIDI_Interfaces/BLEMIDI/BTstackBackgroundBackend.hpp>
#include <MIDI_Interfaces/MIDI_Pipes.hpp>
#endif
#ifdef CS_MIDI_USB
#include <MIDI_Interfaces/USBMIDI_Interface.hpp>
#endif
#ifdef CS_MIDI_SERIAL
#include <MIDI_Interfaces/SerialMIDI_Interface.hpp>
#endif
// Control Surface singleton
#include <Control_Surface/Control_Surface_Class.hpp>
@ -72,7 +84,9 @@
BEGIN_CS_NAMESPACE
#ifdef CS_MIDI_BLE
using BluetoothMIDI_Interface =
GenericBLEMIDI_Interface<BTstackBackgroundBackend>;
#endif
END_CS_NAMESPACE

20
templates/tusb_config.h Normal file
View File

@ -0,0 +1,20 @@
// tusb_config.h — Minimal TinyUSB configuration for USB MIDI.
// Copy this file into your project root (alongside btstack_config.h).
// Adjust VID/PID and descriptor strings in usb_descriptors.c.
#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_
#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE
#define CFG_TUD_MIDI 1
#define CFG_TUD_MIDI_RX_BUFSIZE 64
#define CFG_TUD_MIDI_TX_BUFSIZE 64
// Disable unused device classes
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 0
#define CFG_TUD_VENDOR 0
#endif

View File

@ -0,0 +1,75 @@
// usb_descriptors.c — USB MIDI device descriptors for TinyUSB.
// Copy this file into your project and adjust VID, PID, and strings.
#include "tusb.h"
#define USB_VID 0xCafe
#define USB_PID 0x4001
#define USB_BCD 0x0200
// Device descriptor
tusb_desc_device_t const desc_device = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = USB_BCD,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = USB_VID,
.idProduct = USB_PID,
.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 (Audio + MIDI)
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI_DESC_LEN)
uint8_t const desc_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, 2, 0, CONFIG_TOTAL_LEN,
0x00, 100),
TUD_MIDI_DESCRIPTOR(1, 0, 0x01, 0x81, 64)
};
uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
return desc_configuration;
}
// String descriptors
static char const *string_desc_arr[] = {
(const char[]){0x09, 0x04}, // 0: English
"cs-midi", // 1: Manufacturer
"MIDI Controller", // 2: Product
"000001", // 3: Serial
};
static uint16_t _desc_str[32 + 1];
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
(void)langid;
uint8_t chr_count;
if (index == 0) {
memcpy(&_desc_str[1], string_desc_arr[0], 2);
chr_count = 1;
} else {
if (index >= sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))
return NULL;
const char *str = string_desc_arr[index];
chr_count = (uint8_t)strlen(str);
if (chr_count > 31) chr_count = 31;
for (uint8_t i = 0; i < chr_count; i++)
_desc_str[1 + i] = str[i];
}
_desc_str[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
return _desc_str;
}

View File

@ -1,5 +1,4 @@
# Standalone build for cs-midi examples compile-only verification.
# Invoked via: make dist-tests (from lib/cs-midi/)
set(PICO_BOARD pico2_w)
@ -13,9 +12,12 @@ set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
set(CS_MIDI_BLE ON CACHE BOOL "" FORCE)
set(CS_MIDI_USB ON CACHE BOOL "" FORCE)
set(CS_MIDI_SERIAL ON CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/cs_midi)
# cs_midi's BTstack sources need btstack_config.h from this directory
target_include_directories(cs_midi PRIVATE ${CMAKE_CURRENT_LIST_DIR})
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/examples ${CMAKE_CURRENT_BINARY_DIR}/examples)

View File

@ -1,5 +1,42 @@
# Build each example as a standalone executable to verify compilation.
# Invoked via: make dist-tests (from lib/cs-midi/)
# Compile-verify all examples as a single OBJECT library.
# Each .cpp compiles independently no linking, no main() conflicts.
# --------------------------------------------------------------------------- #
# pico_deps aggregates all pico-sdk and cs_midi include directories
# and compile definitions into one STATIC library. Everything links PRIVATE
# so INTERFACE_SOURCES compile only once here. Examples then copy the
# resolved includes/defs via generator expressions (no target_link_libraries
# on the OBJECT library, so no INTERFACE_SOURCES leak into it).
# --------------------------------------------------------------------------- #
add_library(pico_deps STATIC pico_deps_stub.c)
target_include_directories(pico_deps PUBLIC
${CMAKE_SOURCE_DIR}
${CMAKE_CURRENT_LIST_DIR}
)
target_link_libraries(pico_deps PRIVATE pico_stdlib hardware_adc hardware_sync cs_midi)
if(CS_MIDI_BLE)
target_link_libraries(pico_deps PRIVATE
pico_cyw43_arch_lwip_threadsafe_background
pico_btstack_ble
pico_btstack_cyw43
)
endif()
if(CS_MIDI_USB)
target_link_libraries(pico_deps PRIVATE tinyusb_device tinyusb_board)
endif()
if(CS_MIDI_SERIAL)
target_link_libraries(pico_deps PRIVATE hardware_uart)
endif()
# --------------------------------------------------------------------------- #
# Example sources
# --------------------------------------------------------------------------- #
set(EXAMPLE_SOURCES
# Output elements
@ -50,21 +87,31 @@ set(EXAMPLE_SOURCES
banks/bankable_note_led.cpp
)
foreach(src ${EXAMPLE_SOURCES})
get_filename_component(name ${src} NAME_WE)
set(target "example_${name}")
add_executable(${target} ${src})
target_include_directories(${target} PRIVATE ${CMAKE_SOURCE_DIR})
target_link_libraries(${target}
pico_stdlib
pico_cyw43_arch_lwip_threadsafe_background
pico_btstack_ble
pico_btstack_cyw43
hardware_adc
cs_midi
if(CS_MIDI_USB)
list(APPEND EXAMPLE_SOURCES
interfaces/usb_midi.cpp
usb_descriptors.c
)
set_target_properties(${target} PROPERTIES EXCLUDE_FROM_ALL TRUE)
list(APPEND EXAMPLE_TARGETS ${target})
endforeach()
endif()
add_custom_target(examples DEPENDS ${EXAMPLE_TARGETS})
if(CS_MIDI_SERIAL)
list(APPEND EXAMPLE_SOURCES interfaces/serial_midi.cpp)
endif()
if(CS_MIDI_BLE AND CS_MIDI_USB)
list(APPEND EXAMPLE_SOURCES interfaces/dual_midi.cpp)
endif()
# --------------------------------------------------------------------------- #
# OBJECT library compile only, no linking.
# Uses resolved includes/defs from pico_deps directly to avoid
# pico-sdk INTERFACE_SOURCES being injected into this target.
# --------------------------------------------------------------------------- #
add_library(examples OBJECT ${EXAMPLE_SOURCES})
target_include_directories(examples PRIVATE
$<TARGET_PROPERTY:pico_deps,INCLUDE_DIRECTORIES>
)
target_compile_definitions(examples PRIVATE
$<TARGET_PROPERTY:pico_deps,COMPILE_DEFINITIONS>
)

View File

@ -0,0 +1,31 @@
// Dual MIDI — BLE + USB with pipe routing.
// Elements send to both interfaces simultaneously.
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "tusb.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface ble;
USBMIDI_Interface usb;
MIDI_PipeFactory<2> pipes;
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
CCRotaryEncoder enc {{0, 2}, {16, Channel_1}, 1, 4};
int main() {
stdio_init_all();
if (cyw43_arch_init()) return 1;
tusb_init();
Control_Surface >> pipes >> ble;
Control_Surface >> pipes >> usb;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
}
}

View File

@ -0,0 +1,20 @@
// Serial MIDI — classic 5-pin DIN MIDI over UART at 31250 baud.
// Uses hardware UART0 with TX on GPIO 12, RX on GPIO 13.
#include "pico/stdlib.h"
#include <cs_midi.h>
using namespace cs;
HardwareSerialMIDI_Interface midi {uart0, 12, 13};
CCRotaryEncoder enc {{0, 2}, {16, Channel_1}, 1, 4};
int main() {
stdio_init_all();
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}

View File

@ -0,0 +1,22 @@
// USB MIDI — basic USB MIDI interface with TinyUSB.
// Requires tusb_config.h and usb_descriptors.c in the project.
// See lib/cs-midi/templates/ for reference files.
#include "pico/stdlib.h"
#include "tusb.h"
#include <cs_midi.h>
using namespace cs;
USBMIDI_Interface midi;
NoteButton button {5, {MIDI_Notes::C[4], Channel_1}};
int main() {
stdio_init_all();
tusb_init();
Control_Surface.begin();
while (true) {
Control_Surface.loop();
}
}

View File

@ -0,0 +1 @@
// Stub — pico_deps collects pico-sdk INTERFACE sources into one static lib.

View File

@ -0,0 +1,66 @@
// Minimal USB MIDI descriptors for compile-verification of examples.
#include "tusb.h"
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 = 0x4001,
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01
};
uint8_t const *tud_descriptor_device_cb(void) {
return (uint8_t const *)&desc_device;
}
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI_DESC_LEN)
uint8_t const desc_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, 2, 0, CONFIG_TOTAL_LEN, 0x00, 100),
TUD_MIDI_DESCRIPTOR(1, 0, 0x01, 0x81, 64)
};
uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
return desc_configuration;
}
static char const *string_desc_arr[] = {
(const char[]){0x09, 0x04},
"cs-midi",
"MIDI Test",
"000001",
};
static uint16_t _desc_str[32 + 1];
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
(void)langid;
uint8_t chr_count;
if (index == 0) {
memcpy(&_desc_str[1], string_desc_arr[0], 2);
chr_count = 1;
} else {
if (index >= sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))
return NULL;
const char *str = string_desc_arr[index];
chr_count = (uint8_t)strlen(str);
if (chr_count > 31) chr_count = 31;
for (uint8_t i = 0; i < chr_count; i++)
_desc_str[1 + i] = str[i];
}
_desc_str[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
return _desc_str;
}

15
tests/tusb_config.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_
#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE
#define CFG_TUD_MIDI 1
#define CFG_TUD_MIDI_RX_BUFSIZE 64
#define CFG_TUD_MIDI_TX_BUFSIZE 64
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 0
#define CFG_TUD_VENDOR 0
#endif