pull/905/merge
probonopd 2 days ago committed by GitHub
commit de0a4d1eb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      src/Makefile
  2. 48
      src/midichunker.cpp
  3. 37
      src/midichunker.h
  4. 102
      src/mididevice.cpp
  5. 5
      src/mididevice.h
  6. 113
      src/midikeyboard.cpp
  7. 14
      src/midikeyboard.h
  8. 63
      src/minidexed.cpp
  9. 1
      src/minidexed.h
  10. 45
      src/net/applemidi.cpp
  11. 3
      src/net/applemidi.h
  12. 26
      src/udpmididevice.cpp
  13. 7
      src/udpmididevice.h

@ -11,7 +11,8 @@ OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \
sysexfileloader.o performanceconfig.o perftimer.o \ sysexfileloader.o performanceconfig.o perftimer.o \
effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \ effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \
arm_float_to_q23.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 OPTIMIZE = -O3

@ -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 <http://www.gnu.org/licenses/>.
//
#include "midichunker.h"
#include <algorithm>
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<uint8_t> 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<uint8_t> chunk(m_data + m_offset, m_data + m_offset + chunkLen);
m_offset += chunkLen;
return chunk;
}
void MIDISysExChunker::reset() {
m_offset = 0;
}

@ -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 <http://www.gnu.org/licenses/>.
//
#pragma once
#include <cstddef>
#include <vector>
#include <cstdint>
class MIDISysExChunker {
public:
MIDISysExChunker(const uint8_t* data, size_t length, size_t chunkSize = 256);
bool hasNext() const;
std::vector<uint8_t> next();
void reset();
private:
const uint8_t* m_data;
size_t m_length;
size_t m_chunkSize;
size_t m_offset;
};

@ -29,8 +29,9 @@
#include <assert.h> #include <assert.h>
#include "midi.h" #include "midi.h"
#include "userinterface.h" #include "userinterface.h"
#include "midichunker.h"
LOGMODULE ("mididevice"); LOGMODULE("midikeyboard");
// MIDI "System" level (i.e. all TG) custom CC maps // MIDI "System" level (i.e. all TG) custom CC maps
// Note: Even if number of TGs is not 8, there are only 8 // 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); m_pSynthesizer->setMasterVolume(fMasterVolume);
} }
else 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(); unsigned nPerfCh = m_pSynthesizer->GetPerformanceSelectChannel();
switch (ucType) switch (ucType)
{ {
@ -320,23 +320,21 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
// Process MIDI for each active Tone Generator // Process MIDI for each active Tone Generator
bool bSystemCCHandled = false; bool bSystemCCHandled = false;
bool bSystemCCChecked = false; bool bSystemCCChecked = false;
for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators() && !bSystemCCHandled; nTG++) if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN) {
{
if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN)
{
// MIDI SYSEX per MIDI channel
uint8_t ucSysExChannel = (pMessage[2] & 0x0F); uint8_t ucSysExChannel = (pMessage[2] & 0x0F);
if (m_ChannelMap[nTG] == ucSysExChannel || m_ChannelMap[nTG] == OmniMode) 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); LOGNOTE("MIDI-SYSEX: channel: %u, len: %u, TG: %u",m_ChannelMap[nTG],nLength,nTG);
HandleSystemExclusive(pMessage, nLength, nCable, 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 if ( m_ChannelMap[nTG] == ucChannel
|| m_ChannelMap[nTG] == OmniMode) || m_ChannelMap[nTG] == OmniMode) {
{
switch (ucType) switch (ucType)
{ {
case MIDI_NOTE_ON: case MIDI_NOTE_ON:
@ -610,6 +608,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) 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);
SendSystemExclusiveBank(nTG, m_DeviceName, nCable, nTG);
return;
}
int16_t sysex_return; int16_t sysex_return;
sysex_return = m_pSynthesizer->checkSystemExclusive(pMessage, nLength, nTG); sysex_return = m_pSynthesizer->checkSystemExclusive(pMessage, nLength, nTG);
@ -732,28 +747,55 @@ void CMIDIDevice::HandleSystemExclusive(const uint8_t* pMessage, const size_t nL
break; 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;
} }
} }
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)
{ {
// Example: F0 43 20 00 F7
uint8_t voicedump[163]; uint8_t voicedump[163];
// Get voice sysex dump from TG
m_pSynthesizer->getSysExVoiceDump(voicedump, nTG); 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());
}
}
TDeviceMap::const_iterator Iterator; void CMIDIDevice::SendSystemExclusiveBank(uint8_t nVoice, const std::string& deviceName, unsigned nCable, uint8_t nTG)
{
// send voice dump to all MIDI interfaces LOGNOTE("SendSystemExclusiveBank");
for(Iterator = s_DeviceMap.begin(); Iterator != s_DeviceMap.end(); ++Iterator) static uint8_t voicedump[4096]; // Correct size for DX7 bank dump
{ m_pSynthesizer->getSysExBankDump(voicedump, nTG);
Iterator->second->Send (voicedump, sizeof(voicedump)*sizeof(uint8_t)); LOGNOTE("SendSystemExclusiveBank: after getSysExBankDump");
// LOGDBG("Send SYSEX voice dump %u to \"%s\"",nVoice,Iterator->first.c_str()); 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<uint8_t> 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");
} }

