mirror of https://github.com/probonopd/MiniDexed
commit
607de7e515
@ -0,0 +1,88 @@ |
|||||||
|
#include "arm_float_to_q23.h" |
||||||
|
|
||||||
|
#if defined(ARM_MATH_NEON_EXPERIMENTAL) |
||||||
|
void arm_float_to_q23(const float32_t * pSrc, q23_t * pDst, uint32_t blockSize) |
||||||
|
{ |
||||||
|
const float32_t *pIn = pSrc; /* Src pointer */ |
||||||
|
uint32_t blkCnt; /* loop counter */ |
||||||
|
|
||||||
|
float32x4_t inV; |
||||||
|
|
||||||
|
int32x4_t cvt; |
||||||
|
|
||||||
|
blkCnt = blockSize >> 2U; |
||||||
|
|
||||||
|
/* Compute 4 outputs at a time.
|
||||||
|
** a second loop below computes the remaining 1 to 3 samples. */ |
||||||
|
while (blkCnt > 0U) |
||||||
|
{ |
||||||
|
/* C = A * 8388608 */ |
||||||
|
/* Convert from float to q23 and then store the results in the destination buffer */ |
||||||
|
inV = vld1q_f32(pIn); |
||||||
|
|
||||||
|
cvt = vcvtq_n_s32_f32(inV, 23); |
||||||
|
|
||||||
|
/* saturate */ |
||||||
|
cvt = vminq_s32(cvt, vdupq_n_s32(0x007fffff)); |
||||||
|
cvt = vmaxq_s32(cvt, vdupq_n_s32(0xff800000)); |
||||||
|
|
||||||
|
vst1q_s32(pDst, cvt); |
||||||
|
pDst += 4; |
||||||
|
pIn += 4; |
||||||
|
|
||||||
|
/* Decrement the loop counter */ |
||||||
|
blkCnt--; |
||||||
|
} |
||||||
|
|
||||||
|
/* If the blockSize is not a multiple of 4, compute any remaining output samples here.
|
||||||
|
** No loop unrolling is used. */ |
||||||
|
blkCnt = blockSize & 3; |
||||||
|
|
||||||
|
while (blkCnt > 0U) |
||||||
|
{ |
||||||
|
/* C = A * 8388608 */ |
||||||
|
/* Convert from float to q23 and then store the results in the destination buffer */ |
||||||
|
*pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); |
||||||
|
|
||||||
|
/* Decrement the loop counter */ |
||||||
|
blkCnt--; |
||||||
|
} |
||||||
|
} |
||||||
|
#else |
||||||
|
void arm_float_to_q23(const float32_t * pSrc, q23_t * pDst, uint32_t blockSize) |
||||||
|
{ |
||||||
|
uint32_t blkCnt; /* Loop counter */ |
||||||
|
const float32_t *pIn = pSrc; /* Source pointer */ |
||||||
|
|
||||||
|
/* Loop unrolling: Compute 4 outputs at a time */ |
||||||
|
blkCnt = blockSize >> 2U; |
||||||
|
|
||||||
|
while (blkCnt > 0U) |
||||||
|
{ |
||||||
|
/* C = A * 8388608 */ |
||||||
|
/* convert from float to Q23 and store result in destination buffer */ |
||||||
|
|
||||||
|
*pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); |
||||||
|
*pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); |
||||||
|
*pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); |
||||||
|
*pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); |
||||||
|
|
||||||
|
/* Decrement loop counter */ |
||||||
|
blkCnt--; |
||||||
|
} |
||||||
|
|
||||||
|
/* Loop unrolling: Compute remaining outputs */ |
||||||
|
blkCnt = blockSize % 0x4U; |
||||||
|
|
||||||
|
while (blkCnt > 0U) |
||||||
|
{ |
||||||
|
/* C = A * 8388608 */ |
||||||
|
/* Convert from float to q23 and then store the results in the destination buffer */ |
||||||
|
*pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); |
||||||
|
|
||||||
|
/* Decrement loop counter */ |
||||||
|
blkCnt--; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
#endif /* #if defined(ARM_MATH_NEON_EXPERIMENTAL) */ |
@ -0,0 +1,22 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "arm_math_types.h" |
||||||
|
|
||||||
|
typedef int32_t q23_t; |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
extern "C" |
||||||
|
{ |
||||||
|
#endif |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Converts the elements of the floating-point vector to Q23 vector. |
||||||
|
* @param[in] pSrc points to the floating-point input vector |
||||||
|
* @param[out] pDst points to the Q23 output vector |
||||||
|
* @param[in] blockSize length of the input vector |
||||||
|
*/ |
||||||
|
void arm_float_to_q23(const float32_t * pSrc, q23_t * pDst, uint32_t blockSize); |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
} |
||||||
|
#endif |
@ -0,0 +1,52 @@ |
|||||||
|
//
|
||||||
|
// midi.h
|
||||||
|
//
|
||||||
|
// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
|
||||||
|
// Copyright (C) 2025 The MiniDexed Team
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
#ifndef _midi_h |
||||||
|
#define _midi_h |
||||||
|
|
||||||
|
#define MIDI_NOTE_OFF 0b1000 |
||||||
|
#define MIDI_NOTE_ON 0b1001 |
||||||
|
#define MIDI_AFTERTOUCH 0b1010 // TODO
|
||||||
|
#define MIDI_CHANNEL_AFTERTOUCH 0b1101 // right now Synth_Dexed just manage Channel Aftertouch not Polyphonic AT -> 0b1010
|
||||||
|
#define MIDI_CONTROL_CHANGE 0b1011 |
||||||
|
|
||||||
|
#define MIDI_CC_BANK_SELECT_MSB 0 |
||||||
|
#define MIDI_CC_MODULATION 1 |
||||||
|
#define MIDI_CC_BREATH_CONTROLLER 2 |
||||||
|
#define MIDI_CC_FOOT_PEDAL 4 |
||||||
|
#define MIDI_CC_PORTAMENTO_TIME 5 |
||||||
|
#define MIDI_CC_VOLUME 7 |
||||||
|
#define MIDI_CC_PAN_POSITION 10 |
||||||
|
#define MIDI_CC_EXPRESSION 11 |
||||||
|
#define MIDI_CC_BANK_SELECT_LSB 32 |
||||||
|
#define MIDI_CC_BANK_SUSTAIN 64 |
||||||
|
#define MIDI_CC_PORTAMENTO 65 |
||||||
|
#define MIDI_CC_SOSTENUTO 66 |
||||||
|
#define MIDI_CC_HOLD2 69 |
||||||
|
#define MIDI_CC_RESONANCE 71 |
||||||
|
#define MIDI_CC_FREQUENCY_CUTOFF 74 |
||||||
|
#define MIDI_CC_REVERB_LEVEL 91 |
||||||
|
#define MIDI_CC_DETUNE_LEVEL 94 |
||||||
|
#define MIDI_CC_ALL_SOUND_OFF 120 |
||||||
|
#define MIDI_CC_ALL_NOTES_OFF 123 |
||||||
|
|
||||||
|
#define MIDI_PROGRAM_CHANGE 0b1100 |
||||||
|
#define MIDI_PITCH_BEND 0b1110 |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,874 @@ |
|||||||
|
//
|
||||||
|
// applemidi.cpp
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <circle/logger.h> |
||||||
|
#include <circle/macros.h> |
||||||
|
#include <circle/net/in.h> |
||||||
|
#include <circle/net/netsubsystem.h> |
||||||
|
#include <circle/sched/scheduler.h> |
||||||
|
#include <circle/timer.h> |
||||||
|
#include <circle/util.h> |
||||||
|
|
||||||
|
#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<const TAppleMIDISession*>(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, "<unknown>", sizeof(pOutPacket->Name)); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
bool ParseEndSessionPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) |
||||||
|
{ |
||||||
|
const TAppleMIDISession* const pInPacket = reinterpret_cast<const TAppleMIDISession*>(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<const TAppleMIDISync*>(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<const TRTPMIDI*>(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<const char*>(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<size_t>(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, "MiniDexed", 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)); |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
//
|
||||||
|
// applemidi.h
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef _applemidi_h |
||||||
|
#define _applemidi_h |
||||||
|
|
||||||
|
#include <circle/bcmrandom.h> |
||||||
|
#include <circle/net/ipaddress.h> |
||||||
|
#include <circle/net/socket.h> |
||||||
|
#include <circle/sched/task.h> |
||||||
|
|
||||||
|
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 |
@ -0,0 +1,42 @@ |
|||||||
|
//
|
||||||
|
// byteorder.h
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#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 |
@ -0,0 +1,111 @@ |
|||||||
|
//
|
||||||
|
// ftpdaemon.cpp
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <circle/logger.h> |
||||||
|
#include <circle/net/in.h> |
||||||
|
#include <circle/net/ipaddress.h> |
||||||
|
#include <circle/net/netsubsystem.h> |
||||||
|
#include <circle/string.h> |
||||||
|
|
||||||
|
#include "ftpdaemon.h" |
||||||
|
#include "ftpworker.h" |
||||||
|
|
||||||
|
LOGMODULE("ftpd"); |
||||||
|
|
||||||
|
constexpr u16 ListenPort = 21; |
||||||
|
constexpr u8 MaxConnections = 1; |
||||||
|
|
||||||
|
CFTPDaemon::CFTPDaemon(const char* pUser, const char* pPassword) |
||||||
|
: CTask(TASK_STACK_SIZE, true), |
||||||
|
m_pListenSocket(nullptr), |
||||||
|
m_pUser(pUser), |
||||||
|
m_pPassword(pPassword) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
CFTPDaemon::~CFTPDaemon() |
||||||
|
{ |
||||||
|
if (m_pListenSocket) |
||||||
|
delete m_pListenSocket; |
||||||
|
} |
||||||
|
|
||||||
|
bool CFTPDaemon::Initialize() |
||||||
|
{ |
||||||
|
CNetSubSystem* const pNet = CNetSubSystem::Get(); |
||||||
|
|
||||||
|
if ((m_pListenSocket = new CSocket(pNet, IPPROTO_TCP)) == nullptr) |
||||||
|
return false; |
||||||
|
|
||||||
|
if (m_pListenSocket->Bind(ListenPort) != 0) |
||||||
|
{ |
||||||
|
LOGERR("Couldn't bind to port %d", ListenPort); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (m_pListenSocket->Listen() != 0) |
||||||
|
{ |
||||||
|
LOGERR("Failed to listen on control socket"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// We started as a suspended task; run now that initialization is successful
|
||||||
|
Start(); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void CFTPDaemon::Run() |
||||||
|
{ |
||||||
|
assert(m_pListenSocket != nullptr); |
||||||
|
|
||||||
|
LOGNOTE("Listener task spawned"); |
||||||
|
|
||||||
|
while (true) |
||||||
|
{ |
||||||
|
CIPAddress ClientIPAddress; |
||||||
|
u16 nClientPort; |
||||||
|
|
||||||
|
LOGDBG("Listener: waiting for connection"); |
||||||
|
CSocket* pConnection = m_pListenSocket->Accept(&ClientIPAddress, &nClientPort); |
||||||
|
|
||||||
|
if (pConnection == nullptr) |
||||||
|
{ |
||||||
|
LOGERR("Unable to accept connection"); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
CString IPAddressString; |
||||||
|
ClientIPAddress.Format(&IPAddressString); |
||||||
|
LOGNOTE("Incoming connection from %s:%d", static_cast<const char*>(IPAddressString), nClientPort); |
||||||
|
|
||||||
|
if (CFTPWorker::GetInstanceCount() >= MaxConnections) |
||||||
|
{ |
||||||
|
pConnection->Send("421 Maximum number of connections reached.\r\n", 45, 0); |
||||||
|
delete pConnection; |
||||||
|
LOGWARN("Maximum number of connections reached"); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Spawn new worker
|
||||||
|
new CFTPWorker(pConnection, m_pUser, m_pPassword); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
//
|
||||||
|
// ftpdaemon.h
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef _ftpdaemon_h |
||||||
|
#define _ftpdaemon_h |
||||||
|
|
||||||
|
#include <circle/net/socket.h> |
||||||
|
#include <circle/sched/task.h> |
||||||
|
|
||||||
|
class CFTPDaemon : protected CTask |
||||||
|
{ |
||||||
|
public: |
||||||
|
CFTPDaemon(const char* pUser, const char* pPassword); |
||||||
|
virtual ~CFTPDaemon() override; |
||||||
|
|
||||||
|
bool Initialize(); |
||||||
|
|
||||||
|
virtual void Run() override; |
||||||
|
|
||||||
|
private: |
||||||
|
// TCP sockets
|
||||||
|
CSocket* m_pListenSocket; |
||||||
|
|
||||||
|
const char* m_pUser; |
||||||
|
const char* m_pPassword; |
||||||
|
}; |
||||||
|
|
||||||
|
#endif |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,157 @@ |
|||||||
|
//
|
||||||
|
// ftpworker.h
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef _ftpworker_h |
||||||
|
#define _ftpworker_h |
||||||
|
|
||||||
|
#include <circle/net/ipaddress.h> |
||||||
|
#include <circle/net/socket.h> |
||||||
|
#include <circle/sched/task.h> |
||||||
|
#include <circle/string.h> |
||||||
|
|
||||||
|
// TODO: These may be incomplete/inaccurate
|
||||||
|
enum TFTPStatus |
||||||
|
{ |
||||||
|
FileStatusOk = 150, |
||||||
|
|
||||||
|
Success = 200, |
||||||
|
SystemType = 215, |
||||||
|
ReadyForNewUser = 220, |
||||||
|
ClosingControl = 221, |
||||||
|
TransferComplete = 226, |
||||||
|
EnteringPassiveMode = 227, |
||||||
|
UserLoggedIn = 230, |
||||||
|
FileActionOk = 250, |
||||||
|
PathCreated = 257, |
||||||
|
|
||||||
|
PasswordRequired = 331, |
||||||
|
AccountRequired = 332, |
||||||
|
PendingFurtherInfo = 350, |
||||||
|
|
||||||
|
ServiceNotAvailable = 421, |
||||||
|
DataConnectionFailed = 425, |
||||||
|
FileActionNotTaken = 450, |
||||||
|
ActionAborted = 451, |
||||||
|
|
||||||
|
CommandUnrecognized = 500, |
||||||
|
SyntaxError = 501, |
||||||
|
CommandNotImplemented = 502, |
||||||
|
BadCommandSequence = 503, |
||||||
|
NotLoggedIn = 530, |
||||||
|
FileNotFound = 550, |
||||||
|
FileNameNotAllowed = 553, |
||||||
|
}; |
||||||
|
|
||||||
|
enum class TTransferMode |
||||||
|
{ |
||||||
|
Active, |
||||||
|
Passive, |
||||||
|
}; |
||||||
|
|
||||||
|
enum class TDataType |
||||||
|
{ |
||||||
|
ASCII, |
||||||
|
Binary, |
||||||
|
}; |
||||||
|
|
||||||
|
struct TFTPCommand; |
||||||
|
struct TDirectoryListEntry; |
||||||
|
|
||||||
|
class CFTPWorker : protected CTask |
||||||
|
{ |
||||||
|
public: |
||||||
|
CFTPWorker(CSocket* pControlSocket, const char* pExpectedUser, const char* pExpectedPassword); |
||||||
|
virtual ~CFTPWorker() override; |
||||||
|
|
||||||
|
virtual void Run() override; |
||||||
|
|
||||||
|
static u8 GetInstanceCount() { return s_nInstanceCount; } |
||||||
|
|
||||||
|
private: |
||||||
|
CSocket* OpenDataConnection(); |
||||||
|
|
||||||
|
bool SendStatus(TFTPStatus StatusCode, const char* pMessage); |
||||||
|
|
||||||
|
bool CheckLoggedIn(); |
||||||
|
|
||||||
|
// Directory navigation
|
||||||
|
CString RealPath(const char* pInBuffer) const; |
||||||
|
const TDirectoryListEntry* BuildDirectoryList(size_t& nOutEntries) const; |
||||||
|
|
||||||
|
// FTP command handlers
|
||||||
|
bool System(const char* pArgs); |
||||||
|
bool Username(const char* pArgs); |
||||||
|
bool Port(const char* pArgs); |
||||||
|
bool Passive(const char* pArgs); |
||||||
|
bool Password(const char* pArgs); |
||||||
|
bool Type(const char* pArgs); |
||||||
|
bool Retrieve(const char* pArgs); |
||||||
|
bool Store(const char* pArgs); |
||||||
|
bool Delete(const char* pArgs); |
||||||
|
bool MakeDirectory(const char* pArgs); |
||||||
|
bool ChangeWorkingDirectory(const char* pArgs); |
||||||
|
bool ChangeToParentDirectory(const char* pArgs); |
||||||
|
bool PrintWorkingDirectory(const char* pArgs); |
||||||
|
bool List(const char* pArgs); |
||||||
|
bool ListFileNames(const char* pArgs); |
||||||
|
bool RenameFrom(const char* pArgs); |
||||||
|
bool RenameTo(const char* pArgs); |
||||||
|
bool Bye(const char* pArgs); |
||||||
|
bool NoOp(const char* pArgs); |
||||||
|
|
||||||
|
CString m_LogName; |
||||||
|
|
||||||
|
// Authentication
|
||||||
|
const char* m_pExpectedUser; |
||||||
|
const char* m_pExpectedPassword; |
||||||
|
|
||||||
|
// TCP sockets
|
||||||
|
CSocket* m_pControlSocket; |
||||||
|
CSocket* m_pDataSocket; |
||||||
|
u16 m_nDataSocketPort; |
||||||
|
CIPAddress m_DataSocketIPAddress; |
||||||
|
|
||||||
|
// Command/data buffers
|
||||||
|
char m_CommandBuffer[FRAME_BUFFER_SIZE]; |
||||||
|
u8 m_DataBuffer[FRAME_BUFFER_SIZE]; |
||||||
|
|
||||||
|
// Session state
|
||||||
|
CString m_User; |
||||||
|
CString m_Password; |
||||||
|
TDataType m_DataType; |
||||||
|
TTransferMode m_TransferMode; |
||||||
|
CString m_CurrentPath; |
||||||
|
CString m_RenameFrom; |
||||||
|
|
||||||
|
static void FatFsPathToFTPPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); |
||||||
|
static void FTPPathToFatFsPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); |
||||||
|
|
||||||
|
static void FatFsParentPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); |
||||||
|
|
||||||
|
static void FormatLastModifiedDate(u16 nDate, char* pOutBuffer, size_t nSize); |
||||||
|
static void FormatLastModifiedTime(u16 nDate, char* pOutBuffer, size_t nSize); |
||||||
|
|
||||||
|
static const TFTPCommand Commands[]; |
||||||
|
static u8 s_nInstanceCount; |
||||||
|
}; |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,351 @@ |
|||||||
|
//
|
||||||
|
// mdnspublisher.cpp
|
||||||
|
//
|
||||||
|
// Circle - A C++ bare metal environment for Raspberry Pi
|
||||||
|
// Copyright (C) 2024 R. Stange <rsta2@o2online.de>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
#include "mdnspublisher.h" |
||||||
|
#include <circle/sched/scheduler.h> |
||||||
|
#include <circle/net/in.h> |
||||||
|
#include <circle/logger.h> |
||||||
|
#include <circle/util.h> |
||||||
|
#include <assert.h> |
||||||
|
#define MDNS_HOST_GROUP {224, 0, 0, 251} |
||||||
|
#define MDNS_PORT 5353 |
||||||
|
#define MDNS_DOMAIN "local" |
||||||
|
#define RR_TYPE_A 1 |
||||||
|
#define RR_TYPE_PTR 12 |
||||||
|
#define RR_TYPE_TXT 16 |
||||||
|
#define RR_TYPE_SRV 33 |
||||||
|
#define RR_CLASS_IN 1 |
||||||
|
#define RR_CACHE_FLUSH 0x8000 |
||||||
|
LOGMODULE ("mdnspub"); |
||||||
|
CmDNSPublisher::CmDNSPublisher (CNetSubSystem *pNet) |
||||||
|
: m_pNet (pNet), |
||||||
|
m_pSocket (nullptr), |
||||||
|
m_bRunning (FALSE), |
||||||
|
m_pWritePtr (nullptr), |
||||||
|
m_pDataLen (nullptr) |
||||||
|
{ |
||||||
|
SetName ("mdnspub"); |
||||||
|
} |
||||||
|
CmDNSPublisher::~CmDNSPublisher (void) |
||||||
|
{ |
||||||
|
assert (!m_pSocket); |
||||||
|
m_bRunning = FALSE; |
||||||
|
} |
||||||
|
boolean CmDNSPublisher::PublishService (const char *pServiceName, const char *pServiceType, |
||||||
|
u16 usServicePort, const char *ppText[]) |
||||||
|
{ |
||||||
|
if (!m_bRunning) |
||||||
|
{ |
||||||
|
// Let task can run once to initialize
|
||||||
|
CScheduler::Get ()->Yield (); |
||||||
|
if (!m_bRunning) |
||||||
|
{ |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
} |
||||||
|
assert (pServiceName); |
||||||
|
assert (pServiceType); |
||||||
|
TService *pService = new TService {pServiceName, pServiceType, usServicePort, 0}; |
||||||
|
assert (pService); |
||||||
|
if (ppText) |
||||||
|
{ |
||||||
|
for (unsigned i = 0; i < MaxTextRecords && ppText[i]; i++) |
||||||
|
{ |
||||||
|
pService->ppText[i] = new CString (ppText[i]); |
||||||
|
assert (pService->ppText[i]); |
||||||
|
pService->nTextRecords++; |
||||||
|
} |
||||||
|
} |
||||||
|
m_Mutex.Acquire (); |
||||||
|
// Insert as first element into list
|
||||||
|
TPtrListElement *pElement = m_ServiceList.GetFirst (); |
||||||
|
if (pElement) |
||||||
|
{ |
||||||
|
m_ServiceList.InsertBefore (pElement, pService); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
m_ServiceList.InsertAfter (nullptr, pService); |
||||||
|
} |
||||||
|
m_Mutex.Release (); |
||||||
|
LOGDBG ("Publish service %s", (const char *) pService->ServiceName); |
||||||
|
m_Event.Set (); // Trigger resent for everything
|
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
boolean CmDNSPublisher::UnpublishService (const char *pServiceName) |
||||||
|
{ |
||||||
|
if (!m_bRunning) |
||||||
|
{ |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
assert (pServiceName); |
||||||
|
m_Mutex.Acquire (); |
||||||
|
// Find service in the list and remove it
|
||||||
|
TService *pService = nullptr; |
||||||
|
TPtrListElement *pElement = m_ServiceList.GetFirst (); |
||||||
|
while (pElement) |
||||||
|
{ |
||||||
|
pService = static_cast<TService *> (CPtrList::GetPtr (pElement)); |
||||||
|
assert (pService); |
||||||
|
if (pService->ServiceName.Compare (pServiceName) == 0) |
||||||
|
{ |
||||||
|
m_ServiceList.Remove (pElement); |
||||||
|
break; |
||||||
|
} |
||||||
|
pService = nullptr; |
||||||
|
pElement = m_ServiceList.GetNext (pElement); |
||||||
|
} |
||||||
|
m_Mutex.Release (); |
||||||
|
if (!pService) |
||||||
|
{ |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
LOGDBG ("Unpublish service %s", (const char *) pService->ServiceName); |
||||||
|
SendResponse (pService, FALSE); |
||||||
|
/*
|
||||||
|
if (!SendResponse (pService, TRUE)) |
||||||
|
{ |
||||||
|
LOGWARN ("Send failed"); |
||||||
|
} |
||||||
|
*/ |
||||||
|
for (unsigned i = 0; i < pService->nTextRecords; i++) |
||||||
|
{ |
||||||
|
delete pService->ppText[i]; |
||||||
|
} |
||||||
|
delete pService; |
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
void CmDNSPublisher::Run (void) |
||||||
|
{ |
||||||
|
assert (m_pNet); |
||||||
|
assert (!m_pSocket); |
||||||
|
m_pSocket = new CSocket (m_pNet, IPPROTO_UDP); |
||||||
|
assert (m_pSocket); |
||||||
|
if (m_pSocket->Bind (MDNS_PORT) < 0) |
||||||
|
{ |
||||||
|
LOGERR ("Cannot bind to port %u", MDNS_PORT); |
||||||
|
delete m_pSocket; |
||||||
|
m_pSocket = nullptr; |
||||||
|
while (1) |
||||||
|
{ |
||||||
|
m_Event.Clear (); |
||||||
|
m_Event.Wait (); |
||||||
|
} |
||||||
|
} |
||||||
|
static const u8 mDNSIPAddress[] = MDNS_HOST_GROUP; |
||||||
|
CIPAddress mDNSIP (mDNSIPAddress); |
||||||
|
if (m_pSocket->Connect (mDNSIP, MDNS_PORT) < 0) |
||||||
|
{ |
||||||
|
LOGERR ("Cannot connect to mDNS host group"); |
||||||
|
delete m_pSocket; |
||||||
|
m_pSocket = nullptr; |
||||||
|
while (1) |
||||||
|
{ |
||||||
|
m_Event.Clear (); |
||||||
|
m_Event.Wait (); |
||||||
|
} |
||||||
|
} |
||||||
|
m_bRunning = TRUE; |
||||||
|
while (1) |
||||||
|
{ |
||||||
|
m_Event.Clear (); |
||||||
|
m_Event.WaitWithTimeout ((TTLShort - 10) * 1000000); |
||||||
|
for (unsigned i = 1; i <= 3; i++) |
||||||
|
{ |
||||||
|
m_Mutex.Acquire (); |
||||||
|
TPtrListElement *pElement = m_ServiceList.GetFirst (); |
||||||
|
while (pElement) |
||||||
|
{ |
||||||
|
TService *pService = |
||||||
|
static_cast<TService *> (CPtrList::GetPtr (pElement)); |
||||||
|
assert (pService); |
||||||
|
SendResponse (pService, FALSE); |
||||||
|
/*
|
||||||
|
if (!SendResponse (pService, FALSE)) |
||||||
|
{ |
||||||
|
LOGWARN ("Send failed"); |
||||||
|
} |
||||||
|
*/ |
||||||
|
pElement = m_ServiceList.GetNext (pElement); |
||||||
|
} |
||||||
|
m_Mutex.Release (); |
||||||
|
CScheduler::Get ()->Sleep (1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
boolean CmDNSPublisher::SendResponse (TService *pService, boolean bDelete) |
||||||
|
{ |
||||||
|
assert (pService); |
||||||
|
assert (m_pNet); |
||||||
|
// Collect data
|
||||||
|
static const char Domain[] = "." MDNS_DOMAIN; |
||||||
|
CString ServiceType (pService->ServiceType); |
||||||
|
ServiceType.Append (Domain); |
||||||
|
CString ServiceName (pService->ServiceName); |
||||||
|
ServiceName.Append ("."); |
||||||
|
ServiceName.Append (ServiceType); |
||||||
|
CString Hostname (m_pNet->GetHostname ()); |
||||||
|
Hostname.Append (Domain); |
||||||
|
// Start writing buffer
|
||||||
|
assert (!m_pWritePtr); |
||||||
|
m_pWritePtr = m_Buffer; |
||||||
|
// mDNS Header
|
||||||
|
PutWord (0); // Transaction ID
|
||||||
|
PutWord (0x8400); // Message is a response, Server is an authority for the domain
|
||||||
|
PutWord (0); // Questions
|
||||||
|
PutWord (5); // Answer RRs
|
||||||
|
PutWord (0); // Authority RRs
|
||||||
|
PutWord (0); // Additional RRs
|
||||||
|
// Answer RRs
|
||||||
|
// PTR
|
||||||
|
PutDNSName ("_services._dns-sd._udp.local"); |
||||||
|
PutWord (RR_TYPE_PTR); |
||||||
|
PutWord (RR_CLASS_IN); |
||||||
|
PutDWord (bDelete ? TTLDelete : TTLLong); |
||||||
|
ReserveDataLength (); |
||||||
|
u8 *pServiceTypePtr = m_pWritePtr; |
||||||
|
PutDNSName (ServiceType); |
||||||
|
SetDataLength (); |
||||||
|
// PTR
|
||||||
|
PutCompressedString (pServiceTypePtr); |
||||||
|
PutWord (RR_TYPE_PTR); |
||||||
|
PutWord (RR_CLASS_IN); |
||||||
|
PutDWord (bDelete ? TTLDelete : TTLLong); |
||||||
|
ReserveDataLength (); |
||||||
|
u8 *pServiceNamePtr = m_pWritePtr; |
||||||
|
PutDNSName (ServiceName); |
||||||
|
SetDataLength (); |
||||||
|
// SRV
|
||||||
|
PutCompressedString (pServiceNamePtr); |
||||||
|
PutWord (RR_TYPE_SRV); |
||||||
|
PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); |
||||||
|
PutDWord (bDelete ? TTLDelete : TTLShort); |
||||||
|
ReserveDataLength (); |
||||||
|
PutWord (0); // Priority
|
||||||
|
PutWord (0); // Weight
|
||||||
|
PutWord (pService->usServicePort); |
||||||
|
u8 *pHostnamePtr = m_pWritePtr; |
||||||
|
PutDNSName (Hostname); |
||||||
|
SetDataLength (); |
||||||
|
// A
|
||||||
|
PutCompressedString (pHostnamePtr); |
||||||
|
PutWord (RR_TYPE_A); |
||||||
|
PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); |
||||||
|
PutDWord (TTLShort); |
||||||
|
ReserveDataLength (); |
||||||
|
PutIPAddress (*m_pNet->GetConfig ()->GetIPAddress ()); |
||||||
|
SetDataLength (); |
||||||
|
// TXT
|
||||||
|
PutCompressedString (pServiceNamePtr); |
||||||
|
PutWord (RR_TYPE_TXT); |
||||||
|
PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); |
||||||
|
PutDWord (bDelete ? TTLDelete : TTLLong); |
||||||
|
ReserveDataLength (); |
||||||
|
for (int i = pService->nTextRecords-1; i >= 0; i--) // In reverse order
|
||||||
|
{ |
||||||
|
assert (pService->ppText[i]); |
||||||
|
PutString (*pService->ppText[i]); |
||||||
|
} |
||||||
|
SetDataLength (); |
||||||
|
unsigned nMsgSize = m_pWritePtr - m_Buffer; |
||||||
|
m_pWritePtr = nullptr; |
||||||
|
if (nMsgSize >= MaxMessageSize) |
||||||
|
{ |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
assert (m_pSocket); |
||||||
|
return m_pSocket->Send (m_Buffer, nMsgSize, MSG_DONTWAIT) == (int) nMsgSize; |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutByte (u8 uchValue) |
||||||
|
{ |
||||||
|
assert (m_pWritePtr); |
||||||
|
if ((unsigned) (m_pWritePtr - m_Buffer) < MaxMessageSize) |
||||||
|
{ |
||||||
|
*m_pWritePtr++ = uchValue; |
||||||
|
} |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutWord (u16 usValue) |
||||||
|
{ |
||||||
|
PutByte (usValue >> 8); |
||||||
|
PutByte (usValue & 0xFF); |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutDWord (u32 nValue) |
||||||
|
{ |
||||||
|
PutWord (nValue >> 16); |
||||||
|
PutWord (nValue & 0xFFFF); |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutString (const char *pValue) |
||||||
|
{ |
||||||
|
assert (pValue); |
||||||
|
size_t nLen = strlen (pValue); |
||||||
|
assert (nLen <= 255); |
||||||
|
PutByte (nLen); |
||||||
|
while (*pValue) |
||||||
|
{ |
||||||
|
PutByte (static_cast<u8> (*pValue++)); |
||||||
|
} |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutCompressedString (const u8 *pWritePtr) |
||||||
|
{ |
||||||
|
assert (m_pWritePtr); |
||||||
|
assert (pWritePtr < m_pWritePtr); |
||||||
|
unsigned nOffset = pWritePtr - m_Buffer; |
||||||
|
assert (nOffset < MaxMessageSize); |
||||||
|
nOffset |= 0xC000; |
||||||
|
PutWord (static_cast<u16> (nOffset)); |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutDNSName (const char *pValue) |
||||||
|
{ |
||||||
|
char Buffer[256]; |
||||||
|
assert (pValue); |
||||||
|
strncpy (Buffer, pValue, sizeof Buffer); |
||||||
|
Buffer[sizeof Buffer-1] = '\0'; |
||||||
|
char *pSavePtr = nullptr; |
||||||
|
char *pToken = strtok_r (Buffer, ".", &pSavePtr); |
||||||
|
while (pToken) |
||||||
|
{ |
||||||
|
PutString (pToken); |
||||||
|
pToken = strtok_r (nullptr, ".", &pSavePtr); |
||||||
|
} |
||||||
|
PutByte (0); |
||||||
|
} |
||||||
|
void CmDNSPublisher::PutIPAddress (const CIPAddress &rValue) |
||||||
|
{ |
||||||
|
u8 Buffer[IP_ADDRESS_SIZE]; |
||||||
|
rValue.CopyTo (Buffer); |
||||||
|
for (unsigned i = 0; i < IP_ADDRESS_SIZE; i++) |
||||||
|
{ |
||||||
|
PutByte (Buffer[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
void CmDNSPublisher::ReserveDataLength (void) |
||||||
|
{ |
||||||
|
assert (!m_pDataLen); |
||||||
|
m_pDataLen = m_pWritePtr; |
||||||
|
assert (m_pDataLen); |
||||||
|
PutWord (0); |
||||||
|
} |
||||||
|
void CmDNSPublisher::SetDataLength (void) |
||||||
|
{ |
||||||
|
assert (m_pDataLen); |
||||||
|
assert (m_pWritePtr); |
||||||
|
assert (m_pWritePtr > m_pDataLen); |
||||||
|
*reinterpret_cast<u16 *> (m_pDataLen) = le2be16 (m_pWritePtr - m_pDataLen - sizeof (u16)); |
||||||
|
m_pDataLen = nullptr; |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
//
|
||||||
|
// mdnspublisher.h
|
||||||
|
//
|
||||||
|
// Circle - A C++ bare metal environment for Raspberry Pi
|
||||||
|
// Copyright (C) 2024 R. Stange <rsta2@o2online.de>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
#ifndef _circle_net_mdnspublisher_h |
||||||
|
#define _circle_net_mdnspublisher_h |
||||||
|
#include <circle/sched/task.h> |
||||||
|
#include <circle/sched/mutex.h> |
||||||
|
#include <circle/sched/synchronizationevent.h> |
||||||
|
#include <circle/net/netsubsystem.h> |
||||||
|
#include <circle/net/socket.h> |
||||||
|
#include <circle/net/ipaddress.h> |
||||||
|
#include <circle/ptrlist.h> |
||||||
|
#include <circle/string.h> |
||||||
|
#include <circle/types.h> |
||||||
|
class CmDNSPublisher : public CTask /// mDNS / Bonjour client task
|
||||||
|
{ |
||||||
|
public: |
||||||
|
static constexpr const char *ServiceTypeAppleMIDI = "_apple-midi._udp"; |
||||||
|
public: |
||||||
|
/// \param pNet Pointer to the network subsystem object
|
||||||
|
CmDNSPublisher (CNetSubSystem *pNet); |
||||||
|
~CmDNSPublisher (void); |
||||||
|
/// \brief Start publishing a service
|
||||||
|
/// \param pServiceName Name of the service to be published
|
||||||
|
/// \param pServiceType Type of the service to be published (e.g. ServiceTypeAppleMIDI)
|
||||||
|
/// \param usServicePort Port number of the service to be published (in host byte order)
|
||||||
|
/// \param ppText Descriptions of the service (terminated with a nullptr, or nullptr itself)
|
||||||
|
/// \return Operation successful?
|
||||||
|
boolean PublishService (const char *pServiceName, |
||||||
|
const char *pServiceType, |
||||||
|
u16 usServicePort, |
||||||
|
const char *ppText[] = nullptr); |
||||||
|
/// \brief Stop publishing a service
|
||||||
|
/// \param pServiceName Name of the service to be unpublished (same as when published)
|
||||||
|
/// \return Operation successful?
|
||||||
|
boolean UnpublishService (const char *pServiceName); |
||||||
|
void Run (void) override; |
||||||
|
private: |
||||||
|
static const unsigned MaxTextRecords = 10; |
||||||
|
static const unsigned MaxMessageSize = 1400; // safe UDP payload in an Ethernet frame
|
||||||
|
static const unsigned TTLShort = 15; // seconds
|
||||||
|
static const unsigned TTLLong = 4500; |
||||||
|
static const unsigned TTLDelete = 0; |
||||||
|
struct TService |
||||||
|
{ |
||||||
|
CString ServiceName; |
||||||
|
CString ServiceType; |
||||||
|
u16 usServicePort; |
||||||
|
unsigned nTextRecords; |
||||||
|
CString *ppText[MaxTextRecords]; |
||||||
|
}; |
||||||
|
boolean SendResponse (TService *pService, boolean bDelete); |
||||||
|
// Helpers for writing to buffer
|
||||||
|
void PutByte (u8 uchValue); |
||||||
|
void PutWord (u16 usValue); |
||||||
|
void PutDWord (u32 nValue); |
||||||
|
void PutString (const char *pValue); |
||||||
|
void PutCompressedString (const u8 *pWritePtr); |
||||||
|
void PutDNSName (const char *pValue); |
||||||
|
void PutIPAddress (const CIPAddress &rValue); |
||||||
|
void ReserveDataLength (void); |
||||||
|
void SetDataLength (void); |
||||||
|
private: |
||||||
|
CNetSubSystem *m_pNet; |
||||||
|
CPtrList m_ServiceList; |
||||||
|
CMutex m_Mutex; |
||||||
|
CSocket *m_pSocket; |
||||||
|
boolean m_bRunning; |
||||||
|
CSynchronizationEvent m_Event; |
||||||
|
u8 m_Buffer[MaxMessageSize]; |
||||||
|
u8 *m_pWritePtr; |
||||||
|
u8 *m_pDataLen; |
||||||
|
}; |
||||||
|
#endif |
@ -0,0 +1,89 @@ |
|||||||
|
//
|
||||||
|
// udpmidi.cpp
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <circle/logger.h> |
||||||
|
#include <circle/net/in.h> |
||||||
|
#include <circle/net/netsubsystem.h> |
||||||
|
#include <circle/sched/scheduler.h> |
||||||
|
|
||||||
|
#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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
//
|
||||||
|
// udpmidi.h
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef _udpmidi_h |
||||||
|
#define _udpmidi_h |
||||||
|
|
||||||
|
#include <circle/net/ipaddress.h> |
||||||
|
#include <circle/net/socket.h> |
||||||
|
#include <circle/sched/task.h> |
||||||
|
|
||||||
|
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 |
@ -0,0 +1,193 @@ |
|||||||
|
|
||||||
|
//
|
||||||
|
// utility.h
|
||||||
|
//
|
||||||
|
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
|
||||||
|
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef _utility_h |
||||||
|
#define _utility_h |
||||||
|
|
||||||
|
#include <circle/string.h> |
||||||
|
#include <circle/util.h> |
||||||
|
|
||||||
|
// Macro to extract the string representation of an enum
|
||||||
|
#define CONFIG_ENUM_VALUE(VALUE, STRING) VALUE, |
||||||
|
|
||||||
|
// Macro to extract the enum value
|
||||||
|
#define CONFIG_ENUM_STRING(VALUE, STRING) #STRING, |
||||||
|
|
||||||
|
// Macro to declare the enum itself
|
||||||
|
#define CONFIG_ENUM(NAME, VALUES) enum class NAME { VALUES(CONFIG_ENUM_VALUE) } |
||||||
|
|
||||||
|
// Macro to declare an array of string representations for an enum
|
||||||
|
#define CONFIG_ENUM_STRINGS(NAME, DATA) static const char* NAME##Strings[] = { DATA(CONFIG_ENUM_STRING) } |
||||||
|
|
||||||
|
namespace Utility |
||||||
|
{ |
||||||
|
// Templated function for clamping a value between a minimum and a maximum
|
||||||
|
template <class T> |
||||||
|
constexpr T Clamp(const T& nValue, const T& nMin, const T& nMax) |
||||||
|
{ |
||||||
|
return (nValue < nMin) ? nMin : (nValue > nMax) ? nMax : nValue; |
||||||
|
} |
||||||
|
|
||||||
|
// Templated function for taking the minimum of two values
|
||||||
|
template <class T> |
||||||
|
constexpr T Min(const T& nLHS, const T& nRHS) |
||||||
|
{ |
||||||
|
return nLHS < nRHS ? nLHS : nRHS; |
||||||
|
} |
||||||
|
|
||||||
|
// Templated function for taking the maximum of two values
|
||||||
|
template <class T> |
||||||
|
constexpr T Max(const T& nLHS, const T& nRHS) |
||||||
|
{ |
||||||
|
return nLHS > nRHS ? nLHS : nRHS; |
||||||
|
} |
||||||
|
|
||||||
|
// Function for performing a linear interpolation of a value
|
||||||
|
constexpr float Lerp(float nValue, float nMinA, float nMaxA, float nMinB, float nMaxB) |
||||||
|
{ |
||||||
|
return nMinB + (nValue - nMinA) * ((nMaxB - nMinB) / (nMaxA - nMinA)); |
||||||
|
} |
||||||
|
|
||||||
|
// Return number of elements in an array
|
||||||
|
template <class T, size_t N> |
||||||
|
constexpr size_t ArraySize(const T(&)[N]) { return N; } |
||||||
|
|
||||||
|
// Returns whether some value is a power of 2
|
||||||
|
template <class T> |
||||||
|
constexpr bool IsPowerOfTwo(const T& nValue) |
||||||
|
{ |
||||||
|
return nValue && ((nValue & (nValue - 1)) == 0); |
||||||
|
} |
||||||
|
|
||||||
|
// Rounds a number to a nearest multiple; only works for integer values/multiples
|
||||||
|
template <class T> |
||||||
|
constexpr T RoundToNearestMultiple(const T& nValue, const T& nMultiple) |
||||||
|
{ |
||||||
|
return ((nValue + nMultiple / 2) / nMultiple) * nMultiple; |
||||||
|
} |
||||||
|
|
||||||
|
// Convert between milliseconds and ticks of a 1MHz clock
|
||||||
|
template <class T> |
||||||
|
constexpr T MillisToTicks(const T& nMillis) |
||||||
|
{ |
||||||
|
return nMillis * 1000; |
||||||
|
} |
||||||
|
|
||||||
|
template <class T> |
||||||
|
constexpr T TicksToMillis(const T& nTicks) |
||||||
|
{ |
||||||
|
return nTicks / 1000; |
||||||
|
} |
||||||
|
|
||||||
|
// Computes the Roland checksum
|
||||||
|
constexpr u8 RolandChecksum(const u8* pData, size_t nSize) |
||||||
|
{ |
||||||
|
u8 nSum = 0; |
||||||
|
for (size_t i = 0; i < nSize; ++i) |
||||||
|
nSum = (nSum + pData[i]) & 0x7F; |
||||||
|
|
||||||
|
return 128 - nSum; |
||||||
|
} |
||||||
|
|
||||||
|
// Comparators for sorting
|
||||||
|
namespace Comparator |
||||||
|
{ |
||||||
|
template<class T> |
||||||
|
using TComparator = bool (*)(const T&, const T&); |
||||||
|
|
||||||
|
template<class T> |
||||||
|
inline bool LessThan(const T& ObjectA, const T& ObjectB) |
||||||
|
{ |
||||||
|
return ObjectA < ObjectB; |
||||||
|
} |
||||||
|
|
||||||
|
template<class T> |
||||||
|
inline bool GreaterThan(const T& ObjectA, const T& ObjectB) |
||||||
|
{ |
||||||
|
return ObjectA > ObjectB; |
||||||
|
} |
||||||
|
|
||||||
|
inline bool CaseInsensitiveAscending(const CString& StringA, const CString& StringB) |
||||||
|
{ |
||||||
|
return strcasecmp(StringA, StringB) < 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Swaps two objects in-place
|
||||||
|
template<class T> |
||||||
|
inline void Swap(T& ObjectA, T& ObjectB) |
||||||
|
{ |
||||||
|
u8 Buffer[sizeof(T)]; |
||||||
|
memcpy(Buffer, &ObjectA, sizeof(T)); |
||||||
|
memcpy(&ObjectA, &ObjectB, sizeof(T)); |
||||||
|
memcpy(&ObjectB, Buffer, sizeof(T)); |
||||||
|
} |
||||||
|
|
||||||
|
namespace |
||||||
|
{ |
||||||
|
// Quicksort partition function (private)
|
||||||
|
template<class T> |
||||||
|
size_t Partition(T* Items, Comparator::TComparator<T> Comparator, size_t nLow, size_t nHigh) |
||||||
|
{ |
||||||
|
const size_t nPivotIndex = (nHigh + nLow) / 2; |
||||||
|
T* Pivot = &Items[nPivotIndex]; |
||||||
|
|
||||||
|
while (true) |
||||||
|
{ |
||||||
|
while (Comparator(Items[nLow], *Pivot)) |
||||||
|
++nLow; |
||||||
|
|
||||||
|
while (Comparator(*Pivot, Items[nHigh])) |
||||||
|
--nHigh; |
||||||
|
|
||||||
|
if (nLow >= nHigh) |
||||||
|
return nHigh; |
||||||
|
|
||||||
|
Swap(Items[nLow], Items[nHigh]); |
||||||
|
|
||||||
|
// Update pointer if pivot was swapped
|
||||||
|
if (nPivotIndex == nLow) |
||||||
|
Pivot = &Items[nHigh]; |
||||||
|
else if (nPivotIndex == nHigh) |
||||||
|
Pivot = &Items[nLow]; |
||||||
|
|
||||||
|
++nLow; |
||||||
|
--nHigh; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Sorts an array in-place using the Tony Hoare Quicksort algorithm
|
||||||
|
template <class T> |
||||||
|
void QSort(T* Items, Comparator::TComparator<T> Comparator, size_t nLow, size_t nHigh) |
||||||
|
{ |
||||||
|
if (nLow < nHigh) |
||||||
|
{ |
||||||
|
size_t p = Partition(Items, Comparator, nLow, nHigh); |
||||||
|
QSort(Items, Comparator, nLow, p); |
||||||
|
QSort(Items, Comparator, p + 1, nHigh); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,90 @@ |
|||||||
|
//
|
||||||
|
// udpmididevice.cpp
|
||||||
|
//
|
||||||
|
// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
|
||||||
|
// Copyright (C) 2022 The MiniDexed Team
|
||||||
|
//
|
||||||
|
// Original author of this class:
|
||||||
|
// R. Stange <rsta2@o2online.de>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <circle/logger.h> |
||||||
|
#include <cstring> |
||||||
|
#include "udpmididevice.h" |
||||||
|
#include <assert.h> |
||||||
|
|
||||||
|
#define VIRTUALCABLE 24 |
||||||
|
|
||||||
|
LOGMODULE("udpmididevice"); |
||||||
|
|
||||||
|
CUDPMIDIDevice::CUDPMIDIDevice (CMiniDexed *pSynthesizer, |
||||||
|
CConfig *pConfig, CUserInterface *pUI) |
||||||
|
: CMIDIDevice (pSynthesizer, pConfig, pUI), |
||||||
|
m_pSynthesizer (pSynthesizer), |
||||||
|
m_pConfig (pConfig) |
||||||
|
{ |
||||||
|
AddDevice ("udp"); |
||||||
|
} |
||||||
|
|
||||||
|
CUDPMIDIDevice::~CUDPMIDIDevice (void) |
||||||
|
{ |
||||||
|
//m_pSynthesizer = 0;
|
||||||
|
} |
||||||
|
|
||||||
|
boolean CUDPMIDIDevice::Initialize (void) |
||||||
|
{ |
||||||
|
m_pAppleMIDIParticipant = new CAppleMIDIParticipant(&m_Random, this); |
||||||
|
if (!m_pAppleMIDIParticipant->Initialize()) |
||||||
|
{ |
||||||
|
LOGERR("Failed to init RTP listener"); |
||||||
|
delete m_pAppleMIDIParticipant; |
||||||
|
m_pAppleMIDIParticipant = nullptr; |
||||||
|
} |
||||||
|
else |
||||||
|
LOGNOTE("RTP Listener initialized"); |
||||||
|
m_pUDPMIDIReceiver = new CUDPMIDIReceiver(this); |
||||||
|
if (!m_pUDPMIDIReceiver->Initialize()) |
||||||
|
{ |
||||||
|
LOGERR("Failed to init UDP MIDI receiver"); |
||||||
|
delete m_pUDPMIDIReceiver; |
||||||
|
m_pUDPMIDIReceiver = nullptr; |
||||||
|
} |
||||||
|
else |
||||||
|
LOGNOTE("UDP MIDI receiver initialized"); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// Methods to handle MIDI events
|
||||||
|
|
||||||
|
void CUDPMIDIDevice::OnAppleMIDIDataReceived(const u8* pData, size_t nSize) |
||||||
|
{ |
||||||
|
MIDIMessageHandler(pData, nSize, VIRTUALCABLE); |
||||||
|
} |
||||||
|
|
||||||
|
void CUDPMIDIDevice::OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) |
||||||
|
{ |
||||||
|
LOGNOTE("RTP Device connected"); |
||||||
|
} |
||||||
|
|
||||||
|
void CUDPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) |
||||||
|
{ |
||||||
|
LOGNOTE("RTP Device disconnected"); |
||||||
|
} |
||||||
|
|
||||||
|
void CUDPMIDIDevice::OnUDPMIDIDataReceived(const u8* pData, size_t nSize) |
||||||
|
{ |
||||||
|
MIDIMessageHandler(pData, nSize, VIRTUALCABLE); |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
//
|
||||||
|
// udpmididevice.h
|
||||||
|
//
|
||||||
|
// Virtual midi device for data recieved on network
|
||||||
|
//
|
||||||
|
// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
|
||||||
|
// Copyright (C) 2022 The MiniDexed Team
|
||||||
|
//
|
||||||
|
// Original author of this class:
|
||||||
|
// R. Stange <rsta2@o2online.de>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
#ifndef _udpmididevice_h |
||||||
|
#define _udpmididevice_h |
||||||
|
|
||||||
|
#include "mididevice.h" |
||||||
|
#include "config.h" |
||||||
|
#include "net/applemidi.h" |
||||||
|
#include "net/udpmidi.h" |
||||||
|
|
||||||
|
class CMiniDexed; |
||||||
|
|
||||||
|
class CUDPMIDIDevice : CAppleMIDIHandler, CUDPMIDIHandler, public CMIDIDevice |
||||||
|
{ |
||||||
|
public: |
||||||
|
CUDPMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI); |
||||||
|
~CUDPMIDIDevice (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; |
||||||
|
virtual void OnUDPMIDIDataReceived(const u8* pData, size_t nSize) override; |
||||||
|
|
||||||
|
private: |
||||||
|
CMiniDexed *m_pSynthesizer; |
||||||
|
CConfig *m_pConfig; |
||||||
|
CBcmRandomNumberGenerator m_Random; |
||||||
|
CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance
|
||||||
|
CUDPMIDIReceiver* m_pUDPMIDIReceiver; |
||||||
|
}; |
||||||
|
|
||||||
|
#endif |
@ -1,24 +1,27 @@ |
|||||||
#!/bin/bash |
#!/bin/bash |
||||||
set -ex |
set -ex |
||||||
# |
|
||||||
# Update top-level modules as a baseline |
# Update top-level modules as a baseline |
||||||
git submodule update --init --recursive |
git submodule update --init --recursive -f |
||||||
# |
|
||||||
# Use fixed master branch of circle-stdlib then re-update |
# Use fixed master branch of circle-stdlib then re-update |
||||||
cd circle-stdlib/ |
cd circle-stdlib/ |
||||||
git checkout 3bd135d |
git reset --hard |
||||||
git submodule update --init --recursive |
git checkout 1111eee -f # Matches Circle Step49 |
||||||
|
git submodule update --init --recursive -f |
||||||
cd - |
cd - |
||||||
# |
|
||||||
# Optional update submodules explicitly |
# Optional update submodules explicitly |
||||||
cd circle-stdlib/libs/circle |
#cd circle-stdlib/libs/circle |
||||||
git checkout tags/Step49 |
#git reset --hard |
||||||
cd - |
#git checkout tags/Step49 |
||||||
cd circle-stdlib/libs/circle-newlib |
#cd - |
||||||
|
#cd circle-stdlib/libs/circle-newlib |
||||||
#git checkout develop |
#git checkout develop |
||||||
cd - |
#cd - |
||||||
# |
|
||||||
# Use fixed master branch of Synth_Dexed |
# Use fixed master branch of Synth_Dexed |
||||||
cd Synth_Dexed/ |
cd Synth_Dexed/ |
||||||
git checkout c9f5274 |
git reset --hard |
||||||
|
git checkout 65d8383ad5 -f |
||||||
cd - |
cd - |
||||||
|
@ -0,0 +1,57 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Syslog server to receive and display syslog messages from MiniDexed. |
||||||
|
""" |
||||||
|
|
||||||
|
import socket |
||||||
|
import time |
||||||
|
import threading |
||||||
|
|
||||||
|
class SyslogServer: |
||||||
|
def __init__(self, host='0.0.0.0', port=8514): |
||||||
|
self.host = host |
||||||
|
self.port = port |
||||||
|
self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
||||||
|
self.server.bind((self.host, self.port)) |
||||||
|
self.start_time = None |
||||||
|
self.running = True |
||||||
|
|
||||||
|
def start(self): |
||||||
|
ip_address = socket.gethostbyname(socket.gethostname()) |
||||||
|
print(f"Syslog server listening on {ip_address}:{self.port}") |
||||||
|
input_thread = threading.Thread(target=self.wait_for_input) |
||||||
|
input_thread.daemon = True |
||||||
|
input_thread.start() |
||||||
|
while self.running: |
||||||
|
try: |
||||||
|
data, address = self.server.recvfrom(1024) |
||||||
|
self.handle_message(data) |
||||||
|
except KeyboardInterrupt: |
||||||
|
self.running = False |
||||||
|
|
||||||
|
def handle_message(self, data): |
||||||
|
message = data[2:].decode('utf-8').strip() |
||||||
|
|
||||||
|
if self.start_time is None: |
||||||
|
self.start_time = time.time() |
||||||
|
relative_time = "0:00:00.000" |
||||||
|
else: |
||||||
|
elapsed_time = time.time() - self.start_time |
||||||
|
hours = int(elapsed_time // 3600) |
||||||
|
minutes = int((elapsed_time % 3600) // 60) |
||||||
|
seconds = int(elapsed_time % 60) |
||||||
|
milliseconds = int((elapsed_time % 1) * 1000) |
||||||
|
relative_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}" |
||||||
|
|
||||||
|
print(f"{relative_time} {message}") |
||||||
|
|
||||||
|
def wait_for_input(self): |
||||||
|
input("Press any key to exit...") |
||||||
|
self.running = False |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
server = SyslogServer() |
||||||
|
server.start() |
||||||
|
print("Syslog server stopped.") |
@ -0,0 +1,280 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
# Updater for MiniDexed |
||||||
|
|
||||||
|
import os |
||||||
|
import sys |
||||||
|
import tempfile |
||||||
|
import zipfile |
||||||
|
import requests |
||||||
|
import ftplib |
||||||
|
import socket |
||||||
|
import atexit |
||||||
|
import re |
||||||
|
import argparse |
||||||
|
|
||||||
|
try: |
||||||
|
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf |
||||||
|
except ImportError: |
||||||
|
print("Please install the zeroconf library to use mDNS functionality.") |
||||||
|
print("You can install it using: pip install zeroconf") |
||||||
|
sys.exit(1) |
||||||
|
|
||||||
|
class MyListener(ServiceListener): |
||||||
|
def __init__(self, ip_list, name_list): |
||||||
|
self.ip_list = ip_list |
||||||
|
self.name_list = name_list |
||||||
|
|
||||||
|
def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: |
||||||
|
print(f"Service {name} updated") |
||||||
|
|
||||||
|
def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: |
||||||
|
print(f"Service {name} removed") |
||||||
|
|
||||||
|
def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: |
||||||
|
info = zc.get_service_info(type_, name) |
||||||
|
print(f"Service {name} added, service info: {info}") |
||||||
|
if info and info.addresses: |
||||||
|
ip = socket.inet_ntoa(info.addresses[0]) |
||||||
|
if ip not in self.ip_list: |
||||||
|
self.ip_list.append(ip) |
||||||
|
self.name_list.append(info.server.rstrip('.')) |
||||||
|
|
||||||
|
|
||||||
|
# Constants |
||||||
|
TEMP_DIR = tempfile.gettempdir() |
||||||
|
|
||||||
|
# Register cleanup function for temp files |
||||||
|
zip_path = None |
||||||
|
extract_path = None |
||||||
|
def cleanup_temp_files(): |
||||||
|
if zip_path and os.path.exists(zip_path): |
||||||
|
os.remove(zip_path) |
||||||
|
if extract_path and os.path.exists(extract_path): |
||||||
|
for root, dirs, files in os.walk(extract_path, topdown=False): |
||||||
|
for name in files: |
||||||
|
os.remove(os.path.join(root, name)) |
||||||
|
for name in dirs: |
||||||
|
os.rmdir(os.path.join(root, name)) |
||||||
|
os.rmdir(extract_path) |
||||||
|
print("Cleaned up temporary files.") |
||||||
|
atexit.register(cleanup_temp_files) |
||||||
|
|
||||||
|
# Function to download the latest release |
||||||
|
def download_latest_release(url): |
||||||
|
response = requests.get(url, stream=True) |
||||||
|
if response.status_code == 200: |
||||||
|
zip_path = os.path.join(TEMP_DIR, "MiniDexed_latest.zip") |
||||||
|
with open(zip_path, 'wb') as f: |
||||||
|
for chunk in response.iter_content(chunk_size=8192): |
||||||
|
f.write(chunk) |
||||||
|
return zip_path |
||||||
|
return None |
||||||
|
|
||||||
|
# Function to extract the downloaded zip file |
||||||
|
def extract_zip(zip_path): |
||||||
|
extract_path = os.path.join(TEMP_DIR, "MiniDexed") |
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref: |
||||||
|
zip_ref.extractall(extract_path) |
||||||
|
return extract_path |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
parser = argparse.ArgumentParser(description="MiniDexed Updater") |
||||||
|
parser.add_argument("-v", action="store_true", help="Enable verbose FTP debug output") |
||||||
|
args = parser.parse_args() |
||||||
|
|
||||||
|
import time |
||||||
|
# Ask user which release to download (numbered choices) |
||||||
|
release_options = [ |
||||||
|
("Latest official release", "https://github.com/probonopd/MiniDexed/releases/expanded_assets/latest"), |
||||||
|
("Continuous (experimental) build", "https://github.com/probonopd/MiniDexed/releases/expanded_assets/continuous") |
||||||
|
] |
||||||
|
print("Which release do you want to download?") |
||||||
|
for idx, (desc, _) in enumerate(release_options): |
||||||
|
print(f" [{idx+1}] {desc}") |
||||||
|
while True: |
||||||
|
choice = input(f"Enter the number of your choice (1-{len(release_options)}): ").strip() |
||||||
|
if choice.isdigit() and 1 <= int(choice) <= len(release_options): |
||||||
|
github_url = release_options[int(choice)-1][1] |
||||||
|
break |
||||||
|
print("Invalid selection. Please enter a valid number.") |
||||||
|
|
||||||
|
# Using mDNS to find the IP address of the device(s) that advertise the FTP service "_ftp._tcp." |
||||||
|
ip_addresses = [] |
||||||
|
device_names = [] |
||||||
|
zeroconf = Zeroconf() |
||||||
|
listener = MyListener(ip_addresses, device_names) |
||||||
|
browser = ServiceBrowser(zeroconf, "_ftp._tcp.local.", listener) |
||||||
|
try: |
||||||
|
print("Searching for devices...") |
||||||
|
time.sleep(5) |
||||||
|
if ip_addresses: |
||||||
|
print("Devices found:") |
||||||
|
for idx, (name, ip) in enumerate(zip(device_names, ip_addresses)): |
||||||
|
print(f" [{idx+1}] {name} ({ip})") |
||||||
|
while True: |
||||||
|
selection = input(f"Enter the number of the device to upload to (1-{len(ip_addresses)}): ").strip() |
||||||
|
if selection.isdigit() and 1 <= int(selection) <= len(ip_addresses): |
||||||
|
selected_ip = ip_addresses[int(selection)-1] |
||||||
|
selected_name = device_names[int(selection)-1] |
||||||
|
break |
||||||
|
print("Invalid selection. Please enter a valid number.") |
||||||
|
else: |
||||||
|
print("No devices found.") |
||||||
|
sys.exit(1) |
||||||
|
finally: |
||||||
|
zeroconf.close() |
||||||
|
print("Devices found:", list(zip(device_names, ip_addresses))) |
||||||
|
|
||||||
|
# Use the selected GitHub URL for release |
||||||
|
def get_release_url(github_url): |
||||||
|
print(f"Fetching release page: {github_url}") |
||||||
|
response = requests.get(github_url) |
||||||
|
print(f"HTTP status code: {response.status_code}") |
||||||
|
if response.status_code == 200: |
||||||
|
print("Successfully fetched release page. Scanning for MiniDexed*.zip links...") |
||||||
|
# Find all <a ... href="..."> tags with a <span class="Truncate-text text-bold">MiniDexed*.zip</span> |
||||||
|
pattern = re.compile(r'<a[^>]+href=["\']([^"\']+\.zip)["\'][^>]*>\s*<span[^>]*class=["\']Truncate-text text-bold["\'][^>]*>(MiniDexed[^<]*?\.zip)</span>', re.IGNORECASE) |
||||||
|
matches = pattern.findall(response.text) |
||||||
|
print(f"Found {len(matches)} candidate .zip links.") |
||||||
|
for href, filename in matches: |
||||||
|
print(f"Examining link: href={href}, filename={filename}") |
||||||
|
if filename.startswith("MiniDexed") and filename.endswith(".zip"): |
||||||
|
if href.startswith('http'): |
||||||
|
print(f"Selected direct link: {href}") |
||||||
|
return href |
||||||
|
else: |
||||||
|
full_url = f"https://github.com{href}" |
||||||
|
print(f"Selected relative link, full URL: {full_url}") |
||||||
|
return full_url |
||||||
|
print("No valid MiniDexed*.zip link found.") |
||||||
|
else: |
||||||
|
print(f"Failed to fetch release page. Status code: {response.status_code}") |
||||||
|
return None |
||||||
|
|
||||||
|
latest_release_url = get_release_url(github_url) |
||||||
|
if latest_release_url: |
||||||
|
print(f"Release URL: {latest_release_url}") |
||||||
|
zip_path = download_latest_release(latest_release_url) |
||||||
|
if zip_path: |
||||||
|
print(f"Downloaded to: {zip_path}") |
||||||
|
extract_path = extract_zip(zip_path) |
||||||
|
print(f"Extracted to: {extract_path}") |
||||||
|
else: |
||||||
|
print("Failed to download the release.") |
||||||
|
sys.exit(1) |
||||||
|
else: |
||||||
|
print("Failed to get the release URL.") |
||||||
|
sys.exit(1) |
||||||
|
|
||||||
|
# Ask user if they want to update Performances (default no) |
||||||
|
update_perf = input("Do you want to update the Performances? This will OVERWRITE all existing performances. [y/N]: ").strip().lower() |
||||||
|
update_performances = update_perf == 'y' |
||||||
|
|
||||||
|
# Log into the selected device and upload the new version of MiniDexed |
||||||
|
print(f"Connecting to {selected_name} ({selected_ip})...") |
||||||
|
try: |
||||||
|
ftp = ftplib.FTP() |
||||||
|
if args.v: |
||||||
|
ftp.set_debuglevel(2) |
||||||
|
ftp.connect(selected_ip, 21, timeout=10) |
||||||
|
ftp.login("admin", "admin") |
||||||
|
ftp.set_pasv(True) |
||||||
|
print(f"Connected to {selected_ip} (passive mode).") |
||||||
|
# --- Performances update logic --- |
||||||
|
if update_performances: |
||||||
|
print("Updating Performance: recursively deleting and uploading /SD/performance directory...") |
||||||
|
def ftp_rmdirs(ftp, path): |
||||||
|
try: |
||||||
|
items = ftp.nlst(path) |
||||||
|
except Exception as e: |
||||||
|
print(f"[WARN] Could not list {path}: {e}") |
||||||
|
return |
||||||
|
for item in items: |
||||||
|
if item in ['.', '..', path]: |
||||||
|
continue |
||||||
|
full_path = f"{path}/{item}" if not item.startswith(path) else item |
||||||
|
try: |
||||||
|
# Try to delete as a file first |
||||||
|
ftp.delete(full_path) |
||||||
|
print(f"Deleted file: {full_path}") |
||||||
|
except Exception: |
||||||
|
# If not a file, try as a directory |
||||||
|
try: |
||||||
|
ftp_rmdirs(ftp, full_path) |
||||||
|
ftp.rmd(full_path) |
||||||
|
print(f"Deleted directory: {full_path}") |
||||||
|
except Exception as e: |
||||||
|
print(f"[WARN] Could not delete {full_path}: {e}") |
||||||
|
try: |
||||||
|
ftp_rmdirs(ftp, '/SD/performance') |
||||||
|
try: |
||||||
|
ftp.rmd('/SD/performance') |
||||||
|
print("Deleted /SD/performance on device.") |
||||||
|
except Exception as e: |
||||||
|
print(f"[WARN] Could not delete /SD/performance directory itself: {e}") |
||||||
|
except Exception as e: |
||||||
|
print(f"Warning: Could not delete /SD/performance: {e}") |
||||||
|
# Upload extracted performance/ recursively |
||||||
|
local_perf = os.path.join(extract_path, 'performance') |
||||||
|
def ftp_mkdirs(ftp, path): |
||||||
|
try: |
||||||
|
ftp.mkd(path) |
||||||
|
except Exception: |
||||||
|
pass |
||||||
|
def ftp_upload_dir(ftp, local_dir, remote_dir): |
||||||
|
ftp_mkdirs(ftp, remote_dir) |
||||||
|
for item in os.listdir(local_dir): |
||||||
|
lpath = os.path.join(local_dir, item) |
||||||
|
rpath = f"{remote_dir}/{item}" |
||||||
|
if os.path.isdir(lpath): |
||||||
|
ftp_upload_dir(ftp, lpath, rpath) |
||||||
|
else: |
||||||
|
with open(lpath, 'rb') as fobj: |
||||||
|
ftp.storbinary(f'STOR {rpath}', fobj) |
||||||
|
print(f"Uploaded {rpath}") |
||||||
|
if os.path.isdir(local_perf): |
||||||
|
ftp_upload_dir(ftp, local_perf, '/SD/performance') |
||||||
|
print("Uploaded new /SD/performance directory.") |
||||||
|
else: |
||||||
|
print("No extracted performance/ directory found, skipping upload.") |
||||||
|
# Upload performance.ini if it exists in extract_path |
||||||
|
local_perfini = os.path.join(extract_path, 'performance.ini') |
||||||
|
if os.path.isfile(local_perfini): |
||||||
|
with open(local_perfini, 'rb') as fobj: |
||||||
|
ftp.storbinary('STOR /SD/performance.ini', fobj) |
||||||
|
print("Uploaded /SD/performance.ini.") |
||||||
|
else: |
||||||
|
print("No extracted performance.ini found, skipping upload.") |
||||||
|
# Upload kernel files |
||||||
|
for root, dirs, files in os.walk(extract_path): |
||||||
|
for file in files: |
||||||
|
if file.startswith("kernel") and file.endswith(".img"): |
||||||
|
local_path = os.path.join(root, file) |
||||||
|
remote_path = f"/SD/{file}" |
||||||
|
# Check if file exists on FTP server |
||||||
|
file_exists = False |
||||||
|
try: |
||||||
|
ftp.cwd("/SD") |
||||||
|
if file in ftp.nlst(): |
||||||
|
file_exists = True |
||||||
|
except Exception as e: |
||||||
|
print(f"Error checking for {file} on FTP server: {e}") |
||||||
|
file_exists = False |
||||||
|
if not file_exists: |
||||||
|
print(f"Skipping {file}: does not exist on device.") |
||||||
|
continue |
||||||
|
filesize = os.path.getsize(local_path) |
||||||
|
uploaded = [0] |
||||||
|
def progress_callback(data): |
||||||
|
uploaded[0] += len(data) |
||||||
|
percent = uploaded[0] * 100 // filesize |
||||||
|
print(f"\rUploading {file}: {percent}%", end="", flush=True) |
||||||
|
with open(local_path, 'rb') as f: |
||||||
|
ftp.storbinary(f'STOR {remote_path}', f, 8192, callback=progress_callback) |
||||||
|
print(f"\nUploaded {file} to {selected_ip}.") |
||||||
|
ftp.sendcmd("BYE") |
||||||
|
print(f"Disconnected from {selected_ip}.") |
||||||
|
except ftplib.all_errors as e: |
||||||
|
print(f"FTP error: {e}") |
Loading…
Reference in new issue