Compare commits
2 Commits
782ff440f0
...
2187943124
| Author | SHA1 | Date |
|---|---|---|
|
|
2187943124 | |
|
|
517cbbb318 |
|
|
@ -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
|
||||||
MIDI_Interfaces/BLEMIDI/BLEMIDIPacketBuilder.cpp
|
|
||||||
MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp
|
|
||||||
MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(CS_MIDI_SOURCES ${CS_MIDI_CORE_SOURCES})
|
||||||
|
|
||||||
|
if(CS_MIDI_BLE)
|
||||||
|
list(APPEND CS_MIDI_SOURCES
|
||||||
|
MIDI_Interfaces/BLEMIDI/BLEMIDIPacketBuilder.cpp
|
||||||
|
MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp
|
||||||
|
MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CS_MIDI_USB)
|
||||||
|
list(APPEND CS_MIDI_SOURCES
|
||||||
|
MIDI_Parsers/USBMIDI_Parser.cpp
|
||||||
|
MIDI_Interfaces/USBMIDI_Interface.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CS_MIDI_SERIAL)
|
||||||
|
list(APPEND CS_MIDI_SOURCES
|
||||||
|
MIDI_Interfaces/SerialMIDI_Interface.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
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
|
||||||
PRIVATE pico_btstack_ble pico_btstack_cyw43
|
|
||||||
pico_cyw43_arch_lwip_threadsafe_background
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(CS_MIDI_BLE)
|
||||||
|
target_link_libraries(cs_midi
|
||||||
|
PRIVATE pico_btstack_ble pico_btstack_cyw43
|
||||||
|
pico_cyw43_arch_lwip_threadsafe_background
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CS_MIDI_SERIAL)
|
||||||
|
target_link_libraries(cs_midi
|
||||||
|
PRIVATE hardware_uart
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CS_MIDI_USB)
|
||||||
|
target_link_libraries(cs_midi
|
||||||
|
PRIVATE tinyusb_device tinyusb_board
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
#include "AppleMIDI.h"
|
||||||
|
|
||||||
|
BEGIN_APPLEMIDI_NAMESPACE
|
||||||
|
|
||||||
|
unsigned long now = 0;
|
||||||
|
|
||||||
|
END_APPLEMIDI_NAMESPACE
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AppleMIDI_Namespace.h"
|
||||||
|
|
||||||
|
BEGIN_APPLEMIDI_NAMESPACE
|
||||||
|
|
||||||
|
#ifdef WIN32
|
||||||
|
#pragma pack(pop)
|
||||||
|
#endif
|
||||||
|
#undef PACKED
|
||||||
|
|
||||||
|
END_APPLEMIDI_NAMESPACE
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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_;
|
||||||
|
};
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 */
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
24
Makefile
24
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
22
cs_midi.h
22
cs_midi.h
|
|
@ -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
2
docs
|
|
@ -1 +1 @@
|
||||||
Subproject commit a8d3b6177fd623f1af91947b22c9cb28188f0419
|
Subproject commit 3927d073e938973be113ee08db4e596c7d853ad3
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// Stub — pico_deps collects pico-sdk INTERFACE sources into one static lib.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue