diff --git a/src/Makefile b/src/Makefile index 73dbddc..f3b878a 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 \ + performance_sysex.o OPTIMIZE = -O3 @@ -23,4 +24,4 @@ include ./Rules.mk clean: @echo "Cleaning up..." - rm -f $(OBJS) *.o *.d *~ core + rm -f $(OBJS) *.o *.d *~ core \ No newline at end of file diff --git a/src/mididevice.cpp b/src/mididevice.cpp index 413dd13..318c5ae 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -29,6 +29,17 @@ #include #include "midi.h" #include "userinterface.h" +#include "performanceconfig.h" +#include "performance_sysex.h" + +// RAII guard for MIDI spinlock +class MIDISpinLockGuard { +public: + MIDISpinLockGuard(CSpinLock& lock) : m_lock(lock) { m_lock.Acquire(); } + ~MIDISpinLockGuard() { m_lock.Release(); } +private: + CSpinLock& m_lock; +}; LOGMODULE ("mididevice"); @@ -128,6 +139,8 @@ u8 CMIDIDevice::GetChannel (unsigned nTG) const void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsigned nCable) { + MIDISpinLockGuard guard(m_MIDISpinLock); + // The packet contents are just normal MIDI data - see // https://www.midi.org/specifications/item/table-1-summary-of-midi-message @@ -207,8 +220,6 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign return; } - m_MIDISpinLock.Acquire (); - u8 ucStatus = pMessage[0]; u8 ucChannel = ucStatus & 0x0F; u8 ucType = ucStatus >> 4; @@ -222,14 +233,23 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign LOGNOTE("MIDI-SYSEX: Set TG%d to MIDI Channel %d", mTG + 1, val & 0x0F); m_pSynthesizer->SetMIDIChannel(val & 0x0F, mTG); // Do not process this message further for any TGs - m_MIDISpinLock.Release(); return; } + // Handle MiniDexed performance SysEx messages + if (pMessage[0] == MIDI_SYSTEM_EXCLUSIVE_BEGIN && pMessage[1] == 0x7D ) { + LOGNOTE("MiniDexed SysEx handler entered, nLength=%u", nLength); + // Use performance_sysex.h to handle the SysEx message + handle_performance_sysex(pMessage, this, m_pSynthesizer->GetPerformanceConfig(), nCable); + return; + } else { + LOGNOTE("MiniDexed SysEx handler NOT entered, nLength=%u", nLength); + } + // Master Volume is set using a MIDI SysEx message as follows: // F0 Start of SysEx // 7F System Realtime SysEx - // 7F SysEx "channel" - 7F = all devices + // 7F SysEx "channel" - 7F = all devices // 04 Device Control // 01 Master Volume Device Control // LL Low 7-bits of 14-bit volume @@ -449,11 +469,8 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign break; } } - else - { - HandleSystemExclusive(pMessage, nLength, nCable, nTG); - } } + } } else { for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators() && !bSystemCCHandled; nTG++) { @@ -671,7 +688,6 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign } } } - m_MIDISpinLock.Release (); } void CMIDIDevice::AddDevice (const char *pDeviceName) @@ -700,7 +716,7 @@ bool CMIDIDevice::HandleMIDISystemCC(const u8 ucCC, const u8 ucCCval) } // Not looking for duplicate CCs so return once handled - for (unsigned tg=0; tg<8; tg++) { +for (unsigned tg=0; tg<8; tg++) { if (m_nMIDISystemCCVol != 0) { if (ucCC == MIDISystemCCMap[m_nMIDISystemCCVol][tg]) { m_pSynthesizer->SetVolume (ucCCval, tg); @@ -736,7 +752,17 @@ 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) { - LOGDBG("HandleSystemExclusive: TG %d, length %zu", nTG, nLength); + LOGDBG("HandleSystemExclusive: TG %d, length %u", nTG, nLength); + + // Only handle Yamaha (0x43) or MiniDexed (0x7D) SysEx + if (nLength < 2) { + LOGDBG("SysEx too short, ignoring"); + return; + } + if (pMessage[1] != 0x43 && pMessage[1] != 0x7D) { + LOGDBG("Ignoring SysEx with manufacturer 0x%02X (not Yamaha or MiniDexed)", pMessage[1]); + return; + } // 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 diff --git a/src/midikeyboard.cpp b/src/midikeyboard.cpp index db71168..3d127ed 100644 --- a/src/midikeyboard.cpp +++ b/src/midikeyboard.cpp @@ -95,52 +95,58 @@ void CMIDIKeyboard::USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsign if ((pPacket[0] == 0xF0) && (m_nSysExIdx == 0)) { // Start of SysEx message - //printf("SysEx Start Idx=%d, (%d)\n", m_nSysExIdx, nLength); for (unsigned i=0; i 2 && m_SysEx[1] != 0x43 && m_SysEx[1] != 0x7D) { + // LOGNOTE("Aborting SysEx assembly: manufacturer 0x%02X not Yamaha or MiniDexed", m_SysEx[1]); + m_nSysExIdx = 0; + // Do not process remaining bytes as MIDI events + } } else if (m_nSysExIdx != 0) { - // Continue processing SysEx message - //printf("SysEx Packet Idx=%d, (%d)\n", m_nSysExIdx, nLength); for (unsigned i=0; i= USB_SYSEX_BUFFER_SIZE) { - // Run out of space, so reset and ignore rest of the message + // LOGERR("SysEx buffer overflow, resetting SysEx assembly"); m_nSysExIdx = 0; break; } else if (pPacket[i] == 0xF7) { - // End of SysEx message m_SysEx[m_nSysExIdx++] = pPacket[i]; - //printf ("SysEx End Idx=%d\n", m_nSysExIdx); - MIDIMessageHandler (m_SysEx, m_nSysExIdx, nCable); - // Reset ready for next time + // Check manufacturer ID before passing to handler + if (m_SysEx[1] == 0x43 || m_SysEx[1] == 0x7D) { + MIDIMessageHandler (m_SysEx, m_nSysExIdx, nCable); + } else { + // LOGNOTE("Ignoring completed SysEx: manufacturer 0x%02X not Yamaha or MiniDexed", m_SysEx[1]); + } m_nSysExIdx = 0; } else if ((pPacket[i] & 0x80) != 0) { - // Received another command, so reset processing as something has gone wrong - //printf ("SysEx Reset\n"); + // LOGERR("Unexpected status byte 0x%02X in SysEx, resetting", pPacket[i]); m_nSysExIdx = 0; break; } - else - { - // Store the byte + else { m_SysEx[m_nSysExIdx++] = pPacket[i]; + // Early check for manufacturer ID after at least 2 bytes + if (m_nSysExIdx == 2 && m_SysEx[1] != 0x43 && m_SysEx[1] != 0x7D) { + // LOGNOTE("Aborting SysEx assembly: manufacturer 0x%02X not Yamaha or MiniDexed", m_SysEx[1]); + m_nSysExIdx = 0; + // Do not process remaining bytes as MIDI events + } } } } else { - // Assume it is a standard message MIDIMessageHandler (pPacket, nLength, nCable); } } diff --git a/src/net/applemidi.cpp b/src/net/applemidi.cpp index 8cbd3a4..d047025 100644 --- a/src/net/applemidi.cpp +++ b/src/net/applemidi.cpp @@ -919,6 +919,6 @@ bool CAppleMIDIParticipant::SendMIDIToHost(const u8* pData, size_t nSize) return false; } - LOGDBG("Successfully sent %zu bytes of MIDI data", nSize); + LOGDBG("Successfully sent %u bytes of MIDI data", nSize); return true; } \ No newline at end of file diff --git a/src/performance_sysex.cpp b/src/performance_sysex.cpp new file mode 100644 index 0000000..efda130 --- /dev/null +++ b/src/performance_sysex.cpp @@ -0,0 +1,205 @@ +// +// performance_sysex_handler.cpp +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2025 The MiniDexed Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include "performance_sysex.h" +#include "performanceconfig.h" +#include +#include +#include "minidexed.h" +#include "mididevice.h" + +LOGMODULE ("PerformanceSysEx"); + +static constexpr uint8_t SYSEX_START = 0xF0; +static constexpr uint8_t SYSEX_END = 0xF7; +static constexpr uint8_t MANUFACTURER_ID = 0x7D; +static constexpr uint8_t CMD_GET = 0x10; +static constexpr uint8_t CMD_SET = 0x20; + +static void send_sysex_response(const uint8_t* data, size_t len, CMIDIDevice* pDevice, unsigned nCable) { + if (pDevice) { + pDevice->Send(data, len, nCable); + LOGNOTE("Sent SysEx response (%u bytes) on cable %u", (unsigned)len, nCable); + } +} + +void handle_performance_sysex(const uint8_t* pMessage, CMIDIDevice* pDevice, CPerformanceConfig* perf, unsigned nCable) { + if (!pMessage || !perf) { + LOGERR("handle_performance_sysex: Null pointer or PerformanceConfig not set"); + return; + } + if (pMessage[0] != SYSEX_START) { + LOGERR("SysEx: Invalid start byte: 0x%02X", pMessage[0]); + return; + } + int len = 0; + while (len < 256 && pMessage[len] != SYSEX_END) ++len; + if (len < 3 || pMessage[len] != SYSEX_END) { + LOGERR("SysEx: No end byte or too short"); + return; + } + if (pMessage[1] != MANUFACTURER_ID) { + LOGERR("SysEx: Invalid manufacturer ID: 0x%02X", pMessage[1]); + return; + } + + uint8_t tg = 0xFF; // 0xFF = global + uint8_t cmd = 0; + uint8_t offset = 0; + + if (pMessage[2] == CMD_GET || pMessage[2] == CMD_SET) { + cmd = pMessage[2]; + offset = 3; + LOGNOTE("SysEx: Global %s request", cmd == CMD_GET ? "GET" : "SET"); + } else if (pMessage[2] < 0x80 && (pMessage[3] == CMD_GET || pMessage[3] == CMD_SET)) { + tg = pMessage[2]; + cmd = pMessage[3]; + offset = 4; + LOGNOTE("SysEx: TG-specific %s request for TG %u", cmd == CMD_GET ? "GET" : "SET", tg); + } else { + LOGERR("SysEx: Unrecognized message structure"); + return; + } + + if (offset == len) { + // Dump all global or all TG parameters + if (cmd == CMD_GET) { + if (tg == 0xFF) { + // Dump all global + size_t count = 0; + const uint16_t* params = CPerformanceConfig::GetAllGlobalParams(count); + uint16_t values[16] = {0}; + if (perf->GetGlobalParameters(params, values, count)) { + LOGNOTE("SysEx: Dumping all global parameters"); + uint8_t resp[64] = {0}; + resp[0] = SYSEX_START; + resp[1] = MANUFACTURER_ID; + resp[2] = CMD_GET; + size_t idx = 3; + for (size_t i = 0; i < count; ++i) { + resp[idx++] = (params[i] >> 8) & 0xFF; + resp[idx++] = params[i] & 0xFF; + resp[idx++] = (values[i] >> 8) & 0xFF; + resp[idx++] = values[i] & 0xFF; + LOGNOTE(" Param 0x%04X = 0x%04X", params[i], values[i]); + } + resp[idx++] = SYSEX_END; + send_sysex_response(resp, idx, pDevice, nCable); + } else { + LOGERR("SysEx: Failed to get all global parameters"); + } + } else { + // Dump all TG + size_t count = 0; + const uint16_t* params = CPerformanceConfig::GetAllTGParams(count); + uint16_t values[32] = {0}; + if (perf->GetTGParameters(params, values, count, tg)) { + LOGNOTE("SysEx: Dumping all TG parameters for TG %u", tg); + uint8_t resp[128] = {0}; + resp[0] = SYSEX_START; + resp[1] = MANUFACTURER_ID; + resp[2] = tg; + resp[3] = CMD_GET; + size_t idx = 4; + for (size_t i = 0; i < count; ++i) { + resp[idx++] = (params[i] >> 8) & 0xFF; + resp[idx++] = params[i] & 0xFF; + resp[idx++] = (values[i] >> 8) & 0xFF; + resp[idx++] = values[i] & 0xFF; + LOGNOTE(" Param 0x%04X = 0x%04X", params[i], values[i]); + } + resp[idx++] = SYSEX_END; + send_sysex_response(resp, idx, pDevice, nCable); + } else { + LOGERR("SysEx: Failed to get all TG parameters for TG %u", tg); + } + } + } + return; + } + while (offset + 1 < len) { + uint16_t param = (pMessage[offset] << 8) | pMessage[offset+1]; + offset += 2; + if (cmd == CMD_GET) { + uint16_t value = 0; + bool ok = false; + if (tg == 0xFF) { + ok = perf->GetGlobalParameters(¶m, &value, 1); + LOGNOTE("SysEx: GET global param 0x%04X -> 0x%04X (%s)", param, value, ok ? "OK" : "FAIL"); + // Build and send response + uint8_t resp[9] = {SYSEX_START, MANUFACTURER_ID, CMD_GET, (uint8_t)(param >> 8), (uint8_t)(param & 0xFF), (uint8_t)(value >> 8), (uint8_t)(value & 0xFF), SYSEX_END}; + send_sysex_response(resp, 8, pDevice, nCable); + } else { + ok = perf->GetTGParameters(¶m, &value, 1, tg); + LOGNOTE("SysEx: GET TG %u param 0x%04X -> 0x%04X (%s)", tg, param, value, ok ? "OK" : "FAIL"); + uint8_t resp[10] = {SYSEX_START, MANUFACTURER_ID, tg, CMD_GET, (uint8_t)(param >> 8), (uint8_t)(param & 0xFF), (uint8_t)(value >> 8), (uint8_t)(value & 0xFF), SYSEX_END}; + send_sysex_response(resp, 9, pDevice, nCable); + } + } else if (cmd == CMD_SET) { + if (offset + 1 >= len) { + LOGERR("SysEx: SET param 0x%04X missing value bytes", param); + break; + } + uint16_t value = (pMessage[offset] << 8) | pMessage[offset+1]; + offset += 2; + bool ok = false; + if (tg == 0xFF) { + ok = perf->SetGlobalParameters(¶m, &value, 1); + LOGNOTE("SysEx: SET global param 0x%04X = 0x%04X (%s)", param, value, ok ? "OK" : "FAIL"); + } else { + ok = perf->SetTGParameters(¶m, &value, 1, tg); + LOGNOTE("SysEx: SET TG %u param 0x%04X = 0x%04X (%s)", tg, param, value, ok ? "OK" : "FAIL"); + } + } + } +} + +/* +Examples of MiniDexed Performance SysEx messages and expected handler behavior: + +1. Get a Global Parameter (e.g., CompressorEnable) + Send: F0 7D 10 00 00 F7 + - Logs GET for global param 0x0000, queries value, logs result. + +2. Set a Global Parameter (e.g., ReverbEnable ON) + Send: F0 7D 20 00 01 00 01 F7 + - Logs SET for global param 0x0001, updates value, logs result. + +3. Get All Global Parameters + Send: F0 7D 10 F7 + - Logs and dumps all global parameters and values. + +4. Get a TG Parameter (e.g., Volume for TG 2) + Send: F0 7D 02 10 00 03 F7 + - Logs GET for TG 2 param 0x0003, queries value, logs result. + +5. Set a TG Parameter (e.g., Pan for TG 1 to 64) + Send: F0 7D 01 20 00 04 00 40 F7 + - Logs SET for TG 1 param 0x0004, updates value, logs result. + +6. Get All Parameters for TG 0 + Send: F0 7D 00 10 F7 + - Logs and dumps all TG 0 parameters and values. + +7. Malformed or Unknown Parameter + Send: F0 7D 10 12 34 F7 + - Logs GET for global param 0x1234, logs FAIL if unsupported. + +*/ diff --git a/src/performance_sysex.h b/src/performance_sysex.h new file mode 100644 index 0000000..3a85de7 --- /dev/null +++ b/src/performance_sysex.h @@ -0,0 +1,35 @@ +// +// performance_sysex_handler.h +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2025 The MiniDexed Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifndef PERFORMANCE_SYSEX_H +#define PERFORMANCE_SYSEX_H + +#include +#include +#include "performanceconfig.h" + +class CMIDIDevice; +// Handles a MiniDexed performance SysEx message. +// pMessage: pointer to the SysEx message (must be at least 2 bytes) +// pDevice: the MIDI device to send the response to +// perf: the performance config to operate on +void handle_performance_sysex(const uint8_t* pMessage, CMIDIDevice* pDevice, CPerformanceConfig* perf, unsigned nCable = 0); + +#endif // PERFORMANCE_SYSEX_H diff --git a/src/performanceconfig.cpp b/src/performanceconfig.cpp index dfff8a9..03732dc 100644 --- a/src/performanceconfig.cpp +++ b/src/performanceconfig.cpp @@ -1314,3 +1314,307 @@ bool CPerformanceConfig::IsValidPerformanceBank(unsigned nBankID) } return true; } + +// Static arrays for all parameter numbers +static const uint16_t s_globalParams[] = { + CPerformanceConfig::PARAM_COMPRESSOR_ENABLE, + CPerformanceConfig::PARAM_REVERB_ENABLE, + CPerformanceConfig::PARAM_REVERB_SIZE, + CPerformanceConfig::PARAM_REVERB_HIGHDAMP, + CPerformanceConfig::PARAM_REVERB_LOWDAMP, + CPerformanceConfig::PARAM_REVERB_LOWPASS, + CPerformanceConfig::PARAM_REVERB_DIFFUSION, + CPerformanceConfig::PARAM_REVERB_LEVEL +}; +static const uint16_t s_tgParams[] = { + CPerformanceConfig::PARAM_BANK_NUMBER, + CPerformanceConfig::PARAM_VOICE_NUMBER, + CPerformanceConfig::PARAM_MIDI_CHANNEL, + CPerformanceConfig::PARAM_VOLUME, + CPerformanceConfig::PARAM_PAN, + CPerformanceConfig::PARAM_DETUNE, + CPerformanceConfig::PARAM_CUTOFF, + CPerformanceConfig::PARAM_RESONANCE, + CPerformanceConfig::PARAM_NOTE_LIMIT_LOW, + CPerformanceConfig::PARAM_NOTE_LIMIT_HIGH, + CPerformanceConfig::PARAM_NOTE_SHIFT, + CPerformanceConfig::PARAM_REVERB_SEND, + CPerformanceConfig::PARAM_PITCH_BEND_RANGE, + CPerformanceConfig::PARAM_PITCH_BEND_STEP, + CPerformanceConfig::PARAM_PORTAMENTO_MODE, + CPerformanceConfig::PARAM_PORTAMENTO_GLISSANDO, + CPerformanceConfig::PARAM_PORTAMENTO_TIME, + CPerformanceConfig::PARAM_MONO_MODE, + CPerformanceConfig::PARAM_MODWHEEL_RANGE, + CPerformanceConfig::PARAM_MODWHEEL_TARGET, + CPerformanceConfig::PARAM_FOOTCTRL_RANGE, + CPerformanceConfig::PARAM_FOOTCTRL_TARGET, + CPerformanceConfig::PARAM_BREATHCTRL_RANGE, + CPerformanceConfig::PARAM_BREATHCTRL_TARGET, + CPerformanceConfig::PARAM_AFTERTOUCH_RANGE, + CPerformanceConfig::PARAM_AFTERTOUCH_TARGET +}; + +const uint16_t* CPerformanceConfig::GetAllGlobalParams(size_t& count) { + count = sizeof(s_globalParams) / sizeof(s_globalParams[0]); + return s_globalParams; +} +const uint16_t* CPerformanceConfig::GetAllTGParams(size_t& count) { + count = sizeof(s_tgParams) / sizeof(s_tgParams[0]); + return s_tgParams; +} + +// Generic parameter access for SysEx (pp pp, vv vv scheme) +bool CPerformanceConfig::GetGlobalParameters(const uint16_t *params, uint16_t *values, size_t count) const { + bool allOk = true; + for (size_t i = 0; i < count; ++i) { + switch (params[i]) { + case PARAM_COMPRESSOR_ENABLE: + values[i] = GetCompressorEnable() ? 1 : 0; + break; + case PARAM_REVERB_ENABLE: + values[i] = GetReverbEnable() ? 1 : 0; + break; + case PARAM_REVERB_SIZE: + values[i] = GetReverbSize(); + break; + case PARAM_REVERB_HIGHDAMP: + values[i] = GetReverbHighDamp(); + break; + case PARAM_REVERB_LOWDAMP: + values[i] = GetReverbLowDamp(); + break; + case PARAM_REVERB_LOWPASS: + values[i] = GetReverbLowPass(); + break; + case PARAM_REVERB_DIFFUSION: + values[i] = GetReverbDiffusion(); + break; + case PARAM_REVERB_LEVEL: + values[i] = GetReverbLevel(); + break; + default: + allOk = false; + break; + } + } + return allOk; +} + +bool CPerformanceConfig::SetGlobalParameters(const uint16_t *params, const uint16_t *values, size_t count) { + bool allOk = true; + for (size_t i = 0; i < count; ++i) { + switch (params[i]) { + case PARAM_COMPRESSOR_ENABLE: + SetCompressorEnable(values[i] != 0); + break; + case PARAM_REVERB_ENABLE: + SetReverbEnable(values[i] != 0); + break; + case PARAM_REVERB_SIZE: + SetReverbSize(values[i]); + break; + case PARAM_REVERB_HIGHDAMP: + SetReverbHighDamp(values[i]); + break; + case PARAM_REVERB_LOWDAMP: + SetReverbLowDamp(values[i]); + break; + case PARAM_REVERB_LOWPASS: + SetReverbLowPass(values[i]); + break; + case PARAM_REVERB_DIFFUSION: + SetReverbDiffusion(values[i]); + break; + case PARAM_REVERB_LEVEL: + SetReverbLevel(values[i]); + break; + default: + allOk = false; + break; + } + } + return allOk; +} + +bool CPerformanceConfig::GetTGParameters(const uint16_t *params, uint16_t *values, size_t count, unsigned tg) const { + if (tg >= CConfig::AllToneGenerators) return false; + bool allOk = true; + for (size_t i = 0; i < count; ++i) { + switch (params[i]) { + case PARAM_BANK_NUMBER: + values[i] = GetBankNumber(tg); + break; + case PARAM_VOICE_NUMBER: + values[i] = GetVoiceNumber(tg); + break; + case PARAM_MIDI_CHANNEL: + values[i] = GetMIDIChannel(tg); + break; + case PARAM_VOLUME: + values[i] = GetVolume(tg); + break; + case PARAM_PAN: + values[i] = GetPan(tg); + break; + case PARAM_DETUNE: + values[i] = static_cast(GetDetune(tg)); + break; + case PARAM_CUTOFF: + values[i] = GetCutoff(tg); + break; + case PARAM_RESONANCE: + values[i] = GetResonance(tg); + break; + case PARAM_NOTE_LIMIT_LOW: + values[i] = GetNoteLimitLow(tg); + break; + case PARAM_NOTE_LIMIT_HIGH: + values[i] = GetNoteLimitHigh(tg); + break; + case PARAM_NOTE_SHIFT: + values[i] = static_cast(GetNoteShift(tg)); + break; + case PARAM_REVERB_SEND: + values[i] = GetReverbSend(tg); + break; + case PARAM_PITCH_BEND_RANGE: + values[i] = GetPitchBendRange(tg); + break; + case PARAM_PITCH_BEND_STEP: + values[i] = GetPitchBendStep(tg); + break; + case PARAM_PORTAMENTO_MODE: + values[i] = GetPortamentoMode(tg); + break; + case PARAM_PORTAMENTO_GLISSANDO: + values[i] = GetPortamentoGlissando(tg); + break; + case PARAM_PORTAMENTO_TIME: + values[i] = GetPortamentoTime(tg); + break; + case PARAM_MONO_MODE: + values[i] = GetMonoMode(tg) ? 1 : 0; + break; + case PARAM_MODWHEEL_RANGE: + values[i] = GetModulationWheelRange(tg); + break; + case PARAM_MODWHEEL_TARGET: + values[i] = GetModulationWheelTarget(tg); + break; + case PARAM_FOOTCTRL_RANGE: + values[i] = GetFootControlRange(tg); + break; + case PARAM_FOOTCTRL_TARGET: + values[i] = GetFootControlTarget(tg); + break; + case PARAM_BREATHCTRL_RANGE: + values[i] = GetBreathControlRange(tg); + break; + case PARAM_BREATHCTRL_TARGET: + values[i] = GetBreathControlTarget(tg); + break; + case PARAM_AFTERTOUCH_RANGE: + values[i] = GetAftertouchRange(tg); + break; + case PARAM_AFTERTOUCH_TARGET: + values[i] = GetAftertouchTarget(tg); + break; + default: + allOk = false; + break; + } + } + return allOk; +} + +bool CPerformanceConfig::SetTGParameters(const uint16_t *params, const uint16_t *values, size_t count, unsigned tg) { + if (tg >= CConfig::AllToneGenerators) return false; + bool allOk = true; + for (size_t i = 0; i < count; ++i) { + switch (params[i]) { + case PARAM_BANK_NUMBER: + SetBankNumber(values[i], tg); + break; + case PARAM_VOICE_NUMBER: + SetVoiceNumber(values[i], tg); + break; + case PARAM_MIDI_CHANNEL: + SetMIDIChannel(values[i], tg); + break; + case PARAM_VOLUME: + SetVolume(values[i], tg); + break; + case PARAM_PAN: + SetPan(values[i], tg); + break; + case PARAM_DETUNE: + SetDetune(static_cast(values[i]), tg); + break; + case PARAM_CUTOFF: + SetCutoff(values[i], tg); + break; + case PARAM_RESONANCE: + SetResonance(values[i], tg); + break; + case PARAM_NOTE_LIMIT_LOW: + SetNoteLimitLow(values[i], tg); + break; + case PARAM_NOTE_LIMIT_HIGH: + SetNoteLimitHigh(values[i], tg); + break; + case PARAM_NOTE_SHIFT: + SetNoteShift(static_cast(values[i]), tg); + break; + case PARAM_REVERB_SEND: + SetReverbSend(values[i], tg); + break; + case PARAM_PITCH_BEND_RANGE: + SetPitchBendRange(values[i], tg); + break; + case PARAM_PITCH_BEND_STEP: + SetPitchBendStep(values[i], tg); + break; + case PARAM_PORTAMENTO_MODE: + SetPortamentoMode(values[i], tg); + break; + case PARAM_PORTAMENTO_GLISSANDO: + SetPortamentoGlissando(values[i], tg); + break; + case PARAM_PORTAMENTO_TIME: + SetPortamentoTime(values[i], tg); + break; + case PARAM_MONO_MODE: + SetMonoMode(values[i] != 0, tg); + break; + case PARAM_MODWHEEL_RANGE: + SetModulationWheelRange(values[i], tg); + break; + case PARAM_MODWHEEL_TARGET: + SetModulationWheelTarget(values[i], tg); + break; + case PARAM_FOOTCTRL_RANGE: + SetFootControlRange(values[i], tg); + break; + case PARAM_FOOTCTRL_TARGET: + SetFootControlTarget(values[i], tg); + break; + case PARAM_BREATHCTRL_RANGE: + SetBreathControlRange(values[i], tg); + break; + case PARAM_BREATHCTRL_TARGET: + SetBreathControlTarget(values[i], tg); + break; + case PARAM_AFTERTOUCH_RANGE: + SetAftertouchRange(values[i], tg); + break; + case PARAM_AFTERTOUCH_TARGET: + SetAftertouchTarget(values[i], tg); + break; + default: + allOk = false; + break; + } + } + return allOk; +} diff --git a/src/performanceconfig.h b/src/performanceconfig.h index 916a2ee..9428e53 100644 --- a/src/performanceconfig.h +++ b/src/performanceconfig.h @@ -33,6 +33,47 @@ class CPerformanceConfig // Performance configuration { public: + // Parameter numbers for SysEx (MiniDexed Format) + enum ParameterNumber { + // Global parameters (pp pp) + PARAM_COMPRESSOR_ENABLE = 0x0000, + PARAM_REVERB_ENABLE = 0x0001, + PARAM_REVERB_SIZE = 0x0002, + PARAM_REVERB_HIGHDAMP = 0x0003, + PARAM_REVERB_LOWDAMP = 0x0004, + PARAM_REVERB_LOWPASS = 0x0005, + PARAM_REVERB_DIFFUSION = 0x0006, + PARAM_REVERB_LEVEL = 0x0007, + + // TG-specific parameters (pp pp) + PARAM_BANK_NUMBER = 0x0000, + PARAM_VOICE_NUMBER = 0x0001, + PARAM_MIDI_CHANNEL = 0x0002, + PARAM_VOLUME = 0x0003, + PARAM_PAN = 0x0004, + PARAM_DETUNE = 0x0005, + PARAM_CUTOFF = 0x0006, + PARAM_RESONANCE = 0x0007, + PARAM_NOTE_LIMIT_LOW = 0x0008, + PARAM_NOTE_LIMIT_HIGH = 0x0009, + PARAM_NOTE_SHIFT = 0x000A, + PARAM_REVERB_SEND = 0x000B, + PARAM_PITCH_BEND_RANGE = 0x000C, + PARAM_PITCH_BEND_STEP = 0x000D, + PARAM_PORTAMENTO_MODE = 0x000E, + PARAM_PORTAMENTO_GLISSANDO = 0x000F, + PARAM_PORTAMENTO_TIME = 0x0010, + PARAM_MONO_MODE = 0x0011, + PARAM_MODWHEEL_RANGE = 0x0012, + PARAM_MODWHEEL_TARGET = 0x0013, + PARAM_FOOTCTRL_RANGE = 0x0014, + PARAM_FOOTCTRL_TARGET = 0x0015, + PARAM_BREATHCTRL_RANGE = 0x0016, + PARAM_BREATHCTRL_TARGET = 0x0017, + PARAM_AFTERTOUCH_RANGE = 0x0018, + PARAM_AFTERTOUCH_TARGET = 0x0019 + }; + CPerformanceConfig (FATFS *pFileSystem); ~CPerformanceConfig (void); @@ -149,6 +190,14 @@ public: std::string GetPerformanceBankName(unsigned nBankID); bool IsValidPerformanceBank(unsigned nBankID); + bool GetGlobalParameters(const uint16_t *params, uint16_t *values, size_t count) const; + bool SetGlobalParameters(const uint16_t *params, const uint16_t *values, size_t count); + bool GetTGParameters(const uint16_t *params, uint16_t *values, size_t count, unsigned tg) const; + bool SetTGParameters(const uint16_t *params, const uint16_t *values, size_t count, unsigned tg); + + static const uint16_t* GetAllGlobalParams(size_t& count); + static const uint16_t* GetAllTGParams(size_t& count); + private: CPropertiesFatFsFile m_Properties; diff --git a/src/serialmididevice.cpp b/src/serialmididevice.cpp index 3fe01e8..239cfd6 100644 --- a/src/serialmididevice.cpp +++ b/src/serialmididevice.cpp @@ -75,30 +75,24 @@ void CSerialMIDIDevice::Process (void) return; } -/* if (m_pConfig->GetMIDIDumpEnabled ()) - { - printf("Incoming MIDI data:"); - for (uint16_t i = 0; i < nResult; i++) - { - if((i % 8) == 0) - printf("\n%04d:",i); - printf(" 0x%02x",Buffer[i]); - } - printf("\n"); - }*/ - - // Process MIDI messages - // See: https://www.midi.org/specifications/item/table-1-summary-of-midi-message - // "Running status" see: https://www.lim.di.unimi.it/IEEE/MIDI/SOT5.HTM#Running- - for (int i = 0; i < nResult; i++) { u8 uchData = Buffer[i]; - if(uchData == 0xF0) - { - // SYSEX found - m_SerialMessage[m_nSysEx++]=uchData; + // SysEx reassembly logic + if (uchData == 0xF0 && !m_SysExActive) { + m_SysExActive = true; + m_SysExLen = 0; + } + if (m_SysExActive) { + if (m_SysExLen < MAX_MIDI_MESSAGE) { + m_SysExBuffer[m_SysExLen++] = uchData; + } + if (uchData == 0xF7 || m_SysExLen >= MAX_MIDI_MESSAGE) { + MIDIMessageHandler(m_SysExBuffer, m_SysExLen, 0); + m_SysExActive = false; + m_SysExLen = 0; + } continue; } diff --git a/src/serialmididevice.h b/src/serialmididevice.h index fdf61b2..a59ae64 100644 --- a/src/serialmididevice.h +++ b/src/serialmididevice.h @@ -53,6 +53,11 @@ private: u8 m_SerialMessage[MAX_MIDI_MESSAGE]; CWriteBufferDevice m_SendBuffer; + + // SysEx reassembly buffer for serial MIDI + uint8_t m_SysExBuffer[MAX_MIDI_MESSAGE]; + size_t m_SysExLen = 0; + bool m_SysExActive = false; }; #endif diff --git a/src/udpmididevice.cpp b/src/udpmididevice.cpp index 000bc3d..57b87c3 100644 --- a/src/udpmididevice.cpp +++ b/src/udpmididevice.cpp @@ -80,7 +80,25 @@ boolean CUDPMIDIDevice::Initialize (void) void CUDPMIDIDevice::OnAppleMIDIDataReceived(const u8* pData, size_t nSize) { - MIDIMessageHandler(pData, nSize, VIRTUALCABLE); + for (size_t i = 0; i < nSize; ++i) { + u8 byte = pData[i]; + if (byte == 0xF0 && !m_SysExActive) { + m_SysExActive = true; + m_SysExLen = 0; + } + if (m_SysExActive) { + if (m_SysExLen < MAX_MIDI_MESSAGE) { + m_SysExBuffer[m_SysExLen++] = byte; + } + if (byte == 0xF7 || m_SysExLen >= MAX_MIDI_MESSAGE) { + MIDIMessageHandler(m_SysExBuffer, m_SysExLen, VIRTUALCABLE); + m_SysExActive = false; + m_SysExLen = 0; + } + continue; + } + MIDIMessageHandler(&byte, 1, VIRTUALCABLE); + } } void CUDPMIDIDevice::OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) @@ -95,7 +113,25 @@ void CUDPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const c void CUDPMIDIDevice::OnUDPMIDIDataReceived(const u8* pData, size_t nSize) { - MIDIMessageHandler(pData, nSize, VIRTUALCABLE); + for (size_t i = 0; i < nSize; ++i) { + u8 byte = pData[i]; + if (byte == 0xF0 && !m_SysExActive) { + m_SysExActive = true; + m_SysExLen = 0; + } + if (m_SysExActive) { + if (m_SysExLen < MAX_MIDI_MESSAGE) { + m_SysExBuffer[m_SysExLen++] = byte; + } + if (byte == 0xF7 || m_SysExLen >= MAX_MIDI_MESSAGE) { + MIDIMessageHandler(m_SysExBuffer, m_SysExLen, VIRTUALCABLE); + m_SysExActive = false; + m_SysExLen = 0; + } + continue; + } + MIDIMessageHandler(&byte, 1, VIRTUALCABLE); + } } void CUDPMIDIDevice::Send(const u8 *pMessage, size_t nLength, unsigned nCable) @@ -103,14 +139,14 @@ void CUDPMIDIDevice::Send(const u8 *pMessage, size_t nLength, unsigned nCable) bool sentRTP = false; if (m_pAppleMIDIParticipant && m_pAppleMIDIParticipant->SendMIDIToHost(pMessage, nLength)) { sentRTP = true; - LOGNOTE("Sent %zu bytes to RTP-MIDI host", nLength); + LOGNOTE("Sent %u bytes to RTP-MIDI host", nLength); } if (!sentRTP && m_pUDPSendSocket) { int res = m_pUDPSendSocket->SendTo(pMessage, nLength, 0, m_UDPDestAddress, m_UDPDestPort); if (res < 0) { - LOGERR("Failed to send %zu bytes to UDP MIDI host", nLength); + LOGERR("Failed to send %u bytes to UDP MIDI host", nLength); } else { - LOGNOTE("Sent %zu bytes to UDP MIDI host (broadcast)", nLength); + LOGNOTE("Sent %u bytes to UDP MIDI host (broadcast)", nLength); } } } \ No newline at end of file diff --git a/src/udpmididevice.h b/src/udpmididevice.h index 9895c9d..3c399f8 100644 --- a/src/udpmididevice.h +++ b/src/udpmididevice.h @@ -57,6 +57,11 @@ private: unsigned m_UDPDestPort = 1999; CIPAddress m_LastUDPSenderAddress; unsigned m_LastUDPSenderPort = 0; + + // SysEx reassembly buffer for UDP/AppleMIDI + uint8_t m_SysExBuffer[MAX_MIDI_MESSAGE]; + size_t m_SysExLen = 0; + bool m_SysExActive = false; }; #endif