Compare commits

...

2 Commits

Author SHA1 Message Date
pszsh 2187943124 doozy 2026-03-07 10:43:00 -08:00
pszsh 517cbbb318 Added tests and updated Documentation 2026-03-04 06:02:26 -08:00
52 changed files with 5404 additions and 44 deletions

View File

@ -1,6 +1,14 @@
cmake_minimum_required(VERSION 3.13) 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)
option(CS_MIDI_APPLEMIDI "Enable AppleMIDI interface (RTP-MIDI over WiFi)" OFF)
option(CS_MIDI_HID_MOUSE "Advertise as HID mouse for BLE auto-reconnect" OFF)
# Core sources always compiled
set(CS_MIDI_CORE_SOURCES
AH/Debug/Debug.cpp AH/Debug/Debug.cpp
AH/Error/Exit.cpp AH/Error/Exit.cpp
AH/PrintStream/PrintStream.cpp AH/PrintStream/PrintStream.cpp
@ -21,27 +29,88 @@ add_library(cs_midi STATIC
MIDI_Parsers/MIDI_MessageTypes.cpp MIDI_Parsers/MIDI_MessageTypes.cpp
MIDI_Interfaces/MIDI_Interface.cpp MIDI_Interfaces/MIDI_Interface.cpp
MIDI_Interfaces/MIDI_Pipes.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/BLEMIDIPacketBuilder.cpp
MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp
MIDI_Interfaces/BLEMIDI/BTstack/advertising.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()
if(CS_MIDI_APPLEMIDI)
list(APPEND CS_MIDI_SOURCES
MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp
MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.cpp
MIDI_Interfaces/AppleMIDI_Interface.cpp
)
endif()
add_library(cs_midi STATIC ${CS_MIDI_SOURCES})
target_include_directories(cs_midi PUBLIC target_include_directories(cs_midi PUBLIC
${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}
) )
if(CS_MIDI_APPLEMIDI)
target_include_directories(cs_midi PRIVATE
${CMAKE_CURRENT_LIST_DIR}/MIDI_Interfaces/AppleMIDI/vendor
)
endif()
target_compile_definitions(cs_midi PUBLIC target_compile_definitions(cs_midi PUBLIC
MIDI_NUM_CABLES=1 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>
$<$<BOOL:${CS_MIDI_APPLEMIDI}>:CS_MIDI_APPLEMIDI=1>
$<$<BOOL:${CS_MIDI_HID_MOUSE}>:CS_MIDI_HID_MOUSE=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 target_link_libraries(cs_midi
PUBLIC pico_stdlib hardware_sync hardware_adc PUBLIC pico_stdlib hardware_sync hardware_adc
)
if(CS_MIDI_BLE)
target_link_libraries(cs_midi
PRIVATE pico_btstack_ble pico_btstack_cyw43 PRIVATE pico_btstack_ble pico_btstack_cyw43
pico_cyw43_arch_lwip_threadsafe_background 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()
if(CS_MIDI_APPLEMIDI)
target_link_libraries(cs_midi
PRIVATE pico_cyw43_arch_lwip_threadsafe_background
pico_lwip_mdns
)
endif()
target_compile_features(cs_midi PUBLIC cxx_std_17) target_compile_features(cs_midi PUBLIC cxx_std_17)

View File

@ -0,0 +1,156 @@
#include "LwIPUDP.hpp"
#include "pico/cyw43_arch.h"
#include <string.h>
LwIPUDP::LwIPUDP()
: pcb_(nullptr), rxHead_(0), rxTail_(0),
current_(nullptr), txPort_(0), txBuf_(nullptr)
{
memset(rxQueue_, 0, sizeof(rxQueue_));
ip_addr_set_zero(&txAddr_);
}
LwIPUDP::~LwIPUDP() {
stop();
}
bool LwIPUDP::begin(uint16_t port) {
if (pcb_) stop();
cyw43_arch_lwip_begin();
pcb_ = udp_new();
if (!pcb_) {
cyw43_arch_lwip_end();
return false;
}
if (udp_bind(pcb_, IP_ADDR_ANY, port) != ERR_OK) {
udp_remove(pcb_);
pcb_ = nullptr;
cyw43_arch_lwip_end();
return false;
}
udp_recv(pcb_, recvCb, this);
cyw43_arch_lwip_end();
return true;
}
void LwIPUDP::stop() {
if (pcb_) {
cyw43_arch_lwip_begin();
udp_remove(pcb_);
cyw43_arch_lwip_end();
pcb_ = nullptr;
}
// Drain rx queue
while (rxTail_ != rxHead_) {
auto &pkt = rxQueue_[rxTail_ % RX_QUEUE_SIZE];
if (pkt.p) { pbuf_free(pkt.p); pkt.p = nullptr; }
rxTail_++;
}
rxHead_ = rxTail_ = 0;
current_ = nullptr;
if (txBuf_) { pbuf_free(txBuf_); txBuf_ = nullptr; }
}
void LwIPUDP::recvCb(void *arg, struct udp_pcb *, struct pbuf *p,
const ip_addr_t *addr, u16_t port) {
auto *self = static_cast<LwIPUDP *>(arg);
uint8_t next = (self->rxHead_ + 1) % RX_QUEUE_SIZE;
if (next == self->rxTail_ % RX_QUEUE_SIZE) {
// Queue full — drop
pbuf_free(p);
return;
}
auto &slot = self->rxQueue_[self->rxHead_ % RX_QUEUE_SIZE];
slot.addr = *addr;
slot.port = port;
slot.p = p;
slot.offset = 0;
self->rxHead_ = next;
}
void LwIPUDP::advanceRx() {
if (current_) {
if (current_->p) { pbuf_free(current_->p); current_->p = nullptr; }
rxTail_ = (rxTail_ + 1) % RX_QUEUE_SIZE;
current_ = nullptr;
}
}
int LwIPUDP::available() {
if (current_ && current_->p)
return current_->p->tot_len - current_->offset;
return 0;
}
int LwIPUDP::parsePacket() {
// Drop any unfinished current packet
advanceRx();
if (rxTail_ == rxHead_) return 0;
current_ = &rxQueue_[rxTail_ % RX_QUEUE_SIZE];
return current_->p ? current_->p->tot_len : 0;
}
int LwIPUDP::read(uint8_t *buf, size_t len) {
if (!current_ || !current_->p) return 0;
uint16_t remaining = current_->p->tot_len - current_->offset;
uint16_t toRead = (len < remaining) ? (uint16_t)len : remaining;
uint16_t copied = pbuf_copy_partial(current_->p, buf, toRead, current_->offset);
current_->offset += copied;
return copied;
}
int LwIPUDP::read() {
uint8_t b;
return (read(&b, 1) == 1) ? b : -1;
}
IPAddress LwIPUDP::remoteIP() {
if (current_) return IPAddress(current_->addr);
return IPAddress();
}
uint16_t LwIPUDP::remotePort() {
if (current_) return current_->port;
return 0;
}
int LwIPUDP::beginPacket(IPAddress ip, uint16_t port) {
if (txBuf_) { pbuf_free(txBuf_); txBuf_ = nullptr; }
txAddr_ = ip.toLwIP();
txPort_ = port;
return 1;
}
size_t LwIPUDP::write(const uint8_t *buf, size_t len) {
if (!len) return 0;
struct pbuf *seg = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
if (!seg) return 0;
memcpy(seg->payload, buf, len);
if (txBuf_) {
pbuf_cat(txBuf_, seg);
} else {
txBuf_ = seg;
}
return len;
}
int LwIPUDP::endPacket() {
if (!pcb_ || !txBuf_) return 0;
cyw43_arch_lwip_begin();
err_t err = udp_sendto(pcb_, txBuf_, &txAddr_, txPort_);
cyw43_arch_lwip_end();
pbuf_free(txBuf_);
txBuf_ = nullptr;
return (err == ERR_OK) ? 1 : 0;
}

View File

@ -0,0 +1,59 @@
#pragma once
#include "vendor/shim/IPAddress.h"
#include "lwip/udp.h"
#include "lwip/pbuf.h"
// Arduino WiFiUDP-compatible wrapper around lwIP raw UDP API.
// Template parameter for AppleMIDISession<LwIPUDP>.
class LwIPUDP {
public:
LwIPUDP();
~LwIPUDP();
bool begin(uint16_t port);
void stop();
// Rx — packet-oriented read
int available();
int parsePacket();
int read(uint8_t *buf, size_t len);
int read();
IPAddress remoteIP();
uint16_t remotePort();
// Tx — packet-oriented write
int beginPacket(IPAddress ip, uint16_t port);
size_t write(const uint8_t *buf, size_t len);
int endPacket();
void flush() {} // no-op; UDP sends are immediate
private:
static constexpr uint8_t RX_QUEUE_SIZE = 4;
struct RxPacket {
ip_addr_t addr;
uint16_t port;
struct pbuf *p;
uint16_t offset;
};
struct udp_pcb *pcb_;
RxPacket rxQueue_[RX_QUEUE_SIZE];
volatile uint8_t rxHead_;
volatile uint8_t rxTail_;
// Current packet being read
RxPacket *current_;
void advanceRx();
// Outgoing
ip_addr_t txAddr_;
uint16_t txPort_;
struct pbuf *txBuf_;
static void recvCb(void *arg, struct udp_pcb *pcb,
struct pbuf *p, const ip_addr_t *addr, u16_t port);
};

View File

@ -0,0 +1,7 @@
#include "AppleMIDI.h"
BEGIN_APPLEMIDI_NAMESPACE
unsigned long now = 0;
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,402 @@
#pragma once
#include "AppleMIDI_Debug.h"
// https://developer.apple.com/library/archive/documentation/Audio/Conceptual/MIDINetworkDriverProtocol/MIDI/MIDI.html
#include "shim/MIDI.h"
using namespace MIDI_NAMESPACE;
#include "shim/IPAddress.h"
#include "AppleMIDI_PlatformBegin.h"
#include "AppleMIDI_Defs.h"
#include "AppleMIDI_Settings.h"
#include "rtp_Defs.h"
#include "rtpMIDI_Defs.h"
#include "rtpMIDI_Clock.h"
#include "AppleMIDI_Participant.h"
#include "AppleMIDI_Parser.h"
#include "rtpMIDI_Parser.h"
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
extern unsigned long now;
struct AppleMIDISettings : public MIDI_NAMESPACE::DefaultSettings
{
// Packet based protocols prefer the entire message to be parsed
// as a whole.
static const bool Use1ByteParsing = false;
};
template <class UdpClass, class _Settings = DefaultSettings, class _Platform = DefaultPlatform>
class AppleMIDISession
{
typedef _Settings Settings;
typedef _Platform Platform;
// Allow these internal classes access to our private members
// to avoid access by the .ino to internal messages
friend class AppleMIDIParser<UdpClass, Settings, Platform>;
friend class rtpMIDIParser<UdpClass, Settings, Platform>;
public:
AppleMIDISession(const char *sessionName, const uint16_t port = DEFAULT_CONTROL_PORT)
{
this->port = port;
#ifdef KEEP_SESSION_NAME
strncpy(this->localName, sessionName, Settings::MaxSessionNameLen);
this->localName[Settings::MaxSessionNameLen] = '\0';
#endif
#ifdef ONE_PARTICIPANT
participant.ssrc = 0;
#endif
};
virtual ~AppleMIDISession(){};
AppleMIDISession &setHandleConnected(void (*fptr)(const ssrc_t &, const char *))
{
_connectedCallback = fptr;
return *this;
}
AppleMIDISession &setHandleDisconnected(void (*fptr)(const ssrc_t &))
{
_disconnectedCallback = fptr;
return *this;
}
#ifdef USE_EXT_CALLBACKS
AppleMIDISession &setHandleException(void (*fptr)(const ssrc_t &, const Exception &, const int32_t value))
{
_exceptionCallback = fptr;
return *this;
}
AppleMIDISession &setHandleReceivedRtp(void (*fptr)(const ssrc_t &, const Rtp_t &, const int32_t &))
{
_receivedRtpCallback = fptr;
return *this;
}
AppleMIDISession &setHandleStartReceivedMidi(void (*fptr)(const ssrc_t &))
{
_startReceivedMidiByteCallback = fptr;
return *this;
}
AppleMIDISession &setHandleReceivedMidi(void (*fptr)(const ssrc_t &, byte))
{
_receivedMidiByteCallback = fptr;
return *this;
}
AppleMIDISession &setHandleEndReceivedMidi(void (*fptr)(const ssrc_t &))
{
_endReceivedMidiByteCallback = fptr;
return *this;
}
AppleMIDISession &setHandleSentRtp(void (*fptr)(const Rtp_t &))
{
_sentRtpCallback = fptr;
return *this;
}
AppleMIDISession &setHandleSentRtpMidi(void (*fptr)(const RtpMIDI_t &))
{
_sentRtpMidiCallback = fptr;
return *this;
}
#endif
#ifdef KEEP_SESSION_NAME
const char *getName() const
{
return this->localName;
};
AppleMIDISession &setName(const char *sessionName)
{
strncpy(this->localName, sessionName, Settings::MaxSessionNameLen);
this->localName[Settings::MaxSessionNameLen] = '\0';
return *this;
};
#else
const char *getName() const
{
return nullptr;
};
AppleMIDISession &setName(const char *sessionName) { return *this; };
#endif
const uint16_t getPort() const
{
return this->port;
};
// call this method *before* calling begin()
AppleMIDISession & setPort(const uint16_t port)
{
this->port = port;
return *this;
}
const ssrc_t getSynchronizationSource() const { return this->ssrc; };
#ifdef APPLEMIDI_INITIATOR
bool sendInvite(IPAddress ip, uint16_t port = DEFAULT_CONTROL_PORT);
#endif
void sendEndSession();
public:
// Override default thruActivated. Must be false for all packet based messages
static const bool thruActivated = false;
#ifdef USE_DIRECTORY
Deque<IPAddress, Settings::MaxNumberOfComputersInDirectory> directory;
WhoCanConnectToMe whoCanConnectToMe = Anyone;
#endif
void begin()
{
_appleMIDIParser.session = this;
_rtpMIDIParser.session = this;
// analogRead(0) is not available on all platforms. The use of millis()
// as it preceded by network calls, so timing is variable and usable
// for the random generator.
randomSeed(millis());
// Each stream is distinguished by a unique SSRC value and has a unique sequence
// number and RTP timestamp space.
// this is our SSRC
//
// NOTE: Arduino random only goes to INT32_MAX (not UINT32_MAX)
this->ssrc = random(1, INT32_MAX / 2) * 2;
controlPort.begin(port);
dataPort.begin(port + 1);
rtpMidiClock.Init(rtpMidiClock.Now(), MIDI_SAMPLING_RATE_DEFAULT);
}
void end()
{
#ifdef ONE_PARTICIPANT
participant.ssrc = 0;
#endif
controlPort.stop();
dataPort.stop();
}
bool beginTransmission(MIDI_NAMESPACE::MidiType)
{
// All MIDI commands queued up in the same cycle (during 1 loop execution)
// are send in a single MIDI packet
// (The actual sending happen in the available() method, called at the start of the
// event loop() method.
//
// http://www.rfc-editor.org/rfc/rfc4696.txt
//
// 4.1. Queuing and Coding Incoming MIDI Data
// ...
// More sophisticated sending algorithms
// [GRAME] improve efficiency by coding small groups of commands into a
// single packet, at the expense of increasing the sender queuing
// latency.
//
if (!outMidiBuffer.empty())
{
// Check if there is still room for more - like for 3 bytes or so)
if ((outMidiBuffer.size() + 1 + 3) > outMidiBuffer.max_size())
writeRtpMidiToAllParticipants();
else
outMidiBuffer.push_back(0x00); // zero timestamp
}
// We can't start the writing process here, as we do not know the length
// of what we are to send (The RtpMidi protocol start with writing the
// length of the buffer). So we'll copy to a buffer in the 'write' method,
// and actually serialize for real in the endTransmission method
#ifndef ONE_PARTICIPANT
return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participants.size() > 0);
#else
return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participant.ssrc != 0);
#endif
};
void write(byte byte)
{
// do we still have place in the buffer for 1 more character?
if ((outMidiBuffer.size()) + 2 > outMidiBuffer.max_size())
{
// buffer is almost full, only 1 more character
if (MIDI_NAMESPACE::MidiType::SystemExclusive == outMidiBuffer.front())
{
// Add Sysex at the end of this partial SysEx (in the last availble slot) ...
outMidiBuffer.push_back(MIDI_NAMESPACE::MidiType::SystemExclusiveStart);
writeRtpMidiToAllParticipants();
// and start again with a fresh continuation of
// a next SysEx block.
outMidiBuffer.clear();
outMidiBuffer.push_back(MIDI_NAMESPACE::MidiType::SystemExclusiveEnd);
}
else
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, BufferFullException, 0);
#endif
return;
}
}
// store in local buffer, as we do *not* know the length of the message prior to sending
outMidiBuffer.push_back(byte);
};
void endTransmission(){};
// first things MIDI.read() calls in this method
// MIDI-read() must be called at the start of loop()
unsigned available()
{
now = millis();
#ifdef APPLEMIDI_INITIATOR
manageSessionInvites();
#endif
// All MIDI commands queued up in the same cycle (during 1 loop execution)
// are send in a single MIDI packet
if (outMidiBuffer.size() > 0)
writeRtpMidiToAllParticipants();
// assert(outMidiBuffer.size() == 0); // must be empty
if (inMidiBuffer.size() > 0)
return inMidiBuffer.size();
if (readDataPackets()) // from socket into dataBuffer
parseDataPackets(); // from dataBuffer into inMidiBuffer
if (readControlPackets()) // from socket into controlBuffer
parseControlPackets(); // from controlBuffer to AppleMIDI
manageReceiverFeedback();
manageSynchronization();
return inMidiBuffer.size();
};
byte read()
{
auto byte = inMidiBuffer.front();
inMidiBuffer.pop_front();
return byte;
};
protected:
UdpClass controlPort;
UdpClass dataPort;
private:
RtpBuffer_t controlBuffer;
RtpBuffer_t dataBuffer;
byte packetBuffer[Settings::UdpTxPacketMaxSize];
AppleMIDIParser<UdpClass, Settings, Platform> _appleMIDIParser;
rtpMIDIParser<UdpClass, Settings, Platform> _rtpMIDIParser;
connectedCallback _connectedCallback = nullptr;
disconnectedCallback _disconnectedCallback = nullptr;
#ifdef USE_EXT_CALLBACKS
startReceivedMidiByteCallback _startReceivedMidiByteCallback = nullptr;
receivedMidiByteCallback _receivedMidiByteCallback = nullptr;
endReceivedMidiByteCallback _endReceivedMidiByteCallback = nullptr;
receivedRtpCallback _receivedRtpCallback = nullptr;
sentRtpCallback _sentRtpCallback = nullptr;
sentRtpMidiCallback _sentRtpMidiCallback = nullptr;
exceptionCallback _exceptionCallback = nullptr;
#endif
// buffer for incoming and outgoing MIDI messages
MidiBuffer_t inMidiBuffer;
MidiBuffer_t outMidiBuffer;
rtpMidi_Clock rtpMidiClock;
ssrc_t ssrc = 0;
uint16_t port = DEFAULT_CONTROL_PORT;
#ifdef ONE_PARTICIPANT
Participant<Settings> participant;
#else
Deque<Participant<Settings>, Settings::MaxNumberOfParticipants> participants;
#endif
#ifdef KEEP_SESSION_NAME
char localName[Settings::MaxSessionNameLen + 1];
#endif
private:
size_t readControlPackets();
size_t readDataPackets();
void parseControlPackets();
void parseDataPackets();
void ReceivedInvitation(AppleMIDI_Invitation_t &, const amPortType &);
void ReceivedControlInvitation(AppleMIDI_Invitation_t &);
void ReceivedDataInvitation(AppleMIDI_Invitation_t &);
void ReceivedSynchronization(AppleMIDI_Synchronization_t &);
void ReceivedReceiverFeedback(AppleMIDI_ReceiverFeedback_t &);
void ReceivedEndSession(AppleMIDI_EndSession_t &);
void ReceivedBitrateReceiveLimit(AppleMIDI_BitrateReceiveLimit &);
void ReceivedInvitationAccepted(AppleMIDI_InvitationAccepted_t &, const amPortType &);
void ReceivedControlInvitationAccepted(AppleMIDI_InvitationAccepted_t &);
void ReceivedDataInvitationAccepted(AppleMIDI_InvitationAccepted_t &);
void ReceivedInvitationRejected(AppleMIDI_InvitationRejected_t &);
// rtpMIDI callback from parser
void ReceivedRtp(const Rtp_t &);
void StartReceivedMidi();
void ReceivedMidi(byte data);
void EndReceivedMidi();
// Helpers
void writeInvitation(UdpClass &, const IPAddress &, const uint16_t &, AppleMIDI_Invitation_t &, const byte *command);
void writeReceiverFeedback(const IPAddress &, const uint16_t &, AppleMIDI_ReceiverFeedback_t &);
void writeSynchronization(const IPAddress &, const uint16_t &, AppleMIDI_Synchronization_t &);
void writeEndSession(const IPAddress &, const uint16_t &, AppleMIDI_EndSession_t &);
void sendEndSession(Participant<Settings> *);
void writeRtpMidiToAllParticipants();
void writeRtpMidiBuffer(Participant<Settings> *);
void manageReceiverFeedback();
void manageSessionInvites();
void manageSynchronization();
void manageSynchronizationInitiator();
void manageSynchronizationInitiatorHeartBeat(Participant<Settings> *);
void manageSynchronizationInitiatorInvites(size_t);
void sendSynchronization(Participant<Settings> *);
#ifndef ONE_PARTICIPANT
Participant<Settings> *getParticipantBySSRC(const ssrc_t &);
Participant<Settings> *getParticipantByInitiatorToken(const uint32_t &initiatorToken);
#endif
#ifdef USE_DIRECTORY
bool IsComputerInDirectory(IPAddress) const;
#endif
};
END_APPLEMIDI_NAMESPACE
#include "AppleMIDI.hpp"
#include "AppleMIDI_PlatformEnd.h"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
#pragma once
#ifdef SerialMon
static inline void AM_DBG_SETUP(unsigned long baud) {
SerialMon.begin(baud);
const unsigned long timeout_ms = 2000;
const unsigned long start_ms = millis();
while (!SerialMon && (millis() - start_ms) < timeout_ms) {
// Avoid hard lock on boards without native USB or no host.
}
}
template <typename T>
static inline void AM_DBG_PLAIN(T last) {
SerialMon.println(last);
}
template <typename T, typename... Args>
static inline void AM_DBG_PLAIN(T head, Args... tail) {
SerialMon.print(head);
SerialMon.print(' ');
AM_DBG_PLAIN(tail...);
}
template <typename... Args>
static inline void AM_DBG(Args... args) {
AM_DBG_PLAIN(args...);
}
#else
#define AM_DBG_SETUP(...)
#define AM_DBG_PLAIN(...)
#define AM_DBG(...)
#endif

View File

@ -0,0 +1,214 @@
#pragma once
#include "AppleMIDI_Settings.h"
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
#define APPLEMIDI_LIBRARY_VERSION 0x030000
#define APPLEMIDI_LIBRARY_VERSION_MAJOR 3
#define APPLEMIDI_LIBRARY_VERSION_MINOR 0
#define APPLEMIDI_LIBRARY_VERSION_PATCH 0
#define DEFAULT_CONTROL_PORT 5004
typedef uint32_t ssrc_t;
typedef uint32_t initiatorToken_t;
typedef uint64_t timestamp_t;
union conversionBuffer
{
uint8_t value8;
uint16_t value16;
uint32_t value32;
uint64_t value64;
uint8_t buffer[8];
};
enum parserReturn: uint8_t
{
Processed,
NotSureGiveMeMoreData,
NotEnoughData,
UnexpectedData,
UnexpectedMidiData,
UnexpectedJournalData,
SessionNameVeryLong,
};
#if defined(__AVR__)
#define APPLEMIDI_PROGMEM PROGMEM
typedef const __FlashStringHelper* AppleMIDIConstStr;
#define GFP(x) (reinterpret_cast<AppleMIDIConstStr>(x))
#define GF(x) F(x)
#else
#define APPLEMIDI_PROGMEM
typedef const char* AppleMIDIConstStr;
#define GFP(x) x
#define GF(x) x
#endif
#define RtpBuffer_t Deque<byte, Settings::MaxBufferSize>
#define MidiBuffer_t Deque<byte, Settings::MaxBufferSize>
// #define USE_EXT_CALLBACKS
// #define ONE_PARTICIPANT // memory optimization
// #define USE_DIRECTORY
// By defining NO_SESSION_NAME in the sketch, you can save 100 bytes
#ifndef NO_SESSION_NAME
#define KEEP_SESSION_NAME
#endif
#define MIDI_SAMPLING_RATE_176K4HZ 176400
#define MIDI_SAMPLING_RATE_192KHZ 192000
#define MIDI_SAMPLING_RATE_DEFAULT 10000
struct Rtp;
typedef Rtp Rtp_t;
struct RtpMIDI;
typedef RtpMIDI RtpMIDI_t;
#ifdef USE_DIRECTORY
enum WhoCanConnectToMe : uint8_t
{
None,
OnlyComputersInMyDirectory,
Anyone,
};
#endif
// from: https://en.wikipedia.org/wiki/RTP-MIDI
// Apple decided to create their own protocol, imposing all parameters related to
// synchronization like the sampling frequency. This session protocol is called "AppleMIDI"
// in Wireshark software. Session management with AppleMIDI protocol requires two UDP ports,
// the first one is called "Control Port", the second one is called "Data Port". When used
// within a multithread implementation, only the Data port requires a "real-time" thread,
// the other port can be controlled by a normal priority thread. These two ports must be
// located at two consecutive locations (n / n+1); the first one can be any of the 65536
// possible ports.
enum amPortType : uint8_t
{
Control = 0,
Data = 1,
};
// from: https://en.wikipedia.org/wiki/RTP-MIDI
// AppleMIDI implementation defines two kind of session controllers: session initiators
// and session listeners. Session initiators are in charge of inviting the session listeners,
// and are responsible of the clock synchronization sequence. Session initiators can generally
// be session listeners, but some devices, such as iOS devices, can be session listeners only.
enum ParticipantKind : uint8_t
{
Listener,
Initiator,
};
enum InviteStatus : uint8_t
{
Initiating,
AwaitingControlInvitationAccepted,
ControlInvitationAccepted,
AwaitingDataInvitationAccepted,
DataInvitationAccepted,
Connected
};
enum Exception : uint8_t
{
BufferFullException,
ParseException,
UnexpectedParseException,
TooManyParticipantsException,
ComputerNotInDirectory,
NotAcceptingAnyone,
UnexpectedInviteException,
ParticipantNotFoundException,
ListenerTimeOutException,
MaxAttemptsException,
NoResponseFromConnectionRequestException,
SendPacketsDropped,
ReceivedPacketsDropped,
UdpBeginPacketFailed,
};
using connectedCallback = void (*)(const ssrc_t&, const char *);
using disconnectedCallback = void (*)(const ssrc_t&);
#ifdef USE_EXT_CALLBACKS
using startReceivedMidiByteCallback = void (*)(const ssrc_t&);
using receivedMidiByteCallback = void (*)(const ssrc_t&, byte);
using endReceivedMidiByteCallback = void (*)(const ssrc_t&);
using receivedRtpCallback = void (*)(const ssrc_t&, const Rtp_t&, const int32_t&);
using exceptionCallback = void (*)(const ssrc_t&, const Exception&, const int32_t value);
using sentRtpCallback = void (*)(const Rtp_t&);
using sentRtpMidiCallback = void (*)(const RtpMIDI_t&);
#endif
/* Signature "Magic Value" for Apple network MIDI session establishment */
static constexpr uint8_t amSignature[] = {0xff, 0xff};
/* 2 (stored in network byte order (big-endian)) */
static constexpr uint8_t amProtocolVersion[] = {0x00, 0x00, 0x00, 0x02};
/* Apple network MIDI valid commands */
static constexpr uint8_t amInvitation[] = {'I', 'N'};
static constexpr uint8_t amEndSession[] = {'B', 'Y'};
static constexpr uint8_t amSynchronization[] = {'C', 'K'};
static constexpr uint8_t amInvitationAccepted[] = {'O', 'K'};
static constexpr uint8_t amInvitationRejected[] = {'N', 'O'};
static constexpr uint8_t amReceiverFeedback[] = {'R', 'S'};
static constexpr uint8_t amBitrateReceiveLimit[] = {'R', 'L'};
const uint8_t SYNC_CK0 = 0;
const uint8_t SYNC_CK1 = 1;
const uint8_t SYNC_CK2 = 2;
typedef struct PACKED AppleMIDI_Invitation
{
initiatorToken_t initiatorToken;
ssrc_t ssrc;
#ifdef KEEP_SESSION_NAME
char sessionName[DefaultSettings::MaxSessionNameLen + 1];
const size_t getLength() const
{
return sizeof(AppleMIDI_Invitation) - (DefaultSettings::MaxSessionNameLen) + strlen(sessionName);
}
#else
const size_t getLength() const
{
return sizeof(AppleMIDI_Invitation);
}
#endif
} AppleMIDI_Invitation_t, AppleMIDI_InvitationAccepted_t, AppleMIDI_InvitationRejected_t;
typedef struct PACKED AppleMIDI_BitrateReceiveLimit
{
ssrc_t ssrc;
uint32_t bitratelimit;
} AppleMIDI_BitrateReceiveLimit_t;
typedef struct PACKED AppleMIDI_Synchronization
{
ssrc_t ssrc;
uint8_t count;
uint8_t padding[3] = {0,0,0};
timestamp_t timestamps[3];
} AppleMIDI_Synchronization_t;
typedef struct PACKED AppleMIDI_ReceiverFeedback
{
ssrc_t ssrc;
uint16_t sequenceNr;
uint16_t dummy;
} AppleMIDI_ReceiverFeedback_t;
typedef struct PACKED AppleMIDI_EndSession
{
initiatorToken_t initiatorToken;
ssrc_t ssrc;
} AppleMIDI_EndSession_t;
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,9 @@
#pragma once
#define APPLEMIDI_NAMESPACE appleMidi
#define BEGIN_APPLEMIDI_NAMESPACE \
namespace APPLEMIDI_NAMESPACE \
{
#define END_APPLEMIDI_NAMESPACE }
#define USING_NAMESPACE_APPLEMIDI using namespace APPLEMIDI_NAMESPACE;

View File

@ -0,0 +1,419 @@
#pragma once
#include "utility/Deque.h"
#include "AppleMIDI_Defs.h"
#include "AppleMIDI_Settings.h"
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
template <class UdpClass, class Settings, class Platform>
class AppleMIDISession;
template <class UdpClass, class Settings, class Platform>
class AppleMIDIParser
{
public:
AppleMIDISession<UdpClass, Settings, Platform> *session;
parserReturn parse(RtpBuffer_t &buffer, const amPortType &portType)
{
conversionBuffer cb;
uint8_t signature[2]; // Signature "Magic Value" for Apple network MIDI session establishment
uint8_t command[2]; // 16-bit command identifier (two ASCII characters, first in high 8 bits, second in low 8 bits)
size_t minimumLen = (sizeof(signature) + sizeof(command)); // Signature + Command
if (buffer.size() < minimumLen)
return parserReturn::NotSureGiveMeMoreData;
size_t i = 0;
signature[0] = buffer[i++];
signature[1] = buffer[i++];
if (0 != memcmp(signature, amSignature, sizeof(amSignature)))
{
return parserReturn::UnexpectedData;
}
command[0] = buffer[i++];
command[1] = buffer[i++];
if (0 == memcmp(command, amInvitation, sizeof(amInvitation)))
{
uint8_t protocolVersion[4];
minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t));
if (buffer.size() < minimumLen)
{
return parserReturn::NotEnoughData;
}
// 2 (stored in network byte order (big-endian))
protocolVersion[0] = buffer[i++];
protocolVersion[1] = buffer[i++];
protocolVersion[2] = buffer[i++];
protocolVersion[3] = buffer[i++];
if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion)))
{
return parserReturn::UnexpectedData;
}
AppleMIDI_Invitation invitation;
// A random number generated by the session's APPLEMIDI_INITIATOR.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
invitation.initiatorToken = __ntohl(cb.value32);
// The sender's synchronization source identifier.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
invitation.ssrc = __ntohl(cb.value32);
#ifdef KEEP_SESSION_NAME
uint16_t bi = 0;
while (i < buffer.size())
{
if (bi < Settings::MaxSessionNameLen)
invitation.sessionName[bi++] = buffer[i++];
else
i++;
}
invitation.sessionName[bi++] = '\0';
#else
while (i < buffer.size())
i++;
#endif
auto retVal = parserReturn::Processed;
// when given a Session Name and the buffer has been fully processed and the
// last character is not 'endl', then we got a very long sessionName. It will
// continue in the next memory chunk of the packet. We don't care, so indicated
// flush the remainder of the packet.
// First part if the session name is kept, processing continues
if (i > minimumLen)
if (i == buffer.size() && buffer[buffer.size() - 1] != 0x00)
retVal = parserReturn::SessionNameVeryLong;
while (i > 0)
{
buffer.pop_front(); // consume all the bytes that made up this message
--i;
}
session->ReceivedInvitation(invitation, portType);
return retVal;
}
else if (0 == memcmp(command, amEndSession, sizeof(amEndSession)))
{
uint8_t protocolVersion[4];
minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t));
if (buffer.size() < minimumLen)
return parserReturn::NotEnoughData;
// 2 (stored in network byte order (big-endian))
protocolVersion[0] = buffer[i++];
protocolVersion[1] = buffer[i++];
protocolVersion[2] = buffer[i++];
protocolVersion[3] = buffer[i++];
if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion)))
{
return parserReturn::UnexpectedData;
}
AppleMIDI_EndSession_t endSession;
// A random number generated by the session's APPLEMIDI_INITIATOR.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
endSession.initiatorToken = __ntohl(cb.value32);
// The sender's synchronization source identifier.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
endSession.ssrc = __ntohl(cb.value32);
while (i > 0)
{
buffer.pop_front(); // consume all the bytes that made up this message
--i;
}
session->ReceivedEndSession(endSession);
return parserReturn::Processed;
}
else if (0 == memcmp(command, amSynchronization, sizeof(amSynchronization)))
{
AppleMIDI_Synchronization_t synchronization;
// minimum amount : 4 bytes for sender SSRC, 1 byte for count, 3 bytes padding and 3 times 8 bytes
minimumLen += (4 + 1 + 3 + (3 * 8));
if (buffer.size() < minimumLen)
return parserReturn::NotEnoughData;
// The sender's synchronization source identifier.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
synchronization.ssrc = __ntohl(cb.value32);
synchronization.count = buffer[i++];
buffer[i++];
buffer[i++];
buffer[i++]; // padding, unused
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
cb.buffer[4] = buffer[i++];
cb.buffer[5] = buffer[i++];
cb.buffer[6] = buffer[i++];
cb.buffer[7] = buffer[i++];
synchronization.timestamps[0] = __ntohll(cb.value64);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
cb.buffer[4] = buffer[i++];
cb.buffer[5] = buffer[i++];
cb.buffer[6] = buffer[i++];
cb.buffer[7] = buffer[i++];
synchronization.timestamps[1] = __ntohll(cb.value64);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
cb.buffer[4] = buffer[i++];
cb.buffer[5] = buffer[i++];
cb.buffer[6] = buffer[i++];
cb.buffer[7] = buffer[i++];
synchronization.timestamps[2] = __ntohll(cb.value64);
while (i > 0)
{
buffer.pop_front(); // consume all the bytes that made up this message
--i;
}
session->ReceivedSynchronization(synchronization);
return parserReturn::Processed;
}
else if (0 == memcmp(command, amReceiverFeedback, sizeof(amReceiverFeedback)))
{
AppleMIDI_ReceiverFeedback_t receiverFeedback;
minimumLen += (sizeof(receiverFeedback.ssrc) + sizeof(receiverFeedback.sequenceNr) + sizeof(receiverFeedback.dummy));
if (buffer.size() < minimumLen)
return parserReturn::NotEnoughData;
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
receiverFeedback.ssrc = __ntohl(cb.value32);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
receiverFeedback.sequenceNr = __ntohs(cb.value16);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
receiverFeedback.dummy = __ntohs(cb.value16);
while (i--)
buffer.pop_front(); // consume all the bytes that made up this message
session->ReceivedReceiverFeedback(receiverFeedback);
return parserReturn::Processed;
}
#ifdef APPLEMIDI_INITIATOR
else if (0 == memcmp(command, amInvitationAccepted, sizeof(amInvitationAccepted)))
{
uint8_t protocolVersion[4];
minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t));
if (buffer.size() < minimumLen)
{
return parserReturn::NotEnoughData;
}
// 2 (stored in network byte order (big-endian))
protocolVersion[0] = buffer[i++];
protocolVersion[1] = buffer[i++];
protocolVersion[2] = buffer[i++];
protocolVersion[3] = buffer[i++];
if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion)))
{
return parserReturn::UnexpectedData;
}
AppleMIDI_InvitationAccepted_t invitationAccepted;
// A random number generated by the session's APPLEMIDI_INITIATOR.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
invitationAccepted.initiatorToken = __ntohl(cb.value32);
// The sender's synchronization source identifier.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
invitationAccepted.ssrc = __ntohl(cb.value32);
#ifdef KEEP_SESSION_NAME
uint16_t bi = 0;
while (i < buffer.size())
{
if (bi < Settings::MaxSessionNameLen)
invitationAccepted.sessionName[bi++] = buffer[i++];
else
i++;
}
invitationAccepted.sessionName[bi++] = '\0';
#else
while (i < buffer.size())
i++;
#endif
auto retVal = parserReturn::Processed;
// when given a Session Name and the buffer has been fully processed and the
// last character is not 'endl', then we got a very long sessionName. It will
// continue in the next memory chunk of the packet. We don't care, so indicated
// flush the remainder of the packet.
// First part if the session name is kept, processing continues
if (i > minimumLen)
if (i == buffer.size() && buffer[buffer.size() - 1] != 0x00)
retVal = parserReturn::SessionNameVeryLong;
while (i > 0)
{
buffer.pop_front(); // consume all the bytes that made up this message
--i;
}
session->ReceivedInvitationAccepted(invitationAccepted, portType);
return retVal;
}
else if (0 == memcmp(command, amInvitationRejected, sizeof(amInvitationRejected)))
{
uint8_t protocolVersion[4];
minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t));
if (buffer.size() < minimumLen)
{
return parserReturn::NotEnoughData;
}
// 2 (stored in network byte order (big-endian))
protocolVersion[0] = buffer[i++];
protocolVersion[1] = buffer[i++];
protocolVersion[2] = buffer[i++];
protocolVersion[3] = buffer[i++];
if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion)))
{
return parserReturn::UnexpectedData;
}
AppleMIDI_InvitationRejected_t invitationRejected;
// A random number generated by the session's APPLEMIDI_INITIATOR.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
invitationRejected.initiatorToken = __ntohl(cb.value32);
// The sender's synchronization source identifier.
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
invitationRejected.ssrc = __ntohl(cb.value32);
#ifdef KEEP_SESSION_NAME
uint16_t bi = 0;
while ((i < buffer.size()) && (buffer[i] != 0x00))
{
if (bi < Settings::MaxSessionNameLen)
invitationRejected.sessionName[bi++] = buffer[i];
i++;
}
invitationRejected.sessionName[bi++] = '\0';
#else
while ((i < buffer.size()) && (buffer[i] != 0x00))
i++;
#endif
// session name is optional.
// If i > minimum size (16), then a sessionName was provided and must include 0x00
if (i > minimumLen)
if (i == buffer.size() || buffer[i++] != 0x00)
return parserReturn::NotEnoughData;
while (i > 0)
{
buffer.pop_front(); // consume all the bytes that made up this message
--i;
}
session->ReceivedInvitationRejected(invitationRejected);
return parserReturn::Processed;
}
else if (0 == memcmp(command, amBitrateReceiveLimit, sizeof(amBitrateReceiveLimit)))
{
AppleMIDI_BitrateReceiveLimit bitrateReceiveLimit;
minimumLen += (sizeof(bitrateReceiveLimit.ssrc) + sizeof(bitrateReceiveLimit.bitratelimit));
if (buffer.size() < minimumLen)
return parserReturn::NotEnoughData;
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
bitrateReceiveLimit.ssrc = __ntohl(cb.value32);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
bitrateReceiveLimit.bitratelimit = __ntohl(cb.value32);
while (i > 0)
{
buffer.pop_front(); // consume all the bytes that made up this message
--i;
}
session->ReceivedBitrateReceiveLimit(bitrateReceiveLimit);
return parserReturn::Processed;
}
#endif
return parserReturn::UnexpectedData;
}
};
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,46 @@
#pragma once
#include "AppleMIDI_Defs.h"
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
template <class Settings>
struct Participant
{
ParticipantKind kind = Listener;
ssrc_t ssrc = 0;
IPAddress remoteIP = INADDR_NONE;
uint16_t remotePort = 0;
unsigned long receiverFeedbackStartTime = 0;
bool doReceiverFeedback = false;
uint16_t sendSequenceNr = 0; // seeded when session/participant is created
uint16_t receiveSequenceNr = 0;
unsigned long lastSyncExchangeTime = 0;
#ifdef APPLEMIDI_INITIATOR
uint8_t connectionAttempts = 0;
uint32_t initiatorToken = 0;
unsigned long lastInviteSentTime = 0;
InviteStatus invitationStatus = Initiating;
uint8_t synchronizationHeartBeats = 0;
uint8_t synchronizationCount = 0;
bool synchronizing = false;
#endif
#ifdef USE_EXT_CALLBACKS
bool firstMessageReceived = true;
uint32_t offsetEstimate = 0;
#endif
#ifdef KEEP_SESSION_NAME
char sessionName[Settings::MaxSessionNameLen + 1];
#endif
} ;
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,20 @@
#pragma once
#include "AppleMIDI_Namespace.h"
#include "utility/endian.h"
BEGIN_APPLEMIDI_NAMESPACE
#ifdef _MSC_VER
#pragma pack(push, 1)
#define PACKED
#else
#define PACKED __attribute__((__packed__))
#endif
struct DefaultPlatform
{
};
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,12 @@
#pragma once
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
#ifdef WIN32
#pragma pack(pop)
#endif
#undef PACKED
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,47 @@
#pragma once
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
struct DefaultSettings
{
// Small default to fit constrained MCUs; raise if you send larger SysEx.
static const size_t UdpTxPacketMaxSize = 24;
// MIDI buffer size in bytes; should be >= 3 * max message length.
static const size_t MaxBufferSize = 64;
static const size_t MaxSessionNameLen = 24;
static const uint8_t MaxNumberOfParticipants = 2;
static const uint8_t MaxNumberOfComputersInDirectory = 5;
// The recovery journal mechanism requires that the receiver periodically
// inform the sender of the sequence number of the most recently received packet.
// This allows the sender to reduce the size of the recovery journal, to encapsulate
// only those changes to the MIDI stream state occurring after the specified packet number.
//
// Each partner then sends cyclically to the other partner the RS message, indicating
// the last sequence number received correctly, in other words, without any gap between
// two sequence numbers. The sender can then free the memory containing old journalling data if necessary.
static const unsigned long ReceiversFeedbackThreshold = 1000;
// The initiator must initiate a new sync exchange at least once every 60 seconds.
// This value includes a small 1s slack.
// https://developer.apple.com/library/archive/documentation/Audio/Conceptual/MIDINetworkDriverProtocol/MIDI/MIDI.html
static const unsigned long CK_MaxTimeOut = 61000;
// when set to true, the lower 32-bits of the rtpClock are sent
// when set to false, 0 will be set for immediate playout.
static const bool TimestampRtpPackets = true;
static const uint8_t MaxSessionInvitesAttempts = 13;
static const uint8_t MaxSynchronizationCK0Attempts = 5;
static const unsigned long SynchronizationHeartBeat = 10000;
};
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,74 @@
#pragma once
#include <stdint.h>
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
#define MSEC_PER_SEC 1000
typedef struct rtpMidi_Clock
{
uint32_t clockRate_;
uint64_t startTime_;
uint64_t initialTimeStamp_;
uint32_t low32_;
uint32_t high32_;
void Init(uint64_t initialTimeStamp, uint32_t clockRate)
{
initialTimeStamp_ = initialTimeStamp;
clockRate_ = clockRate;
if (clockRate_ == 0)
{
clockRate_ = MIDI_SAMPLING_RATE_DEFAULT;
}
low32_ = millis();
high32_ = 0;
startTime_ = Ticks();
}
/// <summary>
/// Returns a timestamp value suitable for inclusion in a RTP packet header.
/// </summary>
uint64_t Now()
{
return CalculateCurrentTimeStamp();
}
private:
uint64_t CalculateCurrentTimeStamp()
{
return initialTimeStamp_ + (CalculateTimeSpent() * clockRate_) / MSEC_PER_SEC;
}
/// <summary>
/// Returns the time spent since the initial clock timestamp value.
/// The returned value is expressed in milliseconds.
/// </summary>
uint64_t CalculateTimeSpent()
{
return Ticks() - startTime_;
}
/// <summary>
/// millis() as a 64bit (not the default 32bit)
/// this prevents wrap around.
/// Note: rollover tracking is per instance; call Init() before use.
/// </summary>
uint64_t Ticks()
{
uint32_t new_low32 = millis();
if (new_low32 < low32_) high32_++;
low32_ = new_low32;
return (uint64_t) high32_ << 32 | low32_;
}
} RtpMidiClock_t;
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,141 @@
#pragma once
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
/* used to mask the most significant bit, which flags the start of a new MIDI-command! */
#define RTP_MIDI_COMMAND_STATUS_FLAG 0x80
/* used to mask the lower 7 bits of the single octets that make up the delta-time */
#define RTP_MIDI_DELTA_TIME_OCTET_MASK 0x7f
/* used to mask the most significant bit, which flags the extension of the delta-time */
#define RTP_MIDI_DELTA_TIME_EXTENSION 0x80
#define RTP_MIDI_CS_FLAG_B 0x80
#define RTP_MIDI_CS_FLAG_J 0x40
#define RTP_MIDI_CS_FLAG_Z 0x20
#define RTP_MIDI_CS_FLAG_P 0x10
#define RTP_MIDI_CS_MASK_SHORTLEN 0x0f
#define RTP_MIDI_CS_MASK_LONGLEN 0x0fff
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_J 0x80
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_K 0x40
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_L 0x20
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_M 0x10
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_N 0x08
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_T 0x04
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_V 0x02
#define RTP_MIDI_CJ_CHAPTER_M_FLAG_R 0x01
#define RTP_MIDI_JS_FLAG_S 0x80
#define RTP_MIDI_JS_FLAG_Y 0x40
#define RTP_MIDI_JS_FLAG_A 0x20
#define RTP_MIDI_JS_FLAG_H 0x10
#define RTP_MIDI_JS_MASK_TOTALCHANNELS 0x0f
#define RTP_MIDI_SJ_FLAG_S 0x8000
#define RTP_MIDI_SJ_FLAG_D 0x4000
#define RTP_MIDI_SJ_FLAG_V 0x2000
#define RTP_MIDI_SJ_FLAG_Q 0x1000
#define RTP_MIDI_SJ_FLAG_F 0x0800
#define RTP_MIDI_SJ_FLAG_X 0x0400
#define RTP_MIDI_SJ_MASK_LENGTH 0x03ff
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_B 0x40
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_G 0x20
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_H 0x10
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_J 0x08
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_K 0x04
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_Y 0x02
#define RTP_MIDI_SJ_CHAPTER_D_FLAG_Z 0x01
#define RTP_MIDI_SJ_CHAPTER_D_RESET_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_D_RESET_COUNT 0x7f
#define RTP_MIDI_SJ_CHAPTER_D_TUNE_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_D_TUNE_COUNT 0x7f
#define RTP_MIDI_SJ_CHAPTER_D_SONG_SEL_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_D_SONG_SEL_VALUE 0x7f
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_S 0x8000
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_C 0x4000
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_V 0x2000
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_L 0x1000
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_MASK_DSZ 0x0c00
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_MASK_LENGTH 0x03ff
#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_MASK_COUNT 0xff
#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_FLAG_C 0x40
#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_FLAG_L 0x20
#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_MASK_LENGTH 0x1f
#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_MASK_COUNT 0xff
#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_N 0x40
#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_D 0x20
#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_C 0x10
#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_T 0x80
#define RTP_MIDI_SJ_CHAPTER_Q_MASK_TOP 0x07
#define RTP_MIDI_SJ_CHAPTER_Q_MASK_CLOCK 0x07ffff
#define RTP_MIDI_SJ_CHAPTER_Q_MASK_TIMETOOLS 0xffffff
#define RTP_MIDI_SJ_CHAPTER_F_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_F_FLAG_C 0x40
#define RTP_MIDI_SJ_CHAPTER_F_FLAG_P 0x20
#define RTP_MIDI_SJ_CHAPTER_F_FLAG_Q 0x10
#define RTP_MIDI_SJ_CHAPTER_F_FLAG_D 0x08
#define RTP_MIDI_SJ_CHAPTER_F_MASK_POINT 0x07
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT0 0xf0000000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT1 0x0f000000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT2 0x00f00000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT3 0x000f0000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT4 0x0000f000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT5 0x00000f00
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT6 0x000000f0
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT7 0x0000000f
#define RTP_MIDI_SJ_CHAPTER_F_MASK_HR 0xff000000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_MN 0x00ff0000
#define RTP_MIDI_SJ_CHAPTER_F_MASK_SC 0x0000ff00
#define RTP_MIDI_SJ_CHAPTER_F_MASK_FR 0x000000ff
#define RTP_MIDI_SJ_CHAPTER_X_FLAG_S 0x80
#define RTP_MIDI_SJ_CHAPTER_X_FLAG_T 0x40
#define RTP_MIDI_SJ_CHAPTER_X_FLAG_C 0x20
#define RTP_MIDI_SJ_CHAPTER_X_FLAG_F 0x10
#define RTP_MIDI_SJ_CHAPTER_X_FLAG_D 0x08
#define RTP_MIDI_SJ_CHAPTER_X_FLAG_L 0x04
#define RTP_MIDI_SJ_CHAPTER_X_MASK_STA 0x03
#define RTP_MIDI_SJ_CHAPTER_X_MASK_TCOUNT 0xff
#define RTP_MIDI_SJ_CHAPTER_X_MASK_COUNT 0xff
#define RTP_MIDI_CJ_FLAG_S 0x800000
#define RTP_MIDI_CJ_FLAG_H 0x040000
#define RTP_MIDI_CJ_FLAG_P 0x000080
#define RTP_MIDI_CJ_FLAG_C 0x000040
#define RTP_MIDI_CJ_FLAG_M 0x000020
#define RTP_MIDI_CJ_FLAG_W 0x000010
#define RTP_MIDI_CJ_FLAG_N 0x000008
#define RTP_MIDI_CJ_FLAG_E 0x000004
#define RTP_MIDI_CJ_FLAG_T 0x000002
#define RTP_MIDI_CJ_FLAG_A 0x000001
#define RTP_MIDI_CJ_MASK_LENGTH 0x03ff00
#define RTP_MIDI_CJ_MASK_CHANNEL 0x780000
#define RTP_MIDI_CJ_CHANNEL_SHIFT 19
#define RTP_MIDI_CJ_CHAPTER_M_MASK_LENGTH 0x3f
#define RTP_MIDI_CJ_CHAPTER_N_MASK_LENGTH 0x7f00
#define RTP_MIDI_CJ_CHAPTER_N_MASK_LOW 0x00f0
#define RTP_MIDI_CJ_CHAPTER_N_MASK_HIGH 0x000f
#define RTP_MIDI_CJ_CHAPTER_E_MASK_LENGTH 0x7f
#define RTP_MIDI_CJ_CHAPTER_A_MASK_LENGTH 0x7f
typedef struct PACKED RtpMIDI
{
uint8_t flags;
} RtpMIDI_t;
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,232 @@
#pragma once
#include "utility/Deque.h"
#include "shim/midi_Defs.h"
#include "rtpMIDI_Defs.h"
#include "rtp_Defs.h"
#include "AppleMIDI_Settings.h"
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
template <class UdpClass, class Settings, class Platform>
class AppleMIDISession;
template <class UdpClass, class Settings, class Platform>
class rtpMIDIParser
{
private:
bool _rtpHeadersComplete = false;
bool _journalSectionComplete = false;
bool _channelJournalSectionComplete = false;
uint16_t midiCommandLength;
uint8_t _journalTotalChannels;
uint8_t rtpMidi_Flags = 0;
int cmdCount = 0;
uint8_t runningstatus = 0;
size_t _bytesToFlush = 0;
protected:
void debugPrintBuffer(RtpBuffer_t &buffer)
{
#if defined(DEBUG) && defined(SerialMon)
for (size_t i = 0; i < buffer.size(); i++)
{
SerialMon.print(" ");
SerialMon.print(i);
SerialMon.print(i < 10 ? " " : " ");
}
for (size_t i = 0; i < buffer.size(); i++)
{
SerialMon.print("0x");
SerialMon.print(buffer[i] < 16 ? "0" : "");
SerialMon.print(buffer[i], HEX);
SerialMon.print(" ");
}
#endif
}
public:
AppleMIDISession<UdpClass, Settings, Platform> * session;
// Parse the incoming string
// return:
// - return 0, when the parse does not have enough data
// - return a negative number, when the parser encounters invalid or
// unexpected data. The negative number indicates the amount of bytes
// that were processed. They can be purged safely
// - a positive number indicates the amount of valid bytes processed
//
parserReturn parse(RtpBuffer_t &buffer)
{
debugPrintBuffer(buffer);
conversionBuffer cb;
// [RFC3550] provides a complete description of the RTP header fields.
// In this section, we clarify the role of a few RTP header fields for
// MIDI applications. All fields are coded in network byte order (big-
// endian).
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | V |P|X| CC |M| PT | Sequence number |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Timestamp |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | MIDI command section ... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Journal section ... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
if (_rtpHeadersComplete == false)
{
auto minimumLen = sizeof(Rtp_t);
if (buffer.size() < minimumLen)
return parserReturn::NotSureGiveMeMoreData;
size_t i = 0; // todo: rename to consumed
Rtp_t rtp;
rtp.vpxcc = buffer[i++];
rtp.mpayload = buffer[i++];
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
rtp.sequenceNr = __ntohs(cb.value16);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
rtp.timestamp = __ntohl(cb.value32);
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
cb.buffer[2] = buffer[i++];
cb.buffer[3] = buffer[i++];
rtp.ssrc = __ntohl(cb.value32);
uint8_t version = RTP_VERSION(rtp.vpxcc);
#if defined(DEBUG) && defined(SerialMon)
bool padding = RTP_PADDING(rtp.vpxcc);
bool extension = RTP_EXTENSION(rtp.vpxcc);
uint8_t csrc_count = RTP_CSRC_COUNT(rtp.vpxcc);
#endif
if (RTP_VERSION_2 != version)
{
return parserReturn::UnexpectedData;
}
#if defined(DEBUG) && defined(SerialMon)
bool marker = RTP_MARKER(rtp.mpayload);
#endif
uint8_t payloadType = RTP_PAYLOAD_TYPE(rtp.mpayload);
if (PAYLOADTYPE_RTPMIDI != payloadType)
{
return parserReturn::UnexpectedData;
}
session->ReceivedRtp(rtp);
// Next byte is the flag
minimumLen += 1;
if (buffer.size() < minimumLen)
return parserReturn::NotSureGiveMeMoreData;
// 2.2. MIDI Payload (https://www.ietf.org/rfc/rfc4695.html#section-2.2)
// The payload MUST begin with the MIDI command section. The
// MIDI command section codes a (possibly empty) list of timestamped
// MIDI commands and provides the essential service of the payload
// format.
/* RTP-MIDI starts with 4 bits of flags... */
rtpMidi_Flags = buffer[i++];
// ...followed by a length-field of at least 4 bits
midiCommandLength = rtpMidi_Flags & RTP_MIDI_CS_MASK_SHORTLEN;
/* see if we have small or large len-field */
if (rtpMidi_Flags & RTP_MIDI_CS_FLAG_B)
{
minimumLen += 1;
if (buffer.size() < minimumLen)
return parserReturn::NotSureGiveMeMoreData;
// long header
uint8_t octet = buffer[i++];
midiCommandLength = (midiCommandLength << 8) | octet;
}
cmdCount = 0;
runningstatus = 0;
while (i > 0)
{
buffer.pop_front();
--i;
}
_rtpHeadersComplete = true;
// initialize the Journal Section
_journalSectionComplete = false;
_channelJournalSectionComplete = false;
_journalTotalChannels = 0;
}
// Always a MIDI section
if (midiCommandLength > 0)
{
auto retVal = decodeMIDICommandSection(buffer);
if (retVal != parserReturn::Processed) return retVal;
}
// The payload MAY also contain a journal section. The journal section
// provides resiliency by coding the recent history of the stream. A
// flag in the MIDI command section codes the presence of a journal
// section in the payload.
if (rtpMidi_Flags & RTP_MIDI_CS_FLAG_J)
{
auto retVal = decodeJournalSection(buffer);
switch (retVal) {
case parserReturn::Processed:
break;
case parserReturn::NotEnoughData:
return parserReturn::NotEnoughData;
case parserReturn::UnexpectedJournalData:
_rtpHeadersComplete = false;
_journalSectionComplete = false;
_channelJournalSectionComplete = false;
_journalTotalChannels = 0;
default:
// Reset all journal state on any non-recoverable error to avoid
// leaking partial state into the next packet.
_rtpHeadersComplete = false;
_journalSectionComplete = false;
_channelJournalSectionComplete = false;
_journalTotalChannels = 0;
return retVal;
}
}
_rtpHeadersComplete = false;
return parserReturn::Processed;
}
#include "rtpMIDI_Parser_JournalSection.hpp"
#include "rtpMIDI_Parser_CommandSection.hpp"
};
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,279 @@
// https://www.ietf.org/rfc/rfc4695.html#section-3
parserReturn decodeMIDICommandSection(RtpBuffer_t &buffer)
{
debugPrintBuffer(buffer);
// https://www.ietf.org/rfc/rfc4695.html#section-3.2
//
// The first MIDI channel command in the MIDI list MUST include a status
// octet.Running status coding, as defined in[MIDI], MAY be used for
// all subsequent MIDI channel commands in the list.As in[MIDI],
// System Commonand System Exclusive messages(0xF0 ... 0xF7) cancel
// the running status state, but System Real - time messages(0xF8 ...
// 0xFF) do not affect the running status state. All System commands in
// the MIDI list MUST include a status octet.
// As we note above, the first channel command in the MIDI list MUST
// include a status octet.However, the corresponding command in the
// original MIDI source data stream might not have a status octet(in
// this case, the source would be coding the command using running
// status). If the status octet of the first channel command in the
// MIDI list does not appear in the source data stream, the P(phantom)
// header bit MUST be set to 1. In all other cases, the P bit MUST be
// set to 0.
//
// Note that the P bit describes the MIDI source data stream, not the
// MIDI list encoding; regardless of the state of the P bit, the MIDI
// list MUST include the status octet.
//
// As receivers MUST be able to decode running status, sender
// implementors should feel free to use running status to improve
// bandwidth efficiency. However, senders SHOULD NOT introduce timing
// jitter into an existing MIDI command stream through an inappropriate
// use or removal of running status coding. This warning primarily
// applies to senders whose RTP MIDI streams may be transcoded onto a
// MIDI 1.0 DIN cable[MIDI] by the receiver : both the timestamps and
// the command coding (running status or not) must comply with the
// physical restrictions of implicit time coding over a slow serial
// line.
// (lathoub: RTP_MIDI_CS_FLAG_P((phantom) not implemented
/* Multiple MIDI-commands might follow - the exact number can only be discovered by really decoding the commands! */
while (midiCommandLength)
{
/* for the first command we only have a delta-time if Z-Flag is set */
if ((cmdCount) || (rtpMidi_Flags & RTP_MIDI_CS_FLAG_Z))
{
size_t consumed = 0;
auto retVal = decodeTime(buffer, consumed);
if (retVal != parserReturn::Processed) return retVal;
midiCommandLength -= consumed;
while (consumed--)
buffer.pop_front();
}
if (midiCommandLength > 0)
{
cmdCount++;
size_t consumed = 0;
auto retVal = decodeMidi(buffer, runningstatus, consumed);
if (retVal == parserReturn::NotEnoughData) {
cmdCount = 0; // avoid first command again
return retVal;
}
midiCommandLength -= consumed;
while (consumed--)
buffer.pop_front();
}
}
return parserReturn::Processed;
}
parserReturn decodeTime(RtpBuffer_t &buffer, size_t &consumed)
{
debugPrintBuffer(buffer);
uint32_t deltatime = 0;
/* RTP-MIDI deltatime is "compressed" using only the necessary amount of octets */
for (uint8_t j = 0; j < 4; j++)
{
if (buffer.size() < 1)
return parserReturn::NotEnoughData;
uint8_t octet = buffer[consumed];
deltatime = (deltatime << 7) | (octet & RTP_MIDI_DELTA_TIME_OCTET_MASK);
consumed++;
if ((octet & RTP_MIDI_DELTA_TIME_EXTENSION) == 0)
break;
}
return parserReturn::Processed;
}
parserReturn decodeMidi(RtpBuffer_t &buffer, uint8_t &runningstatus, size_t &consumed)
{
debugPrintBuffer(buffer);
if (buffer.size() < 1)
return parserReturn::NotEnoughData;
auto octet = buffer.front();
/* MIDI realtime-data -> one octet -- unlike serial-wired MIDI realtime-commands in RTP-MIDI will
* not be intermingled with other MIDI-commands, so we handle this case right here and return */
if (octet >= 0xf8)
{
consumed = 1;
session->StartReceivedMidi();
session->ReceivedMidi(octet);
session->EndReceivedMidi();
return parserReturn::Processed;
}
/* see if this first octet is a status message */
if ((octet & RTP_MIDI_COMMAND_STATUS_FLAG) == 0)
{
/* if we have no running status yet -> error */
if (((runningstatus)&RTP_MIDI_COMMAND_STATUS_FLAG) == 0)
{
return parserReturn::Processed;
}
/* our first octet is "virtual" coming from a preceding MIDI-command,
* so actually we have not really consumed anything yet */
octet = runningstatus;
}
else
{
/* Let's see how this octet influences our running-status */
/* if we have a "normal" MIDI-command then the new status replaces the current running-status */
if (octet < 0xf0)
{
runningstatus = octet;
}
else
{
/* system-realtime-commands maintain the current running-status
* other system-commands clear the running-status, since we
* already handled realtime, we can reset it here */
runningstatus = 0;
}
consumed++;
}
/* non-system MIDI-commands encode the command in the high nibble and the channel
* in the low nibble - so we will take care of those cases next */
if (octet < 0xf0)
{
switch (octet & 0xf0)
{
case MIDI_NAMESPACE::MidiType::NoteOff:
consumed += 2;
break;
case MIDI_NAMESPACE::MidiType::NoteOn:
consumed += 2;
break;
case MIDI_NAMESPACE::MidiType::AfterTouchPoly:
consumed += 2;
break;
case MIDI_NAMESPACE::MidiType::ControlChange:
consumed += 2;
break;
case MIDI_NAMESPACE::MidiType::ProgramChange:
consumed += 1;
break;
case MIDI_NAMESPACE::MidiType::AfterTouchChannel:
consumed += 1;
break;
case MIDI_NAMESPACE::MidiType::PitchBend:
consumed += 2;
break;
}
if (buffer.size() < consumed) {
return parserReturn::NotEnoughData;
}
session->StartReceivedMidi();
for (size_t j = 0; j < consumed; j++)
session->ReceivedMidi(buffer[j]);
session->EndReceivedMidi();
return parserReturn::Processed;
}
/* Here we catch the remaining system-common commands */
switch (octet)
{
case MIDI_NAMESPACE::MidiType::SystemExclusiveStart:
case MIDI_NAMESPACE::MidiType::SystemExclusiveEnd:
decodeMidiSysEx(buffer, consumed);
break;
case MIDI_NAMESPACE::MidiType::TimeCodeQuarterFrame:
consumed += 1;
break;
case MIDI_NAMESPACE::MidiType::SongPosition:
consumed += 2;
break;
case MIDI_NAMESPACE::MidiType::SongSelect:
consumed += 1;
break;
case MIDI_NAMESPACE::MidiType::TuneRequest:
break;
}
if (buffer.size() < consumed)
return parserReturn::NotEnoughData;
session->StartReceivedMidi();
for (size_t j = 0; j < consumed; j++)
session->ReceivedMidi(buffer[j]);
session->EndReceivedMidi();
return parserReturn::Processed;
}
parserReturn decodeMidiSysEx(RtpBuffer_t &buffer, size_t &consumed)
{
debugPrintBuffer(buffer);
// consumed = 1; // beginning SysEx Token is not counted (as it could remain)
size_t i = 1; // 0 = start of SysEx, so we can start with 1
while (i < buffer.size())
{
consumed++;
auto octet = buffer[i++];
AM_DBG("0x");
AM_DBG(octet < 16 ? "0" : "");
AM_DBG(octet, HEX);
AM_DBG(" ");
if (octet == MIDI_NAMESPACE::MidiType::SystemExclusiveEnd) // Complete message
{
return parserReturn::Processed;
}
else if (octet == MIDI_NAMESPACE::MidiType::SystemExclusiveStart) // Start
{
return parserReturn::Processed;
}
}
// begin of the SysEx is found, not the end.
// so transmit what we have, add a stop-token at the end,
// remove the bytes, modify the length and indicate
// not-enough data, so we buffer gets filled with the remaining bytes.
// to compensate for adding the sysex at the end.
consumed--;
// send MIDI data
session->StartReceivedMidi();
for (size_t j = 0; j < consumed; j++)
session->ReceivedMidi(buffer[j]);
session->ReceivedMidi(MIDI_NAMESPACE::MidiType::SystemExclusiveStart);
session->EndReceivedMidi();
// Remove the bytes that were submitted
for (size_t j = 0; j < consumed; j++)
buffer.pop_front();
// Start a new SysEx train
buffer.push_front(MIDI_NAMESPACE::MidiType::SystemExclusiveEnd);
midiCommandLength -= consumed;
midiCommandLength += 1; // for adding the manual SysEx SystemExclusiveEnd in front
// indicates split SysEx
consumed = buffer.max_size() + 1;
return parserReturn::Processed;
}

