diff --git a/CMakeLists.txt b/CMakeLists.txt index c2cb8ba..07162eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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,27 +27,65 @@ add_library(cs_midi STATIC MIDI_Parsers/MIDI_MessageTypes.cpp MIDI_Interfaces/MIDI_Interface.cpp MIDI_Interfaces/MIDI_Pipes.cpp - MIDI_Interfaces/BLEMIDI/BLEMIDIPacketBuilder.cpp - MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp - MIDI_Interfaces/BLEMIDI/BTstack/advertising.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} ) target_compile_definitions(cs_midi PUBLIC MIDI_NUM_CABLES=1 + $<$:CS_MIDI_BLE=1> + $<$:CS_MIDI_USB=1> + $<$: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) diff --git a/MIDI_Interfaces/SerialMIDI_Interface.cpp b/MIDI_Interfaces/SerialMIDI_Interface.cpp new file mode 100644 index 0000000..e07dcc3 --- /dev/null +++ b/MIDI_Interfaces/SerialMIDI_Interface.cpp @@ -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 diff --git a/MIDI_Interfaces/SerialMIDI_Interface.hpp b/MIDI_Interfaces/SerialMIDI_Interface.hpp new file mode 100644 index 0000000..d03e6ef --- /dev/null +++ b/MIDI_Interfaces/SerialMIDI_Interface.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "MIDI_Interface.hpp" +#include + +#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 diff --git a/MIDI_Interfaces/USBMIDI_Interface.cpp b/MIDI_Interfaces/USBMIDI_Interface.cpp new file mode 100644 index 0000000..8cf93c7 --- /dev/null +++ b/MIDI_Interfaces/USBMIDI_Interface.cpp @@ -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((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((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 diff --git a/MIDI_Interfaces/USBMIDI_Interface.hpp b/MIDI_Interfaces/USBMIDI_Interface.hpp new file mode 100644 index 0000000..a5b0015 --- /dev/null +++ b/MIDI_Interfaces/USBMIDI_Interface.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "MIDI_Interface.hpp" +#include + +#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 diff --git a/MIDI_Parsers/USBMIDI_Parser.cpp b/MIDI_Parsers/USBMIDI_Parser.cpp new file mode 100644 index 0000000..ba0d82c --- /dev/null +++ b/MIDI_Parsers/USBMIDI_Parser.cpp @@ -0,0 +1,166 @@ +#include "USBMIDI_Parser.hpp" +#include + +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 +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 diff --git a/Makefile b/Makefile index cf210e6..8030751 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cs_midi.h b/cs_midi.h index d50edeb..b8b9262 100644 --- a/cs_midi.h +++ b/cs_midi.h @@ -21,10 +21,22 @@ #include #include -// MIDI interfaces +// MIDI interfaces — common +#include + +// MIDI interfaces — transport-specific +#ifdef CS_MIDI_BLE #include #include -#include +#endif + +#ifdef CS_MIDI_USB +#include +#endif + +#ifdef CS_MIDI_SERIAL +#include +#endif // Control Surface singleton #include @@ -72,7 +84,9 @@ BEGIN_CS_NAMESPACE +#ifdef CS_MIDI_BLE using BluetoothMIDI_Interface = GenericBLEMIDI_Interface; +#endif END_CS_NAMESPACE diff --git a/templates/tusb_config.h b/templates/tusb_config.h new file mode 100644 index 0000000..19029e2 --- /dev/null +++ b/templates/tusb_config.h @@ -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 diff --git a/templates/usb_descriptors.c b/templates/usb_descriptors.c new file mode 100644 index 0000000..05738ec --- /dev/null +++ b/templates/usb_descriptors.c @@ -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; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7e385c1..ccee34e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/examples/CMakeLists.txt b/tests/examples/CMakeLists.txt index 4eea5f1..50a7ef7 100644 --- a/tests/examples/CMakeLists.txt +++ b/tests/examples/CMakeLists.txt @@ -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_compile_definitions(examples PRIVATE + $ +) diff --git a/tests/examples/interfaces/dual_midi.cpp b/tests/examples/interfaces/dual_midi.cpp new file mode 100644 index 0000000..92afbc6 --- /dev/null +++ b/tests/examples/interfaces/dual_midi.cpp @@ -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 + +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(); + } +} diff --git a/tests/examples/interfaces/serial_midi.cpp b/tests/examples/interfaces/serial_midi.cpp new file mode 100644 index 0000000..5edc381 --- /dev/null +++ b/tests/examples/interfaces/serial_midi.cpp @@ -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 + +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); + } +} diff --git a/tests/examples/interfaces/usb_midi.cpp b/tests/examples/interfaces/usb_midi.cpp new file mode 100644 index 0000000..fd9e88a --- /dev/null +++ b/tests/examples/interfaces/usb_midi.cpp @@ -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 + +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(); + } +} diff --git a/tests/examples/pico_deps_stub.c b/tests/examples/pico_deps_stub.c new file mode 100644 index 0000000..f88729d --- /dev/null +++ b/tests/examples/pico_deps_stub.c @@ -0,0 +1 @@ +// Stub — pico_deps collects pico-sdk INTERFACE sources into one static lib. diff --git a/tests/examples/usb_descriptors.c b/tests/examples/usb_descriptors.c new file mode 100644 index 0000000..b0bb7fa --- /dev/null +++ b/tests/examples/usb_descriptors.c @@ -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; +} diff --git a/tests/tusb_config.h b/tests/tusb_config.h new file mode 100644 index 0000000..e0a439b --- /dev/null +++ b/tests/tusb_config.h @@ -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