@ -54,7 +54,10 @@ public:
u8 GetChannel (unsigned nTG) const; u8 GetChannel (unsigned nTG) const;
virtual void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) {} 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: protected:
void MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsigned nCable = 0); void MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsigned nCable = 0);

@ -21,15 +21,24 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// //
#include "midikeyboard.h" #include "midikeyboard.h"
#include "midichunker.h"
#include <circle/devicenameservice.h> #include <circle/devicenameservice.h>
#include <circle/sched/scheduler.h>
#include <circle/timer.h>
#include <circle/logger.h>
#include <cstring> #include <cstring>
#include <assert.h> #include <assert.h>
#include <unistd.h>
#include <vector>
LOGMODULE("midikeyboard");
CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance) CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance)
: CMIDIDevice (pSynthesizer, pConfig, pUI), : CMIDIDevice (pSynthesizer, pConfig, pUI),
m_nSysExIdx (0), m_nSysExIdx (0),
m_nInstance (nInstance), m_nInstance (nInstance),
m_pMIDIDevice (0) m_pMIDIDevice (0),
m_HasQueuedSysEx(false)
{ {
m_DeviceName.Format ("umidi%u", nInstance+1); m_DeviceName.Format ("umidi%u", nInstance+1);
@ -42,6 +51,34 @@ CMIDIKeyboard::~CMIDIKeyboard (void)
void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated) 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 ()) while (!m_SendQueue.empty ())
{ {
TSendQueueEntry Entry = m_SendQueue.front (); 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<std::vector<uint8_t>> SysExToUSBMIDIPackets(const uint8_t* data, size_t length, unsigned cable)
{ {
LOGNOTE("SysExToUSBMIDIPackets: length=%u, cable=%u", (unsigned)length, cable);
std::vector<std::vector<uint8_t>> 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;
}
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.
// 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<uint8_t> 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; TSendQueueEntry Entry;
Entry.pMessage = new u8[nLength]; Entry.pMessage = new u8[nLength];
Entry.nLength = nLength; Entry.nLength = nLength;
Entry.nCable = nCable; Entry.nCable = nCable;
memcpy(Entry.pMessage, pMessage, nLength);
memcpy (Entry.pMessage, pMessage, nLength); m_SendQueue.push(Entry);
m_SendQueue.push (Entry);
} }
// Most packets will be passed straight onto the main MIDI message handler // Most packets will be passed straight onto the main MIDI message handler

@ -30,6 +30,7 @@
#include <circle/string.h> #include <circle/string.h>
#include <circle/types.h> #include <circle/types.h>
#include <queue> #include <queue>
#include <vector>
#define USB_SYSEX_BUFFER_SIZE (MAX_DX7_SYSEX_LENGTH+128) // Allow a bit spare to handle unexpected SysEx messages #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 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<uint8_t>& GetQueuedSysExResponse() const { return m_QueuedSysEx; }
private: private:
static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam); static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam);
static void DeviceRemovedHandler (CDevice *pDevice, void *pContext); static void DeviceRemovedHandler (CDevice *pDevice, void *pContext);
@ -68,6 +77,11 @@ private:
CUSBMIDIDevice * volatile m_pMIDIDevice; CUSBMIDIDevice * volatile m_pMIDIDevice;
std::queue<TSendQueueEntry> m_SendQueue; std::queue<TSendQueueEntry> m_SendQueue;
std::vector<uint8_t> m_QueuedSysEx;
bool m_HasQueuedSysEx = false;
}; };
std::vector<std::vector<uint8_t>> SysExToUSBMIDIPackets(const uint8_t* data, size_t length, unsigned cable);
#endif #endif

