diff --git a/src/Makefile b/src/Makefile
index 73dbddc..86105c0 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -11,7 +11,8 @@ OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \
sysexfileloader.o performanceconfig.o perftimer.o \
effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \
arm_float_to_q23.o \
- net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o net/mdnspublisher.o udpmididevice.o
+ net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o net/mdnspublisher.o udpmididevice.o \
+ midichunker.o
OPTIMIZE = -O3
diff --git a/src/midichunker.cpp b/src/midichunker.cpp
new file mode 100644
index 0000000..b069aae
--- /dev/null
+++ b/src/midichunker.cpp
@@ -0,0 +1,48 @@
+//
+// midichunker.cpp
+//
+// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
+// Copyright (C) 2022-25 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 .
+//
+
+#include "midichunker.h"
+#include
+
+MIDISysExChunker::MIDISysExChunker(const uint8_t* data, size_t length, size_t chunkSize)
+ : m_data(data), m_length(length), m_chunkSize(chunkSize), m_offset(0) {}
+
+bool MIDISysExChunker::hasNext() const {
+ return m_offset < m_length;
+}
+
+std::vector MIDISysExChunker::next() {
+ if (!hasNext()) return {};
+ size_t remaining = m_length - m_offset;
+ size_t chunkLen = std::min(m_chunkSize, remaining);
+ // Only the last chunk should contain the final 0xF7
+ if (m_offset + chunkLen >= m_length && m_data[m_length-1] == 0xF7) {
+ chunkLen = m_length - m_offset;
+ } else if (m_offset + chunkLen > 0 && m_data[m_offset + chunkLen - 1] == 0xF7) {
+ chunkLen--;
+ }
+ std::vector chunk(m_data + m_offset, m_data + m_offset + chunkLen);
+ m_offset += chunkLen;
+ return chunk;
+}
+
+void MIDISysExChunker::reset() {
+ m_offset = 0;
+}
diff --git a/src/midichunker.h b/src/midichunker.h
new file mode 100644
index 0000000..06f49ac
--- /dev/null
+++ b/src/midichunker.h
@@ -0,0 +1,37 @@
+//
+// midichunker.h
+//
+// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
+// Copyright (C) 2022-25 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 .
+//
+
+#pragma once
+#include
+#include
+#include
+
+class MIDISysExChunker {
+public:
+ MIDISysExChunker(const uint8_t* data, size_t length, size_t chunkSize = 256);
+ bool hasNext() const;
+ std::vector next();
+ void reset();
+private:
+ const uint8_t* m_data;
+ size_t m_length;
+ size_t m_chunkSize;
+ size_t m_offset;
+};
diff --git a/src/mididevice.cpp b/src/mididevice.cpp
index 576821f..4039afd 100644
--- a/src/mididevice.cpp
+++ b/src/mididevice.cpp
@@ -29,8 +29,9 @@
#include
#include "midi.h"
#include "userinterface.h"
+#include "midichunker.h"
-LOGMODULE ("mididevice");
+LOGMODULE("midikeyboard");
// MIDI "System" level (i.e. all TG) custom CC maps
// Note: Even if number of TGs is not 8, there are only 8
@@ -249,8 +250,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
m_pSynthesizer->setMasterVolume(fMasterVolume);
}
else
- {
- // Perform any MiniDexed level MIDI handling before specific Tone Generators
+ { // Perform any MiniDexed level MIDI handling before specific Tone Generators
unsigned nPerfCh = m_pSynthesizer->GetPerformanceSelectChannel();
switch (ucType)
{
@@ -320,23 +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);
+ 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:
@@ -610,150 +608,194 @@ 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)
{
- int16_t sysex_return;
-
- sysex_return = m_pSynthesizer->checkSystemExclusive(pMessage, nLength, nTG);
- LOGDBG("SYSEX handler return value: %d", sysex_return);
-
- switch (sysex_return)
- {
- case -1:
- LOGERR("SysEx end status byte not detected.");
- break;
- case -2:
- LOGERR("SysEx vendor not Yamaha.");
- break;
- case -3:
- LOGERR("Unknown SysEx parameter change.");
- break;
- case -4:
- LOGERR("Unknown SysEx voice or function.");
- break;
- case -5:
- LOGERR("Not a SysEx voice bulk upload.");
- break;
- case -6:
- LOGERR("Wrong length for SysEx voice bulk upload (not 155).");
- break;
- case -7:
- LOGERR("Checksum error for one voice.");
- break;
- case -8:
- LOGERR("Not a SysEx bank bulk upload.");
- break;
- case -9:
- LOGERR("Wrong length for SysEx bank bulk upload (not 4096).");
- case -10:
- LOGERR("Checksum error for bank.");
- break;
- case -11:
- LOGERR("Unknown SysEx message.");
- break;
- case 64:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setMonoMode(pMessage[5],nTG);
- break;
- case 65:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setPitchbendRange(pMessage[5],nTG);
- break;
- case 66:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setPitchbendStep(pMessage[5],nTG);
- break;
- case 67:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setPortamentoMode(pMessage[5],nTG);
- break;
- case 68:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setPortamentoGlissando(pMessage[5],nTG);
- break;
- case 69:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setPortamentoTime(pMessage[5],nTG);
- break;
- case 70:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setModWheelRange(pMessage[5],nTG);
- break;
- case 71:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setModWheelTarget(pMessage[5],nTG);
- break;
- case 72:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setFootControllerRange(pMessage[5],nTG);
- break;
- case 73:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setFootControllerTarget(pMessage[5],nTG);
- break;
- case 74:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setBreathControllerRange(pMessage[5],nTG);
- break;
- case 75:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setBreathControllerTarget(pMessage[5],nTG);
- break;
- case 76:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setAftertouchRange(pMessage[5],nTG);
- break;
- case 77:
- LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
- m_pSynthesizer->setAftertouchTarget(pMessage[5],nTG);
- break;
- case 100:
- // load sysex-data into voice memory
- LOGDBG("One Voice bulk upload");
- m_pSynthesizer->loadVoiceParameters(pMessage,nTG);
- break;
- case 200:
- LOGDBG("Bank bulk upload.");
- //TODO: add code for storing a bank bulk upload
- LOGNOTE("Currently code for storing a bulk bank upload is missing!");
- break;
- case 455:
- // Parameter 155 + 300 added by Synth_Dexed = 455
- LOGDBG("Operators enabled: %d%d%d%d%d%d", (pMessage[5] & 0x20) ? 1 : 0, (pMessage[5] & 0x10) ? 1 : 0, (pMessage[5] & 0x08) ? 1 : 0, (pMessage[5] & 0x04) ? 1 : 0, (pMessage[5] & 0x02) ? 1 : 0, (pMessage[5] & 0x01) ? 1 : 0);
- m_pSynthesizer->setOPMask(pMessage[5], nTG);
- break;
- default:
- if(sysex_return >= 300 && sysex_return < 500)
- {
- LOGDBG("SysEx voice parameter change: Parameter %d value: %d",pMessage[4] + ((pMessage[3] & 0x03) * 128), pMessage[5]);
- m_pSynthesizer->setVoiceDataElement(pMessage[4] + ((pMessage[3] & 0x03) * 128), pMessage[5],nTG);
- switch(pMessage[4] + ((pMessage[3] & 0x03) * 128))
- {
- case 134:
- m_pSynthesizer->notesOff(0,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);
+ SendSystemExclusiveBank(nTG, m_DeviceName, nCable, nTG);
+ return;
+ }
+
+ int16_t sysex_return;
+
+ sysex_return = m_pSynthesizer->checkSystemExclusive(pMessage, nLength, nTG);
+ LOGDBG("SYSEX handler return value: %d", sysex_return);
+
+ switch (sysex_return)
+ {
+ case -1:
+ LOGERR("SysEx end status byte not detected.");
break;
- }
- }
- else if(sysex_return >= 500 && sysex_return < 600)
- {
- LOGDBG("SysEx send voice %u request",sysex_return-500);
- SendSystemExclusiveVoice(sysex_return-500, nCable, nTG);
- }
- break;
- }
+ case -2:
+ LOGERR("SysEx vendor not Yamaha.");
+ break;
+ case -3:
+ LOGERR("Unknown SysEx parameter change.");
+ break;
+ case -4:
+ LOGERR("Unknown SysEx voice or function.");
+ break;
+ case -5:
+ LOGERR("Not a SysEx voice bulk upload.");
+ break;
+ case -6:
+ LOGERR("Wrong length for SysEx voice bulk upload (not 155).");
+ break;
+ case -7:
+ LOGERR("Checksum error for one voice.");
+ break;
+ case -8:
+ LOGERR("Not a SysEx bank bulk upload.");
+ break;
+ case -9:
+ LOGERR("Wrong length for SysEx bank bulk upload (not 4096).");
+ case -10:
+ LOGERR("Checksum error for bank.");
+ break;
+ case -11:
+ LOGERR("Unknown SysEx message.");
+ break;
+ case 64:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setMonoMode(pMessage[5],nTG);
+ break;
+ case 65:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setPitchbendRange(pMessage[5],nTG);
+ break;
+ case 66:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setPitchbendStep(pMessage[5],nTG);
+ break;
+ case 67:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setPortamentoMode(pMessage[5],nTG);
+ break;
+ case 68:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setPortamentoGlissando(pMessage[5],nTG);
+ break;
+ case 69:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setPortamentoTime(pMessage[5],nTG);
+ break;
+ case 70:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setModWheelRange(pMessage[5],nTG);
+ break;
+ case 71:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setModWheelTarget(pMessage[5],nTG);
+ break;
+ case 72:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setFootControllerRange(pMessage[5],nTG);
+ break;
+ case 73:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setFootControllerTarget(pMessage[5],nTG);
+ break;
+ case 74:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setBreathControllerRange(pMessage[5],nTG);
+ break;
+ case 75:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setBreathControllerTarget(pMessage[5],nTG);
+ break;
+ case 76:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setAftertouchRange(pMessage[5],nTG);
+ break;
+ case 77:
+ LOGDBG("SysEx Function parameter change: %d Value %d",pMessage[4],pMessage[5]);
+ m_pSynthesizer->setAftertouchTarget(pMessage[5],nTG);
+ break;
+ case 100:
+ // load sysex-data into voice memory
+ LOGDBG("One Voice bulk upload");
+ m_pSynthesizer->loadVoiceParameters(pMessage,nTG);
+ break;
+ case 200:
+ LOGDBG("Bank bulk upload.");
+ //TODO: add code for storing a bank bulk upload
+ LOGNOTE("Currently code for storing a bulk bank upload is missing!");
+ break;
+ case 455:
+ // Parameter 155 + 300 added by Synth_Dexed = 455
+ LOGDBG("Operators enabled: %d%d%d%d%d%d", (pMessage[5] & 0x20) ? 1 : 0, (pMessage[5] & 0x10) ? 1 : 0, (pMessage[5] & 0x08) ? 1 : 0, (pMessage[5] & 0x04) ? 1 : 0, (pMessage[5] & 0x02) ? 1 : 0, (pMessage[5] & 0x01) ? 1 : 0);
+ m_pSynthesizer->setOPMask(pMessage[5], nTG);
+ break;
+ default:
+ if(sysex_return >= 300 && sysex_return < 500)
+ {
+ LOGDBG("SysEx voice parameter change: Parameter %d value: %d",pMessage[4] + ((pMessage[3] & 0x03) * 128), pMessage[5]);
+ m_pSynthesizer->setVoiceDataElement(pMessage[4] + ((pMessage[3] & 0x03) * 128), pMessage[5],nTG);
+ switch(pMessage[4] + ((pMessage[3] & 0x03) * 128))
+ {
+ case 134:
+ m_pSynthesizer->notesOff(0,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;
+ // Example: F0 43 20 00 F7
+ 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());
+ } else {
+ LOGWARN("No device found in s_DeviceMap for name: %s", deviceName.c_str());
+ }
+}
- // 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());
- }
-}
+void CMIDIDevice::SendSystemExclusiveBank(uint8_t nVoice, const std::string& deviceName, unsigned nCable, uint8_t nTG)
+{
+ LOGNOTE("SendSystemExclusiveBank");
+ static uint8_t voicedump[4096]; // Correct size for DX7 bank dump
+ m_pSynthesizer->getSysExBankDump(voicedump, nTG);
+ LOGNOTE("SendSystemExclusiveBank: after getSysExBankDump");
+ LOGNOTE("SendSystemExclusiveBank: before chunking");
+ MIDISysExChunker chunker(voicedump, 4096, 512);
+ LOGNOTE("SendSystemExclusiveBank: after chunker creation");
+ TDeviceMap::const_iterator Iterator = s_DeviceMap.find(deviceName);
+ if (Iterator != s_DeviceMap.end()) {
+ LOGNOTE("SendSystemExclusiveBank: device found, starting chunk send loop");
+ int chunkCount = 0;
+ while (chunker.hasNext()) {
+ std::vector chunk = chunker.next();
+ // Pad chunk to a multiple of 4 bytes for USB MIDI
+ size_t pad = chunk.size() % 4;
+ if (pad != 0) {
+ chunk.resize(chunk.size() + (4 - pad), 0x00);
+ }
+ if (chunk.size() % 4 != 0) {
+ LOGERR("Chunk size %u is not a multiple of 4 before Send!", (unsigned)chunk.size());
+ assert(chunk.size() % 4 == 0);
+ }
+ LOGNOTE("SendSystemExclusiveBank: sending chunk %d, size=%u", chunkCount, (unsigned)chunk.size());
+ Iterator->second->Send(chunk.data(), chunk.size(), nCable);
+ chunkCount++;
+ }
+ LOGNOTE("SendSystemExclusiveBank: all chunks sent, total=%d", chunkCount);
+ LOGDBG("Send SYSEX bank dump %u to \"%s\" in 512-byte chunks", nVoice, deviceName.c_str());
+ } else {
+ LOGWARN("SendSystemExclusiveBank: No device found in s_DeviceMap for name: %s", deviceName.c_str());
+ }
+ LOGNOTE("SendSystemExclusiveBank: exit");
+}
diff --git a/src/mididevice.h b/src/mididevice.h
index 94b789b..104275a 100644
--- a/src/mididevice.h
+++ b/src/mididevice.h
@@ -54,7 +54,10 @@ 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);
+ void SendSystemExclusiveBank(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/midikeyboard.cpp b/src/midikeyboard.cpp
index db71168..b581210 100644
--- a/src/midikeyboard.cpp
+++ b/src/midikeyboard.cpp
@@ -21,15 +21,24 @@
// along with this program. If not, see .
//
#include "midikeyboard.h"
+#include "midichunker.h"
#include
+#include
+#include
+#include
#include
#include
+#include
+#include
+
+LOGMODULE("midikeyboard");
CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance)
: CMIDIDevice (pSynthesizer, pConfig, pUI),
m_nSysExIdx (0),
m_nInstance (nInstance),
- m_pMIDIDevice (0)
+ m_pMIDIDevice (0),
+ m_HasQueuedSysEx(false)
{
m_DeviceName.Format ("umidi%u", nInstance+1);
@@ -42,6 +51,34 @@ CMIDIKeyboard::~CMIDIKeyboard (void)
void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated)
{
+ // Send any queued SysEx response in a safe context
+ if (m_HasQueuedSysEx && m_pMIDIDevice) {
+ // Pad to multiple of 4 bytes for USB MIDI event packets
+ size_t sysexLen = m_QueuedSysEx.size();
+ size_t paddedLen = (sysexLen + 3) & ~3; // round up to next multiple of 4
+ if (paddedLen > sysexLen) {
+ m_QueuedSysEx.resize(paddedLen, 0x00);
+ }
+ // Send in safe chunks to avoid USB lockup
+ static constexpr size_t kUSBMIDIMaxChunk = 256; // or 512 if your stack allows
+ size_t offset = 0;
+ // Only send one chunk per Process() call to avoid blocking or watchdog reset
+ if (offset < m_QueuedSysEx.size()) {
+ size_t chunk = std::min(kUSBMIDIMaxChunk, m_QueuedSysEx.size() - offset);
+ LOGNOTE("SendEventPackets: about to send chunk at offset %u, length=%u", offset, chunk);
+ m_pMIDIDevice->SendEventPackets(m_QueuedSysEx.data() + offset, chunk);
+ offset += chunk;
+ // Save progress for next Process() call
+ if (offset < m_QueuedSysEx.size()) {
+ // Not done yet, keep queued SysEx and return
+ m_QueuedSysEx.erase(m_QueuedSysEx.begin(), m_QueuedSysEx.begin() + chunk);
+ return;
+ }
+ }
+ m_QueuedSysEx.clear();
+ m_HasQueuedSysEx = false;
+ }
+
while (!m_SendQueue.empty ())
{
TSendQueueEntry Entry = m_SendQueue.front ();
@@ -73,16 +110,80 @@ void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated)
}
}
-void CMIDIKeyboard::Send (const u8 *pMessage, size_t nLength, unsigned nCable)
+// Helper: Convert SysEx to USB MIDI event packets
+std::vector> SysExToUSBMIDIPackets(const uint8_t* data, size_t length, unsigned cable)
{
- TSendQueueEntry Entry;
- Entry.pMessage = new u8[nLength];
- Entry.nLength = nLength;
- Entry.nCable = nCable;
+ LOGNOTE("SysExToUSBMIDIPackets: length=%u, cable=%u", (unsigned)length, cable);
+ std::vector> packets;
+ size_t idx = 0;
+ while (idx < length) {
+ size_t remaining = length - idx;
+ uint8_t cin;
+ uint8_t packet[4] = {0};
+ packet[0] = (uint8_t)(cable << 4); // Upper nibble: cable number, lower: CIN
+ if (remaining >= 3) {
+ if (idx == 0) {
+ cin = 0x4; // SysEx Start or continue
+ } else {
+ cin = 0x4; // SysEx continue
+ }
+ packet[0] |= cin;
+ packet[1] = data[idx];
+ packet[2] = data[idx+1];
+ packet[3] = data[idx+2];
+ LOGNOTE(" Packet: [%02X %02X %02X %02X] (idx=%u)", packet[0], packet[1], packet[2], packet[3], (unsigned)idx);
+ idx += 3;
+ } else if (remaining == 2) {
+ cin = 0x6; // SysEx ends with 2 bytes
+ packet[0] |= cin;
+ packet[1] = data[idx];
+ packet[2] = data[idx+1];
+ packet[3] = 0;
+ LOGNOTE(" Packet: [%02X %02X %02X %02X] (last 2 bytes)", packet[0], packet[1], packet[2], packet[3]);
+ idx += 2;
+ } else if (remaining == 1) {
+ cin = 0x5; // SysEx ends with 1 byte
+ packet[0] |= cin;
+ packet[1] = data[idx];
+ packet[2] = 0;
+ packet[3] = 0;
+ LOGNOTE(" Packet: [%02X %02X %02X %02X] (last 1 byte)", packet[0], packet[1], packet[2], packet[3]);
+ idx += 1;
+ }
+ packets.push_back({packet[0], packet[1], packet[2], packet[3]});
+ }
+ LOGNOTE("SysExToUSBMIDIPackets: total packets=%u", (unsigned)packets.size());
+ return packets;
+}
- memcpy (Entry.pMessage, pMessage, nLength);
+void CMIDIKeyboard::Send(const u8 *pMessage, size_t nLength, unsigned nCable)
+{
+ // NOTE: For USB MIDI, we do NOT use MIDISysExChunker for SysEx sending.
+ // The chunker splits SysEx into arbitrary chunks for traditional MIDI (e.g., serial/DIN),
+ // but USB MIDI requires SysEx to be split into 4-byte USB MIDI event packets with specific CIN headers.
+ // Therefore, for USB MIDI, we packetize SysEx according to the USB MIDI spec and send with SendEventPackets().
+ // See: https://www.usb.org/sites/default/files/midi10.pdf (USB MIDI 1.0 spec)
+ // This is why the chunker is bypassed for USB MIDI SysEx sending.
- m_SendQueue.push (Entry);
+ // Check for valid SysEx
+ if (nLength >= 2 && pMessage[0] == 0xF0 && pMessage[nLength-1] == 0xF7 && m_pMIDIDevice) {
+ // Convert to USB MIDI event packets and send directly
+ auto packets = SysExToUSBMIDIPackets(pMessage, nLength, nCable);
+ std::vector flat;
+ for (const auto& pkt : packets) {
+ flat.insert(flat.end(), pkt.begin(), pkt.end());
+ }
+ m_QueuedSysEx = flat;
+ m_HasQueuedSysEx = true;
+ return;
+ }
+ // Not a SysEx, send as-is
+ TSendQueueEntry Entry;
+ Entry.pMessage = new u8[nLength];
+ Entry.nLength = nLength;
+ Entry.nCable = nCable;
+ memcpy(Entry.pMessage, pMessage, nLength);
+ m_SendQueue.push(Entry);
}
// Most packets will be passed straight onto the main MIDI message handler
diff --git a/src/midikeyboard.h b/src/midikeyboard.h
index bf62689..642c726 100644
--- a/src/midikeyboard.h
+++ b/src/midikeyboard.h
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#define USB_SYSEX_BUFFER_SIZE (MAX_DX7_SYSEX_LENGTH+128) // Allow a bit spare to handle unexpected SysEx messages
@@ -45,6 +46,14 @@ public:
void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) override;
+ void QueueSysExResponse(const uint8_t* data, size_t len) {
+ m_QueuedSysEx.assign(data, data + len);
+ m_HasQueuedSysEx = true;
+ }
+ bool HasQueuedSysExResponse() const { return m_HasQueuedSysEx; }
+ void ClearQueuedSysExResponse() { m_QueuedSysEx.clear(); m_HasQueuedSysEx = false; }
+ const std::vector& GetQueuedSysExResponse() const { return m_QueuedSysEx; }
+
private:
static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam);
static void DeviceRemovedHandler (CDevice *pDevice, void *pContext);
@@ -68,6 +77,11 @@ private:
CUSBMIDIDevice * volatile m_pMIDIDevice;
std::queue m_SendQueue;
+
+ std::vector m_QueuedSysEx;
+ bool m_HasQueuedSysEx = false;
};
+std::vector> SysExToUSBMIDIPackets(const uint8_t* data, size_t length, unsigned cable);
+
#endif
diff --git a/src/minidexed.cpp b/src/minidexed.cpp
index 40c69b7..b66a34e 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);
}
}
@@ -1873,6 +1873,67 @@ void CMiniDexed::getSysExVoiceDump(uint8_t* dest, uint8_t nTG)
dest[162] = 0xF7; // SysEx end
}
+void CMiniDexed::getSysExBankDump(uint8_t* dest, uint8_t nTG)
+{
+ LOGNOTE("getSysExBankDump: called for TG=%u", nTG);
+ return;
+ constexpr size_t kVoices = 32;
+ constexpr size_t kPackedVoiceSize = 128;
+ constexpr size_t kBulkDataSize = kVoices * kPackedVoiceSize; // 4096
+ constexpr size_t kHeaderSize = 6;
+ constexpr size_t kTotalSize = kHeaderSize + kBulkDataSize + 2; // +checksum +F7 = 4104
+
+ // Header (Yamaha DX7 standard)
+ LOGNOTE("getSysExBankDump: writing header");
+ dest[0] = 0xF0; // SysEx start
+ dest[1] = 0x43; // Yamaha ID
+ dest[2] = 0x00; // Sub-status (0), device/channel (0)
+ dest[3] = 0x09; // Format number (9 = 32 voices)
+ dest[4] = 0x20; // Byte count MSB (4096 = 0x1000, MSB=0x20)
+ dest[5] = 0x00; // Byte count LSB
+
+ LOGNOTE("getSysExBankDump: header: %02X %02X %02X %02X %02X %02X", dest[0], dest[1], dest[2], dest[3], dest[4], dest[5]);
+
+ // Fill packed voice data
+ uint8_t* pData = dest + kHeaderSize;
+ uint8_t checksum = 0;
+ for (size_t v = 0; v < kVoices; ++v) {
+ uint8_t packedVoice[kPackedVoiceSize];
+ m_SysExFileLoader.GetVoice(m_nVoiceBankID[nTG], v, packedVoice);
+ for (size_t b = 0; b < kPackedVoiceSize; ++b) {
+ pData[v * kPackedVoiceSize + b] = packedVoice[b];
+ checksum += packedVoice[b];
+ }
+ }
+ LOGNOTE("getSysExBankDump: packed data filled, checksum before complement: %02X", checksum);
+
+ // Checksum: 2's complement, masked to 7 bits
+ checksum = (~checksum + 1) & 0x7F;
+ dest[kHeaderSize + kBulkDataSize] = checksum;
+ LOGNOTE("getSysExBankDump: checksum after complement: %02X", checksum);
+
+ // Footer
+ dest[kHeaderSize + kBulkDataSize + 1] = 0xF7;
+ LOGNOTE("getSysExBankDump: footer: %02X", dest[kHeaderSize + kBulkDataSize + 1]);
+
+ // Log summary of dump
+ LOGNOTE("getSysExBankDump: total size: %d bytes", kTotalSize);
+ std::string dumpStart, dumpEnd;
+ char buf[8];
+ for (size_t i = 0; i < 16; ++i) {
+ snprintf(buf, sizeof(buf), "%02X", dest[i]);
+ dumpStart += buf;
+ if (i < 15) dumpStart += " ";
+ }
+ for (size_t i = kTotalSize - 16; i < kTotalSize; ++i) {
+ snprintf(buf, sizeof(buf), "%02X", dest[i]);
+ dumpEnd += buf;
+ if (i < kTotalSize - 1) dumpEnd += " ";
+ }
+ LOGNOTE("getSysExBankDump: first 16 bytes: %s", dumpStart.c_str());
+ LOGNOTE("getSysExBankDump: last 16 bytes: %s", dumpEnd.c_str());
+}
+
void CMiniDexed::setOPMask(uint8_t uchOPMask, uint8_t nTG)
{
m_uchOPMask[nTG] = uchOPMask;
diff --git a/src/minidexed.h b/src/minidexed.h
index 1658482..3e877a2 100644
--- a/src/minidexed.h
+++ b/src/minidexed.h
@@ -124,6 +124,7 @@ public:
void loadVoiceParameters(const uint8_t* data, uint8_t nTG);
void setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG);
void getSysExVoiceDump(uint8_t* dest, uint8_t nTG);
+ void getSysExBankDump(uint8_t* dest, uint8_t nTG);
void setOPMask(uint8_t uchOPMask, uint8_t nTG);
void setModController (unsigned controller, unsigned parameter, uint8_t value, uint8_t nTG);
diff --git a/src/net/applemidi.cpp b/src/net/applemidi.cpp
index 82014e1..8cbd3a4 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");
@@ -876,4 +879,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 a774592..492be10 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 1934e42..c915989 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 %d 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 %d bytes to UDP MIDI host", nLength);
+ } else {
+ LOGNOTE("Sent %d 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