View File

@ -0,0 +1,180 @@
// The recovery journal is the default resiliency tool for unreliable
// transport. In this section, we normatively define the roles that
// senders and receivers play in the recovery journal system.
//
// This section introduces the structure of the recovery journal and
// defines the bitfields of recovery journal headers. Appendices A and
// B complete the bitfield definition of the recovery journal.
//
// The recovery journal has a three-level structure:
//
// o Top-level header.
//
// o Channel and system journal headers. These headers encode recovery
// information for a single voice channel (channel journal) or for
// all system commands (system journal).
//
// o Chapters. Chapters describe recovery information for a single
// MIDI command type.
//
parserReturn decodeJournalSection(RtpBuffer_t &buffer)
{
size_t minimumLen = 0;
conversionBuffer cb;
if (false == _journalSectionComplete)
{
size_t i = 0;
// Minimum size for the Journal section is 3
minimumLen += 3;
if (buffer.size() < minimumLen)
return parserReturn::NotEnoughData;
/* lets get the main flags from the recovery journal header */
uint8_t flags = buffer[i++];
// The 16-bit Checkpoint Packet Seqnum header field codes the sequence
// number of the checkpoint packet for this journal, in network byte
// order (big-endian). The choice of the checkpoint packet sets the
// depth of the checkpoint history for the journal (defined in Appendix A.1).
//
// Receivers may use the Checkpoint Packet Seqnum field of the packet
// that ends a loss event to verify that the journal checkpoint history
// covers the entire loss event. The checkpoint history covers the loss
// event if the Checkpoint Packet Seqnum field is less than or equal to
// one plus the highest RTP sequence number previously received on the
// stream (modulo 2^16).
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
// uint16_t checkPoint = __ntohs(cb.value16); ; // unused
// (RFC 4695, 5 Recovery Journal Format)
// If A and Y are both zero, the recovery journal only contains its 3-
// octet header and is considered to be an "empty" journal.
if ((flags & RTP_MIDI_JS_FLAG_Y) == 0 && (flags & RTP_MIDI_JS_FLAG_A) == 0)
{
// Big fixed by @hugbug
while (minimumLen-- > 0)
buffer.pop_front();
_journalSectionComplete = true;
return parserReturn::Processed;
}
// By default, the payload format does not use enhanced Chapter C
// encoding. In this default case, the H bit MUST be set to 0 for all
// packets in the stream.
if (flags & RTP_MIDI_JS_FLAG_H)
{
// The H bit indicates if MIDI channels in the stream have been
// configured to use the enhanced Chapter C encoding
}
// The S (single-packet loss) bit appears in most recovery journal
// structures, including the recovery journal header. The S bit helps
// receivers efficiently parse the recovery journal in the common case
// of the loss of a single packet.
if (flags & RTP_MIDI_JS_FLAG_S)
{
// special encoding
}
// If the Y header bit is set to 1, the system journal appears in the
// recovery journal, directly following the recovery journal header.
if (flags & RTP_MIDI_JS_FLAG_Y)
{
minimumLen += 2;
if (buffer.size() < minimumLen)
{
return parserReturn::NotEnoughData;
}
cb.buffer[0] = buffer[i++];
cb.buffer[1] = buffer[i++];
uint16_t systemflags = __ntohs(cb.value16);
uint16_t sysjourlen = systemflags & RTP_MIDI_SJ_MASK_LENGTH;
uint16_t remainingBytes = sysjourlen - 2;
minimumLen += remainingBytes;
if (buffer.size() < minimumLen)
{
return parserReturn::NotEnoughData;
}
i += remainingBytes;
}
// If the A header bit is set to 1, the recovery journal ends with a
// list of (TOTCHAN + 1) channel journals (the 4-bit TOTCHAN header
// field is interpreted as an unsigned integer).
if (flags & RTP_MIDI_JS_FLAG_A)
{
/* At the same place we find the total channels encoded in the channel journal */
_journalTotalChannels = (flags & RTP_MIDI_JS_MASK_TOTALCHANNELS) + 1;
}
while (i-- > 0) // is that the same as while (i--) ??
buffer.pop_front();
_journalSectionComplete = true;
}
// iterate through all the channels specified in header
while (_journalTotalChannels > 0)
{
if (false == _channelJournalSectionComplete) {
if (buffer.size() < 3)
return parserReturn::NotEnoughData;
// 3 bytes for channel journal
cb.buffer[0] = 0x00;
cb.buffer[1] = buffer[0];
cb.buffer[2] = buffer[1];
cb.buffer[3] = buffer[2];
uint32_t chanflags = __ntohl(cb.value32);
bool S_flag = (chanflags & RTP_MIDI_CJ_FLAG_S) == 1;
uint8_t channelNr = (chanflags & RTP_MIDI_CJ_MASK_CHANNEL) >> RTP_MIDI_CJ_CHANNEL_SHIFT;
bool H_flag = (chanflags & RTP_MIDI_CJ_FLAG_H) == 1;
uint8_t chanjourlen = (chanflags & RTP_MIDI_CJ_MASK_LENGTH) >> 8;
if ((chanflags & RTP_MIDI_CJ_FLAG_P)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_C)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_M)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_W)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_N)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_E)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_T)) {
}
if ((chanflags & RTP_MIDI_CJ_FLAG_A)) {
}
_bytesToFlush = chanjourlen;
_channelJournalSectionComplete = true;
}
while (buffer.size() > 0 && _bytesToFlush > 0) {
_bytesToFlush--;
buffer.pop_front();
}
if (_bytesToFlush > 0) {
return parserReturn::NotEnoughData;
}
_journalTotalChannels--;
}
return parserReturn::Processed;
}

