doozy
This commit is contained in:
parent
517cbbb318
commit
2187943124
|
|
@ -4,6 +4,8 @@ cmake_minimum_required(VERSION 3.13)
|
|||
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
|
||||
|
|
@ -52,17 +54,33 @@ if(CS_MIDI_SERIAL)
|
|||
)
|
||||
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
|
||||
${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
|
||||
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>
|
||||
)
|
||||
|
||||
target_link_libraries(cs_midi
|
||||
|
|
@ -88,4 +106,11 @@ if(CS_MIDI_USB)
|
|||
)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
#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[] {
|
||||
// Flags general discoverable
|
||||
0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06,
|
||||
// Connection interval range
|
||||
0x05, BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE, 0x0c, 0x00, 0x0c,
|
||||
0x00,
|
||||
// Service UUID
|
||||
// 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};
|
||||
#endif
|
||||
static_assert(sizeof(adv_data) <= LE_ADVERTISING_DATA_SIZE);
|
||||
uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] {
|
||||
// Name header
|
||||
|
|
@ -27,6 +42,9 @@ uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] {
|
|||
' ', 'M', 'I', 'D', 'I'};
|
||||
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) {
|
||||
uint8_t *slave_itvl_range = adv_data + 5;
|
||||
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[3] = (max_itvl >> 8) & 0xFF;
|
||||
}
|
||||
#endif
|
||||
|
||||
void set_adv_name(const char *name) {
|
||||
auto len = std::min(std::strlen(name), sizeof(adv_rsp_data) - 2);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@
|
|||
|
||||
#include "../BLEAPI.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"
|
||||
#endif
|
||||
#include "hci_event_names.hpp"
|
||||
|
||||
#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 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
|
||||
|
||||
// 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]] uint8_t *buffer,
|
||||
[[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)
|
||||
return 0; // MIDI always responds with no data
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -236,6 +294,10 @@ void le_midi_setup(const BLESettings &ble_settings) {
|
|||
SM_AUTHREQ_BONDING);
|
||||
// setup ATT server
|
||||
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
|
||||
le_midi_setup_adv(ble_settings);
|
||||
// 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
|
||||
|
|
@ -38,6 +38,10 @@
|
|||
#include <MIDI_Interfaces/SerialMIDI_Interface.hpp>
|
||||
#endif
|
||||
|
||||
#ifdef CS_MIDI_APPLEMIDI
|
||||
#include <MIDI_Interfaces/AppleMIDI_Interface.hpp>
|
||||
#endif
|
||||
|
||||
// Control Surface singleton
|
||||
#include <Control_Surface/Control_Surface_Class.hpp>
|
||||
|
||||
|
|
|
|||
2
docs
2
docs
|
|
@ -1 +1 @@
|
|||
Subproject commit a8d3b6177fd623f1af91947b22c9cb28188f0419
|
||||
Subproject commit 3927d073e938973be113ee08db4e596c7d853ad3
|
||||
|
|
@ -15,6 +15,7 @@ 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ 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
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
@ -102,6 +110,14 @@ 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,11 @@
|
|||
#define LWIP_UDP 1
|
||||
#define LWIP_TCP 1
|
||||
#define MEM_SIZE 4096
|
||||
#define MEMP_NUM_UDP_PCB 6
|
||||
#define MEMP_NUM_UDP_PCB 8
|
||||
#define MEMP_NUM_TCP_PCB 4
|
||||
|
||||
#define LWIP_MDNS_RESPONDER 1
|
||||
#define MDNS_MAX_SERVICES 2
|
||||
#define LWIP_NUM_NETIF_CLIENT_DATA 1
|
||||
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in New Issue