From bd8926a5f3b8a776ebce453477b49535525e316e Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 29 Apr 2025 23:43:33 +0200 Subject: [PATCH 1/3] SysEx voice dump request sends something back over rtpMIDI --- src/mididevice.cpp | 44 +++++++++++++++++++++++++++--------------- src/mididevice.h | 4 +++- src/minidexed.cpp | 2 +- src/net/applemidi.cpp | 45 +++++++++++++++++++++++++++++++++++++++++++ src/net/applemidi.h | 3 +++ src/udpmididevice.cpp | 26 +++++++++++++++++++++++++ src/udpmididevice.h | 7 +++++++ 7 files changed, 113 insertions(+), 18 deletions(-) diff --git a/src/mididevice.cpp b/src/mididevice.cpp index 576821f..cdb2f3e 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -330,6 +330,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign { LOGNOTE("MIDI-SYSEX: channel: %u, len: %u, TG: %u",m_ChannelMap[nTG],nLength,nTG); HandleSystemExclusive(pMessage, nLength, nCable, nTG); + break; // Only the first TG listening to the MIDI channel can handle the SysEx message (e.g., voice dump); may need to restrict this to dump requests only } } else @@ -610,6 +611,23 @@ bool CMIDIDevice::HandleMIDISystemCC(const u8 ucCC, const u8 ucCCval) void CMIDIDevice::HandleSystemExclusive(const uint8_t* pMessage, const size_t nLength, const unsigned nCable, const uint8_t nTG) { + + // Check if it is a dump request; these have the format F0 43 2n ff F7 + // with n = the MIDI channel and ff = 00 for voice or 09 for bank + // It was confirmed that on the TX816, the device number is interpreted as the MIDI channel; + if (nLength == 5 && pMessage[3] == 0x00) + { + LOGDBG("SysEx voice dump request: device %d", nTG); + SendSystemExclusiveVoice(nTG, m_DeviceName, nCable, nTG); + return; + } + else if (nLength == 5 && pMessage[3] == 0x09) + { + LOGDBG("SysEx bank dump request: device %d", nTG); + LOGDBG("Still to be implemented"); + return; + } + int16_t sysex_return; sysex_return = m_pSynthesizer->checkSystemExclusive(pMessage, nLength, nTG); @@ -735,25 +753,19 @@ void CMIDIDevice::HandleSystemExclusive(const uint8_t* pMessage, const size_t nL else if(sysex_return >= 500 && sysex_return < 600) { LOGDBG("SysEx send voice %u request",sysex_return-500); - SendSystemExclusiveVoice(sysex_return-500, nCable, nTG); + SendSystemExclusiveVoice(sysex_return-500, m_DeviceName, nCable, nTG); } break; } } -void CMIDIDevice::SendSystemExclusiveVoice(uint8_t nVoice, const unsigned nCable, uint8_t nTG) +void CMIDIDevice::SendSystemExclusiveVoice(uint8_t nVoice, const std::string& deviceName, unsigned nCable, uint8_t nTG) { - uint8_t voicedump[163]; - - // Get voice sysex dump from TG - m_pSynthesizer->getSysExVoiceDump(voicedump, nTG); - - TDeviceMap::const_iterator Iterator; - - // send voice dump to all MIDI interfaces - for(Iterator = s_DeviceMap.begin(); Iterator != s_DeviceMap.end(); ++Iterator) - { - Iterator->second->Send (voicedump, sizeof(voicedump)*sizeof(uint8_t)); - // LOGDBG("Send SYSEX voice dump %u to \"%s\"",nVoice,Iterator->first.c_str()); - } -} + uint8_t voicedump[163]; + m_pSynthesizer->getSysExVoiceDump(voicedump, nTG); + TDeviceMap::const_iterator Iterator = s_DeviceMap.find(deviceName); + if (Iterator != s_DeviceMap.end()) { + Iterator->second->Send(voicedump, sizeof(voicedump), nCable); + LOGDBG("Send SYSEX voice dump %u to \"%s\"", nVoice, deviceName.c_str()); + } +} diff --git a/src/mididevice.h b/src/mididevice.h index 94b789b..7da8717 100644 --- a/src/mididevice.h +++ b/src/mididevice.h @@ -54,7 +54,9 @@ public: u8 GetChannel (unsigned nTG) const; virtual void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) {} - virtual void SendSystemExclusiveVoice(uint8_t nVoice, const unsigned nCable, uint8_t nTG); + // Change signature to specify device name + void SendSystemExclusiveVoice(uint8_t nVoice, const std::string& deviceName, unsigned nCable, uint8_t nTG); + const std::string& GetDeviceName() const { return m_DeviceName; } protected: void MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsigned nCable = 0); diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 1e925ed..f7c341d 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -666,7 +666,7 @@ void CMiniDexed::ProgramChange (unsigned nProgram, unsigned nTG) // MIDI channel configured for this TG if (m_nMIDIChannel[nTG] < CMIDIDevice::Channels) { - m_SerialMIDI.SendSystemExclusiveVoice(nProgram,0,nTG); + m_SerialMIDI.SendSystemExclusiveVoice(nProgram, m_SerialMIDI.GetDeviceName(), 0, nTG); } } diff --git a/src/net/applemidi.cpp b/src/net/applemidi.cpp index e14b216..8440426 100644 --- a/src/net/applemidi.cpp +++ b/src/net/applemidi.cpp @@ -31,6 +31,9 @@ #include "applemidi.h" #include "byteorder.h" +#define MAX_DX7_SYSEX_LENGTH 4104 +#define MAX_MIDI_MESSAGE MAX_DX7_SYSEX_LENGTH + // #define APPLEMIDI_DEBUG LOGMODULE("applemidi"); @@ -871,4 +874,46 @@ bool CAppleMIDIParticipant::SendFeedbackPacket() #endif return SendPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort, &FeedbackPacket, sizeof(FeedbackPacket)); +} + +bool CAppleMIDIParticipant::SendMIDIToHost(const u8* pData, size_t nSize) +{ + if (m_State != TState::Connected) + return false; + + // Build RTP-MIDI packet + TRTPMIDI packet; + packet.nFlags = htons((RTPMIDIVersion << 14) | RTPMIDIPayloadType); + packet.nSequence = htons(++m_nSequence); + packet.nTimestamp = htonl(0); // No timestamping for now + packet.nSSRC = htonl(m_nSSRC); + + // RTP-MIDI command section: header + MIDI data + // Header: 0x80 | length (if length < 0x0F) + u8 midiHeader = 0x00; + size_t midiLen = nSize; + if (midiLen < 0x0F) { + midiHeader = midiLen & 0x0F; + } else { + midiHeader = 0x80 | ((midiLen >> 8) & 0x0F); + } + + u8 buffer[sizeof(TRTPMIDI) + 2 + MAX_MIDI_MESSAGE]; + size_t offset = 0; + memcpy(buffer + offset, &packet, sizeof(TRTPMIDI)); + offset += sizeof(TRTPMIDI); + buffer[offset++] = midiHeader; + if (midiLen >= 0x0F) { + buffer[offset++] = midiLen & 0xFF; + } + memcpy(buffer + offset, pData, midiLen); + offset += midiLen; + + if (SendPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort, buffer, offset) <= 0) { + LOGNOTE("Failed to send MIDI data to host"); + return false; + } + + LOGDBG("Successfully sent %zu bytes of MIDI data", nSize); + return true; } \ No newline at end of file diff --git a/src/net/applemidi.h b/src/net/applemidi.h index 3df68ae..bc8d2b8 100644 --- a/src/net/applemidi.h +++ b/src/net/applemidi.h @@ -46,6 +46,9 @@ public: virtual void Run() override; +public: + bool SendMIDIToHost(const u8* pData, size_t nSize); + private: void ControlInvitationState(); void MIDIInvitationState(); diff --git a/src/udpmididevice.cpp b/src/udpmididevice.cpp index 4b0d1c7..4c46649 100644 --- a/src/udpmididevice.cpp +++ b/src/udpmididevice.cpp @@ -25,6 +25,8 @@ #include #include "udpmididevice.h" #include +#include +#include #define VIRTUALCABLE 24 @@ -64,6 +66,13 @@ boolean CUDPMIDIDevice::Initialize (void) } else LOGNOTE("UDP MIDI receiver initialized"); + + // UDP MIDI send socket setup (default: broadcast 255.255.255.255:1999) + CNetSubSystem* pNet = CNetSubSystem::Get(); + m_pUDPSendSocket = new CSocket(pNet, IPPROTO_UDP); + m_UDPDestAddress.Set(0xFFFFFFFF); // Broadcast by default + m_UDPDestPort = 1999; + return true; } @@ -87,4 +96,21 @@ void CUDPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const c void CUDPMIDIDevice::OnUDPMIDIDataReceived(const u8* pData, size_t nSize) { MIDIMessageHandler(pData, nSize, VIRTUALCABLE); +} + +void CUDPMIDIDevice::Send(const u8 *pMessage, size_t nLength, unsigned nCable) +{ + bool sentRTP = false; + if (m_pAppleMIDIParticipant && m_pAppleMIDIParticipant->SendMIDIToHost(pMessage, nLength)) { + sentRTP = true; + LOGNOTE("Sent %zu bytes to RTP-MIDI host", nLength); + } + if (!sentRTP && m_pUDPSendSocket) { + int res = m_pUDPSendSocket->SendTo(pMessage, nLength, 0, m_UDPDestAddress, m_UDPDestPort); + if (res < 0) { + LOGERR("Failed to send %zu bytes to UDP MIDI host", nLength); + } else { + LOGNOTE("Sent %zu bytes to UDP MIDI host (broadcast)", nLength); + } + } } \ No newline at end of file diff --git a/src/udpmididevice.h b/src/udpmididevice.h index de50172..9895c9d 100644 --- a/src/udpmididevice.h +++ b/src/udpmididevice.h @@ -29,6 +29,7 @@ #include "config.h" #include "net/applemidi.h" #include "net/udpmidi.h" +#include "midi.h" class CMiniDexed; @@ -43,6 +44,7 @@ public: 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; + virtual void Send(const u8 *pMessage, size_t nLength, unsigned nCable = 0) override; private: CMiniDexed *m_pSynthesizer; @@ -50,6 +52,11 @@ private: CBcmRandomNumberGenerator m_Random; CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance CUDPMIDIReceiver* m_pUDPMIDIReceiver; + CSocket* m_pUDPSendSocket = nullptr; + CIPAddress m_UDPDestAddress; + unsigned m_UDPDestPort = 1999; + CIPAddress m_LastUDPSenderAddress; + unsigned m_LastUDPSenderPort = 0; }; #endif From 4ffed80497d634381ded553058f2a2f41c19d283 Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 1 May 2025 09:10:21 +0200 Subject: [PATCH 2/3] Add a warning log when no matching device is found --- src/mididevice.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mididevice.cpp b/src/mididevice.cpp index cdb2f3e..ce7e920 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -767,5 +767,7 @@ void CMIDIDevice::SendSystemExclusiveVoice(uint8_t nVoice, const std::string& de if (Iterator != s_DeviceMap.end()) { Iterator->second->Send(voicedump, sizeof(voicedump), nCable); LOGDBG("Send SYSEX voice dump %u to \"%s\"", nVoice, deviceName.c_str()); + } else { + LOGWARN("No device found in s_DeviceMap for name: %s", deviceName.c_str()); } } From 32b3b57ca1a12acc49f91bb872b6727b79714f6e Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 1 May 2025 09:23:25 +0200 Subject: [PATCH 3/3] Limit to first matching TG only for dump requests Send dump request only to the first TG that matches the MIDI channel requested via the SysEx message device ID but send other SysEx to all, not only the first, matching TGs --- src/mididevice.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/mididevice.cpp b/src/mididevice.cpp index ce7e920..f20cefc 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -320,24 +320,21 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign // Process MIDI for each active Tone Generator bool bSystemCCHandled = false; bool bSystemCCChecked = false; - for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators() && !bSystemCCHandled; nTG++) - { - if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN) - { - // MIDI SYSEX per MIDI channel - uint8_t ucSysExChannel = (pMessage[2] & 0x0F); - if (m_ChannelMap[nTG] == ucSysExChannel || m_ChannelMap[nTG] == OmniMode) - { + if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN) { + uint8_t ucSysExChannel = (pMessage[2] & 0x0F); + for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators(); nTG++) { + if (m_ChannelMap[nTG] == ucSysExChannel || m_ChannelMap[nTG] == OmniMode) { LOGNOTE("MIDI-SYSEX: channel: %u, len: %u, TG: %u",m_ChannelMap[nTG],nLength,nTG); HandleSystemExclusive(pMessage, nLength, nCable, nTG); - break; // Only the first TG listening to the MIDI channel can handle the SysEx message (e.g., voice dump); may need to restrict this to dump requests only + if (nLength == 5) { + break; // Send dump request only to the first TG that matches the MIDI channel requested via the SysEx message device ID + } } } - else - { + } else { + for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators() && !bSystemCCHandled; nTG++) { if ( m_ChannelMap[nTG] == ucChannel - || m_ChannelMap[nTG] == OmniMode) - { + || m_ChannelMap[nTG] == OmniMode) { switch (ucType) { case MIDI_NOTE_ON: @@ -761,6 +758,7 @@ void CMIDIDevice::HandleSystemExclusive(const uint8_t* pMessage, const size_t nL void CMIDIDevice::SendSystemExclusiveVoice(uint8_t nVoice, const std::string& deviceName, unsigned nCable, uint8_t nTG) { + // Example: F0 43 20 00 F7 uint8_t voicedump[163]; m_pSynthesizer->getSysExVoiceDump(voicedump, nTG); TDeviceMap::const_iterator Iterator = s_DeviceMap.find(deviceName);