View File

@ -0,0 +1,55 @@
#pragma once
#include "AppleMIDI_Namespace.h"
BEGIN_APPLEMIDI_NAMESPACE
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | V |P|X| CC |M| PT | Sequence number |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
#define RTP_VERSION_2 2
// first octet
#define RTP_P_FIELD 0x20
#define RTP_X_FIELD 0x10
#define RTP_CC_FIELD 0x0F
// second octet
#define RTP_M_FIELD 0x80
#define RTP_PT_FIELD 0x7F
/* magic number */
#define PAYLOADTYPE_RTPMIDI 97
/* Version is the first 2 bits of the first octet*/
#define RTP_VERSION(octet) (((octet) >> 6) & 0x03)
/* Padding is the third bit; No need to shift, because true is any value
other than 0! */
#define RTP_PADDING(octet) ((octet)&RTP_P_FIELD)
/* Extension bit is the fourth bit */
#define RTP_EXTENSION(octet) ((octet)&RTP_X_FIELD)
/* CSRC count is the last four bits */
#define RTP_CSRC_COUNT(octet) ((octet)&RTP_CC_FIELD)
/* Marker is the first bit of the second octet */
#define RTP_MARKER(octet) ((octet)&RTP_M_FIELD)
/* Payload type is the last 7 bits */
#define RTP_PAYLOAD_TYPE(octet) ((octet)&RTP_PT_FIELD)
typedef struct PACKED Rtp
{
uint8_t vpxcc;
uint8_t mpayload;
uint16_t sequenceNr;
uint32_t timestamp;
ssrc_t ssrc;
} Rtp_t;
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,41 @@
#pragma once
// Minimal IPAddress for Arduino-AppleMIDI-Library on pico-sdk / lwIP.
// Only implements what the vendored code actually uses.
#include <stdint.h>
#include <string.h>
#include "lwip/ip_addr.h"
#ifndef INADDR_NONE
#define INADDR_NONE ((uint32_t)0xFFFFFFFFUL)
#endif
class IPAddress {
public:
IPAddress() : addr_(INADDR_NONE) {}
IPAddress(uint32_t raw) : addr_(raw) {}
IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
: addr_((uint32_t)a | ((uint32_t)b << 8) |
((uint32_t)c << 16) | ((uint32_t)d << 24)) {}
IPAddress(const ip_addr_t &lwip) : addr_(ip_addr_get_ip4_u32(&lwip)) {}
operator uint32_t() const { return addr_; }
bool operator==(const IPAddress &rhs) const { return addr_ == rhs.addr_; }
bool operator!=(const IPAddress &rhs) const { return addr_ != rhs.addr_; }
ip_addr_t toLwIP() const {
ip_addr_t a;
ip_addr_set_ip4_u32(&a, addr_);
return a;
}
uint8_t operator[](int idx) const {
return (addr_ >> (idx * 8)) & 0xFF;
}
private:
uint32_t addr_;
};

