From 753c205991200c9e3757e91166509bcd00863d34 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 16 Jan 2024 19:11:03 +0100 Subject: [PATCH 01/61] Update getsysex.sh (#602) Counting Bank voices from 1, to make use of bank 000000_rom3a.syx, as banks are indexed from 1 https://github.com/probonopd/MiniDexed/issues/599 Co-authored-by: Gintaras Valatka --- getsysex.sh | 54 ++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/getsysex.sh b/getsysex.sh index fa73153..30e5aa5 100755 --- a/getsysex.sh +++ b/getsysex.sh @@ -11,33 +11,33 @@ DIR="https://yamahablackboxes.com/patches/dx7/factory" # wget -c "${DIR}"/rom1b.syx -O sysex/voice/000001_rom1b.syx # wget -c "${DIR}"/rom2a.syx -O sysex/voice/000002_rom2a.syx # wget -c "${DIR}"/rom2b.syx -O sysex/voice/000003_rom2b.syx -wget -c "${DIR}"/rom3a.syx -O sysex/voice/000000_rom3a.syx -wget -c "${DIR}"/rom3b.syx -O sysex/voice/000001_rom3b.syx -wget -c "${DIR}"/rom4a.syx -O sysex/voice/000002_rom4a.syx -wget -c "${DIR}"/rom4b.syx -O sysex/voice/000003_rom4b.syx +wget -c "${DIR}"/rom3a.syx -O sysex/voice/000001_rom3a.syx +wget -c "${DIR}"/rom3b.syx -O sysex/voice/000002_rom3b.syx +wget -c "${DIR}"/rom4a.syx -O sysex/voice/000003_rom4a.syx +wget -c "${DIR}"/rom4b.syx -O sysex/voice/000004_rom4b.syx DIR="https://yamahablackboxes.com/patches/dx7/vrc" -wget -c "${DIR}"/vrc101b.syx -O sysex/voice/000004_vrc101b.syx -wget -c "${DIR}"/vrc102a.syx -O sysex/voice/000005_vrc102a.syx -wget -c "${DIR}"/vrc102b.syx -O sysex/voice/000006_vrc102b.syx -wget -c "${DIR}"/vrc103a.syx -O sysex/voice/000007_vrc103a.syx -wget -c "${DIR}"/vrc103b.syx -O sysex/voice/000008_vrc103b.syx -wget -c "${DIR}"/vrc104a.syx -O sysex/voice/000009_vrc104a.syx -wget -c "${DIR}"/vrc104b.syx -O sysex/voice/000010_vrc104b.syx -wget -c "${DIR}"/vrc105a.syx -O sysex/voice/000011_vrc105a.syx -wget -c "${DIR}"/vrc105b.syx -O sysex/voice/000012_vrc105b.syx -wget -c "${DIR}"/vrc106a.syx -O sysex/voice/000013_vrc106a.syx -wget -c "${DIR}"/vrc106b.syx -O sysex/voice/000014_vrc106b.syx -wget -c "${DIR}"/vrc107a.syx -O sysex/voice/000015_vrc107a.syx -wget -c "${DIR}"/vrc107b.syx -O sysex/voice/000016_vrc107b.syx -wget -c "${DIR}"/vrc108a.syx -O sysex/voice/000017_vrc108a.syx -wget -c "${DIR}"/vrc108b.syx -O sysex/voice/000018_vrc108b.syx -wget -c "${DIR}"/vrc109a.syx -O sysex/voice/000019_vrc109a.syx -wget -c "${DIR}"/vrc109b.syx -O sysex/voice/000020_vrc109b.syx -wget -c "${DIR}"/vrc110a.syx -O sysex/voice/000021_vrc110a.syx -wget -c "${DIR}"/vrc110b.syx -O sysex/voice/000022_vrc110b.syx -wget -c "${DIR}"/vrc111a.syx -O sysex/voice/000023_vrc111a.syx -wget -c "${DIR}"/vrc111b.syx -O sysex/voice/000024_vrc111b.syx -wget -c "${DIR}"/vrc112a.syx -O sysex/voice/000025_vrc112a.syx -wget -c "${DIR}"/vrc112b.syx -O sysex/voice/000026_vrc112b.syx +wget -c "${DIR}"/vrc101b.syx -O sysex/voice/000005_vrc101b.syx +wget -c "${DIR}"/vrc102a.syx -O sysex/voice/000006_vrc102a.syx +wget -c "${DIR}"/vrc102b.syx -O sysex/voice/000007_vrc102b.syx +wget -c "${DIR}"/vrc103a.syx -O sysex/voice/000008_vrc103a.syx +wget -c "${DIR}"/vrc103b.syx -O sysex/voice/000009_vrc103b.syx +wget -c "${DIR}"/vrc104a.syx -O sysex/voice/000010_vrc104a.syx +wget -c "${DIR}"/vrc104b.syx -O sysex/voice/000011_vrc104b.syx +wget -c "${DIR}"/vrc105a.syx -O sysex/voice/000012_vrc105a.syx +wget -c "${DIR}"/vrc105b.syx -O sysex/voice/000013_vrc105b.syx +wget -c "${DIR}"/vrc106a.syx -O sysex/voice/000014_vrc106a.syx +wget -c "${DIR}"/vrc106b.syx -O sysex/voice/000015_vrc106b.syx +wget -c "${DIR}"/vrc107a.syx -O sysex/voice/000016_vrc107a.syx +wget -c "${DIR}"/vrc107b.syx -O sysex/voice/000017_vrc107b.syx +wget -c "${DIR}"/vrc108a.syx -O sysex/voice/000018_vrc108a.syx +wget -c "${DIR}"/vrc108b.syx -O sysex/voice/000019_vrc108b.syx +wget -c "${DIR}"/vrc109a.syx -O sysex/voice/000020_vrc109a.syx +wget -c "${DIR}"/vrc109b.syx -O sysex/voice/000021_vrc109b.syx +wget -c "${DIR}"/vrc110a.syx -O sysex/voice/000022_vrc110a.syx +wget -c "${DIR}"/vrc110b.syx -O sysex/voice/000023_vrc110b.syx +wget -c "${DIR}"/vrc111a.syx -O sysex/voice/000024_vrc111a.syx +wget -c "${DIR}"/vrc111b.syx -O sysex/voice/000025_vrc111b.syx +wget -c "${DIR}"/vrc112a.syx -O sysex/voice/000026_vrc112a.syx +wget -c "${DIR}"/vrc112b.syx -O sysex/voice/000027_vrc112b.syx From 4755c5d861d837c68866f98efa46a36fa1dc28a3 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 27 Jan 2024 17:52:22 +0000 Subject: [PATCH 02/61] Performance file handling (#581) Implements #580 * Initial update in performance file handling. This change makes the 6-digit number in the filename indicate a performance "voice number" in MiniDexed. The external filename numbers will now match any Program Change messages using the common MIDI concept of user selecting 1..128 whilst internally they are treated as 0..127. Note: in the case of performances, performance 1 (index 0) is the Default "performance.ini" file for backwards compatibility. Also note that in this version, new performances, when saved, cannot occupy free slots between other performances - they are added to the end. Even though the filename standard gives 6 digit numbers, the actual number of performances is still limited to 256. * Start of subdirectory implementation for performance banks. * Initial version with performance banks, selectable over MIDI only. * Initial implementation of performance bank switching in the UI menu. * Remove debug information, fix few bugs, including PgmUpDown handling and performance numbers out of range. * Bugfixes for legacy cases when no performance directory exists plus some extra checks for saving and deleting performances. * Remove verbose debug options (doh!) * Fix a minor off-by-one error found in review. * Bugfix - removed redundant legacy check that results in out of order performance files being skipped on load. * Fix bug in MIDI button handling commands. * Fix for issue where wrong performance is selected [L] on new save. * Suggested update to UI to show bank/performance numbers. * Make performance bank select asynchronous to MIDI and UI to stop corruptions on loading performances. * Fix an assert that should be a run-time test. * Ensure bank selection works when PCCH is not enabled, and that UI remains consistent when changing banks. --------- Co-authored-by: Kevin <68612569+diyelectromusic@users.noreply.github.com> --- src/mididevice.cpp | 28 +- src/minidexed.cpp | 169 ++++++++++-- src/minidexed.h | 21 ++ src/performanceconfig.cpp | 533 ++++++++++++++++++++++++++++++++------ src/performanceconfig.h | 33 ++- src/uimenu.cpp | 186 +++++++++++-- src/uimenu.h | 2 + src/userinterface.cpp | 3 + 8 files changed, 842 insertions(+), 133 deletions(-) diff --git a/src/mididevice.cpp b/src/mididevice.cpp index 5f231d6..81314ba 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -184,9 +184,35 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign else { // Perform any MiniDexed level MIDI handling before specific Tone Generators + unsigned nPerfCh = m_pSynthesizer->GetPerformanceSelectChannel(); switch (ucType) { case MIDI_CONTROL_CHANGE: + // Check for performance PC messages + if (nPerfCh != Disabled) + { + if ((ucChannel == nPerfCh) || (nPerfCh == OmniMode)) + { + if (pMessage[1] == MIDI_CC_BANK_SELECT_MSB) + { + m_pSynthesizer->BankSelectMSBPerformance (pMessage[2]); + } + else if (pMessage[1] == MIDI_CC_BANK_SELECT_LSB) + { + m_pSynthesizer->BankSelectLSBPerformance (pMessage[2]); + } + else + { + // Ignore any other CC messages at this time + } + } + } + if (nLength == 3) + { + m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]); + } + break; + case MIDI_NOTE_OFF: case MIDI_NOTE_ON: if (nLength < 3) @@ -195,11 +221,11 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign } m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]); break; + case MIDI_PROGRAM_CHANGE: // Check for performance PC messages if( m_pConfig->GetMIDIRXProgramChange() ) { - unsigned nPerfCh = m_pSynthesizer->GetPerformanceSelectChannel(); if( nPerfCh != Disabled) { if ((ucChannel == nPerfCh) || (nPerfCh == OmniMode)) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 71b7247..968320a 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -53,8 +53,11 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_bSavePerformance (false), m_bSavePerformanceNewFile (false), m_bSetNewPerformance (false), + m_bSetNewPerformanceBank (false), + m_bSetFirstPerformance (false), m_bDeletePerformance (false), - m_bLoadPerformanceBusy(false) + m_bLoadPerformanceBusy(false), + m_bLoadPerformanceBankBusy(false) { assert (m_pConfig); @@ -170,6 +173,8 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, SetParameter (ParameterCompressorEnable, 1); SetPerformanceSelectChannel(m_pConfig->GetPerformanceSelectChannel()); + + SetParameter (ParameterPerformanceBank, 0); }; bool CMiniDexed::Initialize (void) @@ -227,6 +232,7 @@ bool CMiniDexed::Initialize (void) reverb_send_mixer->gain(i,mapfloat(m_nReverbSend[i],0,99,0.0f,1.0f)); } + m_PerformanceConfig.Init(); if (m_PerformanceConfig.Load ()) { LoadPerformanceParameters(); @@ -236,12 +242,6 @@ bool CMiniDexed::Initialize (void) SetMIDIChannel (CMIDIDevice::OmniMode, 0); } - // load performances file list, and attempt to create the performance folder - if (!m_PerformanceConfig.ListPerformances()) - { - LOGERR ("Cannot create internal Performance folder, new performances can't be created"); - } - // setup and start the sound device if (!m_pSoundDevice->AllocateQueueFrames (m_pConfig->GetChunkSize ())) { @@ -305,14 +305,30 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) m_bSavePerformanceNewFile = false; } - if (m_bSetNewPerformance && !m_bLoadPerformanceBusy) + if (m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy) + { + DoSetNewPerformanceBank (); + if (m_nSetNewPerformanceBankID == GetActualPerformanceBankID()) + { + m_bSetNewPerformanceBank = false; + } + + // If there is no pending SetNewPerformance already, then see if we need to find the first performance to load + // NB: If called from the UI, then there will not be a SetNewPerformance, so load the first existing one. + // If called from MIDI, there will probably be a SetNewPerformance alongside the Bank select. + if (!m_bSetNewPerformance && m_bSetFirstPerformance) + { + DoSetFirstPerformance(); + } + } + + if (m_bSetNewPerformance && !m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy) { DoSetNewPerformance (); if (m_nSetNewPerformanceID == GetActualPerformanceID()) { m_bSetNewPerformance = false; } - } if(m_bDeletePerformance) @@ -392,6 +408,11 @@ CSysExFileLoader *CMiniDexed::GetSysExFileLoader (void) return &m_SysExFileLoader; } +CPerformanceConfig *CMiniDexed::GetPerformanceConfig (void) +{ + return &m_PerformanceConfig; +} + void CMiniDexed::BankSelect (unsigned nBank, unsigned nTG) { nBank=constrain((int)nBank,0,16383); @@ -407,6 +428,20 @@ void CMiniDexed::BankSelect (unsigned nBank, unsigned nTG) } } +void CMiniDexed::BankSelectPerformance (unsigned nBank) +{ + nBank=constrain((int)nBank,0,16383); + + if (GetPerformanceConfig ()->IsValidPerformanceBank(nBank)) + { + // Only change if we have the bank loaded + m_nVoiceBankIDPerformance = nBank; + SetNewPerformanceBank (nBank); + + m_UI.ParameterChanged (); + } +} + void CMiniDexed::BankSelectMSB (unsigned nBankMSB, unsigned nTG) { nBankMSB=constrain((int)nBankMSB,0,127); @@ -422,6 +457,12 @@ void CMiniDexed::BankSelectMSB (unsigned nBankMSB, unsigned nTG) m_nVoiceBankIDMSB[nTG] = nBankMSB; } +void CMiniDexed::BankSelectMSBPerformance (unsigned nBankMSB) +{ + nBankMSB=constrain((int)nBankMSB,0,127); + m_nVoiceBankIDMSBPerformance = nBankMSB; +} + void CMiniDexed::BankSelectLSB (unsigned nBankLSB, unsigned nTG) { nBankLSB=constrain((int)nBankLSB,0,127); @@ -435,6 +476,18 @@ void CMiniDexed::BankSelectLSB (unsigned nBankLSB, unsigned nTG) BankSelect(nBank, nTG); } +void CMiniDexed::BankSelectLSBPerformance (unsigned nBankLSB) +{ + nBankLSB=constrain((int)nBankLSB,0,127); + + unsigned nBank = m_nVoiceBankIDPerformance; + unsigned nBankMSB = m_nVoiceBankIDMSBPerformance; + nBank = (nBankMSB << 7) + nBankLSB; + + // Now should have both MSB and LSB so enable the BankSelect + BankSelectPerformance(nBank); +} + void CMiniDexed::ProgramChange (unsigned nProgram, unsigned nTG) { assert (m_pConfig); @@ -489,10 +542,7 @@ void CMiniDexed::ProgramChangePerformance (unsigned nProgram) if (m_nParameter[ParameterPerformanceSelectChannel] != CMIDIDevice::Disabled) { // Program Change messages change Performances. - unsigned nLastPerformance = m_PerformanceConfig.GetLastPerformance(); - - // GetLastPerformance actually returns 1-indexed, number of performances - if (nProgram < nLastPerformance - 1) + if (m_PerformanceConfig.IsValidPerformance(nProgram)) { SetNewPerformance(nProgram); } @@ -800,6 +850,10 @@ void CMiniDexed::SetParameter (TParameter Parameter, int nValue) // Nothing more to do break; + case ParameterPerformanceBank: + BankSelectPerformance(nValue); + break; + default: assert (0); break; @@ -1181,10 +1235,17 @@ void CMiniDexed::SetPerformanceSelectChannel (unsigned uCh) bool CMiniDexed::SavePerformance (bool bSaveAsDeault) { - m_bSavePerformance = true; - m_bSaveAsDeault=bSaveAsDeault; + if (m_PerformanceConfig.GetInternalFolderOk()) + { + m_bSavePerformance = true; + m_bSaveAsDeault=bSaveAsDeault; - return true; + return true; + } + else + { + return false; + } } bool CMiniDexed::DoSavePerformance (void) @@ -1501,6 +1562,16 @@ unsigned CMiniDexed::GetLastPerformance() return m_PerformanceConfig.GetLastPerformance(); } +unsigned CMiniDexed::GetPerformanceBank() +{ + return m_PerformanceConfig.GetPerformanceBank(); +} + +unsigned CMiniDexed::GetLastPerformanceBank() +{ + return m_PerformanceConfig.GetLastPerformanceBank(); +} + unsigned CMiniDexed::GetActualPerformanceID() { return m_PerformanceConfig.GetActualPerformanceID(); @@ -1511,6 +1582,16 @@ void CMiniDexed::SetActualPerformanceID(unsigned nID) m_PerformanceConfig.SetActualPerformanceID(nID); } +unsigned CMiniDexed::GetActualPerformanceBankID() +{ + return m_PerformanceConfig.GetActualPerformanceBankID(); +} + +void CMiniDexed::SetActualPerformanceBankID(unsigned nBankID) +{ + m_PerformanceConfig.SetActualPerformanceBankID(nBankID); +} + bool CMiniDexed::SetNewPerformance(unsigned nID) { m_bSetNewPerformance = true; @@ -1519,6 +1600,20 @@ bool CMiniDexed::SetNewPerformance(unsigned nID) return true; } +bool CMiniDexed::SetNewPerformanceBank(unsigned nBankID) +{ + m_bSetNewPerformanceBank = true; + m_nSetNewPerformanceBankID = nBankID; + + return true; +} + +void CMiniDexed::SetFirstPerformance(void) +{ + m_bSetFirstPerformance = true; + return; +} + bool CMiniDexed::DoSetNewPerformance (void) { m_bLoadPerformanceBusy = true; @@ -1540,6 +1635,25 @@ bool CMiniDexed::DoSetNewPerformance (void) } } +bool CMiniDexed::DoSetNewPerformanceBank (void) +{ + m_bLoadPerformanceBankBusy = true; + + unsigned nBankID = m_nSetNewPerformanceBankID; + m_PerformanceConfig.SetNewPerformanceBank(nBankID); + + m_bLoadPerformanceBankBusy = false; + return true; +} + +void CMiniDexed::DoSetFirstPerformance(void) +{ + unsigned nID = m_PerformanceConfig.FindFirstPerformance(); + SetNewPerformance(nID); + m_bSetFirstPerformance = false; + return; +} + bool CMiniDexed::SavePerformanceNewFile () { m_bSavePerformanceNewFile = m_PerformanceConfig.GetInternalFolderOk() && m_PerformanceConfig.CheckFreePerformanceSlot(); @@ -1631,6 +1745,16 @@ void CMiniDexed::SetNewPerformanceName(std::string nName) m_PerformanceConfig.SetNewPerformanceName(nName); } +bool CMiniDexed::IsValidPerformance(unsigned nID) +{ + return m_PerformanceConfig.IsValidPerformance(nID); +} + +bool CMiniDexed::IsValidPerformanceBank(unsigned nBankID) +{ + return m_PerformanceConfig.IsValidPerformanceBank(nBankID); +} + void CMiniDexed::SetVoiceName (std::string VoiceName, unsigned nTG) { assert (nTG < CConfig::ToneGenerators); @@ -1642,10 +1766,17 @@ void CMiniDexed::SetVoiceName (std::string VoiceName, unsigned nTG) bool CMiniDexed::DeletePerformance(unsigned nID) { - m_bDeletePerformance = true; - m_nDeletePerformanceID = nID; + if (m_PerformanceConfig.IsValidPerformance(nID) && m_PerformanceConfig.GetInternalFolderOk()) + { + m_bDeletePerformance = true; + m_nDeletePerformanceID = nID; - return true; + return true; + } + else + { + return false; + } } bool CMiniDexed::DoDeletePerformance(void) diff --git a/src/minidexed.h b/src/minidexed.h index 1aa3096..407d5db 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -62,10 +62,14 @@ public: #endif CSysExFileLoader *GetSysExFileLoader (void); + CPerformanceConfig *GetPerformanceConfig (void); void BankSelect (unsigned nBank, unsigned nTG); + void BankSelectPerformance (unsigned nBank); void BankSelectMSB (unsigned nBankMSB, unsigned nTG); + void BankSelectMSBPerformance (unsigned nBankMSB); void BankSelectLSB (unsigned nBankLSB, unsigned nTG); + void BankSelectLSBPerformance (unsigned nBankLSB); void ProgramChange (unsigned nProgram, unsigned nTG); void ProgramChangePerformance (unsigned nProgram); void SetVolume (unsigned nVolume, unsigned nTG); @@ -117,17 +121,27 @@ public: std::string GetPerformanceFileName(unsigned nID); std::string GetPerformanceName(unsigned nID); unsigned GetLastPerformance(); + unsigned GetPerformanceBank(); + unsigned GetLastPerformanceBank(); unsigned GetActualPerformanceID(); void SetActualPerformanceID(unsigned nID); + unsigned GetActualPerformanceBankID(); + void SetActualPerformanceBankID(unsigned nBankID); bool SetNewPerformance(unsigned nID); + bool SetNewPerformanceBank(unsigned nBankID); + void SetFirstPerformance(void); + void DoSetFirstPerformance(void); bool SavePerformanceNewFile (); bool DoSavePerformanceNewFile (void); bool DoSetNewPerformance (void); + bool DoSetNewPerformanceBank (void); bool GetPerformanceSelectToLoad(void); bool SavePerformance (bool bSaveAsDeault); unsigned GetPerformanceSelectChannel (void); void SetPerformanceSelectChannel (unsigned uCh); + bool IsValidPerformance(unsigned nID); + bool IsValidPerformanceBank(unsigned nBankID); // Must match the order in CUIMenu::TParameter enum TParameter @@ -141,6 +155,7 @@ public: ParameterReverbDiffusion, ParameterReverbLevel, ParameterPerformanceSelectChannel, + ParameterPerformanceBank, ParameterUnknown }; @@ -238,6 +253,8 @@ private: unsigned m_nVoiceBankID[CConfig::ToneGenerators]; unsigned m_nVoiceBankIDMSB[CConfig::ToneGenerators]; + unsigned m_nVoiceBankIDPerformance; + unsigned m_nVoiceBankIDMSBPerformance; unsigned m_nProgram[CConfig::ToneGenerators]; unsigned m_nVolume[CConfig::ToneGenerators]; unsigned m_nPan[CConfig::ToneGenerators]; @@ -305,9 +322,13 @@ private: bool m_bSavePerformanceNewFile; bool m_bSetNewPerformance; unsigned m_nSetNewPerformanceID; + bool m_bSetNewPerformanceBank; + unsigned m_nSetNewPerformanceBankID; + bool m_bSetFirstPerformance; bool m_bDeletePerformance; unsigned m_nDeletePerformanceID; bool m_bLoadPerformanceBusy; + bool m_bLoadPerformanceBankBusy; bool m_bSaveAsDeault; }; diff --git a/src/performanceconfig.cpp b/src/performanceconfig.cpp index 853ccf5..8cd6275 100644 --- a/src/performanceconfig.cpp +++ b/src/performanceconfig.cpp @@ -28,8 +28,15 @@ LOGMODULE ("Performance"); +//#define VERBOSE_DEBUG + +#define PERFORMANCE_DIR "performance" +#define DEFAULT_PERFORMANCE_FILENAME "performance.ini" +#define DEFAULT_PERFORMANCE_NAME "Default" +#define DEFAULT_PERFORMANCE_BANK_NAME "Default" + CPerformanceConfig::CPerformanceConfig (FATFS *pFileSystem) -: m_Properties ("performance.ini", pFileSystem) +: m_Properties (DEFAULT_PERFORMANCE_FILENAME, pFileSystem) { m_pFileSystem = pFileSystem; } @@ -38,6 +45,47 @@ CPerformanceConfig::~CPerformanceConfig (void) { } +bool CPerformanceConfig::Init (void) +{ + // Check intermal performance directory exists + DIR Directory; + FRESULT Result; + //Check if internal "performance" directory exists + Result = f_opendir (&Directory, "SD:/" PERFORMANCE_DIR); + if (Result == FR_OK) + { + m_bPerformanceDirectoryExists=true; + Result = f_closedir (&Directory); + } + else + { + m_bPerformanceDirectoryExists = false; + } + + // List banks if present + ListPerformanceBanks(); + +#ifdef VERBOSE_DEBUG +#warning "PerformanceConfig in verbose debug printing mode" + LOGNOTE("Testing loading of banks"); + for (unsigned i=0; i= NUM_PERFORMANCES) { + if (!m_bPerformanceDirectoryExists) + { + // Nothing can be done if there is no performance directory + LOGNOTE("Performance directory does not exist"); + return false; + } + if (m_nLastPerformance >= NUM_PERFORMANCES) { // No space left for new performances LOGWARN ("No space left for new performance"); return false; } + + // Note: New performances are created at the end of the currently selected bank. + // Sould we default to a new bank just for user-performances? + // + // There is the possibility of MIDI changing the Bank Number and the user + // not spotting the bank has changed... + // + // Another option would be to find empty slots in the current bank + // rather than always add at the end. + // + // Sorting this out is left for a future update. std::string sPerformanceName = NewPerformanceName; NewPerformanceName=""; - nActualPerformance=nLastPerformance; + unsigned nNewPerformance = m_nLastPerformance + 1; std::string nFileName; std::string nPath; std::string nIndex = "000000"; - nIndex += std::to_string(++nLastFileIndex); + nIndex += std::to_string(nNewPerformance+1); // Index on disk = index in memory+1 nIndex = nIndex.substr(nIndex.length()-6,6); @@ -793,10 +930,11 @@ bool CPerformanceConfig::CreateNewPerformanceFile(void) nFileName +=sPerformanceName.substr(0,14); } nFileName += ".ini"; - m_nPerformanceFileName[nLastPerformance]= nFileName; + m_PerformanceFileName[nNewPerformance]= sPerformanceName; nPath = "SD:/" ; nPath += PERFORMANCE_DIR; + nPath += AddPerformanceBankDirName(m_nPerformanceBank); nPath += "/"; nFileName = nPath + nFileName; @@ -804,17 +942,18 @@ bool CPerformanceConfig::CreateNewPerformanceFile(void) FRESULT Result = f_open (&File, nFileName.c_str(), FA_WRITE | FA_CREATE_ALWAYS); if (Result != FR_OK) { - m_nPerformanceFileName[nLastPerformance]=nullptr; + m_PerformanceFileName[nNewPerformance]=nullptr; return false; } if (f_close (&File) != FR_OK) { - m_nPerformanceFileName[nLastPerformance]=nullptr; + m_PerformanceFileName[nNewPerformance]=nullptr; return false; } - nLastPerformance++; + m_nLastPerformance = nNewPerformance; + m_nActualPerformance = nNewPerformance; new (&m_Properties) CPropertiesFatFsFile(nFileName.c_str(), m_pFileSystem); return true; @@ -822,88 +961,124 @@ bool CPerformanceConfig::CreateNewPerformanceFile(void) bool CPerformanceConfig::ListPerformances() { - nInternalFolderOk=false; - nExternalFolderOk=false; // for future USB implementation - nLastPerformance=0; - nLastFileIndex=0; - m_nPerformanceFileName[nLastPerformance++]="performance.ini"; // in order to assure retrocompatibility - - unsigned nPIndex; - DIR Directory; - FILINFO FileInfo; - FRESULT Result; - //Check if internal "performance" directory exists - Result = f_opendir (&Directory, "SD:/" PERFORMANCE_DIR); - if (Result == FR_OK) + // Clear any existing lists of performances + for (unsigned i=0; i= NUM_PERFORMANCES) { - LOGNOTE ("Skipping performance %s", FileInfo.fname); - } else { - if (!(FileInfo.fattrib & (AM_HID | AM_SYS))) + if (!(FileInfo.fattrib & (AM_HID | AM_SYS))) + { + std::string OriFileName = FileInfo.fname; + size_t nLen = OriFileName.length(); + if ( nLen > 8 && nLen <26 && strcmp(OriFileName.substr(6,1).c_str(), "_")==0) { - std::string FileName = FileInfo.fname; - size_t nLen = FileName.length(); - if ( nLen > 8 && nLen <26 && strcmp(FileName.substr(6,1).c_str(), "_")==0) - { - nPIndex=stoi(FileName.substr(0,6)); - if(nPIndex > nLastFileIndex) + // Note: m_nLastPerformance - refers to the number (index) of the last performance in memory, + // which includes a default performance. + // + // Filenames on the disk start from 1 to match what the user might see in MIDI. + // So this means that actually file 000001_ will correspond to index position [0]. + // For the default bank though, ID 1 is the default performance, so will already exist. + // m_PerformanceFileName[0] = default performance (file 000001) + // m_PerformanceFileName[1] = first available on-disk performance (file 000002) + // + // Note2: filenames assume 6 digits, underscore, name, finally ".ini" + // i.e. 123456_Performance Name.ini + // + nPIndex=stoi(OriFileName.substr(0,6)); + if ((nPIndex < 1) || (nPIndex >= (NUM_PERFORMANCES+1))) + { + // Index is out of range - skip to next file + LOGNOTE ("Performance number out of range: %s (%d to %d)", FileInfo.fname, 1, NUM_PERFORMANCES); + } + else + { + // Convert from "user facing" 1..indexed number to internal 0..indexed + nPIndex = nPIndex-1; + if (m_PerformanceFileName[nPIndex].empty()) { - nLastFileIndex=nPIndex; - } + if(nPIndex > m_nLastPerformance) + { + m_nLastPerformance=nPIndex; + } - m_nPerformanceFileName[nLastPerformance++]= FileName; - } + std::string FileName = OriFileName.substr(0,OriFileName.length()-4).substr(7,14); + + m_PerformanceFileName[nPIndex] = FileName; +#ifdef VERBOSE_DEBUG + LOGNOTE ("Loading performance %s (%d, %s)", OriFileName.c_str(), nPIndex, FileName.c_str()); +#endif + } + else + { + LOGNOTE ("Duplicate performance %s", OriFileName.c_str()); + } + } } } Result = f_findnext (&Directory, &FileInfo); } - // sort by performance number-name - if (nLastPerformance > 2) - { - sort (m_nPerformanceFileName+1, m_nPerformanceFileName + nLastPerformance); // default is always on first place. %%%%%%%%%%%%%%%% - } + f_closedir (&Directory); } - LOGNOTE ("Number of Performances: %d", nLastPerformance); - - return nInternalFolderOk; -} - + return true; +} void CPerformanceConfig::SetNewPerformance (unsigned nID) { - nActualPerformance=nID; - std::string FileN = ""; - if (nID != 0) // in order to assure retrocompatibility + assert (nID < NUM_PERFORMANCES); + m_nActualPerformance=nID; + std::string FileN = GetPerformanceFullFilePath(nID); + + new (&m_Properties) CPropertiesFatFsFile(FileN.c_str(), m_pFileSystem); +#ifdef VERBOSE_DEBUG + LOGNOTE("Selecting Performance: %d (%s)", nID+1, FileN.c_str()); +#endif +} + +unsigned CPerformanceConfig::FindFirstPerformance (void) +{ + for (int nID=0; nID < NUM_PERFORMANCES; nID++) + { + if (IsValidPerformance(nID)) { - FileN += PERFORMANCE_DIR; - FileN += "/"; + return nID; } - FileN += m_nPerformanceFileName[nID]; - new (&m_Properties) CPropertiesFatFsFile(FileN.c_str(), m_pFileSystem); - + } + + return 0; // Even though 0 is a valid performance, not much else to do } std::string CPerformanceConfig::GetNewPerformanceDefaultName(void) { std::string nIndex = "000000"; - nIndex += std::to_string(nLastFileIndex+1); + nIndex += std::to_string(m_nLastPerformance+1+1); // Convert from internal 0.. index to a file-based 1.. index to show the user nIndex = nIndex.substr(nIndex.length()-6,6); return "Perf" + nIndex; } @@ -923,31 +1098,229 @@ void CPerformanceConfig::SetNewPerformanceName(std::string nName) bool CPerformanceConfig::DeletePerformance(unsigned nID) { + if (!m_bPerformanceDirectoryExists) + { + // Nothing can be done if there is no performance directory + LOGNOTE("Performance directory does not exist"); + return false; + } bool bOK = false; - if(nID == 0){return bOK;} // default (performance.ini at root directory) can't be deleted + if((m_nPerformanceBank == 0) && (nID == 0)){return bOK;} // default (performance.ini at root directory) can't be deleted DIR Directory; FILINFO FileInfo; std::string FileN = "SD:/"; FileN += PERFORMANCE_DIR; - + FileN += AddPerformanceBankDirName(m_nPerformanceBank); - FRESULT Result = f_findfirst (&Directory, &FileInfo, FileN.c_str(), m_nPerformanceFileName[nID].c_str()); + FRESULT Result = f_findfirst (&Directory, &FileInfo, FileN.c_str(), GetPerformanceFileName(nID).c_str()); if (Result == FR_OK && FileInfo.fname[0]) { FileN += "/"; - FileN += m_nPerformanceFileName[nID]; + FileN += GetPerformanceFileName(nID); Result=f_unlink (FileN.c_str()); if (Result == FR_OK) { SetNewPerformance(0); - nActualPerformance =0; + m_nActualPerformance =0; //nMenuSelectedPerformance=0; - m_nPerformanceFileName[nID]="ZZZZZZ"; - sort (m_nPerformanceFileName+1, m_nPerformanceFileName + nLastPerformance); // test si va con -1 o no - --nLastPerformance; - m_nPerformanceFileName[nLastPerformance]=nullptr; + m_PerformanceFileName[nID].clear(); + // If this was the last performance in the bank... + if (nID == m_nLastPerformance) + { + do + { + // Find the new last performance + m_nLastPerformance--; + } while (!IsValidPerformance(m_nLastPerformance) && (m_nLastPerformance > 0)); + } bOK=true; } + else + { + LOGNOTE ("Failed to delete %s", FileN.c_str()); + } } return bOK; } + +bool CPerformanceConfig::ListPerformanceBanks() +{ + m_nPerformanceBank = 0; + m_nLastPerformance = 0; + m_nLastPerformanceBank = 0; + + // Open performance directory + DIR Directory; + FILINFO FileInfo; + FRESULT Result; + Result = f_opendir (&Directory, "SD:/" PERFORMANCE_DIR); + if (Result != FR_OK) + { + // No performance directory, so no performance banks. + // So nothing else to do here + LOGNOTE ("No performance banks detected"); + m_bPerformanceDirectoryExists = false; + return false; + } + + unsigned nNumBanks = 0; + + // Add in the default performance directory as the first bank + m_PerformanceBankName[0] = DEFAULT_PERFORMANCE_BANK_NAME; + nNumBanks = 1; + m_nLastPerformanceBank = 0; + + // List directories with names in format 01_Perf Bank Name + Result = f_findfirst (&Directory, &FileInfo, "SD:/" PERFORMANCE_DIR, "*"); + for (unsigned i = 0; Result == FR_OK && FileInfo.fname[0]; i++) + { + // Check to see if it is a directory + if ((FileInfo.fattrib & AM_DIR) != 0) + { + // Looking for Performance banks of the form: 01_Perf Bank Name + // So positions 0,1,2 = decimal digit + // position 3 = "_" + // positions 4.. = actual name + // + std::string OriFileName = FileInfo.fname; + size_t nLen = OriFileName.length(); + if ( nLen > 4 && nLen <26 && strcmp(OriFileName.substr(3,1).c_str(), "_")==0) + { + unsigned nBankIndex = stoi(OriFileName.substr(0,3)); + // Recall user index numbered 002..NUM_PERFORMANCE_BANKS + // NB: Bank 001 is reserved for the default performance directory + if ((nBankIndex > 0) && (nBankIndex <= NUM_PERFORMANCE_BANKS)) + { + // Convert from "user facing" 1..indexed number to internal 0..indexed + nBankIndex = nBankIndex-1; + if (m_PerformanceBankName[nBankIndex].empty()) + { + std::string BankName = OriFileName.substr(4,nLen); + + m_PerformanceBankName[nBankIndex] = BankName; +#ifdef VERBOSE_DEBUG + LOGNOTE ("Found performance bank %s (%d, %s)", OriFileName.c_str(), nBankIndex, BankName.c_str()); +#endif + nNumBanks++; + if (nBankIndex > m_nLastPerformanceBank) + { + m_nLastPerformanceBank = nBankIndex; + } + } + else + { + LOGNOTE ("Duplicate Performance Bank: %s", FileInfo.fname); + if (nBankIndex==0) + { + LOGNOTE ("(Bank 001 is the default performance directory)"); + } + } + } + else + { + LOGNOTE ("Performance Bank number out of range: %s (%d to %d)", FileInfo.fname, 1, NUM_PERFORMANCE_BANKS); + } + } + else + { +#ifdef VERBOSE_DEBUG + LOGNOTE ("Skipping: %s", FileInfo.fname); +#endif + } + } + + Result = f_findnext (&Directory, &FileInfo); + } + + if (nNumBanks > 0) + { + LOGNOTE ("Number of Performance Banks: %d (last = %d)", nNumBanks, m_nLastPerformanceBank+1); + } + + f_closedir (&Directory); + return true; +} + +void CPerformanceConfig::SetNewPerformanceBank(unsigned nBankID) +{ + assert (nBankID < NUM_PERFORMANCE_BANKS); + if (IsValidPerformanceBank(nBankID)) + { +#ifdef VERBOSE_DEBUG + LOGNOTE("Selecting Performance Bank: %d", nBankID+1); +#endif + m_nPerformanceBank = nBankID; + m_nActualPerformanceBank = nBankID; + ListPerformances(); + } + else + { +#ifdef VERBOSE_DEBUG + LOGNOTE("Not selecting invalid Performance Bank: %d", nBankID+1); +#endif + } +} + +unsigned CPerformanceConfig::GetPerformanceBank(void) +{ + return m_nPerformanceBank; +} + +std::string CPerformanceConfig::GetPerformanceBankName(unsigned nBankID) +{ + assert (nBankID < NUM_PERFORMANCE_BANKS); + if (IsValidPerformanceBank(nBankID)) + { + return m_PerformanceBankName[nBankID]; + } + else + { + return DEFAULT_PERFORMANCE_BANK_NAME; + } +} + +std::string CPerformanceConfig::AddPerformanceBankDirName(unsigned nBankID) +{ + assert (nBankID < NUM_PERFORMANCE_BANKS); + if (IsValidPerformanceBank(nBankID)) + { + // Performance Banks directories in format "001_Bank Name" + std::string Index; + if (nBankID == 0) + { + // Legacy: Bank 1 is the default performance directory + return ""; + } + + if (nBankID < 9) + { + Index = "00" + std::to_string(nBankID+1); + } + else if (nBankID < 99) + { + Index = "0" + std::to_string(nBankID+1); + } + else + { + Index = std::to_string(nBankID+1); + } + + return "/" + Index + "_" + m_PerformanceBankName[nBankID]; + } + else + { + return ""; + } +} + +bool CPerformanceConfig::IsValidPerformanceBank(unsigned nBankID) +{ + if (nBankID >= NUM_PERFORMANCE_BANKS) { + return false; + } + if (m_PerformanceBankName[nBankID].empty()) + { + return false; + } + return true; +} diff --git a/src/performanceconfig.h b/src/performanceconfig.h index c151c7a..0d50daa 100644 --- a/src/performanceconfig.h +++ b/src/performanceconfig.h @@ -27,14 +27,16 @@ #include #include #define NUM_VOICE_PARAM 156 -#define PERFORMANCE_DIR "performance" -#define NUM_PERFORMANCES 256 +#define NUM_PERFORMANCES 128 +#define NUM_PERFORMANCE_BANKS 128 class CPerformanceConfig // Performance configuration { public: CPerformanceConfig (FATFS *pFileSystem); ~CPerformanceConfig (void); + + bool Init (void); bool Load (void); @@ -122,17 +124,30 @@ public: bool ListPerformances(); //std::string m_DirName; void SetNewPerformance (unsigned nID); + unsigned FindFirstPerformance (void); std::string GetPerformanceFileName(unsigned nID); + std::string GetPerformanceFullFilePath(unsigned nID); std::string GetPerformanceName(unsigned nID); unsigned GetLastPerformance(); + unsigned GetLastPerformanceBank(); void SetActualPerformanceID(unsigned nID); unsigned GetActualPerformanceID(); + void SetActualPerformanceBankID(unsigned nBankID); + unsigned GetActualPerformanceBankID(); bool CreateNewPerformanceFile(void); bool GetInternalFolderOk(); std::string GetNewPerformanceDefaultName(void); void SetNewPerformanceName(std::string nName); bool DeletePerformance(unsigned nID); bool CheckFreePerformanceSlot(void); + std::string AddPerformanceBankDirName(unsigned nBankID); + bool IsValidPerformance(unsigned nID); + + bool ListPerformanceBanks(void); + void SetNewPerformanceBank(unsigned nBankID); + unsigned GetPerformanceBank(void); + std::string GetPerformanceBankName(unsigned nBankID); + bool IsValidPerformanceBank(unsigned nBankID); private: CPropertiesFatFsFile m_Properties; @@ -166,15 +181,17 @@ private: unsigned m_nAftertouchRange[CConfig::ToneGenerators]; unsigned m_nAftertouchTarget[CConfig::ToneGenerators]; - unsigned nLastPerformance; - unsigned nLastFileIndex; - unsigned nActualPerformance = 0; + unsigned m_nLastPerformance; + unsigned m_nActualPerformance = 0; + unsigned m_nActualPerformanceBank = 0; + unsigned m_nPerformanceBank; + unsigned m_nLastPerformanceBank; + bool m_bPerformanceDirectoryExists; //unsigned nMenuSelectedPerformance = 0; - std::string m_nPerformanceFileName[NUM_PERFORMANCES]; + std::string m_PerformanceFileName[NUM_PERFORMANCES]; + std::string m_PerformanceBankName[NUM_PERFORMANCE_BANKS]; FATFS *m_pFileSystem; - bool nInternalFolderOk=false; - bool nExternalFolderOk=false; // for future USB implementation std::string NewPerformanceName=""; bool m_bCompressorEnable; diff --git a/src/uimenu.cpp b/src/uimenu.cpp index 93e6b0e..82a426a 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -214,7 +214,8 @@ const CUIMenu::TParameter CUIMenu::s_GlobalParameter[CMiniDexed::ParameterUnknow {0, 99, 1}, // ParameterReverbLowPass {0, 99, 1}, // ParameterReverbDiffusion {0, 99, 1}, // ParameterReverbLevel - {0, CMIDIDevice::ChannelUnknown-1, 1, ToMIDIChannel} // ParameterPerformanceSelectChannel + {0, CMIDIDevice::ChannelUnknown-1, 1, ToMIDIChannel}, // ParameterPerformanceSelectChannel + {0, NUM_PERFORMANCE_BANKS, 1} // ParameterPerformanceBank }; // must match CMiniDexed::TTGParameter @@ -327,6 +328,7 @@ const CUIMenu::TMenuItem CUIMenu::s_PerformanceMenu[] = {"Load", PerformanceMenu, 0, 0}, {"Save", MenuHandler, s_SaveMenu}, {"Delete", PerformanceMenu, 0, 1}, + {"Bank", EditPerformanceBankNumber, 0, 0}, {"PCCH", EditGlobalParameter, 0, CMiniDexed::ParameterPerformanceSelectChannel}, {0} }; @@ -1211,24 +1213,43 @@ void CUIMenu::PgmUpDownHandler (TMenuEvent Event) // Program Up/Down acts on performances unsigned nLastPerformance = m_pMiniDexed->GetLastPerformance(); unsigned nPerformance = m_pMiniDexed->GetActualPerformanceID(); + unsigned nStart = nPerformance; //LOGNOTE("Performance actual=%d, last=%d", nPerformance, nLastPerformance); if (Event == MenuEventPgmDown) { - if (nPerformance > 0) + do { - m_nSelectedPerformanceID = nPerformance-1; - m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID); - //LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance); - } + if (nPerformance == 0) + { + // Wrap around + nPerformance = nLastPerformance; + } + else if (nPerformance > 0) + { + --nPerformance; + } + } while ((m_pMiniDexed->IsValidPerformance(nPerformance) != true) && (nPerformance != nStart)); + m_nSelectedPerformanceID = nPerformance; + m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID); + //LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance); } - else + else // MenuEventPgmUp { - if (nPerformance < nLastPerformance-1) + do { - m_nSelectedPerformanceID = nPerformance+1; - m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID); - //LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance); - } + if (nPerformance == nLastPerformance) + { + // Wrap around + nPerformance = 0; + } + else if (nPerformance < nLastPerformance) + { + ++nPerformance; + } + } while ((m_pMiniDexed->IsValidPerformance(nPerformance) != true) && (nPerformance != nStart)); + m_nSelectedPerformanceID = nPerformance; + m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID); + //LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance); } } else @@ -1366,7 +1387,15 @@ void CUIMenu::TimerHandlerNoBack (TKernelTimerHandle hTimer, void *pParam, void void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event) { bool bPerformanceSelectToLoad = pUIMenu->m_pMiniDexed->GetPerformanceSelectToLoad(); + unsigned nLastPerformance = pUIMenu->m_pMiniDexed->GetLastPerformance(); unsigned nValue = pUIMenu->m_nSelectedPerformanceID; + unsigned nStart = nValue; + if (pUIMenu->m_pMiniDexed->IsValidPerformance(nValue) != true) + { + // A bank change has left the selected performance out of sync + nValue = pUIMenu->m_pMiniDexed->GetActualPerformanceID(); + pUIMenu->m_nSelectedPerformanceID = nValue; + } std::string Value; if (Event == MenuEventUpdate) @@ -1380,17 +1409,25 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event) } if(!pUIMenu->m_bPerformanceDeleteMode) - { + { switch (Event) { case MenuEventUpdate: break; case MenuEventStepDown: - if (nValue > 0) + do { - --nValue; - } + if (nValue == 0) + { + // Wrap around + nValue = nLastPerformance; + } + else if (nValue > 0) + { + --nValue; + } + } while ((pUIMenu->m_pMiniDexed->IsValidPerformance(nValue) != true) && (nValue != nStart)); pUIMenu->m_nSelectedPerformanceID = nValue; if (!bPerformanceSelectToLoad && pUIMenu->m_nCurrentParameter==0) { @@ -1399,10 +1436,18 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event) break; case MenuEventStepUp: - if (++nValue > (unsigned) pUIMenu->m_pMiniDexed->GetLastPerformance()-1) + do { - nValue = pUIMenu->m_pMiniDexed->GetLastPerformance()-1; - } + if (nValue == nLastPerformance) + { + // Wrap around + nValue = 0; + } + else if (nValue < nLastPerformance) + { + ++nValue; + } + } while ((pUIMenu->m_pMiniDexed->IsValidPerformance(nValue) != true) && (nValue != nStart)); pUIMenu->m_nSelectedPerformanceID = nValue; if (!bPerformanceSelectToLoad && pUIMenu->m_nCurrentParameter==0) { @@ -1421,7 +1466,7 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event) break; case 1: - if (pUIMenu->m_nSelectedPerformanceID != 0) + if (pUIMenu->m_pMiniDexed->IsValidPerformance(pUIMenu->m_nSelectedPerformanceID)) { pUIMenu->m_bPerformanceDeleteMode=true; pUIMenu->m_bConfirmDeletePerformance=false; @@ -1474,17 +1519,24 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event) if(!pUIMenu->m_bPerformanceDeleteMode) { Value = pUIMenu->m_pMiniDexed->GetPerformanceName(nValue); + unsigned nBankNum = pUIMenu->m_pMiniDexed->GetPerformanceBank(); - - std::string nPSelected = ""; + std::string nPSelected = "000"; + nPSelected += std::to_string(nBankNum+1); // Convert to user-facing bank number rather than index + nPSelected = nPSelected.substr(nPSelected.length()-3,3); + std::string nPPerf = "000"; + nPPerf += std::to_string(nValue+1); // Convert to user-facing performance number rather than index + nPPerf = nPPerf.substr(nPPerf.length()-3,3); + + nPSelected += ":"+nPPerf; if(nValue == pUIMenu->m_pMiniDexed->GetActualPerformanceID()) { - nPSelected= "[L]"; + nPSelected += " [L]"; } pUIMenu->m_pUI->DisplayWrite (pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, nPSelected.c_str(), - Value.c_str (), - (int) nValue > 0, (int) nValue < (int) pUIMenu->m_pMiniDexed->GetLastPerformance()-1); + Value.c_str (), true, true); +// (int) nValue > 0, (int) nValue < (int) pUIMenu->m_pMiniDexed->GetLastPerformance()); } else { @@ -1492,6 +1544,90 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event) } } +void CUIMenu::EditPerformanceBankNumber (CUIMenu *pUIMenu, TMenuEvent Event) +{ + bool bPerformanceSelectToLoad = pUIMenu->m_pMiniDexed->GetPerformanceSelectToLoad(); + unsigned nLastPerformanceBank = pUIMenu->m_pMiniDexed->GetLastPerformanceBank(); + unsigned nValue = pUIMenu->m_nSelectedPerformanceBankID; + unsigned nStart = nValue; + std::string Value; + + switch (Event) + { + case MenuEventUpdate: + break; + + case MenuEventStepDown: + do + { + if (nValue == 0) + { + // Wrap around + nValue = nLastPerformanceBank; + } + else if (nValue > 0) + { + --nValue; + } + } while ((pUIMenu->m_pMiniDexed->IsValidPerformanceBank(nValue) != true) && (nValue != nStart)); + pUIMenu->m_nSelectedPerformanceBankID = nValue; + if (!bPerformanceSelectToLoad) + { + // Switch to the new bank and select the first performance voice + pUIMenu->m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nValue); + pUIMenu->m_pMiniDexed->SetFirstPerformance(); + } + break; + + case MenuEventStepUp: + do + { + if (nValue == nLastPerformanceBank) + { + // Wrap around + nValue = 0; + } + else if (nValue < nLastPerformanceBank) + { + ++nValue; + } + } while ((pUIMenu->m_pMiniDexed->IsValidPerformanceBank(nValue) != true) && (nValue != nStart)); + pUIMenu->m_nSelectedPerformanceBankID = nValue; + if (!bPerformanceSelectToLoad) + { + pUIMenu->m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nValue); + pUIMenu->m_pMiniDexed->SetFirstPerformance(); + } + break; + + case MenuEventSelect: + if (bPerformanceSelectToLoad) + { + pUIMenu->m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nValue); + pUIMenu->m_pMiniDexed->SetFirstPerformance(); + } + break; + + default: + return; + } + + Value = pUIMenu->m_pMiniDexed->GetPerformanceConfig ()->GetPerformanceBankName(nValue); + std::string nPSelected = "000"; + nPSelected += std::to_string(nValue+1); // Convert to user-facing number rather than index + nPSelected = nPSelected.substr(nPSelected.length()-3,3); + + if(nValue == (unsigned)pUIMenu->m_pMiniDexed->GetParameter (CMiniDexed::ParameterPerformanceBank)) + { + nPSelected += " [L]"; + } + + pUIMenu->m_pUI->DisplayWrite (pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, nPSelected.c_str(), + Value.c_str (), + nValue > 0, + nValue < pUIMenu->m_pMiniDexed->GetLastPerformanceBank()-1); +} + void CUIMenu::InputTxt (CUIMenu *pUIMenu, TMenuEvent Event) { unsigned nTG=0; diff --git a/src/uimenu.h b/src/uimenu.h index b66d65c..d5b48dc 100644 --- a/src/uimenu.h +++ b/src/uimenu.h @@ -91,6 +91,7 @@ private: static void EditTGParameterModulation (CUIMenu *pUIMenu, TMenuEvent Event); static void PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event); static void SavePerformanceNewFile (CUIMenu *pUIMenu, TMenuEvent Event); + static void EditPerformanceBankNumber (CUIMenu *pUIMenu, TMenuEvent Event); static std::string GetGlobalValueString (unsigned nParameter, int nValue); static std::string GetTGValueString (unsigned nTGParameter, int nValue); @@ -169,6 +170,7 @@ private: bool m_bPerformanceDeleteMode=false; bool m_bConfirmDeletePerformance=false; unsigned m_nSelectedPerformanceID =0; + unsigned m_nSelectedPerformanceBankID =0; bool m_bSplashShow=false; }; diff --git a/src/userinterface.cpp b/src/userinterface.cpp index 1c0716e..a2d78f1 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -388,13 +388,16 @@ void CUserInterface::UISetMIDIButtonChannel (unsigned uCh) if (uCh == 0) { m_nMIDIButtonCh = CMIDIDevice::Disabled; + LOGNOTE("MIDI Button channel not set"); } else if (uCh < CMIDIDevice::Channels) { m_nMIDIButtonCh = uCh - 1; + LOGNOTE("MIDI Button channel set to: %d", m_nMIDIButtonCh); } else { m_nMIDIButtonCh = CMIDIDevice::OmniMode; + LOGNOTE("MIDI Button channel set to: OMNI"); } } \ No newline at end of file From 891a57809c098d77374558baa49141144771eb14 Mon Sep 17 00:00:00 2001 From: Geo Maciolek Date: Mon, 29 Jan 2024 16:21:39 -0500 Subject: [PATCH 03/61] Wiki (anchor) links fixed, markdown linting (#607) "*" lists changed to "-" lists, bare URLs changd to use <...>, alt text added, etc. --- README.md | 76 +++++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 8623d9b..a0e6ae5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# MiniDexed ![](https://github.com/probonopd/MiniDexed/actions/workflows/build.yml/badge.svg) +# MiniDexed ![Github Build Status](https://github.com/probonopd/MiniDexed/actions/workflows/build.yml/badge.svg) ![minidexed](https://user-images.githubusercontent.com/2480569/161813414-bb156a1c-efec-44c0-802a-8926412a08e0.jpg) -MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA). +MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [Adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA). ## Features - [x] Uses [Synth_Dexed](https://codeberg.org/dcoredump/Synth_Dexed) with [circle-stdlib](https://github.com/smuehlst/circle-stdlib) - [x] SD card contents can be downloaded from [GitHub Releases](../../releases) - [x] Runs on all Raspberry Pi models (except Pico); see below for details -- [x] Produces sound on the headphone jack, HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) (better), or a [dedicated DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2c-dac) (best) +- [x] Produces sound on the headphone jack, HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) (better), or a [dedicated DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2s-dac) (best) - [x] Supports multiple voices through Program Change and Bank Change LSB/MSB MIDI messages - [x] Loads voices from `.syx` files from SD card (e.g., using `getsysex.sh` or from [Dexed_cart_1.0.zip](http://hsjp.eu/downloads/Dexed/Dexed_cart_1.0.zip)) - [x] Menu structure on optional [HD44780 display](https://www.berrybase.de/sensoren-module/displays/alphanumerische-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) and rotary encoder @@ -18,48 +18,48 @@ MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known - [x] Allows to configure multiple Dexed instances through `performance.ini` files (e.g., [converted](https://github.com/BobanSpasic/MDX_Vault) from DX1, DX5, TX816, DX7II, TX802) - [x] Compressor effect - [x] Reverb effect -- [x] Voices can be edited over MIDI, e.g., using the [synthmata](https://synthmata.github.io/volca-fm/) online editor (requires [additional hardware](https://github.com/probonopd/MiniDexed/wiki/Hardware#usb-midi-device)) +- [x] Voices can be edited over MIDI, e.g., using the [synthmata](https://synthmata.github.io/volca-fm/) online editor (requires [additional hardware](https://github.com/probonopd/MiniDexed/wiki/Hardware#usb-midi-devices)) ## Introduction Video about this project by [Floyd Steinberg](https://www.youtube.com/watch?v=Z3t94ceMHJo): -[![](https://i.ytimg.com/vi/Z3t94ceMHJo/sddefault.jpg)](https://www.youtube.com/watch?v=Z3t94ceMHJo) +[![YouTube Video about MiniDexed (Floyd Steinberg)](https://i.ytimg.com/vi/Z3t94ceMHJo/sddefault.jpg)](https://www.youtube.com/watch?v=Z3t94ceMHJo) ## System Requirements -* Raspberry Pi 1, 2, 3, 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out). On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8) -* A [PCM5102A or PCM5122 based DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2c-dac), HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) for good sound quality. If you don't have this, you can use the headphone jack on the Raspberry Pi but on anything but the Raspberry 4 the sound quality will be seriously limited -* Optionally (but highly recommended), an [LCDC1602 Display](https://www.berrybase.de/en/sensors-modules/displays/alphanumeric-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) (with or without i2c "backpack" board) and a [KY-040 rotary encoder](https://www.berrybase.de/en/components/passive-components/potentiometer/rotary-encoder/drehregler/rotary-encoder-mit-breakoutboard-ohne-gewinde-und-mutter) +- Raspberry Pi 1, 2, 3, 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out). On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8) +- A [PCM5102A or PCM5122 based DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2s-dac), HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) for good sound quality. If you don't have this, you can use the headphone jack on the Raspberry Pi but on anything but the Raspberry 4 the sound quality will be seriously limited +- Optionally (but highly recommended), an [LCDC1602 Display](https://www.berrybase.de/en/sensors-modules/displays/alphanumeric-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) (with or without i2c "backpack" board) and a [KY-040 rotary encoder](https://www.berrybase.de/en/components/passive-components/potentiometer/rotary-encoder/drehregler/rotary-encoder-mit-breakoutboard-ohne-gewinde-und-mutter) ## Usage -* In the case of Raspberry Pi 4, Update the firmware and bootloader to the latest version (not doing this may cause USB reliability issues) -* Download from [GitHub Releases](../../releases) -* Unzip -* Put the files into the root directory of a FAT32 formatted partition on SD/microSD card (Note for small SD cards which are no longer sold: If less than 65525 clusters, you may need to format as FAT16.) -* Put SD/microSD card into Raspberry Pi 1, 2, 3 or 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2c DAC for audio out) -* Attach headphones to the headphone jack using `SoundDevice=pwm` in `minidexed.ini` (default) (poor audio quality) -* Alternatively, attach a PCM5102A or PCM5122 based DAC and select i2c sound output using `SoundDevice=i2s` in `minidexed.ini` (best audio quality) -* Alternatively, attach a HDMI display with sound and select HDMI sound output using `SoundDevice=hdmi` in `minidexed.ini` (this may introduce slight latency) -* Attach a MIDI keyboard via USB (alternatively you can build a circuit that allows you to attach a "traditional" MIDI keyboard using a DIN connector, or use a DIN-MIDI-to-USB adapter) -* If you are using a LCDC1602 with an i2c "backpack" board, then you need to set `LCDI2CAddress=0x27` (or another address your i2c "backpack" board is set to) in `minidexed.ini` -* Boot -* Start playing -* If the system seems to become unresponsive after a few seconds, remove `usbspeed=full` from `cmdline.txt` and repeat ([details](https://github.com/probonopd/MiniDexed/issues/39)) -* Optionally, put voices in `.syx` files onto the SD card (e.g., using `getsysex.sh`) -* See the Wiki for [Menu](https://github.com/probonopd/MiniDexed/wiki/Menu) operation -* For voice programming, use any DX series editor (using MIDI sysex), including Dexed -* For library management, use the dedicated [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software -* If something is unclear or does not work, don't hesitate to [ask](https://github.com/probonopd/MiniDexed/discussions/)! +- In the case of Raspberry Pi 4, Update the firmware and bootloader to the latest version (not doing this may cause USB reliability issues) +- Download from [GitHub Releases](../../releases) +- Unzip +- Put the files into the root directory of a FAT32 formatted partition on SD/microSD card (Note for small SD cards which are no longer sold: If less than 65525 clusters, you may need to format as FAT16.) +- Put SD/microSD card into Raspberry Pi 1, 2, 3 or 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2c DAC for audio out) +- Attach headphones to the headphone jack using `SoundDevice=pwm` in `minidexed.ini` (default) (poor audio quality) +- Alternatively, attach a PCM5102A or PCM5122 based DAC and select i2c sound output using `SoundDevice=i2s` in `minidexed.ini` (best audio quality) +- Alternatively, attach a HDMI display with sound and select HDMI sound output using `SoundDevice=hdmi` in `minidexed.ini` (this may introduce slight latency) +- Attach a MIDI keyboard via USB (alternatively you can build a circuit that allows you to attach a "traditional" MIDI keyboard using a DIN connector, or use a DIN-MIDI-to-USB adapter) +- If you are using a LCDC1602 with an i2c "backpack" board, then you need to set `LCDI2CAddress=0x27` (or another address your i2c "backpack" board is set to) in `minidexed.ini` +- Boot +- Start playing +- If the system seems to become unresponsive after a few seconds, remove `usbspeed=full` from `cmdline.txt` and repeat ([details](https://github.com/probonopd/MiniDexed/issues/39)) +- Optionally, put voices in `.syx` files onto the SD card (e.g., using `getsysex.sh`) +- See the Wiki for [Menu](https://github.com/probonopd/MiniDexed/wiki/Menu) operation +- For voice programming, use any DX series editor (using MIDI sysex), including Dexed +- For library management, use the dedicated [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software +- If something is unclear or does not work, don't hesitate to [ask](https://github.com/probonopd/MiniDexed/discussions/)! ## Pinout All devices on Raspberry Pi GPIOs are **optional**. -![](https://user-images.githubusercontent.com/2480569/166105580-da11481c-8fc7-4375-8ab1-3031ab5c6ad0.png) +![Raspberry Pi Pinout/GPIO Diagram](https://user-images.githubusercontent.com/2480569/166105580-da11481c-8fc7-4375-8ab1-3031ab5c6ad0.png) -Please the the [wiki](https://github.com/probonopd/MiniDexed/wiki) for more information. +Please see the [wiki](https://github.com/probonopd/MiniDexed/wiki) for more information. ## Downloading @@ -71,24 +71,24 @@ Please see the [wiki](https://github.com/probonopd/MiniDexed/wiki/Development#bu ## Contributing -This project lives from the contributions of skilled C++ developers, testers, writers, etc. Please see https://github.com/probonopd/MiniDexed/issues. +This project lives from the contributions of skilled C++ developers, testers, writers, etc. Please see . ## Discussions -We are happy to hear from you. Please join the discussions on https://github.com/probonopd/MiniDexed/discussions. +We are happy to hear from you. Please join the discussions on . ## Documentation -Project documentation is at https://github.com/probonopd/MiniDexed/wiki. +Project documentation is at . ## Acknowledgements This project stands on the shoulders of giants. Special thanks to: -* [raphlinus](https://github.com/raphlinus) for the [MSFA](https://github.com/google/music-synthesizer-for-android) sound engine -* [asb2m10](https://github.com/asb2m10/dexed) for the [Dexed](https://github.com/asb2m10/dexed) software -* [dcoredump](https://github.com/dcoredump) for https://codeberg.org/dcoredump/Synth_Dexed, a port of Dexed for embedded systems -* [rsta2](https://github.com/rsta2) for https://github.com/rsta2/circle, the library to run code on bare metal Raspberry Pi (without a Linux kernel or operating system) and for the bulk of the MiniDexed code -* [smuehlst](https://github.com/smuehlst) for https://github.com/smuehlst/circle-stdlib, a version with Standard C and C++ library support -* [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed -* [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault) +- [raphlinus](https://github.com/raphlinus) for the [MSFA](https://github.com/google/music-synthesizer-for-android) sound engine +- [asb2m10](https://github.com/asb2m10/dexed) for the [Dexed](https://github.com/asb2m10/dexed) software +- [dcoredump](https://github.com/dcoredump) for [Synth Dexed](https://codeberg.org/dcoredump/Synth_Dexed), a port of Dexed for embedded systems +- [rsta2](https://github.com/rsta2) for [Circle](https://github.com/rsta2/circle), the library to run code on bare metal Raspberry Pi (without a Linux kernel or operating system) and for the bulk of the MiniDexed code +- [smuehlst](https://github.com/smuehlst) for [circle-stdlib](https://github.com/smuehlst/circle-stdlib), a version with Standard C and C++ library support +- [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed +- [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault) From cc7b1a5ac2df0222f5d2f54805a2756c58cbd64f Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 11 Mar 2024 21:57:52 +0100 Subject: [PATCH 04/61] diyelectromusic [ci skip] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a0e6ae5..ead3e58 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,4 @@ This project stands on the shoulders of giants. Special thanks to: - [smuehlst](https://github.com/smuehlst) for [circle-stdlib](https://github.com/smuehlst/circle-stdlib), a version with Standard C and C++ library support - [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed - [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault) +- [diyelectromusic](https://github.com/diyelectromusic/) for many [contributions](https://github.com/probonopd/MiniDexed/commits?author=diyelectromusic) From 544aaff5b8a970dbebbfdb74a4302630cdddd53c Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:32:23 +0000 Subject: [PATCH 05/61] Fix for Issue #628 - MIDI Button Channel 16 treated as OMNI by mistake. (#629) --- src/userinterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/userinterface.cpp b/src/userinterface.cpp index a2d78f1..241605e 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -390,10 +390,10 @@ void CUserInterface::UISetMIDIButtonChannel (unsigned uCh) m_nMIDIButtonCh = CMIDIDevice::Disabled; LOGNOTE("MIDI Button channel not set"); } - else if (uCh < CMIDIDevice::Channels) + else if (uCh <= CMIDIDevice::Channels) { m_nMIDIButtonCh = uCh - 1; - LOGNOTE("MIDI Button channel set to: %d", m_nMIDIButtonCh); + LOGNOTE("MIDI Button channel set to: %d", m_nMIDIButtonCh+1); } else { From 082445b6304ffb4cd59897fab20c836ad602284d Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 25 Mar 2024 13:51:22 +0000 Subject: [PATCH 06/61] Default MIDIAutoVoiceDumpOnPC to 0 (#614) Fixes #611 --- src/config.cpp | 2 +- src/config.h | 2 +- src/minidexed.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 526d8ad..bb65840 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -82,7 +82,7 @@ void CConfig::Load (void) m_bMIDIRXProgramChange = m_Properties.GetNumber ("MIDIRXProgramChange", 1) != 0; m_bIgnoreAllNotesOff = m_Properties.GetNumber ("IgnoreAllNotesOff", 0) != 0; - m_bMIDIAutoVoiceDumpOnPC = m_Properties.GetNumber ("MIDIAutoVoiceDumpOnPC", 1) != 0; + m_bMIDIAutoVoiceDumpOnPC = m_Properties.GetNumber ("MIDIAutoVoiceDumpOnPC", 0) != 0; m_bHeaderlessSysExVoices = m_Properties.GetNumber ("HeaderlessSysExVoices", 0) != 0; m_bExpandPCAcrossBanks = m_Properties.GetNumber ("ExpandPCAcrossBanks", 1) != 0; diff --git a/src/config.h b/src/config.h index f83c177..55c038e 100644 --- a/src/config.h +++ b/src/config.h @@ -80,7 +80,7 @@ public: const char *GetMIDIThruOut (void) const; // "" if not specified bool GetMIDIRXProgramChange (void) const; // true if not specified bool GetIgnoreAllNotesOff (void) const; - bool GetMIDIAutoVoiceDumpOnPC (void) const; // true if not specified + bool GetMIDIAutoVoiceDumpOnPC (void) const; // false if not specified bool GetHeaderlessSysExVoices (void) const; // false if not specified bool GetExpandPCAcrossBanks (void) const; // true if not specified diff --git a/src/minidexed.ini b/src/minidexed.ini index cd5ea1a..84d76ac 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -17,7 +17,7 @@ EngineType=1 MIDIBaudRate=31250 #MIDIThru=umidi1,ttyS1 IgnoreAllNotesOff=0 -MIDIAutoVoiceDumpOnPC=1 +MIDIAutoVoiceDumpOnPC=0 HeaderlessSysExVoices=0 # Program Change enable # 0 = Ignore all Program Change messages. From 349100d65ad59e73b091031ea011b0f73242890b Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 25 Mar 2024 16:12:44 +0100 Subject: [PATCH 07/61] Demo songs [ci skip] --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ead3e58..5c5aca4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [Adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA). +## Demo songs + +Listen to some examples made with MiniDexed by Banana71 [here](https://soundcloud.com/soundplantage/sets/minidexed2). + ## Features - [x] Uses [Synth_Dexed](https://codeberg.org/dcoredump/Synth_Dexed) with [circle-stdlib](https://github.com/smuehlst/circle-stdlib) From 75eb763055739e4a5e5625e273ed4ab6252f0a24 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:21:18 +0100 Subject: [PATCH 08/61] Very early initial support for RPi 5 based on dev branch of circle. (#638) * Very early initial support for RPi 5 based on dev branch of circle. * Added parameter to choose UART option on all RPi versions to be GP14/15 * Build for Raspberry Pi 5 * Update README.md for RPi 5 --------- Co-authored-by: probonopd --- .github/workflows/build.yml | 6 ++++++ README.md | 3 ++- src/config.txt | 6 +++++- src/kernel.cpp | 4 ++++ src/minidexed.cpp | 8 ++++++++ src/serialmididevice.cpp | 6 +++++- src/usbminidexedmidigadget.h | 18 ++++++++++++++++++ submod.sh | 4 ++-- 8 files changed, 50 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56ef1fd..8df5911 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,12 @@ jobs: wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz tar xf *-arm-none-eabi.tar.xz mkdir -p kernels + - name: Build for Raspberry Pi 5 + run: | + set -ex + export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH + RPI=5 bash -ex build.sh + cp ./src/kernel*.img ./kernels/ - name: Build for Raspberry Pi 4 run: | set -ex diff --git a/README.md b/README.md index 5c5aca4..e1807bf 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Video about this project by [Floyd Steinberg](https://www.youtube.com/watch?v=Z3 ## System Requirements -- Raspberry Pi 1, 2, 3, 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out). On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8) +- Raspberry Pi 1, 2, 3, 4, or 400. Raspberry Pi Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out. On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8) +- Raspberry Pi 5 can be used but currently support is experimental; HDMI sound and USB Gadget mode are not available yet - A [PCM5102A or PCM5122 based DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2s-dac), HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) for good sound quality. If you don't have this, you can use the headphone jack on the Raspberry Pi but on anything but the Raspberry 4 the sound quality will be seriously limited - Optionally (but highly recommended), an [LCDC1602 Display](https://www.berrybase.de/en/sensors-modules/displays/alphanumeric-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) (with or without i2c "backpack" board) and a [KY-040 rotary encoder](https://www.berrybase.de/en/components/passive-components/potentiometer/rotary-encoder/drehregler/rotary-encoder-mit-breakoutboard-ohne-gewinde-und-mutter) diff --git a/src/config.txt b/src/config.txt index 7957b5c..c10dfa1 100644 --- a/src/config.txt +++ b/src/config.txt @@ -10,7 +10,7 @@ gpu_mem=16 disable_overscan=0 # -# Use 64-bit for RPi 3, 4, 400 and Zero 2, and 32-bit for all other models +# Use 64-bit for RPi 3, 4, 400, 5 and Zero 2, and 32-bit for all other models # [pi3] @@ -24,3 +24,7 @@ kernel=kernel8-rpi4.img # Zero 2 W [pi02] arm_64bit=1 + +[pi5] +arm_64bit=1 +kernel=kernel_2712.img diff --git a/src/kernel.cpp b/src/kernel.cpp index c06c386..3ff835c 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -68,8 +68,12 @@ bool CKernel::Initialize (void) if (m_Config.GetUSBGadgetMode()) { +#if RASPPI==5 +#warning No support for USB Gadget Mode on RPI 5 yet +#else // Run the USB stack in USB Gadget (device) mode m_pUSB = new CUSBMiniDexedMIDIGadget (&mInterrupt); +#endif } else { diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 968320a..e2576b5 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -103,7 +103,11 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, if (pConfig->GetUSBGadgetMode()) { +#if RASPPI==5 + LOGNOTE ("USB Gadget (Device) Mode NOT supported on RPI 5"); +#else LOGNOTE ("USB In Gadget (Device) Mode"); +#endif } else { @@ -128,6 +132,9 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, } else if (strcmp (pDeviceName, "hdmi") == 0) { +#if RASPPI==5 + LOGNOTE ("HDMI mode NOT supported on RPI 5."); +#else LOGNOTE ("HDMI mode"); m_pSoundDevice = new CHDMISoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), @@ -136,6 +143,7 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, // The channels are swapped by default in the HDMI sound driver. // TODO: Remove this line, when this has been fixed in the driver. m_bChannelsSwapped = !m_bChannelsSwapped; +#endif } else { diff --git a/src/serialmididevice.cpp b/src/serialmididevice.cpp index 883fd4d..b37a498 100644 --- a/src/serialmididevice.cpp +++ b/src/serialmididevice.cpp @@ -28,11 +28,15 @@ LOGMODULE("serialmididevice"); +// There are several UART options - see circle/include/serial.h +// 0 corresponds to GP14/GP15 on all RPi versions. +#define SERIAL_MIDI_DEVICE 0 + CSerialMIDIDevice::CSerialMIDIDevice (CMiniDexed *pSynthesizer, CInterruptSystem *pInterrupt, CConfig *pConfig, CUserInterface *pUI) : CMIDIDevice (pSynthesizer, pConfig, pUI), m_pConfig (pConfig), - m_Serial (pInterrupt, TRUE), + m_Serial (pInterrupt, TRUE, SERIAL_MIDI_DEVICE), m_nSerialState (0), m_nSysEx (0), m_SendBuffer (&m_Serial) diff --git a/src/usbminidexedmidigadget.h b/src/usbminidexedmidigadget.h index bbb5f44..8513549 100644 --- a/src/usbminidexedmidigadget.h +++ b/src/usbminidexedmidigadget.h @@ -22,6 +22,23 @@ #ifndef _usbminidexedmidigadget_h #define _usbminidexedmidigadget_h +#if RASPPI==5 +#include +#include + +#warning No support for USB Gadget Mode on RPI 5 yet +class CUSBMiniDexedMIDIGadget +{ +public: + CUSBMiniDexedMIDIGadget (CInterruptSystem *pInterruptSystem) + { + } + + ~CUSBMiniDexedMIDIGadget (void) + { + } +}; +#else #include #include #include @@ -82,5 +99,6 @@ protected: return CUSBMIDIGadget::GetDescriptor(wValue, wIndex, pLength); } }; +#endif #endif diff --git a/submod.sh b/submod.sh index 2e24228..d72d19c 100755 --- a/submod.sh +++ b/submod.sh @@ -6,13 +6,13 @@ git submodule update --init --recursive # # Use fixed master branch of circle-stdlib then re-update cd circle-stdlib/ -git checkout 695ab4a +git checkout 3bd135d git submodule update --init --recursive cd - # # Optional update submodules explicitly cd circle-stdlib/libs/circle -git checkout fe09c4b +git checkout 4b3e06f cd - cd circle-stdlib/libs/circle-newlib #git checkout develop From e2dc897c8a7402c49bd23b6a2d3428b77f2bf4c1 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:13:17 +0100 Subject: [PATCH 09/61] Update README.md (#643) [ci skip] * Update README.md Added recommendation to use an older or cheaper Pi (not a Pi 5) for a first build. * Update README.md --------- Co-authored-by: probonopd --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1807bf..dcb9d84 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Video about this project by [Floyd Steinberg](https://www.youtube.com/watch?v=Z3 ## System Requirements - Raspberry Pi 1, 2, 3, 4, or 400. Raspberry Pi Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out. On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8) -- Raspberry Pi 5 can be used but currently support is experimental; HDMI sound and USB Gadget mode are not available yet +- Raspberry Pi 5 can be used but currently support is experimental: HDMI sound and USB Gadget mode are not available yet, and it is not clear if there are implications for cooling from running MiniDexed. Also, MiniDexed is currently not taking advantage of the higher processing power of the Raspberry Pi 5 yet. *Hence, you may consider using one of the less expensive, older Raspberry Pi boards for your first build.* - A [PCM5102A or PCM5122 based DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2s-dac), HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) for good sound quality. If you don't have this, you can use the headphone jack on the Raspberry Pi but on anything but the Raspberry 4 the sound quality will be seriously limited - Optionally (but highly recommended), an [LCDC1602 Display](https://www.berrybase.de/en/sensors-modules/displays/alphanumeric-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) (with or without i2c "backpack" board) and a [KY-040 rotary encoder](https://www.berrybase.de/en/components/passive-components/potentiometer/rotary-encoder/drehregler/rotary-encoder-mit-breakoutboard-ohne-gewinde-und-mutter) From 4fa9e167b27f59490f86d74f628cc7e624c1c4fd Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Thu, 25 Apr 2024 18:30:02 +0100 Subject: [PATCH 10/61] Initial implementation of USB MIDI SysEx message handling. (#634) * Initial implementation of USB MIDI SysEx message handling. * Remove unused parameter from USB message handling. --- src/mididevice.h | 3 ++ src/midikeyboard.cpp | 67 +++++++++++++++++++++++++++++++++++++--- src/midikeyboard.h | 7 +++++ src/serialmididevice.cpp | 4 +-- src/serialmididevice.h | 3 -- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/mididevice.h b/src/mididevice.h index 44be25a..0fc027c 100644 --- a/src/mididevice.h +++ b/src/mididevice.h @@ -30,6 +30,9 @@ #include #include "userinterface.h" +#define MAX_DX7_SYSEX_LENGTH 4104 +#define MAX_MIDI_MESSAGE MAX_DX7_SYSEX_LENGTH + class CMiniDexed; class CMIDIDevice diff --git a/src/midikeyboard.cpp b/src/midikeyboard.cpp index 169f165..2eae9d8 100644 --- a/src/midikeyboard.cpp +++ b/src/midikeyboard.cpp @@ -37,6 +37,7 @@ TMIDIPacketHandler * const CMIDIKeyboard::s_pMIDIPacketHandler[MaxInstances] = CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance) : CMIDIDevice (pSynthesizer, pConfig, pUI), + m_nSysExIdx (0), m_nInstance (nInstance), m_pMIDIDevice (0) { @@ -100,28 +101,86 @@ void CMIDIKeyboard::Send (const u8 *pMessage, size_t nLength, unsigned nCable) m_SendQueue.push (Entry); } +// Most packets will be passed straight onto the main MIDI message handler +// but SysEx messages are multiple USB packets and so will need building up +// before parsing. +void CMIDIKeyboard::USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable) +{ + 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= USB_SYSEX_BUFFER_SIZE) { + // Run out of space, so reset and ignore rest of the message + 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 + m_nSysExIdx = 0; + } + else if ((pPacket[i] & 0x80) != 0) { + // Received another command, so reset processing as something has gone wrong + //printf ("SysEx Reset\n"); + m_nSysExIdx = 0; + break; + } + else + { + // Store the byte + m_SysEx[m_nSysExIdx++] = pPacket[i]; + } + } + } + else + { + // Assume it is a standard message + MIDIMessageHandler (pPacket, nLength, nCable); + } +} + void CMIDIKeyboard::MIDIPacketHandler0 (unsigned nCable, u8 *pPacket, unsigned nLength) { assert (s_pThis[0] != 0); - s_pThis[0]->MIDIMessageHandler (pPacket, nLength, nCable); + s_pThis[0]->USBMIDIMessageHandler (pPacket, nLength, nCable); } void CMIDIKeyboard::MIDIPacketHandler1 (unsigned nCable, u8 *pPacket, unsigned nLength) { assert (s_pThis[1] != 0); - s_pThis[1]->MIDIMessageHandler (pPacket, nLength, nCable); + s_pThis[1]->USBMIDIMessageHandler (pPacket, nLength, nCable); } void CMIDIKeyboard::MIDIPacketHandler2 (unsigned nCable, u8 *pPacket, unsigned nLength) { assert (s_pThis[2] != 0); - s_pThis[2]->MIDIMessageHandler (pPacket, nLength, nCable); + s_pThis[2]->USBMIDIMessageHandler (pPacket, nLength, nCable); } void CMIDIKeyboard::MIDIPacketHandler3 (unsigned nCable, u8 *pPacket, unsigned nLength) { assert (s_pThis[3] != 0); - s_pThis[3]->MIDIMessageHandler (pPacket, nLength, nCable); + s_pThis[3]->USBMIDIMessageHandler (pPacket, nLength, nCable); } void CMIDIKeyboard::DeviceRemovedHandler (CDevice *pDevice, void *pContext) diff --git a/src/midikeyboard.h b/src/midikeyboard.h index 0868f9c..047fa52 100644 --- a/src/midikeyboard.h +++ b/src/midikeyboard.h @@ -31,6 +31,8 @@ #include #include +#define USB_SYSEX_BUFFER_SIZE (MAX_DX7_SYSEX_LENGTH+128) // Allow a bit spare to handle unexpected SysEx messages + class CMiniDexed; class CMIDIKeyboard : public CMIDIDevice @@ -53,6 +55,8 @@ private: static void MIDIPacketHandler3 (unsigned nCable, u8 *pPacket, unsigned nLength); static void DeviceRemovedHandler (CDevice *pDevice, void *pContext); + + void USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable); private: struct TSendQueueEntry @@ -61,6 +65,8 @@ private: size_t nLength; unsigned nCable; }; + uint8_t m_SysEx[USB_SYSEX_BUFFER_SIZE]; + unsigned m_nSysExIdx; private: unsigned m_nInstance; @@ -73,6 +79,7 @@ private: static CMIDIKeyboard *s_pThis[MaxInstances]; static TMIDIPacketHandler * const s_pMIDIPacketHandler[MaxInstances]; + }; #endif diff --git a/src/serialmididevice.cpp b/src/serialmididevice.cpp index b37a498..3fe01e8 100644 --- a/src/serialmididevice.cpp +++ b/src/serialmididevice.cpp @@ -75,7 +75,7 @@ void CSerialMIDIDevice::Process (void) return; } - if (m_pConfig->GetMIDIDumpEnabled ()) +/* if (m_pConfig->GetMIDIDumpEnabled ()) { printf("Incoming MIDI data:"); for (uint16_t i = 0; i < nResult; i++) @@ -85,7 +85,7 @@ void CSerialMIDIDevice::Process (void) printf(" 0x%02x",Buffer[i]); } printf("\n"); - } + }*/ // Process MIDI messages // See: https://www.midi.org/specifications/item/table-1-summary-of-midi-message diff --git a/src/serialmididevice.h b/src/serialmididevice.h index 1a5b465..fdf61b2 100644 --- a/src/serialmididevice.h +++ b/src/serialmididevice.h @@ -30,9 +30,6 @@ #include #include -#define MAX_DX7_SYSEX_LENGTH 4104 -#define MAX_MIDI_MESSAGE MAX_DX7_SYSEX_LENGTH - class CMiniDexed; class CSerialMIDIDevice : public CMIDIDevice From d08280bc7005499773df5c27fe9f0d53cda1ebab Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Thu, 9 May 2024 18:58:46 +0100 Subject: [PATCH 11/61] Support for ST7789 based (SPI) displays (#652) * Initial build with basic structure to support st7789device once added to circle. * Implementation of ST7789 display - requires updated circle with new ST7789 character driver. * Added more details to minidexed.ini about ST7789 and SPI options. * Update to develop branch of circle that now supports st7789 character mode display. * Minor formatting fixes to tidy up. * Allow setting of more advanced SPI parameters: mode and clock. * Update to allow for font size as an option. --- src/config.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++ src/config.h | 36 ++++++++++++++++++++- src/kernel.cpp | 29 ++++++++++++++++- src/kernel.h | 2 ++ src/minidexed.cpp | 4 +-- src/minidexed.h | 3 +- src/minidexed.ini | 21 +++++++++++++ src/userinterface.cpp | 69 +++++++++++++++++++++++++++++++++++----- src/userinterface.h | 7 ++++- submod.sh | 2 +- 10 files changed, 232 insertions(+), 14 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index bb65840..d456c58 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -102,6 +102,20 @@ void CConfig::Load (void) m_bSSD1306LCDRotate = m_Properties.GetNumber ("SSD1306LCDRotate", 0) != 0; m_bSSD1306LCDMirror = m_Properties.GetNumber ("SSD1306LCDMirror", 0) != 0; + m_nSPIBus = m_Properties.GetNumber ("SPIBus", SPI_INACTIVE); // Disabled by default + m_nSPIMode = m_Properties.GetNumber ("SPIMode", SPI_DEF_MODE); + m_nSPIClockKHz = m_Properties.GetNumber ("SPIClockKHz", SPI_DEF_CLOCK); + + m_bST7789Enabled = m_Properties.GetNumber ("ST7789Enabled", 0) != 0; + m_nST7789Data = m_Properties.GetNumber ("ST7789Data", 0); + m_nST7789Select = m_Properties.GetNumber ("ST7789Select", 0); + m_nST7789Reset = m_Properties.GetNumber ("ST7789Reset", 0); // optional + m_nST7789Backlight = m_Properties.GetNumber ("ST7789Backlight", 0); // optional + m_nST7789Width = m_Properties.GetNumber ("ST7789Width", 240); + m_nST7789Height = m_Properties.GetNumber ("ST7789Height", 240); + m_nST7789Rotation = m_Properties.GetNumber ("ST7789Rotation", 0); + m_bST7789SmallFont = m_Properties.GetNumber ("ST7789SmallFont", 0) != 0; + m_nLCDColumns = m_Properties.GetNumber ("LCDColumns", 16); m_nLCDRows = m_Properties.GetNumber ("LCDRows", 2); @@ -299,6 +313,65 @@ bool CConfig::GetSSD1306LCDMirror (void) const return m_bSSD1306LCDMirror; } +unsigned CConfig::GetSPIBus (void) const +{ + return m_nSPIBus; +} + +unsigned CConfig::GetSPIMode (void) const +{ + return m_nSPIMode; +} + +unsigned CConfig::GetSPIClockKHz (void) const +{ + return m_nSPIClockKHz; +} + +bool CConfig::GetST7789Enabled (void) const +{ + return m_bST7789Enabled; +} + +unsigned CConfig::GetST7789Data (void) const +{ + return m_nST7789Data; +} + +unsigned CConfig::GetST7789Select (void) const +{ + return m_nST7789Select; +} + +unsigned CConfig::GetST7789Reset (void) const +{ + return m_nST7789Reset; +} + +unsigned CConfig::GetST7789Backlight (void) const +{ + return m_nST7789Backlight; +} + +unsigned CConfig::GetST7789Width (void) const +{ + return m_nST7789Width; +} + +unsigned CConfig::GetST7789Height (void) const +{ + return m_nST7789Height; +} + +unsigned CConfig::GetST7789Rotation (void) const +{ + return m_nST7789Rotation; +} + +bool CConfig::GetST7789SmallFont (void) const +{ + return m_bST7789SmallFont; +} unsigned CConfig::GetLCDColumns (void) const { return m_nLCDColumns; diff --git a/src/config.h b/src/config.h index 55c038e..71bb5ad 100644 --- a/src/config.h +++ b/src/config.h @@ -28,6 +28,10 @@ #include #include +#define SPI_INACTIVE 255 +#define SPI_DEF_CLOCK 15000 // kHz +#define SPI_DEF_MODE 0 // Default mode (0,1,2,3) + class CConfig // Configuration for MiniDexed { public: @@ -103,6 +107,22 @@ public: bool GetSSD1306LCDRotate (void) const; bool GetSSD1306LCDMirror (void) const; + // SPI support + unsigned GetSPIBus (void) const; + unsigned GetSPIMode (void) const; + unsigned GetSPIClockKHz (void) const; + + // ST7789 LCD + bool GetST7789Enabled (void) const; + unsigned GetST7789Data (void) const; + unsigned GetST7789Select (void) const; + unsigned GetST7789Reset (void) const; + unsigned GetST7789Backlight (void) const; + unsigned GetST7789Width (void) const; + unsigned GetST7789Height (void) const; + unsigned GetST7789Rotation (void) const; + bool GetST7789SmallFont (void) const; + unsigned GetLCDColumns (void) const; unsigned GetLCDRows (void) const; @@ -204,7 +224,21 @@ private: unsigned m_nSSD1306LCDHeight; bool m_bSSD1306LCDRotate; bool m_bSSD1306LCDMirror; - + + unsigned m_nSPIBus; + unsigned m_nSPIMode; + unsigned m_nSPIClockKHz; + + bool m_bST7789Enabled; + unsigned m_nST7789Data; + unsigned m_nST7789Select; + unsigned m_nST7789Reset; + unsigned m_nST7789Backlight; + unsigned m_nST7789Width; + unsigned m_nST7789Height; + unsigned m_nST7789Rotation; + unsigned m_bST7789SmallFont; + unsigned m_nLCDColumns; unsigned m_nLCDRows; diff --git a/src/kernel.cpp b/src/kernel.cpp index 3ff835c..33bc5f8 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -33,6 +33,7 @@ CKernel::CKernel (void) m_Config (&mFileSystem), m_GPIOManager (&mInterrupt), m_I2CMaster (CMachineInfo::Get ()->GetDevice (DeviceI2CMaster), TRUE), + m_pSPIMaster (nullptr), m_pDexed (0) { s_pThis = this; @@ -66,6 +67,32 @@ bool CKernel::Initialize (void) m_Config.Load (); + unsigned nSPIMaster = m_Config.GetSPIBus(); + unsigned nSPIMode = m_Config.GetSPIMode(); + unsigned long nSPIClock = 1000 * m_Config.GetSPIClockKHz(); +#if RASPPI<4 + // By default older RPI versions use SPI 0. + // It is possible to build circle to support SPI 1 for + // devices that use the 40-pin header, but that isn't + // enabled at present... + if (nSPIMaster == 0) +#else + // RPI 4+ has several possible SPI Bus Configurations. + // As mentioned above, SPI 1 is not built by default. + // See circle/include/circle/spimaster.h + if (nSPIMaster == 0 || nSPIMaster == 3 || nSPIMaster == 4 || nSPIMaster == 5 || nSPIMaster == 6) +#endif + { + unsigned nCPHA = (nSPIMode & 1) ? 1 : 0; + unsigned nCPOL = (nSPIMode & 2) ? 1 : 0; + m_pSPIMaster = new CSPIMaster (nSPIClock, nCPOL, nCPHA, nSPIMaster); + if (!m_pSPIMaster->Initialize()) + { + delete (m_pSPIMaster); + m_pSPIMaster = nullptr; + } + } + if (m_Config.GetUSBGadgetMode()) { #if RASPPI==5 @@ -86,7 +113,7 @@ bool CKernel::Initialize (void) return FALSE; } - m_pDexed = new CMiniDexed (&m_Config, &mInterrupt, &m_GPIOManager, &m_I2CMaster, + m_pDexed = new CMiniDexed (&m_Config, &mInterrupt, &m_GPIOManager, &m_I2CMaster, m_pSPIMaster, &mFileSystem); assert (m_pDexed); diff --git a/src/kernel.h b/src/kernel.h index 7d2f346..efe5f4f 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "config.h" #include "minidexed.h" @@ -54,6 +55,7 @@ private: CCPUThrottle m_CPUThrottle; CGPIOManager m_GPIOManager; CI2CMaster m_I2CMaster; + CSPIMaster *m_pSPIMaster; CMiniDexed *m_pDexed; CUSBController *m_pUSB; diff --git a/src/minidexed.cpp b/src/minidexed.cpp index e2576b5..9547baf 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -31,13 +31,13 @@ LOGMODULE ("minidexed"); CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, - CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, FATFS *pFileSystem) + CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, FATFS *pFileSystem) : #ifdef ARM_ALLOW_MULTI_CORE CMultiCoreSupport (CMemorySystem::Get ()), #endif m_pConfig (pConfig), - m_UI (this, pGPIOManager, pI2CMaster, pConfig), + m_UI (this, pGPIOManager, pI2CMaster, pSPIMaster, pConfig), m_PerformanceConfig (pFileSystem), m_PCKeyboard (this, pConfig, &m_UI), m_SerialMIDI (this, pInterrupt, pConfig, &m_UI), diff --git a/src/minidexed.h b/src/minidexed.h index 407d5db..8ca74c8 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,7 @@ class CMiniDexed { public: CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, - CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, FATFS *pFileSystem); + CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, FATFS *pFileSystem); bool Initialize (void); diff --git a/src/minidexed.ini b/src/minidexed.ini index 84d76ac..de9f150 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -55,6 +55,27 @@ SSD1306LCDHeight=32 SSD1306LCDRotate=0 SSD1306LCDMirror=0 +# ST7789 LCD +# SPIBus=0 for any RPi (GPIO 10,11,8,7). +# Note: Leave blank (default) if no SPI device required. +# Select=0|1 for CE0 or CE1 +# Data = GPIO pin number +# Optional: Reset, Backlight = GPIO pin numbers +# Rotation=0,90,180,270 +# SmallFont=0 (default), 1 +# +# For a 240 wide display set LCDColumns=15 with LCDRows=2 +SPIBus= +ST7789Enabled=0 +ST7789Data= +ST7789Select= +ST7789Reset= +ST7789Backlight= +ST7789Width=240 +ST7789Height=240 +ST7789Rotation=0 +ST7789SmallFont=0 + # Default is 16x2 display (e.g. HD44780) LCDColumns=16 LCDRows=2 diff --git a/src/userinterface.cpp b/src/userinterface.cpp index 241605e..9e7c112 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -27,10 +27,11 @@ LOGMODULE ("ui"); -CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CConfig *pConfig) +CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, CConfig *pConfig) : m_pMiniDexed (pMiniDexed), m_pGPIOManager (pGPIOManager), m_pI2CMaster (pI2CMaster), + m_pSPIMaster (pSPIMaster), m_pConfig (pConfig), m_pLCD (0), m_pLCDBuffered (0), @@ -57,17 +58,69 @@ bool CUserInterface::Initialize (void) { unsigned i2caddr = m_pConfig->GetLCDI2CAddress (); unsigned ssd1306addr = m_pConfig->GetSSD1306LCDI2CAddress (); + bool st7789 = m_pConfig->GetST7789Enabled (); if (ssd1306addr != 0) { m_pSSD1306 = new CSSD1306Device (m_pConfig->GetSSD1306LCDWidth (), m_pConfig->GetSSD1306LCDHeight (), m_pI2CMaster, ssd1306addr, m_pConfig->GetSSD1306LCDRotate (), m_pConfig->GetSSD1306LCDMirror ()); - LOGDBG ("LCD: SSD1306"); if (!m_pSSD1306->Initialize ()) { + LOGDBG("LCD: SSD1306 initialization failed"); return false; } + LOGDBG ("LCD: SSD1306"); m_pLCD = m_pSSD1306; - } else if (i2caddr == 0) + } + else if (st7789) + { + if (m_pSPIMaster == nullptr) + { + LOGDBG("LCD: ST7789 Enabled but SPI Initialisation Failed"); + return false; + } + + unsigned long nSPIClock = 1000 * m_pConfig->GetSPIClockKHz(); + unsigned nSPIMode = m_pConfig->GetSPIMode(); + unsigned nCPHA = (nSPIMode & 1) ? 1 : 0; + unsigned nCPOL = (nSPIMode & 2) ? 1 : 0; + LOGDBG("SPI: CPOL=%u; CPHA=%u; CLK=%u",nCPOL,nCPHA,nSPIClock); + m_pST7789Display = new CST7789Display (m_pSPIMaster, + m_pConfig->GetST7789Data(), + m_pConfig->GetST7789Reset(), + m_pConfig->GetST7789Backlight(), + m_pConfig->GetST7789Width(), + m_pConfig->GetST7789Height(), + nCPOL, nCPHA, nSPIClock, + m_pConfig->GetST7789Select()); + if (m_pST7789Display->Initialize()) + { + m_pST7789Display->SetRotation (m_pConfig->GetST7789Rotation()); + bool bLargeFont = !(m_pConfig->GetST7789SmallFont()); + m_pST7789 = new CST7789Device (m_pSPIMaster, m_pST7789Display, m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows (), bLargeFont, bLargeFont); + if (m_pST7789->Initialize()) + { + LOGDBG ("LCD: ST7789"); + m_pLCD = m_pST7789; + } + else + { + LOGDBG ("LCD: Failed to initalize ST7789 character device"); + delete (m_pST7789); + delete (m_pST7789Display); + m_pST7789 = nullptr; + m_pST7789Display = nullptr; + return false; + } + } + else + { + LOGDBG ("LCD: Failed to initialize ST7789 display"); + delete (m_pST7789Display); + m_pST7789Display = nullptr; + return false; + } + } + else if (i2caddr == 0) { m_pHD44780 = new CHD44780Device (m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows (), m_pConfig->GetLCDPinData4 (), @@ -77,22 +130,24 @@ bool CUserInterface::Initialize (void) m_pConfig->GetLCDPinEnable (), m_pConfig->GetLCDPinRegisterSelect (), m_pConfig->GetLCDPinReadWrite ()); - LOGDBG ("LCD: HD44780"); if (!m_pHD44780->Initialize ()) { + LOGDBG("LCD: HD44780 initialization failed"); return false; } + LOGDBG ("LCD: HD44780"); m_pLCD = m_pHD44780; } else { m_pHD44780 = new CHD44780Device (m_pI2CMaster, i2caddr, m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows ()); - LOGDBG ("LCD: HD44780 I2C"); if (!m_pHD44780->Initialize ()) { + LOGDBG("LCD: HD44780 (I2C) initialization failed"); return false; } + LOGDBG ("LCD: HD44780 I2C"); m_pLCD = m_pHD44780; } assert (m_pLCD); @@ -217,7 +272,7 @@ void CUserInterface::DisplayWrite (const char *pMenu, const char *pParam, const CString Value (" "); if (bArrowDown) { - Value = "\x7F"; // arrow left character + Value = "<"; // arrow left character } Value.Append (pValue); @@ -232,7 +287,7 @@ void CUserInterface::DisplayWrite (const char *pMenu, const char *pParam, const } } - Value.Append ("\x7E"); // arrow right character + Value.Append (">"); // arrow right character } Msg.Append (Value); diff --git a/src/userinterface.h b/src/userinterface.h index 5de2846..a8026db 100644 --- a/src/userinterface.h +++ b/src/userinterface.h @@ -26,16 +26,18 @@ #include #include #include +#include #include #include #include +#include class CMiniDexed; class CUserInterface { public: - CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CConfig *pConfig); + CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, CConfig *pConfig); ~CUserInterface (void); bool Initialize (void); @@ -68,11 +70,14 @@ private: CMiniDexed *m_pMiniDexed; CGPIOManager *m_pGPIOManager; CI2CMaster *m_pI2CMaster; + CSPIMaster *m_pSPIMaster; CConfig *m_pConfig; CCharDevice *m_pLCD; CHD44780Device *m_pHD44780; CSSD1306Device *m_pSSD1306; + CST7789Display *m_pST7789Display; + CST7789Device *m_pST7789; CWriteBufferDevice *m_pLCDBuffered; CUIButtons *m_pUIButtons; diff --git a/submod.sh b/submod.sh index d72d19c..c13dca3 100755 --- a/submod.sh +++ b/submod.sh @@ -12,7 +12,7 @@ cd - # # Optional update submodules explicitly cd circle-stdlib/libs/circle -git checkout 4b3e06f +git checkout 4155f43 cd - cd circle-stdlib/libs/circle-newlib #git checkout develop From afa72d21aa81a9876b3f9e50f3bd6f4cd56cdac0 Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 30 May 2024 18:43:48 +0200 Subject: [PATCH 12/61] Attempt to fix #646 (#649) * Attempt to fix #646 https://github.com/probonopd/MiniDexed/issues/648 * SetVoiceName() suggested by @diyelectromusic --- src/minidexed.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 9547baf..02d8fd6 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -1767,8 +1767,9 @@ void CMiniDexed::SetVoiceName (std::string VoiceName, unsigned nTG) { assert (nTG < CConfig::ToneGenerators); assert (m_pTG[nTG]); - char Name[10]; + char Name[11]; strncpy(Name, VoiceName.c_str(),10); + Name[10] = '\0'; m_pTG[nTG]->getName (Name); } From 98b5274cf3889577d7a402abc5ddce529e233350 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Sat, 29 Jun 2024 22:44:52 +0100 Subject: [PATCH 13/61] Add support for 8 channel I2S mono audio output on RPi 5 (#657) * First implementation of using four PCM5102 I2S sound devices for 8-channel mono sound output on a Raspberry Pi 5. Requires latest develop branch of circle. * Update to required develop branch of circle * Adjusted default chunk sizes to correctly support number of channels. * Update queue size as per Rene's suggestion. --------- Co-authored-by: probonopd --- src/config.cpp | 18 +++- src/config.h | 2 + src/minidexed.cpp | 222 ++++++++++++++++++++++++++++++---------------- src/minidexed.h | 1 + submod.sh | 2 +- 5 files changed, 167 insertions(+), 78 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index d456c58..ea6bde3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -41,15 +41,22 @@ void CConfig::Load (void) m_SoundDevice = m_Properties.GetString ("SoundDevice", "pwm"); m_nSampleRate = m_Properties.GetNumber ("SampleRate", 48000); + m_bQuadDAC8Chan = m_Properties.GetNumber ("QuadDAC8Chan", 0) != 0; + if (m_SoundDevice == "hdmi") { + m_nChunkSize = m_Properties.GetNumber ("ChunkSize", 384*6); + } + else + { #ifdef ARM_ALLOW_MULTI_CORE - m_nChunkSize = m_Properties.GetNumber ("ChunkSize", m_SoundDevice == "hdmi" ? 384*6 : 256); + m_nChunkSize = m_Properties.GetNumber ("ChunkSize", m_bQuadDAC8Chan ? 1024 : 256); // 128 per channel #else - m_nChunkSize = m_Properties.GetNumber ("ChunkSize", m_SoundDevice == "hdmi" ? 384*6 : 1024); + m_nChunkSize = m_Properties.GetNumber ("ChunkSize", 1024); #endif + } m_nDACI2CAddress = m_Properties.GetNumber ("DACI2CAddress", 0); m_bChannelsSwapped = m_Properties.GetNumber ("ChannelsSwapped", 0) != 0; - unsigned newEngineType = m_Properties.GetNumber ("EngineType", 1); + unsigned newEngineType = m_Properties.GetNumber ("EngineType", 1); if (newEngineType == 2) { m_EngineType = MKI; } else if (newEngineType == 3) { @@ -243,6 +250,11 @@ bool CConfig::GetExpandPCAcrossBanks (void) const return m_bExpandPCAcrossBanks; } +bool CConfig::GetQuadDAC8Chan (void) const +{ + return m_bQuadDAC8Chan; +} + bool CConfig::GetLCDEnabled (void) const { return m_bLCDEnabled; diff --git a/src/config.h b/src/config.h index 71bb5ad..5944dd2 100644 --- a/src/config.h +++ b/src/config.h @@ -87,6 +87,7 @@ public: bool GetMIDIAutoVoiceDumpOnPC (void) const; // false if not specified bool GetHeaderlessSysExVoices (void) const; // false if not specified bool GetExpandPCAcrossBanks (void) const; // true if not specified + bool GetQuadDAC8Chan (void) const; // false if not specified // HD44780 LCD // GPIO pin numbers are chip numbers, not header positions @@ -208,6 +209,7 @@ private: bool m_bMIDIAutoVoiceDumpOnPC; bool m_bHeaderlessSysExVoices; bool m_bExpandPCAcrossBanks; + bool m_bQuadDAC8Chan; bool m_bLCDEnabled; unsigned m_nLCDPinEnable; diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 02d8fd6..429fddc 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -42,6 +42,7 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_PCKeyboard (this, pConfig, &m_UI), m_SerialMIDI (this, pInterrupt, pConfig, &m_UI), m_bUseSerial (false), + m_bQuadDAC8Chan (false), m_pSoundDevice (0), m_bChannelsSwapped (pConfig->GetChannelsSwapped ()), #ifdef ARM_ALLOW_MULTI_CORE @@ -125,10 +126,26 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, if (strcmp (pDeviceName, "i2s") == 0) { LOGNOTE ("I2S mode"); - - m_pSoundDevice = new CI2SSoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), - pConfig->GetChunkSize (), false, - pI2CMaster, pConfig->GetDACI2CAddress ()); +#if RASPPI==5 + // Quad DAC 8-channel mono only an option for RPI 5 + m_bQuadDAC8Chan = pConfig->GetQuadDAC8Chan (); +#endif + if (m_bQuadDAC8Chan) { + LOGNOTE ("Configured for Quad DAC 8-channel Mono audio"); + m_pSoundDevice = new CI2SSoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), + pConfig->GetChunkSize (), false, + pI2CMaster, pConfig->GetDACI2CAddress (), + CI2SSoundBaseDevice::DeviceModeTXOnly, + 8); // 8 channels - L+R x4 across 4 I2S lanes + } + else + { + m_pSoundDevice = new CI2SSoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), + pConfig->GetChunkSize (), false, + pI2CMaster, pConfig->GetDACI2CAddress (), + CI2SSoundBaseDevice::DeviceModeTXOnly, + 2); // 2 channels - L+R + } } else if (strcmp (pDeviceName, "hdmi") == 0) { @@ -251,18 +268,30 @@ bool CMiniDexed::Initialize (void) } // setup and start the sound device - if (!m_pSoundDevice->AllocateQueueFrames (m_pConfig->GetChunkSize ())) + int Channels = 1; // 16-bit Mono +#ifdef ARM_ALLOW_MULTI_CORE + if (m_bQuadDAC8Chan) + { + Channels = 8; // 16-bit 8-channel mono + } + else + { + Channels = 2; // 16-bit Stereo + } +#endif + // Need 2 x ChunkSize / Channel queue frames as the audio driver uses + // two DMA channels each of ChunkSize and one single single frame + // contains a sample for each of all the channels. + // + // See discussion here: https://github.com/rsta2/circle/discussions/453 + if (!m_pSoundDevice->AllocateQueueFrames (2 * m_pConfig->GetChunkSize () / Channels)) { LOGERR ("Cannot allocate sound queue"); return false; } -#ifndef ARM_ALLOW_MULTI_CORE - m_pSoundDevice->SetWriteFormat (SoundFormatSigned16, 1); // 16-bit Mono -#else - m_pSoundDevice->SetWriteFormat (SoundFormatSigned16, 2); // 16-bit Stereo -#endif + m_pSoundDevice->SetWriteFormat (SoundFormatSigned16, Channels); m_nQueueSizeFrames = m_pSoundDevice->GetQueueSizeFrames (); @@ -1128,85 +1157,130 @@ void CMiniDexed::ProcessSound (void) assert (CConfig::ToneGenerators == 8); - uint8_t indexL=0, indexR=1; - - // BEGIN TG mixing - float32_t tmp_float[nFrames*2]; - int16_t tmp_int[nFrames*2]; + if (m_bQuadDAC8Chan) { + // No mixing is performed by MiniDexed, sound is output in 8 channels. + // Note: one TG per audio channel; output=mono; no processing. + const int Channels = 8; // One TG per channel + float32_t tmp_float[nFrames*Channels]; + int16_t tmp_int[nFrames*Channels]; - if(nMasterVolume > 0.0) - { - for (uint8_t i = 0; i < CConfig::ToneGenerators; i++) + if(nMasterVolume > 0.0) { - tg_mixer->doAddMix(i,m_OutputLevel[i]); - reverb_send_mixer->doAddMix(i,m_OutputLevel[i]); + // Convert dual float array (8 chan) to single int16 array (8 chan) + for(uint16_t i=0; i0.0 && nMasterVolume <1.0) + { + tmp_float[(i*Channels)+tg]=m_OutputLevel[tg][i] * nMasterVolume; + } + else if(nMasterVolume == 1.0) + { + tmp_float[(i*Channels)+tg]=m_OutputLevel[tg][i]; + } + } + } + arm_float_to_q15(tmp_float,tmp_int,nFrames*Channels); } - // END TG mixing - - // BEGIN create SampleBuffer for holding audio data - float32_t SampleBuffer[2][nFrames]; - // END create SampleBuffer for holding audio data - - // get the mix of all TGs - tg_mixer->getMix(SampleBuffer[indexL], SampleBuffer[indexR]); - - // BEGIN adding reverb - if (m_nParameter[ParameterReverbEnable]) + else { - float32_t ReverbBuffer[2][nFrames]; - float32_t ReverbSendBuffer[2][nFrames]; - - arm_fill_f32(0.0f, ReverbBuffer[indexL], nFrames); - arm_fill_f32(0.0f, ReverbBuffer[indexR], nFrames); - arm_fill_f32(0.0f, ReverbSendBuffer[indexR], nFrames); - arm_fill_f32(0.0f, ReverbSendBuffer[indexL], nFrames); - - m_ReverbSpinLock.Acquire (); - - reverb_send_mixer->getMix(ReverbSendBuffer[indexL], ReverbSendBuffer[indexR]); - reverb->doReverb(ReverbSendBuffer[indexL],ReverbSendBuffer[indexR],ReverbBuffer[indexL], ReverbBuffer[indexR],nFrames); - - // scale down and add left reverb buffer by reverb level - arm_scale_f32(ReverbBuffer[indexL], reverb->get_level(), ReverbBuffer[indexL], nFrames); - arm_add_f32(SampleBuffer[indexL], ReverbBuffer[indexL], SampleBuffer[indexL], nFrames); - // scale down and add right reverb buffer by reverb level - arm_scale_f32(ReverbBuffer[indexR], reverb->get_level(), ReverbBuffer[indexR], nFrames); - arm_add_f32(SampleBuffer[indexR], ReverbBuffer[indexR], SampleBuffer[indexR], nFrames); - - m_ReverbSpinLock.Release (); + arm_fill_q15(0, tmp_int, nFrames*Channels); } - // END adding reverb - - // swap stereo channels if needed prior to writing back out - if (m_bChannelsSwapped) + + if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int)) { - indexL=1; - indexR=0; + LOGERR ("Sound data dropped"); } + } + else + { + // Mix everything down to stereo + uint8_t indexL=0, indexR=1; + + // BEGIN TG mixing + float32_t tmp_float[nFrames*2]; + int16_t tmp_int[nFrames*2]; - // Convert dual float array (left, right) to single int16 array (left/right) - for(uint16_t i=0; i 0.0) { - if(nMasterVolume >0.0 && nMasterVolume <1.0) + for (uint8_t i = 0; i < CConfig::ToneGenerators; i++) { - tmp_float[i*2]=SampleBuffer[indexL][i] * nMasterVolume; - tmp_float[(i*2)+1]=SampleBuffer[indexR][i] * nMasterVolume; + tg_mixer->doAddMix(i,m_OutputLevel[i]); + reverb_send_mixer->doAddMix(i,m_OutputLevel[i]); } - else if(nMasterVolume == 1.0) + // END TG mixing + + // BEGIN create SampleBuffer for holding audio data + float32_t SampleBuffer[2][nFrames]; + // END create SampleBuffer for holding audio data + + // get the mix of all TGs + tg_mixer->getMix(SampleBuffer[indexL], SampleBuffer[indexR]); + + // BEGIN adding reverb + if (m_nParameter[ParameterReverbEnable]) { - tmp_float[i*2]=SampleBuffer[indexL][i]; - tmp_float[(i*2)+1]=SampleBuffer[indexR][i]; + float32_t ReverbBuffer[2][nFrames]; + float32_t ReverbSendBuffer[2][nFrames]; + + arm_fill_f32(0.0f, ReverbBuffer[indexL], nFrames); + arm_fill_f32(0.0f, ReverbBuffer[indexR], nFrames); + arm_fill_f32(0.0f, ReverbSendBuffer[indexR], nFrames); + arm_fill_f32(0.0f, ReverbSendBuffer[indexL], nFrames); + + m_ReverbSpinLock.Acquire (); + + reverb_send_mixer->getMix(ReverbSendBuffer[indexL], ReverbSendBuffer[indexR]); + reverb->doReverb(ReverbSendBuffer[indexL],ReverbSendBuffer[indexR],ReverbBuffer[indexL], ReverbBuffer[indexR],nFrames); + + // scale down and add left reverb buffer by reverb level + arm_scale_f32(ReverbBuffer[indexL], reverb->get_level(), ReverbBuffer[indexL], nFrames); + arm_add_f32(SampleBuffer[indexL], ReverbBuffer[indexL], SampleBuffer[indexL], nFrames); + // scale down and add right reverb buffer by reverb level + arm_scale_f32(ReverbBuffer[indexR], reverb->get_level(), ReverbBuffer[indexR], nFrames); + arm_add_f32(SampleBuffer[indexR], ReverbBuffer[indexR], SampleBuffer[indexR], nFrames); + + m_ReverbSpinLock.Release (); } + // END adding reverb + + // swap stereo channels if needed prior to writing back out + if (m_bChannelsSwapped) + { + indexL=1; + indexR=0; + } + + // Convert dual float array (left, right) to single int16 array (left/right) + for(uint16_t i=0; i0.0 && nMasterVolume <1.0) + { + tmp_float[i*2]=SampleBuffer[indexL][i] * nMasterVolume; + tmp_float[(i*2)+1]=SampleBuffer[indexR][i] * nMasterVolume; + } + else if(nMasterVolume == 1.0) + { + tmp_float[i*2]=SampleBuffer[indexL][i]; + tmp_float[(i*2)+1]=SampleBuffer[indexR][i]; + } + } + arm_float_to_q15(tmp_float,tmp_int,nFrames*2); + } + else + { + arm_fill_q15(0, tmp_int, nFrames * 2); } - arm_float_to_q15(tmp_float,tmp_int,nFrames*2); - } - else - arm_fill_q15(0, tmp_int, nFrames * 2); - if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int)) - { - LOGERR ("Sound data dropped"); - } + if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int)) + { + LOGERR ("Sound data dropped"); + } + } // End of Stereo mixing if (m_bProfileEnabled) { diff --git a/src/minidexed.h b/src/minidexed.h index 8ca74c8..6e5e012 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -298,6 +298,7 @@ private: CPCKeyboard m_PCKeyboard; CSerialMIDIDevice m_SerialMIDI; bool m_bUseSerial; + bool m_bQuadDAC8Chan; CSoundBaseDevice *m_pSoundDevice; bool m_bChannelsSwapped; diff --git a/submod.sh b/submod.sh index c13dca3..f9524a3 100755 --- a/submod.sh +++ b/submod.sh @@ -12,7 +12,7 @@ cd - # # Optional update submodules explicitly cd circle-stdlib/libs/circle -git checkout 4155f43 +git checkout fff3764 cd - cd circle-stdlib/libs/circle-newlib #git checkout develop From 75e82caf72c2154c9c3c909835d09d005bbc24bc Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 5 Jul 2024 18:58:01 +0200 Subject: [PATCH 14/61] Try USBGadgetPin (#670) * Try USBGadgetPin * CGPIOPin usbGadgetPin(usbGadgetPinNumber, GPIOModeInputPullUp); * Remove extraneous line --- src/config.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config.cpp b/src/config.cpp index ea6bde3..ad55d13 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -22,6 +22,7 @@ // #include "config.h" #include "../Synth_Dexed/src/dexed.h" +#include CConfig::CConfig (FATFS *pFileSystem) : m_Properties ("minidexed.ini", pFileSystem) @@ -37,6 +38,13 @@ void CConfig::Load (void) m_Properties.Load (); m_bUSBGadgetMode = m_Properties.GetNumber ("USBGadget", 0) != 0; + unsigned usbGadgetPinNumber = m_Properties.GetNumber ("USBGadgetPin", 26); // Default to GPIO pin 26 if not specified + CGPIOPin usbGadgetPin(usbGadgetPinNumber, GPIOModeInputPullUp); + + if (usbGadgetPin.Read() == 0) // If the pin is pulled down + { + m_bUSBGadgetMode = true; + } m_SoundDevice = m_Properties.GetString ("SoundDevice", "pwm"); From 24a309ebb44fb4831ff5629c2cbf020008375d63 Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 11 Jul 2024 19:18:36 +0200 Subject: [PATCH 15/61] Add artifact links to PR and issues --- .github/workflows/pr-comment.yml | 78 ++++++++------------------------ 1 file changed, 20 insertions(+), 58 deletions(-) diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index c5c8ebb..179dc7b 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -1,64 +1,26 @@ -# https://nightly.link/ -# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml +# https://github.com/subsurface/subsurface/blob/master/.github/workflows/artifact-links.yml + +name: Add artifact links to pull request -name: Comment on pull request on: workflow_run: - workflows: ['Build'] + workflows: ["Build"] types: [completed] -jobs: - pr_comment: - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6 - with: - # This snippet is public-domain, taken from - # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml - script: | - async function upsertComment(owner, repo, issue_number, purpose, body) { - const {data: comments} = await github.rest.issues.listComments( - {owner, repo, issue_number}); - - const marker = ``; - body = marker + "\n" + body; - - const existing = comments.filter((c) => c.body.includes(marker)); - if (existing.length > 0) { - const last = existing[existing.length - 1]; - core.info(`Updating comment ${last.id}`); - await github.rest.issues.updateComment({ - owner, repo, - body, - comment_id: last.id, - }); - } else { - core.info(`Creating a comment in issue / PR #${issue_number}`); - await github.rest.issues.createComment({issue_number, body, owner, repo}); - } - } - const {owner, repo} = context.repo; - const run_id = ${{github.event.workflow_run.id}}; - - const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }}; - if (!pull_requests.length) { - return core.error("This workflow doesn't match any pull requests!"); - } - - const artifacts = await github.paginate( - github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id}); - if (!artifacts.length) { - return core.error(`No artifacts found`); - } - let body = `Download the artifacts for this pull request:\n`; - for (const art of artifacts) { - body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; - } - - core.info("Review thread message body:", body); +jobs: + artifacts-url-comments: + name: Add artifact links to PR and issues + runs-on: ubuntu-22.04 - for (const pr of pull_requests) { - await upsertComment(owner, repo, pr.number, - "nightly-link", body); - } + steps: + - name: Add artifact links to PR and issues + if: github.event.workflow_run.event == 'pull_request' + uses: tonyhallett/artifacts-url-comments@v1.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + prefix: "Build for testing:" + suffix: "Use at your own risk._" + format: name + addTo: pull + errorNoArtifacts: false From 6664e63fe517c26e0759c354dbeea67f128bfecd Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 11 Jul 2024 20:04:30 +0200 Subject: [PATCH 16/61] Fix typo [ci skip] --- .github/workflows/pr-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index 179dc7b..74b0c0f 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -20,7 +20,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: prefix: "Build for testing:" - suffix: "Use at your own risk._" + suffix: "Use at your own risk." format: name addTo: pull errorNoArtifacts: false From 3b033db4afd54cef2f7af2a22ec5957422393722 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Sat, 13 Jul 2024 17:21:09 +0100 Subject: [PATCH 17/61] Implement USBGadgetPin and appropriate boot messages. (#674) --- src/config.cpp | 27 ++++++++++++++++++--------- src/config.h | 7 ++++++- src/kernel.cpp | 28 ++++++++++++++++++++++++++-- src/minidexed.cpp | 32 +++++++++++++++++++++++++++++--- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index ad55d13..884d7cc 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -22,7 +22,6 @@ // #include "config.h" #include "../Synth_Dexed/src/dexed.h" -#include CConfig::CConfig (FATFS *pFileSystem) : m_Properties ("minidexed.ini", pFileSystem) @@ -37,14 +36,9 @@ void CConfig::Load (void) { m_Properties.Load (); - m_bUSBGadgetMode = m_Properties.GetNumber ("USBGadget", 0) != 0; - unsigned usbGadgetPinNumber = m_Properties.GetNumber ("USBGadgetPin", 26); // Default to GPIO pin 26 if not specified - CGPIOPin usbGadgetPin(usbGadgetPinNumber, GPIOModeInputPullUp); - - if (usbGadgetPin.Read() == 0) // If the pin is pulled down - { - m_bUSBGadgetMode = true; - } + m_bUSBGadget = m_Properties.GetNumber ("USBGadget", 0) != 0; + m_nUSBGadgetPin = m_Properties.GetNumber ("USBGadgetPin", 0); // Default OFF + SetUSBGadgetMode(m_bUSBGadget); // Might get overriden later by USBGadgetPin state m_SoundDevice = m_Properties.GetString ("SoundDevice", "pwm"); @@ -183,11 +177,26 @@ void CConfig::Load (void) m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); } +bool CConfig::GetUSBGadget (void) const +{ + return m_bUSBGadget; +} + +unsigned CConfig::GetUSBGadgetPin (void) const +{ + return m_nUSBGadgetPin; +} + bool CConfig::GetUSBGadgetMode (void) const { return m_bUSBGadgetMode; } +void CConfig::SetUSBGadgetMode (bool USBGadgetMode) +{ + m_bUSBGadgetMode = USBGadgetMode; +} + const char *CConfig::GetSoundDevice (void) const { return m_SoundDevice.c_str (); diff --git a/src/config.h b/src/config.h index 5944dd2..7497c21 100644 --- a/src/config.h +++ b/src/config.h @@ -68,7 +68,10 @@ public: void Load (void); // USB Mode - bool GetUSBGadgetMode (void) const; // true if in USB gadget mode + bool GetUSBGadget (void) const; + unsigned GetUSBGadgetPin (void) const; + bool GetUSBGadgetMode (void) const; // true if in USB gadget mode depending on USBGadget and USBGadgetPin + void SetUSBGadgetMode (bool USBGadgetMode); // Sound device const char *GetSoundDevice (void) const; @@ -192,6 +195,8 @@ public: private: CPropertiesFatFsFile m_Properties; + bool m_bUSBGadget; + unsigned m_nUSBGadgetPin; bool m_bUSBGadgetMode; std::string m_SoundDevice; diff --git a/src/kernel.cpp b/src/kernel.cpp index 33bc5f8..446747b 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -20,6 +20,7 @@ #include "kernel.h" #include #include +#include #include #include #include "usbminidexedmidigadget.h" @@ -92,8 +93,30 @@ bool CKernel::Initialize (void) m_pSPIMaster = nullptr; } } - - if (m_Config.GetUSBGadgetMode()) + + bool bUSBGadgetMode = false; + if (m_Config.GetUSBGadget()) + { + unsigned nUSBGadgetPin = m_Config.GetUSBGadgetPin(); + if (nUSBGadgetPin == 0) + { + // No hardware config option + bUSBGadgetMode = true; + } + else + { + // State of USB Gadget Mode determined by state of the pin. + // Pulled down = enable USB Gadget mode + CGPIOPin usbGadgetPin(nUSBGadgetPin, GPIOModeInputPullUp); + + if (usbGadgetPin.Read() == 0) + { + bUSBGadgetMode = true; + } + } + } + + if (bUSBGadgetMode) { #if RASPPI==5 #warning No support for USB Gadget Mode on RPI 5 yet @@ -107,6 +130,7 @@ bool CKernel::Initialize (void) // Run the USB stack in USB Host (default) mode m_pUSB = new CUSBHCIDevice (&mInterrupt, &mTimer, TRUE); } + m_Config.SetUSBGadgetMode(bUSBGadgetMode); if (!m_pUSB->Initialize ()) { diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 429fddc..da9e598 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -102,17 +102,43 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_pTG[i]->activate (); } - if (pConfig->GetUSBGadgetMode()) + unsigned nUSBGadgetPin = pConfig->GetUSBGadgetPin(); + bool bUSBGadget = pConfig->GetUSBGadget(); + bool bUSBGadgetMode = pConfig->GetUSBGadgetMode(); + + if (bUSBGadgetMode) { #if RASPPI==5 LOGNOTE ("USB Gadget (Device) Mode NOT supported on RPI 5"); #else - LOGNOTE ("USB In Gadget (Device) Mode"); + if (nUSBGadgetPin == 0) + { + LOGNOTE ("USB In Gadget (Device) Mode"); + } + else + { + LOGNOTE ("USB In Gadget (Device) Mode [USBGadgetPin %d = LOW]", nUSBGadgetPin); + } #endif } else { - LOGNOTE ("USB In Host Mode"); + if (bUSBGadget) + { + if (nUSBGadgetPin == 0) + { + // This shouldn't be possible... + LOGNOTE ("USB State Unknown"); + } + else + { + LOGNOTE ("USB In Host Mode [USBGadgetPin %d = HIGH]", nUSBGadgetPin); + } + } + else + { + LOGNOTE ("USB In Host Mode"); + } } for (unsigned i = 0; i < CConfig::MaxUSBMIDIDevices; i++) From dd5459eece66ca7292ae0b0aad032028775f8142 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 27 Jul 2024 11:53:20 +0200 Subject: [PATCH 18/61] Restrict permissions and pin "uses:" --- .github/workflows/pr-comment.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index 74b0c0f..863a6d8 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -12,10 +12,16 @@ jobs: name: Add artifact links to PR and issues runs-on: ubuntu-22.04 + # Restrict permissions for the GITHUB_TOKEN, https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + permissions: + issues: write + pull-requests: write + actions: read + steps: - name: Add artifact links to PR and issues if: github.event.workflow_run.event == 'pull_request' - uses: tonyhallett/artifacts-url-comments@v1.1.0 + uses: tonyhallett/artifacts-url-comments@0965ff1a7ae03c5c1644d3c30f956effea4e05ef # v1.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From f6ebd7871d0bc055224cf1b72278589a6be386ac Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Mon, 5 Aug 2024 08:39:32 +0100 Subject: [PATCH 19/61] Another attempt to support configuration TGs and polyphony (#690) * Initial commit for configuration TGs and polyphony across RPI1-5. * Ensure unused TGs in a performance are MIDI disabled. Set polyphony to higher defaults on Pi 4 and 5. * Actually, can just default to MIDI "disabled" directly in performance config if not present. * Fix issue with choosing max polyphony --- src/config.cpp | 56 ++++++++ src/config.h | 48 ++++++- src/mididevice.cpp | 10 +- src/mididevice.h | 2 +- src/minidexed.cpp | 284 +++++++++++++++++++++++++++----------- src/minidexed.h | 69 ++++----- src/performanceconfig.cpp | 126 +++++++++-------- src/performanceconfig.h | 62 +++++---- src/sysexfileloader.cpp | 17 +++ src/sysexfileloader.h | 1 + src/uimenu.cpp | 233 +++++++++++++++++++------------ src/uimenu.h | 6 +- src/userinterface.cpp | 2 +- 13 files changed, 618 insertions(+), 298 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 884d7cc..15a37ad 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -36,6 +36,20 @@ void CConfig::Load (void) { m_Properties.Load (); + // Number of Tone Generators and Polyphony + m_nToneGenerators = m_Properties.GetNumber ("ToneGenerators", DefToneGenerators); + m_nPolyphony = m_Properties.GetNumber ("Polyphony", DefaultNotes); + // At present there are only two options for tone generators: min or max + // and for the Pi 1,2,3 these are the same anyway. + if ((m_nToneGenerators != MinToneGenerators) && (m_nToneGenerators != AllToneGenerators)) + { + m_nToneGenerators = DefToneGenerators; + } + if (m_nPolyphony > MaxNotes) + { + m_nPolyphony = DefaultNotes; + } + m_bUSBGadget = m_Properties.GetNumber ("USBGadget", 0) != 0; m_nUSBGadgetPin = m_Properties.GetNumber ("USBGadgetPin", 0); // Default OFF SetUSBGadgetMode(m_bUSBGadget); // Might get overriden later by USBGadgetPin state @@ -177,6 +191,48 @@ void CConfig::Load (void) m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); } +unsigned CConfig::GetToneGenerators (void) const +{ + return m_nToneGenerators; +} + +unsigned CConfig::GetPolyphony (void) const +{ + return m_nPolyphony; +} + +unsigned CConfig::GetTGsCore1 (void) const +{ +#ifndef ARM_ALLOW_MULTI_CORE + return 0; +#else + if (m_nToneGenerators > MinToneGenerators) + { + return TGsCore1 + TGsCore1Opt; + } + else + { + return TGsCore1; + } +#endif +} + +unsigned CConfig::GetTGsCore23 (void) const +{ +#ifndef ARM_ALLOW_MULTI_CORE + return 0; +#else + if (m_nToneGenerators > MinToneGenerators) + { + return TGsCore23 + TGsCore23Opt; + } + else + { + return TGsCore23; + } +#endif +} + bool CConfig::GetUSBGadget (void) const { return m_bUSBGadget; diff --git a/src/config.h b/src/config.h index 7497c21..166c4bb 100644 --- a/src/config.h +++ b/src/config.h @@ -35,18 +35,49 @@ class CConfig // Configuration for MiniDexed { public: +// Set maximum, minimum and default numbers of tone generators, depending on Pi version. +// Actual number in can be changed via config settings for some Pis. #ifndef ARM_ALLOW_MULTI_CORE - static const unsigned ToneGenerators = 1; + // Pi V1 or Zero (single core) + static const unsigned MinToneGenerators = 1; + static const unsigned AllToneGenerators = 1; + static const unsigned DefToneGenerators = AllToneGenerators; #else +#if (RASPPI==4 || RASPPI==5) + // Pi 4 and 5 quad core + // These are max values, default is to support 8 in total with optional 16 TGs static const unsigned TGsCore1 = 2; // process 2 TGs on core 1 static const unsigned TGsCore23 = 3; // process 3 TGs on core 2 and 3 each - static const unsigned ToneGenerators = TGsCore1 + 2*TGsCore23; + static const unsigned TGsCore1Opt = 2; // process optional additional 2 TGs on core 1 + static const unsigned TGsCore23Opt = 3; // process optional additional 3 TGs on core 2 and 3 each + static const unsigned MinToneGenerators = TGsCore1 + 2*TGsCore23; + static const unsigned AllToneGenerators = TGsCore1 + TGsCore1Opt + 2*TGsCore23 + 2*TGsCore23Opt; + static const unsigned DefToneGenerators = MinToneGenerators; +#else + // Pi 2 or 3 quad core + static const unsigned TGsCore1 = 2; // process 2 TGs on core 1 + static const unsigned TGsCore23 = 3; // process 3 TGs on core 2 and 3 each + static const unsigned TGsCore1Opt = 0; + static const unsigned TGsCore23Opt = 0; + static const unsigned MinToneGenerators = TGsCore1 + 2*TGsCore23; + static const unsigned AllToneGenerators = MinToneGenerators; + static const unsigned DefToneGenerators = AllToneGenerators; #endif - +#endif + +// Set maximum polyphony, depending on PI version. This can be changed via config settings #if RASPPI == 1 - static const unsigned MaxNotes = 8; // polyphony + static const unsigned MaxNotes = 8; + static const unsigned DefaultNotes = 8; +#elif RASPPI == 4 + static const unsigned MaxNotes = 32; + static const unsigned DefaultNotes = 24; +#elif RASPPI == 5 + static const unsigned MaxNotes = 32; + static const unsigned DefaultNotes = 32; #else static const unsigned MaxNotes = 16; + static const unsigned DefaultNotes = 16; #endif static const unsigned MaxChunkSize = 4096; @@ -67,6 +98,12 @@ public: void Load (void); + // TGs and Polyphony + unsigned GetToneGenerators (void) const; + unsigned GetPolyphony (void) const; + unsigned GetTGsCore1 (void) const; + unsigned GetTGsCore23 (void) const; + // USB Mode bool GetUSBGadget (void) const; unsigned GetUSBGadgetPin (void) const; @@ -195,6 +232,9 @@ public: private: CPropertiesFatFsFile m_Properties; + unsigned m_nToneGenerators; + unsigned m_nPolyphony; + bool m_bUSBGadget; unsigned m_nUSBGadgetPin; bool m_bUSBGadgetMode; diff --git a/src/mididevice.cpp b/src/mididevice.cpp index 81314ba..504423e 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -65,7 +65,7 @@ CMIDIDevice::CMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInter m_pConfig (pConfig), m_pUI (pUI) { - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + for (unsigned nTG = 0; nTG < CConfig::AllToneGenerators; nTG++) { m_ChannelMap[nTG] = Disabled; } @@ -78,13 +78,13 @@ CMIDIDevice::~CMIDIDevice (void) void CMIDIDevice::SetChannel (u8 ucChannel, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_ChannelMap[nTG] = ucChannel; } u8 CMIDIDevice::GetChannel (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_ChannelMap[nTG]; } @@ -238,8 +238,8 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign break; } - // Process MIDI for each Tone Generator - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + // Process MIDI for each active Tone Generator + for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators(); nTG++) { if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN) { diff --git a/src/mididevice.h b/src/mididevice.h index 0fc027c..b7e7fe7 100644 --- a/src/mididevice.h +++ b/src/mididevice.h @@ -65,7 +65,7 @@ private: CConfig *m_pConfig; CUserInterface *m_pUI; - u8 m_ChannelMap[CConfig::ToneGenerators]; + u8 m_ChannelMap[CConfig::AllToneGenerators]; std::string m_DeviceName; diff --git a/src/minidexed.cpp b/src/minidexed.cpp index da9e598..2e45f46 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -46,7 +46,7 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_pSoundDevice (0), m_bChannelsSwapped (pConfig->GetChannelsSwapped ()), #ifdef ARM_ALLOW_MULTI_CORE - m_nActiveTGsLog2 (0), +// m_nActiveTGsLog2 (0), #endif m_GetChunkTimer ("GetChunk", 1000000U * pConfig->GetChunkSize ()/2 / pConfig->GetSampleRate ()), @@ -61,8 +61,12 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_bLoadPerformanceBankBusy(false) { assert (m_pConfig); + + m_nToneGenerators = m_pConfig->GetToneGenerators(); + m_nPolyphony = m_pConfig->GetPolyphony(); + LOGNOTE("Tone Generators=%d, Polyphony=%d", m_nToneGenerators, m_nPolyphony); - for (unsigned i = 0; i < CConfig::ToneGenerators; i++) + for (unsigned i = 0; i < CConfig::AllToneGenerators; i++) { m_nVoiceBankID[i] = 0; m_nVoiceBankIDMSB[i] = 0; @@ -93,15 +97,20 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_nAftertouchTarget[i]=0; m_nReverbSend[i] = 0; - m_uchOPMask[i] = 0b111111; // All operators on - m_pTG[i] = new CDexedAdapter (CConfig::MaxNotes, pConfig->GetSampleRate ()); - assert (m_pTG[i]); - - m_pTG[i]->setEngineType(pConfig->GetEngineType ()); - m_pTG[i]->activate (); + // Active the required number of active TGs + if (iGetSampleRate ()); + assert (m_pTG[i]); + + m_pTG[i]->setEngineType(pConfig->GetEngineType ()); + m_pTG[i]->activate (); + } } - + unsigned nUSBGadgetPin = pConfig->GetUSBGadgetPin(); bool bUSBGadget = pConfig->GetUSBGadget(); bool bUSBGadgetMode = pConfig->GetUSBGadgetMode(); @@ -156,6 +165,11 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, // Quad DAC 8-channel mono only an option for RPI 5 m_bQuadDAC8Chan = pConfig->GetQuadDAC8Chan (); #endif + if (m_bQuadDAC8Chan && (m_nToneGenerators != 8)) + { + LOGNOTE("ERROR: Quad DAC Mode is only valid when number of TGs = 8. Defaulting to non-Quad DAC mode,"); + m_bQuadDAC8Chan = false; + } if (m_bQuadDAC8Chan) { LOGNOTE ("Configured for Quad DAC 8-channel Mono audio"); m_pSoundDevice = new CI2SSoundBaseDevice (pInterrupt, pConfig->GetSampleRate (), @@ -206,11 +220,11 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, setMasterVolume(1.0); // BEGIN setup tg_mixer - tg_mixer = new AudioStereoMixer(pConfig->GetChunkSize()/2); + tg_mixer = new AudioStereoMixer(pConfig->GetChunkSize()/2); // END setup tgmixer // BEGIN setup reverb - reverb_send_mixer = new AudioStereoMixer(pConfig->GetChunkSize()/2); + reverb_send_mixer = new AudioStereoMixer(pConfig->GetChunkSize()/2); reverb = new AudioEffectPlateReverb(pConfig->GetSampleRate()); SetParameter (ParameterReverbEnable, 1); SetParameter (ParameterReverbSize, 70); @@ -261,7 +275,7 @@ bool CMiniDexed::Initialize (void) LOGNOTE("Program Change: Disabled"); } - for (unsigned i = 0; i < CConfig::ToneGenerators; i++) + for (unsigned i = 0; i < m_nToneGenerators; i++) { assert (m_pTG[i]); @@ -283,7 +297,7 @@ bool CMiniDexed::Initialize (void) reverb_send_mixer->gain(i,mapfloat(m_nReverbSend[i],0,99,0.0f,1.0f)); } - m_PerformanceConfig.Init(); + m_PerformanceConfig.Init(m_nToneGenerators); if (m_PerformanceConfig.Load ()) { LoadPerformanceParameters(); @@ -453,12 +467,16 @@ void CMiniDexed::Run (unsigned nCore) // process the TGs, assigned to this core (2 or 3) - assert (m_nFramesToProcess <= CConfig::MaxChunkSize); - unsigned nTG = CConfig::TGsCore1 + (nCore-2)*CConfig::TGsCore23; - for (unsigned i = 0; i < CConfig::TGsCore23; i++, nTG++) + assert (m_nFramesToProcess <= m_pConfig->MaxChunkSize); + unsigned nTG = m_pConfig->GetTGsCore1() + (nCore-2)*m_pConfig->GetTGsCore23(); + for (unsigned i = 0; i < m_pConfig->GetTGsCore23(); i++, nTG++) { - assert (m_pTG[nTG]); - m_pTG[nTG]->getSamples (m_OutputLevel[nTG],m_nFramesToProcess); + assert (nTG < CConfig::AllToneGenerators); + if (nTG < m_pConfig->GetToneGenerators()) + { + assert (m_pTG[nTG]); + m_pTG[nTG]->getSamples (m_OutputLevel[nTG],m_nFramesToProcess); + } } } } @@ -480,7 +498,8 @@ void CMiniDexed::BankSelect (unsigned nBank, unsigned nTG) { nBank=constrain((int)nBank,0,16383); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG if (GetSysExFileLoader ()->IsValidBank(nBank)) { @@ -509,7 +528,9 @@ void CMiniDexed::BankSelectMSB (unsigned nBankMSB, unsigned nTG) { nBankMSB=constrain((int)nBankMSB,0,127); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + // MIDI Spec 1.0 "BANK SELECT" states: // "The transmitter must transmit the MSB and LSB as a pair, // and the Program Change must be sent immediately after @@ -530,7 +551,9 @@ void CMiniDexed::BankSelectLSB (unsigned nBankLSB, unsigned nTG) { nBankLSB=constrain((int)nBankLSB,0,127); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + unsigned nBank = m_nVoiceBankID[nTG]; unsigned nBankMSB = m_nVoiceBankIDMSB[nTG]; nBank = (nBankMSB << 7) + nBankLSB; @@ -578,7 +601,9 @@ void CMiniDexed::ProgramChange (unsigned nProgram, unsigned nTG) nProgram=constrain((int)nProgram,0,31); } - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nProgram[nTG] = nProgram; uint8_t Buffer[156]; @@ -617,7 +642,9 @@ void CMiniDexed::SetVolume (unsigned nVolume, unsigned nTG) { nVolume=constrain((int)nVolume,0,127); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nVolume[nTG] = nVolume; assert (m_pTG[nTG]); @@ -630,7 +657,9 @@ void CMiniDexed::SetPan (unsigned nPan, unsigned nTG) { nPan=constrain((int)nPan,0,127); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nPan[nTG] = nPan; tg_mixer->pan(nTG,mapfloat(nPan,0,127,0.0f,1.0f)); @@ -643,7 +672,9 @@ void CMiniDexed::SetReverbSend (unsigned nReverbSend, unsigned nTG) { nReverbSend=constrain((int)nReverbSend,0,99); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nReverbSend[nTG] = nReverbSend; reverb_send_mixer->gain(nTG,mapfloat(nReverbSend,0,99,0.0f,1.0f)); @@ -655,7 +686,9 @@ void CMiniDexed::SetMasterTune (int nMasterTune, unsigned nTG) { nMasterTune=constrain((int)nMasterTune,-99,99); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nMasterTune[nTG] = nMasterTune; assert (m_pTG[nTG]); @@ -668,7 +701,9 @@ void CMiniDexed::SetCutoff (int nCutoff, unsigned nTG) { nCutoff = constrain (nCutoff, 0, 99); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nCutoff[nTG] = nCutoff; assert (m_pTG[nTG]); @@ -681,7 +716,9 @@ void CMiniDexed::SetResonance (int nResonance, unsigned nTG) { nResonance = constrain (nResonance, 0, 99); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + m_nResonance[nTG] = nResonance; assert (m_pTG[nTG]); @@ -694,7 +731,9 @@ void CMiniDexed::SetResonance (int nResonance, unsigned nTG) void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (uchChannel < CMIDIDevice::ChannelUnknown); m_nMIDIChannel[nTG] = uchChannel; @@ -713,6 +752,7 @@ void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG) } #ifdef ARM_ALLOW_MULTI_CORE +/* This doesn't appear to be used anywhere... unsigned nActiveTGs = 0; for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) { @@ -725,6 +765,7 @@ void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG) assert (nActiveTGs <= 8); static const unsigned Log2[] = {0, 0, 1, 2, 2, 3, 3, 3, 3}; m_nActiveTGsLog2 = Log2[nActiveTGs]; +*/ #endif m_UI.ParameterChanged (); @@ -732,7 +773,9 @@ void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG) void CMiniDexed::keyup (int16_t pitch, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); pitch = ApplyNoteLimits (pitch, nTG); @@ -744,7 +787,9 @@ void CMiniDexed::keyup (int16_t pitch, unsigned nTG) void CMiniDexed::keydown (int16_t pitch, uint8_t velocity, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); pitch = ApplyNoteLimits (pitch, nTG); @@ -756,7 +801,8 @@ void CMiniDexed::keydown (int16_t pitch, uint8_t velocity, unsigned nTG) int16_t CMiniDexed::ApplyNoteLimits (int16_t pitch, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return -1; // Not an active TG if ( pitch < (int16_t) m_nNoteLimitLow[nTG] || pitch > (int16_t) m_nNoteLimitHigh[nTG]) @@ -777,14 +823,18 @@ int16_t CMiniDexed::ApplyNoteLimits (int16_t pitch, unsigned nTG) void CMiniDexed::setSustain(bool sustain, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setSustain (sustain); } void CMiniDexed::panic(uint8_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); if (value == 0) { m_pTG[nTG]->panic (); @@ -793,7 +843,9 @@ void CMiniDexed::panic(uint8_t value, unsigned nTG) void CMiniDexed::notesOff(uint8_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); if (value == 0) { m_pTG[nTG]->notesOff (); @@ -802,7 +854,9 @@ void CMiniDexed::notesOff(uint8_t value, unsigned nTG) void CMiniDexed::setModWheel (uint8_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setModWheel (value); } @@ -810,35 +864,45 @@ void CMiniDexed::setModWheel (uint8_t value, unsigned nTG) void CMiniDexed::setFootController (uint8_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setFootController (value); } void CMiniDexed::setBreathController (uint8_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setBreathController (value); } void CMiniDexed::setAftertouch (uint8_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setAftertouch (value); } void CMiniDexed::setPitchbend (int16_t value, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setPitchbend (value); } void CMiniDexed::ControllersRefresh (unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->ControllersRefresh (); } @@ -853,7 +917,7 @@ void CMiniDexed::SetParameter (TParameter Parameter, int nValue) switch (Parameter) { case ParameterCompressorEnable: - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + for (unsigned nTG = 0; nTG < m_nToneGenerators; nTG++) { assert (m_pTG[nTG]); m_pTG[nTG]->setCompressor (!!nValue); @@ -931,7 +995,8 @@ int CMiniDexed::GetParameter (TParameter Parameter) void CMiniDexed::SetTGParameter (TTGParameter Parameter, int nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG switch (Parameter) { @@ -986,7 +1051,7 @@ void CMiniDexed::SetTGParameter (TTGParameter Parameter, int nValue, unsigned nT int CMiniDexed::GetTGParameter (TTGParameter Parameter, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); switch (Parameter) { @@ -1037,7 +1102,9 @@ int CMiniDexed::GetTGParameter (TTGParameter Parameter, unsigned nTG) void CMiniDexed::SetVoiceParameter (uint8_t uchOffset, uint8_t uchValue, unsigned nOP, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); assert (nOP <= 6); @@ -1070,7 +1137,9 @@ void CMiniDexed::SetVoiceParameter (uint8_t uchOffset, uint8_t uchValue, unsigne uint8_t CMiniDexed::GetVoiceParameter (uint8_t uchOffset, unsigned nOP, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return 0; // Not an active TG + assert (m_pTG[nTG]); assert (nOP <= 6); @@ -1094,13 +1163,15 @@ std::string CMiniDexed::GetVoiceName (unsigned nTG) { char VoiceName[11]; memset (VoiceName, 0, sizeof VoiceName); + VoiceName[0] = 32; // space + assert (nTG < CConfig::AllToneGenerators); - assert (nTG < CConfig::ToneGenerators); - assert (m_pTG[nTG]); - m_pTG[nTG]->setName (VoiceName); - + if (nTG < m_nToneGenerators) + { + assert (m_pTG[nTG]); + m_pTG[nTG]->setName (VoiceName); + } std::string Result (VoiceName); - return Result; } @@ -1142,6 +1213,7 @@ void CMiniDexed::ProcessSound (void) void CMiniDexed::ProcessSound (void) { assert (m_pSoundDevice); + assert (m_pConfig); unsigned nFrames = m_nQueueSizeFrames - m_pSoundDevice->GetQueueFramesAvail (); if (nFrames >= m_nQueueSizeFrames/2) @@ -1162,7 +1234,7 @@ void CMiniDexed::ProcessSound (void) // process the TGs assigned to core 1 assert (nFrames <= CConfig::MaxChunkSize); - for (unsigned i = 0; i < CConfig::TGsCore1; i++) + for (unsigned i = 0; i < m_pConfig->GetTGsCore1(); i++) { assert (m_pTG[i]); m_pTG[i]->getSamples (m_OutputLevel[i], nFrames); @@ -1181,9 +1253,10 @@ void CMiniDexed::ProcessSound (void) // Audio signal path after tone generators starts here // - assert (CConfig::ToneGenerators == 8); - if (m_bQuadDAC8Chan) { + // This is only supported when there are 8 TGs + assert (m_nToneGenerators == 8); + // No mixing is performed by MiniDexed, sound is output in 8 channels. // Note: one TG per audio channel; output=mono; no processing. const int Channels = 8; // One TG per channel @@ -1233,7 +1306,7 @@ void CMiniDexed::ProcessSound (void) if(nMasterVolume > 0.0) { - for (uint8_t i = 0; i < CConfig::ToneGenerators; i++) + for (uint8_t i = 0; i < m_nToneGenerators; i++) { tg_mixer->doAddMix(i,m_OutputLevel[i]); reverb_send_mixer->doAddMix(i,m_OutputLevel[i]); @@ -1358,7 +1431,7 @@ bool CMiniDexed::SavePerformance (bool bSaveAsDeault) bool CMiniDexed::DoSavePerformance (void) { - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + for (unsigned nTG = 0; nTG < CConfig::AllToneGenerators; nTG++) { m_PerformanceConfig.SetBankNumber (m_nVoiceBankID[nTG], nTG); m_PerformanceConfig.SetVoiceNumber (m_nProgram[nTG], nTG); @@ -1377,8 +1450,14 @@ bool CMiniDexed::DoSavePerformance (void) m_PerformanceConfig.SetNoteLimitLow (m_nNoteLimitLow[nTG], nTG); m_PerformanceConfig.SetNoteLimitHigh (m_nNoteLimitHigh[nTG], nTG); m_PerformanceConfig.SetNoteShift (m_nNoteShift[nTG], nTG); - m_pTG[nTG]->getVoiceData(m_nRawVoiceData); - m_PerformanceConfig.SetVoiceDataToTxt (m_nRawVoiceData, nTG); + if (nTG < m_pConfig->GetToneGenerators()) + { + m_pTG[nTG]->getVoiceData(m_nRawVoiceData); + } else { + // Not an active TG so provide default voice by asking for an invalid voice ID. + m_SysExFileLoader.GetVoice(CSysExFileLoader::MaxVoiceBankID, CSysExFileLoader::VoicesPerBank+1, m_nRawVoiceData); + } + m_PerformanceConfig.SetVoiceDataToTxt (m_nRawVoiceData, nTG); m_PerformanceConfig.SetMonoMode (m_bMonoMode[nTG], nTG); m_PerformanceConfig.SetModulationWheelRange (m_nModulationWheelRange[nTG], nTG); @@ -1412,7 +1491,9 @@ bool CMiniDexed::DoSavePerformance (void) void CMiniDexed::setMonoMode(uint8_t mono, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_bMonoMode[nTG]= mono != 0; m_pTG[nTG]->setMonoMode(constrain(mono, 0, 1)); @@ -1423,7 +1504,9 @@ void CMiniDexed::setMonoMode(uint8_t mono, uint8_t nTG) void CMiniDexed::setPitchbendRange(uint8_t range, uint8_t nTG) { range = constrain (range, 0, 12); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nPitchBendRange[nTG] = range; @@ -1435,7 +1518,9 @@ void CMiniDexed::setPitchbendRange(uint8_t range, uint8_t nTG) void CMiniDexed::setPitchbendStep(uint8_t step, uint8_t nTG) { step= constrain (step, 0, 12); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nPitchBendStep[nTG] = step; @@ -1448,7 +1533,9 @@ void CMiniDexed::setPortamentoMode(uint8_t mode, uint8_t nTG) { mode= constrain (mode, 0, 1); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nPortamentoMode[nTG] = mode; @@ -1460,7 +1547,9 @@ void CMiniDexed::setPortamentoMode(uint8_t mode, uint8_t nTG) void CMiniDexed::setPortamentoGlissando(uint8_t glissando, uint8_t nTG) { glissando = constrain (glissando, 0, 1); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nPortamentoGlissando[nTG] = glissando; @@ -1472,7 +1561,9 @@ void CMiniDexed::setPortamentoGlissando(uint8_t glissando, uint8_t nTG) void CMiniDexed::setPortamentoTime(uint8_t time, uint8_t nTG) { time = constrain (time, 0, 99); - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nPortamentoTime[nTG] = time; @@ -1483,7 +1574,9 @@ void CMiniDexed::setPortamentoTime(uint8_t time, uint8_t nTG) void CMiniDexed::setModWheelRange(uint8_t range, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nModulationWheelRange[nTG] = range; @@ -1496,7 +1589,9 @@ void CMiniDexed::setModWheelRange(uint8_t range, uint8_t nTG) void CMiniDexed::setModWheelTarget(uint8_t target, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nModulationWheelTarget[nTG] = target; @@ -1508,7 +1603,9 @@ void CMiniDexed::setModWheelTarget(uint8_t target, uint8_t nTG) void CMiniDexed::setFootControllerRange(uint8_t range, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nFootControlRange[nTG]=range; @@ -1521,7 +1618,9 @@ void CMiniDexed::setFootControllerRange(uint8_t range, uint8_t nTG) void CMiniDexed::setFootControllerTarget(uint8_t target, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nFootControlTarget[nTG] = target; @@ -1533,7 +1632,9 @@ void CMiniDexed::setFootControllerTarget(uint8_t target, uint8_t nTG) void CMiniDexed::setBreathControllerRange(uint8_t range, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nBreathControlRange[nTG]=range; @@ -1546,7 +1647,9 @@ void CMiniDexed::setBreathControllerRange(uint8_t range, uint8_t nTG) void CMiniDexed::setBreathControllerTarget(uint8_t target, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nBreathControlTarget[nTG]=target; @@ -1558,7 +1661,9 @@ void CMiniDexed::setBreathControllerTarget(uint8_t target, uint8_t nTG) void CMiniDexed::setAftertouchRange(uint8_t range, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nAftertouchRange[nTG]=range; @@ -1571,7 +1676,9 @@ void CMiniDexed::setAftertouchRange(uint8_t range, uint8_t nTG) void CMiniDexed::setAftertouchTarget(uint8_t target, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_nAftertouchTarget[nTG]=target; @@ -1583,7 +1690,9 @@ void CMiniDexed::setAftertouchTarget(uint8_t target, uint8_t nTG) void CMiniDexed::loadVoiceParameters(const uint8_t* data, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); uint8_t voice[161]; @@ -1604,7 +1713,9 @@ void CMiniDexed::loadVoiceParameters(const uint8_t* data, uint8_t nTG) void CMiniDexed::setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); m_pTG[nTG]->setVoiceDataElement(constrain(data, 0, 155),constrain(number, 0, 99)); @@ -1614,7 +1725,9 @@ void CMiniDexed::setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG) int16_t CMiniDexed::checkSystemExclusive(const uint8_t* pMessage,const uint16_t nLength, uint8_t nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return 0; // Not an active TG + assert (m_pTG[nTG]); return(m_pTG[nTG]->checkSystemExclusive(pMessage, nLength)); @@ -1625,10 +1738,17 @@ void CMiniDexed::getSysExVoiceDump(uint8_t* dest, uint8_t nTG) uint8_t checksum = 0; uint8_t data[155]; - assert (nTG < CConfig::ToneGenerators); - assert (m_pTG[nTG]); - - m_pTG[nTG]->getVoiceData(data); + assert (nTG < CConfig::AllToneGenerators); + if (nTG < m_nToneGenerators) + { + assert (m_pTG[nTG]); + m_pTG[nTG]->getVoiceData(data); + } + else + { + // Not an active TG so grab a default voice + m_SysExFileLoader.GetVoice(CSysExFileLoader::MaxVoiceBankID, CSysExFileLoader::VoicesPerBank+1, data); + } dest[0] = 0xF0; // SysEx start dest[1] = 0x43; // ID=Yamaha @@ -1791,7 +1911,7 @@ bool CMiniDexed::DoSavePerformanceNewFile (void) void CMiniDexed::LoadPerformanceParameters(void) { - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + for (unsigned nTG = 0; nTG < CConfig::AllToneGenerators; nTG++) { BankSelect (m_PerformanceConfig.GetBankNumber (nTG), nTG); @@ -1865,7 +1985,9 @@ bool CMiniDexed::IsValidPerformanceBank(unsigned nBankID) void CMiniDexed::SetVoiceName (std::string VoiceName, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + assert (m_pTG[nTG]); char Name[11]; strncpy(Name, VoiceName.c_str(),10); diff --git a/src/minidexed.h b/src/minidexed.h index 6e5e012..69dcf9c 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -230,7 +230,7 @@ public: private: int16_t ApplyNoteLimits (int16_t pitch, unsigned nTG); // returns < 0 to ignore note - uint8_t m_uchOPMask[CConfig::ToneGenerators]; + uint8_t m_uchOPMask[CConfig::AllToneGenerators]; void LoadPerformanceParameters(void); void ProcessSound (void); @@ -249,41 +249,44 @@ private: CConfig *m_pConfig; int m_nParameter[ParameterUnknown]; // global (non-TG) parameters + + unsigned m_nToneGenerators; + unsigned m_nPolyphony; - CDexedAdapter *m_pTG[CConfig::ToneGenerators]; + CDexedAdapter *m_pTG[CConfig::AllToneGenerators]; - unsigned m_nVoiceBankID[CConfig::ToneGenerators]; - unsigned m_nVoiceBankIDMSB[CConfig::ToneGenerators]; + unsigned m_nVoiceBankID[CConfig::AllToneGenerators]; + unsigned m_nVoiceBankIDMSB[CConfig::AllToneGenerators]; unsigned m_nVoiceBankIDPerformance; unsigned m_nVoiceBankIDMSBPerformance; - unsigned m_nProgram[CConfig::ToneGenerators]; - unsigned m_nVolume[CConfig::ToneGenerators]; - unsigned m_nPan[CConfig::ToneGenerators]; - int m_nMasterTune[CConfig::ToneGenerators]; - int m_nCutoff[CConfig::ToneGenerators]; - int m_nResonance[CConfig::ToneGenerators]; - unsigned m_nMIDIChannel[CConfig::ToneGenerators]; - unsigned m_nPitchBendRange[CConfig::ToneGenerators]; - unsigned m_nPitchBendStep[CConfig::ToneGenerators]; - unsigned m_nPortamentoMode[CConfig::ToneGenerators]; - unsigned m_nPortamentoGlissando[CConfig::ToneGenerators]; - unsigned m_nPortamentoTime[CConfig::ToneGenerators]; - bool m_bMonoMode[CConfig::ToneGenerators]; + unsigned m_nProgram[CConfig::AllToneGenerators]; + unsigned m_nVolume[CConfig::AllToneGenerators]; + unsigned m_nPan[CConfig::AllToneGenerators]; + int m_nMasterTune[CConfig::AllToneGenerators]; + int m_nCutoff[CConfig::AllToneGenerators]; + int m_nResonance[CConfig::AllToneGenerators]; + unsigned m_nMIDIChannel[CConfig::AllToneGenerators]; + unsigned m_nPitchBendRange[CConfig::AllToneGenerators]; + unsigned m_nPitchBendStep[CConfig::AllToneGenerators]; + unsigned m_nPortamentoMode[CConfig::AllToneGenerators]; + unsigned m_nPortamentoGlissando[CConfig::AllToneGenerators]; + unsigned m_nPortamentoTime[CConfig::AllToneGenerators]; + bool m_bMonoMode[CConfig::AllToneGenerators]; - unsigned m_nModulationWheelRange[CConfig::ToneGenerators]; - unsigned m_nModulationWheelTarget[CConfig::ToneGenerators]; - unsigned m_nFootControlRange[CConfig::ToneGenerators]; - unsigned m_nFootControlTarget[CConfig::ToneGenerators]; - unsigned m_nBreathControlRange[CConfig::ToneGenerators]; - unsigned m_nBreathControlTarget[CConfig::ToneGenerators]; - unsigned m_nAftertouchRange[CConfig::ToneGenerators]; - unsigned m_nAftertouchTarget[CConfig::ToneGenerators]; + unsigned m_nModulationWheelRange[CConfig::AllToneGenerators]; + unsigned m_nModulationWheelTarget[CConfig::AllToneGenerators]; + unsigned m_nFootControlRange[CConfig::AllToneGenerators]; + unsigned m_nFootControlTarget[CConfig::AllToneGenerators]; + unsigned m_nBreathControlRange[CConfig::AllToneGenerators]; + unsigned m_nBreathControlTarget[CConfig::AllToneGenerators]; + unsigned m_nAftertouchRange[CConfig::AllToneGenerators]; + unsigned m_nAftertouchTarget[CConfig::AllToneGenerators]; - unsigned m_nNoteLimitLow[CConfig::ToneGenerators]; - unsigned m_nNoteLimitHigh[CConfig::ToneGenerators]; - int m_nNoteShift[CConfig::ToneGenerators]; + unsigned m_nNoteLimitLow[CConfig::AllToneGenerators]; + unsigned m_nNoteLimitHigh[CConfig::AllToneGenerators]; + int m_nNoteShift[CConfig::AllToneGenerators]; - unsigned m_nReverbSend[CConfig::ToneGenerators]; + unsigned m_nReverbSend[CConfig::AllToneGenerators]; uint8_t m_nRawVoiceData[156]; @@ -305,18 +308,18 @@ private: unsigned m_nQueueSizeFrames; #ifdef ARM_ALLOW_MULTI_CORE - unsigned m_nActiveTGsLog2; +// unsigned m_nActiveTGsLog2; volatile TCoreStatus m_CoreStatus[CORES]; volatile unsigned m_nFramesToProcess; - float32_t m_OutputLevel[CConfig::ToneGenerators][CConfig::MaxChunkSize]; + float32_t m_OutputLevel[CConfig::AllToneGenerators][CConfig::MaxChunkSize]; #endif CPerformanceTimer m_GetChunkTimer; bool m_bProfileEnabled; AudioEffectPlateReverb* reverb; - AudioStereoMixer* tg_mixer; - AudioStereoMixer* reverb_send_mixer; + AudioStereoMixer* tg_mixer; + AudioStereoMixer* reverb_send_mixer; CSpinLock m_ReverbSpinLock; diff --git a/src/performanceconfig.cpp b/src/performanceconfig.cpp index 8cd6275..8a182e7 100644 --- a/src/performanceconfig.cpp +++ b/src/performanceconfig.cpp @@ -45,8 +45,20 @@ CPerformanceConfig::~CPerformanceConfig (void) { } -bool CPerformanceConfig::Init (void) +bool CPerformanceConfig::Init (unsigned nToneGenerators) { + // Different versions of Pi allow different TG configurations. + // On loading, performances will load up to the number of + // supported/active TGs. + // + // On saving, the active/supported number of TGs is used. + // + // This means that if an 8TG performance is loaded into + // a 16 TG system and then saved, the saved performance + // will include all 16 TG configurations. + // + m_nToneGenerators = nToneGenerators; + // Check intermal performance directory exists DIR Directory; FRESULT Result; @@ -95,7 +107,7 @@ bool CPerformanceConfig::Load (void) bool bResult = false; - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + for (unsigned nTG = 0; nTG < CConfig::AllToneGenerators; nTG++) { CString PropertyName; @@ -110,7 +122,7 @@ bool CPerformanceConfig::Load (void) } PropertyName.Format ("MIDIChannel%u", nTG+1); - unsigned nMIDIChannel = m_Properties.GetNumber (PropertyName, 255); + unsigned nMIDIChannel = m_Properties.GetNumber (PropertyName, 0); if (nMIDIChannel == 0) { m_nMIDIChannel[nTG] = CMIDIDevice::Disabled; @@ -217,7 +229,7 @@ bool CPerformanceConfig::Save (void) { m_Properties.RemoveAll (); - for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++) + for (unsigned nTG = 0; nTG < m_nToneGenerators; nTG++) { CString PropertyName; @@ -333,145 +345,145 @@ bool CPerformanceConfig::Save (void) unsigned CPerformanceConfig::GetBankNumber (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nBankNumber[nTG]; } unsigned CPerformanceConfig::GetVoiceNumber (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nVoiceNumber[nTG]; } unsigned CPerformanceConfig::GetMIDIChannel (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nMIDIChannel[nTG]; } unsigned CPerformanceConfig::GetVolume (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nVolume[nTG]; } unsigned CPerformanceConfig::GetPan (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nPan[nTG]; } int CPerformanceConfig::GetDetune (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nDetune[nTG]; } unsigned CPerformanceConfig::GetCutoff (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nCutoff[nTG]; } unsigned CPerformanceConfig::GetResonance (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nResonance[nTG]; } unsigned CPerformanceConfig::GetNoteLimitLow (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nNoteLimitLow[nTG]; } unsigned CPerformanceConfig::GetNoteLimitHigh (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nNoteLimitHigh[nTG]; } int CPerformanceConfig::GetNoteShift (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nNoteShift[nTG]; } unsigned CPerformanceConfig::GetReverbSend (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nReverbSend[nTG]; } void CPerformanceConfig::SetBankNumber (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nBankNumber[nTG] = nValue; } void CPerformanceConfig::SetVoiceNumber (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nVoiceNumber[nTG] = nValue; } void CPerformanceConfig::SetMIDIChannel (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nMIDIChannel[nTG] = nValue; } void CPerformanceConfig::SetVolume (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nVolume[nTG] = nValue; } void CPerformanceConfig::SetPan (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nPan[nTG] = nValue; } void CPerformanceConfig::SetDetune (int nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nDetune[nTG] = nValue; } void CPerformanceConfig::SetCutoff (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nCutoff[nTG] = nValue; } void CPerformanceConfig::SetResonance (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nResonance[nTG] = nValue; } void CPerformanceConfig::SetNoteLimitLow (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nNoteLimitLow[nTG] = nValue; } void CPerformanceConfig::SetNoteLimitHigh (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nNoteLimitHigh[nTG] = nValue; } void CPerformanceConfig::SetNoteShift (int nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nNoteShift[nTG] = nValue; } void CPerformanceConfig::SetReverbSend (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nReverbSend[nTG] = nValue; } @@ -557,71 +569,71 @@ void CPerformanceConfig::SetReverbLevel (unsigned nValue) // Pitch bender and portamento: void CPerformanceConfig::SetPitchBendRange (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nPitchBendRange[nTG] = nValue; } unsigned CPerformanceConfig::GetPitchBendRange (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nPitchBendRange[nTG]; } void CPerformanceConfig::SetPitchBendStep (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nPitchBendStep[nTG] = nValue; } unsigned CPerformanceConfig::GetPitchBendStep (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nPitchBendStep[nTG]; } void CPerformanceConfig::SetPortamentoMode (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nPortamentoMode[nTG] = nValue; } unsigned CPerformanceConfig::GetPortamentoMode (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nPortamentoMode[nTG]; } void CPerformanceConfig::SetPortamentoGlissando (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nPortamentoGlissando[nTG] = nValue; } unsigned CPerformanceConfig::GetPortamentoGlissando (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nPortamentoGlissando[nTG]; } void CPerformanceConfig::SetPortamentoTime (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nPortamentoTime[nTG] = nValue; } unsigned CPerformanceConfig::GetPortamentoTime (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nPortamentoTime[nTG]; } void CPerformanceConfig::SetMonoMode (bool bValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_bMonoMode[nTG] = bValue; } @@ -632,103 +644,103 @@ bool CPerformanceConfig::GetMonoMode (unsigned nTG) const void CPerformanceConfig::SetModulationWheelRange (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nModulationWheelRange[nTG] = nValue; } unsigned CPerformanceConfig::GetModulationWheelRange (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nModulationWheelRange[nTG]; } void CPerformanceConfig::SetModulationWheelTarget (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nModulationWheelTarget[nTG] = nValue; } unsigned CPerformanceConfig::GetModulationWheelTarget (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nModulationWheelTarget[nTG]; } void CPerformanceConfig::SetFootControlRange (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nFootControlRange[nTG] = nValue; } unsigned CPerformanceConfig::GetFootControlRange (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nFootControlRange[nTG]; } void CPerformanceConfig::SetFootControlTarget (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nFootControlTarget[nTG] = nValue; } unsigned CPerformanceConfig::GetFootControlTarget (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nFootControlTarget[nTG]; } void CPerformanceConfig::SetBreathControlRange (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nBreathControlRange[nTG] = nValue; } unsigned CPerformanceConfig::GetBreathControlRange (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nBreathControlRange[nTG]; } void CPerformanceConfig::SetBreathControlTarget (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nBreathControlTarget[nTG] = nValue; } unsigned CPerformanceConfig::GetBreathControlTarget (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nBreathControlTarget[nTG]; } void CPerformanceConfig::SetAftertouchRange (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nAftertouchRange[nTG] = nValue; } unsigned CPerformanceConfig::GetAftertouchRange (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nAftertouchRange[nTG]; } void CPerformanceConfig::SetAftertouchTarget (unsigned nValue, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nAftertouchTarget[nTG] = nValue; } unsigned CPerformanceConfig::GetAftertouchTarget (unsigned nTG) const { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); return m_nAftertouchTarget[nTG]; } void CPerformanceConfig::SetVoiceDataToTxt (const uint8_t *pData, unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); m_nVoiceDataTxt[nTG] = ""; char nDtoH[]="0123456789ABCDEF"; for (int i = 0; i < NUM_VOICE_PARAM; i++) @@ -744,7 +756,7 @@ void CPerformanceConfig::SetVoiceDataToTxt (const uint8_t *pData, unsigned nTG) uint8_t *CPerformanceConfig::GetVoiceDataFromTxt (unsigned nTG) { - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); static uint8_t pData[NUM_VOICE_PARAM]; std::string nHtoD="0123456789ABCDEF"; diff --git a/src/performanceconfig.h b/src/performanceconfig.h index 0d50daa..c9c4b27 100644 --- a/src/performanceconfig.h +++ b/src/performanceconfig.h @@ -36,7 +36,7 @@ public: CPerformanceConfig (FATFS *pFileSystem); ~CPerformanceConfig (void); - bool Init (void); + bool Init (unsigned nToneGenerators); bool Load (void); @@ -151,35 +151,37 @@ public: private: CPropertiesFatFsFile m_Properties; - - unsigned m_nBankNumber[CConfig::ToneGenerators]; - unsigned m_nVoiceNumber[CConfig::ToneGenerators]; - unsigned m_nMIDIChannel[CConfig::ToneGenerators]; - unsigned m_nVolume[CConfig::ToneGenerators]; - unsigned m_nPan[CConfig::ToneGenerators]; - int m_nDetune[CConfig::ToneGenerators]; - unsigned m_nCutoff[CConfig::ToneGenerators]; - unsigned m_nResonance[CConfig::ToneGenerators]; - unsigned m_nNoteLimitLow[CConfig::ToneGenerators]; - unsigned m_nNoteLimitHigh[CConfig::ToneGenerators]; - int m_nNoteShift[CConfig::ToneGenerators]; - int m_nReverbSend[CConfig::ToneGenerators]; - unsigned m_nPitchBendRange[CConfig::ToneGenerators]; - unsigned m_nPitchBendStep[CConfig::ToneGenerators]; - unsigned m_nPortamentoMode[CConfig::ToneGenerators]; - unsigned m_nPortamentoGlissando[CConfig::ToneGenerators]; - unsigned m_nPortamentoTime[CConfig::ToneGenerators]; - std::string m_nVoiceDataTxt[CConfig::ToneGenerators]; - bool m_bMonoMode[CConfig::ToneGenerators]; - - unsigned m_nModulationWheelRange[CConfig::ToneGenerators]; - unsigned m_nModulationWheelTarget[CConfig::ToneGenerators]; - unsigned m_nFootControlRange[CConfig::ToneGenerators]; - unsigned m_nFootControlTarget[CConfig::ToneGenerators]; - unsigned m_nBreathControlRange[CConfig::ToneGenerators]; - unsigned m_nBreathControlTarget[CConfig::ToneGenerators]; - unsigned m_nAftertouchRange[CConfig::ToneGenerators]; - unsigned m_nAftertouchTarget[CConfig::ToneGenerators]; + + unsigned m_nToneGenerators; + + unsigned m_nBankNumber[CConfig::AllToneGenerators]; + unsigned m_nVoiceNumber[CConfig::AllToneGenerators]; + unsigned m_nMIDIChannel[CConfig::AllToneGenerators]; + unsigned m_nVolume[CConfig::AllToneGenerators]; + unsigned m_nPan[CConfig::AllToneGenerators]; + int m_nDetune[CConfig::AllToneGenerators]; + unsigned m_nCutoff[CConfig::AllToneGenerators]; + unsigned m_nResonance[CConfig::AllToneGenerators]; + unsigned m_nNoteLimitLow[CConfig::AllToneGenerators]; + unsigned m_nNoteLimitHigh[CConfig::AllToneGenerators]; + int m_nNoteShift[CConfig::AllToneGenerators]; + int m_nReverbSend[CConfig::AllToneGenerators]; + unsigned m_nPitchBendRange[CConfig::AllToneGenerators]; + unsigned m_nPitchBendStep[CConfig::AllToneGenerators]; + unsigned m_nPortamentoMode[CConfig::AllToneGenerators]; + unsigned m_nPortamentoGlissando[CConfig::AllToneGenerators]; + unsigned m_nPortamentoTime[CConfig::AllToneGenerators]; + std::string m_nVoiceDataTxt[CConfig::AllToneGenerators]; + bool m_bMonoMode[CConfig::AllToneGenerators]; + + unsigned m_nModulationWheelRange[CConfig::AllToneGenerators]; + unsigned m_nModulationWheelTarget[CConfig::AllToneGenerators]; + unsigned m_nFootControlRange[CConfig::AllToneGenerators]; + unsigned m_nFootControlTarget[CConfig::AllToneGenerators]; + unsigned m_nBreathControlRange[CConfig::AllToneGenerators]; + unsigned m_nBreathControlTarget[CConfig::AllToneGenerators]; + unsigned m_nAftertouchRange[CConfig::AllToneGenerators]; + unsigned m_nAftertouchTarget[CConfig::AllToneGenerators]; unsigned m_nLastPerformance; unsigned m_nActualPerformance = 0; diff --git a/src/sysexfileloader.cpp b/src/sysexfileloader.cpp index ea5ba5c..6b83f5b 100644 --- a/src/sysexfileloader.cpp +++ b/src/sysexfileloader.cpp @@ -273,6 +273,23 @@ std::string CSysExFileLoader::GetBankName (unsigned nBankID) return "NO NAME"; } +std::string CSysExFileLoader::GetVoiceName (unsigned nBankID, unsigned nVoiceID) +{ + if ((nBankID <= MaxVoiceBankID) && (nVoiceID < VoicesPerBank)) + { + if (IsValidBank(nBankID)) + { + // The name is the last 10 characters of the voice data + char sVoiceName[11]; + strncpy (sVoiceName, (char *)((char *)&(m_pVoiceBank[nBankID]->Voice[nVoiceID]) + SizePackedVoice - 10), 10); + sVoiceName[10] = 0; + std::string result(sVoiceName); + return result; + } + } + return "INIT VOICE"; +} + unsigned CSysExFileLoader::GetNextBankUp (unsigned nBankID) { // Find the next loaded bank "up" from the provided bank ID diff --git a/src/sysexfileloader.h b/src/sysexfileloader.h index 4918db6..272d775 100644 --- a/src/sysexfileloader.h +++ b/src/sysexfileloader.h @@ -60,6 +60,7 @@ public: void Load (bool bHeaderlessSysExVoices = false); std::string GetBankName (unsigned nBankID); // 0 .. MaxVoiceBankID + std::string GetVoiceName (unsigned nBankID, unsigned nVoice); // 0 .. MaxVoiceBankID, 0 .. VoicesPerBank-1 unsigned GetNumHighestBank (); // 0 .. MaxVoiceBankID bool IsValidBank (unsigned nBankID); unsigned GetNextBankUp (unsigned nBankID); diff --git a/src/uimenu.cpp b/src/uimenu.cpp index 82a426a..8b72425 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -51,6 +51,16 @@ const CUIMenu::TMenuItem CUIMenu::s_MainMenu[] = {"TG6", MenuHandler, s_TGMenu, 5}, {"TG7", MenuHandler, s_TGMenu, 6}, {"TG8", MenuHandler, s_TGMenu, 7}, +#if (RASPPI==4 || RASPPI==5) + {"TG9", MenuHandler, s_TGMenu, 8}, + {"TG10", MenuHandler, s_TGMenu, 9}, + {"TG11", MenuHandler, s_TGMenu, 10}, + {"TG12", MenuHandler, s_TGMenu, 11}, + {"TG13", MenuHandler, s_TGMenu, 12}, + {"TG14", MenuHandler, s_TGMenu, 13}, + {"TG15", MenuHandler, s_TGMenu, 14}, + {"TG16", MenuHandler, s_TGMenu, 15}, +#endif #endif {"Effects", MenuHandler, s_EffectsMenu}, {"Performance", MenuHandler, s_PerformanceMenu}, @@ -334,9 +344,10 @@ const CUIMenu::TMenuItem CUIMenu::s_PerformanceMenu[] = }; -CUIMenu::CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed) +CUIMenu::CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed, CConfig *pConfig) : m_pUI (pUI), m_pMiniDexed (pMiniDexed), + m_pConfig (pConfig), m_pParentMenu (s_MenuRoot), m_pCurrentMenu (s_MainMenu), m_nCurrentMenuItem (0), @@ -344,23 +355,27 @@ CUIMenu::CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed) m_nCurrentParameter (0), m_nCurrentMenuDepth (0) { -#ifndef ARM_ALLOW_MULTI_CORE - // If there is just one core, then there is only a single - // tone generator so start on the TG1 menu... - m_pParentMenu = s_MainMenu; - m_pCurrentMenu = s_TGMenu; - m_nCurrentMenuItem = 0; - m_nCurrentSelection = 0; - m_nCurrentParameter = 0; - m_nCurrentMenuDepth = 1; + assert (m_pConfig); + m_nToneGenerators = m_pConfig->GetToneGenerators(); - // Place the "root" menu at the top of the stack - m_MenuStackParent[0] = s_MenuRoot; - m_MenuStackMenu[0] = s_MainMenu; - m_nMenuStackItem[0] = 0; - m_nMenuStackSelection[0] = 0; - m_nMenuStackParameter[0] = 0; -#endif + if (m_nToneGenerators == 1) + { + // If there is just one core, then there is only a single + // tone generator so start on the TG1 menu... + m_pParentMenu = s_MainMenu; + m_pCurrentMenu = s_TGMenu; + m_nCurrentMenuItem = 0; + m_nCurrentSelection = 0; + m_nCurrentParameter = 0; + m_nCurrentMenuDepth = 1; + + // Place the "root" menu at the top of the stack + m_MenuStackParent[0] = s_MenuRoot; + m_MenuStackMenu[0] = s_MainMenu; + m_nMenuStackItem[0] = 0; + m_nMenuStackSelection[0] = 0; + m_nMenuStackParameter[0] = 0; + } } void CUIMenu::EventHandler (TMenuEvent Event) @@ -383,28 +398,31 @@ void CUIMenu::EventHandler (TMenuEvent Event) break; case MenuEventHome: -#ifdef ARM_ALLOW_MULTI_CORE - m_pParentMenu = s_MenuRoot; - m_pCurrentMenu = s_MainMenu; - m_nCurrentMenuItem = 0; - m_nCurrentSelection = 0; - m_nCurrentParameter = 0; - m_nCurrentMenuDepth = 0; -#else - // "Home" is the TG0 menu if only one TG active - m_pParentMenu = s_MainMenu; - m_pCurrentMenu = s_TGMenu; - m_nCurrentMenuItem = 0; - m_nCurrentSelection = 0; - m_nCurrentParameter = 0; - m_nCurrentMenuDepth = 1; - // Place the "root" menu at the top of the stack - m_MenuStackParent[0] = s_MenuRoot; - m_MenuStackMenu[0] = s_MainMenu; - m_nMenuStackItem[0] = 0; - m_nMenuStackSelection[0] = 0; - m_nMenuStackParameter[0] = 0; -#endif + if (m_nToneGenerators == 1) + { + // "Home" is the TG0 menu if only one TG active + m_pParentMenu = s_MainMenu; + m_pCurrentMenu = s_TGMenu; + m_nCurrentMenuItem = 0; + m_nCurrentSelection = 0; + m_nCurrentParameter = 0; + m_nCurrentMenuDepth = 1; + // Place the "root" menu at the top of the stack + m_MenuStackParent[0] = s_MenuRoot; + m_MenuStackMenu[0] = s_MainMenu; + m_nMenuStackItem[0] = 0; + m_nMenuStackSelection[0] = 0; + m_nMenuStackParameter[0] = 0; + } + else + { + m_pParentMenu = s_MenuRoot; + m_pCurrentMenu = s_MainMenu; + m_nCurrentMenuItem = 0; + m_nCurrentSelection = 0; + m_nCurrentParameter = 0; + m_nCurrentMenuDepth = 0; + } EventHandler (MenuEventUpdate); break; @@ -453,7 +471,30 @@ void CUIMenu::MenuHandler (CUIMenu *pUIMenu, TMenuEvent Event) break; case MenuEventStepDown: - if (pUIMenu->m_nCurrentSelection > 0) + if (pUIMenu->m_nCurrentSelection == 0) + { + // If in main mennu, wrap around + if (pUIMenu->m_pCurrentMenu == s_MainMenu) + { + // Find last entry with a name + while (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name) + { + pUIMenu->m_nCurrentSelection++; + } + } + } + else if (pUIMenu->m_nCurrentSelection > 0) + { + pUIMenu->m_nCurrentSelection--; + } + // Might need to trim menu if number of TGs is configured to be less than the maximum supported + while ((pUIMenu->m_pCurrentMenu == s_MainMenu) && (pUIMenu->m_nCurrentSelection > 0) && + ( // Skip any unused menus + (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].MenuItem == s_TGMenu) && + (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter >= pUIMenu->m_nToneGenerators) && + (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter < CConfig::AllToneGenerators) + ) + ) { pUIMenu->m_nCurrentSelection--; } @@ -463,7 +504,27 @@ void CUIMenu::MenuHandler (CUIMenu *pUIMenu, TMenuEvent Event) ++pUIMenu->m_nCurrentSelection; if (!pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Name) // more entries? { - pUIMenu->m_nCurrentSelection--; + if (pUIMenu->m_pCurrentMenu == s_MainMenu) + { + // If in main mennu, wrap around + pUIMenu->m_nCurrentSelection = 0; + } + else + { + // Return to last known good item + pUIMenu->m_nCurrentSelection--; + } + } + // Might need to trim menu if number of TGs is configured to be less than the maximum supported + while ((pUIMenu->m_pCurrentMenu == s_MainMenu) && (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name) && + ( // Skip any unused TG menus + (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].MenuItem == s_TGMenu) && + (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter >= pUIMenu->m_nToneGenerators) && + (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter < CConfig::AllToneGenerators) + ) + ) + { + pUIMenu->m_nCurrentSelection++; } break; @@ -1151,7 +1212,7 @@ void CUIMenu::TGShortcutHandler (TMenuEvent Event) assert (m_nCurrentMenuDepth >= 2); assert (m_MenuStackMenu[0] = s_MainMenu); unsigned nTG = m_nMenuStackSelection[0]; - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); assert (m_nMenuStackItem[1] == nTG); assert (m_nMenuStackParameter[1] == nTG); @@ -1166,7 +1227,7 @@ void CUIMenu::TGShortcutHandler (TMenuEvent Event) nTG++; } - if (nTG < CConfig::ToneGenerators) + if (nTG < m_nToneGenerators) { m_nMenuStackSelection[0] = nTG; m_nMenuStackItem[1] = nTG; @@ -1262,51 +1323,53 @@ void CUIMenu::PgmUpDownHandler (TMenuEvent Event) if (m_MenuStackMenu[0] == s_MainMenu && (m_pCurrentMenu == s_TGMenu) || (m_MenuStackMenu[1] == s_TGMenu)) { nTG = m_nMenuStackSelection[0]; } - assert (nTG < CConfig::ToneGenerators); - - int nPgm = m_pMiniDexed->GetTGParameter (CMiniDexed::TGParameterProgram, nTG); - - assert (Event == MenuEventPgmDown || Event == MenuEventPgmUp); - if (Event == MenuEventPgmDown) + assert (nTG < CConfig::AllToneGenerators); + if (nTG < m_nToneGenerators) { - //LOGNOTE("PgmDown"); - if (--nPgm < 0) + int nPgm = m_pMiniDexed->GetTGParameter (CMiniDexed::TGParameterProgram, nTG); + + assert (Event == MenuEventPgmDown || Event == MenuEventPgmUp); + if (Event == MenuEventPgmDown) { - // Switch down a voice bank and set to the last voice - nPgm = CSysExFileLoader::VoicesPerBank-1; - int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG); - nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankDown(nVB); - m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG); + //LOGNOTE("PgmDown"); + if (--nPgm < 0) + { + // Switch down a voice bank and set to the last voice + nPgm = CSysExFileLoader::VoicesPerBank-1; + int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG); + nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankDown(nVB); + m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG); + } + m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG); } - m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG); - } - else - { - //LOGNOTE("PgmUp"); - if (++nPgm > (int) CSysExFileLoader::VoicesPerBank-1) + else { - // Switch up a voice bank and reset to voice 0 - nPgm = 0; - int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG); - nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankUp(nVB); - m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG); + //LOGNOTE("PgmUp"); + if (++nPgm > (int) CSysExFileLoader::VoicesPerBank-1) + { + // Switch up a voice bank and reset to voice 0 + nPgm = 0; + int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG); + nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankUp(nVB); + m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG); + } + m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG); } - m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG); - } - // Skip empty voices. - // Use same criteria in EditProgramNumber () too. - string voiceName = m_pMiniDexed->GetVoiceName (nTG); - if (voiceName == "EMPTY " - || voiceName == " " - || voiceName == "----------" - || voiceName == "~~~~~~~~~~" ) - { - if (Event == MenuEventPgmUp) { - PgmUpDownHandler (MenuEventPgmUp); - } - if (Event == MenuEventPgmDown) { - PgmUpDownHandler (MenuEventPgmDown); + // Skip empty voices. + // Use same criteria in EditProgramNumber () too. + string voiceName = m_pMiniDexed->GetVoiceName (nTG); + if (voiceName == "EMPTY " + || voiceName == " " + || voiceName == "----------" + || voiceName == "~~~~~~~~~~" ) + { + if (Event == MenuEventPgmUp) { + PgmUpDownHandler (MenuEventPgmUp); + } + if (Event == MenuEventPgmDown) { + PgmUpDownHandler (MenuEventPgmDown); + } } } } @@ -1317,7 +1380,7 @@ void CUIMenu::TGUpDownHandler (TMenuEvent Event) // This will update the menus to position it for the next TG up or down unsigned nTG = 0; - if (CConfig::ToneGenerators <= 1) { + if (m_nToneGenerators <= 1) { // Nothing to do if only a single TG return; } @@ -1328,7 +1391,7 @@ void CUIMenu::TGUpDownHandler (TMenuEvent Event) nTG = m_nMenuStackSelection[0]; } - assert (nTG < CConfig::ToneGenerators); + assert (nTG < CConfig::AllToneGenerators); assert (Event == MenuEventTGDown || Event == MenuEventTGUp); if (Event == MenuEventTGDown) { @@ -1340,7 +1403,7 @@ void CUIMenu::TGUpDownHandler (TMenuEvent Event) else { //LOGNOTE("TGUp"); - if (nTG < CConfig::ToneGenerators - 1) { + if (nTG < m_nToneGenerators - 1) { nTG++; } } diff --git a/src/uimenu.h b/src/uimenu.h index d5b48dc..0034278 100644 --- a/src/uimenu.h +++ b/src/uimenu.h @@ -25,6 +25,7 @@ #include #include +#include "config.h" class CMiniDexed; class CUserInterface; @@ -53,7 +54,7 @@ public: }; public: - CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed); + CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed, CConfig *pConfig); void EventHandler (TMenuEvent Event); @@ -128,6 +129,9 @@ private: private: CUserInterface *m_pUI; CMiniDexed *m_pMiniDexed; + CConfig *m_pConfig; + + unsigned m_nToneGenerators; const TMenuItem *m_pParentMenu; const TMenuItem *m_pCurrentMenu; diff --git a/src/userinterface.cpp b/src/userinterface.cpp index 9e7c112..aa46f9e 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -38,7 +38,7 @@ CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManag m_pUIButtons (0), m_pRotaryEncoder (0), m_bSwitchPressed (false), - m_Menu (this, pMiniDexed) + m_Menu (this, pMiniDexed, pConfig) { } From e03122a3d232e7b20e5ccbb54de6f0dff5f7666f Mon Sep 17 00:00:00 2001 From: probonopd Date: Wed, 7 Aug 2024 18:24:32 +0200 Subject: [PATCH 20/61] Stargazers over time [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index dcb9d84..68cdb02 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,6 @@ This project stands on the shoulders of giants. Special thanks to: - [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed - [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault) - [diyelectromusic](https://github.com/diyelectromusic/) for many [contributions](https://github.com/probonopd/MiniDexed/commits?author=diyelectromusic) + +## Stargazers over time +[![Stargazers over time](https://starchart.cc/probonopd/MiniDexed.svg?variant=adaptive)](https://starchart.cc/probonopd/MiniDexed) From 6c56960888c1ab144f0229ca3d0c323027302a3f Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 8 Sep 2024 13:58:41 +0200 Subject: [PATCH 21/61] Lower debounce time (#714) https://github.com/probonopd/MiniDexed/issues/713 --- src/uibuttons.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uibuttons.h b/src/uibuttons.h index 47d128d..44dcca0 100644 --- a/src/uibuttons.h +++ b/src/uibuttons.h @@ -26,7 +26,7 @@ #include "config.h" #define BUTTONS_UPDATE_NUM_TICKS 100 -#define DEBOUNCE_TIME 100 +#define DEBOUNCE_TIME 20 #define MAX_GPIO_BUTTONS 9 // 5 UI buttons, 4 Program/TG Select buttons #define MAX_MIDI_BUTTONS 9 #define MAX_BUTTONS (MAX_GPIO_BUTTONS+MAX_MIDI_BUTTONS) From 8c18e60e5133a56e7208c4ff051cc2ec80e0aaa1 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:29:26 +0100 Subject: [PATCH 22/61] Initial implementation of system-level, i.e. all TG, MIDI control maps (#704) * Initial implementation of system-level, i.e. all TG, MIDI control maps * Ensure msg only handled once by the first TG with a matching receiving MIDI channel. * Optimise system CC handling esp when unrecognised CCs are received. * Update in optimisation of System CC handling. --- src/config.cpp | 23 +++++++++- src/config.h | 10 ++++- src/mididevice.cpp | 104 ++++++++++++++++++++++++++++++++++++++++++++- src/mididevice.h | 9 ++++ 4 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 15a37ad..5214713 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -108,6 +108,10 @@ void CConfig::Load (void) m_bMIDIAutoVoiceDumpOnPC = m_Properties.GetNumber ("MIDIAutoVoiceDumpOnPC", 0) != 0; m_bHeaderlessSysExVoices = m_Properties.GetNumber ("HeaderlessSysExVoices", 0) != 0; m_bExpandPCAcrossBanks = m_Properties.GetNumber ("ExpandPCAcrossBanks", 1) != 0; + + m_nMIDISystemCCVol = m_Properties.GetNumber ("MIDISystemCCVol", 0); + m_nMIDISystemCCPan = m_Properties.GetNumber ("MIDISystemCCPan", 0); + m_nMIDISystemCCDetune = m_Properties.GetNumber ("MIDISystemCCDetune", 0); m_bLCDEnabled = m_Properties.GetNumber ("LCDEnabled", 0) != 0; m_nLCDPinEnable = m_Properties.GetNumber ("LCDPinEnable", 4); @@ -283,6 +287,11 @@ unsigned CConfig::GetEngineType (void) const return m_EngineType; } +bool CConfig::GetQuadDAC8Chan (void) const +{ + return m_bQuadDAC8Chan; +} + unsigned CConfig::GetMIDIBaudRate (void) const { return m_nMIDIBaudRate; @@ -323,9 +332,19 @@ bool CConfig::GetExpandPCAcrossBanks (void) const return m_bExpandPCAcrossBanks; } -bool CConfig::GetQuadDAC8Chan (void) const +unsigned CConfig::GetMIDISystemCCVol (void) const { - return m_bQuadDAC8Chan; + return m_nMIDISystemCCVol; +} + +unsigned CConfig::GetMIDISystemCCPan (void) const +{ + return m_nMIDISystemCCPan; +} + +unsigned CConfig::GetMIDISystemCCDetune (void) const +{ + return m_nMIDISystemCCDetune; } bool CConfig::GetLCDEnabled (void) const diff --git a/src/config.h b/src/config.h index 166c4bb..a454d90 100644 --- a/src/config.h +++ b/src/config.h @@ -117,6 +117,7 @@ public: unsigned GetDACI2CAddress (void) const; // 0 for auto probing bool GetChannelsSwapped (void) const; unsigned GetEngineType (void) const; + bool GetQuadDAC8Chan (void) const; // false if not specified // MIDI unsigned GetMIDIBaudRate (void) const; @@ -127,7 +128,9 @@ public: bool GetMIDIAutoVoiceDumpOnPC (void) const; // false if not specified bool GetHeaderlessSysExVoices (void) const; // false if not specified bool GetExpandPCAcrossBanks (void) const; // true if not specified - bool GetQuadDAC8Chan (void) const; // false if not specified + unsigned GetMIDISystemCCVol (void) const; + unsigned GetMIDISystemCCPan (void) const; + unsigned GetMIDISystemCCDetune (void) const; // HD44780 LCD // GPIO pin numbers are chip numbers, not header positions @@ -245,6 +248,7 @@ private: unsigned m_nDACI2CAddress; bool m_bChannelsSwapped; unsigned m_EngineType; + bool m_bQuadDAC8Chan; unsigned m_nMIDIBaudRate; std::string m_MIDIThruIn; @@ -254,7 +258,9 @@ private: bool m_bMIDIAutoVoiceDumpOnPC; bool m_bHeaderlessSysExVoices; bool m_bExpandPCAcrossBanks; - bool m_bQuadDAC8Chan; + unsigned m_nMIDISystemCCVol; + unsigned m_nMIDISystemCCPan; + unsigned m_nMIDISystemCCDetune; bool m_bLCDEnabled; unsigned m_nLCDPinEnable; diff --git a/src/mididevice.cpp b/src/mididevice.cpp index 504423e..f2b51de 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -53,6 +53,21 @@ LOGMODULE ("mididevice"); #define MIDI_PROGRAM_CHANGE 0b1100 #define MIDI_PITCH_BEND 0b1110 +// MIDI "System" level (i.e. all TG) custom CC maps +// Note: Even if number of TGs is not 8, there are only 8 +// available to be used in the mappings here. +#define NUM_MIDI_CC_MAPS 8 +const unsigned MIDISystemCCMap[NUM_MIDI_CC_MAPS][8] = { + {0,0,0,0,0,0,0,0}, // 0 = disabled + {16,17,18,19,80,81,82,83}, // 1 = General Purpose Controllers 1-8 + {20,21,22,23,24,25,26,27}, + {52,53,54,55,56,57,58,59}, + {102,103,104,105,106,107,108,109}, + {110,111,112,113,114,115,116,117}, + {3,9,14,15,28,29,30,31}, + {35,41,46,47,60,61,62,63} +}; + #define MIDI_SYSTEM_EXCLUSIVE_BEGIN 0xF0 #define MIDI_SYSTEM_EXCLUSIVE_END 0xF7 #define MIDI_TIMING_CLOCK 0xF8 @@ -69,6 +84,34 @@ CMIDIDevice::CMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInter { m_ChannelMap[nTG] = Disabled; } + + m_nMIDISystemCCVol = m_pConfig->GetMIDISystemCCVol(); + m_nMIDISystemCCPan = m_pConfig->GetMIDISystemCCPan(); + m_nMIDISystemCCDetune = m_pConfig->GetMIDISystemCCDetune(); + + m_MIDISystemCCBitmap[0] = 0; + m_MIDISystemCCBitmap[1] = 0; + m_MIDISystemCCBitmap[2] = 0; + m_MIDISystemCCBitmap[3] = 0; + + for (int tg=0; tg<8; tg++) + { + if (m_nMIDISystemCCVol != 0) { + u8 cc = MIDISystemCCMap[m_nMIDISystemCCVol][tg]; + m_MIDISystemCCBitmap[cc>>5] |= (1<<(cc%32)); + } + if (m_nMIDISystemCCPan != 0) { + u8 cc = MIDISystemCCMap[m_nMIDISystemCCPan][tg]; + m_MIDISystemCCBitmap[cc>>5] |= (1<<(cc%32)); + } + if (m_nMIDISystemCCDetune != 0) { + u8 cc = MIDISystemCCMap[m_nMIDISystemCCDetune][tg]; + m_MIDISystemCCBitmap[cc>>5] |= (1<<(cc%32)); + } + } + if (m_pConfig->GetMIDIDumpEnabled ()) { + LOGNOTE("MIDI System CC Map: %08X %08X %08X %08X", m_MIDISystemCCBitmap[3],m_MIDISystemCCBitmap[2],m_MIDISystemCCBitmap[1],m_MIDISystemCCBitmap[0]); + } } CMIDIDevice::~CMIDIDevice (void) @@ -239,7 +282,9 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign } // Process MIDI for each active Tone Generator - for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators(); nTG++) + bool bSystemCCHandled = false; + bool bSystemCCChecked = false; + for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators() && !bSystemCCHandled; nTG++) { if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN) { @@ -373,6 +418,17 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign m_pSynthesizer->notesOff (pMessage[2], nTG); } break; + + default: + // Check for system-level, cross-TG MIDI Controls, but only do it once. + // Also, if successfully handled, then no need to process other TGs, + // so it is possible to break out of the main TG loop too. + // Note: We handle this here so we get the TG MIDI channel checking. + if (!bSystemCCChecked) { + bSystemCCHandled = HandleMIDISystemCC(pMessage[1], pMessage[2]); + bSystemCCChecked = true; + } + break; } break; @@ -418,6 +474,52 @@ void CMIDIDevice::AddDevice (const char *pDeviceName) s_DeviceMap.insert (std::pair (pDeviceName, this)); } +bool CMIDIDevice::HandleMIDISystemCC(const u8 ucCC, const u8 ucCCval) +{ + // This only makes sense when there are at least 8 TGs. + // Note: If more than 8 TGs then only 8 TGs are controllable this way. + if (m_pConfig->GetToneGenerators() < 8) { + return false; + } + + // Quickly reject any CCs not in the configured maps + if ((m_MIDISystemCCBitmap[ucCC>>5] & (1<<(ucCC%32))) == 0) { + // Not in the map + return false; + } + + // Not looking for duplicate CCs so return once handled + for (unsigned tg=0; tg<8; tg++) { + if (m_nMIDISystemCCVol != 0) { + if (ucCC == MIDISystemCCMap[m_nMIDISystemCCVol][tg]) { + m_pSynthesizer->SetVolume (ucCCval, tg); + return true; + } + } + if (m_nMIDISystemCCPan != 0) { + if (ucCC == MIDISystemCCMap[m_nMIDISystemCCPan][tg]) { + m_pSynthesizer->SetPan (ucCCval, tg); + return true; + } + } + if (m_nMIDISystemCCDetune != 0) { + if (ucCC == MIDISystemCCMap[m_nMIDISystemCCDetune][tg]) { + if (ucCCval == 0) + { + m_pSynthesizer->SetMasterTune (0, tg); + } + else + { + m_pSynthesizer->SetMasterTune (maplong (ucCCval, 1, 127, -99, 99), tg); + } + return true; + } + } + } + + return false; +} + void CMIDIDevice::HandleSystemExclusive(const uint8_t* pMessage, const size_t nLength, const unsigned nCable, const uint8_t nTG) { int16_t sysex_return; diff --git a/src/mididevice.h b/src/mididevice.h index b7e7fe7..44f1691 100644 --- a/src/mididevice.h +++ b/src/mididevice.h @@ -60,12 +60,21 @@ protected: void MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsigned nCable = 0); void AddDevice (const char *pDeviceName); void HandleSystemExclusive(const uint8_t* pMessage, const size_t nLength, const unsigned nCable, const uint8_t nTG); + +private: + bool HandleMIDISystemCC(const u8 ucCC, const u8 ucCCval); + private: CMiniDexed *m_pSynthesizer; CConfig *m_pConfig; CUserInterface *m_pUI; u8 m_ChannelMap[CConfig::AllToneGenerators]; + + unsigned m_nMIDISystemCCVol; + unsigned m_nMIDISystemCCPan; + unsigned m_nMIDISystemCCDetune; + u32 m_MIDISystemCCBitmap[4]; // to allow for 128 bit entries std::string m_DeviceName; From f033fc4cc02bf543dd527f1b8c1a03d77cca35fd Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 16 Nov 2024 13:35:53 +0100 Subject: [PATCH 23/61] The MagPi magazine [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68cdb02..81bd3b5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![minidexed](https://user-images.githubusercontent.com/2480569/161813414-bb156a1c-efec-44c0-802a-8926412a08e0.jpg) -MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [Adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA). +MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [Adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), [The MagPi magazine](https://magpi.raspberrypi.com/articles/mini-dexed) (Issue 142 June 2024, [PDF](https://magpi.raspberrypi.com/issues/142)) and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA). ## Demo songs From c2cb172baad63e5192806d41c8a49643b285bc78 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 16 Nov 2024 17:24:53 +0100 Subject: [PATCH 24/61] Ship example minidexed.ini files for various devices (#754) --- .github/workflows/build.yml | 7 ++++++ .gitignore | 3 ++- hwconfig/DT-DX.override | 46 ++++++++++++++++++++++++++++++++++ hwconfig/customize.sh | 33 ++++++++++++++++++++++++ hwconfig/pirate_audio.override | 32 +++++++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 hwconfig/DT-DX.override create mode 100755 hwconfig/customize.sh create mode 100644 hwconfig/pirate_audio.override diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8df5911..4d62881 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,6 +82,13 @@ jobs: zip -r ../MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip * echo "artifactName=MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV cd - + - name: Hardware configration files + run: | + cd hwconfig + sh -ex ./customize.sh + cd - + mkdir -p ./sdcard/hardware/ + cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/ - uses: actions/upload-artifact@v3 with: name: ${{ env.artifactName }} # Exported above diff --git a/.gitignore b/.gitignore index 93bd001..5e9f987 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ sdcard CMSIS_5/ Synth_Dexed/ -circle-stdlib/ \ No newline at end of file +circle-stdlib/ +minidexed_* \ No newline at end of file diff --git a/hwconfig/DT-DX.override b/hwconfig/DT-DX.override new file mode 100644 index 0000000..1e6ba75 --- /dev/null +++ b/hwconfig/DT-DX.override @@ -0,0 +1,46 @@ +# DTronics DT-DX +# https://www.dtronics.nl/dt-dx + +SoundDevice=i2s +SampleRate=22000 +ChunkSize=256 +DACI2CAddress=0x0 +ChannelsSwapped=1 + +LCDEnabled=1 +LCDPinEnable=17 +LCDPinRegisterSelect=27 +LCDPinReadWrite=16 +LCDPinData4=22 +LCDPinData5=23 +LCDPinData6=24 +LCDPinData7=25 +LCDI2CAddress=0x00 + +SSD1306LCDI2CAddress=0x00 +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 + +LCDColumns=16 +LCDRows=2 + +ButtonPinPrev=0 +ButtonActionPrev=0 +ButtonPinNext=0 +ButtonActionNext=0 +ButtonPinBack=26 +ButtonActionBack=longpress +ButtonPinSelect=26 +ButtonActionSelect=click +ButtonPinHome=26 +ButtonActionHome=doubleclick +ButtonPinShortcut=26 + +DoubleClickTimeout=400 +LongPressTimeout=400 + +EncoderEnabled=1 +EncoderPinClock=6 +EncoderPinData=5 \ No newline at end of file diff --git a/hwconfig/customize.sh b/hwconfig/customize.sh new file mode 100755 index 0000000..687b568 --- /dev/null +++ b/hwconfig/customize.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# This script creates a set of ini files from the *.override files +# to provide customized configurations for well-known hardware + +# Find all files named *.override, and run the following on each of them +for file in *.override; do + # Copy the file minidexed.ini to the name of this file but with .ini extension instead + name_of_ini_file=minidexed_$(echo "$file" | sed 's/\.override$/.ini/') + cp ../src/minidexed.ini "$name_of_ini_file" + + # Change the values in the ini file, leaving the rest of the file unchanged + while IFS='=' read -r key value; do + # Skip empty lines and comments + if [ -z "$key" ] || [ "${key#\#}" != "$key" ]; then + continue + fi + value=$(echo "$value" | tr -d '\r') + if [ -n "$value" ]; then + sed -i "s/^$key=.*/$key=$value/" "$name_of_ini_file" + fi + done < "$file" + + # Process the last line of the override file separately, if it doesn't end with a newline + if [ -n "$key" ]; then + value=$(echo "$value" | tr -d '\r') + if [ -n "$value" ]; then + sed -i "s/^$key=.*/$key=$value/" "$name_of_ini_file" + fi + fi + + echo "Created $name_of_ini_file" +done diff --git a/hwconfig/pirate_audio.override b/hwconfig/pirate_audio.override new file mode 100644 index 0000000..680f3fe --- /dev/null +++ b/hwconfig/pirate_audio.override @@ -0,0 +1,32 @@ +# Pimoroni Pirate Audio (screen, buttons and audio output) +# https://shop.pimoroni.com/search?q=pirate%20audio + +SoundDevice=i2s +LCDEnabled=1 + +SPIBus=0 +ST7789Enabled=1 +ST7789Data=9 +ST7789Select=1 +ST7789Reset= +ST7789Backlight=13 +ST7789Width=240 +ST7789Height=240 +ST7789Rotation=90 + +LCDColumns=15 +LCDRows=2 + +ButtonPinPrev=5 +ButtonActionPrev=click +ButtonPinNext=6 +ButtonActionNext=click +ButtonPinBack=16 +ButtonActionBack=click +ButtonPinSelect=24 +ButtonActionSelect=click +ButtonPinHome=16 +ButtonActionHome=doubleclick +ButtonPinShortcut=0 + +EncoderEnabled=0 \ No newline at end of file From 1d9639789e2f7b6cae18efbaf29984279c68ae81 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Sat, 16 Nov 2024 16:28:25 +0000 Subject: [PATCH 25/61] Implement Bank Select Buttons and MIDI Buttons (#737) * Move button config into uibuttons rather than userinterface.cpp * Implementation of Bank Select buttons and MIDI buttons. * Fix MIDI button mapping issue. --- src/config.cpp | 36 ++++++++++++++++ src/config.h | 12 ++++++ src/minidexed.ini | 8 +++- src/uibuttons.cpp | 98 ++++++++++++++++++++++--------------------- src/uibuttons.h | 35 +++++++--------- src/uimenu.cpp | 86 +++++++++++++++++++++++++++++++++++++ src/uimenu.h | 3 ++ src/userinterface.cpp | 40 ++++-------------- 8 files changed, 219 insertions(+), 99 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 5214713..482b2b2 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -164,11 +164,15 @@ void CConfig::Load (void) m_nButtonPinPgmUp = m_Properties.GetNumber ("ButtonPinPgmUp", 0); m_nButtonPinPgmDown = m_Properties.GetNumber ("ButtonPinPgmDown", 0); + m_nButtonPinBankUp = m_Properties.GetNumber ("ButtonPinBankUp", 0); + m_nButtonPinBankDown = m_Properties.GetNumber ("ButtonPinBankDown", 0); m_nButtonPinTGUp = m_Properties.GetNumber ("ButtonPinTGUp", 0); m_nButtonPinTGDown = m_Properties.GetNumber ("ButtonPinTGDown", 0); m_ButtonActionPgmUp = m_Properties.GetString ("ButtonActionPgmUp", ""); m_ButtonActionPgmDown = m_Properties.GetString ("ButtonActionPgmDown", ""); + m_ButtonActionBankUp = m_Properties.GetString ("ButtonActionBankUp", ""); + m_ButtonActionBankDown = m_Properties.GetString ("ButtonActionBankDown", ""); m_ButtonActionTGUp = m_Properties.GetString ("ButtonActionTGUp", ""); m_ButtonActionTGDown = m_Properties.GetString ("ButtonActionTGDown", ""); @@ -182,6 +186,8 @@ void CConfig::Load (void) m_nMIDIButtonPgmUp = m_Properties.GetNumber ("MIDIButtonPgmUp", 0); m_nMIDIButtonPgmDown = m_Properties.GetNumber ("MIDIButtonPgmDown", 0); + m_nMIDIButtonBankUp = m_Properties.GetNumber ("MIDIButtonBankUp", 0); + m_nMIDIButtonBankDown = m_Properties.GetNumber ("MIDIButtonBankDown", 0); m_nMIDIButtonTGUp = m_Properties.GetNumber ("MIDIButtonTGUp", 0); m_nMIDIButtonTGDown = m_Properties.GetNumber ("MIDIButtonTGDown", 0); @@ -561,6 +567,16 @@ unsigned CConfig::GetButtonPinPgmDown (void) const return m_nButtonPinPgmDown; } +unsigned CConfig::GetButtonPinBankUp (void) const +{ + return m_nButtonPinBankUp; +} + +unsigned CConfig::GetButtonPinBankDown (void) const +{ + return m_nButtonPinBankDown; +} + unsigned CConfig::GetButtonPinTGUp (void) const { return m_nButtonPinTGUp; @@ -581,6 +597,16 @@ const char *CConfig::GetButtonActionPgmDown (void) const return m_ButtonActionPgmDown.c_str(); } +const char *CConfig::GetButtonActionBankUp (void) const +{ + return m_ButtonActionBankUp.c_str(); +} + +const char *CConfig::GetButtonActionBankDown (void) const +{ + return m_ButtonActionBankDown.c_str(); +} + const char *CConfig::GetButtonActionTGUp (void) const { return m_ButtonActionTGUp.c_str(); @@ -636,6 +662,16 @@ unsigned CConfig::GetMIDIButtonPgmDown (void) const return m_nMIDIButtonPgmDown; } +unsigned CConfig::GetMIDIButtonBankUp (void) const +{ + return m_nMIDIButtonBankUp; +} + +unsigned CConfig::GetMIDIButtonBankDown (void) const +{ + return m_nMIDIButtonBankDown; +} + unsigned CConfig::GetMIDIButtonTGUp (void) const { return m_nMIDIButtonTGUp; diff --git a/src/config.h b/src/config.h index a454d90..5d0cbc1 100644 --- a/src/config.h +++ b/src/config.h @@ -194,12 +194,16 @@ public: // GPIO pin numbers are chip numbers, not header positions unsigned GetButtonPinPgmUp (void) const; unsigned GetButtonPinPgmDown (void) const; + unsigned GetButtonPinBankUp (void) const; + unsigned GetButtonPinBankDown (void) const; unsigned GetButtonPinTGUp (void) const; unsigned GetButtonPinTGDown (void) const; // Action type for buttons: "click", "doubleclick", "longpress", "" const char *GetButtonActionPgmUp (void) const; const char *GetButtonActionPgmDown (void) const; + const char *GetButtonActionBankUp (void) const; + const char *GetButtonActionBankDown (void) const; const char *GetButtonActionTGUp (void) const; const char *GetButtonActionTGDown (void) const; @@ -215,6 +219,8 @@ public: // MIDI Button Program and TG Selection unsigned GetMIDIButtonPgmUp (void) const; unsigned GetMIDIButtonPgmDown (void) const; + unsigned GetMIDIButtonBankUp (void) const; + unsigned GetMIDIButtonBankDown (void) const; unsigned GetMIDIButtonTGUp (void) const; unsigned GetMIDIButtonTGDown (void) const; @@ -303,6 +309,8 @@ private: unsigned m_nButtonPinShortcut; unsigned m_nButtonPinPgmUp; unsigned m_nButtonPinPgmDown; + unsigned m_nButtonPinBankUp; + unsigned m_nButtonPinBankDown; unsigned m_nButtonPinTGUp; unsigned m_nButtonPinTGDown; @@ -313,6 +321,8 @@ private: std::string m_ButtonActionHome; std::string m_ButtonActionPgmUp; std::string m_ButtonActionPgmDown; + std::string m_ButtonActionBankUp; + std::string m_ButtonActionBankDown; std::string m_ButtonActionTGUp; std::string m_ButtonActionTGDown; @@ -328,6 +338,8 @@ private: unsigned m_nMIDIButtonHome; unsigned m_nMIDIButtonPgmUp; unsigned m_nMIDIButtonPgmDown; + unsigned m_nMIDIButtonBankUp; + unsigned m_nMIDIButtonBankDown; unsigned m_nMIDIButtonTGUp; unsigned m_nMIDIButtonTGDown; diff --git a/src/minidexed.ini b/src/minidexed.ini index de9f150..fd32a70 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -95,12 +95,16 @@ ButtonActionHome=doubleclick ButtonPinShortcut=11 # (Shortcut doesn't have an action) -# GPIO Program/TG Selection +# GPIO Program/Bank/TG Selection # Any buttons set to 0 will be ignored ButtonPinPgmUp=0 ButtonActionPgmUp= ButtonPinPgmDown=0 ButtonActionPgmDown= +ButtonPinBankUp=0 +ButtonActionBankUp= +ButtonPinBankDown=0 +ButtonActionBankDown= ButtonPinTGUp=0 ButtonActionTGUp= ButtonPinTGDown=0 @@ -125,6 +129,8 @@ MIDIButtonSelect=0 MIDIButtonHome=0 MIDIButtonPgmUp=0 MIDIButtonPgmDown=0 +MIDIButtonBankUp=0 +MIDIButtonBankDown=0 MIDIButtonTGUp=0 MIDIButtonTGDown=0 diff --git a/src/uibuttons.cpp b/src/uibuttons.cpp index 0e361ae..ae206dc 100644 --- a/src/uibuttons.cpp +++ b/src/uibuttons.cpp @@ -257,50 +257,8 @@ CUIButton::BtnTrigger CUIButton::triggerTypeFromString(const char* triggerString } -CUIButtons::CUIButtons ( - unsigned prevPin, const char *prevAction, - unsigned nextPin, const char *nextAction, - unsigned backPin, const char *backAction, - unsigned selectPin, const char *selectAction, - unsigned homePin, const char *homeAction, - unsigned pgmUpPin, const char *pgmUpAction, - unsigned pgmDownPin, const char *pgmDownAction, - unsigned TGUpPin, const char *TGUpAction, - unsigned TGDownPin, const char *TGDownAction, - unsigned doubleClickTimeout, unsigned longPressTimeout, - unsigned notesMidi, unsigned prevMidi, unsigned nextMidi, unsigned backMidi, unsigned selectMidi, unsigned homeMidi, - unsigned pgmUpMidi, unsigned pgmDownMidi, unsigned TGUpMidi, unsigned TGDownMidi -) -: m_doubleClickTimeout(doubleClickTimeout), - m_longPressTimeout(longPressTimeout), - m_prevPin(prevPin), - m_prevAction(CUIButton::triggerTypeFromString(prevAction)), - m_nextPin(nextPin), - m_nextAction(CUIButton::triggerTypeFromString(nextAction)), - m_backPin(backPin), - m_backAction(CUIButton::triggerTypeFromString(backAction)), - m_selectPin(selectPin), - m_selectAction(CUIButton::triggerTypeFromString(selectAction)), - m_homePin(homePin), - m_homeAction(CUIButton::triggerTypeFromString(homeAction)), - m_pgmUpPin(pgmUpPin), - m_pgmUpAction(CUIButton::triggerTypeFromString(pgmUpAction)), - m_pgmDownPin(pgmDownPin), - m_pgmDownAction(CUIButton::triggerTypeFromString(pgmDownAction)), - m_TGUpPin(TGUpPin), - m_TGUpAction(CUIButton::triggerTypeFromString(TGUpAction)), - m_TGDownPin(TGDownPin), - m_TGDownAction(CUIButton::triggerTypeFromString(TGDownAction)), - m_notesMidi(notesMidi), - m_prevMidi(ccToMidiPin(prevMidi)), - m_nextMidi(ccToMidiPin(nextMidi)), - m_backMidi(ccToMidiPin(backMidi)), - m_selectMidi(ccToMidiPin(selectMidi)), - m_homeMidi(ccToMidiPin(homeMidi)), - m_pgmUpMidi(ccToMidiPin(pgmUpMidi)), - m_pgmDownMidi(ccToMidiPin(pgmDownMidi)), - m_TGUpMidi(ccToMidiPin(TGUpMidi)), - m_TGDownMidi(ccToMidiPin(TGDownMidi)), +CUIButtons::CUIButtons (CConfig *pConfig) +: m_pConfig(pConfig), m_eventHandler (0), m_lastTick (0) { @@ -312,6 +270,46 @@ CUIButtons::~CUIButtons (void) boolean CUIButtons::Initialize (void) { + assert (m_pConfig); + + // Read the button configuration + m_doubleClickTimeout = m_pConfig->GetDoubleClickTimeout (); + m_longPressTimeout = m_pConfig->GetLongPressTimeout (); + m_prevPin = m_pConfig->GetButtonPinPrev (); + m_prevAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionPrev ()); + m_nextPin = m_pConfig->GetButtonPinNext (); + m_nextAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionNext ()); + m_backPin = m_pConfig->GetButtonPinBack (); + m_backAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionBack ()); + m_selectPin = m_pConfig->GetButtonPinSelect (); + m_selectAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionSelect ()); + m_homePin = m_pConfig->GetButtonPinHome (); + m_homeAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionHome ()); + m_pgmUpPin = m_pConfig->GetButtonPinPgmUp (); + m_pgmUpAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionPgmUp ()); + m_pgmDownPin = m_pConfig->GetButtonPinPgmDown (); + m_pgmDownAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionPgmDown ()); + m_BankUpPin = m_pConfig->GetButtonPinBankUp (); + m_BankUpAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionBankUp ()); + m_BankDownPin = m_pConfig->GetButtonPinBankDown (); + m_BankDownAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionBankDown ()); + m_TGUpPin = m_pConfig->GetButtonPinTGUp (); + m_TGUpAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionTGUp ()); + m_TGDownPin = m_pConfig->GetButtonPinTGDown (); + m_TGDownAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionTGDown ()); + m_notesMidi = ccToMidiPin( m_pConfig->GetMIDIButtonNotes ()); + m_prevMidi = ccToMidiPin( m_pConfig->GetMIDIButtonPrev ()); + m_nextMidi = ccToMidiPin( m_pConfig->GetMIDIButtonNext ()); + m_backMidi = ccToMidiPin( m_pConfig->GetMIDIButtonBack ()); + m_selectMidi = ccToMidiPin( m_pConfig->GetMIDIButtonSelect ()); + m_homeMidi = ccToMidiPin( m_pConfig->GetMIDIButtonHome ()); + m_pgmUpMidi = ccToMidiPin( m_pConfig->GetMIDIButtonPgmUp ()); + m_pgmDownMidi = ccToMidiPin( m_pConfig->GetMIDIButtonPgmDown ()); + m_BankUpMidi = ccToMidiPin( m_pConfig->GetMIDIButtonBankUp ()); + m_BankDownMidi = ccToMidiPin( m_pConfig->GetMIDIButtonBankDown ()); + m_TGUpMidi = ccToMidiPin( m_pConfig->GetMIDIButtonTGUp ()); + m_TGDownMidi = ccToMidiPin( m_pConfig->GetMIDIButtonTGDown ()); + // First sanity check and convert the timeouts: // Internally values are in tenths of a millisecond, but config values // are in milliseconds @@ -332,16 +330,16 @@ boolean CUIButtons::Initialize (void) // longpress. We may not initialise all of the buttons. // MIDI buttons only support a single click. unsigned pins[MAX_BUTTONS] = { - m_prevPin, m_nextPin, m_backPin, m_selectPin, m_homePin, m_pgmUpPin, m_pgmDownPin, m_TGUpPin, m_TGDownPin, - m_prevMidi, m_nextMidi, m_backMidi, m_selectMidi, m_homeMidi, m_pgmUpMidi, m_pgmDownMidi, m_TGUpMidi, m_TGDownMidi + m_prevPin, m_nextPin, m_backPin, m_selectPin, m_homePin, m_pgmUpPin, m_pgmDownPin, m_BankUpPin, m_BankDownPin, m_TGUpPin, m_TGDownPin, + m_prevMidi, m_nextMidi, m_backMidi, m_selectMidi, m_homeMidi, m_pgmUpMidi, m_pgmDownMidi, m_BankUpMidi, m_BankDownMidi, m_TGUpMidi, m_TGDownMidi }; CUIButton::BtnTrigger triggers[MAX_BUTTONS] = { // Normal buttons m_prevAction, m_nextAction, m_backAction, m_selectAction, m_homeAction, - m_pgmUpAction, m_pgmDownAction, m_TGUpAction, m_TGDownAction, + m_pgmUpAction, m_pgmDownAction, m_BankUpAction, m_BankDownAction, m_TGUpAction, m_TGDownAction, // MIDI Buttons only support a single click (at present) CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, - CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick + CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick }; CUIButton::BtnEvent events[MAX_BUTTONS] = { // Normal buttons @@ -352,6 +350,8 @@ boolean CUIButtons::Initialize (void) CUIButton::BtnEventHome, CUIButton::BtnEventPgmUp, CUIButton::BtnEventPgmDown, + CUIButton::BtnEventBankUp, + CUIButton::BtnEventBankDown, CUIButton::BtnEventTGUp, CUIButton::BtnEventTGDown, // MIDI buttons @@ -362,6 +362,8 @@ boolean CUIButtons::Initialize (void) CUIButton::BtnEventHome, CUIButton::BtnEventPgmUp, CUIButton::BtnEventPgmDown, + CUIButton::BtnEventBankUp, + CUIButton::BtnEventBankDown, CUIButton::BtnEventTGUp, CUIButton::BtnEventTGDown }; diff --git a/src/uibuttons.h b/src/uibuttons.h index 44dcca0..be17934 100644 --- a/src/uibuttons.h +++ b/src/uibuttons.h @@ -27,8 +27,8 @@ #define BUTTONS_UPDATE_NUM_TICKS 100 #define DEBOUNCE_TIME 20 -#define MAX_GPIO_BUTTONS 9 // 5 UI buttons, 4 Program/TG Select buttons -#define MAX_MIDI_BUTTONS 9 +#define MAX_GPIO_BUTTONS 11 // 5 UI buttons, 6 Program/Bank/TG Select buttons +#define MAX_MIDI_BUTTONS 11 #define MAX_BUTTONS (MAX_GPIO_BUTTONS+MAX_MIDI_BUTTONS) class CUIButtons; @@ -54,9 +54,11 @@ public: BtnEventHome = 5, BtnEventPgmUp = 6, BtnEventPgmDown = 7, - BtnEventTGUp = 8, - BtnEventTGDown = 9, - BtnEventUnknown = 10 + BtnEventBankUp = 8, + BtnEventBankDown = 9, + BtnEventTGUp = 10, + BtnEventTGDown = 11, + BtnEventUnknown = 12 }; CUIButton (void); @@ -111,20 +113,7 @@ public: typedef void BtnEventHandler (CUIButton::BtnEvent Event, void *param); public: - CUIButtons ( - unsigned prevPin, const char *prevAction, - unsigned nextPin, const char *nextAction, - unsigned backPin, const char *backAction, - unsigned selectPin, const char *selectAction, - unsigned homePin, const char *homeAction, - unsigned pgmUpPin, const char *pgmUpAction, - unsigned pgmDownPin, const char *pgmDownAction, - unsigned TGUpPin, const char *TGUpAction, - unsigned TGDownPin, const char *TGDownAction, - unsigned doubleClickTimeout, unsigned longPressTimeout, - unsigned notesMidi, unsigned prevMidi, unsigned nextMidi, unsigned backMidi, unsigned selectMidi, unsigned homeMidi, - unsigned pgmUpMidi, unsigned pgmDownMidi, unsigned TGUpMidi, unsigned TGDownMidi - ); + CUIButtons (CConfig *pConfig); ~CUIButtons (void); boolean Initialize (void); @@ -138,6 +127,8 @@ public: void BtnMIDICmdHandler (unsigned nMidiCmd, unsigned nMidiData1, unsigned nMidiData2); private: + CConfig *m_pConfig; + // Array of normal GPIO buttons and "MIDI buttons" CUIButton m_buttons[MAX_BUTTONS]; @@ -163,6 +154,10 @@ private: CUIButton::BtnTrigger m_pgmUpAction; unsigned m_pgmDownPin; CUIButton::BtnTrigger m_pgmDownAction; + unsigned m_BankUpPin; + CUIButton::BtnTrigger m_BankUpAction; + unsigned m_BankDownPin; + CUIButton::BtnTrigger m_BankDownAction; unsigned m_TGUpPin; CUIButton::BtnTrigger m_TGUpAction; unsigned m_TGDownPin; @@ -178,6 +173,8 @@ private: unsigned m_pgmUpMidi; unsigned m_pgmDownMidi; + unsigned m_BankUpMidi; + unsigned m_BankDownMidi; unsigned m_TGUpMidi; unsigned m_TGDownMidi; diff --git a/src/uimenu.cpp b/src/uimenu.cpp index 8b72425..ed36cd4 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -431,6 +431,11 @@ void CUIMenu::EventHandler (TMenuEvent Event) PgmUpDownHandler(Event); break; + case MenuEventBankUp: + case MenuEventBankDown: + BankUpDownHandler(Event); + break; + case MenuEventTGUp: case MenuEventTGDown: TGUpDownHandler(Event); @@ -1375,6 +1380,87 @@ void CUIMenu::PgmUpDownHandler (TMenuEvent Event) } } +void CUIMenu::BankUpDownHandler (TMenuEvent Event) +{ + if (m_pMiniDexed->GetParameter (CMiniDexed::ParameterPerformanceSelectChannel) != CMIDIDevice::Disabled) + { + // Bank Up/Down acts on performances + unsigned nLastPerformanceBank = m_pMiniDexed->GetLastPerformanceBank(); + unsigned nPerformanceBank = m_nSelectedPerformanceBankID; + unsigned nStartBank = nPerformanceBank; + //LOGNOTE("Performance Bank actual=%d, last=%d", nPerformanceBank, nLastPerformanceBank); + if (Event == MenuEventBankDown) + { + do + { + if (nPerformanceBank == 0) + { + // Wrap around + nPerformanceBank = nLastPerformanceBank; + } + else if (nPerformanceBank > 0) + { + --nPerformanceBank; + } + } while ((m_pMiniDexed->IsValidPerformanceBank(nPerformanceBank) != true) && (nPerformanceBank != nStartBank)); + m_nSelectedPerformanceBankID = nPerformanceBank; + // Switch to the new bank and select the first performance voice + m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nPerformanceBank); + m_pMiniDexed->SetFirstPerformance(); + //LOGNOTE("Performance Bank new=%d, last=%d", m_nSelectedPerformanceBankID, nLastPerformanceBank); + } + else // MenuEventBankUp + { + do + { + if (nPerformanceBank == nLastPerformanceBank) + { + // Wrap around + nPerformanceBank = 0; + } + else if (nPerformanceBank < nLastPerformanceBank) + { + ++nPerformanceBank; + } + } while ((m_pMiniDexed->IsValidPerformanceBank(nPerformanceBank) != true) && (nPerformanceBank != nStartBank)); + m_nSelectedPerformanceBankID = nPerformanceBank; + m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nPerformanceBank); + m_pMiniDexed->SetFirstPerformance(); + //LOGNOTE("Performance Bank new=%d, last=%d", m_nSelectedPerformanceBankID, nLastPerformanceBank); + } + } + else + { + // Bank Up/Down acts on voices within a TG. + + // If we're not in the root menu, then see if we are already in a TG menu, + // then find the current TG number. Otherwise assume TG1 (nTG=0). + unsigned nTG = 0; + if (m_MenuStackMenu[0] == s_MainMenu && (m_pCurrentMenu == s_TGMenu) || (m_MenuStackMenu[1] == s_TGMenu)) { + nTG = m_nMenuStackSelection[0]; + } + assert (nTG < CConfig::AllToneGenerators); + if (nTG < m_nToneGenerators) + { + int nBank = m_pMiniDexed->GetTGParameter (CMiniDexed::TGParameterVoiceBank, nTG); + + assert (Event == MenuEventBankDown || Event == MenuEventBankUp); + if (Event == MenuEventBankDown) + { + //LOGNOTE("BankDown"); + nBank = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankDown(nBank); + m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nBank, nTG); + } + else + { + //LOGNOTE("BankUp"); + nBank = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankUp(nBank); + m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nBank, nTG); + } + } + } +} + void CUIMenu::TGUpDownHandler (TMenuEvent Event) { // This will update the menus to position it for the next TG up or down diff --git a/src/uimenu.h b/src/uimenu.h index 0034278..d9dc3ee 100644 --- a/src/uimenu.h +++ b/src/uimenu.h @@ -48,6 +48,8 @@ public: MenuEventPressAndStepUp, MenuEventPgmUp, MenuEventPgmDown, + MenuEventBankUp, + MenuEventBankDown, MenuEventTGUp, MenuEventTGDown, MenuEventUnknown @@ -119,6 +121,7 @@ private: void OPShortcutHandler (TMenuEvent Event); void PgmUpDownHandler (TMenuEvent Event); + void BankUpDownHandler (TMenuEvent Event); void TGUpDownHandler (TMenuEvent Event); static void TimerHandler (TKernelTimerHandle hTimer, void *pParam, void *pContext); diff --git a/src/userinterface.cpp b/src/userinterface.cpp index aa46f9e..32222a1 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -162,37 +162,7 @@ bool CUserInterface::Initialize (void) LOGDBG ("LCD initialized"); } - m_pUIButtons = new CUIButtons ( m_pConfig->GetButtonPinPrev (), - m_pConfig->GetButtonActionPrev (), - m_pConfig->GetButtonPinNext (), - m_pConfig->GetButtonActionNext (), - m_pConfig->GetButtonPinBack (), - m_pConfig->GetButtonActionBack (), - m_pConfig->GetButtonPinSelect (), - m_pConfig->GetButtonActionSelect (), - m_pConfig->GetButtonPinHome (), - m_pConfig->GetButtonActionHome (), - m_pConfig->GetButtonPinPgmUp (), - m_pConfig->GetButtonActionPgmUp (), - m_pConfig->GetButtonPinPgmDown (), - m_pConfig->GetButtonActionPgmDown (), - m_pConfig->GetButtonPinTGUp (), - m_pConfig->GetButtonActionTGUp (), - m_pConfig->GetButtonPinTGDown (), - m_pConfig->GetButtonActionTGDown (), - m_pConfig->GetDoubleClickTimeout (), - m_pConfig->GetLongPressTimeout (), - m_pConfig->GetMIDIButtonNotes (), - m_pConfig->GetMIDIButtonPrev (), - m_pConfig->GetMIDIButtonNext (), - m_pConfig->GetMIDIButtonBack (), - m_pConfig->GetMIDIButtonSelect (), - m_pConfig->GetMIDIButtonHome (), - m_pConfig->GetMIDIButtonPgmUp (), - m_pConfig->GetMIDIButtonPgmDown (), - m_pConfig->GetMIDIButtonTGUp (), - m_pConfig->GetMIDIButtonTGDown () - ); + m_pUIButtons = new CUIButtons ( m_pConfig ); assert (m_pUIButtons); if (!m_pUIButtons->Initialize ()) @@ -397,6 +367,14 @@ void CUserInterface::UIButtonsEventHandler (CUIButton::BtnEvent Event) m_Menu.EventHandler (CUIMenu::MenuEventPgmDown); break; + case CUIButton::BtnEventBankUp: + m_Menu.EventHandler (CUIMenu::MenuEventBankUp); + break; + + case CUIButton::BtnEventBankDown: + m_Menu.EventHandler (CUIMenu::MenuEventBankDown); + break; + case CUIButton::BtnEventTGUp: m_Menu.EventHandler (CUIMenu::MenuEventTGUp); break; From 0487dc86b60ec010c376d3ed9833709310d673a2 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 16 Nov 2024 21:01:26 +0100 Subject: [PATCH 26/61] Working defaults for MIDI Button Navigation [ci skip] --- src/minidexed.ini | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/minidexed.ini b/src/minidexed.ini index fd32a70..5b6b13d 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -120,19 +120,24 @@ LongPressTimeout=400 # CC channel: 0=OFF; 1-16 MIDI Ch; >16 Omni # If MIDIButtonNotes>0 then treat MIDIButton numbers as MIDI # Note numbers, triggered with NoteOn/NoteOff, not CC numbers. -MIDIButtonCh=0 +MIDIButtonCh=17 MIDIButtonNotes=0 -MIDIButtonPrev=0 -MIDIButtonNext=0 -MIDIButtonBack=0 -MIDIButtonSelect=0 -MIDIButtonHome=0 -MIDIButtonPgmUp=0 -MIDIButtonPgmDown=0 -MIDIButtonBankUp=0 -MIDIButtonBankDown=0 -MIDIButtonTGUp=0 -MIDIButtonTGDown=0 +# Arrow left +MIDIButtonPrev=46 +# Arrow right +MIDIButtonNext=47 +# Arrow up +MIDIButtonBack=48 +# Arrow down +MIDIButtonSelect=49 +# Home button +MIDIButtonHome=50 +MIDIButtonPgmUp=51 +MIDIButtonPgmDown=52 +MIDIButtonBankUp=53 +MIDIButtonBankDown=54 +MIDIButtonTGUp=55 +MIDIButtonTGDown=56 # KY-040 Rotary Encoder EncoderEnabled=1 From 30e95bbe05ee40d08038f9af554751098f210b01 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 16 Nov 2024 22:09:01 +0100 Subject: [PATCH 27/61] Add more acknowledgements [ci skip] --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 81bd3b5..4b61de1 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ This project stands on the shoulders of giants. Special thanks to: - [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed - [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault) - [diyelectromusic](https://github.com/diyelectromusic/) for many [contributions](https://github.com/probonopd/MiniDexed/commits?author=diyelectromusic) +- [dwhinham/mt32-pi](https://github.com/dwhinham/mt32-pi) for creating networking support for circle +- [omersiar](https://github.com/omersiar) for porting networking support to MiniDexed ## Stargazers over time [![Stargazers over time](https://starchart.cc/probonopd/MiniDexed.svg?variant=adaptive)](https://starchart.cc/probonopd/MiniDexed) From 35ce69bd1cd03a4b2d93a592c6da74e57135f663 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 16 Nov 2024 22:09:52 +0100 Subject: [PATCH 28/61] Fix typo [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b61de1..25ee4d2 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ This project stands on the shoulders of giants. Special thanks to: - [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed - [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault) - [diyelectromusic](https://github.com/diyelectromusic/) for many [contributions](https://github.com/probonopd/MiniDexed/commits?author=diyelectromusic) -- [dwhinham/mt32-pi](https://github.com/dwhinham/mt32-pi) for creating networking support for circle +- [dwhinham/mt32-pi](https://github.com/dwhinham/mt32-pi) for creating networking support for Circle - [omersiar](https://github.com/omersiar) for porting networking support to MiniDexed ## Stargazers over time From 281e7421d7008a2d4eddac2ae970d4ea76d17200 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 16 Nov 2024 23:36:46 +0100 Subject: [PATCH 29/61] Put git hash in startup message --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d62881..c6fef95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,10 @@ jobs: - name: Get specific commits of git submodules run: | sh -ex ./submod.sh + - name: Apply patches + run: | + # Put git hash in startup message + sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp - name: Install toolchains run: | set -ex From 62074b6cd8337f760a16d72e632dabb62704b0a9 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 17 Nov 2024 17:09:44 +0100 Subject: [PATCH 30/61] Build all PRs [ci skip] --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6fef95..a5cf9e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] jobs: Build: From 45529b5d443e2f4be8e4ce4714b6da3f324cf581 Mon Sep 17 00:00:00 2001 From: probonopd Date: Thu, 28 Nov 2024 08:17:23 +0100 Subject: [PATCH 31/61] Update pirate_audio.override https://github.com/probonopd/MiniDexed/pull/652#issuecomment-2504718865 Thanks @GertSchepens --- hwconfig/pirate_audio.override | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hwconfig/pirate_audio.override b/hwconfig/pirate_audio.override index 680f3fe..72376c7 100644 --- a/hwconfig/pirate_audio.override +++ b/hwconfig/pirate_audio.override @@ -19,14 +19,14 @@ LCDRows=2 ButtonPinPrev=5 ButtonActionPrev=click -ButtonPinNext=6 +ButtonPinNext=16 ButtonActionNext=click -ButtonPinBack=16 +ButtonPinBack=24 ButtonActionBack=click -ButtonPinSelect=24 +ButtonPinSelect=6 ButtonActionSelect=click -ButtonPinHome=16 +ButtonPinHome=24 ButtonActionHome=doubleclick ButtonPinShortcut=0 -EncoderEnabled=0 \ No newline at end of file +EncoderEnabled=0 From 0ae7b26b97e05d0645cf2d927eeaacd6cee18de3 Mon Sep 17 00:00:00 2001 From: soyer Date: Tue, 3 Dec 2024 20:08:04 +0100 Subject: [PATCH 32/61] register MIDI packet handlers as TMIDIPacketHandlerEx (#769) There is a new RegisterPacketHandler function that can save a pointer, eliminating the need to write multiple packet handler functions. --- src/midikeyboard.cpp | 46 ++++++++------------------------------------ src/midikeyboard.h | 16 ++------------- 2 files changed, 10 insertions(+), 52 deletions(-) diff --git a/src/midikeyboard.cpp b/src/midikeyboard.cpp index 2eae9d8..db71168 100644 --- a/src/midikeyboard.cpp +++ b/src/midikeyboard.cpp @@ -25,25 +25,12 @@ #include #include -CMIDIKeyboard *CMIDIKeyboard::s_pThis[MaxInstances] = {0}; - -TMIDIPacketHandler * const CMIDIKeyboard::s_pMIDIPacketHandler[MaxInstances] = -{ - MIDIPacketHandler0, - MIDIPacketHandler1, - MIDIPacketHandler2, - MIDIPacketHandler3 -}; - CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance) : CMIDIDevice (pSynthesizer, pConfig, pUI), m_nSysExIdx (0), m_nInstance (nInstance), m_pMIDIDevice (0) { - assert (m_nInstance < MaxInstances); - s_pThis[m_nInstance] = this; - m_DeviceName.Format ("umidi%u", nInstance+1); AddDevice (m_DeviceName); @@ -51,8 +38,6 @@ CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserI CMIDIKeyboard::~CMIDIKeyboard (void) { - assert (m_nInstance < MaxInstances); - s_pThis[m_nInstance] = 0; } void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated) @@ -81,8 +66,7 @@ void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated) (CUSBMIDIDevice *) CDeviceNameService::Get ()->GetDevice (m_DeviceName, FALSE); if (m_pMIDIDevice != 0) { - assert (m_nInstance < MaxInstances); - m_pMIDIDevice->RegisterPacketHandler (s_pMIDIPacketHandler[m_nInstance]); + m_pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler, this); m_pMIDIDevice->RegisterRemovedHandler (DeviceRemovedHandler, this); } @@ -104,8 +88,10 @@ void CMIDIKeyboard::Send (const u8 *pMessage, size_t nLength, unsigned nCable) // Most packets will be passed straight onto the main MIDI message handler // but SysEx messages are multiple USB packets and so will need building up // before parsing. -void CMIDIKeyboard::USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable) +void CMIDIKeyboard::USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable, unsigned nDevice) { + assert (nDevice == m_nInstance + 1); + if ((pPacket[0] == 0xF0) && (m_nSysExIdx == 0)) { // Start of SysEx message @@ -159,28 +145,12 @@ void CMIDIKeyboard::USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsign } } -void CMIDIKeyboard::MIDIPacketHandler0 (unsigned nCable, u8 *pPacket, unsigned nLength) -{ - assert (s_pThis[0] != 0); - s_pThis[0]->USBMIDIMessageHandler (pPacket, nLength, nCable); -} - -void CMIDIKeyboard::MIDIPacketHandler1 (unsigned nCable, u8 *pPacket, unsigned nLength) -{ - assert (s_pThis[1] != 0); - s_pThis[1]->USBMIDIMessageHandler (pPacket, nLength, nCable); -} - -void CMIDIKeyboard::MIDIPacketHandler2 (unsigned nCable, u8 *pPacket, unsigned nLength) +void CMIDIKeyboard::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam) { - assert (s_pThis[2] != 0); - s_pThis[2]->USBMIDIMessageHandler (pPacket, nLength, nCable); -} + CMIDIKeyboard *pThis = static_cast (pParam); + assert (pThis != 0); -void CMIDIKeyboard::MIDIPacketHandler3 (unsigned nCable, u8 *pPacket, unsigned nLength) -{ - assert (s_pThis[3] != 0); - s_pThis[3]->USBMIDIMessageHandler (pPacket, nLength, nCable); + pThis->USBMIDIMessageHandler (pPacket, nLength, nCable, nDevice); } void CMIDIKeyboard::DeviceRemovedHandler (CDevice *pDevice, void *pContext) diff --git a/src/midikeyboard.h b/src/midikeyboard.h index 047fa52..bf62689 100644 --- a/src/midikeyboard.h +++ b/src/midikeyboard.h @@ -37,9 +37,6 @@ class CMiniDexed; class CMIDIKeyboard : public CMIDIDevice { -public: - static const unsigned MaxInstances = 4; - public: CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance = 0); ~CMIDIKeyboard (void); @@ -49,14 +46,10 @@ public: void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) override; private: - static void MIDIPacketHandler0 (unsigned nCable, u8 *pPacket, unsigned nLength); - static void MIDIPacketHandler1 (unsigned nCable, u8 *pPacket, unsigned nLength); - static void MIDIPacketHandler2 (unsigned nCable, u8 *pPacket, unsigned nLength); - static void MIDIPacketHandler3 (unsigned nCable, u8 *pPacket, unsigned nLength); - + static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam); static void DeviceRemovedHandler (CDevice *pDevice, void *pContext); - void USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable); + void USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable, unsigned nDevice); private: struct TSendQueueEntry @@ -75,11 +68,6 @@ private: CUSBMIDIDevice * volatile m_pMIDIDevice; std::queue m_SendQueue; - - static CMIDIKeyboard *s_pThis[MaxInstances]; - - static TMIDIPacketHandler * const s_pMIDIPacketHandler[MaxInstances]; - }; #endif From 594791530b912926978bf860eb5dd54de644a7d1 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Sat, 14 Dec 2024 21:07:51 +0000 Subject: [PATCH 33/61] Fix Device Master Volume Control handling (#775) --- src/mididevice.cpp | 53 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/mididevice.cpp b/src/mididevice.cpp index f2b51de..216b466 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -144,35 +144,36 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign if ( pMessage[0] != MIDI_TIMING_CLOCK && pMessage[0] != MIDI_ACTIVE_SENSING) { - printf ("MIDI%u: %02X\n", nCable, (unsigned) pMessage[0]); + fprintf (stderr, "MIDI%u: %02X\n", nCable, (unsigned) pMessage[0]); } break; case 2: - printf ("MIDI%u: %02X %02X\n", nCable, + fprintf (stderr, "MIDI%u: %02X %02X\n", nCable, (unsigned) pMessage[0], (unsigned) pMessage[1]); break; case 3: - printf ("MIDI%u: %02X %02X %02X\n", nCable, + fprintf (stderr, "MIDI%u: %02X %02X %02X\n", nCable, (unsigned) pMessage[0], (unsigned) pMessage[1], (unsigned) pMessage[2]); break; + default: switch(pMessage[0]) { case MIDI_SYSTEM_EXCLUSIVE_BEGIN: - printf("MIDI%u: SysEx data length: [%d]:",nCable, uint16_t(nLength)); + fprintf(stderr, "MIDI%u: SysEx data length: [%d]:",nCable, uint16_t(nLength)); for (uint16_t i = 0; i < nLength; i++) { if((i % 16) == 0) - printf("\n%04d:",i); - printf(" 0x%02x",pMessage[i]); + fprintf(stderr, "\n%04d:",i); + fprintf(stderr, " 0x%02x",pMessage[i]); } - printf("\n"); + fprintf(stderr, "\n"); break; default: - printf("MIDI%u: Unhandled MIDI event type %0x02x\n",nCable,pMessage[0]); + fprintf(stderr, "MIDI%u: Unhandled MIDI event type %0x02x\n",nCable,pMessage[0]); } break; } @@ -218,11 +219,39 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign u8 ucType = ucStatus >> 4; // GLOBAL MIDI SYSEX - if (pMessage[0] == MIDI_SYSTEM_EXCLUSIVE_BEGIN && pMessage[3] == 0x04 && pMessage[4] == 0x01 && pMessage[nLength-1] == MIDI_SYSTEM_EXCLUSIVE_END) // MASTER VOLUME + // + // 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 + // 04 Device Control + // 01 Master Volume Device Control + // LL Low 7-bits of 14-bit volume + // HH High 7-bits of 14-bit volume + // F7 End SysEx + // + // See MIDI Specification "Device Control" + // "Master Volume and Master Balance" + // + // Need to scale the volume parameter to fit + // a 14-bit value: 0..16383 + // and then split into LSB/MSB. + if (nLength == 8 && + pMessage[0] == MIDI_SYSTEM_EXCLUSIVE_BEGIN && + pMessage[1] == 0x7F && + pMessage[2] == 0x7F && + pMessage[3] == 0x04 && + pMessage[4] == 0x01 && + // pMessage[5] and pMessage[6] = LSB+MSB + pMessage[7] == MIDI_SYSTEM_EXCLUSIVE_END + ) // MASTER VOLUME { - float32_t nMasterVolume=((pMessage[5] & 0x7c) & ((pMessage[6] & 0x7c) <<7))/(1<<14); - LOGNOTE("Master volume: %f",nMasterVolume); - m_pSynthesizer->setMasterVolume(nMasterVolume); + // Convert LSB/MSB to 14-bit integer volume + uint32_t nMasterVolume=((pMessage[5] & 0x7F) | ((pMessage[6] & 0x7F) <<7)); + // Convert to value between 0.0 and 1.0 + float32_t fMasterVolume = (float32_t)nMasterVolume / 16384.0; + //printf("Master volume: %f (%d)\n",fMasterVolume, nMasterVolume); + m_pSynthesizer->setMasterVolume(fMasterVolume); } else { From 3871ac5748cfb0c11bb10fcba1ba3b9eaf95b260 Mon Sep 17 00:00:00 2001 From: soyer Date: Sat, 21 Dec 2024 12:15:39 +0100 Subject: [PATCH 34/61] Always show both arrows in the main menu (#777) Since it wraps around since a while. (Not so sure we actually want it to wrap around in some but not other places, but for now it wraps around, so we should show both arrows.) --- src/uimenu.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uimenu.cpp b/src/uimenu.cpp index ed36cd4..efaf21f 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -539,12 +539,13 @@ void CUIMenu::MenuHandler (CUIMenu *pUIMenu, TMenuEvent Event) if (pUIMenu->m_pCurrentMenu) // if this is another menu? { + bool bIsMainMenu = pUIMenu->m_pCurrentMenu == s_MainMenu; pUIMenu->m_pUI->DisplayWrite ( pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, "", pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Name, - pUIMenu->m_nCurrentSelection > 0, - !!pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name); + pUIMenu->m_nCurrentSelection > 0 || bIsMainMenu, + !!pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name || bIsMainMenu); } else { From e9751e62411b49af6823c4eab0411a4c9a2ab143 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 21 Dec 2024 21:24:45 +0100 Subject: [PATCH 35/61] retention-days: 3 # To not exceed the free MB/month quota so quickly [ci skip] --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5cf9e8..43f6e6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,6 +96,7 @@ jobs: with: name: ${{ env.artifactName }} # Exported above path: ./sdcard/* + retention-days: 3 # To not exceed the free MB/month quota so quickly - name: Upload to GitHub Releases (only when building from main branch) if: ${{ github.ref == 'refs/heads/main' }} run: | From ffb6449258a3c87333e4df3e3786039faaaa2f06 Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 23 Dec 2024 21:12:24 +0100 Subject: [PATCH 36/61] Add hardware configuration for genXnoise devices (#768) --- hwconfig/customize.sh | 12 ++++++ hwconfig/dxeus_machina_eurorack.override | 44 ++++++++++++++++++++++ hwconfig/genxnoise_desktop_module.override | 31 +++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 hwconfig/dxeus_machina_eurorack.override create mode 100644 hwconfig/genxnoise_desktop_module.override diff --git a/hwconfig/customize.sh b/hwconfig/customize.sh index 687b568..73b3485 100755 --- a/hwconfig/customize.sh +++ b/hwconfig/customize.sh @@ -29,5 +29,17 @@ for file in *.override; do fi fi + # Configure genxnoise_desktop_module as USB gadget (as intended by the manufacturer) + case "$file" in + *genxnoise_desktop_module*) + echo "" >> "$name_of_ini_file" + echo "# CAUTION: To prevent hardware damage, DO NOT use the port labeled 'PWR'" >> "$name_of_ini_file" + echo "# (the microUSB port near the edge of the device) when USBGadget is set to 1!" >> "$name_of_ini_file" + echo "# You need to disable USBGadget if you would like to use that port!" >> "$name_of_ini_file" + echo "# See https://github.com/probonopd/MiniDexed/wiki/Hardware#usb-gadget-mode for more information" >> "$name_of_ini_file" + echo "USBGadget=1" >> "$name_of_ini_file" + ;; + esac + echo "Created $name_of_ini_file" done diff --git a/hwconfig/dxeus_machina_eurorack.override b/hwconfig/dxeus_machina_eurorack.override new file mode 100644 index 0000000..6e1ed46 --- /dev/null +++ b/hwconfig/dxeus_machina_eurorack.override @@ -0,0 +1,44 @@ +# genXnoise dXeus machina +# https://www.genxnoise.com/product-page/dxeus-machina-minidexed-eurorack-format + +SoundDevice=i2s +SampleRate=48000 +DACI2CAddress=0 +ChannelsSwapped=0 + +MIDIThru=ttyS1,ttyS1 + +LCDEnabled=1 +LCDPinEnable=17 +LCDPinRegisterSelect=4 +LCDPinReadWrite=0 +LCDPinData4=22 +LCDPinData5=23 +LCDPinData6=24 +LCDPinData7=25 +LCDI2CAddress=0x00 + +SSD1306LCDI2CAddress=0x3c +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 + +LCDColumns=20 +LCDRows=2 + +ButtonPinPrev=0 +ButtonActionPrev= +ButtonPinNext=0 +ButtonActionNext= +ButtonPinBack=11 +ButtonActionBack=longpress +ButtonPinSelect=11 +ButtonActionSelect=click +ButtonPinHome=11 +ButtonActionHome=doubleclick +ButtonPinShortcut=11 + +EncoderEnabled=1 +EncoderPinClock=9 +EncoderPinData=10 diff --git a/hwconfig/genxnoise_desktop_module.override b/hwconfig/genxnoise_desktop_module.override new file mode 100644 index 0000000..14f1192 --- /dev/null +++ b/hwconfig/genxnoise_desktop_module.override @@ -0,0 +1,31 @@ +# genXnoise desktop module +# https://www.genxnoise.com/product-page/minidexed-midi-tone-module + +SoundDevice=i2s +DACI2CAddress=0 +ChannelsSwapped=0 + +LCDEnabled=1 +LCDPinEnable=17 +LCDPinRegisterSelect=4 +LCDPinReadWrite=0 +LCDPinData4=22 +LCDPinData5=23 +LCDPinData6=24 +LCDPinData7=25 +LCDI2CAddress=0x00 + +SSD1306LCDI2CAddress=0x3c +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 + +LCDColumns=20 +LCDRows=2 + +EncoderEnabled=1 +EncoderPinClock=10 +EncoderPinData=9 + +USBGadget=1 From 0f7f8f45e4a6957c67d937e2a66fa04fbc4a3f61 Mon Sep 17 00:00:00 2001 From: soyer Date: Thu, 2 Jan 2025 08:47:50 +0100 Subject: [PATCH 37/61] pass strings by const reference and use find_last_not_of (#779) * pass std::strings by const reference * use std::string::find_last_not_of --- src/minidexed.cpp | 6 +++--- src/minidexed.h | 4 ++-- src/performanceconfig.cpp | 12 ++---------- src/performanceconfig.h | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 2e45f46..1a714fc 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -1968,9 +1968,9 @@ std::string CMiniDexed::GetNewPerformanceDefaultName(void) return m_PerformanceConfig.GetNewPerformanceDefaultName(); } -void CMiniDexed::SetNewPerformanceName(std::string nName) +void CMiniDexed::SetNewPerformanceName(const std::string &Name) { - m_PerformanceConfig.SetNewPerformanceName(nName); + m_PerformanceConfig.SetNewPerformanceName(Name); } bool CMiniDexed::IsValidPerformance(unsigned nID) @@ -1983,7 +1983,7 @@ bool CMiniDexed::IsValidPerformanceBank(unsigned nBankID) return m_PerformanceConfig.IsValidPerformanceBank(nBankID); } -void CMiniDexed::SetVoiceName (std::string VoiceName, unsigned nTG) +void CMiniDexed::SetVoiceName (const std::string &VoiceName, unsigned nTG) { assert (nTG < CConfig::AllToneGenerators); if (nTG >= m_nToneGenerators) return; // Not an active TG diff --git a/src/minidexed.h b/src/minidexed.h index 69dcf9c..bb6290b 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -164,8 +164,8 @@ public: int GetParameter (TParameter Parameter); std::string GetNewPerformanceDefaultName(void); - void SetNewPerformanceName(std::string nName); - void SetVoiceName (std::string VoiceName, unsigned nTG); + void SetNewPerformanceName(const std::string &Name); + void SetVoiceName (const std::string &VoiceName, unsigned nTG); bool DeletePerformance(unsigned nID); bool DoDeletePerformance(void); diff --git a/src/performanceconfig.cpp b/src/performanceconfig.cpp index 8a182e7..d0ce2de 100644 --- a/src/performanceconfig.cpp +++ b/src/performanceconfig.cpp @@ -1095,17 +1095,9 @@ std::string CPerformanceConfig::GetNewPerformanceDefaultName(void) return "Perf" + nIndex; } -void CPerformanceConfig::SetNewPerformanceName(std::string nName) +void CPerformanceConfig::SetNewPerformanceName(const std::string &Name) { - int i = nName.length(); - do - { - --i; - } - while (i>=0 && nName[i] == 32); - nName=nName.substr(0,i+1) ; - - NewPerformanceName = nName; + NewPerformanceName = Name.substr(0, Name.find_last_not_of(' ') + 1); } bool CPerformanceConfig::DeletePerformance(unsigned nID) diff --git a/src/performanceconfig.h b/src/performanceconfig.h index c9c4b27..916a2ee 100644 --- a/src/performanceconfig.h +++ b/src/performanceconfig.h @@ -137,7 +137,7 @@ public: bool CreateNewPerformanceFile(void); bool GetInternalFolderOk(); std::string GetNewPerformanceDefaultName(void); - void SetNewPerformanceName(std::string nName); + void SetNewPerformanceName(const std::string &Name); bool DeletePerformance(unsigned nID); bool CheckFreePerformanceSlot(void); std::string AddPerformanceBankDirName(unsigned nBankID); From 3878f3ef8d1de54c85c0940d1642fa3003388baf Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 27 Jan 2025 22:30:00 +0100 Subject: [PATCH 38/61] Add hardware configuration for mt32-Pi-Midi-Hat device (#787) --- hwconfig/chrissy-mt32-pi-midi-hat.override | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 hwconfig/chrissy-mt32-pi-midi-hat.override diff --git a/hwconfig/chrissy-mt32-pi-midi-hat.override b/hwconfig/chrissy-mt32-pi-midi-hat.override new file mode 100644 index 0000000..a5078ff --- /dev/null +++ b/hwconfig/chrissy-mt32-pi-midi-hat.override @@ -0,0 +1,31 @@ +# mt32-Pi-Midi-Hat by Chrissy version 1.7.2 +# https://github.com/chris-jh/mt32-pi-midi-hat + +SoundDevice=i2s +SampleRate=48000 +DACI2CAddress=0 +ChannelsSwapped=0 + +MIDIBaudRate=31250 +MIDIThru=umidi1,ttyS1 + +SSD1306LCDI2CAddress=0x3c +SSD1306LCDWidth=128 +SSD1306LCDHeight=64 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 + +LCDColumns=20 +LCDRows=4 + +ButtonPinPrev=22 +ButtonActionPrev=click +ButtonPinNext=23 +ButtonActionNext=click +ButtonPinBack=27 +ButtonActionBack=click +ButtonPinSelect=17 +ButtonActionSelect=click +ButtonPinHome=17 +ButtonActionHome=longpress +ButtonPinShortcut=0 From 7da3649166671458dd874ff7563bb1293d53679c Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 27 Jan 2025 22:31:18 +0100 Subject: [PATCH 39/61] Add hardware configuration for diyelectromusic boards (#788) --- .../diyelectromusic-RPi400MIDIAudio.override | 31 ++++++++++++++++++ ...yelectromusic-RpiMiniDexedHD44780.override | 31 ++++++++++++++++++ ...yelectromusic-RpiMiniDexedSSD1306.override | 26 +++++++++++++++ ...yelectromusic-RpiQuadDACMiniDexed.override | 32 +++++++++++++++++++ ...lectromusic-RpiV1MiniDexedIOBoard.override | 30 +++++++++++++++++ src/minidexed.ini | 1 + 6 files changed, 151 insertions(+) create mode 100644 hwconfig/diyelectromusic-RPi400MIDIAudio.override create mode 100644 hwconfig/diyelectromusic-RpiMiniDexedHD44780.override create mode 100644 hwconfig/diyelectromusic-RpiMiniDexedSSD1306.override create mode 100644 hwconfig/diyelectromusic-RpiQuadDACMiniDexed.override create mode 100644 hwconfig/diyelectromusic-RpiV1MiniDexedIOBoard.override diff --git a/hwconfig/diyelectromusic-RPi400MIDIAudio.override b/hwconfig/diyelectromusic-RPi400MIDIAudio.override new file mode 100644 index 0000000..ef76890 --- /dev/null +++ b/hwconfig/diyelectromusic-RPi400MIDIAudio.override @@ -0,0 +1,31 @@ +# diyelectromusic Raspberry Pi 400 MIDI and Audio Module (RPi400MIDIAudio) +# https://diyelectromusic.wordpress.com/2023/12/18/rpi-400-midi-and-audio-pcb-design/ +# https://diyelectromusic.wordpress.com/2023/12/18/rpi-400-midi-and-audio-pcb-build-guide/ + +SoundDevice=i2s + +LCDEnabled=1 +SSD1306LCDI2CAddress=0x3C +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 + +LCDColumns=20 +LCDRows=2 + +ButtonPinPrev=0 +ButtonActionPrev= +ButtonPinNext=0 +ButtonActionNext= +ButtonPinBack=5 +ButtonActionBack=click +ButtonPinSelect=11 +ButtonActionSelect=click +ButtonPinHome=6 +ButtonActionHome=click +ButtonPinShortcut=11 + +EncoderEnabled=1 +EncoderPinClock=10 +EncoderPinData=9 diff --git a/hwconfig/diyelectromusic-RpiMiniDexedHD44780.override b/hwconfig/diyelectromusic-RpiMiniDexedHD44780.override new file mode 100644 index 0000000..1e39915 --- /dev/null +++ b/hwconfig/diyelectromusic-RpiMiniDexedHD44780.override @@ -0,0 +1,31 @@ +# diyelectromusic Raspberry Pi MiniDexed IO Module (HD44780 Version) (RpiMiniDexedHD44780) +# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiMiniDexedHD44780 +# https://diyelectromusic.wordpress.com/2022/08/16/minidexed-raspberry-pi-io-board-part-3/ + +SoundDevice=i2s + +LCDEnabled=1 +LCDPinEnable=10 +LCDPinRegisterSelect=9 +LCDPinReadWrite=0 +LCDPinData4=22 +LCDPinData5=27 +LCDPinData6=17 +LCDPinData7=4 +LCDI2CAddress=0x00 +SSD1306LCDI2CAddress=0 +LCDColumns=16 +LCDRows=2 + +EncoderEnabled=1 +EncoderPinClock=24 +EncoderPinData=23 +For the two buttons, and the rotary encoder switch itself: + +ButtonPinBack=25 +ButtonActionBack=longpress +ButtonPinSelect=25 +ButtonActionSelect=click +ButtonPinHome=25 +ButtonActionHome=doubleclick +ButtonPinShortcut=25 diff --git a/hwconfig/diyelectromusic-RpiMiniDexedSSD1306.override b/hwconfig/diyelectromusic-RpiMiniDexedSSD1306.override new file mode 100644 index 0000000..93b25a4 --- /dev/null +++ b/hwconfig/diyelectromusic-RpiMiniDexedSSD1306.override @@ -0,0 +1,26 @@ +# diyelectromusic Raspberry Pi MiniDexed IO Module (SSD1306 Version) (RpiMiniDexedSSD1306) +# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiMiniDexedSSD1306 +# https://diyelectromusic.com/2022/08/16/minidexed-raspberry-pi-io-board-part-2/ + +SoundDevice=i2s + +LCDEnabled=1 +SSD1306LCDI2CAddress=0x3C +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 +LCDColumns=20 +LCDRows=2 + +ButtonPinBack=5 +ButtonActionBack=click +ButtonPinSelect=11 +ButtonActionSelect=click +ButtonPinHome=6 +ButtonActionHome=click +ButtonPinShortcut=11 + +EncoderEnabled=1 +EncoderPinClock=9 +EncoderPinData=10 diff --git a/hwconfig/diyelectromusic-RpiQuadDACMiniDexed.override b/hwconfig/diyelectromusic-RpiQuadDACMiniDexed.override new file mode 100644 index 0000000..f60048e --- /dev/null +++ b/hwconfig/diyelectromusic-RpiQuadDACMiniDexed.override @@ -0,0 +1,32 @@ +# diyelectromusic MiniDexed Quad DAC (RpiQuadDACMiniDexed) +# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiQuadDACMiniDexed +# https://diyelectromusic.com/2024/06/09/minidexed-quad-dac-pcb-design/ +# https://diyelectromusic.com/2024/06/09/minidexed-quad-dac-pcb-build-guide/ + +SoundDevice=i2s +QuadDAC8Chan=1 + +LCDEnabled=1 +SSD1306LCDI2CAddress=0x3C +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=1 +SSD1306LCDMirror=0 + +LCDColumns=20 +LCDRows=2 + +ButtonPinPrev=0 +ButtonActionPrev= +ButtonPinNext=0 +ButtonActionNext= +ButtonPinBack=5 +ButtonActionBack=click +ButtonPinSelect=11 +ButtonActionSelect=click +ButtonPinHome=6 +ButtonActionHome=click + +EncoderEnabled=1 +EncoderPinClock=10 +EncoderPinData=9 diff --git a/hwconfig/diyelectromusic-RpiV1MiniDexedIOBoard.override b/hwconfig/diyelectromusic-RpiV1MiniDexedIOBoard.override new file mode 100644 index 0000000..6a4ffb6 --- /dev/null +++ b/hwconfig/diyelectromusic-RpiV1MiniDexedIOBoard.override @@ -0,0 +1,30 @@ +# diyelectromusic MiniDexed Raspberry Pi V1 IO Board (RpiV1MiniDexedIOBoard) +# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiV1MiniDexedIOBoard +# https://diyelectromusic.com/2023/02/28/minidexed-raspberry-pi-v1-io-board-part-2/ + +SoundDevice=i2s + +LCDEnabled=1 +SSD1306LCDI2CAddress=0x3C +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=0 +SSD1306LCDMirror=0 +LCDColumns=20 +LCDRows=2 + +ButtonPinPrev=0 +ButtonActionPrev= +ButtonPinNext=0 +ButtonActionNext= +ButtonPinBack=22 +ButtonActionBack=click +ButtonPinSelect=11 +ButtonActionSelect=click +ButtonPinHome=27 +ButtonActionHome=click +ButtonPinShortcut=11 + +EncoderEnabled=1 +EncoderPinClock=9 +EncoderPinData=10 diff --git a/src/minidexed.ini b/src/minidexed.ini index 5b6b13d..7fbb229 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -12,6 +12,7 @@ DACI2CAddress=0 ChannelsSwapped=0 # Engine Type ( 1=Modern ; 2=Mark I ; 3=OPL ) EngineType=1 +QuadDAC8Chan=0 # MIDI MIDIBaudRate=31250 From 2e7d1144c78345d787072677d1f048349196b42e Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 27 Jan 2025 22:31:43 +0100 Subject: [PATCH 40/61] Add hardware configuration for Serdaco MP32L device (#786) --- hwconfig/serdaco_mp32l.override | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 hwconfig/serdaco_mp32l.override diff --git a/hwconfig/serdaco_mp32l.override b/hwconfig/serdaco_mp32l.override new file mode 100644 index 0000000..bf8e024 --- /dev/null +++ b/hwconfig/serdaco_mp32l.override @@ -0,0 +1,33 @@ +# Serdaco MP32L +# https://www.serdashop.com/MP32L + +SoundDevice=i2s +SampleRate=48000 +DACI2CAddress=0 +ChannelsSwapped=0 + +MIDIBaudRate=31250 +MIDIThru=umidi1,ttyS1 + +SSD1306LCDI2CAddress=0x3c +SSD1306LCDWidth=128 +SSD1306LCDHeight=32 +SSD1306LCDRotate=1 +SSD1306LCDMirror=0 + +LCDColumns=20 +LCDRows=2 + +ButtonPinPrev=17 +ButtonActionPrev=click +ButtonPinNext=27 +ButtonActionNext=click +ButtonPinBack=22 +ButtonActionBack=click +ButtonPinSelect=23 +ButtonActionSelect=click +ButtonPinHome=23 +ButtonActionHome=longpress +ButtonPinShortcut=0 + +EncoderEnabled=0 From 69edb84492edfe31c431953a60f0fceffadac80e Mon Sep 17 00:00:00 2001 From: probonopd Date: Wed, 5 Feb 2025 20:35:57 +0100 Subject: [PATCH 41/61] uses: actions/upload-artifact@v4 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43f6e6f..d1131ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,7 +92,7 @@ jobs: cd - mkdir -p ./sdcard/hardware/ cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/ - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ env.artifactName }} # Exported above path: ./sdcard/* From a7b8c80f4cae2d45e949f77634b94065ad93265e Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 15 Feb 2025 09:49:50 +0100 Subject: [PATCH 42/61] retention-days: 14 [ci skip] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1131ad..6ed1513 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,7 +96,7 @@ jobs: with: name: ${{ env.artifactName }} # Exported above path: ./sdcard/* - retention-days: 3 # To not exceed the free MB/month quota so quickly + retention-days: 14 # To not exceed the free MB/month quota so quickly - name: Upload to GitHub Releases (only when building from main branch) if: ${{ github.ref == 'refs/heads/main' }} run: | From bba0c93f04f8a40a897faf54378d7f11f2729452 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Mon, 10 Mar 2025 19:53:03 +0000 Subject: [PATCH 43/61] Fix for Issue #820 Saving default performance always overwrites the first performance in the current bank, but should act on bank 0, performance 0 --- src/minidexed.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 1a714fc..a485981 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -1483,6 +1483,7 @@ bool CMiniDexed::DoSavePerformance (void) if(m_bSaveAsDeault) { + m_PerformanceConfig.SetNewPerformanceBank(0); m_PerformanceConfig.SetNewPerformance(0); } From f546448371c4d630fff2d66ac2083711dc0e63bc Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 30 Mar 2025 13:30:54 +0200 Subject: [PATCH 44/61] Update circle to Step49 (#755) References: * https://github.com/probonopd/MiniDexed/pull/750 * https://github.com/probonopd/MiniDexed/pull/747 --- src/userinterface.cpp | 4 ++-- submod.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/userinterface.cpp b/src/userinterface.cpp index 32222a1..f99d16c 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -96,7 +96,7 @@ bool CUserInterface::Initialize (void) { m_pST7789Display->SetRotation (m_pConfig->GetST7789Rotation()); bool bLargeFont = !(m_pConfig->GetST7789SmallFont()); - m_pST7789 = new CST7789Device (m_pSPIMaster, m_pST7789Display, m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows (), bLargeFont, bLargeFont); + m_pST7789 = new CST7789Device (m_pSPIMaster, m_pST7789Display, m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows (), Font8x16, bLargeFont, bLargeFont); if (m_pST7789->Initialize()) { LOGDBG ("LCD: ST7789"); @@ -433,4 +433,4 @@ void CUserInterface::UISetMIDIButtonChannel (unsigned uCh) m_nMIDIButtonCh = CMIDIDevice::OmniMode; LOGNOTE("MIDI Button channel set to: OMNI"); } -} \ No newline at end of file +} diff --git a/submod.sh b/submod.sh index f9524a3..2e2f1a8 100755 --- a/submod.sh +++ b/submod.sh @@ -12,7 +12,7 @@ cd - # # Optional update submodules explicitly cd circle-stdlib/libs/circle -git checkout fff3764 +git checkout tags/Step49 cd - cd circle-stdlib/libs/circle-newlib #git checkout develop From 29bbce3472f3b03a15e96a8805ee9e3c04283f46 Mon Sep 17 00:00:00 2001 From: Kevin <68612569+diyelectromusic@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:26:15 +0100 Subject: [PATCH 45/61] MIDI CC 11 (Expression) (#830) * Simple implementation of MIDI CC 11 (Expression) as a live, MIDI only, multiplier for channel volume. * Add config option for global MIDI CC Expression channel --- src/config.cpp | 6 ++++++ src/config.h | 2 ++ src/mididevice.cpp | 34 +++++++++++++++++++++++++++++++--- src/mididevice.h | 1 + src/minidexed.cpp | 21 ++++++++++++++++++++- src/minidexed.h | 2 ++ 6 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 482b2b2..5a6fc88 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -112,6 +112,7 @@ void CConfig::Load (void) m_nMIDISystemCCVol = m_Properties.GetNumber ("MIDISystemCCVol", 0); m_nMIDISystemCCPan = m_Properties.GetNumber ("MIDISystemCCPan", 0); m_nMIDISystemCCDetune = m_Properties.GetNumber ("MIDISystemCCDetune", 0); + m_nMIDIGlobalExpression = m_Properties.GetNumber ("MIDIGlobalExpression", 0); m_bLCDEnabled = m_Properties.GetNumber ("LCDEnabled", 0) != 0; m_nLCDPinEnable = m_Properties.GetNumber ("LCDPinEnable", 4); @@ -353,6 +354,11 @@ unsigned CConfig::GetMIDISystemCCDetune (void) const return m_nMIDISystemCCDetune; } +unsigned CConfig::GetMIDIGlobalExpression (void) const +{ + return m_nMIDIGlobalExpression; +} + bool CConfig::GetLCDEnabled (void) const { return m_bLCDEnabled; diff --git a/src/config.h b/src/config.h index 5d0cbc1..166a14e 100644 --- a/src/config.h +++ b/src/config.h @@ -131,6 +131,7 @@ public: unsigned GetMIDISystemCCVol (void) const; unsigned GetMIDISystemCCPan (void) const; unsigned GetMIDISystemCCDetune (void) const; + unsigned GetMIDIGlobalExpression (void) const; // HD44780 LCD // GPIO pin numbers are chip numbers, not header positions @@ -267,6 +268,7 @@ private: unsigned m_nMIDISystemCCVol; unsigned m_nMIDISystemCCPan; unsigned m_nMIDISystemCCDetune; + unsigned m_nMIDIGlobalExpression; bool m_bLCDEnabled; unsigned m_nLCDPinEnable; diff --git a/src/mididevice.cpp b/src/mididevice.cpp index 216b466..fefe9fc 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -37,14 +37,15 @@ LOGMODULE ("mididevice"); #define MIDI_CHANNEL_AFTERTOUCH 0b1101 // right now Synth_Dexed just manage Channel Aftertouch not Polyphonic AT -> 0b1010 #define MIDI_CONTROL_CHANGE 0b1011 #define MIDI_CC_BANK_SELECT_MSB 0 - #define MIDI_CC_MODULATION 1 + #define MIDI_CC_MODULATION 1 #define MIDI_CC_BREATH_CONTROLLER 2 #define MIDI_CC_FOOT_PEDAL 4 - #define MIDI_CC_VOLUME 7 + #define MIDI_CC_VOLUME 7 #define MIDI_CC_PAN_POSITION 10 + #define MIDI_CC_EXPRESSION 11 #define MIDI_CC_BANK_SELECT_LSB 32 #define MIDI_CC_BANK_SUSTAIN 64 - #define MIDI_CC_RESONANCE 71 + #define MIDI_CC_RESONANCE 71 #define MIDI_CC_FREQUENCY_CUTOFF 74 #define MIDI_CC_REVERB_LEVEL 91 #define MIDI_CC_DETUNE_LEVEL 94 @@ -94,6 +95,15 @@ CMIDIDevice::CMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInter m_MIDISystemCCBitmap[2] = 0; m_MIDISystemCCBitmap[3] = 0; + m_nMIDIGlobalExpression = m_pConfig->GetMIDIGlobalExpression(); + // convert from config channels 1..16 to internal channels + if ((m_nMIDIGlobalExpression >= 1) && (m_nMIDIGlobalExpression <= 16)) { + m_nMIDIGlobalExpression = m_nMIDIGlobalExpression - 1; + } else { + // Either disabled or OMNI means disabled + m_nMIDIGlobalExpression = Disabled; + } + for (int tg=0; tg<8; tg++) { if (m_nMIDISystemCCVol != 0) { @@ -279,6 +289,17 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign } } } + if (m_nMIDIGlobalExpression != Disabled) + { + // Expression is global so check for expression MIDI channel + // NB: OMNI not supported + if (ucChannel == m_nMIDIGlobalExpression) { + // Send to all TGs regardless of their own channel + for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators(); nTG++) { + m_pSynthesizer->SetExpression (pMessage[2], nTG); + } + } + } if (nLength == 3) { m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]); @@ -398,6 +419,13 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign m_pSynthesizer->SetPan (pMessage[2], nTG); break; + case MIDI_CC_EXPRESSION: + if (m_nMIDIGlobalExpression == Disabled) { + // Expression is per channel only + m_pSynthesizer->SetExpression (pMessage[2], nTG); + } + break; + case MIDI_CC_BANK_SELECT_MSB: m_pSynthesizer->BankSelectMSB (pMessage[2], nTG); break; diff --git a/src/mididevice.h b/src/mididevice.h index 44f1691..a8eae40 100644 --- a/src/mididevice.h +++ b/src/mididevice.h @@ -75,6 +75,7 @@ private: unsigned m_nMIDISystemCCPan; unsigned m_nMIDISystemCCDetune; u32 m_MIDISystemCCBitmap[4]; // to allow for 128 bit entries + unsigned m_nMIDIGlobalExpression; std::string m_DeviceName; diff --git a/src/minidexed.cpp b/src/minidexed.cpp index a485981..4d9a53c 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -72,6 +72,7 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_nVoiceBankIDMSB[i] = 0; m_nProgram[i] = 0; m_nVolume[i] = 100; + m_nExpression[i] = 127; m_nPan[i] = 64; m_nMasterTune[i] = 0; m_nCutoff[i] = 99; @@ -280,6 +281,7 @@ bool CMiniDexed::Initialize (void) assert (m_pTG[i]); SetVolume (100, i); + SetExpression (127, i); ProgramChange (0, i); m_pTG[i]->setTranspose (24); @@ -648,11 +650,28 @@ void CMiniDexed::SetVolume (unsigned nVolume, unsigned nTG) m_nVolume[nTG] = nVolume; assert (m_pTG[nTG]); - m_pTG[nTG]->setGain (nVolume / 127.0f); + m_pTG[nTG]->setGain ((m_nVolume[nTG] * m_nExpression[nTG]) / (127.0f * 127.0f)); m_UI.ParameterChanged (); } +void CMiniDexed::SetExpression (unsigned nExpression, unsigned nTG) +{ + nExpression=constrain((int)nExpression,0,127); + + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + + m_nExpression[nTG] = nExpression; + + assert (m_pTG[nTG]); + m_pTG[nTG]->setGain ((m_nVolume[nTG] * m_nExpression[nTG]) / (127.0f * 127.0f)); + + // Expression is a "live performance" parameter only set + // via MIDI and not via the UI. + //m_UI.ParameterChanged (); +} + void CMiniDexed::SetPan (unsigned nPan, unsigned nTG) { nPan=constrain((int)nPan,0,127); diff --git a/src/minidexed.h b/src/minidexed.h index bb6290b..6a7ef81 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -74,6 +74,7 @@ public: void ProgramChange (unsigned nProgram, unsigned nTG); void ProgramChangePerformance (unsigned nProgram); void SetVolume (unsigned nVolume, unsigned nTG); + void SetExpression (unsigned nExpression, unsigned nTG); void SetPan (unsigned nPan, unsigned nTG); // 0 .. 127 void SetMasterTune (int nMasterTune, unsigned nTG); // -99 .. 99 void SetCutoff (int nCutoff, unsigned nTG); // 0 .. 99 @@ -261,6 +262,7 @@ private: unsigned m_nVoiceBankIDMSBPerformance; unsigned m_nProgram[CConfig::AllToneGenerators]; unsigned m_nVolume[CConfig::AllToneGenerators]; + unsigned m_nExpression[CConfig::AllToneGenerators]; unsigned m_nPan[CConfig::AllToneGenerators]; int m_nMasterTune[CConfig::AllToneGenerators]; int m_nCutoff[CConfig::AllToneGenerators]; From d147e9017cf8f6470c32ea771590801e66076155 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 13:50:21 +0200 Subject: [PATCH 46/61] SampleRate=48000 https://github.com/probonopd/MiniDexed/discussions/836#discussioncomment-12875015 --- hwconfig/DT-DX.override | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hwconfig/DT-DX.override b/hwconfig/DT-DX.override index 1e6ba75..f27c4e6 100644 --- a/hwconfig/DT-DX.override +++ b/hwconfig/DT-DX.override @@ -2,7 +2,7 @@ # https://www.dtronics.nl/dt-dx SoundDevice=i2s -SampleRate=22000 +SampleRate=48000 ChunkSize=256 DACI2CAddress=0x0 ChannelsSwapped=1 @@ -43,4 +43,4 @@ LongPressTimeout=400 EncoderEnabled=1 EncoderPinClock=6 -EncoderPinData=5 \ No newline at end of file +EncoderPinData=5 From 7c75611b197dcc50c89ffdc338bd01872be52898 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 13:52:54 +0200 Subject: [PATCH 47/61] runs-on: ubuntu-22.04 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ed1513..ca76f8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: jobs: Build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 From 3880c5f50398dd0985e95f25d6f2d5c6ee9c0cd8 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 14:33:24 +0200 Subject: [PATCH 48/61] Comment out retention-days [ci skip] https://github.com/soyersoyer/MiniDexed/pull/5#issuecomment-2815324521 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca76f8a..af59cac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,7 +96,7 @@ jobs: with: name: ${{ env.artifactName }} # Exported above path: ./sdcard/* - retention-days: 14 # To not exceed the free MB/month quota so quickly + # retention-days: 14 # To not exceed the free MB/month quota so quickly - name: Upload to GitHub Releases (only when building from main branch) if: ${{ github.ref == 'refs/heads/main' }} run: | From 4ee36edaf64a6aa22b9ac2e99f6d0e5a89d04fd4 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 18:40:35 +0200 Subject: [PATCH 49/61] List files --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af59cac..7c6a8e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,6 +92,8 @@ jobs: cd - mkdir -p ./sdcard/hardware/ cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/ + ls -lh ./sdcard/ + ls -lh ./sdcard/hardware/ - uses: actions/upload-artifact@v4 with: name: ${{ env.artifactName }} # Exported above From 4ac357e51d13a1ec2f3e2272d86f9946eec64154 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 18:51:08 +0200 Subject: [PATCH 50/61] Zip after adding hardware --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c6a8e7..4299396 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,10 +81,6 @@ jobs: run: | git clone https://github.com/Banana71/Soundplantage --depth 1 # depth 1 means only the latest commit cp -r ./Soundplantage/performance ./Soundplantage/*.pdf ./sdcard/ - cd sdcard - zip -r ../MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip * - echo "artifactName=MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - cd - - name: Hardware configration files run: | cd hwconfig @@ -92,8 +88,12 @@ jobs: cd - mkdir -p ./sdcard/hardware/ cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/ - ls -lh ./sdcard/ - ls -lh ./sdcard/hardware/ + - name: zip + run: | + cd sdcard + zip -r ../MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip * + echo "artifactName=MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV + cd - - uses: actions/upload-artifact@v4 with: name: ${{ env.artifactName }} # Exported above From 9b211c1c015a195cfceb6eb5bce9ea23f48bedd0 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 20:22:00 +0200 Subject: [PATCH 51/61] Show bank:voice number for voices (#840) e.g. 005:012 Closes #832 --- src/uimenu.cpp | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/uimenu.cpp b/src/uimenu.cpp index efaf21f..6239f65 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -702,15 +702,30 @@ void CUIMenu::EditProgramNumber (CUIMenu *pUIMenu, TMenuEvent Event) CUIMenu::EditProgramNumber (pUIMenu, MenuEventStepDown); } } else { - string TG ("TG"); - TG += to_string (nTG+1); - - string Value = to_string (nValue+1) + "=" + pUIMenu->m_pMiniDexed->GetVoiceName (nTG); - - pUIMenu->m_pUI->DisplayWrite (TG.c_str (), - pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, - Value.c_str (), - nValue > 0, nValue < (int) CSysExFileLoader::VoicesPerBank-1); + // Format: 000:000 TG1 (bank:voice padded, TGx right-aligned) + int nBank = pUIMenu->m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG); + std::string left = "000"; + left += std::to_string(nBank+1); + left = left.substr(left.length()-3,3); + left += ":"; + std::string voiceNum = "000"; + voiceNum += std::to_string(nValue+1); + voiceNum = voiceNum.substr(voiceNum.length()-3,3); + left += voiceNum; + + std::string tgLabel = "TG" + std::to_string(nTG+1); + unsigned lcdCols = pUIMenu->m_pConfig->GetLCDColumns(); + unsigned pad = 0; + if (lcdCols > left.length() + tgLabel.length()) + pad = lcdCols - (unsigned)(left.length() + tgLabel.length()); + std::string topLine = left + std::string(pad, ' ') + tgLabel; + + std::string Value = pUIMenu->m_pMiniDexed->GetVoiceName (nTG); + + pUIMenu->m_pUI->DisplayWrite (topLine.c_str(), + "", + Value.c_str(), + nValue > 0, nValue < (int) CSysExFileLoader::VoicesPerBank); } } @@ -1996,5 +2011,3 @@ void CUIMenu::EditTGParameterModulation (CUIMenu *pUIMenu, TMenuEvent Event) nValue > rParam.Minimum, nValue < rParam.Maximum); } - - From 98a31df28ddac5086c10dadaa7015a49870f4e5f Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 22:05:10 +0200 Subject: [PATCH 52/61] Prevent PCM510x has a zero-data detect from kicking in (#843) The PCM510x has a zero-data detect function. When the device detects continuous zero data, it enters a full analog mute condition. The PCM510x counts zero data over 1024LRCKs (21ms @ 48kHz) before setting analog mute. This caused audible artifacts. Hence we are generating an inaudibly small signal to prevent the analog mute from kicking in. Thanks @soyersoyer Similar to soyersoyer#5 --- src/minidexed.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 4d9a53c..f49c840 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -1309,6 +1309,15 @@ void CMiniDexed::ProcessSound (void) arm_fill_q15(0, tmp_int, nFrames*Channels); } + // Prevent PCM510x analog mute from kicking in + for (uint8_t tg = 0; tg < Channels; tg++) + { + if (tmp_int[(nFrames - 1) * Channels + tg] == 0) + { + tmp_int[(nFrames - 1) * Channels + tg]++; + } + } + if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int)) { LOGERR ("Sound data dropped"); @@ -1394,6 +1403,12 @@ void CMiniDexed::ProcessSound (void) arm_fill_q15(0, tmp_int, nFrames * 2); } + // Prevent PCM510x analog mute from kicking in + if (tmp_int[nFrames * 2 - 1] == 0) + { + tmp_int[nFrames * 2 - 1]++; + } + if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int)) { LOGERR ("Sound data dropped"); From 6cbdab5f895a1810cd922d079a6c56d45a3fcf2b Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 23:18:31 +0200 Subject: [PATCH 53/61] Introduce MasterVolume (#842) --- src/config.cpp | 2 ++ src/config.h | 4 ++++ src/minidexed.cpp | 18 +++++++++++------- src/minidexed.ini | 2 ++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 5a6fc88..7a53d58 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -200,6 +200,8 @@ void CConfig::Load (void) m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0; m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 1) != 0; m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); + + m_nMasterVolume = m_Properties.GetNumber ("MasterVolume", 64); } unsigned CConfig::GetToneGenerators (void) const diff --git a/src/config.h b/src/config.h index 166a14e..b0735d3 100644 --- a/src/config.h +++ b/src/config.h @@ -239,6 +239,8 @@ public: bool GetPerformanceSelectToLoad (void) const; unsigned GetPerformanceSelectChannel (void) const; + unsigned GetMasterVolume() const { return m_nMasterVolume; } + private: CPropertiesFatFsFile m_Properties; @@ -353,6 +355,8 @@ private: bool m_bProfileEnabled; bool m_bPerformanceSelectToLoad; unsigned m_bPerformanceSelectChannel; + + unsigned m_nMasterVolume; // Master volume 0-127 }; #endif diff --git a/src/minidexed.cpp b/src/minidexed.cpp index f49c840..21709af 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -218,7 +218,8 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, } #endif - setMasterVolume(1.0); + float masterVolNorm = (float)(pConfig->GetMasterVolume()) / 127.0f; + setMasterVolume(masterVolNorm); // BEGIN setup tg_mixer tg_mixer = new AudioStereoMixer(pConfig->GetChunkSize()/2); @@ -1800,14 +1801,17 @@ void CMiniDexed::getSysExVoiceDump(uint8_t* dest, uint8_t nTG) dest[162] = 0xF7; // SysEx end } -void CMiniDexed::setMasterVolume (float32_t vol) +void CMiniDexed::setMasterVolume(float32_t vol) { - if(vol < 0.0) - vol = 0.0; - else if(vol > 1.0) - vol = 1.0; + if (vol < 0.0) + vol = 0.0; + else if (vol > 1.0) + vol = 1.0; - nMasterVolume=vol; + // Apply logarithmic scaling to match perceived loudness + vol = powf(vol, 2.0f); + + nMasterVolume = vol; } std::string CMiniDexed::GetPerformanceFileName(unsigned nID) diff --git a/src/minidexed.ini b/src/minidexed.ini index 7fbb229..a20b41d 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -13,6 +13,8 @@ ChannelsSwapped=0 # Engine Type ( 1=Modern ; 2=Mark I ; 3=OPL ) EngineType=1 QuadDAC8Chan=0 +# Master Volume (0-127) +MasterVolume=64 # MIDI MIDIBaudRate=31250 From d7a955e193f53eeecda3175cbab833b895221ab5 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 20 Apr 2025 16:25:30 +0200 Subject: [PATCH 54/61] Mark continuous builds as prerelease --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4299396..fb2ef34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,6 +103,9 @@ jobs: if: ${{ github.ref == 'refs/heads/main' }} run: | set -ex + UPLOADTOOL_ISPRERELEASE=true + UPLOADTOOL_PR_BODY="This is a continuous build. Feedback is appreciated." + UPLOADTOOL_BODY="This is a continuous build. Feedback is appreciated." wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh bash ./upload.sh ./MiniDexed*.zip From 5bc489574dbb461cfba991f9447849646e7bd8db Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 20 Apr 2025 16:37:26 +0200 Subject: [PATCH 55/61] Export variables --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb2ef34..e0e3918 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,9 +103,9 @@ jobs: if: ${{ github.ref == 'refs/heads/main' }} run: | set -ex - UPLOADTOOL_ISPRERELEASE=true - UPLOADTOOL_PR_BODY="This is a continuous build. Feedback is appreciated." - UPLOADTOOL_BODY="This is a continuous build. Feedback is appreciated." + export UPLOADTOOL_ISPRERELEASE=true + export UPLOADTOOL_PR_BODY="This is a continuous build. Feedback is appreciated." + export UPLOADTOOL_BODY="This is a continuous build. Feedback is appreciated." wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh bash ./upload.sh ./MiniDexed*.zip From 5f8389905def692b8edd0e377217b9afd5ed5bcc Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 21 Apr 2025 13:47:58 +0200 Subject: [PATCH 56/61] Fix line endings sed -i 's/\r$//' build.sh --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index b69ba6b..ed319a6 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e set -x if [ -z "${RPI}" ] ; then From 04c5ce381895b767cf1392281faf843ba62d6b3d Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 21 Apr 2025 21:50:10 +0200 Subject: [PATCH 57/61] Network support by @omersiar Network support by @omersiar, closes #43 (and much more) Features as described on https://github.com/probonopd/MiniDexed/wiki/Networking Big thanks to @soyersoyer for debugging the crackle issue --- .github/workflows/build.yml | 233 ++++--- build.sh | 17 +- src/Makefile | 10 +- src/Rules.mk | 7 +- src/config.cpp | 68 ++ src/config.h | 25 + src/kernel.cpp | 5 +- src/kernel.h | 2 + src/minidexed.cpp | 252 +++++++- src/minidexed.h | 25 +- src/minidexed.ini | 12 + src/net/applemidi.cpp | 874 +++++++++++++++++++++++++ src/net/applemidi.h | 111 ++++ src/net/byteorder.h | 42 ++ src/net/ftpdaemon.cpp | 111 ++++ src/net/ftpdaemon.h | 47 ++ src/net/ftpworker.cpp | 1218 +++++++++++++++++++++++++++++++++++ src/net/ftpworker.h | 157 +++++ src/net/mdnspublisher.cpp | 345 ++++++++++ src/net/mdnspublisher.h | 90 +++ src/net/udpmidi.cpp | 89 +++ src/net/udpmidi.h | 57 ++ src/net/utility.h | 193 ++++++ src/udpmididevice.cpp | 90 +++ src/udpmididevice.h | 55 ++ submod.sh | 23 +- 26 files changed, 4043 insertions(+), 115 deletions(-) create mode 100644 src/net/applemidi.cpp create mode 100644 src/net/applemidi.h create mode 100644 src/net/byteorder.h create mode 100644 src/net/ftpdaemon.cpp create mode 100644 src/net/ftpdaemon.h create mode 100644 src/net/ftpworker.cpp create mode 100644 src/net/ftpworker.h create mode 100644 src/net/mdnspublisher.cpp create mode 100644 src/net/mdnspublisher.h create mode 100644 src/net/udpmidi.cpp create mode 100644 src/net/udpmidi.h create mode 100644 src/net/utility.h create mode 100644 src/udpmididevice.cpp create mode 100644 src/udpmididevice.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0e3918..0b3c310 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,5 @@ +# Build 32-bit and 64-bit separately + name: Build env: @@ -9,103 +11,142 @@ on: pull_request: jobs: - Build: + build64: + name: Build 64-bit kernels + runs-on: ubuntu-22.04 + outputs: + artifact-path: ${{ steps.upload64.outputs.artifact-path }} + steps: + - uses: actions/checkout@v2 + - name: Get specific commits of git submodules + run: sh -ex ./submod.sh + - name: Create sdcard directory + run: mkdir -p ./sdcard/ + - name: Put git hash in startup message + run: | + sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp + + # Install 64-bit toolchain (aarch64) + - name: Install 64-bit toolchain + run: | + set -ex + wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz + tar xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz + + - name: Build for Raspberry Pi 5 (64-bit) + run: | + set -ex + export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH + RPI=5 bash -ex build.sh + cp ./src/kernel*.img ./sdcard/ + + - name: Build for Raspberry Pi 4 (64-bit) + run: | + set -ex + export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH + RPI=4 bash -ex build.sh + cp ./src/kernel*.img ./sdcard/ + + - name: Build for Raspberry Pi 3 (64-bit) + run: | + set -ex + export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH + RPI=3 bash -ex build.sh + cp ./src/kernel*.img ./sdcard/ + + - name: Prepare SD card content for 64-bit + run: | + set -ex + export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH + cd ./circle-stdlib/libs/circle/boot + make + make armstub64 + cd - + cp -r ./circle-stdlib/libs/circle/boot/* sdcard + rm -rf sdcard/config*.txt sdcard/README sdcard/Makefile sdcard/armstub sdcard/COPYING.linux + cp ./src/config.txt ./src/minidexed.ini ./src/*img ./src/performance.ini sdcard/ + cp ./getsysex.sh sdcard/ + echo "usbspeed=full" > sdcard/cmdline.txt + + - name: Upload 64-bit artifacts + id: upload64 + uses: actions/upload-artifact@v4 + with: + name: build64-artifacts + path: sdcard/* + + build32: + name: Build 32-bit kernels runs-on: ubuntu-22.04 + outputs: + artifact-path: ${{ steps.upload32.outputs.artifact-path }} + steps: + - uses: actions/checkout@v2 + - name: Get specific commits of git submodules + run: sh -ex ./submod.sh + - name: Create sdcard directory + run: mkdir -p ./sdcard/ + - name: Put git hash in startup message + run: | + sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp + + # Install 32-bit toolchain (arm-none-eabi) + - name: Install 32-bit toolchain + run: | + set -ex + wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz + tar xf gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz + + - name: Build for Raspberry Pi 2 (32-bit) + run: | + set -ex + export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-arm-none-eabi/bin):$PATH + RPI=2 bash -ex build.sh + cp ./src/kernel*.img ./sdcard/ + - name: Build for Raspberry Pi 1 (32-bit) + run: | + set -ex + export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-arm-none-eabi/bin):$PATH + RPI=1 bash -ex build.sh + cp ./src/kernel*.img ./sdcard/ + + - name: Upload 32-bit artifacts + id: upload32 + uses: actions/upload-artifact@v4 + with: + name: build32-artifacts + path: sdcard/* + + combine: + name: Combine Artifacts + runs-on: ubuntu-22.04 + needs: [ build64, build32 ] steps: - - uses: actions/checkout@v2 - - name: Get specific commits of git submodules - run: | - sh -ex ./submod.sh - - name: Apply patches - run: | - # Put git hash in startup message - sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp - - name: Install toolchains - run: | - set -ex - wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz - tar xf *-aarch64-none-elf.tar.xz - wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz - tar xf *-arm-none-eabi.tar.xz - mkdir -p kernels - - name: Build for Raspberry Pi 5 - run: | - set -ex - export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH - RPI=5 bash -ex build.sh - cp ./src/kernel*.img ./kernels/ - - name: Build for Raspberry Pi 4 - run: | - set -ex - export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH - RPI=4 bash -ex build.sh - cp ./src/kernel*.img ./kernels/ - - name: Build for Raspberry Pi 3 - run: | - set -ex - export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH - RPI=3 bash -ex build.sh - cp ./src/kernel*.img ./kernels/ - - name: Build for Raspberry Pi 2 - run: | - set -ex - export PATH=$(readlink -f ./gcc-*arm-none*/bin/):$PATH - RPI=2 bash -ex build.sh - cp ./src/kernel*.img ./kernels/ - - name: Build for Raspberry Pi 1 - run: | - set -ex - export PATH=$(readlink -f ./gcc-*arm-none*/bin/):$PATH - RPI=1 bash -ex build.sh - cp ./src/kernel*.img ./kernels/ - - name: Get Raspberry Pi boot files - run: | - set -ex - export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH - cd ./circle-stdlib/libs/circle/boot - make - make armstub64 - cd - - mkdir -p sdcard - cp -r ./circle-stdlib/libs/circle/boot/* sdcard - rm -rf sdcard/config*.txt sdcard/README sdcard/Makefile sdcard/armstub sdcard/COPYING.linux - cp ./src/config.txt ./src/minidexed.ini ./src/*img ./src/performance.ini sdcard/ - cp ./getsysex.sh sdcard/ - echo "usbspeed=full" > sdcard/cmdline.txt - cd sdcard - cp ../kernels/* . || true - cd - - - name: Get performance files - run: | - git clone https://github.com/Banana71/Soundplantage --depth 1 # depth 1 means only the latest commit - cp -r ./Soundplantage/performance ./Soundplantage/*.pdf ./sdcard/ - - name: Hardware configration files - run: | - cd hwconfig - sh -ex ./customize.sh - cd - - mkdir -p ./sdcard/hardware/ - cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/ - - name: zip - run: | - cd sdcard - zip -r ../MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip * - echo "artifactName=MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - cd - - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.artifactName }} # Exported above - path: ./sdcard/* - # retention-days: 14 # To not exceed the free MB/month quota so quickly - - name: Upload to GitHub Releases (only when building from main branch) - if: ${{ github.ref == 'refs/heads/main' }} - run: | - set -ex - export UPLOADTOOL_ISPRERELEASE=true - export UPLOADTOOL_PR_BODY="This is a continuous build. Feedback is appreciated." - export UPLOADTOOL_BODY="This is a continuous build. Feedback is appreciated." - wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh - bash ./upload.sh ./MiniDexed*.zip - + - name: Download 64-bit artifacts + uses: actions/download-artifact@v4 + with: + name: build64-artifacts + path: combined + - name: Download 32-bit artifacts + uses: actions/download-artifact@v4 + with: + name: build32-artifacts + path: combined + - name: Create combined ZIP file + run: | + cd combined + zip -r ../MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip . + cd .. + - name: Upload combined ZIP artifact + uses: actions/upload-artifact@v4 + with: + name: combined-artifact + path: MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip + - name: Upload to GitHub Releases (only from main branch) + if: ${{ github.ref == 'refs/heads/main' }} + run: | + set -ex + wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh + bash ./upload.sh MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip diff --git a/build.sh b/build.sh index ed319a6..8da3296 100755 --- a/build.sh +++ b/build.sh @@ -20,6 +20,11 @@ if [ "${RPI}" -gt "1" ]; then OPTIONS="${OPTIONS} -o ARM_ALLOW_MULTI_CORE" fi +# For wireless access +if [ "${RPI}" == "3" ]; then + OPTIONS="${OPTIONS} -o USE_SDHOST" +fi + # USB Vendor and Device ID for use with USB Gadget Mode source USBID.sh if [ "${USB_VID}" ] ; then @@ -39,6 +44,11 @@ make -j cd libs/circle/addon/display/ make clean || true make -j + +cd ../wlan/ +make clean || true +make -j + cd ../sensor/ make clean || true make -j @@ -51,7 +61,12 @@ cd .. # Build MiniDexed cd src -make clean || true +make clean +echo "***** DEBUG *****" +env +rm -rf ./gcc-* || true +grep -r 'aarch64-none-elf' . || true +find . -type d -name 'aarch64-none-elf' || true make -j ls *.img cd .. diff --git a/src/Makefile b/src/Makefile index 540ae68..7882018 100644 --- a/src/Makefile +++ b/src/Makefile @@ -9,9 +9,17 @@ CMSIS_DIR = ../CMSIS_5/CMSIS OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \ mididevice.o midikeyboard.o serialmididevice.o pckeyboard.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 \ + net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o net/mdnspublisher.o udpmididevice.o OPTIMIZE = -O3 include ./Synth_Dexed.mk include ./Rules.mk + +# Clean target +.PHONY: clean + +clean: + @echo "Cleaning up..." + rm -f $(OBJS) *.o *.d *~ core diff --git a/src/Rules.mk b/src/Rules.mk index 2ebc132..771350f 100644 --- a/src/Rules.mk +++ b/src/Rules.mk @@ -28,6 +28,11 @@ LIBS += \ $(CIRCLEHOME)/addon/fatfs/libfatfs.a \ $(CIRCLEHOME)/lib/fs/libfs.a \ $(CIRCLEHOME)/lib/sched/libsched.a \ - $(CIRCLEHOME)/lib/libcircle.a + $(CIRCLEHOME)/lib/libcircle.a \ + $(CIRCLEHOME)/addon/wlan/hostap/wpa_supplicant/libwpa_supplicant.a \ + $(CIRCLEHOME)/addon/wlan/libwlan.a \ + $(CIRCLEHOME)/lib/net/libnet.a + +EXTRACLEAN += $(NET_DIR)/*.d $(NET_DIR)/*.o -include $(DEPS) diff --git a/src/config.cpp b/src/config.cpp index 7a53d58..672d7cc 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -200,6 +200,23 @@ void CConfig::Load (void) m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0; m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 1) != 0; m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); + + // Network + m_bNetworkEnabled = m_Properties.GetNumber ("NetworkEnabled", 0) != 0; + m_bNetworkDHCP = m_Properties.GetNumber ("NetworkDHCP", 0) != 0; + m_NetworkType = m_Properties.GetString ("NetworkType", "wlan"); + m_NetworkHostname = m_Properties.GetString ("NetworkHostname", "MiniDexed"); + m_INetworkIPAddress = m_Properties.GetIPAddress("NetworkIPAddress") != 0; + m_INetworkSubnetMask = m_Properties.GetIPAddress("NetworkSubnetMask") != 0; + m_INetworkDefaultGateway = m_Properties.GetIPAddress("NetworkDefaultGateway") != 0; + m_bSyslogEnabled = m_Properties.GetNumber ("SyslogEnabled", 0) != 0; + m_INetworkDNSServer = m_Properties.GetIPAddress("NetworkDNSServer") != 0; + + const u8 *pSyslogServerIP = m_Properties.GetIPAddress ("NetworkSyslogServerIPAddress"); + if (pSyslogServerIP) + { + m_INetworkSyslogServerIPAddress.Set (pSyslogServerIP); + } m_nMasterVolume = m_Properties.GetNumber ("MasterVolume", 64); } @@ -724,3 +741,54 @@ unsigned CConfig::GetPerformanceSelectChannel (void) const { return m_bPerformanceSelectChannel; } + +// Network +bool CConfig::GetNetworkEnabled (void) const +{ + return m_bNetworkEnabled; +} + +bool CConfig::GetNetworkDHCP (void) const +{ + return m_bNetworkDHCP; +} + +const char *CConfig::GetNetworkType (void) const +{ + return m_NetworkType.c_str(); +} + +const char *CConfig::GetNetworkHostname (void) const +{ + return m_NetworkHostname.c_str(); +} + +CIPAddress CConfig::GetNetworkIPAddress (void) const +{ + return m_INetworkIPAddress; +} + +CIPAddress CConfig::GetNetworkSubnetMask (void) const +{ + return m_INetworkSubnetMask; +} + +CIPAddress CConfig::GetNetworkDefaultGateway (void) const +{ + return m_INetworkDefaultGateway; +} + +CIPAddress CConfig::GetNetworkDNSServer (void) const +{ + return m_INetworkDNSServer; +} + +bool CConfig::GetSyslogEnabled (void) const +{ + return m_bSyslogEnabled; +} + +CIPAddress CConfig::GetNetworkSyslogServerIPAddress (void) const +{ + return m_INetworkSyslogServerIPAddress; +} diff --git a/src/config.h b/src/config.h index b0735d3..fcb8cca 100644 --- a/src/config.h +++ b/src/config.h @@ -23,6 +23,7 @@ #ifndef _config_h #define _config_h +#include #include #include #include @@ -241,6 +242,18 @@ public: unsigned GetMasterVolume() const { return m_nMasterVolume; } + // Network + bool GetNetworkEnabled (void) const; + bool GetNetworkDHCP (void) const; + const char *GetNetworkType (void) const; + const char *GetNetworkHostname (void) const; + CIPAddress GetNetworkIPAddress (void) const; + CIPAddress GetNetworkSubnetMask (void) const; + CIPAddress GetNetworkDefaultGateway (void) const; + CIPAddress GetNetworkDNSServer (void) const; + bool GetSyslogEnabled (void) const; + CIPAddress GetNetworkSyslogServerIPAddress (void) const; + private: CPropertiesFatFsFile m_Properties; @@ -357,6 +370,18 @@ private: unsigned m_bPerformanceSelectChannel; unsigned m_nMasterVolume; // Master volume 0-127 + + // Network + bool m_bNetworkEnabled; + bool m_bNetworkDHCP; + std::string m_NetworkType; + std::string m_NetworkHostname; + CIPAddress m_INetworkIPAddress; + CIPAddress m_INetworkSubnetMask; + CIPAddress m_INetworkDefaultGateway; + CIPAddress m_INetworkDNSServer; + bool m_bSyslogEnabled; + CIPAddress m_INetworkSyslogServerIPAddress; }; #endif diff --git a/src/kernel.cpp b/src/kernel.cpp index 446747b..c6c86d9 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -25,12 +25,15 @@ #include #include "usbminidexedmidigadget.h" +#define NET_DEVICE_TYPE NetDeviceTypeWLAN // or: NetDeviceTypeWLAN + LOGMODULE ("kernel"); CKernel *CKernel::s_pThis = 0; CKernel::CKernel (void) -: CStdlibAppStdio ("minidexed"), +: + CStdlibAppStdio ("minidexed"), m_Config (&mFileSystem), m_GPIOManager (&mInterrupt), m_I2CMaster (CMachineInfo::Get ()->GetDevice (DeviceI2CMaster), TRUE), diff --git a/src/kernel.h b/src/kernel.h index efe5f4f..22d6891 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -26,6 +26,7 @@ #include #include #include +#include #include "config.h" #include "minidexed.h" @@ -58,6 +59,7 @@ private: CSPIMaster *m_pSPIMaster; CMiniDexed *m_pDexed; CUSBController *m_pUSB; + CScheduler m_Scheduler; static CKernel *s_pThis; }; diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 21709af..1cca446 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -23,11 +23,18 @@ #include #include #include +#include +#include #include #include #include #include +const char WLANFirmwarePath[] = "SD:firmware/"; +const char WLANConfigFile[] = "SD:wpa_supplicant.conf"; +#define FTPUSERNAME "admin" +#define FTPPASSWORD "admin" + LOGMODULE ("minidexed"); CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, @@ -51,6 +58,14 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_GetChunkTimer ("GetChunk", 1000000U * pConfig->GetChunkSize ()/2 / pConfig->GetSampleRate ()), m_bProfileEnabled (m_pConfig->GetProfileEnabled ()), + m_pNet(nullptr), + m_pNetDevice(nullptr), + m_WLAN(nullptr), + m_WPASupplicant(nullptr), + m_bNetworkReady(false), + m_bNetworkInit(false), + m_UDPMIDI(nullptr), + m_pmDNSPublisher (nullptr), m_bSavePerformance (false), m_bSavePerformanceNewFile (false), m_bSetNewPerformance (false), @@ -244,8 +259,18 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, SetParameter (ParameterPerformanceBank, 0); }; +CMiniDexed::~CMiniDexed (void) +{ + delete m_WLAN; + delete m_WPASupplicant; + delete m_UDPMIDI; + delete m_pFTPDaemon; + delete m_pmDNSPublisher; +} + bool CMiniDexed::Initialize (void) { + LOGNOTE("CMiniDexed::Initialize called"); assert (m_pConfig); assert (m_pSoundDevice); @@ -346,21 +371,27 @@ bool CMiniDexed::Initialize (void) { return false; } + + InitNetwork(); // returns bool but we continue even if something goes wrong + LOGNOTE("CMiniDexed::Initialize: InitNetwork() called"); #endif - + return true; } void CMiniDexed::Process (bool bPlugAndPlayUpdated) { + CScheduler* const pScheduler = CScheduler::Get(); #ifndef ARM_ALLOW_MULTI_CORE ProcessSound (); + pScheduler->Yield(); #endif for (unsigned i = 0; i < CConfig::MaxUSBMIDIDevices; i++) { assert (m_pMIDIKeyboard[i]); m_pMIDIKeyboard[i]->Process (bPlugAndPlayUpdated); + pScheduler->Yield(); } m_PCKeyboard.Process (bPlugAndPlayUpdated); @@ -368,6 +399,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) if (m_bUseSerial) { m_SerialMIDI.Process (); + pScheduler->Yield(); } m_UI.Process (); @@ -377,12 +409,14 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) DoSavePerformance (); m_bSavePerformance = false; + pScheduler->Yield(); } if (m_bSavePerformanceNewFile) { DoSavePerformanceNewFile (); m_bSavePerformanceNewFile = false; + pScheduler->Yield(); } if (m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy) @@ -400,6 +434,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) { DoSetFirstPerformance(); } + pScheduler->Yield(); } if (m_bSetNewPerformance && !m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy) @@ -409,18 +444,26 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) { m_bSetNewPerformance = false; } + pScheduler->Yield(); } if(m_bDeletePerformance) { DoDeletePerformance (); m_bDeletePerformance = false; + pScheduler->Yield(); } if (m_bProfileEnabled) { m_GetChunkTimer.Dump (); + pScheduler->Yield(); + } + if (m_pNet) { + UpdateNetwork(); } + // Allow other tasks to run + pScheduler->Yield(); } #ifdef ARM_ALLOW_MULTI_CORE @@ -770,6 +813,10 @@ void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG) { m_SerialMIDI.SetChannel (uchChannel, nTG); } + if (m_UDPMIDI) + { + m_UDPMIDI->SetChannel (uchChannel, nTG); + } #ifdef ARM_ALLOW_MULTI_CORE /* This doesn't appear to be used anywhere... @@ -1688,7 +1735,7 @@ void CMiniDexed::setBreathControllerTarget(uint8_t target, uint8_t nTG) assert (m_pTG[nTG]); - m_nBreathControlTarget[nTG]=target; + m_nBreathControlTarget[nTG] = target; m_pTG[nTG]->setBreathControllerTarget(constrain(target, 0, 7)); m_pTG[nTG]->ControllersRefresh(); @@ -1717,7 +1764,7 @@ void CMiniDexed::setAftertouchTarget(uint8_t target, uint8_t nTG) assert (m_pTG[nTG]); - m_nAftertouchTarget[nTG]=target; + m_nAftertouchTarget[nTG] = target; m_pTG[nTG]->setAftertouchTarget(constrain(target, 0, 7)); m_pTG[nTG]->ControllersRefresh(); @@ -1755,7 +1802,6 @@ void CMiniDexed::setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG) assert (m_pTG[nTG]); m_pTG[nTG]->setVoiceDataElement(constrain(data, 0, 155),constrain(number, 0, 99)); - //m_pTG[nTG]->doRefreshVoice(); m_UI.ParameterChanged (); } @@ -2203,3 +2249,201 @@ unsigned CMiniDexed::getModController (unsigned controller, unsigned parameter, } } + +void CMiniDexed::UpdateNetwork() +{ + if (!m_pNet) { + LOGNOTE("CMiniDexed::UpdateNetwork: m_pNet is nullptr, returning early"); + return; + } + + bool bNetIsRunning = m_pNet->IsRunning(); + if (m_pNetDevice->GetType() == NetDeviceTypeEthernet) + bNetIsRunning &= m_pNetDevice->IsLinkUp(); + else if (m_pNetDevice->GetType() == NetDeviceTypeWLAN) + bNetIsRunning &= (m_WPASupplicant && m_WPASupplicant->IsConnected()); + + if (!m_bNetworkInit && bNetIsRunning) + { + LOGNOTE("CMiniDexed::UpdateNetwork: Network became ready, initializing network services"); + m_bNetworkInit = true; + CString IPString; + m_pNet->GetConfig()->GetIPAddress()->Format(&IPString); + + if (m_UDPMIDI) + { + m_UDPMIDI->Initialize(); + } + + m_pFTPDaemon = new CFTPDaemon(FTPUSERNAME, FTPPASSWORD); + + if (!m_pFTPDaemon->Initialize()) + { + LOGERR("Failed to init FTP daemon"); + delete m_pFTPDaemon; + m_pFTPDaemon = nullptr; + } + else + { + LOGNOTE("FTP daemon initialized"); + } + m_UI.DisplayWrite (IPString, "", "TG1", 0, 1); + + m_pmDNSPublisher = new CmDNSPublisher (m_pNet); + assert (m_pmDNSPublisher); + + if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), CmDNSPublisher::ServiceTypeAppleMIDI, + 5004)) + { + LOGPANIC ("Cannot publish mdns service"); + } + + static constexpr const char *ServiceTypeFTP = "_ftp._tcp"; + if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), ServiceTypeFTP, 21)) + { + LOGPANIC ("Cannot publish mdns service"); + } + + if (m_pConfig->GetSyslogEnabled()) + { + CIPAddress ServerIP = m_pConfig->GetNetworkSyslogServerIPAddress(); + if (ServerIP.IsSet () && !ServerIP.IsNull ()) + { + static const u16 usServerPort = 8514; + CString IPString; + ServerIP.Format (&IPString); + LOGNOTE ("Sending log messages to syslog server %s:%u", + (const char *) IPString, (unsigned) usServerPort); + + new CSysLogDaemon (m_pNet, ServerIP, usServerPort); + } + } + m_bNetworkReady = true; + } + + if (m_bNetworkReady && !bNetIsRunning) + { + LOGNOTE("CMiniDexed::UpdateNetwork: Network disconnected"); + m_bNetworkReady = false; + m_pmDNSPublisher->UnpublishService (m_pConfig->GetNetworkHostname()); + LOGNOTE("Network disconnected."); + } + else if (!m_bNetworkReady && bNetIsRunning) + { + LOGNOTE("CMiniDexed::UpdateNetwork: Network connection reestablished"); + m_bNetworkReady = true; + + if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), CmDNSPublisher::ServiceTypeAppleMIDI, + 5004)) + { + LOGPANIC ("Cannot publish mdns service"); + } + + static constexpr const char *ServiceTypeFTP = "_ftp._tcp"; + if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), ServiceTypeFTP, 21)) + { + LOGPANIC ("Cannot publish mdns service"); + } + + m_bNetworkReady = true; + + LOGNOTE("Network connection reestablished."); + + } +} + +bool CMiniDexed::InitNetwork() +{ + LOGNOTE("CMiniDexed::InitNetwork called"); + assert(m_pNet == nullptr); + + TNetDeviceType NetDeviceType = NetDeviceTypeUnknown; + + if (m_pConfig->GetNetworkEnabled()) + { + LOGNOTE("CMiniDexed::InitNetwork: Network is enabled in configuration"); + + LOGNOTE("CMiniDexed::InitNetwork: Network type set in configuration: %s", m_pConfig->GetNetworkType()); + + if (strcmp(m_pConfig->GetNetworkType(), "wlan") == 0) + { + LOGNOTE("CMiniDexed::InitNetwork: Initializing WLAN"); + NetDeviceType = NetDeviceTypeWLAN; + m_WLAN = new CBcm4343Device(WLANFirmwarePath); + if (m_WLAN && m_WLAN->Initialize()) + { + LOGNOTE("CMiniDexed::InitNetwork: WLAN initialized"); + } + else + { + LOGERR("CMiniDexed::InitNetwork: Failed to initialize WLAN, maybe firmware files are missing?"); + delete m_WLAN; m_WLAN = nullptr; + return false; + } + } + else if (strcmp(m_pConfig->GetNetworkType(), "ethernet") == 0) + { + LOGNOTE("CMiniDexed::InitNetwork: Initializing Ethernet"); + NetDeviceType = NetDeviceTypeEthernet; + } + else + { + LOGERR("CMiniDexed::InitNetwork: Network type is not set, please check your minidexed configuration file."); + NetDeviceType = NetDeviceTypeUnknown; + } + + if (NetDeviceType != NetDeviceTypeUnknown) + { + LOGNOTE("CMiniDexed::InitNetwork: Creating CNetSubSystem"); + if (m_pConfig->GetNetworkDHCP()) + m_pNet = new CNetSubSystem(0, 0, 0, 0, m_pConfig->GetNetworkHostname(), NetDeviceType); + else + m_pNet = new CNetSubSystem( + m_pConfig->GetNetworkIPAddress().Get(), + m_pConfig->GetNetworkSubnetMask().Get(), + m_pConfig->GetNetworkDefaultGateway().Get(), + m_pConfig->GetNetworkDNSServer().Get(), + m_pConfig->GetNetworkHostname(), + NetDeviceType + ); + if (!m_pNet || !m_pNet->Initialize(false)) // Check if m_pNet allocation succeeded + { + LOGERR("CMiniDexed::InitNetwork: Failed to initialize network subsystem"); + delete m_pNet; m_pNet = nullptr; // Clean up if failed + delete m_WLAN; m_WLAN = nullptr; // Clean up WLAN if allocated + return false; // Return false as network init failed + } + // WPASupplicant needs to be started after netdevice available + if (NetDeviceType == NetDeviceTypeWLAN) + { + LOGNOTE("CMiniDexed::InitNetwork: Initializing WPASupplicant"); + m_WPASupplicant = new CWPASupplicant(WLANConfigFile); // Allocate m_WPASupplicant + if (!m_WPASupplicant || !m_WPASupplicant->Initialize()) + { + LOGERR("CMiniDexed::InitNetwork: Failed to initialize WPASupplicant, maybe wlan config is missing?"); + delete m_WPASupplicant; m_WPASupplicant = nullptr; // Clean up if failed + // Continue without supplicant? Or return false? Decided to continue for now. + } + } + m_pNetDevice = CNetDevice::GetNetDevice(NetDeviceType); + + // Allocate UDP MIDI device now that network might be up + m_UDPMIDI = new CUDPMIDIDevice(this, m_pConfig, &m_UI); // Allocate m_UDPMIDI + if (!m_UDPMIDI) { + LOGERR("CMiniDexed::InitNetwork: Failed to allocate UDP MIDI device"); + // Clean up other network resources if needed, or handle error appropriately + } else { + // Synchronize UDP MIDI channels with current assignments + for (unsigned nTG = 0; nTG < m_nToneGenerators; ++nTG) + m_UDPMIDI->SetChannel(m_nMIDIChannel[nTG], nTG); + } + } + LOGNOTE("CMiniDexed::InitNetwork: returning %d", m_pNet != nullptr); + return m_pNet != nullptr; + } + else + { + LOGNOTE("CMiniDexed::InitNetwork: Network is not enabled in configuration"); + return false; + } +} diff --git a/src/minidexed.h b/src/minidexed.h index 6a7ef81..b382f7e 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -39,12 +39,19 @@ #include #include #include +#include +#include +#include +#include +#include "net/mdnspublisher.h" #include #include "common.h" #include "effect_mixer.hpp" #include "effect_platervbstereo.h" #include "effect_compressor.h" - +#include "udpmididevice.h" +#include "net/ftpdaemon.h" + class CMiniDexed #ifdef ARM_ALLOW_MULTI_CORE : public CMultiCoreSupport @@ -53,6 +60,7 @@ class CMiniDexed public: CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, FATFS *pFileSystem); + ~CMiniDexed (void); // Add destructor bool Initialize (void); @@ -229,11 +237,15 @@ public: void setMasterVolume (float32_t vol); + bool InitNetwork(); + void UpdateNetwork(); + private: int16_t ApplyNoteLimits (int16_t pitch, unsigned nTG); // returns < 0 to ignore note uint8_t m_uchOPMask[CConfig::AllToneGenerators]; void LoadPerformanceParameters(void); void ProcessSound (void); + const char* GetNetworkDeviceShortName() const; #ifdef ARM_ALLOW_MULTI_CORE enum TCoreStatus @@ -325,6 +337,17 @@ private: CSpinLock m_ReverbSpinLock; + // Network + CNetSubSystem* m_pNet; + CNetDevice* m_pNetDevice; + CBcm4343Device* m_WLAN; // Changed to pointer + CWPASupplicant* m_WPASupplicant; // Changed to pointer + bool m_bNetworkReady; + bool m_bNetworkInit; + CUDPMIDIDevice* m_UDPMIDI; // Changed to pointer + CFTPDaemon* m_pFTPDaemon; + CmDNSPublisher *m_pmDNSPublisher; + bool m_bSavePerformance; bool m_bSavePerformanceNewFile; bool m_bSetNewPerformance; diff --git a/src/minidexed.ini b/src/minidexed.ini index a20b41d..2291f4f 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -151,5 +151,17 @@ EncoderPinData=9 MIDIDumpEnabled=0 ProfileEnabled=0 +# Network +NetworkEnabled=0 +NetworkDHCP=1 +# NetworkType ( wlan ; ethernet ) +NetworkType=wlan +NetworkHostname=MiniDexed +NetworkIPAddress=0 +NetworkSubnetMask=0 +NetworkDefaultGateway=0 +NetworkDNSServer=0 +NetworkSyslogServerIPAddress=0 + # Performance PerformanceSelectToLoad=1 diff --git a/src/net/applemidi.cpp b/src/net/applemidi.cpp new file mode 100644 index 0000000..e14b216 --- /dev/null +++ b/src/net/applemidi.cpp @@ -0,0 +1,874 @@ +// +// applemidi.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#include +#include +#include +#include +#include +#include +#include + +#include "applemidi.h" +#include "byteorder.h" + +// #define APPLEMIDI_DEBUG + +LOGMODULE("applemidi"); + +constexpr u16 ControlPort = 5004; +constexpr u16 MIDIPort = ControlPort + 1; + +constexpr u16 AppleMIDISignature = 0xFFFF; +constexpr u8 AppleMIDIVersion = 2; + +constexpr u8 RTPMIDIPayloadType = 0x61; +constexpr u8 RTPMIDIVersion = 2; + +// Arbitrary value +constexpr size_t MaxNameLength = 256; + +// Timeout period for invitation (5 seconds in 100 microsecond units) +constexpr unsigned int InvitationTimeout = 5 * 10000; + +// Timeout period for sync packets (60 seconds in 100 microsecond units) +constexpr unsigned int SyncTimeout = 60 * 10000; + +// Receiver feedback packet frequency (1 second in 100 microsecond units) +constexpr unsigned int ReceiverFeedbackPeriod = 1 * 10000; + +constexpr u16 CommandWord(const char Command[2]) { return Command[0] << 8 | Command[1]; } + +enum TAppleMIDICommand : u16 +{ + Invitation = CommandWord("IN"), + InvitationAccepted = CommandWord("OK"), + InvitationRejected = CommandWord("NO"), + Sync = CommandWord("CK"), + ReceiverFeedback = CommandWord("RS"), + EndSession = CommandWord("BY"), +}; + +struct TAppleMIDISession +{ + u16 nSignature; + u16 nCommand; + u32 nVersion; + u32 nInitiatorToken; + u32 nSSRC; + char Name[MaxNameLength]; +} +PACKED; + +// The Name field is optional +constexpr size_t NamelessSessionPacketSize = sizeof(TAppleMIDISession) - sizeof(TAppleMIDISession::Name); + +struct TAppleMIDISync +{ + u16 nSignature; + u16 nCommand; + u32 nSSRC; + u8 nCount; + u8 Padding[3]; + u64 Timestamps[3]; +} +PACKED; + +struct TAppleMIDIReceiverFeedback +{ + u16 nSignature; + u16 nCommand; + u32 nSSRC; + u32 nSequence; +} +PACKED; + +struct TRTPMIDI +{ + u16 nFlags; + u16 nSequence; + u32 nTimestamp; + u32 nSSRC; +} +PACKED; + +u64 GetSyncClock() +{ + static const u64 nStartTime = CTimer::GetClockTicks(); + const u64 nMicrosSinceEpoch = CTimer::GetClockTicks(); + + // Units of 100 microseconds + return (nMicrosSinceEpoch - nStartTime ) / 100; +} + +bool ParseInvitationPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) +{ + const TAppleMIDISession* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < NamelessSessionPacketSize) + return false; + + const u16 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u16 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != Invitation) + return false; + + const u32 nVersion = ntohl(pInPacket->nVersion); + if (nVersion != AppleMIDIVersion) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nVersion = nVersion; + pOutPacket->nInitiatorToken = ntohl(pInPacket->nInitiatorToken); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + if (nSize > NamelessSessionPacketSize) + strncpy(pOutPacket->Name, pInPacket->Name, sizeof(pOutPacket->Name)); + else + strncpy(pOutPacket->Name, "", sizeof(pOutPacket->Name)); + + return true; +} + +bool ParseEndSessionPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) +{ + const TAppleMIDISession* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < NamelessSessionPacketSize) + return false; + + const u16 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u16 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != EndSession) + return false; + + const u32 nVersion = ntohl(pInPacket->nVersion); + if (nVersion != AppleMIDIVersion) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nVersion = nVersion; + pOutPacket->nInitiatorToken = ntohl(pInPacket->nInitiatorToken); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + return true; +} + +bool ParseSyncPacket(const u8* pBuffer, size_t nSize, TAppleMIDISync* pOutPacket) +{ + const TAppleMIDISync* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < sizeof(TAppleMIDISync)) + return false; + + const u32 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u32 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != Sync) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + pOutPacket->nCount = pInPacket->nCount; + pOutPacket->Timestamps[0] = ntohll(pInPacket->Timestamps[0]); + pOutPacket->Timestamps[1] = ntohll(pInPacket->Timestamps[1]); + pOutPacket->Timestamps[2] = ntohll(pInPacket->Timestamps[2]); + + return true; +} + +u8 ParseMIDIDeltaTime(const u8* pBuffer) +{ + u8 nLength = 0; + u32 nDeltaTime = 0; + + while (nLength < 4) + { + nDeltaTime <<= 7; + nDeltaTime |= pBuffer[nLength] & 0x7F; + + // Upper bit not set; end of timestamp + if ((pBuffer[nLength++] & (1 << 7)) == 0) + break; + } + + return nLength; +} + +size_t ParseSysExCommand(const u8* pBuffer, size_t nSize, CAppleMIDIHandler* pHandler) +{ + size_t nBytesParsed = 1; + const u8 nHead = pBuffer[0]; + u8 nTail = 0; + + while (nBytesParsed < nSize && !(nTail == 0xF0 || nTail == 0xF7 || nTail == 0xF4)) + nTail = pBuffer[nBytesParsed++]; + + size_t nReceiveLength = nBytesParsed; + + // First segmented SysEx packet + if (nHead == 0xF0 && nTail == 0xF0) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (first)"); +#endif + --nReceiveLength; + } + + // Middle segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF0) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (middle)"); +#endif + ++pBuffer; + nBytesParsed -= 2; + } + + // Last segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF7) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (last)"); +#endif + ++pBuffer; + --nReceiveLength; + } + + // Cancelled segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF4) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received cancelled SysEx"); +#endif + nReceiveLength = 1; + } + +#ifdef APPLEMIDI_DEBUG + else + { + LOGNOTE("Received complete SysEx"); + } +#endif + + pHandler->OnAppleMIDIDataReceived(pBuffer, nReceiveLength); + + return nBytesParsed; +} + +size_t ParseMIDICommand(const u8* pBuffer, size_t nSize, u8& nRunningStatus, CAppleMIDIHandler* pHandler) +{ + size_t nBytesParsed = 0; + u8 nByte = pBuffer[0]; + + // System Real-Time message - single byte, handle immediately + // Can appear anywhere in the stream, even in between status/data bytes + if (nByte >= 0xF8) + { + // Ignore undefined System Real-Time + if (nByte != 0xF9 && nByte != 0xFD) + pHandler->OnAppleMIDIDataReceived(&nByte, 1); + + return 1; + } + + // Is it a status byte? + if (nByte & 0x80) + { + // Update running status if non Real-Time System status + if (nByte < 0xF0) + nRunningStatus = nByte; + else + nRunningStatus = 0; + + ++nBytesParsed; + } + else + { + // First byte not a status byte and no running status - invalid + if (!nRunningStatus) + return 0; + + // Use running status + nByte = nRunningStatus; + } + + // Channel messages + if (nByte < 0xF0) + { + // How many data bytes? + switch (nByte & 0xF0) + { + case 0x80: // Note off + case 0x90: // Note on + case 0xA0: // Polyphonic key pressure/aftertouch + case 0xB0: // Control change + case 0xE0: // Pitch bend + nBytesParsed += 2; + break; + + case 0xC0: // Program change + case 0xD0: // Channel pressure/aftertouch + nBytesParsed += 1; + break; + } + + // Handle command + pHandler->OnAppleMIDIDataReceived(pBuffer, nBytesParsed); + return nBytesParsed; + } + + // System common commands + switch (nByte) + { + case 0xF0: // Start of System Exclusive + case 0xF7: // End of Exclusive + return ParseSysExCommand(pBuffer, nSize, pHandler); + + case 0xF1: // MIDI Time Code Quarter Frame + case 0xF3: // Song Select + ++nBytesParsed; + break; + + case 0xF2: // Song Position Pointer + nBytesParsed += 2; + break; + } + + pHandler->OnAppleMIDIDataReceived(pBuffer, nBytesParsed); + return nBytesParsed; +} + +bool ParseMIDICommandSection(const u8* pBuffer, size_t nSize, CAppleMIDIHandler* pHandler) +{ + // Must have at least a header byte and a single status byte + if (nSize < 2) + return false; + + size_t nMIDICommandsProcessed = 0; + size_t nBytesRemaining = nSize - 1; + u8 nRunningStatus = 0; + + const u8 nMIDIHeader = pBuffer[0]; + const u8* pMIDICommands = pBuffer + 1; + + // Lower 4 bits of the header is length + u16 nMIDICommandLength = nMIDIHeader & 0x0F; + + // If B flag is set, length value is 12 bits + if (nMIDIHeader & (1 << 7)) + { + nMIDICommandLength <<= 8; + nMIDICommandLength |= pMIDICommands[0]; + ++pMIDICommands; + --nBytesRemaining; + } + + if (nMIDICommandLength > nBytesRemaining) + { + LOGERR("Invalid MIDI command length"); + return false; + } + + // Begin decoding the command list + while (nMIDICommandLength) + { + // If Z flag is set, first list entry is a delta time + if (nMIDICommandsProcessed || nMIDIHeader & (1 << 5)) + { + const u8 nBytesParsed = ParseMIDIDeltaTime(pMIDICommands); + nMIDICommandLength -= nBytesParsed; + pMIDICommands += nBytesParsed; + } + + if (nMIDICommandLength) + { + const size_t nBytesParsed = ParseMIDICommand(pMIDICommands, nMIDICommandLength, nRunningStatus, pHandler); + nMIDICommandLength -= nBytesParsed; + pMIDICommands += nBytesParsed; + ++nMIDICommandsProcessed; + } + } + + return true; +} + +bool ParseMIDIPacket(const u8* pBuffer, size_t nSize, TRTPMIDI* pOutPacket, CAppleMIDIHandler* pHandler) +{ + assert(pHandler != nullptr); + + const TRTPMIDI* const pInPacket = reinterpret_cast(pBuffer); + const u16 nRTPFlags = ntohs(pInPacket->nFlags); + + // Check size (RTP-MIDI header plus MIDI command section header) + if (nSize < sizeof(TRTPMIDI) + 1) + return false; + + // Check version + if (((nRTPFlags >> 14) & 0x03) != RTPMIDIVersion) + return false; + + // Ensure no CSRC identifiers + if (((nRTPFlags >> 8) & 0x0F) != 0) + return false; + + // Check payload type + if ((nRTPFlags & 0xFF) != RTPMIDIPayloadType) + return false; + + pOutPacket->nFlags = nRTPFlags; + pOutPacket->nSequence = ntohs(pInPacket->nSequence); + pOutPacket->nTimestamp = ntohl(pInPacket->nTimestamp); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + // RTP-MIDI variable-length header + const u8* const pMIDICommandSection = pBuffer + sizeof(TRTPMIDI); + size_t nRemaining = nSize - sizeof(TRTPMIDI); + return ParseMIDICommandSection(pMIDICommandSection, nRemaining, pHandler); +} + +CAppleMIDIParticipant::CAppleMIDIParticipant(CBcmRandomNumberGenerator* pRandom, CAppleMIDIHandler* pHandler) + : CTask(TASK_STACK_SIZE, true), + + m_pRandom(pRandom), + + m_pControlSocket(nullptr), + m_pMIDISocket(nullptr), + + m_nForeignControlPort(0), + m_nForeignMIDIPort(0), + m_nInitiatorControlPort(0), + m_nInitiatorMIDIPort(0), + m_ControlBuffer{0}, + m_MIDIBuffer{0}, + + m_nControlResult(0), + m_nMIDIResult(0), + + m_pHandler(pHandler), + + m_State(TState::ControlInvitation), + + m_nInitiatorToken(0), + m_nInitiatorSSRC(0), + m_nSSRC(0), + m_nLastMIDISequenceNumber(0), + + m_nOffsetEstimate(0), + m_nLastSyncTime(0), + + m_nSequence(0), + m_nLastFeedbackSequence(0), + m_nLastFeedbackTime(0) +{ +} + +CAppleMIDIParticipant::~CAppleMIDIParticipant() +{ + if (m_pControlSocket) + delete m_pControlSocket; + + if (m_pMIDISocket) + delete m_pMIDISocket; +} + +bool CAppleMIDIParticipant::Initialize() +{ + assert(m_pControlSocket == nullptr); + assert(m_pMIDISocket == nullptr); + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pControlSocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if ((m_pMIDISocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if (m_pControlSocket->Bind(ControlPort) != 0) + { + LOGERR("Couldn't bind to port %d", ControlPort); + return false; + } + + if (m_pMIDISocket->Bind(MIDIPort) != 0) + { + LOGERR("Couldn't bind to port %d", MIDIPort); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CAppleMIDIParticipant::Run() +{ + assert(m_pControlSocket != nullptr); + assert(m_pMIDISocket != nullptr); + + CScheduler* const pScheduler = CScheduler::Get(); + + while (true) + { + if ((m_nControlResult = m_pControlSocket->ReceiveFrom(m_ControlBuffer, sizeof(m_ControlBuffer), MSG_DONTWAIT, &m_ForeignControlIPAddress, &m_nForeignControlPort)) < 0) + LOGERR("Control socket receive error: %d", m_nControlResult); + + if ((m_nMIDIResult = m_pMIDISocket->ReceiveFrom(m_MIDIBuffer, sizeof(m_MIDIBuffer), MSG_DONTWAIT, &m_ForeignMIDIIPAddress, &m_nForeignMIDIPort)) < 0) + LOGERR("MIDI socket receive error: %d", m_nMIDIResult); + + switch (m_State) + { + case TState::ControlInvitation: + ControlInvitationState(); + break; + + case TState::MIDIInvitation: + MIDIInvitationState(); + break; + + case TState::Connected: + ConnectedState(); + break; + } + + // Allow other tasks to run + pScheduler->Yield(); + } +} + +void CAppleMIDIParticipant::ControlInvitationState() +{ + TAppleMIDISession SessionPacket; + + if (m_nControlResult == 0) + return; + + if (!ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + LOGERR("Unexpected packet"); + return; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- Control invitation"); +#endif + + // Store initiator details + m_InitiatorIPAddress.Set(m_ForeignControlIPAddress); + m_nInitiatorControlPort = m_nForeignControlPort; + m_nInitiatorToken = SessionPacket.nInitiatorToken; + m_nInitiatorSSRC = SessionPacket.nSSRC; + + // Generate random SSRC and accept + m_nSSRC = m_pRandom->GetNumber(); + if (!SendAcceptInvitationPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort)) + { + LOGERR("Couldn't accept control invitation"); + return; + } + + m_nLastSyncTime = GetSyncClock(); + m_State = TState::MIDIInvitation; +} + +void CAppleMIDIParticipant::MIDIInvitationState() +{ + TAppleMIDISession SessionPacket; + + if (m_nControlResult > 0) + { + if (ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + // Unexpected peer; reject invitation + if (m_ForeignControlIPAddress != m_InitiatorIPAddress || m_nForeignControlPort != m_nInitiatorControlPort) + SendRejectInvitationPacket(m_pControlSocket, &m_ForeignControlIPAddress, m_nForeignControlPort, SessionPacket.nInitiatorToken); + else + LOGERR("Unexpected packet"); + } + } + + if (m_nMIDIResult > 0) + { + if (!ParseInvitationPacket(m_MIDIBuffer, m_nMIDIResult, &SessionPacket)) + { + LOGERR("Unexpected packet"); + return; + } + + // Unexpected peer; reject invitation + if (m_ForeignMIDIIPAddress != m_InitiatorIPAddress) + { + SendRejectInvitationPacket(m_pMIDISocket, &m_ForeignMIDIIPAddress, m_nForeignMIDIPort, SessionPacket.nInitiatorToken); + return; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- MIDI invitation"); +#endif + + m_nInitiatorMIDIPort = m_nForeignMIDIPort; + + if (SendAcceptInvitationPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort)) + { + CString IPAddressString; + m_InitiatorIPAddress.Format(&IPAddressString); + LOGNOTE("Connection to %s (%s) established", SessionPacket.Name, static_cast(IPAddressString)); + m_nLastSyncTime = GetSyncClock(); + m_State = TState::Connected; + m_pHandler->OnAppleMIDIConnect(&m_InitiatorIPAddress, SessionPacket.Name); + } + else + { + LOGERR("Couldn't accept MIDI invitation"); + Reset(); + } + } + + // Timeout + else if ((GetSyncClock() - m_nLastSyncTime) > InvitationTimeout) + { + LOGERR("MIDI port invitation timed out"); + Reset(); + } +} + +void CAppleMIDIParticipant::ConnectedState() +{ + TAppleMIDISession SessionPacket; + TRTPMIDI MIDIPacket; + TAppleMIDISync SyncPacket; + + if (m_nControlResult > 0) + { + if (ParseEndSessionPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- End session"); +#endif + + if (m_ForeignControlIPAddress == m_InitiatorIPAddress && + m_nForeignControlPort == m_nInitiatorControlPort && + SessionPacket.nSSRC == m_nInitiatorSSRC) + { + LOGNOTE("Initiator ended session"); + m_pHandler->OnAppleMIDIDisconnect(&m_InitiatorIPAddress, SessionPacket.Name); + Reset(); + return; + } + } + else if (ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + // Unexpected peer; reject invitation + if (m_ForeignControlIPAddress != m_InitiatorIPAddress || m_nForeignControlPort != m_nInitiatorControlPort) + SendRejectInvitationPacket(m_pControlSocket, &m_ForeignControlIPAddress, m_nForeignControlPort, SessionPacket.nInitiatorToken); + else + LOGERR("Unexpected packet"); + } + } + + if (m_nMIDIResult > 0) + { + if (m_ForeignMIDIIPAddress != m_InitiatorIPAddress || m_nForeignMIDIPort != m_nInitiatorMIDIPort) + LOGERR("Unexpected packet"); + else if (ParseMIDIPacket(m_MIDIBuffer, m_nMIDIResult, &MIDIPacket, m_pHandler)) + m_nSequence = MIDIPacket.nSequence; + else if (ParseSyncPacket(m_MIDIBuffer, m_nMIDIResult, &SyncPacket)) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- Sync %d", SyncPacket.nCount); +#endif + + if (SyncPacket.nSSRC == m_nInitiatorSSRC && (SyncPacket.nCount == 0 || SyncPacket.nCount == 2)) + { + if (SyncPacket.nCount == 0) + SendSyncPacket(SyncPacket.Timestamps[0], GetSyncClock()); + else if (SyncPacket.nCount == 2) + { + m_nOffsetEstimate = ((SyncPacket.Timestamps[2] + SyncPacket.Timestamps[0]) / 2) - SyncPacket.Timestamps[1]; +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Offset estimate: %llu", m_nOffsetEstimate); +#endif + } + + m_nLastSyncTime = GetSyncClock(); + } + else + { + LOGERR("Unexpected sync packet"); + } + } + } + + const u64 nTicks = GetSyncClock(); + + if ((nTicks - m_nLastFeedbackTime) > ReceiverFeedbackPeriod) + { + if (m_nSequence != m_nLastFeedbackSequence) + { + SendFeedbackPacket(); + m_nLastFeedbackSequence = m_nSequence; + } + m_nLastFeedbackTime = nTicks; + } + + if ((nTicks - m_nLastSyncTime) > SyncTimeout) + { + LOGERR("Initiator timed out"); + Reset(); + } +} + +void CAppleMIDIParticipant::Reset() +{ + m_State = TState::ControlInvitation; + + m_nInitiatorToken = 0; + m_nInitiatorSSRC = 0; + m_nSSRC = 0; + m_nLastMIDISequenceNumber = 0; + + m_nOffsetEstimate = 0; + m_nLastSyncTime = 0; + + m_nSequence = 0; + m_nLastFeedbackSequence = 0; + m_nLastFeedbackTime = 0; +} + +bool CAppleMIDIParticipant::SendPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, const void* pData, size_t nSize) +{ + const int nResult = pSocket->SendTo(pData, nSize, MSG_DONTWAIT, *pIPAddress, nPort); + + if (nResult < 0) + { + LOGERR("Send failure, error code: %d", nResult); + return false; + } + + if (static_cast(nResult) != nSize) + { + LOGERR("Send failure, only %d/%d bytes sent", nResult, nSize); + return false; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Sent %d bytes to port %d", nResult, nPort); +#endif + + return true; +} + +bool CAppleMIDIParticipant::SendAcceptInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort) +{ + TAppleMIDISession AcceptPacket = + { + htons(AppleMIDISignature), + htons(InvitationAccepted), + htonl(AppleMIDIVersion), + htonl(m_nInitiatorToken), + htonl(m_nSSRC), + {'\0'} + }; + + // TODO: configurable name + strncpy(AcceptPacket.Name, "MiniDexed", sizeof(AcceptPacket.Name)); + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Accept invitation"); +#endif + + const size_t nSendSize = NamelessSessionPacketSize + strlen(AcceptPacket.Name) + 1; + return SendPacket(pSocket, pIPAddress, nPort, &AcceptPacket, nSendSize); +} + +bool CAppleMIDIParticipant::SendRejectInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, u32 nInitiatorToken) +{ + TAppleMIDISession RejectPacket = + { + htons(AppleMIDISignature), + htons(InvitationRejected), + htonl(AppleMIDIVersion), + htonl(nInitiatorToken), + htonl(m_nSSRC), + {'\0'} + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Reject invitation"); +#endif + + // Send without name + return SendPacket(pSocket, pIPAddress, nPort, &RejectPacket, NamelessSessionPacketSize); +} + +bool CAppleMIDIParticipant::SendSyncPacket(u64 nTimestamp1, u64 nTimestamp2) +{ + const TAppleMIDISync SyncPacket = + { + htons(AppleMIDISignature), + htons(Sync), + htonl(m_nSSRC), + 1, + {0}, + { + htonll(nTimestamp1), + htonll(nTimestamp2), + 0 + } + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Sync 1"); +#endif + + return SendPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort, &SyncPacket, sizeof(SyncPacket)); +} + +bool CAppleMIDIParticipant::SendFeedbackPacket() +{ + const TAppleMIDIReceiverFeedback FeedbackPacket = + { + htons(AppleMIDISignature), + htons(ReceiverFeedback), + htonl(m_nSSRC), + htonl(m_nSequence << 16) + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Feedback"); +#endif + + return SendPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort, &FeedbackPacket, sizeof(FeedbackPacket)); +} \ No newline at end of file diff --git a/src/net/applemidi.h b/src/net/applemidi.h new file mode 100644 index 0000000..3df68ae --- /dev/null +++ b/src/net/applemidi.h @@ -0,0 +1,111 @@ +// +// applemidi.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _applemidi_h +#define _applemidi_h + +#include +#include +#include +#include + +class CAppleMIDIHandler +{ +public: + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) = 0; + virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) = 0; + virtual void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) = 0; +}; + +class CAppleMIDIParticipant : protected CTask +{ +public: + CAppleMIDIParticipant(CBcmRandomNumberGenerator* pRandom, CAppleMIDIHandler* pHandler); + virtual ~CAppleMIDIParticipant() override; + + bool Initialize(); + + virtual void Run() override; + +private: + void ControlInvitationState(); + void MIDIInvitationState(); + void ConnectedState(); + void Reset(); + + bool SendPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, const void* pData, size_t nSize); + bool SendAcceptInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort); + bool SendRejectInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, u32 nInitiatorToken); + bool SendSyncPacket(u64 nTimestamp1, u64 nTimestamp2); + bool SendFeedbackPacket(); + + CBcmRandomNumberGenerator* m_pRandom; + + // UDP sockets + CSocket* m_pControlSocket; + CSocket* m_pMIDISocket; + + // Foreign peers + CIPAddress m_ForeignControlIPAddress; + CIPAddress m_ForeignMIDIIPAddress; + u16 m_nForeignControlPort; + u16 m_nForeignMIDIPort; + + // Connected peer + CIPAddress m_InitiatorIPAddress; + u16 m_nInitiatorControlPort; + u16 m_nInitiatorMIDIPort; + + // Socket receive buffers + u8 m_ControlBuffer[FRAME_BUFFER_SIZE]; + u8 m_MIDIBuffer[FRAME_BUFFER_SIZE]; + + int m_nControlResult; + int m_nMIDIResult; + + // Callback handler + CAppleMIDIHandler* m_pHandler; + + // Participant state machine + enum class TState + { + ControlInvitation, + MIDIInvitation, + Connected + }; + + TState m_State; + + u32 m_nInitiatorToken = 0; + u32 m_nInitiatorSSRC = 0; + u32 m_nSSRC = 0; + u32 m_nLastMIDISequenceNumber = 0; + + u64 m_nOffsetEstimate = 0; + u64 m_nLastSyncTime = 0; + + u16 m_nSequence = 0; + u16 m_nLastFeedbackSequence = 0; + u64 m_nLastFeedbackTime = 0; +}; + +#endif \ No newline at end of file diff --git a/src/net/byteorder.h b/src/net/byteorder.h new file mode 100644 index 0000000..5160119 --- /dev/null +++ b/src/net/byteorder.h @@ -0,0 +1,42 @@ +// +// byteorder.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _byteorder_h +#define _byteorder_h + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define htons(VALUE) (VALUE) +#define htonl(VALUE) (VALUE) +#define htonll(VALUE) (VALUE) +#define ntohs(VALUE) (VALUE) +#define ntohl(VALUE) (VALUE) +#define ntohll(VALUE) (VALUE) +#else +#define htons(VALUE) __builtin_bswap16(VALUE) +#define htonl(VALUE) __builtin_bswap32(VALUE) +#define htonll(VALUE) __builtin_bswap64(VALUE) +#define ntohs(VALUE) __builtin_bswap16(VALUE) +#define ntohl(VALUE) __builtin_bswap32(VALUE) +#define ntohll(VALUE) __builtin_bswap64(VALUE) +#endif + +#endif \ No newline at end of file diff --git a/src/net/ftpdaemon.cpp b/src/net/ftpdaemon.cpp new file mode 100644 index 0000000..0cab51c --- /dev/null +++ b/src/net/ftpdaemon.cpp @@ -0,0 +1,111 @@ +// +// ftpdaemon.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#include +#include +#include +#include +#include + +#include "ftpdaemon.h" +#include "ftpworker.h" + +LOGMODULE("ftpd"); + +constexpr u16 ListenPort = 21; +constexpr u8 MaxConnections = 1; + +CFTPDaemon::CFTPDaemon(const char* pUser, const char* pPassword) + : CTask(TASK_STACK_SIZE, true), + m_pListenSocket(nullptr), + m_pUser(pUser), + m_pPassword(pPassword) +{ +} + +CFTPDaemon::~CFTPDaemon() +{ + if (m_pListenSocket) + delete m_pListenSocket; +} + +bool CFTPDaemon::Initialize() +{ + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pListenSocket = new CSocket(pNet, IPPROTO_TCP)) == nullptr) + return false; + + if (m_pListenSocket->Bind(ListenPort) != 0) + { + LOGERR("Couldn't bind to port %d", ListenPort); + return false; + } + + if (m_pListenSocket->Listen() != 0) + { + LOGERR("Failed to listen on control socket"); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CFTPDaemon::Run() +{ + assert(m_pListenSocket != nullptr); + + LOGNOTE("Listener task spawned"); + + while (true) + { + CIPAddress ClientIPAddress; + u16 nClientPort; + + LOGDBG("Listener: waiting for connection"); + CSocket* pConnection = m_pListenSocket->Accept(&ClientIPAddress, &nClientPort); + + if (pConnection == nullptr) + { + LOGERR("Unable to accept connection"); + continue; + } + + CString IPAddressString; + ClientIPAddress.Format(&IPAddressString); + LOGNOTE("Incoming connection from %s:%d", static_cast(IPAddressString), nClientPort); + + if (CFTPWorker::GetInstanceCount() >= MaxConnections) + { + pConnection->Send("421 Maximum number of connections reached.\r\n", 45, 0); + delete pConnection; + LOGWARN("Maximum number of connections reached"); + continue; + } + + // Spawn new worker + new CFTPWorker(pConnection, m_pUser, m_pPassword); + } +} \ No newline at end of file diff --git a/src/net/ftpdaemon.h b/src/net/ftpdaemon.h new file mode 100644 index 0000000..4d75762 --- /dev/null +++ b/src/net/ftpdaemon.h @@ -0,0 +1,47 @@ +// +// ftpdaemon.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _ftpdaemon_h +#define _ftpdaemon_h + +#include +#include + +class CFTPDaemon : protected CTask +{ +public: + CFTPDaemon(const char* pUser, const char* pPassword); + virtual ~CFTPDaemon() override; + + bool Initialize(); + + virtual void Run() override; + +private: + // TCP sockets + CSocket* m_pListenSocket; + + const char* m_pUser; + const char* m_pPassword; +}; + +#endif \ No newline at end of file diff --git a/src/net/ftpworker.cpp b/src/net/ftpworker.cpp new file mode 100644 index 0000000..6f19f8a --- /dev/null +++ b/src/net/ftpworker.cpp @@ -0,0 +1,1218 @@ +// +// ftpworker.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +//#define FTPDAEMON_DEBUG + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ftpworker.h" +#include "utility.h" + +// Use a per-instance name for the log macros +#define From m_LogName + +constexpr u16 PassivePortBase = 9000; +constexpr size_t TextBufferSize = 512; +constexpr unsigned int SocketTimeout = 20; +constexpr unsigned int NumRetries = 3; + +#ifndef MT32_PI_VERSION +#define MT32_PI_VERSION "(version unknown)" +#endif + +const char MOTDBanner[] = "Welcome to the MiniDexed " MT32_PI_VERSION " embedded FTP server!"; +const char* exclude_filename = "SD:/wpa_supplicant.conf"; + +enum class TDirectoryListEntryType +{ + File, + Directory, +}; + +struct TDirectoryListEntry +{ + char Name[FF_LFN_BUF + 1]; + TDirectoryListEntryType Type; + u32 nSize; + u16 nLastModifedDate; + u16 nLastModifedTime; +}; + +using TCommandHandler = bool (CFTPWorker::*)(const char* pArgs); + +struct TFTPCommand +{ + const char* pCmdStr; + TCommandHandler pHandler; +}; + +const TFTPCommand CFTPWorker::Commands[] = +{ + { "SYST", &CFTPWorker::System }, + { "USER", &CFTPWorker::Username }, + { "PASS", &CFTPWorker::Password }, + { "TYPE", &CFTPWorker::Type }, + { "PASV", &CFTPWorker::Passive }, + { "PORT", &CFTPWorker::Port }, + { "RETR", &CFTPWorker::Retrieve }, + { "STOR", &CFTPWorker::Store }, + { "DELE", &CFTPWorker::Delete }, + { "RMD", &CFTPWorker::Delete }, + { "MKD", &CFTPWorker::MakeDirectory }, + { "CWD", &CFTPWorker::ChangeWorkingDirectory }, + { "CDUP", &CFTPWorker::ChangeToParentDirectory }, + { "PWD", &CFTPWorker::PrintWorkingDirectory }, + { "LIST", &CFTPWorker::List }, + { "NLST", &CFTPWorker::ListFileNames }, + { "RNFR", &CFTPWorker::RenameFrom }, + { "RNTO", &CFTPWorker::RenameTo }, + { "BYE", &CFTPWorker::Bye }, + { "QUIT", &CFTPWorker::Bye }, + { "NOOP", &CFTPWorker::NoOp }, +}; + +u8 CFTPWorker::s_nInstanceCount = 0; + +// Volume names from ffconf.h +// TODO: Share with soundfontmanager.cpp +const char* const VolumeNames[] = { FF_VOLUME_STRS }; + +bool ValidateVolumeName(const char* pVolumeName) +{ + for (const auto pName : VolumeNames) + { + if (strcasecmp(pName, pVolumeName) == 0) + return true; + } + + return false; +} + +// Comparator for sorting directory listings +inline bool DirectoryCaseInsensitiveAscending(const TDirectoryListEntry& EntryA, const TDirectoryListEntry& EntryB) +{ + // Directories first in ascending order + if (EntryA.Type != EntryB.Type) + return EntryA.Type == TDirectoryListEntryType::Directory; + + return strncasecmp(EntryA.Name, EntryB.Name, sizeof(TDirectoryListEntry::Name)) < 0; +} + + +CFTPWorker::CFTPWorker(CSocket* pControlSocket, const char* pExpectedUser, const char* pExpectedPassword) + : CTask(TASK_STACK_SIZE), + m_LogName(), + m_pExpectedUser(pExpectedUser), + m_pExpectedPassword(pExpectedPassword), + m_pControlSocket(pControlSocket), + m_pDataSocket(nullptr), + m_nDataSocketPort(0), + m_DataSocketIPAddress(), + m_CommandBuffer{'\0'}, + m_DataBuffer{0}, + m_User(), + m_Password(), + m_DataType(TDataType::ASCII), + m_TransferMode(TTransferMode::Active), + m_CurrentPath(), + m_RenameFrom() +{ + ++s_nInstanceCount; + m_LogName.Format("ftpd[%d]", s_nInstanceCount); +} + +CFTPWorker::~CFTPWorker() +{ + if (m_pControlSocket) + delete m_pControlSocket; + + if (m_pDataSocket) + delete m_pDataSocket; + + --s_nInstanceCount; + + LOGNOTE("Instance count is now %d", s_nInstanceCount); +} + +void CFTPWorker::Run() +{ + assert(m_pControlSocket != nullptr); + + const size_t nWorkerNumber = s_nInstanceCount; + CScheduler* const pScheduler = CScheduler::Get(); + + LOGNOTE("Worker task %d spawned", nWorkerNumber); + + if (!SendStatus(TFTPStatus::ReadyForNewUser, MOTDBanner)) + return; + + CTimer* const pTimer = CTimer::Get(); + unsigned int nTimeout = pTimer->GetTicks(); + + while (m_pControlSocket) + { + // Block while waiting to receive +#ifdef FTPDAEMON_DEBUG + LOGDBG("Waiting for command"); +#endif + const int nReceiveBytes = m_pControlSocket->Receive(m_CommandBuffer, sizeof(m_CommandBuffer), MSG_DONTWAIT); + + if (nReceiveBytes == 0) + { + if (pTimer->GetTicks() - nTimeout >= SocketTimeout * HZ) + { + LOGERR("Socket timed out"); + break; + } + + pScheduler->Yield(); + continue; + } + + if (nReceiveBytes < 0) + { + LOGNOTE("Connection closed"); + break; + } + + // FIXME + m_CommandBuffer[nReceiveBytes - 2] = '\0'; + +#ifdef FTPDAEMON_DEBUG + const u8* pIPAddress = m_pControlSocket->GetForeignIP(); + LOGDBG("<-- Received %d bytes from %d.%d.%d.%d: '%s'", nReceiveBytes, pIPAddress[0], pIPAddress[1], pIPAddress[2], pIPAddress[3], m_CommandBuffer); +#endif + + char* pSavePtr; + char* pToken = strtok_r(m_CommandBuffer, " \r\n", &pSavePtr); + + if (!pToken) + { + LOGERR("String tokenization error (received: '%s')", m_CommandBuffer); + continue; + } + + TCommandHandler pHandler = nullptr; + for (size_t i = 0; i < Utility::ArraySize(Commands); ++i) + { + if (strcasecmp(pToken, Commands[i].pCmdStr) == 0) + { + pHandler = Commands[i].pHandler; + break; + } + } + + if (pHandler) + (this->*pHandler)(pSavePtr); + else + SendStatus(TFTPStatus::CommandNotImplemented, "Command not implemented."); + + nTimeout = pTimer->GetTicks(); + } + + LOGNOTE("Worker task %d shutting down", nWorkerNumber); + + delete m_pControlSocket; + m_pControlSocket = nullptr; +} + +CSocket* CFTPWorker::OpenDataConnection() +{ + CSocket* pDataSocket = nullptr; + u8 nRetries = NumRetries; + + while (pDataSocket == nullptr && nRetries > 0) + { + // Active: Create new socket and connect to client + if (m_TransferMode == TTransferMode::Active) + { + CNetSubSystem* const pNet = CNetSubSystem::Get(); + pDataSocket = new CSocket(pNet, IPPROTO_TCP); + + if (pDataSocket == nullptr) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not open socket."); + return nullptr; + } + + if (pDataSocket->Connect(m_DataSocketIPAddress, m_nDataSocketPort) < 0) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not connect to data port."); + delete pDataSocket; + pDataSocket = nullptr; + } + } + + // Passive: Use previously-created socket and accept connection from client + else if (m_TransferMode == TTransferMode::Passive && m_pDataSocket != nullptr) + { + CIPAddress ClientIPAddress; + u16 nClientPort; + pDataSocket = m_pDataSocket->Accept(&ClientIPAddress, &nClientPort); + } + + --nRetries; + } + + if (pDataSocket == nullptr) + { + LOGERR("Unable to open data socket after %d attempts", NumRetries); + SendStatus(TFTPStatus::DataConnectionFailed, "Couldn't open data connection."); + } + + return pDataSocket; +} + +bool CFTPWorker::SendStatus(TFTPStatus StatusCode, const char* pMessage) +{ + assert(m_pControlSocket != nullptr); + + const int nLength = snprintf(m_CommandBuffer, sizeof(m_CommandBuffer), "%d %s\r\n", StatusCode, pMessage); + if (m_pControlSocket->Send(m_CommandBuffer, nLength, 0) < 0) + { + LOGERR("Failed to send status"); + return false; + } +#ifdef FTPDAEMON_DEBUG + else + { + m_CommandBuffer[nLength - 2] = '\0'; + LOGDBG("--> Sent: '%s'", m_CommandBuffer); + } +#endif + + return true; +} + +bool CFTPWorker::CheckLoggedIn() +{ +#ifdef FTPDAEMON_DEBUG + LOGDBG("Username compare: expected '%s', actual '%s'", static_cast(m_pExpectedUser), static_cast(m_User)); + LOGDBG("Password compare: expected '%s', actual '%s'", static_cast(m_pExpectedPassword), static_cast(m_Password)); +#endif + + if (m_User.Compare(m_pExpectedUser) == 0 && m_Password.Compare(m_pExpectedPassword) == 0) + return true; + + SendStatus(TFTPStatus::NotLoggedIn, "Not logged in."); + return false; +} + +CString CFTPWorker::RealPath(const char* pInBuffer) const +{ + assert(pInBuffer != nullptr); + + CString Path; + const bool bAbsolute = pInBuffer[0] == '/'; + + if (bAbsolute) + { + char Buffer[TextBufferSize]; + FTPPathToFatFsPath(pInBuffer, Buffer, sizeof(Buffer)); + Path = Buffer; + } + else + Path.Format("%s/%s", static_cast(m_CurrentPath), pInBuffer); + + return Path; +} + +const TDirectoryListEntry* CFTPWorker::BuildDirectoryList(size_t& nOutEntries) const +{ + DIR Dir; + FILINFO FileInfo; + FRESULT Result; + + TDirectoryListEntry* pEntries = nullptr; + nOutEntries = 0; + + // Volume list + if (m_CurrentPath.GetLength() == 0) + { + constexpr size_t nVolumes = Utility::ArraySize(VolumeNames); + bool VolumesAvailable[nVolumes] = { false }; + + for (size_t i = 0; i < nVolumes; ++i) + { + char VolumeName[6]; + strncpy(VolumeName, VolumeNames[i], sizeof(VolumeName) - 1); + strcat(VolumeName, ":"); + + // Returns FR_ + if ((Result = f_opendir(&Dir, VolumeName)) == FR_OK) + { + f_closedir(&Dir); + VolumesAvailable[i] = true; + ++nOutEntries; + } + } + + pEntries = new TDirectoryListEntry[nOutEntries]; + + size_t nCurrentEntry = 0; + for (size_t i = 0; i < nVolumes && nCurrentEntry < nOutEntries; ++i) + { + if (VolumesAvailable[i]) + { + TDirectoryListEntry& Entry = pEntries[nCurrentEntry++]; + strncpy(Entry.Name, VolumeNames[i], sizeof(Entry.Name)); + Entry.Type = TDirectoryListEntryType::Directory; + Entry.nSize = 0; + Entry.nLastModifedDate = 0; + Entry.nLastModifedTime = 0; + } + } + + return pEntries; + } + + // Directory list + Result = f_findfirst(&Dir, &FileInfo, m_CurrentPath, "*"); + if (Result == FR_OK && *FileInfo.fname) + { + // Count how many entries we need + do + { + ++nOutEntries; + Result = f_findnext(&Dir, &FileInfo); + } while (Result == FR_OK && *FileInfo.fname); + + f_closedir(&Dir); + + if (nOutEntries && (pEntries = new TDirectoryListEntry[nOutEntries])) + { + size_t nCurrentEntry = 0; + Result = f_findfirst(&Dir, &FileInfo, m_CurrentPath, "*"); + while (Result == FR_OK && *FileInfo.fname) + { + TDirectoryListEntry& Entry = pEntries[nCurrentEntry++]; + strncpy(Entry.Name, FileInfo.fname, sizeof(Entry.Name)); + + if (FileInfo.fattrib & AM_DIR) + { + Entry.Type = TDirectoryListEntryType::Directory; + Entry.nSize = 0; + } + else + { + Entry.Type = TDirectoryListEntryType::File; + Entry.nSize = FileInfo.fsize; + } + + Entry.nLastModifedDate = FileInfo.fdate; + Entry.nLastModifedTime = FileInfo.ftime; + + Result = f_findnext(&Dir, &FileInfo); + } + + f_closedir(&Dir); + + Utility::QSort(pEntries, DirectoryCaseInsensitiveAscending, 0, nOutEntries - 1); + } + } + + return pEntries; +} + +bool CFTPWorker::System(const char* pArgs) +{ + // Some FTP clients (e.g. Directory Opus) will only attempt to parse LIST responses as IIS/DOS-style if we pretend to be Windows NT + SendStatus(TFTPStatus::SystemType, "Windows_NT"); + return true; +} + +bool CFTPWorker::Username(const char* pArgs) +{ + m_User = pArgs; + char Buffer[TextBufferSize]; + snprintf(Buffer, sizeof(Buffer), "Password required for '%s'.", static_cast(m_User)); + SendStatus(TFTPStatus::PasswordRequired, Buffer); + return true; +} + +bool CFTPWorker::Port(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + strncpy(Buffer, pArgs, sizeof(Buffer) - 1); + + if (m_pDataSocket != nullptr) + { + delete m_pDataSocket; + m_pDataSocket = nullptr; + } + + m_TransferMode = TTransferMode::Active; + + // TODO: PORT IP Address should match original IP address + + u8 PortBytes[6]; + char* pSavePtr; + char* pToken = strtok_r(Buffer, " ,", &pSavePtr); + bool bParseError = (pToken == nullptr); + + if (!bParseError) + { + PortBytes[0] = static_cast(atoi(pToken)); + + for (u8 i = 0; i < 5; ++i) + { + pToken = strtok_r(nullptr, " ,", &pSavePtr); + if (pToken == nullptr) + { + bParseError = true; + break; + } + + PortBytes[i + 1] = static_cast(atoi(pToken)); + } + } + + if (bParseError) + { + SendStatus(TFTPStatus::SyntaxError, "Syntax error."); + return false; + } + + m_DataSocketIPAddress.Set(PortBytes); + m_nDataSocketPort = (PortBytes[4] << 8) + PortBytes[5]; + +#ifdef FTPDAEMON_DEBUG + CString IPAddressString; + m_DataSocketIPAddress.Format(&IPAddressString); + LOGDBG("PORT set to: %s:%d", static_cast(IPAddressString), m_nDataSocketPort); +#endif + + SendStatus(TFTPStatus::Success, "Command OK."); + return true; +} + +bool CFTPWorker::Passive(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (m_pDataSocket == nullptr) + { + m_TransferMode = TTransferMode::Passive; + m_nDataSocketPort = PassivePortBase + s_nInstanceCount - 1; + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + m_pDataSocket = new CSocket(pNet, IPPROTO_TCP); + + if (m_pDataSocket == nullptr) + { + SendStatus(TFTPStatus::ServiceNotAvailable, "Failed to open port for passive mode."); + return false; + } + + if (m_pDataSocket->Bind(m_nDataSocketPort) < 0) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not bind to data port."); + delete m_pDataSocket; + m_pDataSocket = nullptr; + return false; + } + + if (m_pDataSocket->Listen() < 0) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not listen on data port."); + delete m_pDataSocket; + m_pDataSocket = nullptr; + return false; + } + } + + u8 IPAddress[IP_ADDRESS_SIZE]; + CNetSubSystem::Get()->GetConfig()->GetIPAddress()->CopyTo(IPAddress); + + char Buffer[TextBufferSize]; + snprintf(Buffer, sizeof(Buffer), "Entering passive mode (%d,%d,%d,%d,%d,%d).", + IPAddress[0], + IPAddress[1], + IPAddress[2], + IPAddress[3], + (m_nDataSocketPort >> 8) & 0xFF, + m_nDataSocketPort & 0xFF + ); + + SendStatus(TFTPStatus::EnteringPassiveMode, Buffer); + return true; +} + +bool CFTPWorker::Password(const char* pArgs) +{ + if (m_User.GetLength() == 0) + { + SendStatus(TFTPStatus::AccountRequired, "Need account for login."); + return false; + } + + m_Password = pArgs; + + if (!CheckLoggedIn()) + return false; + + SendStatus(TFTPStatus::UserLoggedIn, "User logged in."); + return true; +} + +bool CFTPWorker::Type(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (strcasecmp(pArgs, "A") == 0) + { + m_DataType = TDataType::ASCII; + SendStatus(TFTPStatus::Success, "Type set to ASCII."); + return true; + } + + if (strcasecmp(pArgs, "I") == 0) + { + m_DataType = TDataType::Binary; + SendStatus(TFTPStatus::Success, "Type set to binary."); + return true; + } + + SendStatus(TFTPStatus::SyntaxError, "Syntax error."); + return false; +} + +bool CFTPWorker::Retrieve(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + FIL File; + CString Path = RealPath(pArgs); + typedef const char* LPCTSTR; + //printf("%s\n", (LPCTSTR)Path); + //printf("%s\n", exclude_filename ); + if (strcmp((LPCTSTR)Path, exclude_filename) == 0) + { + SendStatus(TFTPStatus::FileNameNotAllowed, "Reading this file is not allowed"); + return false; + } + + if (f_open(&File, Path, FA_READ) != FR_OK) + { + SendStatus(TFTPStatus::FileActionNotTaken, "Could not open file for reading."); + return false; + } + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + size_t nSize = f_size(&File); + size_t nSent = 0; + + while (nSent < nSize) + { + UINT nBytesRead; +#ifdef FTPDAEMON_DEBUG + LOGDBG("Sending data"); +#endif + if (f_read(&File, m_DataBuffer, sizeof(m_DataBuffer), &nBytesRead) != FR_OK || pDataSocket->Send(m_DataBuffer, nBytesRead, 0) < 0) + { + delete pDataSocket; + f_close(&File); + SendStatus(TFTPStatus::ActionAborted, "File action aborted, local error."); + return false; + } + + nSent += nBytesRead; + assert(nSent <= nSize); + } + + delete pDataSocket; + f_close(&File); + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + + return false; +} + +bool CFTPWorker::Store(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + FIL File; + CString Path = RealPath(pArgs); + + if (f_open(&File, Path, FA_CREATE_ALWAYS | FA_WRITE) != FR_OK) + { + SendStatus(TFTPStatus::FileActionNotTaken, "Could not open file for writing."); + return false; + } + + f_sync(&File); + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + bool bSuccess = true; + + CTimer* const pTimer = CTimer::Get(); + unsigned int nTimeout = pTimer->GetTicks(); + + while (true) + { +#ifdef FTPDAEMON_DEBUG + LOGDBG("Waiting to receive"); +#endif + int nReceiveResult = pDataSocket->Receive(m_DataBuffer, sizeof(m_DataBuffer), MSG_DONTWAIT); + FRESULT nWriteResult; + UINT nWritten; + + if (nReceiveResult == 0) + { + if (pTimer->GetTicks() - nTimeout >= SocketTimeout * HZ) + { + LOGERR("Socket timed out"); + bSuccess = false; + break; + } + CScheduler::Get()->Yield(); + continue; + } + + // All done + if (nReceiveResult < 0) + { + LOGNOTE("Receive done, no more data"); + break; + } + +#ifdef FTPDAEMON_DEBUG + //LOGDBG("Received %d bytes", nReceiveResult); +#endif + + if ((nWriteResult = f_write(&File, m_DataBuffer, nReceiveResult, &nWritten)) != FR_OK) + { + LOGERR("Write FAILED, return code %d", nWriteResult); + bSuccess = false; + break; + } + + f_sync(&File); + CScheduler::Get()->Yield(); + + nTimeout = pTimer->GetTicks(); + } + + if (bSuccess) + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + else + SendStatus(TFTPStatus::ActionAborted, "File action aborted, local error."); + +#ifdef FTPDAEMON_DEBUG + LOGDBG("Closing socket/file"); +#endif + delete pDataSocket; + f_close(&File); + + return true; +} + +bool CFTPWorker::Delete(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + CString Path = RealPath(pArgs); + + if (f_unlink(Path) != FR_OK) + SendStatus(TFTPStatus::FileActionNotTaken, "File was not deleted."); + else + SendStatus(TFTPStatus::FileActionOk, "File deleted."); + + return true; +} + +bool CFTPWorker::MakeDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + CString Path = RealPath(pArgs); + + if (f_mkdir(Path) != FR_OK) + SendStatus(TFTPStatus::FileActionNotTaken, "Directory creation failed."); + else + { + char Buffer[TextBufferSize]; + FatFsPathToFTPPath(Path, Buffer, sizeof(Buffer)); + strcat(Buffer, " directory created."); + SendStatus(TFTPStatus::PathCreated, Buffer); + } + + return true; +} + +bool CFTPWorker::ChangeWorkingDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + bool bSuccess = false; + + const bool bAbsolute = pArgs[0] == '/'; + if (bAbsolute) + { + // Root + if (pArgs[1] == '\0') + { + m_CurrentPath = ""; + bSuccess = true; + } + else + { + DIR Dir; + FTPPathToFatFsPath(pArgs, Buffer, sizeof(Buffer)); + + // f_stat() will fail if we're trying to CWD to the root of a volume, so use f_opendir() + if (f_opendir(&Dir, Buffer) == FR_OK) + { + f_closedir(&Dir); + m_CurrentPath = Buffer; + bSuccess = true; + } + } + } + else + { + const bool bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + { + if (ValidateVolumeName(pArgs)) + { + m_CurrentPath.Format("%s:", pArgs); + bSuccess = true; + } + } + else + { + CString NewPath; + NewPath.Format("%s/%s", static_cast(m_CurrentPath), pArgs); + + if (f_stat(NewPath, nullptr) == FR_OK) + { + m_CurrentPath = NewPath; + bSuccess = true; + } + } + } + + if (bSuccess) + { + const bool bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + strncpy(Buffer, "\"/\"", sizeof(Buffer)); + else + FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); + SendStatus(TFTPStatus::FileActionOk, Buffer); + } + else + SendStatus(TFTPStatus::FileNotFound, "Directory unavailable."); + + return bSuccess; +} + +bool CFTPWorker::ChangeToParentDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + bool bSuccess = false; + bool bAtRoot = m_CurrentPath.GetLength() == 0; + + if (!bAtRoot) + { + DIR Dir; + FatFsParentPath(m_CurrentPath, Buffer, sizeof(Buffer)); + + bAtRoot = Buffer[0] == '\0'; + if (bAtRoot) + { + m_CurrentPath = Buffer; + bSuccess = true; + } + else if (f_opendir(&Dir, Buffer) == FR_OK) + { + f_closedir(&Dir); + m_CurrentPath = Buffer; + bSuccess = true; + } + } + + if (bSuccess) + { + bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + strncpy(Buffer, "\"/\"", sizeof(Buffer)); + else + FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); + SendStatus(TFTPStatus::FileActionOk, Buffer); + } + else + SendStatus(TFTPStatus::FileNotFound, "Directory unavailable."); + + return false; +} + +bool CFTPWorker::PrintWorkingDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + + const bool bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + strncpy(Buffer, "\"/\"", sizeof(Buffer)); + else + FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); + + SendStatus(TFTPStatus::PathCreated, Buffer); + + return true; +} + +bool CFTPWorker::List(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + char Buffer[TextBufferSize]; + char Date[9]; + char Time[8]; + + size_t nEntries; + const TDirectoryListEntry* pDirEntries = BuildDirectoryList(nEntries); + + if (pDirEntries) + { + for (size_t i = 0; i < nEntries; ++i) + { + const TDirectoryListEntry& Entry = pDirEntries[i]; + int nLength; + + // Mimic the Microsoft IIS LIST format + FormatLastModifiedDate(Entry.nLastModifedDate, Date, sizeof(Date)); + FormatLastModifiedTime(Entry.nLastModifedTime, Time, sizeof(Time)); + + if (Entry.Type == TDirectoryListEntryType::Directory) + nLength = snprintf(Buffer, sizeof(Buffer), "%-9s %-13s %-14s %s\r\n", Date, Time, "", Entry.Name); + else + nLength = snprintf(Buffer, sizeof(Buffer), "%-9s %-13s %14d %s\r\n", Date, Time, Entry.nSize, Entry.Name); + + if (pDataSocket->Send(Buffer, nLength, 0) < 0) + { + delete[] pDirEntries; + delete pDataSocket; + SendStatus(TFTPStatus::DataConnectionFailed, "Transfer error."); + return false; + } + } + + delete[] pDirEntries; + } + + delete pDataSocket; + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + return true; +} + +bool CFTPWorker::ListFileNames(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + char Buffer[TextBufferSize]; + size_t nEntries; + const TDirectoryListEntry* pDirEntries = BuildDirectoryList(nEntries); + + if (pDirEntries) + { + for (size_t i = 0; i < nEntries; ++i) + { + const TDirectoryListEntry& Entry = pDirEntries[i]; + if (Entry.Type == TDirectoryListEntryType::Directory) + continue; + const int nLength = snprintf(Buffer, sizeof(Buffer), "%s\r\n", Entry.Name); + if (pDataSocket->Send(Buffer, nLength, 0) < 0) + { + delete[] pDirEntries; + delete pDataSocket; + SendStatus(TFTPStatus::DataConnectionFailed, "Transfer error."); + return false; + } + } + + delete[] pDirEntries; + } + + delete pDataSocket; + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + return true; +} + +bool CFTPWorker::RenameFrom(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + m_RenameFrom = pArgs; + SendStatus(TFTPStatus::PendingFurtherInfo, "Requested file action pending further information."); + + return false; +} + +bool CFTPWorker::RenameTo(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (m_RenameFrom.GetLength() == 0) + { + SendStatus(TFTPStatus::BadCommandSequence, "Bad sequence of commands."); + return false; + } + + CString SourcePath = RealPath(m_RenameFrom); + CString DestPath = RealPath(pArgs); + + if (f_rename(SourcePath, DestPath) != FR_OK) + SendStatus(TFTPStatus::FileNameNotAllowed, "File name not allowed."); + else + SendStatus(TFTPStatus::FileActionOk, "File renamed."); + + m_RenameFrom = ""; + + return false; +} + +bool CFTPWorker::Bye(const char* pArgs) +{ + SendStatus(TFTPStatus::ClosingControl, "Goodbye."); + delete m_pControlSocket; + m_pControlSocket = nullptr; + + // Reboot the system if the user disconnects in order to apply any changes made + reboot (); + return true; +} + +bool CFTPWorker::NoOp(const char* pArgs) +{ + SendStatus(TFTPStatus::Success, "Command OK."); + return true; +} + +void CFTPWorker::FatFsPathToFTPPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) +{ + assert(pOutBuffer && nSize > 2); + const char* pEnd = pOutBuffer + nSize; + const char* pInChar = pInBuffer; + char* pOutChar = pOutBuffer; + + *pOutChar++ = '"'; + *pOutChar++ = '/'; + + while (*pInChar != '\0' && pOutChar < pEnd) + { + // Kill the volume colon + if (*pInChar == ':') + { + *pOutChar++ = '/'; + ++pInChar; + + // Kill any slashes after the colon + while (*pInChar == '/') ++pInChar; + continue; + } + + // Kill duplicate slashes + if (*pInChar == '/') + { + *pOutChar++ = *pInChar++; + while (*pInChar == '/') ++pInChar; + continue; + } + + *pOutChar++ = *pInChar++; + } + + // Kill trailing slash + if (*(pOutChar - 1) == '/') + --pOutChar; + + assert(pOutChar < pEnd - 2); + *pOutChar++ = '"'; + *pOutChar++ = '\0'; +} + +void CFTPWorker::FTPPathToFatFsPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) +{ + assert(pInBuffer && pOutBuffer); + const char* pEnd = pOutBuffer + nSize; + const char* pInChar = pInBuffer; + char* pOutChar = pOutBuffer; + + // Kill leading slashes + while (*pInChar == '/') ++pInChar; + + bool bGotVolume = false; + while (*pInChar != '\0' && pOutChar < pEnd) + { + // Kill the volume colon + if (!bGotVolume && *pInChar == '/') + { + bGotVolume = true; + *pOutChar++ = ':'; + ++pInChar; + + // Kill any slashes after the colon + while (*pInChar == '/') ++pInChar; + continue; + } + + // Kill duplicate slashes + if (*pInChar == '/') + { + *pOutChar++ = *pInChar++; + while (*pInChar == '/') ++pInChar; + continue; + } + + *pOutChar++ = *pInChar++; + } + + assert(pOutChar < pEnd - 2); + + // Kill trailing slash + if (*(pOutChar - 1) == '/') + --pOutChar; + + // Add volume colon + if (!bGotVolume) + *pOutChar++ = ':'; + + *pOutChar++ = '\0'; +} + +void CFTPWorker::FatFsParentPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) +{ + assert(pInBuffer != nullptr && pOutBuffer != nullptr); + + size_t nLength = strlen(pInBuffer); + assert(nLength > 0 && nSize >= nLength); + + const char* pLastChar = pInBuffer + nLength - 1; + const char* pInChar = pLastChar; + + // Kill trailing slashes + while (*pInChar == '/' && pInChar > pInBuffer) --pInChar; + + // Kill subdirectory name + while (*pInChar != '/' && *pInChar != ':' && pInChar > pInBuffer) --pInChar; + + // Kill trailing slashes + while (*pInChar == '/' && pInChar > pInBuffer) --pInChar; + + // Pointer didn't move (we're already at a volume root), or we reached the start of the string (path invalid) + if (pInChar == pLastChar || pInChar == pInBuffer) + { + *pOutBuffer = '\0'; + return; + } + + // Truncate string + nLength = pInChar - pInBuffer + 1; + memcpy(pOutBuffer, pInBuffer, nLength); + pOutBuffer[nLength] = '\0'; +} + +void CFTPWorker::FormatLastModifiedDate(u16 nDate, char* pOutBuffer, size_t nSize) +{ + // 2-digit year + const u16 nYear = (1980 + (nDate >> 9)) % 100; + u16 nMonth = (nDate >> 5) & 0x0F; + u16 nDay = nDate & 0x1F; + + if (nMonth == 0) + nMonth = 1; + if (nDay == 0) + nDay = 1; + + snprintf(pOutBuffer, nSize, "%02d-%02d-%02d", nMonth, nDay, nYear); +} + +void CFTPWorker::FormatLastModifiedTime(u16 nDate, char* pOutBuffer, size_t nSize) +{ + u16 nHour = (nDate >> 11) & 0x1F; + const u16 nMinute = (nDate >> 5) & 0x3F; + const char* pSuffix = nHour < 12 ? "AM" : "PM"; + + if (nHour == 0) + nHour = 12; + else if (nHour >= 12) + nHour -= 12; + + snprintf(pOutBuffer, nSize, "%02d:%02d%s", nHour, nMinute, pSuffix); +} \ No newline at end of file diff --git a/src/net/ftpworker.h b/src/net/ftpworker.h new file mode 100644 index 0000000..62e60ed --- /dev/null +++ b/src/net/ftpworker.h @@ -0,0 +1,157 @@ +// +// ftpworker.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _ftpworker_h +#define _ftpworker_h + +#include +#include +#include +#include + +// TODO: These may be incomplete/inaccurate +enum TFTPStatus +{ + FileStatusOk = 150, + + Success = 200, + SystemType = 215, + ReadyForNewUser = 220, + ClosingControl = 221, + TransferComplete = 226, + EnteringPassiveMode = 227, + UserLoggedIn = 230, + FileActionOk = 250, + PathCreated = 257, + + PasswordRequired = 331, + AccountRequired = 332, + PendingFurtherInfo = 350, + + ServiceNotAvailable = 421, + DataConnectionFailed = 425, + FileActionNotTaken = 450, + ActionAborted = 451, + + CommandUnrecognized = 500, + SyntaxError = 501, + CommandNotImplemented = 502, + BadCommandSequence = 503, + NotLoggedIn = 530, + FileNotFound = 550, + FileNameNotAllowed = 553, +}; + +enum class TTransferMode +{ + Active, + Passive, +}; + +enum class TDataType +{ + ASCII, + Binary, +}; + +struct TFTPCommand; +struct TDirectoryListEntry; + +class CFTPWorker : protected CTask +{ +public: + CFTPWorker(CSocket* pControlSocket, const char* pExpectedUser, const char* pExpectedPassword); + virtual ~CFTPWorker() override; + + virtual void Run() override; + + static u8 GetInstanceCount() { return s_nInstanceCount; } + +private: + CSocket* OpenDataConnection(); + + bool SendStatus(TFTPStatus StatusCode, const char* pMessage); + + bool CheckLoggedIn(); + + // Directory navigation + CString RealPath(const char* pInBuffer) const; + const TDirectoryListEntry* BuildDirectoryList(size_t& nOutEntries) const; + + // FTP command handlers + bool System(const char* pArgs); + bool Username(const char* pArgs); + bool Port(const char* pArgs); + bool Passive(const char* pArgs); + bool Password(const char* pArgs); + bool Type(const char* pArgs); + bool Retrieve(const char* pArgs); + bool Store(const char* pArgs); + bool Delete(const char* pArgs); + bool MakeDirectory(const char* pArgs); + bool ChangeWorkingDirectory(const char* pArgs); + bool ChangeToParentDirectory(const char* pArgs); + bool PrintWorkingDirectory(const char* pArgs); + bool List(const char* pArgs); + bool ListFileNames(const char* pArgs); + bool RenameFrom(const char* pArgs); + bool RenameTo(const char* pArgs); + bool Bye(const char* pArgs); + bool NoOp(const char* pArgs); + + CString m_LogName; + + // Authentication + const char* m_pExpectedUser; + const char* m_pExpectedPassword; + + // TCP sockets + CSocket* m_pControlSocket; + CSocket* m_pDataSocket; + u16 m_nDataSocketPort; + CIPAddress m_DataSocketIPAddress; + + // Command/data buffers + char m_CommandBuffer[FRAME_BUFFER_SIZE]; + u8 m_DataBuffer[FRAME_BUFFER_SIZE]; + + // Session state + CString m_User; + CString m_Password; + TDataType m_DataType; + TTransferMode m_TransferMode; + CString m_CurrentPath; + CString m_RenameFrom; + + static void FatFsPathToFTPPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); + static void FTPPathToFatFsPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); + + static void FatFsParentPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); + + static void FormatLastModifiedDate(u16 nDate, char* pOutBuffer, size_t nSize); + static void FormatLastModifiedTime(u16 nDate, char* pOutBuffer, size_t nSize); + + static const TFTPCommand Commands[]; + static u8 s_nInstanceCount; +}; + +#endif \ No newline at end of file diff --git a/src/net/mdnspublisher.cpp b/src/net/mdnspublisher.cpp new file mode 100644 index 0000000..23052db --- /dev/null +++ b/src/net/mdnspublisher.cpp @@ -0,0 +1,345 @@ +// +// mdnspublisher.cpp +// +// Circle - A C++ bare metal environment for Raspberry Pi +// Copyright (C) 2024 R. Stange +// +// 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 "mdnspublisher.h" +#include +#include +#include +#include +#include +#define MDNS_HOST_GROUP {224, 0, 0, 251} +#define MDNS_PORT 5353 +#define MDNS_DOMAIN "local" +#define RR_TYPE_A 1 +#define RR_TYPE_PTR 12 +#define RR_TYPE_TXT 16 +#define RR_TYPE_SRV 33 +#define RR_CLASS_IN 1 +#define RR_CACHE_FLUSH 0x8000 +LOGMODULE ("mdnspub"); +CmDNSPublisher::CmDNSPublisher (CNetSubSystem *pNet) +: m_pNet (pNet), + m_pSocket (nullptr), + m_bRunning (FALSE), + m_pWritePtr (nullptr), + m_pDataLen (nullptr) +{ + SetName ("mdnspub"); +} +CmDNSPublisher::~CmDNSPublisher (void) +{ + assert (!m_pSocket); + m_bRunning = FALSE; +} +boolean CmDNSPublisher::PublishService (const char *pServiceName, const char *pServiceType, + u16 usServicePort, const char *ppText[]) +{ + if (!m_bRunning) + { + // Let task can run once to initialize + CScheduler::Get ()->Yield (); + if (!m_bRunning) + { + return FALSE; + } + } + assert (pServiceName); + assert (pServiceType); + TService *pService = new TService {pServiceName, pServiceType, usServicePort, 0}; + assert (pService); + if (ppText) + { + for (unsigned i = 0; i < MaxTextRecords && ppText[i]; i++) + { + pService->ppText[i] = new CString (ppText[i]); + assert (pService->ppText[i]); + pService->nTextRecords++; + } + } + m_Mutex.Acquire (); + // Insert as first element into list + TPtrListElement *pElement = m_ServiceList.GetFirst (); + if (pElement) + { + m_ServiceList.InsertBefore (pElement, pService); + } + else + { + m_ServiceList.InsertAfter (nullptr, pService); + } + m_Mutex.Release (); + LOGDBG ("Publish service %s", (const char *) pService->ServiceName); + m_Event.Set (); // Trigger resent for everything + return TRUE; +} +boolean CmDNSPublisher::UnpublishService (const char *pServiceName) +{ + if (!m_bRunning) + { + return FALSE; + } + assert (pServiceName); + m_Mutex.Acquire (); + // Find service in the list and remove it + TService *pService = nullptr; + TPtrListElement *pElement = m_ServiceList.GetFirst (); + while (pElement) + { + pService = static_cast (CPtrList::GetPtr (pElement)); + assert (pService); + if (pService->ServiceName.Compare (pServiceName) == 0) + { + m_ServiceList.Remove (pElement); + break; + } + pService = nullptr; + pElement = m_ServiceList.GetNext (pElement); + } + m_Mutex.Release (); + if (!pService) + { + return FALSE; + } + LOGDBG ("Unpublish service %s", (const char *) pService->ServiceName); + if (!SendResponse (pService, TRUE)) + { + LOGWARN ("Send failed"); + } + for (unsigned i = 0; i < pService->nTextRecords; i++) + { + delete pService->ppText[i]; + } + delete pService; + return TRUE; +} +void CmDNSPublisher::Run (void) +{ + assert (m_pNet); + assert (!m_pSocket); + m_pSocket = new CSocket (m_pNet, IPPROTO_UDP); + assert (m_pSocket); + if (m_pSocket->Bind (MDNS_PORT) < 0) + { + LOGERR ("Cannot bind to port %u", MDNS_PORT); + delete m_pSocket; + m_pSocket = nullptr; + while (1) + { + m_Event.Clear (); + m_Event.Wait (); + } + } + static const u8 mDNSIPAddress[] = MDNS_HOST_GROUP; + CIPAddress mDNSIP (mDNSIPAddress); + if (m_pSocket->Connect (mDNSIP, MDNS_PORT) < 0) + { + LOGERR ("Cannot connect to mDNS host group"); + delete m_pSocket; + m_pSocket = nullptr; + while (1) + { + m_Event.Clear (); + m_Event.Wait (); + } + } + m_bRunning = TRUE; + while (1) + { + m_Event.Clear (); + m_Event.WaitWithTimeout ((TTLShort - 10) * 1000000); + for (unsigned i = 1; i <= 3; i++) + { + m_Mutex.Acquire (); + TPtrListElement *pElement = m_ServiceList.GetFirst (); + while (pElement) + { + TService *pService = + static_cast (CPtrList::GetPtr (pElement)); + assert (pService); + if (!SendResponse (pService, FALSE)) + { + LOGWARN ("Send failed"); + } + pElement = m_ServiceList.GetNext (pElement); + } + m_Mutex.Release (); + CScheduler::Get ()->Sleep (1); + } + } +} +boolean CmDNSPublisher::SendResponse (TService *pService, boolean bDelete) +{ + assert (pService); + assert (m_pNet); + // Collect data + static const char Domain[] = "." MDNS_DOMAIN; + CString ServiceType (pService->ServiceType); + ServiceType.Append (Domain); + CString ServiceName (pService->ServiceName); + ServiceName.Append ("."); + ServiceName.Append (ServiceType); + CString Hostname (m_pNet->GetHostname ()); + Hostname.Append (Domain); + // Start writing buffer + assert (!m_pWritePtr); + m_pWritePtr = m_Buffer; + // mDNS Header + PutWord (0); // Transaction ID + PutWord (0x8400); // Message is a response, Server is an authority for the domain + PutWord (0); // Questions + PutWord (5); // Answer RRs + PutWord (0); // Authority RRs + PutWord (0); // Additional RRs + // Answer RRs + // PTR + PutDNSName ("_services._dns-sd._udp.local"); + PutWord (RR_TYPE_PTR); + PutWord (RR_CLASS_IN); + PutDWord (bDelete ? TTLDelete : TTLLong); + ReserveDataLength (); + u8 *pServiceTypePtr = m_pWritePtr; + PutDNSName (ServiceType); + SetDataLength (); + // PTR + PutCompressedString (pServiceTypePtr); + PutWord (RR_TYPE_PTR); + PutWord (RR_CLASS_IN); + PutDWord (bDelete ? TTLDelete : TTLLong); + ReserveDataLength (); + u8 *pServiceNamePtr = m_pWritePtr; + PutDNSName (ServiceName); + SetDataLength (); + // SRV + PutCompressedString (pServiceNamePtr); + PutWord (RR_TYPE_SRV); + PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); + PutDWord (bDelete ? TTLDelete : TTLShort); + ReserveDataLength (); + PutWord (0); // Priority + PutWord (0); // Weight + PutWord (pService->usServicePort); + u8 *pHostnamePtr = m_pWritePtr; + PutDNSName (Hostname); + SetDataLength (); + // A + PutCompressedString (pHostnamePtr); + PutWord (RR_TYPE_A); + PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); + PutDWord (TTLShort); + ReserveDataLength (); + PutIPAddress (*m_pNet->GetConfig ()->GetIPAddress ()); + SetDataLength (); + // TXT + PutCompressedString (pServiceNamePtr); + PutWord (RR_TYPE_TXT); + PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); + PutDWord (bDelete ? TTLDelete : TTLLong); + ReserveDataLength (); + for (int i = pService->nTextRecords-1; i >= 0; i--) // In reverse order + { + assert (pService->ppText[i]); + PutString (*pService->ppText[i]); + } + SetDataLength (); + unsigned nMsgSize = m_pWritePtr - m_Buffer; + m_pWritePtr = nullptr; + if (nMsgSize >= MaxMessageSize) + { + return FALSE; + } + assert (m_pSocket); + return m_pSocket->Send (m_Buffer, nMsgSize, MSG_DONTWAIT) == (int) nMsgSize; +} +void CmDNSPublisher::PutByte (u8 uchValue) +{ + assert (m_pWritePtr); + if ((unsigned) (m_pWritePtr - m_Buffer) < MaxMessageSize) + { + *m_pWritePtr++ = uchValue; + } +} +void CmDNSPublisher::PutWord (u16 usValue) +{ + PutByte (usValue >> 8); + PutByte (usValue & 0xFF); +} +void CmDNSPublisher::PutDWord (u32 nValue) +{ + PutWord (nValue >> 16); + PutWord (nValue & 0xFFFF); +} +void CmDNSPublisher::PutString (const char *pValue) +{ + assert (pValue); + size_t nLen = strlen (pValue); + assert (nLen <= 255); + PutByte (nLen); + while (*pValue) + { + PutByte (static_cast (*pValue++)); + } +} +void CmDNSPublisher::PutCompressedString (const u8 *pWritePtr) +{ + assert (m_pWritePtr); + assert (pWritePtr < m_pWritePtr); + unsigned nOffset = pWritePtr - m_Buffer; + assert (nOffset < MaxMessageSize); + nOffset |= 0xC000; + PutWord (static_cast (nOffset)); +} +void CmDNSPublisher::PutDNSName (const char *pValue) +{ + char Buffer[256]; + assert (pValue); + strncpy (Buffer, pValue, sizeof Buffer); + Buffer[sizeof Buffer-1] = '\0'; + char *pSavePtr = nullptr; + char *pToken = strtok_r (Buffer, ".", &pSavePtr); + while (pToken) + { + PutString (pToken); + pToken = strtok_r (nullptr, ".", &pSavePtr); + } + PutByte (0); +} +void CmDNSPublisher::PutIPAddress (const CIPAddress &rValue) +{ + u8 Buffer[IP_ADDRESS_SIZE]; + rValue.CopyTo (Buffer); + for (unsigned i = 0; i < IP_ADDRESS_SIZE; i++) + { + PutByte (Buffer[i]); + } +} +void CmDNSPublisher::ReserveDataLength (void) +{ + assert (!m_pDataLen); + m_pDataLen = m_pWritePtr; + assert (m_pDataLen); + PutWord (0); +} +void CmDNSPublisher::SetDataLength (void) +{ + assert (m_pDataLen); + assert (m_pWritePtr); + assert (m_pWritePtr > m_pDataLen); + *reinterpret_cast (m_pDataLen) = le2be16 (m_pWritePtr - m_pDataLen - sizeof (u16)); + m_pDataLen = nullptr; +} \ No newline at end of file diff --git a/src/net/mdnspublisher.h b/src/net/mdnspublisher.h new file mode 100644 index 0000000..6b132a7 --- /dev/null +++ b/src/net/mdnspublisher.h @@ -0,0 +1,90 @@ +// +// mdnspublisher.h +// +// Circle - A C++ bare metal environment for Raspberry Pi +// Copyright (C) 2024 R. Stange +// +// 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 _circle_net_mdnspublisher_h +#define _circle_net_mdnspublisher_h +#include +#include +#include +#include +#include +#include +#include +#include +#include +class CmDNSPublisher : public CTask /// mDNS / Bonjour client task +{ +public: + static constexpr const char *ServiceTypeAppleMIDI = "_apple-midi._udp"; +public: + /// \param pNet Pointer to the network subsystem object + CmDNSPublisher (CNetSubSystem *pNet); + ~CmDNSPublisher (void); + /// \brief Start publishing a service + /// \param pServiceName Name of the service to be published + /// \param pServiceType Type of the service to be published (e.g. ServiceTypeAppleMIDI) + /// \param usServicePort Port number of the service to be published (in host byte order) + /// \param ppText Descriptions of the service (terminated with a nullptr, or nullptr itself) + /// \return Operation successful? + boolean PublishService (const char *pServiceName, + const char *pServiceType, + u16 usServicePort, + const char *ppText[] = nullptr); + /// \brief Stop publishing a service + /// \param pServiceName Name of the service to be unpublished (same as when published) + /// \return Operation successful? + boolean UnpublishService (const char *pServiceName); + void Run (void) override; +private: + static const unsigned MaxTextRecords = 10; + static const unsigned MaxMessageSize = 1400; // safe UDP payload in an Ethernet frame + static const unsigned TTLShort = 15; // seconds + static const unsigned TTLLong = 4500; + static const unsigned TTLDelete = 0; + struct TService + { + CString ServiceName; + CString ServiceType; + u16 usServicePort; + unsigned nTextRecords; + CString *ppText[MaxTextRecords]; + }; + boolean SendResponse (TService *pService, boolean bDelete); + // Helpers for writing to buffer + void PutByte (u8 uchValue); + void PutWord (u16 usValue); + void PutDWord (u32 nValue); + void PutString (const char *pValue); + void PutCompressedString (const u8 *pWritePtr); + void PutDNSName (const char *pValue); + void PutIPAddress (const CIPAddress &rValue); + void ReserveDataLength (void); + void SetDataLength (void); +private: + CNetSubSystem *m_pNet; + CPtrList m_ServiceList; + CMutex m_Mutex; + CSocket *m_pSocket; + boolean m_bRunning; + CSynchronizationEvent m_Event; + u8 m_Buffer[MaxMessageSize]; + u8 *m_pWritePtr; + u8 *m_pDataLen; +}; +#endif \ No newline at end of file diff --git a/src/net/udpmidi.cpp b/src/net/udpmidi.cpp new file mode 100644 index 0000000..2f25eda --- /dev/null +++ b/src/net/udpmidi.cpp @@ -0,0 +1,89 @@ +// +// udpmidi.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#include +#include +#include +#include + +#include "udpmidi.h" + +LOGMODULE("udpmidi"); + +constexpr u16 MIDIPort = 1999; + +CUDPMIDIReceiver::CUDPMIDIReceiver(CUDPMIDIHandler* pHandler) + : CTask(TASK_STACK_SIZE, true), + m_pMIDISocket(nullptr), + m_MIDIBuffer{0}, + m_pHandler(pHandler) +{ +} + +CUDPMIDIReceiver::~CUDPMIDIReceiver() +{ + if (m_pMIDISocket) + delete m_pMIDISocket; +} + +bool CUDPMIDIReceiver::Initialize() +{ + assert(m_pMIDISocket == nullptr); + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pMIDISocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if (m_pMIDISocket->Bind(MIDIPort) != 0) + { + LOGERR("Couldn't bind to port %d", MIDIPort); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CUDPMIDIReceiver::Run() +{ + assert(m_pHandler != nullptr); + assert(m_pMIDISocket != nullptr); + + CScheduler* const pScheduler = CScheduler::Get(); + + while (true) + { + // Blocking call + const int nMIDIResult = m_pMIDISocket->Receive(m_MIDIBuffer, sizeof(m_MIDIBuffer), 0); + + if (nMIDIResult < 0) + LOGERR("MIDI socket receive error: %d", nMIDIResult); + else if (nMIDIResult > 0) + m_pHandler->OnUDPMIDIDataReceived(m_MIDIBuffer, nMIDIResult); + + // Allow other tasks to run + pScheduler->Yield(); + } +} \ No newline at end of file diff --git a/src/net/udpmidi.h b/src/net/udpmidi.h new file mode 100644 index 0000000..102d339 --- /dev/null +++ b/src/net/udpmidi.h @@ -0,0 +1,57 @@ +// +// udpmidi.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _udpmidi_h +#define _udpmidi_h + +#include +#include +#include + +class CUDPMIDIHandler +{ +public: + virtual void OnUDPMIDIDataReceived(const u8* pData, size_t nSize) = 0; +}; + +class CUDPMIDIReceiver : protected CTask +{ +public: + CUDPMIDIReceiver(CUDPMIDIHandler* pHandler); + virtual ~CUDPMIDIReceiver() override; + + bool Initialize(); + + virtual void Run() override; + +private: + // UDP sockets + CSocket* m_pMIDISocket; + + // Socket receive buffer + u8 m_MIDIBuffer[FRAME_BUFFER_SIZE]; + + // Callback handler + CUDPMIDIHandler* m_pHandler; +}; + +#endif \ No newline at end of file diff --git a/src/net/utility.h b/src/net/utility.h new file mode 100644 index 0000000..3b64395 --- /dev/null +++ b/src/net/utility.h @@ -0,0 +1,193 @@ + +// +// utility.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _utility_h +#define _utility_h + +#include +#include + +// Macro to extract the string representation of an enum +#define CONFIG_ENUM_VALUE(VALUE, STRING) VALUE, + +// Macro to extract the enum value +#define CONFIG_ENUM_STRING(VALUE, STRING) #STRING, + +// Macro to declare the enum itself +#define CONFIG_ENUM(NAME, VALUES) enum class NAME { VALUES(CONFIG_ENUM_VALUE) } + +// Macro to declare an array of string representations for an enum +#define CONFIG_ENUM_STRINGS(NAME, DATA) static const char* NAME##Strings[] = { DATA(CONFIG_ENUM_STRING) } + +namespace Utility +{ + // Templated function for clamping a value between a minimum and a maximum + template + constexpr T Clamp(const T& nValue, const T& nMin, const T& nMax) + { + return (nValue < nMin) ? nMin : (nValue > nMax) ? nMax : nValue; + } + + // Templated function for taking the minimum of two values + template + constexpr T Min(const T& nLHS, const T& nRHS) + { + return nLHS < nRHS ? nLHS : nRHS; + } + + // Templated function for taking the maximum of two values + template + constexpr T Max(const T& nLHS, const T& nRHS) + { + return nLHS > nRHS ? nLHS : nRHS; + } + + // Function for performing a linear interpolation of a value + constexpr float Lerp(float nValue, float nMinA, float nMaxA, float nMinB, float nMaxB) + { + return nMinB + (nValue - nMinA) * ((nMaxB - nMinB) / (nMaxA - nMinA)); + } + + // Return number of elements in an array + template + constexpr size_t ArraySize(const T(&)[N]) { return N; } + + // Returns whether some value is a power of 2 + template + constexpr bool IsPowerOfTwo(const T& nValue) + { + return nValue && ((nValue & (nValue - 1)) == 0); + } + + // Rounds a number to a nearest multiple; only works for integer values/multiples + template + constexpr T RoundToNearestMultiple(const T& nValue, const T& nMultiple) + { + return ((nValue + nMultiple / 2) / nMultiple) * nMultiple; + } + + // Convert between milliseconds and ticks of a 1MHz clock + template + constexpr T MillisToTicks(const T& nMillis) + { + return nMillis * 1000; + } + + template + constexpr T TicksToMillis(const T& nTicks) + { + return nTicks / 1000; + } + + // Computes the Roland checksum + constexpr u8 RolandChecksum(const u8* pData, size_t nSize) + { + u8 nSum = 0; + for (size_t i = 0; i < nSize; ++i) + nSum = (nSum + pData[i]) & 0x7F; + + return 128 - nSum; + } + + // Comparators for sorting + namespace Comparator + { + template + using TComparator = bool (*)(const T&, const T&); + + template + inline bool LessThan(const T& ObjectA, const T& ObjectB) + { + return ObjectA < ObjectB; + } + + template + inline bool GreaterThan(const T& ObjectA, const T& ObjectB) + { + return ObjectA > ObjectB; + } + + inline bool CaseInsensitiveAscending(const CString& StringA, const CString& StringB) + { + return strcasecmp(StringA, StringB) < 0; + } + } + + // Swaps two objects in-place + template + inline void Swap(T& ObjectA, T& ObjectB) + { + u8 Buffer[sizeof(T)]; + memcpy(Buffer, &ObjectA, sizeof(T)); + memcpy(&ObjectA, &ObjectB, sizeof(T)); + memcpy(&ObjectB, Buffer, sizeof(T)); + } + + namespace + { + // Quicksort partition function (private) + template + size_t Partition(T* Items, Comparator::TComparator Comparator, size_t nLow, size_t nHigh) + { + const size_t nPivotIndex = (nHigh + nLow) / 2; + T* Pivot = &Items[nPivotIndex]; + + while (true) + { + while (Comparator(Items[nLow], *Pivot)) + ++nLow; + + while (Comparator(*Pivot, Items[nHigh])) + --nHigh; + + if (nLow >= nHigh) + return nHigh; + + Swap(Items[nLow], Items[nHigh]); + + // Update pointer if pivot was swapped + if (nPivotIndex == nLow) + Pivot = &Items[nHigh]; + else if (nPivotIndex == nHigh) + Pivot = &Items[nLow]; + + ++nLow; + --nHigh; + } + } + } + + // Sorts an array in-place using the Tony Hoare Quicksort algorithm + template + void QSort(T* Items, Comparator::TComparator Comparator, size_t nLow, size_t nHigh) + { + if (nLow < nHigh) + { + size_t p = Partition(Items, Comparator, nLow, nHigh); + QSort(Items, Comparator, nLow, p); + QSort(Items, Comparator, p + 1, nHigh); + } + } +} + +#endif diff --git a/src/udpmididevice.cpp b/src/udpmididevice.cpp new file mode 100644 index 0000000..4b0d1c7 --- /dev/null +++ b/src/udpmididevice.cpp @@ -0,0 +1,90 @@ +// +// udpmididevice.cpp +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// R. Stange +// +// 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 +#include +#include "udpmididevice.h" +#include + +#define VIRTUALCABLE 24 + +LOGMODULE("udpmididevice"); + +CUDPMIDIDevice::CUDPMIDIDevice (CMiniDexed *pSynthesizer, + CConfig *pConfig, CUserInterface *pUI) +: CMIDIDevice (pSynthesizer, pConfig, pUI), + m_pSynthesizer (pSynthesizer), + m_pConfig (pConfig) +{ + AddDevice ("udp"); +} + +CUDPMIDIDevice::~CUDPMIDIDevice (void) +{ + //m_pSynthesizer = 0; +} + +boolean CUDPMIDIDevice::Initialize (void) +{ + m_pAppleMIDIParticipant = new CAppleMIDIParticipant(&m_Random, this); + if (!m_pAppleMIDIParticipant->Initialize()) + { + LOGERR("Failed to init RTP listener"); + delete m_pAppleMIDIParticipant; + m_pAppleMIDIParticipant = nullptr; + } + else + LOGNOTE("RTP Listener initialized"); + m_pUDPMIDIReceiver = new CUDPMIDIReceiver(this); + if (!m_pUDPMIDIReceiver->Initialize()) + { + LOGERR("Failed to init UDP MIDI receiver"); + delete m_pUDPMIDIReceiver; + m_pUDPMIDIReceiver = nullptr; + } + else + LOGNOTE("UDP MIDI receiver initialized"); + return true; +} + +// Methods to handle MIDI events + +void CUDPMIDIDevice::OnAppleMIDIDataReceived(const u8* pData, size_t nSize) +{ + MIDIMessageHandler(pData, nSize, VIRTUALCABLE); +} + +void CUDPMIDIDevice::OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) +{ + LOGNOTE("RTP Device connected"); +} + +void CUDPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) +{ + LOGNOTE("RTP Device disconnected"); +} + +void CUDPMIDIDevice::OnUDPMIDIDataReceived(const u8* pData, size_t nSize) +{ + MIDIMessageHandler(pData, nSize, VIRTUALCABLE); +} \ No newline at end of file diff --git a/src/udpmididevice.h b/src/udpmididevice.h new file mode 100644 index 0000000..de50172 --- /dev/null +++ b/src/udpmididevice.h @@ -0,0 +1,55 @@ +// +// udpmididevice.h +// +// Virtual midi device for data recieved on network +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// R. Stange +// +// 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 _udpmididevice_h +#define _udpmididevice_h + +#include "mididevice.h" +#include "config.h" +#include "net/applemidi.h" +#include "net/udpmidi.h" + +class CMiniDexed; + +class CUDPMIDIDevice : CAppleMIDIHandler, CUDPMIDIHandler, public CMIDIDevice +{ +public: + CUDPMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI); + ~CUDPMIDIDevice (void); + + boolean Initialize (void); + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) override; + 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; + +private: + CMiniDexed *m_pSynthesizer; + CConfig *m_pConfig; + CBcmRandomNumberGenerator m_Random; + CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance + CUDPMIDIReceiver* m_pUDPMIDIReceiver; +}; + +#endif diff --git a/submod.sh b/submod.sh index 2e2f1a8..6685bef 100755 --- a/submod.sh +++ b/submod.sh @@ -1,24 +1,27 @@ #!/bin/bash set -ex -# + # Update top-level modules as a baseline git submodule update --init --recursive -# + # Use fixed master branch of circle-stdlib then re-update cd circle-stdlib/ -git checkout 3bd135d +git reset --hard +git checkout 1111eee # Matches Circle Step49 git submodule update --init --recursive cd - -# + # Optional update submodules explicitly -cd circle-stdlib/libs/circle -git checkout tags/Step49 -cd - -cd circle-stdlib/libs/circle-newlib +#cd circle-stdlib/libs/circle +#git reset --hard +#git checkout tags/Step49 +#cd - +#cd circle-stdlib/libs/circle-newlib #git checkout develop -cd - -# +#cd - + # Use fixed master branch of Synth_Dexed cd Synth_Dexed/ +git reset --hard git checkout c9f5274 cd - From 9fc4ea90cbb4e34c5717722756abebd4061d8217 Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 21 Apr 2025 21:57:48 +0200 Subject: [PATCH 58/61] Mark continunous builds as prerelease --- .github/workflows/build.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b3c310..a17d88d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -144,9 +144,12 @@ jobs: with: name: combined-artifact path: MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip - - name: Upload to GitHub Releases (only from main branch) + - name: Upload to GitHub Releases (only when building from main branch) if: ${{ github.ref == 'refs/heads/main' }} run: | - set -ex - wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh - bash ./upload.sh MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip + set -ex + export UPLOADTOOL_ISPRERELEASE=true + export UPLOADTOOL_PR_BODY="This is a continuous build. Feedback is appreciated." + export UPLOADTOOL_BODY="This is a continuous build. Feedback is appreciated." + wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh + bash ./upload.sh ./MiniDexed*.zip From c9433e577c3e07b57562b87bce7b1cc55f690258 Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 21 Apr 2025 22:09:04 +0200 Subject: [PATCH 59/61] NET_DIR does not exist --- src/Rules.mk | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Rules.mk b/src/Rules.mk index 771350f..9261ebb 100644 --- a/src/Rules.mk +++ b/src/Rules.mk @@ -33,6 +33,4 @@ LIBS += \ $(CIRCLEHOME)/addon/wlan/libwlan.a \ $(CIRCLEHOME)/lib/net/libnet.a -EXTRACLEAN += $(NET_DIR)/*.d $(NET_DIR)/*.o - -include $(DEPS) From c58acf8c771cd478d66bbc22e7d6bbf29fc1f916 Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 21 Apr 2025 22:51:06 +0200 Subject: [PATCH 60/61] Don't show "Reverb-Send" on RPi Zero/1 (#861) Closes #166 --- src/uimenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uimenu.cpp b/src/uimenu.cpp index 6239f65..df25032 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -74,8 +74,8 @@ const CUIMenu::TMenuItem CUIMenu::s_TGMenu[] = {"Volume", EditTGParameter, 0, CMiniDexed::TGParameterVolume}, #ifdef ARM_ALLOW_MULTI_CORE {"Pan", EditTGParameter, 0, CMiniDexed::TGParameterPan}, -#endif {"Reverb-Send", EditTGParameter, 0, CMiniDexed::TGParameterReverbSend}, +#endif {"Detune", EditTGParameter, 0, CMiniDexed::TGParameterMasterTune}, {"Cutoff", EditTGParameter, 0, CMiniDexed::TGParameterCutoff}, {"Resonance", EditTGParameter, 0, CMiniDexed::TGParameterResonance}, From a90dec8a184eba53e411358ca7a130a0dd9145dc Mon Sep 17 00:00:00 2001 From: soyer Date: Mon, 21 Apr 2025 22:51:45 +0200 Subject: [PATCH 61/61] Always show both arrows in the bank selector menu (#855) It wraps around. --- src/uimenu.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/uimenu.cpp b/src/uimenu.cpp index df25032..d7ef12a 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -1788,9 +1788,7 @@ void CUIMenu::EditPerformanceBankNumber (CUIMenu *pUIMenu, TMenuEvent Event) } pUIMenu->m_pUI->DisplayWrite (pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, nPSelected.c_str(), - Value.c_str (), - nValue > 0, - nValue < pUIMenu->m_pMiniDexed->GetLastPerformanceBank()-1); + Value.c_str (), true, true); } void CUIMenu::InputTxt (CUIMenu *pUIMenu, TMenuEvent Event)