diff --git a/src/Makefile b/src/Makefile index 540ae68..f078fad 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,11 +5,13 @@ CIRCLE_STDLIB_DIR = ../circle-stdlib SYNTH_DEXED_DIR = ../Synth_Dexed/src CMSIS_DIR = ../CMSIS_5/CMSIS +NET_DIR = ./net OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \ - mididevice.o midikeyboard.o serialmididevice.o pckeyboard.o \ + mididevice.o rtpmididevice.o midikeyboard.o serialmididevice.o pckeyboard.o \ sysexfileloader.o performanceconfig.o perftimer.o \ - effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o + effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \ + net/applemidi.o net/udpmidi.o OPTIMIZE = -O3 diff --git a/src/Rules.mk b/src/Rules.mk index 2ebc132..6461833 100644 --- a/src/Rules.mk +++ b/src/Rules.mk @@ -11,7 +11,8 @@ include $(CIRCLEHOME)/Rules.mk INCLUDE += \ -I $(CIRCLE_STDLIB_DIR)/include \ - -I $(NEWLIBDIR)/include + -I $(NEWLIBDIR)/include \ + -I $(NET_DIR) LIBS += \ $(NEWLIBDIR)/lib/libm.a \ @@ -28,6 +29,9 @@ LIBS += \ $(CIRCLEHOME)/addon/fatfs/libfatfs.a \ $(CIRCLEHOME)/lib/fs/libfs.a \ $(CIRCLEHOME)/lib/sched/libsched.a \ - $(CIRCLEHOME)/lib/libcircle.a + $(CIRCLEHOME)/lib/libcircle.a \ + $(CIRCLEHOME)/addon/wlan/hostap/wpa_supplicant/libwpa_supplicant.a \ + $(CIRCLEHOME)/addon/wlan/libwlan.a \ + $(CIRCLEHOME)/lib/net/libnet.a -include $(DEPS) diff --git a/src/config.cpp b/src/config.cpp index 526d8ad..ac6a2e4 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -152,6 +152,10 @@ void CConfig::Load (void) m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0; m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 1) != 0; m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); + + // Network + m_bNetworkEnabled = m_Properties.GetNumber ("NetworkEnabled", 0) != 0; + m_NetworkType = m_Properties.GetString ("NetworkType", ""); } bool CConfig::GetUSBGadgetMode (void) const @@ -503,3 +507,14 @@ unsigned CConfig::GetPerformanceSelectChannel (void) const { return m_bPerformanceSelectChannel; } + +// Network +bool CConfig::GetNetworkEnabled (void) const +{ + return m_bNetworkEnabled; +} + +const char *CConfig::GetNetworkType (void) const +{ + return m_NetworkType.c_str(); +} \ No newline at end of file diff --git a/src/config.h b/src/config.h index f83c177..30ee5ed 100644 --- a/src/config.h +++ b/src/config.h @@ -168,6 +168,10 @@ public: bool GetPerformanceSelectToLoad (void) const; unsigned GetPerformanceSelectChannel (void) const; + // Network + bool GetNetworkEnabled (void) const; + const char *GetNetworkType (void) const; + private: CPropertiesFatFsFile m_Properties; @@ -252,6 +256,10 @@ private: bool m_bProfileEnabled; bool m_bPerformanceSelectToLoad; unsigned m_bPerformanceSelectChannel; + + // Network + bool m_bNetworkEnabled; + std::string m_NetworkType; }; #endif diff --git a/src/kernel.cpp b/src/kernel.cpp index c06c386..5c7a7a9 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -24,12 +24,17 @@ #include #include "usbminidexedmidigadget.h" +#define NET_DEVICE_TYPE NetDeviceTypeWLAN // or: NetDeviceTypeWLAN + LOGMODULE ("kernel"); CKernel *CKernel::s_pThis = 0; CKernel::CKernel (void) -: CStdlibAppStdio ("minidexed"), +: + //CStdlibAppStdio ("minidexed"), + CStdlibAppNetwork ("minidexed", CSTDLIBAPP_DEFAULT_PARTITION, + 0, 0, 0, 0, NET_DEVICE_TYPE), m_Config (&mFileSystem), m_GPIOManager (&mInterrupt), m_I2CMaster (CMachineInfo::Get ()->GetDevice (DeviceI2CMaster), TRUE), @@ -47,7 +52,7 @@ CKernel::~CKernel(void) bool CKernel::Initialize (void) { - if (!CStdlibAppStdio::Initialize ()) + if (!CStdlibAppNetwork::Initialize ()) { return FALSE; } @@ -109,7 +114,7 @@ CStdlibApp::TShutdownMode CKernel::Run (void) mScreen.Update (); } - m_CPUThrottle.Update (); + m_CPUThrottle.Update (); } return ShutdownHalt; diff --git a/src/kernel.h b/src/kernel.h index 7d2f346..31757cd 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -35,7 +35,7 @@ enum TShutdownMode ShutdownReboot }; -class CKernel : public CStdlibAppStdio +class CKernel : public CStdlibAppNetwork { public: CKernel (void); diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 71b7247..d736ed3 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -27,6 +27,19 @@ #include #include #include +#include +#include +#include "circle_stdlib_app.h" +//#include "mididevice.h" + +/* +#define DRIVE "SD:" +#define FIRMWARE_PATH DRIVE "/firmware/" // firmware files must be provided here +#define CONFIG_FILE DRIVE "/wpa_supplicant.conf" + +const char WLANFirmwarePath[] = "SD:firmware/"; +const char WLANConfigFile[] = "SD:wpa_supplicant.conf"; +*/ LOGMODULE ("minidexed"); @@ -54,7 +67,17 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_bSavePerformanceNewFile (false), m_bSetNewPerformance (false), m_bDeletePerformance (false), - m_bLoadPerformanceBusy(false) + m_bLoadPerformanceBusy(false), + /* + m_pNet(nullptr), + m_pNetDevice(nullptr), + m_WLAN(WLANFirmwarePath), + m_WPASupplicant(WLANConfigFile), + m_bNetworkReady(false), + */ + //CNetSubSystem* const pNet = CNetSubSystem::Get(); + m_bNetworkReady(false), + m_RTPMIDI (this, pConfig, &m_UI) { assert (m_pConfig); @@ -267,7 +290,14 @@ bool CMiniDexed::Initialize (void) return false; } #endif + //InitNetwork(); + UpdateNetwork(); + //CMIDIDevice->InitializeRTP(); + if (m_RTPMIDI.Initialize ()) + { + LOGNOTE ("RTP MIDI interface enabled"); + } return true; } @@ -325,6 +355,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) { m_GetChunkTimer.Dump (); } + } #ifdef ARM_ALLOW_MULTI_CORE @@ -1802,3 +1833,111 @@ unsigned CMiniDexed::getModController (unsigned controller, unsigned parameter, } } + +void CMiniDexed::UpdateNetwork() +/*{ + CNetSubSystem* const pNet = CNetSubSystem::Get(); + if (!m_bNetworkReady){ + + m_bNetworkReady = true; + CString IPString; + pNet->GetConfig()->GetIPAddress()->Format(&IPString); + LOGNOTE("Network up and running at: %s", static_cast(IPString)); + } + if (!pNet) + return; + +}*/ + +{ + CNetSubSystem* const pNet = CNetSubSystem::Get(); + if (!pNet) + return; + + bool bNetIsRunning = pNet->IsRunning(); + //bNetIsRunning &= m_WPASupplicant.IsConnected(); + + if (!m_bNetworkReady) + { + m_bNetworkReady = true; + + CString IPString; + pNet->GetConfig()->GetIPAddress()->Format(&IPString); + + LOGNOTE("Network up and running at: %s", static_cast(IPString)); + } + else if (m_bNetworkReady && !bNetIsRunning) + { + m_bNetworkReady = false; + LOGNOTE("Network disconnected."); + + } +} +/* +void CMiniDexed::UpdateNetwork() +{ + if (!m_pNet) + return; + + bool bNetIsRunning = m_pNet->IsRunning(); + if (strcmp(m_pConfig->GetNetworkType(), "ethernet") == 0) + bNetIsRunning &= m_pNetDevice->IsLinkUp(); + else if (strcmp(m_pConfig->GetNetworkType(), "wifi") == 0) + bNetIsRunning &= m_WPASupplicant.IsConnected(); + + if (!m_bNetworkReady && bNetIsRunning) + { + m_bNetworkReady = true; + + CString IPString; + m_pNet->GetConfig()->GetIPAddress()->Format(&IPString); + + LOGNOTE("Network up and running at: %s", static_cast(IPString)); + } + else if (m_bNetworkReady && !bNetIsRunning) + { + m_bNetworkReady = false; + LOGNOTE("Network disconnected."); + } +} + +bool CMiniDexed::InitNetwork() +{ + assert(m_pNet == nullptr); + + TNetDeviceType NetDeviceType = NetDeviceTypeWLAN; + + if (m_pConfig->GetNetworkEnabled () && (strcmp(m_pConfig->GetNetworkType(), "wifi") == 0)) + { + LOGNOTE("Initializing Wi-Fi"); + + if (m_WLAN.Initialize() && m_WPASupplicant.Initialize()) + { + LOGNOTE("wlan and wpasupplicant initialized"); + //NetDeviceType = NetDeviceTypeWLAN; + + } + else + LOGERR("Failed to initialize Wi-Fi"); + } + else if (m_pConfig->GetNetworkEnabled () && (strcmp(m_pConfig->GetNetworkType(), "ethernet") == 0)) + { + LOGNOTE("Initializing Ethernet"); + //NetDeviceType = NetDeviceTypeEthernet; + } + + + LOGNOTE("creating network with wifi and dhcp"); + m_pNet = new CNetSubSystem(0, 0, 0, 0, "minidexed", NetDeviceType); + if (!m_pNet->Initialize(true)) + { + LOGERR("Failed to initialize network subsystem"); + delete m_pNet; + m_pNet = nullptr; + } + m_pNetDevice = CNetDevice::GetNetDevice(NetDeviceType); + + + return m_pNet != nullptr; +} +*/ \ No newline at end of file diff --git a/src/minidexed.h b/src/minidexed.h index 1aa3096..65d4034 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -43,6 +43,10 @@ #include "effect_mixer.hpp" #include "effect_platervbstereo.h" #include "effect_compressor.h" +//#include +//#include +//#include +#include "rtpmididevice.h" class CMiniDexed #ifdef ARM_ALLOW_MULTI_CORE @@ -60,7 +64,6 @@ public: #ifdef ARM_ALLOW_MULTI_CORE void Run (unsigned nCore); #endif - CSysExFileLoader *GetSysExFileLoader (void); void BankSelect (unsigned nBank, unsigned nTG); @@ -211,12 +214,15 @@ public: bool DoSavePerformance (void); void setMasterVolume (float32_t vol); + //bool InitNetwork(); + void UpdateNetwork(); private: int16_t ApplyNoteLimits (int16_t pitch, unsigned nTG); // returns < 0 to ignore note uint8_t m_uchOPMask[CConfig::ToneGenerators]; void LoadPerformanceParameters(void); void ProcessSound (void); + const char* GetNetworkDeviceShortName() const; #ifdef ARM_ALLOW_MULTI_CORE enum TCoreStatus @@ -309,6 +315,19 @@ private: unsigned m_nDeletePerformanceID; bool m_bLoadPerformanceBusy; bool m_bSaveAsDeault; + bool m_bNetworkReady; + //CWPASupplicant m_WPASupplicant; + // Networking + //CNetSubSystem &mNet; + /* + CNetSubSystem* m_pNet; + CNetDevice* m_pNetDevice; + CBcm4343Device m_WLAN; + CWPASupplicant m_WPASupplicant; + bool m_bNetworkReady; + CBcmRandomNumberGenerator m_Random; + */ + CRTPMIDIDevice m_RTPMIDI; }; #endif diff --git a/src/net/applemidi.cpp b/src/net/applemidi.cpp new file mode 100644 index 0000000..b484a07 --- /dev/null +++ b/src/net/applemidi.cpp @@ -0,0 +1,874 @@ +// +// applemidi.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi is free software: you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// mt32-pi is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// mt32-pi. If not, see . +// + +#include +#include +#include +#include +#include +#include +#include + +#include "applemidi.h" +#include "byteorder.h" + +// #define APPLEMIDI_DEBUG + +LOGMODULE("applemidi"); + +constexpr u16 ControlPort = 5004; +constexpr u16 MIDIPort = ControlPort + 1; + +constexpr u16 AppleMIDISignature = 0xFFFF; +constexpr u8 AppleMIDIVersion = 2; + +constexpr u8 RTPMIDIPayloadType = 0x61; +constexpr u8 RTPMIDIVersion = 2; + +// Arbitrary value +constexpr size_t MaxNameLength = 256; + +// Timeout period for invitation (5 seconds in 100 microsecond units) +constexpr unsigned int InvitationTimeout = 5 * 10000; + +// Timeout period for sync packets (60 seconds in 100 microsecond units) +constexpr unsigned int SyncTimeout = 60 * 10000; + +// Receiver feedback packet frequency (1 second in 100 microsecond units) +constexpr unsigned int ReceiverFeedbackPeriod = 1 * 10000; + +constexpr u16 CommandWord(const char Command[2]) { return Command[0] << 8 | Command[1]; } + +enum TAppleMIDICommand : u16 +{ + Invitation = CommandWord("IN"), + InvitationAccepted = CommandWord("OK"), + InvitationRejected = CommandWord("NO"), + Sync = CommandWord("CK"), + ReceiverFeedback = CommandWord("RS"), + EndSession = CommandWord("BY"), +}; + +struct TAppleMIDISession +{ + u16 nSignature; + u16 nCommand; + u32 nVersion; + u32 nInitiatorToken; + u32 nSSRC; + char Name[MaxNameLength]; +} +PACKED; + +// The Name field is optional +constexpr size_t NamelessSessionPacketSize = sizeof(TAppleMIDISession) - sizeof(TAppleMIDISession::Name); + +struct TAppleMIDISync +{ + u16 nSignature; + u16 nCommand; + u32 nSSRC; + u8 nCount; + u8 Padding[3]; + u64 Timestamps[3]; +} +PACKED; + +struct TAppleMIDIReceiverFeedback +{ + u16 nSignature; + u16 nCommand; + u32 nSSRC; + u32 nSequence; +} +PACKED; + +struct TRTPMIDI +{ + u16 nFlags; + u16 nSequence; + u32 nTimestamp; + u32 nSSRC; +} +PACKED; + +u64 GetSyncClock() +{ + static const u64 nStartTime = CTimer::GetClockTicks(); + const u64 nMicrosSinceEpoch = CTimer::GetClockTicks(); + + // Units of 100 microseconds + return (nMicrosSinceEpoch - nStartTime ) / 100; +} + +bool ParseInvitationPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) +{ + const TAppleMIDISession* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < NamelessSessionPacketSize) + return false; + + const u16 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u16 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != Invitation) + return false; + + const u32 nVersion = ntohl(pInPacket->nVersion); + if (nVersion != AppleMIDIVersion) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nVersion = nVersion; + pOutPacket->nInitiatorToken = ntohl(pInPacket->nInitiatorToken); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + if (nSize > NamelessSessionPacketSize) + strncpy(pOutPacket->Name, pInPacket->Name, sizeof(pOutPacket->Name)); + else + strncpy(pOutPacket->Name, "", sizeof(pOutPacket->Name)); + + return true; +} + +bool ParseEndSessionPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) +{ + const TAppleMIDISession* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < NamelessSessionPacketSize) + return false; + + const u16 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u16 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != EndSession) + return false; + + const u32 nVersion = ntohl(pInPacket->nVersion); + if (nVersion != AppleMIDIVersion) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nVersion = nVersion; + pOutPacket->nInitiatorToken = ntohl(pInPacket->nInitiatorToken); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + return true; +} + +bool ParseSyncPacket(const u8* pBuffer, size_t nSize, TAppleMIDISync* pOutPacket) +{ + const TAppleMIDISync* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < sizeof(TAppleMIDISync)) + return false; + + const u32 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u32 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != Sync) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + pOutPacket->nCount = pInPacket->nCount; + pOutPacket->Timestamps[0] = ntohll(pInPacket->Timestamps[0]); + pOutPacket->Timestamps[1] = ntohll(pInPacket->Timestamps[1]); + pOutPacket->Timestamps[2] = ntohll(pInPacket->Timestamps[2]); + + return true; +} + +u8 ParseMIDIDeltaTime(const u8* pBuffer) +{ + u8 nLength = 0; + u32 nDeltaTime = 0; + + while (nLength < 4) + { + nDeltaTime <<= 7; + nDeltaTime |= pBuffer[nLength] & 0x7F; + + // Upper bit not set; end of timestamp + if ((pBuffer[nLength++] & (1 << 7)) == 0) + break; + } + + return nLength; +} + +size_t ParseSysExCommand(const u8* pBuffer, size_t nSize, CAppleMIDIHandler* pHandler) +{ + size_t nBytesParsed = 1; + const u8 nHead = pBuffer[0]; + u8 nTail = 0; + + while (nBytesParsed < nSize && !(nTail == 0xF0 || nTail == 0xF7 || nTail == 0xF4)) + nTail = pBuffer[nBytesParsed++]; + + size_t nReceiveLength = nBytesParsed; + + // First segmented SysEx packet + if (nHead == 0xF0 && nTail == 0xF0) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (first)"); +#endif + --nReceiveLength; + } + + // Middle segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF0) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (middle)"); +#endif + ++pBuffer; + nBytesParsed -= 2; + } + + // Last segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF7) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (last)"); +#endif + ++pBuffer; + --nReceiveLength; + } + + // Cancelled segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF4) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received cancelled SysEx"); +#endif + nReceiveLength = 1; + } + +#ifdef APPLEMIDI_DEBUG + else + { + LOGNOTE("Received complete SysEx"); + } +#endif + + pHandler->OnAppleMIDIDataReceived(pBuffer, nReceiveLength); + + return nBytesParsed; +} + +size_t ParseMIDICommand(const u8* pBuffer, size_t nSize, u8& nRunningStatus, CAppleMIDIHandler* pHandler) +{ + size_t nBytesParsed = 0; + u8 nByte = pBuffer[0]; + + // System Real-Time message - single byte, handle immediately + // Can appear anywhere in the stream, even in between status/data bytes + if (nByte >= 0xF8) + { + // Ignore undefined System Real-Time + if (nByte != 0xF9 && nByte != 0xFD) + pHandler->OnAppleMIDIDataReceived(&nByte, 1); + + return 1; + } + + // Is it a status byte? + if (nByte & 0x80) + { + // Update running status if non Real-Time System status + if (nByte < 0xF0) + nRunningStatus = nByte; + else + nRunningStatus = 0; + + ++nBytesParsed; + } + else + { + // First byte not a status byte and no running status - invalid + if (!nRunningStatus) + return 0; + + // Use running status + nByte = nRunningStatus; + } + + // Channel messages + if (nByte < 0xF0) + { + // How many data bytes? + switch (nByte & 0xF0) + { + case 0x80: // Note off + case 0x90: // Note on + case 0xA0: // Polyphonic key pressure/aftertouch + case 0xB0: // Control change + case 0xE0: // Pitch bend + nBytesParsed += 2; + break; + + case 0xC0: // Program change + case 0xD0: // Channel pressure/aftertouch + nBytesParsed += 1; + break; + } + + // Handle command + pHandler->OnAppleMIDIDataReceived(pBuffer, nBytesParsed); + return nBytesParsed; + } + + // System common commands + switch (nByte) + { + case 0xF0: // Start of System Exclusive + case 0xF7: // End of Exclusive + return ParseSysExCommand(pBuffer, nSize, pHandler); + + case 0xF1: // MIDI Time Code Quarter Frame + case 0xF3: // Song Select + ++nBytesParsed; + break; + + case 0xF2: // Song Position Pointer + nBytesParsed += 2; + break; + } + + pHandler->OnAppleMIDIDataReceived(pBuffer, nBytesParsed); + return nBytesParsed; +} + +bool ParseMIDICommandSection(const u8* pBuffer, size_t nSize, CAppleMIDIHandler* pHandler) +{ + // Must have at least a header byte and a single status byte + if (nSize < 2) + return false; + + size_t nMIDICommandsProcessed = 0; + size_t nBytesRemaining = nSize - 1; + u8 nRunningStatus = 0; + + const u8 nMIDIHeader = pBuffer[0]; + const u8* pMIDICommands = pBuffer + 1; + + // Lower 4 bits of the header is length + u16 nMIDICommandLength = nMIDIHeader & 0x0F; + + // If B flag is set, length value is 12 bits + if (nMIDIHeader & (1 << 7)) + { + nMIDICommandLength <<= 8; + nMIDICommandLength |= pMIDICommands[0]; + ++pMIDICommands; + --nBytesRemaining; + } + + if (nMIDICommandLength > nBytesRemaining) + { + LOGERR("Invalid MIDI command length"); + return false; + } + + // Begin decoding the command list + while (nMIDICommandLength) + { + // If Z flag is set, first list entry is a delta time + if (nMIDICommandsProcessed || nMIDIHeader & (1 << 5)) + { + const u8 nBytesParsed = ParseMIDIDeltaTime(pMIDICommands); + nMIDICommandLength -= nBytesParsed; + pMIDICommands += nBytesParsed; + } + + if (nMIDICommandLength) + { + const size_t nBytesParsed = ParseMIDICommand(pMIDICommands, nMIDICommandLength, nRunningStatus, pHandler); + nMIDICommandLength -= nBytesParsed; + pMIDICommands += nBytesParsed; + ++nMIDICommandsProcessed; + } + } + + return true; +} + +bool ParseMIDIPacket(const u8* pBuffer, size_t nSize, TRTPMIDI* pOutPacket, CAppleMIDIHandler* pHandler) +{ + assert(pHandler != nullptr); + + const TRTPMIDI* const pInPacket = reinterpret_cast(pBuffer); + const u16 nRTPFlags = ntohs(pInPacket->nFlags); + + // Check size (RTP-MIDI header plus MIDI command section header) + if (nSize < sizeof(TRTPMIDI) + 1) + return false; + + // Check version + if (((nRTPFlags >> 14) & 0x03) != RTPMIDIVersion) + return false; + + // Ensure no CSRC identifiers + if (((nRTPFlags >> 8) & 0x0F) != 0) + return false; + + // Check payload type + if ((nRTPFlags & 0xFF) != RTPMIDIPayloadType) + return false; + + pOutPacket->nFlags = nRTPFlags; + pOutPacket->nSequence = ntohs(pInPacket->nSequence); + pOutPacket->nTimestamp = ntohl(pInPacket->nTimestamp); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + // RTP-MIDI variable-length header + const u8* const pMIDICommandSection = pBuffer + sizeof(TRTPMIDI); + size_t nRemaining = nSize - sizeof(TRTPMIDI); + return ParseMIDICommandSection(pMIDICommandSection, nRemaining, pHandler); +} + +CAppleMIDIParticipant::CAppleMIDIParticipant(CBcmRandomNumberGenerator* pRandom, CAppleMIDIHandler* pHandler) + : CTask(TASK_STACK_SIZE, true), + + m_pRandom(pRandom), + + m_pControlSocket(nullptr), + m_pMIDISocket(nullptr), + + m_nForeignControlPort(0), + m_nForeignMIDIPort(0), + m_nInitiatorControlPort(0), + m_nInitiatorMIDIPort(0), + m_ControlBuffer{0}, + m_MIDIBuffer{0}, + + m_nControlResult(0), + m_nMIDIResult(0), + + m_pHandler(pHandler), + + m_State(TState::ControlInvitation), + + m_nInitiatorToken(0), + m_nInitiatorSSRC(0), + m_nSSRC(0), + m_nLastMIDISequenceNumber(0), + + m_nOffsetEstimate(0), + m_nLastSyncTime(0), + + m_nSequence(0), + m_nLastFeedbackSequence(0), + m_nLastFeedbackTime(0) +{ +} + +CAppleMIDIParticipant::~CAppleMIDIParticipant() +{ + if (m_pControlSocket) + delete m_pControlSocket; + + if (m_pMIDISocket) + delete m_pMIDISocket; +} + +bool CAppleMIDIParticipant::Initialize() +{ + assert(m_pControlSocket == nullptr); + assert(m_pMIDISocket == nullptr); + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pControlSocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if ((m_pMIDISocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if (m_pControlSocket->Bind(ControlPort) != 0) + { + LOGERR("Couldn't bind to port %d", ControlPort); + return false; + } + + if (m_pMIDISocket->Bind(MIDIPort) != 0) + { + LOGERR("Couldn't bind to port %d", MIDIPort); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CAppleMIDIParticipant::Run() +{ + assert(m_pControlSocket != nullptr); + assert(m_pMIDISocket != nullptr); + + CScheduler* const pScheduler = CScheduler::Get(); + + while (true) + { + if ((m_nControlResult = m_pControlSocket->ReceiveFrom(m_ControlBuffer, sizeof(m_ControlBuffer), MSG_DONTWAIT, &m_ForeignControlIPAddress, &m_nForeignControlPort)) < 0) + LOGERR("Control socket receive error: %d", m_nControlResult); + + if ((m_nMIDIResult = m_pMIDISocket->ReceiveFrom(m_MIDIBuffer, sizeof(m_MIDIBuffer), MSG_DONTWAIT, &m_ForeignMIDIIPAddress, &m_nForeignMIDIPort)) < 0) + LOGERR("MIDI socket receive error: %d", m_nMIDIResult); + + switch (m_State) + { + case TState::ControlInvitation: + ControlInvitationState(); + break; + + case TState::MIDIInvitation: + MIDIInvitationState(); + break; + + case TState::Connected: + ConnectedState(); + break; + } + + // Allow other tasks to run + pScheduler->Yield(); + } +} + +void CAppleMIDIParticipant::ControlInvitationState() +{ + TAppleMIDISession SessionPacket; + + if (m_nControlResult == 0) + return; + + if (!ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + LOGERR("Unexpected packet"); + return; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- Control invitation"); +#endif + + // Store initiator details + m_InitiatorIPAddress.Set(m_ForeignControlIPAddress); + m_nInitiatorControlPort = m_nForeignControlPort; + m_nInitiatorToken = SessionPacket.nInitiatorToken; + m_nInitiatorSSRC = SessionPacket.nSSRC; + + // Generate random SSRC and accept + m_nSSRC = m_pRandom->GetNumber(); + if (!SendAcceptInvitationPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort)) + { + LOGERR("Couldn't accept control invitation"); + return; + } + + m_nLastSyncTime = GetSyncClock(); + m_State = TState::MIDIInvitation; +} + +void CAppleMIDIParticipant::MIDIInvitationState() +{ + TAppleMIDISession SessionPacket; + + if (m_nControlResult > 0) + { + if (ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + // Unexpected peer; reject invitation + if (m_ForeignControlIPAddress != m_InitiatorIPAddress || m_nForeignControlPort != m_nInitiatorControlPort) + SendRejectInvitationPacket(m_pControlSocket, &m_ForeignControlIPAddress, m_nForeignControlPort, SessionPacket.nInitiatorToken); + else + LOGERR("Unexpected packet"); + } + } + + if (m_nMIDIResult > 0) + { + if (!ParseInvitationPacket(m_MIDIBuffer, m_nMIDIResult, &SessionPacket)) + { + LOGERR("Unexpected packet"); + return; + } + + // Unexpected peer; reject invitation + if (m_ForeignMIDIIPAddress != m_InitiatorIPAddress) + { + SendRejectInvitationPacket(m_pMIDISocket, &m_ForeignMIDIIPAddress, m_nForeignMIDIPort, SessionPacket.nInitiatorToken); + return; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- MIDI invitation"); +#endif + + m_nInitiatorMIDIPort = m_nForeignMIDIPort; + + if (SendAcceptInvitationPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort)) + { + CString IPAddressString; + m_InitiatorIPAddress.Format(&IPAddressString); + LOGNOTE("Connection to %s (%s) established", SessionPacket.Name, static_cast(IPAddressString)); + m_nLastSyncTime = GetSyncClock(); + m_State = TState::Connected; + m_pHandler->OnAppleMIDIConnect(&m_InitiatorIPAddress, SessionPacket.Name); + } + else + { + LOGERR("Couldn't accept MIDI invitation"); + Reset(); + } + } + + // Timeout + else if ((GetSyncClock() - m_nLastSyncTime) > InvitationTimeout) + { + LOGERR("MIDI port invitation timed out"); + Reset(); + } +} + +void CAppleMIDIParticipant::ConnectedState() +{ + TAppleMIDISession SessionPacket; + TRTPMIDI MIDIPacket; + TAppleMIDISync SyncPacket; + + if (m_nControlResult > 0) + { + if (ParseEndSessionPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- End session"); +#endif + + if (m_ForeignControlIPAddress == m_InitiatorIPAddress && + m_nForeignControlPort == m_nInitiatorControlPort && + SessionPacket.nSSRC == m_nInitiatorSSRC) + { + LOGNOTE("Initiator ended session"); + m_pHandler->OnAppleMIDIDisconnect(&m_InitiatorIPAddress, SessionPacket.Name); + Reset(); + return; + } + } + else if (ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + // Unexpected peer; reject invitation + if (m_ForeignControlIPAddress != m_InitiatorIPAddress || m_nForeignControlPort != m_nInitiatorControlPort) + SendRejectInvitationPacket(m_pControlSocket, &m_ForeignControlIPAddress, m_nForeignControlPort, SessionPacket.nInitiatorToken); + else + LOGERR("Unexpected packet"); + } + } + + if (m_nMIDIResult > 0) + { + if (m_ForeignMIDIIPAddress != m_InitiatorIPAddress || m_nForeignMIDIPort != m_nInitiatorMIDIPort) + LOGERR("Unexpected packet"); + else if (ParseMIDIPacket(m_MIDIBuffer, m_nMIDIResult, &MIDIPacket, m_pHandler)) + m_nSequence = MIDIPacket.nSequence; + else if (ParseSyncPacket(m_MIDIBuffer, m_nMIDIResult, &SyncPacket)) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- Sync %d", SyncPacket.nCount); +#endif + + if (SyncPacket.nSSRC == m_nInitiatorSSRC && (SyncPacket.nCount == 0 || SyncPacket.nCount == 2)) + { + if (SyncPacket.nCount == 0) + SendSyncPacket(SyncPacket.Timestamps[0], GetSyncClock()); + else if (SyncPacket.nCount == 2) + { + m_nOffsetEstimate = ((SyncPacket.Timestamps[2] + SyncPacket.Timestamps[0]) / 2) - SyncPacket.Timestamps[1]; +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Offset estimate: %llu", m_nOffsetEstimate); +#endif + } + + m_nLastSyncTime = GetSyncClock(); + } + else + { + LOGERR("Unexpected sync packet"); + } + } + } + + const u64 nTicks = GetSyncClock(); + + if ((nTicks - m_nLastFeedbackTime) > ReceiverFeedbackPeriod) + { + if (m_nSequence != m_nLastFeedbackSequence) + { + SendFeedbackPacket(); + m_nLastFeedbackSequence = m_nSequence; + } + m_nLastFeedbackTime = nTicks; + } + + if ((nTicks - m_nLastSyncTime) > SyncTimeout) + { + LOGERR("Initiator timed out"); + Reset(); + } +} + +void CAppleMIDIParticipant::Reset() +{ + m_State = TState::ControlInvitation; + + m_nInitiatorToken = 0; + m_nInitiatorSSRC = 0; + m_nSSRC = 0; + m_nLastMIDISequenceNumber = 0; + + m_nOffsetEstimate = 0; + m_nLastSyncTime = 0; + + m_nSequence = 0; + m_nLastFeedbackSequence = 0; + m_nLastFeedbackTime = 0; +} + +bool CAppleMIDIParticipant::SendPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, const void* pData, size_t nSize) +{ + const int nResult = pSocket->SendTo(pData, nSize, MSG_DONTWAIT, *pIPAddress, nPort); + + if (nResult < 0) + { + LOGERR("Send failure, error code: %d", nResult); + return false; + } + + if (static_cast(nResult) != nSize) + { + LOGERR("Send failure, only %d/%d bytes sent", nResult, nSize); + return false; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Sent %d bytes to port %d", nResult, nPort); +#endif + + return true; +} + +bool CAppleMIDIParticipant::SendAcceptInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort) +{ + TAppleMIDISession AcceptPacket = + { + htons(AppleMIDISignature), + htons(InvitationAccepted), + htonl(AppleMIDIVersion), + htonl(m_nInitiatorToken), + htonl(m_nSSRC), + {'\0'} + }; + + // TODO: configurable name + strncpy(AcceptPacket.Name, "mt32-pi", sizeof(AcceptPacket.Name)); + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Accept invitation"); +#endif + + const size_t nSendSize = NamelessSessionPacketSize + strlen(AcceptPacket.Name) + 1; + return SendPacket(pSocket, pIPAddress, nPort, &AcceptPacket, nSendSize); +} + +bool CAppleMIDIParticipant::SendRejectInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, u32 nInitiatorToken) +{ + TAppleMIDISession RejectPacket = + { + htons(AppleMIDISignature), + htons(InvitationRejected), + htonl(AppleMIDIVersion), + htonl(nInitiatorToken), + htonl(m_nSSRC), + {'\0'} + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Reject invitation"); +#endif + + // Send without name + return SendPacket(pSocket, pIPAddress, nPort, &RejectPacket, NamelessSessionPacketSize); +} + +bool CAppleMIDIParticipant::SendSyncPacket(u64 nTimestamp1, u64 nTimestamp2) +{ + const TAppleMIDISync SyncPacket = + { + htons(AppleMIDISignature), + htons(Sync), + htonl(m_nSSRC), + 1, + {0}, + { + htonll(nTimestamp1), + htonll(nTimestamp2), + 0 + } + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Sync 1"); +#endif + + return SendPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort, &SyncPacket, sizeof(SyncPacket)); +} + +bool CAppleMIDIParticipant::SendFeedbackPacket() +{ + const TAppleMIDIReceiverFeedback FeedbackPacket = + { + htons(AppleMIDISignature), + htons(ReceiverFeedback), + htonl(m_nSSRC), + htonl(m_nSequence << 16) + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Feedback"); +#endif + + return SendPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort, &FeedbackPacket, sizeof(FeedbackPacket)); +} \ No newline at end of file diff --git a/src/net/applemidi.h b/src/net/applemidi.h new file mode 100644 index 0000000..3df68ae --- /dev/null +++ b/src/net/applemidi.h @@ -0,0 +1,111 @@ +// +// applemidi.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi is free software: you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// mt32-pi is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// mt32-pi. If not, see . +// + +#ifndef _applemidi_h +#define _applemidi_h + +#include +#include +#include +#include + +class CAppleMIDIHandler +{ +public: + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) = 0; + virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) = 0; + virtual void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) = 0; +}; + +class CAppleMIDIParticipant : protected CTask +{ +public: + CAppleMIDIParticipant(CBcmRandomNumberGenerator* pRandom, CAppleMIDIHandler* pHandler); + virtual ~CAppleMIDIParticipant() override; + + bool Initialize(); + + virtual void Run() override; + +private: + void ControlInvitationState(); + void MIDIInvitationState(); + void ConnectedState(); + void Reset(); + + bool SendPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, const void* pData, size_t nSize); + bool SendAcceptInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort); + bool SendRejectInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, u32 nInitiatorToken); + bool SendSyncPacket(u64 nTimestamp1, u64 nTimestamp2); + bool SendFeedbackPacket(); + + CBcmRandomNumberGenerator* m_pRandom; + + // UDP sockets + CSocket* m_pControlSocket; + CSocket* m_pMIDISocket; + + // Foreign peers + CIPAddress m_ForeignControlIPAddress; + CIPAddress m_ForeignMIDIIPAddress; + u16 m_nForeignControlPort; + u16 m_nForeignMIDIPort; + + // Connected peer + CIPAddress m_InitiatorIPAddress; + u16 m_nInitiatorControlPort; + u16 m_nInitiatorMIDIPort; + + // Socket receive buffers + u8 m_ControlBuffer[FRAME_BUFFER_SIZE]; + u8 m_MIDIBuffer[FRAME_BUFFER_SIZE]; + + int m_nControlResult; + int m_nMIDIResult; + + // Callback handler + CAppleMIDIHandler* m_pHandler; + + // Participant state machine + enum class TState + { + ControlInvitation, + MIDIInvitation, + Connected + }; + + TState m_State; + + u32 m_nInitiatorToken = 0; + u32 m_nInitiatorSSRC = 0; + u32 m_nSSRC = 0; + u32 m_nLastMIDISequenceNumber = 0; + + u64 m_nOffsetEstimate = 0; + u64 m_nLastSyncTime = 0; + + u16 m_nSequence = 0; + u16 m_nLastFeedbackSequence = 0; + u64 m_nLastFeedbackTime = 0; +}; + +#endif \ No newline at end of file diff --git a/src/net/byteorder.h b/src/net/byteorder.h new file mode 100644 index 0000000..5160119 --- /dev/null +++ b/src/net/byteorder.h @@ -0,0 +1,42 @@ +// +// byteorder.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi is free software: you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// mt32-pi is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// mt32-pi. If not, see . +// + +#ifndef _byteorder_h +#define _byteorder_h + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define htons(VALUE) (VALUE) +#define htonl(VALUE) (VALUE) +#define htonll(VALUE) (VALUE) +#define ntohs(VALUE) (VALUE) +#define ntohl(VALUE) (VALUE) +#define ntohll(VALUE) (VALUE) +#else +#define htons(VALUE) __builtin_bswap16(VALUE) +#define htonl(VALUE) __builtin_bswap32(VALUE) +#define htonll(VALUE) __builtin_bswap64(VALUE) +#define ntohs(VALUE) __builtin_bswap16(VALUE) +#define ntohl(VALUE) __builtin_bswap32(VALUE) +#define ntohll(VALUE) __builtin_bswap64(VALUE) +#endif + +#endif \ No newline at end of file diff --git a/src/net/udpmidi.cpp b/src/net/udpmidi.cpp new file mode 100644 index 0000000..2f25eda --- /dev/null +++ b/src/net/udpmidi.cpp @@ -0,0 +1,89 @@ +// +// udpmidi.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi is free software: you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// mt32-pi is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// mt32-pi. If not, see . +// + +#include +#include +#include +#include + +#include "udpmidi.h" + +LOGMODULE("udpmidi"); + +constexpr u16 MIDIPort = 1999; + +CUDPMIDIReceiver::CUDPMIDIReceiver(CUDPMIDIHandler* pHandler) + : CTask(TASK_STACK_SIZE, true), + m_pMIDISocket(nullptr), + m_MIDIBuffer{0}, + m_pHandler(pHandler) +{ +} + +CUDPMIDIReceiver::~CUDPMIDIReceiver() +{ + if (m_pMIDISocket) + delete m_pMIDISocket; +} + +bool CUDPMIDIReceiver::Initialize() +{ + assert(m_pMIDISocket == nullptr); + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pMIDISocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if (m_pMIDISocket->Bind(MIDIPort) != 0) + { + LOGERR("Couldn't bind to port %d", MIDIPort); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CUDPMIDIReceiver::Run() +{ + assert(m_pHandler != nullptr); + assert(m_pMIDISocket != nullptr); + + CScheduler* const pScheduler = CScheduler::Get(); + + while (true) + { + // Blocking call + const int nMIDIResult = m_pMIDISocket->Receive(m_MIDIBuffer, sizeof(m_MIDIBuffer), 0); + + if (nMIDIResult < 0) + LOGERR("MIDI socket receive error: %d", nMIDIResult); + else if (nMIDIResult > 0) + m_pHandler->OnUDPMIDIDataReceived(m_MIDIBuffer, nMIDIResult); + + // Allow other tasks to run + pScheduler->Yield(); + } +} \ No newline at end of file diff --git a/src/net/udpmidi.h b/src/net/udpmidi.h new file mode 100644 index 0000000..102d339 --- /dev/null +++ b/src/net/udpmidi.h @@ -0,0 +1,57 @@ +// +// udpmidi.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi is free software: you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// mt32-pi is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// mt32-pi. If not, see . +// + +#ifndef _udpmidi_h +#define _udpmidi_h + +#include +#include +#include + +class CUDPMIDIHandler +{ +public: + virtual void OnUDPMIDIDataReceived(const u8* pData, size_t nSize) = 0; +}; + +class CUDPMIDIReceiver : protected CTask +{ +public: + CUDPMIDIReceiver(CUDPMIDIHandler* pHandler); + virtual ~CUDPMIDIReceiver() override; + + bool Initialize(); + + virtual void Run() override; + +private: + // UDP sockets + CSocket* m_pMIDISocket; + + // Socket receive buffer + u8 m_MIDIBuffer[FRAME_BUFFER_SIZE]; + + // Callback handler + CUDPMIDIHandler* m_pHandler; +}; + +#endif \ No newline at end of file diff --git a/src/rtpmididevice.cpp b/src/rtpmididevice.cpp new file mode 100644 index 0000000..2c29a94 --- /dev/null +++ b/src/rtpmididevice.cpp @@ -0,0 +1,80 @@ +// +// serialmididevice.cpp +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// R. Stange +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include +#include +#include "rtpmididevice.h" +#include + +LOGMODULE("rtpmididevice"); + +CRTPMIDIDevice::CRTPMIDIDevice (CMiniDexed *pSynthesizer, + CConfig *pConfig, CUserInterface *pUI) +: CMIDIDevice (pSynthesizer, pConfig, pUI), + m_pConfig (pConfig) + + //m_Serial (pInterrupt, TRUE), + //m_nSerialState (0), + //m_nSysEx (0), + //m_SendBuffer (&m_Serial) +{ + AddDevice ("rtpdummy"); +} + +CRTPMIDIDevice::~CRTPMIDIDevice (void) +{ + //m_nSerialState = 255; +} + +boolean CRTPMIDIDevice::Initialize (void) +{ + m_pAppleMIDIParticipant = new CAppleMIDIParticipant(&m_Random, this); + if (!m_pAppleMIDIParticipant->Initialize()) + { + LOGERR("Failed to init RTP listener"); + return false; //continue without rtp midi + } + else + LOGNOTE("RTP Listener initialized"); + return true; +} + +// Methods to handle MIDI events + +void CRTPMIDIDevice::OnAppleMIDIDataReceived(const u8* pData, size_t nSize) +{ + LOGNOTE("Recieved RTP MIDI Data"); + printf ("MIDI-RTP: %02X %02X\n", + (unsigned) pData[0], (unsigned) pData[1]); + MIDIMessageHandler(pData, nSize); +} + +void CRTPMIDIDevice::OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) +{ + LOGNOTE("RTP Device connected"); +} + +void CRTPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) +{ + // RemoveRTPDevice +} diff --git a/src/rtpmididevice.h b/src/rtpmididevice.h new file mode 100644 index 0000000..418fcde --- /dev/null +++ b/src/rtpmididevice.h @@ -0,0 +1,69 @@ +// +// .h +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// R. Stange +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#ifndef _rtpmididevice_h +#define _rtpmididevice_h + +#include "mididevice.h" +#include "config.h" +#include "net/applemidi.h" + +#include +#include +#include +#include + +class CMiniDexed; + +class CRTPMIDIDevice : CAppleMIDIHandler, CMIDIDevice +{ +public: + CRTPMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI); + ~CRTPMIDIDevice (void); + + boolean Initialize (void); + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) override; + virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) override; + virtual void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) override; + //void OnAppleMIDIDataReceived(const u8* pData, size_t nSize); + //void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName); + //void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName); + + //void Process (void); + + //void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) override; + +private: + CConfig *m_pConfig; + + //CSerialDevice m_Serial; + //unsigned m_nSerialState; + //unsigned m_nSysEx; + //u8 m_SerialMessage[MAX_MIDI_MESSAGE]; + + //CWriteBufferDevice m_SendBuffer; + CBcmRandomNumberGenerator m_Random; + //CAppleMIDIHandler* m_MIDIHandler; + CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance +}; + +#endif