View File

@ -0,0 +1,21 @@
#pragma once
// Shim replacing FortySevenEffects MIDI.h for Arduino-AppleMIDI-Library
// on pico-sdk. Provides the subset of types actually referenced.
#include "midi_Defs.h"
#include <platform/pico_shim.h>
#include <stdint.h>
#include <stdlib.h>
#ifndef byte
typedef uint8_t byte;
#endif
inline void randomSeed(unsigned long seed) { srand((unsigned int)seed); }
inline long random(long min_val, long max_val) {
if (min_val >= max_val) return min_val;
return min_val + (rand() % (max_val - min_val));
}

View File

@ -0,0 +1,42 @@
#pragma once
// Minimal shim for MIDI_NAMESPACE::MidiType used by Arduino-AppleMIDI-Library.
// Replaces the FortySevenEffects MIDI library dependency.
#include <stdint.h>
#ifndef MIDI_NAMESPACE
#define MIDI_NAMESPACE applemidi_types
#endif
namespace MIDI_NAMESPACE {
enum MidiType : uint8_t {
InvalidType = 0x00,
NoteOff = 0x80,
NoteOn = 0x90,
AfterTouchPoly = 0xA0,
ControlChange = 0xB0,
ProgramChange = 0xC0,
AfterTouchChannel = 0xD0,
PitchBend = 0xE0,
SystemExclusive = 0xF0,
SystemExclusiveStart = 0xF0,
TimeCodeQuarterFrame = 0xF1,
SongPosition = 0xF2,
SongSelect = 0xF3,
TuneRequest = 0xF6,
SystemExclusiveEnd = 0xF7,
Clock = 0xF8,
Start = 0xFA,
Continue = 0xFB,
Stop = 0xFC,
ActiveSensing = 0xFE,
SystemReset = 0xFF,
};
struct DefaultSettings {
static const bool Use1ByteParsing = true;
};
} // namespace MIDI_NAMESPACE

