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