@ -666,7 +666,7 @@ void CMiniDexed::ProgramChange (unsigned nProgram, unsigned nTG)
// MIDI channel configured for this TG // MIDI channel configured for this TG
if (m_nMIDIChannel[nTG] < CMIDIDevice::Channels) 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 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) void CMiniDexed::setOPMask(uint8_t uchOPMask, uint8_t nTG)
{ {
m_uchOPMask[nTG] = uchOPMask; m_uchOPMask[nTG] = uchOPMask;

@ -124,6 +124,7 @@ public:
void loadVoiceParameters(const uint8_t* data, uint8_t nTG); void loadVoiceParameters(const uint8_t* data, uint8_t nTG);
void setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG); void setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG);
void getSysExVoiceDump(uint8_t* dest, 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 setOPMask(uint8_t uchOPMask, uint8_t nTG);
void setModController (unsigned controller, unsigned parameter, uint8_t value, uint8_t nTG); void setModController (unsigned controller, unsigned parameter, uint8_t value, uint8_t nTG);

@ -31,6 +31,9 @@
#include "applemidi.h" #include "applemidi.h"
#include "byteorder.h" #include "byteorder.h"
#define MAX_DX7_SYSEX_LENGTH 4104
#define MAX_MIDI_MESSAGE MAX_DX7_SYSEX_LENGTH
// #define APPLEMIDI_DEBUG // #define APPLEMIDI_DEBUG
LOGMODULE("applemidi"); LOGMODULE("applemidi");
@ -877,3 +880,45 @@ bool CAppleMIDIParticipant::SendFeedbackPacket()
return SendPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort, &FeedbackPacket, sizeof(FeedbackPacket)); 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;
}

@ -46,6 +46,9 @@ public:
virtual void Run() override; virtual void Run() override;
public:
bool SendMIDIToHost(const u8* pData, size_t nSize);
private: private:
void ControlInvitationState(); void ControlInvitationState();
void MIDIInvitationState(); void MIDIInvitationState();

@ -25,6 +25,8 @@
#include <cstring> #include <cstring>
#include "udpmididevice.h" #include "udpmididevice.h"
#include <assert.h> #include <assert.h>
#include <circle/net/netsubsystem.h>
#include <circle/net/in.h>
#define VIRTUALCABLE 24 #define VIRTUALCABLE 24
@ -64,6 +66,13 @@ boolean CUDPMIDIDevice::Initialize (void)
} }
else else
LOGNOTE("UDP MIDI receiver initialized"); 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; return true;
} }
@ -88,3 +97,20 @@ void CUDPMIDIDevice::OnUDPMIDIDataReceived(const u8* pData, size_t nSize)
{ {
MIDIMessageHandler(pData, nSize, VIRTUALCABLE); 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);
}
}
}

@ -29,6 +29,7 @@
#include "config.h" #include "config.h"
#include "net/applemidi.h" #include "net/applemidi.h"
#include "net/udpmidi.h" #include "net/udpmidi.h"
#include "midi.h"
class CMiniDexed; class CMiniDexed;
@ -43,6 +44,7 @@ public:
virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) override; virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) override;
virtual void OnAppleMIDIDisconnect(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 OnUDPMIDIDataReceived(const u8* pData, size_t nSize) override;
virtual void Send(const u8 *pMessage, size_t nLength, unsigned nCable = 0) override;
private: private:
CMiniDexed *m_pSynthesizer; CMiniDexed *m_pSynthesizer;
@ -50,6 +52,11 @@ private:
CBcmRandomNumberGenerator m_Random; CBcmRandomNumberGenerator m_Random;
CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance
CUDPMIDIReceiver* m_pUDPMIDIReceiver; CUDPMIDIReceiver* m_pUDPMIDIReceiver;
CSocket* m_pUDPSendSocket = nullptr;
CIPAddress m_UDPDestAddress;
unsigned m_UDPDestPort = 1999;
CIPAddress m_LastUDPSenderAddress;
unsigned m_LastUDPSenderPort = 0;
}; };
#endif #endif

Loading…
Cancel
Save