View File

@ -0,0 +1,277 @@
#pragma once
#include <string.h>
BEGIN_APPLEMIDI_NAMESPACE
template<typename T, size_t Size>
class Deque {
// class iterator;
private:
int _head, _tail;
T _data[Size];
public:
Deque()
{
clear();
};
size_t free();
const size_t size() const;
const size_t max_size() const;
T & front();
const T & front() const;
T & back();
const T & back() const;
void push_front(const T &);
void push_back(const T &);
size_t push_back(const T *, size_t);
size_t copy_out(T *, size_t) const;
void pop_front();
void pop_back();
T& operator[](size_t);
const T& operator[](size_t) const;
T& at(size_t);
const T& at(size_t) const;
void clear();
// iterator begin();
// iterator end();
void erase(size_t);
void erase(size_t, size_t);
bool empty() const {
return size() == 0;
}
bool full() const {
return (size() == Size);
}
};
template<typename T, size_t Size>
size_t Deque<T, Size>::free()
{
return Size - size();
}
template<typename T, size_t Size>
const size_t Deque<T, Size>::size() const
{
if (_tail < 0)
return 0; // empty
else if (_head > _tail)
return _head - _tail;
else
return Size - _tail + _head;
}
template<typename T, size_t Size>
const size_t Deque<T, Size>::max_size() const
{
return Size;
}
template<typename T, size_t Size>
T & Deque<T, Size>::front()
{
return _data[_tail];
}
template<typename T, size_t Size>
const T & Deque<T, Size>::front() const
{
return _data[_tail];
}
template<typename T, size_t Size>
T & Deque<T, Size>::back()
{
int idx = _head - 1;
if (idx < 0) idx = Size - 1;
return _data[idx];
}
template<typename T, size_t Size>
const T & Deque<T, Size>::back() const
{
int idx = _head - 1;
if (idx < 0) idx = Size - 1;
return _data[idx];
}
template<typename T, size_t Size>
void Deque<T, Size>::push_front(const T &value)
{
//if container is full, do nothing.
if (free()){
if (--_tail < 0)
_tail = Size - 1;
_data[_tail] = value;
}
}
template<typename T, size_t Size>
void Deque<T, Size>::push_back(const T &value)
{
//if container is full, do nothing.
if (free()){
_data[_head] = value;
if (empty())
_tail = _head;
if (++_head >= Size)
_head %= Size;
}
}
template<typename T, size_t Size>
size_t Deque<T, Size>::push_back(const T *values, size_t count)
{
if (values == nullptr || count == 0)
return 0;
const size_t available = free();
if (available == 0)
return 0;
const size_t toWrite = (count < available) ? count : available;
if (empty())
_tail = _head;
size_t first = toWrite;
if (_head + first > Size)
first = Size - _head;
memcpy(&_data[_head], values, first * sizeof(T));
_head = (_head + first) % Size;
const size_t remaining = toWrite - first;
if (remaining > 0)
{
memcpy(&_data[_head], values + first, remaining * sizeof(T));
_head = (_head + remaining) % Size;
}
return toWrite;
}
template<typename T, size_t Size>
size_t Deque<T, Size>::copy_out(T *dest, size_t count) const
{
if (dest == nullptr || count == 0)
return 0;
const size_t available = size();
if (available == 0)
return 0;
const size_t toCopy = (count < available) ? count : available;
const size_t start = (size_t)_tail;
size_t first = toCopy;
if (start + first > Size)
first = Size - start;
memcpy(dest, &_data[start], first * sizeof(T));
const size_t remaining = toCopy - first;
if (remaining > 0)
memcpy(dest + first, &_data[0], remaining * sizeof(T));
return toCopy;
}
template<typename T, size_t Size>
void Deque<T, Size>::pop_front() {
if (empty()) // if empty, do nothing.
return;
if (++_tail >= Size)
_tail %= Size;
if (_tail == _head)
clear();
}
template<typename T, size_t Size>
void Deque<T, Size>::pop_back() {
if (empty()) // if empty, do nothing.
return;
if (--_head < 0)
_head = Size - 1;
if (_head == _tail) //now buffer is empty
clear();
}
template<typename T, size_t Size>
void Deque<T, Size>::erase(size_t position) {
if (position >= size()) // out-of-range!
return; // do nothing.
for (size_t i = position; i < size() - 1; i++){
at(i) = at(i + 1);
}
pop_back();
}
template<typename T, size_t Size>
void Deque<T, Size>::erase(size_t first, size_t last) {
if (first > last // invalid arguments
|| first >= size()) // out-of-range
return; //do nothing.
size_t tgt = first;
for (size_t i = last + 1; i < size(); i++){
at(tgt++) = at(i);
}
for (size_t i = first; i <= last; i++){
pop_back();
}
}
template<typename T, size_t Size>
T& Deque<T, Size>::operator[](size_t index)
{
auto i = _tail + index;
if (i >= Size)
i %= Size;
return _data[i];
}
template<typename T, size_t Size>
const T& Deque<T, Size>::operator[](size_t index) const
{
auto i = _tail + index;
if (i >= Size)
i %= Size;
return _data[i];
}
template<typename T, size_t Size>
T& Deque<T, Size>::at(size_t index)
{
auto i = _tail + index;
if (i >= Size)
i %= Size;
return _data[i];
}
template<typename T, size_t Size>
const T& Deque<T, Size>::at(size_t index) const
{
auto i = _tail + index;
if (i >= Size)
i %= Size;
return _data[i];
}
template<typename T, size_t Size>
void Deque<T, Size>::clear()
{
_tail = -1;
_head = 0;
}
END_APPLEMIDI_NAMESPACE

View File

@ -0,0 +1,78 @@
#pragma once
#if !defined(_BYTE_ORDER)
#define _BIG_ENDIAN 4321
#define _LITTLE_ENDIAN 1234
#define TEST_LITTLE_ENDIAN (((union { unsigned x; unsigned char c; }){1}).c)
#ifdef TEST_LITTLE_ENDIAN
#define _BYTE_ORDER _LITTLE_ENDIAN
#else
#define _BYTE_ORDER _BIG_ENDIAN
#endif
#undef TEST_LITTLE_ENDIAN
#include <stdint.h>
#ifdef __GNUC__
#define __bswap16(_x) __builtin_bswap16(_x)
#define __bswap32(_x) __builtin_bswap32(_x)
#define __bswap64(_x) __builtin_bswap64(_x)
#else /* __GNUC__ */
static __inline __uint16_t
__bswap16(__uint16_t _x)
{
return ((__uint16_t)((_x >> 8) | ((_x << 8) & 0xff00)));
}
static __inline __uint32_t
__bswap32(__uint32_t _x)
{
return ((__uint32_t)((_x >> 24) | ((_x >> 8) & 0xff00) |
((_x << 8) & 0xff0000) | ((_x << 24) & 0xff000000)));
}
static __inline __uint64_t
__bswap64(__uint64_t _x)
{
return ((__uint64_t)((_x >> 56) | ((_x >> 40) & 0xff00) |
((_x >> 24) & 0xff0000) | ((_x >> 8) & 0xff000000) |
((_x << 8) & ((__uint64_t)0xff << 32)) |
((_x << 24) & ((__uint64_t)0xff << 40)) |
((_x << 40) & ((__uint64_t)0xff << 48)) | ((_x << 56))));
}
#endif /* !__GNUC__ */
#ifndef __machine_host_to_from_network_defined
#if _BYTE_ORDER == _LITTLE_ENDIAN
#define __ntohs(x) __bswap16(x)
#define __htons(x) __bswap16(x)
#define __ntohl(x) __bswap32(x)
#define __htonl(x) __bswap32(x)
#define __ntohll(x) __bswap64(x)
#define __htonll(x) __bswap64(x)
#else // BIG_ENDIAN
#define __ntohl(x) ((uint32_t)(x))
#define __ntohs(x) ((uint16_t)(x))
#define __htonl(x) ((uint32_t)(x))
#define __htons(x) ((uint16_t)(x))
#define __ntohll(x) ((uint64_t)(x))
#define __htonll(x) ((uint64_t)(x))
#endif
#endif /* __machine_host_to_from_network_defined */
#endif /* _BYTE_ORDER */
#ifndef __machine_host_to_from_network_defined
#if _BYTE_ORDER == _LITTLE_ENDIAN
#define __ntohll(x) __bswap64(x)
#define __htonll(x) __bswap64(x)
#else // BIG_ENDIAN
#define __ntohll(x) ((uint64_t)(x))
#define __htonll(x) ((uint64_t)(x))
#endif
#endif /* __machine_host_to_from_network_defined */

View File

@ -0,0 +1,80 @@
#include "AppleMIDI_Interface.hpp"
#include "lwip/apps/mdns.h"
#include "pico/cyw43_arch.h"
BEGIN_CS_NAMESPACE
AppleMIDI_Interface::AppleMIDI_Interface(const char *name, uint16_t port)
: session_(name, port), sessionName_(name), port_(port) {}
void AppleMIDI_Interface::begin() {
session_.setName(sessionName_);
session_.setPort(port_);
session_.begin();
initMDNS();
}
void AppleMIDI_Interface::initMDNS() {
cyw43_arch_lwip_begin();
mdns_resp_init();
struct netif *nif = &cyw43_state.netif[CYW43_ITF_STA];
mdns_resp_add_netif(nif, sessionName_);
mdns_resp_add_service(nif, sessionName_, "_apple-midi",
DNSSD_PROTO_UDP, port_, nullptr, nullptr);
cyw43_arch_lwip_end();
}
void AppleMIDI_Interface::update() {
session_.available();
MIDI_Interface::updateIncoming(this);
}
MIDIReadEvent AppleMIDI_Interface::read() {
return parser_.pull(AppleMIDIBytePuller{session_});
}
ChannelMessage AppleMIDI_Interface::getChannelMessage() const {
return parser_.getChannelMessage();
}
SysCommonMessage AppleMIDI_Interface::getSysCommonMessage() const {
return parser_.getSysCommonMessage();
}
RealTimeMessage AppleMIDI_Interface::getRealTimeMessage() const {
return parser_.getRealTimeMessage();
}
SysExMessage AppleMIDI_Interface::getSysExMessage() const {
return parser_.getSysExMessage();
}
void AppleMIDI_Interface::sendChannelMessageImpl(ChannelMessage msg) {
session_.beginTransmission(MIDI_NAMESPACE::MidiType::InvalidType);
session_.write(msg.header);
session_.write(msg.data1);
if (msg.hasTwoDataBytes())
session_.write(msg.data2);
}
void AppleMIDI_Interface::sendSysCommonImpl(SysCommonMessage msg) {
session_.beginTransmission(MIDI_NAMESPACE::MidiType::InvalidType);
session_.write(msg.header);
uint8_t ndata = msg.getNumberOfDataBytes();
if (ndata >= 1) session_.write(msg.data1);
if (ndata >= 2) session_.write(msg.data2);
}
void AppleMIDI_Interface::sendSysExImpl(SysExMessage msg) {
session_.beginTransmission(MIDI_NAMESPACE::MidiType::SystemExclusive);
for (uint16_t i = 0; i < msg.length; i++)
session_.write(msg.data[i]);
}
void AppleMIDI_Interface::sendRealTimeImpl(RealTimeMessage msg) {
session_.beginTransmission(MIDI_NAMESPACE::MidiType::InvalidType);
session_.write(msg.message);
}
END_CS_NAMESPACE

View File

@ -0,0 +1,63 @@
#pragma once
#include "MIDI_Interface.hpp"
#include <MIDI_Parsers/SerialMIDI_Parser.hpp>
#include "AppleMIDI/LwIPUDP.hpp"
#include "AppleMIDI/vendor/AppleMIDI.h"
BEGIN_CS_NAMESPACE
using AppleMIDISession_t = APPLEMIDI_NAMESPACE::AppleMIDISession<LwIPUDP>;
struct AppleMIDIBytePuller {
AppleMIDISession_t &session;
bool pull(uint8_t &byte) {
if (session.available() == 0)
return false;
byte = session.read();
return true;
}
};
class AppleMIDI_Interface : public MIDI_Interface {
public:
/// @param name Session name visible in macOS Audio MIDI Setup
/// @param port AppleMIDI control port (data port = port + 1)
AppleMIDI_Interface(const char *name = "PicoW", uint16_t port = 5004);
void begin() override;
void update() override;
MIDIReadEvent read();
ChannelMessage getChannelMessage() const;
SysCommonMessage getSysCommonMessage() const;
RealTimeMessage getRealTimeMessage() const;
SysExMessage getSysExMessage() const;
/// Must be called after WiFi is connected but before begin()
void setName(const char *name) { sessionName_ = name; }
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 "applemidi"; }
#endif
#endif
void initMDNS();
AppleMIDISession_t session_;
SerialMIDI_Parser parser_;
const char *sessionName_;
uint16_t port_;
};
END_CS_NAMESPACE

View File

@ -8,16 +8,31 @@ namespace cs::midi_ble_btstack {
namespace { namespace {
#ifdef CS_MIDI_HID_MOUSE
uint8_t adv_data[] {
// Flags general discoverable
0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06,
// HID Service UUID (16-bit)
0x03, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
0x12, 0x18,
// Appearance: Mouse (0x03C2)
0x03, BLUETOOTH_DATA_TYPE_APPEARANCE, 0xC2, 0x03,
// MIDI Service UUID (128-bit)
0x11, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed,
0x5a, 0x0e, 0xb8, 0x03};
#else
uint8_t adv_data[] { uint8_t adv_data[] {
// Flags general discoverable // Flags general discoverable
0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, 0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06,
// Connection interval range // Connection interval range
0x05, BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE, 0x0c, 0x00, 0x0c, 0x05, BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE, 0x0c, 0x00, 0x0c,
0x00, 0x00,
// Service UUID // MIDI Service UUID (128-bit)
0x11, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, 0x11, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed,
0x5a, 0x0e, 0xb8, 0x03}; 0x5a, 0x0e, 0xb8, 0x03};
#endif
static_assert(sizeof(adv_data) <= LE_ADVERTISING_DATA_SIZE); static_assert(sizeof(adv_data) <= LE_ADVERTISING_DATA_SIZE);
uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] { uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] {
// Name header // Name header
@ -27,6 +42,9 @@ uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] {
' ', 'M', 'I', 'D', 'I'}; ' ', 'M', 'I', 'D', 'I'};
uint8_t adv_rsp_data_len() { return adv_rsp_data[0] + 1; } uint8_t adv_rsp_data_len() { return adv_rsp_data[0] + 1; }
#ifdef CS_MIDI_HID_MOUSE
void set_adv_connection_interval(uint16_t, uint16_t) {}
#else
void set_adv_connection_interval(uint16_t min_itvl, uint16_t max_itvl) { void set_adv_connection_interval(uint16_t min_itvl, uint16_t max_itvl) {
uint8_t *slave_itvl_range = adv_data + 5; uint8_t *slave_itvl_range = adv_data + 5;
slave_itvl_range[0] = (min_itvl >> 0) & 0xFF; slave_itvl_range[0] = (min_itvl >> 0) & 0xFF;
@ -34,6 +52,7 @@ void set_adv_connection_interval(uint16_t min_itvl, uint16_t max_itvl) {
slave_itvl_range[2] = (max_itvl >> 0) & 0xFF; slave_itvl_range[2] = (max_itvl >> 0) & 0xFF;
slave_itvl_range[3] = (max_itvl >> 8) & 0xFF; slave_itvl_range[3] = (max_itvl >> 8) & 0xFF;
} }
#endif
void set_adv_name(const char *name) { void set_adv_name(const char *name) {
auto len = std::min(std::strlen(name), sizeof(adv_rsp_data) - 2); auto len = std::min(std::strlen(name), sizeof(adv_rsp_data) - 2);

View File

@ -11,7 +11,12 @@
#include "../BLEAPI.hpp" #include "../BLEAPI.hpp"
#include "advertising.hpp" #include "advertising.hpp"
#ifdef CS_MIDI_HID_MOUSE
#include "gatt_midi_hog.h"
#include <ble/gatt-service/hids_device.h>
#else
#include "gatt_midi.h" #include "gatt_midi.h"
#endif
#include "hci_event_names.hpp" #include "hci_event_names.hpp"
#include <platform/pico_shim.h> #include <platform/pico_shim.h>
@ -31,6 +36,47 @@ BLESettings settings;
btstack_packet_callback_registration_t hci_event_callback_registration; btstack_packet_callback_registration_t hci_event_callback_registration;
btstack_packet_callback_registration_t sm_event_callback_registration; btstack_packet_callback_registration_t sm_event_callback_registration;
#ifdef CS_MIDI_HID_MOUSE
const uint8_t hid_descriptor_mouse[] = {
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xa1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID 1
0x09, 0x01, // Usage (Pointer)
0xa1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x03, // Usage Maximum (3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data,Var,Abs)
0x95, 0x01, // Report Count (1)
0x75, 0x05, // Report Size (5)
0x81, 0x03, // Input (Const,Var,Abs) — padding
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7f, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data,Var,Rel)
0xc0, // End Collection
0xc0 // End Collection
};
constexpr uint16_t gap_device_name_handle =
ATT_CHARACTERISTIC_GAP_DEVICE_NAME_01_VALUE_HANDLE;
void hid_packet_handler(uint8_t packet_type, uint16_t, uint8_t *packet, uint16_t) {
if (packet_type != HCI_EVENT_PACKET) return;
if (hci_event_packet_get_type(packet) != HCI_EVENT_HIDS_META) return;
// Accept HID events but never send mouse reports
}
#endif
// callback/event functions // callback/event functions
// HCI_SUBEVENT_LE_CONNECTION_COMPLETE // HCI_SUBEVENT_LE_CONNECTION_COMPLETE
@ -179,8 +225,20 @@ uint16_t att_read_callback([[maybe_unused]] hci_con_handle_t connection_handle,
[[maybe_unused]] uint16_t offset, [[maybe_unused]] uint16_t offset,
[[maybe_unused]] uint8_t *buffer, [[maybe_unused]] uint8_t *buffer,
[[maybe_unused]] uint16_t buffer_size) { [[maybe_unused]] uint16_t buffer_size) {
#ifdef CS_MIDI_HID_MOUSE
if (att_handle == gap_device_name_handle) {
const char *name = settings.device_name;
auto len = static_cast<uint16_t>(std::strlen(name));
if (buffer) {
auto copy = static_cast<uint16_t>(std::min<uint16_t>(len - offset, buffer_size));
std::memcpy(buffer, name + offset, copy);
return copy;
}
return len;
}
#endif
if (att_handle == midi_char_value_handle) if (att_handle == midi_char_value_handle)
return 0; // MIDI always responds with no data return 0;
return 0; return 0;
} }
@ -236,6 +294,10 @@ void le_midi_setup(const BLESettings &ble_settings) {
SM_AUTHREQ_BONDING); SM_AUTHREQ_BONDING);
// setup ATT server // setup ATT server
att_server_init(profile_data, att_read_callback, att_write_callback); att_server_init(profile_data, att_read_callback, att_write_callback);
#ifdef CS_MIDI_HID_MOUSE
hids_device_init(0, hid_descriptor_mouse, sizeof(hid_descriptor_mouse));
hids_device_register_packet_handler(hid_packet_handler);
#endif
// setup advertisements // setup advertisements
le_midi_setup_adv(ble_settings); le_midi_setup_adv(ble_settings);
// register for HCI events // register for HCI events

View File

@ -0,0 +1,14 @@
PRIMARY_SERVICE, GATT_SERVICE
CHARACTERISTIC, GATT_DATABASE_HASH, READ,
PRIMARY_SERVICE, GAP_SERVICE
CHARACTERISTIC, GAP_DEVICE_NAME, READ | DYNAMIC,
// 0x03C2 = HID Mouse
CHARACTERISTIC, GAP_APPEARANCE, READ, C2 03
// MIDI Service
PRIMARY_SERVICE, 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
CHARACTERISTIC, 7772E5DB-3868-4112-A1A9-F2669D106BF3, READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC,
// HID Service
#import <hids.gatt>

View File

@ -0,0 +1,147 @@
// Generated from gatt_midi_hog.gatt — MIDI + HoG Mouse combined GATT database
// Regenerate: compile_gatt.py gatt_midi_hog.gatt gatt_midi_hog.h
// att db format version 1
// binary attribute representation:
// - size in bytes (16), flags(16), handle (16), uuid (16/128), value(...)
#include <stdint.h>
#if __cplusplus >= 200704L
constexpr
#endif
const uint8_t profile_data[] =
{
// ATT DB Version
1,
// 0x0001 PRIMARY_SERVICE-GATT_SERVICE
0x0a, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x28, 0x01, 0x18,
// 0x0002 CHARACTERISTIC-GATT_DATABASE_HASH - READ
0x0d, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x28, 0x02, 0x03, 0x00, 0x2a, 0x2b,
// 0x0003 VALUE CHARACTERISTIC-GATT_DATABASE_HASH - READ
0x18, 0x00, 0x02, 0x00, 0x03, 0x00, 0x2a, 0x2b, 0xce, 0x0f, 0xc6, 0xd0, 0xa1, 0x9c, 0xd7, 0xa2, 0x98, 0xad, 0x7d, 0x4c, 0x9c, 0x85, 0xed, 0x81,
// 0x0004 PRIMARY_SERVICE-GAP_SERVICE
0x0a, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x28, 0x00, 0x18,
// 0x0005 CHARACTERISTIC-GAP_DEVICE_NAME - READ | DYNAMIC
0x0d, 0x00, 0x02, 0x00, 0x05, 0x00, 0x03, 0x28, 0x02, 0x06, 0x00, 0x00, 0x2a,
// 0x0006 VALUE CHARACTERISTIC-GAP_DEVICE_NAME - READ | DYNAMIC
0x08, 0x00, 0x02, 0x01, 0x06, 0x00, 0x00, 0x2a,
// 0x0007 CHARACTERISTIC-GAP_APPEARANCE - READ - Mouse (0x03C2)
0x0d, 0x00, 0x02, 0x00, 0x07, 0x00, 0x03, 0x28, 0x02, 0x08, 0x00, 0x01, 0x2a,
// 0x0008 VALUE CHARACTERISTIC-GAP_APPEARANCE - READ
0x0a, 0x00, 0x02, 0x00, 0x08, 0x00, 0x01, 0x2a, 0xC2, 0x03,
// MIDI Service
// 0x0009 PRIMARY_SERVICE-03B80E5A-EDE8-4B33-A751-6CE34EC4C700
0x18, 0x00, 0x02, 0x00, 0x09, 0x00, 0x00, 0x28, 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, 0x5a, 0x0e, 0xb8, 0x03,
// 0x000a CHARACTERISTIC-7772E5DB-3868-4112-A1A9-F2669D106BF3 - READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC
0x1b, 0x00, 0x02, 0x00, 0x0a, 0x00, 0x03, 0x28, 0x16, 0x0b, 0x00, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1, 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77,
// 0x000b VALUE CHARACTERISTIC-7772E5DB-3868-4112-A1A9-F2669D106BF3 - READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC
0x16, 0x00, 0x06, 0x03, 0x0b, 0x00, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1, 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77,
// 0x000c CLIENT_CHARACTERISTIC_CONFIGURATION
0x0a, 0x00, 0x0e, 0x01, 0x0c, 0x00, 0x02, 0x29, 0x00, 0x00,
// HID Service (hids.gatt)
// 0x000d PRIMARY_SERVICE-ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE
0x0a, 0x00, 0x02, 0x00, 0x0d, 0x00, 0x00, 0x28, 0x12, 0x18,
// 0x000e CHARACTERISTIC-PROTOCOL_MODE - DYNAMIC | READ | WRITE_WITHOUT_RESPONSE
0x0d, 0x00, 0x02, 0x00, 0x0e, 0x00, 0x03, 0x28, 0x06, 0x0f, 0x00, 0x4e, 0x2a,
// 0x000f VALUE CHARACTERISTIC-PROTOCOL_MODE
0x08, 0x00, 0x06, 0x01, 0x0f, 0x00, 0x4e, 0x2a,
// 0x0010 CHARACTERISTIC-REPORT (Input, ID=1) - DYNAMIC | READ | WRITE | NOTIFY | ENCRYPTION_KEY_SIZE_16
0x0d, 0x00, 0x02, 0x00, 0x10, 0x00, 0x03, 0x28, 0x1a, 0x11, 0x00, 0x4d, 0x2a,
// 0x0011 VALUE CHARACTERISTIC-REPORT
0x08, 0x00, 0x0b, 0xf5, 0x11, 0x00, 0x4d, 0x2a,
// 0x0012 CLIENT_CHARACTERISTIC_CONFIGURATION
0x0a, 0x00, 0x0f, 0xf1, 0x12, 0x00, 0x02, 0x29, 0x00, 0x00,
// 0x0013 REPORT_REFERENCE - Report ID=1, Type=Input(1)
0x0a, 0x00, 0x02, 0x00, 0x13, 0x00, 0x08, 0x29, 0x1, 0x1,
// 0x0014 CHARACTERISTIC-REPORT (Output, ID=2) - DYNAMIC | READ | WRITE | WRITE_WITHOUT_RESPONSE | ENCRYPTION_KEY_SIZE_16
0x0d, 0x00, 0x02, 0x00, 0x14, 0x00, 0x03, 0x28, 0x0e, 0x15, 0x00, 0x4d, 0x2a,
// 0x0015 VALUE CHARACTERISTIC-REPORT
0x08, 0x00, 0x0f, 0xf5, 0x15, 0x00, 0x4d, 0x2a,
// 0x0016 REPORT_REFERENCE - Report ID=2, Type=Output(2)
0x0a, 0x00, 0x02, 0x00, 0x16, 0x00, 0x08, 0x29, 0x2, 0x2,
// 0x0017 CHARACTERISTIC-REPORT (Feature, ID=3) - DYNAMIC | READ | WRITE | ENCRYPTION_KEY_SIZE_16
0x0d, 0x00, 0x02, 0x00, 0x17, 0x00, 0x03, 0x28, 0x0a, 0x18, 0x00, 0x4d, 0x2a,
// 0x0018 VALUE CHARACTERISTIC-REPORT
0x08, 0x00, 0x0b, 0xf5, 0x18, 0x00, 0x4d, 0x2a,
// 0x0019 REPORT_REFERENCE - Report ID=3, Type=Feature(3)
0x0a, 0x00, 0x02, 0x00, 0x19, 0x00, 0x08, 0x29, 0x3, 0x3,
// 0x001a CHARACTERISTIC-REPORT_MAP - DYNAMIC | READ
0x0d, 0x00, 0x02, 0x00, 0x1a, 0x00, 0x03, 0x28, 0x02, 0x1b, 0x00, 0x4b, 0x2a,
// 0x001b VALUE CHARACTERISTIC-REPORT_MAP
0x08, 0x00, 0x02, 0x01, 0x1b, 0x00, 0x4b, 0x2a,
// 0x001c CHARACTERISTIC-BOOT_KEYBOARD_INPUT_REPORT - DYNAMIC | READ | WRITE | NOTIFY
0x0d, 0x00, 0x02, 0x00, 0x1c, 0x00, 0x03, 0x28, 0x1a, 0x1d, 0x00, 0x22, 0x2a,
// 0x001d VALUE CHARACTERISTIC-BOOT_KEYBOARD_INPUT_REPORT
0x08, 0x00, 0x0a, 0x01, 0x1d, 0x00, 0x22, 0x2a,
// 0x001e CLIENT_CHARACTERISTIC_CONFIGURATION
0x0a, 0x00, 0x0e, 0x01, 0x1e, 0x00, 0x02, 0x29, 0x00, 0x00,
// 0x001f CHARACTERISTIC-BOOT_KEYBOARD_OUTPUT_REPORT - DYNAMIC | READ | WRITE | WRITE_WITHOUT_RESPONSE
0x0d, 0x00, 0x02, 0x00, 0x1f, 0x00, 0x03, 0x28, 0x0e, 0x20, 0x00, 0x32, 0x2a,
// 0x0020 VALUE CHARACTERISTIC-BOOT_KEYBOARD_OUTPUT_REPORT
0x08, 0x00, 0x0e, 0x01, 0x20, 0x00, 0x32, 0x2a,
// 0x0021 CHARACTERISTIC-BOOT_MOUSE_INPUT_REPORT - DYNAMIC | READ | WRITE | NOTIFY
0x0d, 0x00, 0x02, 0x00, 0x21, 0x00, 0x03, 0x28, 0x1a, 0x22, 0x00, 0x33, 0x2a,
// 0x0022 VALUE CHARACTERISTIC-BOOT_MOUSE_INPUT_REPORT
0x08, 0x00, 0x0a, 0x01, 0x22, 0x00, 0x33, 0x2a,
// 0x0023 CLIENT_CHARACTERISTIC_CONFIGURATION
0x0a, 0x00, 0x0e, 0x01, 0x23, 0x00, 0x02, 0x29, 0x00, 0x00,
// 0x0024 CHARACTERISTIC-HID_INFORMATION - READ
0x0d, 0x00, 0x02, 0x00, 0x24, 0x00, 0x03, 0x28, 0x02, 0x25, 0x00, 0x4a, 0x2a,
// 0x0025 VALUE CHARACTERISTIC-HID_INFORMATION - bcdHID=0x0101, country=0, flags=0x02
0x0c, 0x00, 0x02, 0x00, 0x25, 0x00, 0x4a, 0x2a, 0x01, 0x01, 0x00, 0x02,
// 0x0026 CHARACTERISTIC-HID_CONTROL_POINT - DYNAMIC | WRITE_WITHOUT_RESPONSE
0x0d, 0x00, 0x02, 0x00, 0x26, 0x00, 0x03, 0x28, 0x04, 0x27, 0x00, 0x4c, 0x2a,
// 0x0027 VALUE CHARACTERISTIC-HID_CONTROL_POINT
0x08, 0x00, 0x04, 0x01, 0x27, 0x00, 0x4c, 0x2a,
// END
0x00, 0x00,
}; // total size 252 bytes
//
// list service handle ranges
//
#define ATT_SERVICE_GATT_SERVICE_START_HANDLE 0x0001
#define ATT_SERVICE_GATT_SERVICE_END_HANDLE 0x0003
#define ATT_SERVICE_GATT_SERVICE_01_START_HANDLE 0x0001
#define ATT_SERVICE_GATT_SERVICE_01_END_HANDLE 0x0003
#define ATT_SERVICE_GAP_SERVICE_START_HANDLE 0x0004
#define ATT_SERVICE_GAP_SERVICE_END_HANDLE 0x0008
#define ATT_SERVICE_GAP_SERVICE_01_START_HANDLE 0x0004
#define ATT_SERVICE_GAP_SERVICE_01_END_HANDLE 0x0008
#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_START_HANDLE 0x0009
#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_END_HANDLE 0x000c
#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_01_START_HANDLE 0x0009
#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_01_END_HANDLE 0x000c
#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_START_HANDLE 0x000d
#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_END_HANDLE 0x0027
#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_01_START_HANDLE 0x000d
#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_01_END_HANDLE 0x0027
//
// list mapping between characteristics and handles
//
#define ATT_CHARACTERISTIC_GATT_DATABASE_HASH_01_VALUE_HANDLE 0x0003
#define ATT_CHARACTERISTIC_GAP_DEVICE_NAME_01_VALUE_HANDLE 0x0006
#define ATT_CHARACTERISTIC_GAP_APPEARANCE_01_VALUE_HANDLE 0x0008
#define ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_VALUE_HANDLE 0x000b
#define ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_CLIENT_CONFIGURATION_HANDLE 0x000c
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_PROTOCOL_MODE_01_VALUE_HANDLE 0x000f
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_01_VALUE_HANDLE 0x0011
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_01_CLIENT_CONFIGURATION_HANDLE 0x0012
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_02_VALUE_HANDLE 0x0015
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_03_VALUE_HANDLE 0x0018
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_MAP_01_VALUE_HANDLE 0x001b
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_INPUT_REPORT_01_VALUE_HANDLE 0x001d
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_INPUT_REPORT_01_CLIENT_CONFIGURATION_HANDLE 0x001e
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_OUTPUT_REPORT_01_VALUE_HANDLE 0x0020
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_MOUSE_INPUT_REPORT_01_VALUE_HANDLE 0x0022
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_MOUSE_INPUT_REPORT_01_CLIENT_CONFIGURATION_HANDLE 0x0023
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HID_INFORMATION_01_VALUE_HANDLE 0x0025
#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HID_CONTROL_POINT_01_VALUE_HANDLE 0x0027

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 all: $(BUILD_DIR)/Makefile
@$(MAKE) -C $(BUILD_DIR) examples @$(MAKE) -C $(BUILD_DIR)
@echo "All examples compiled successfully."
$(BUILD_DIR)/Makefile: tests/CMakeLists.txt tests/examples/CMakeLists.txt $(BUILD_DIR)/Makefile: CMakeLists.txt
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake .. @cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake ..
clean: tests: $(TEST_BUILD_DIR)/Makefile
@rm -rf $(BUILD_DIR) @$(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,26 @@
#include <MIDI_Constants/Notes.hpp> #include <MIDI_Constants/Notes.hpp>
#include <MIDI_Constants/Program_Change.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/GenericBLEMIDI_Interface.hpp>
#include <MIDI_Interfaces/BLEMIDI/BTstackBackgroundBackend.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
#ifdef CS_MIDI_APPLEMIDI
#include <MIDI_Interfaces/AppleMIDI_Interface.hpp>
#endif
// Control Surface singleton // Control Surface singleton
#include <Control_Surface/Control_Surface_Class.hpp> #include <Control_Surface/Control_Surface_Class.hpp>
@ -72,7 +88,9 @@
BEGIN_CS_NAMESPACE BEGIN_CS_NAMESPACE
#ifdef CS_MIDI_BLE
using BluetoothMIDI_Interface = using BluetoothMIDI_Interface =
GenericBLEMIDI_Interface<BTstackBackgroundBackend>; GenericBLEMIDI_Interface<BTstackBackgroundBackend>;
#endif
END_CS_NAMESPACE END_CS_NAMESPACE

2
docs

@ -1 +1 @@
Subproject commit a8d3b6177fd623f1af91947b22c9cb28188f0419 Subproject commit 3927d073e938973be113ee08db4e596c7d853ad3

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. # Standalone build for cs-midi examples compile-only verification.
# Invoked via: make dist-tests (from lib/cs-midi/)
set(PICO_BOARD pico2_w) set(PICO_BOARD pico2_w)
@ -13,9 +12,13 @@ set(CMAKE_CXX_STANDARD 17)
pico_sdk_init() 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)
set(CS_MIDI_APPLEMIDI ON CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/cs_midi) 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}) target_include_directories(cs_midi PRIVATE ${CMAKE_CURRENT_LIST_DIR})
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/examples ${CMAKE_CURRENT_BINARY_DIR}/examples) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/examples ${CMAKE_CURRENT_BINARY_DIR}/examples)

View File

@ -1,5 +1,50 @@
# Build each example as a standalone executable to verify compilation. # Compile-verify all examples as a single OBJECT library.
# Invoked via: make dist-tests (from lib/cs-midi/) # 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()
if(CS_MIDI_APPLEMIDI)
target_link_libraries(pico_deps PRIVATE pico_lwip_mdns)
if(NOT CS_MIDI_BLE)
target_link_libraries(pico_deps PRIVATE
pico_cyw43_arch_lwip_threadsafe_background)
endif()
endif()
# --------------------------------------------------------------------------- #
# Example sources
# --------------------------------------------------------------------------- #
set(EXAMPLE_SOURCES set(EXAMPLE_SOURCES
# Output elements # Output elements
@ -50,21 +95,39 @@ set(EXAMPLE_SOURCES
banks/bankable_note_led.cpp banks/bankable_note_led.cpp
) )
foreach(src ${EXAMPLE_SOURCES}) if(CS_MIDI_USB)
get_filename_component(name ${src} NAME_WE) list(APPEND EXAMPLE_SOURCES
set(target "example_${name}") interfaces/usb_midi.cpp
add_executable(${target} ${src}) usb_descriptors.c
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
) )
set_target_properties(${target} PROPERTIES EXCLUDE_FROM_ALL TRUE) endif()
list(APPEND EXAMPLE_TARGETS ${target})
endforeach()
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()
if(CS_MIDI_APPLEMIDI)
list(APPEND EXAMPLE_SOURCES interfaces/applemidi.cpp)
endif()
if(CS_MIDI_APPLEMIDI AND CS_MIDI_BLE)
list(APPEND EXAMPLE_SOURCES interfaces/applemidi_ble.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,29 @@
// AppleMIDI — RTP-MIDI over WiFi.
// Appears as a Bonjour MIDI device in macOS Audio MIDI Setup,
// Windows rtpMIDI, and iOS.
// Requires WiFi connection before Control_Surface.begin().
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
AppleMIDI_Interface midi {"PicoW-MIDI", 5004};
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;
cyw43_arch_enable_sta_mode();
cyw43_arch_wifi_connect_blocking("SSID", "PASS", CYW43_AUTH_WPA2_AES_PSK);
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}

View File

@ -0,0 +1,34 @@
// Dual transport — BLE + AppleMIDI with pipe routing.
// Elements send to both interfaces simultaneously.
// BLE for direct iOS/macOS connection, AppleMIDI for DAW integration.
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
using namespace cs;
BluetoothMIDI_Interface ble;
AppleMIDI_Interface applemidi {"PicoW-MIDI", 5004};
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;
cyw43_arch_enable_sta_mode();
cyw43_arch_wifi_connect_blocking("SSID", "PASS", CYW43_AUTH_WPA2_AES_PSK);
Control_Surface >> pipes >> ble;
Control_Surface >> pipes >> applemidi;
Control_Surface.begin();
while (true) {
Control_Surface.loop();
sleep_ms(1);
}
}

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;
}

View File

@ -13,7 +13,11 @@
#define LWIP_UDP 1 #define LWIP_UDP 1
#define LWIP_TCP 1 #define LWIP_TCP 1
#define MEM_SIZE 4096 #define MEM_SIZE 4096
#define MEMP_NUM_UDP_PCB 6 #define MEMP_NUM_UDP_PCB 8
#define MEMP_NUM_TCP_PCB 4 #define MEMP_NUM_TCP_PCB 4
#define LWIP_MDNS_RESPONDER 1
#define MDNS_MAX_SERVICES 2
#define LWIP_NUM_NETIF_CLIENT_DATA 1
#endif #endif

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