From 9b211c1c015a195cfceb6eb5bce9ea23f48bedd0 Mon Sep 17 00:00:00 2001 From: probonopd Date: Fri, 18 Apr 2025 20:22:00 +0200 Subject: [PATCH 01/27] 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 02/27] 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] 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 10/27] 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 11/27] 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) From 9d8ed87aab4370f11bbddbd8073936744411d20b Mon Sep 17 00:00:00 2001 From: soyer Date: Mon, 21 Apr 2025 23:11:15 +0200 Subject: [PATCH 12/27] move MIDI defines to midi.h (#853) The MIDIi defines would also be needed in uibuttons.cpp. Create a common midi.h with the definitions. --------- Co-authored-by: probonopd --- src/midi.h | 48 +++++++++++++++++++++++++++++++++++++++++++ src/mididevice.cpp | 27 +++--------------------- src/uibuttons.cpp | 23 +++++++++++---------- src/uibuttons.h | 2 +- src/userinterface.cpp | 4 ++-- src/userinterface.h | 2 +- 6 files changed, 67 insertions(+), 39 deletions(-) create mode 100644 src/midi.h diff --git a/src/midi.h b/src/midi.h new file mode 100644 index 0000000..4538df7 --- /dev/null +++ b/src/midi.h @@ -0,0 +1,48 @@ +// +// midi.h +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2025 The MiniDexed Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#ifndef _midi_h +#define _midi_h + +#define MIDI_NOTE_OFF 0b1000 +#define MIDI_NOTE_ON 0b1001 +#define MIDI_AFTERTOUCH 0b1010 // TODO +#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_BREATH_CONTROLLER 2 +#define MIDI_CC_FOOT_PEDAL 4 +#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_FREQUENCY_CUTOFF 74 +#define MIDI_CC_REVERB_LEVEL 91 +#define MIDI_CC_DETUNE_LEVEL 94 +#define MIDI_CC_ALL_SOUND_OFF 120 +#define MIDI_CC_ALL_NOTES_OFF 123 + +#define MIDI_PROGRAM_CHANGE 0b1100 +#define MIDI_PITCH_BEND 0b1110 + +#endif diff --git a/src/mididevice.cpp b/src/mididevice.cpp index fefe9fc..e13709f 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -27,32 +27,11 @@ #include "config.h" #include #include +#include "midi.h" #include "userinterface.h" LOGMODULE ("mididevice"); -#define MIDI_NOTE_OFF 0b1000 -#define MIDI_NOTE_ON 0b1001 -#define MIDI_AFTERTOUCH 0b1010 // TODO -#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_BREATH_CONTROLLER 2 - #define MIDI_CC_FOOT_PEDAL 4 - #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_FREQUENCY_CUTOFF 74 - #define MIDI_CC_REVERB_LEVEL 91 - #define MIDI_CC_DETUNE_LEVEL 94 - #define MIDI_CC_ALL_SOUND_OFF 120 - #define MIDI_CC_ALL_NOTES_OFF 123 -#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 @@ -302,7 +281,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign } if (nLength == 3) { - m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]); + m_pUI->UIMIDICmdHandler (ucChannel, ucType, pMessage[1], pMessage[2]); } break; @@ -312,7 +291,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign { break; } - m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]); + m_pUI->UIMIDICmdHandler (ucChannel, ucType, pMessage[1], pMessage[2]); break; case MIDI_PROGRAM_CHANGE: diff --git a/src/uibuttons.cpp b/src/uibuttons.cpp index ae206dc..06eb857 100644 --- a/src/uibuttons.cpp +++ b/src/uibuttons.cpp @@ -22,6 +22,7 @@ #include #include #include +#include "midi.h" LOGMODULE ("uibuttons"); @@ -490,22 +491,22 @@ void CUIButtons::ResetButton (unsigned pinNumber) } } -void CUIButtons::BtnMIDICmdHandler (unsigned nMidiCmd, unsigned nMidiData1, unsigned nMidiData2) +void CUIButtons::BtnMIDICmdHandler (unsigned nMidiType, unsigned nMidiData1, unsigned nMidiData2) { if (m_notesMidi > 0) { -// LOGDBG("BtnMIDICmdHandler (notes): %x %x %x)", nMidiCmd, nMidiData1, nMidiData2); +// LOGDBG("BtnMIDICmdHandler (notes): %x %x %x)", nMidiType, nMidiData1, nMidiData2); // Using MIDI Note messages for MIDI buttons unsigned midiPin = ccToMidiPin(nMidiData1); for (unsigned i=0; iUIButtonsEventHandler (Event); } -void CUserInterface::UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiCmd, unsigned nMidiData1, unsigned nMidiData2) +void CUserInterface::UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiType, unsigned nMidiData1, unsigned nMidiData2) { if (m_nMIDIButtonCh == CMIDIDevice::Disabled) { @@ -411,7 +411,7 @@ void CUserInterface::UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiCmd, unsi if (m_pUIButtons) { - m_pUIButtons->BtnMIDICmdHandler (nMidiCmd, nMidiData1, nMidiData2); + m_pUIButtons->BtnMIDICmdHandler (nMidiType, nMidiData1, nMidiData2); } } diff --git a/src/userinterface.h b/src/userinterface.h index a8026db..e61f621 100644 --- a/src/userinterface.h +++ b/src/userinterface.h @@ -55,7 +55,7 @@ public: bool bArrowDown, bool bArrowUp); // To be called from the MIDI device on reception of a MIDI CC message - void UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiCmd, unsigned nMidiData1, unsigned nMidiData2); + void UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiType, unsigned nMidiData1, unsigned nMidiData2); private: void LCDWrite (const char *pString); // Print to optional HD44780 display From acf9e11d5f9f60e188635de1714064fc00aa02f7 Mon Sep 17 00:00:00 2001 From: soyer Date: Mon, 21 Apr 2025 23:39:42 +0200 Subject: [PATCH 13/27] Use SoundFormatSigned24_32 with NEON (#852) * use SoundFormatSigned24_32 format instead of SoundFormatSigned16 More detailed, and not much slower. * fix ARM_MATH_NEON defines --------- Co-authored-by: probonopd --- src/Makefile | 1 + src/Synth_Dexed.mk | 3 +- src/arm_float_to_q23.c | 88 ++++++++++++++++++++++++++++++++++++++++++ src/arm_float_to_q23.h | 22 +++++++++++ src/minidexed.cpp | 19 ++++----- 5 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 src/arm_float_to_q23.c create mode 100644 src/arm_float_to_q23.h diff --git a/src/Makefile b/src/Makefile index 7882018..73dbddc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -10,6 +10,7 @@ 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 \ + arm_float_to_q23.o \ net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o net/mdnspublisher.o udpmididevice.o OPTIMIZE = -O3 diff --git a/src/Synth_Dexed.mk b/src/Synth_Dexed.mk index 6aa4a49..4d42e67 100644 --- a/src/Synth_Dexed.mk +++ b/src/Synth_Dexed.mk @@ -40,8 +40,9 @@ INCLUDE += -I $(CMSIS_DSP_COMPUTELIB_INCLUDE_DIR) DEFINE += -DUSE_FX -ifeq ($(strip $(AARCH)),64) +ifeq ($(RPI), $(filter $(RPI), 3 4 5)) DEFINE += -DARM_MATH_NEON +DEFINE += -DARM_MATH_NEON_EXPERIMENTAL DEFINE += -DHAVE_NEON endif diff --git a/src/arm_float_to_q23.c b/src/arm_float_to_q23.c new file mode 100644 index 0000000..8eb21be --- /dev/null +++ b/src/arm_float_to_q23.c @@ -0,0 +1,88 @@ +#include "arm_float_to_q23.h" + +#if defined(ARM_MATH_NEON_EXPERIMENTAL) +void arm_float_to_q23(const float32_t * pSrc, q23_t * pDst, uint32_t blockSize) +{ + const float32_t *pIn = pSrc; /* Src pointer */ + uint32_t blkCnt; /* loop counter */ + + float32x4_t inV; + + int32x4_t cvt; + + blkCnt = blockSize >> 2U; + + /* Compute 4 outputs at a time. + ** a second loop below computes the remaining 1 to 3 samples. */ + while (blkCnt > 0U) + { + /* C = A * 8388608 */ + /* Convert from float to q23 and then store the results in the destination buffer */ + inV = vld1q_f32(pIn); + + cvt = vcvtq_n_s32_f32(inV, 23); + + /* saturate */ + cvt = vminq_s32(cvt, vdupq_n_s32(0x007fffff)); + cvt = vmaxq_s32(cvt, vdupq_n_s32(0xff800000)); + + vst1q_s32(pDst, cvt); + pDst += 4; + pIn += 4; + + /* Decrement the loop counter */ + blkCnt--; + } + + /* If the blockSize is not a multiple of 4, compute any remaining output samples here. + ** No loop unrolling is used. */ + blkCnt = blockSize & 3; + + while (blkCnt > 0U) + { + /* C = A * 8388608 */ + /* Convert from float to q23 and then store the results in the destination buffer */ + *pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); + + /* Decrement the loop counter */ + blkCnt--; + } +} +#else +void arm_float_to_q23(const float32_t * pSrc, q23_t * pDst, uint32_t blockSize) +{ + uint32_t blkCnt; /* Loop counter */ + const float32_t *pIn = pSrc; /* Source pointer */ + + /* Loop unrolling: Compute 4 outputs at a time */ + blkCnt = blockSize >> 2U; + + while (blkCnt > 0U) + { + /* C = A * 8388608 */ + /* convert from float to Q23 and store result in destination buffer */ + + *pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); + *pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); + *pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); + *pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); + + /* Decrement loop counter */ + blkCnt--; + } + + /* Loop unrolling: Compute remaining outputs */ + blkCnt = blockSize % 0x4U; + + while (blkCnt > 0U) + { + /* C = A * 8388608 */ + /* Convert from float to q23 and then store the results in the destination buffer */ + *pDst++ = (q23_t) __SSAT((q31_t) (*pIn++ * 8388608.0f), 24); + + /* Decrement loop counter */ + blkCnt--; + } + +} +#endif /* #if defined(ARM_MATH_NEON_EXPERIMENTAL) */ diff --git a/src/arm_float_to_q23.h b/src/arm_float_to_q23.h new file mode 100644 index 0000000..6a77ea8 --- /dev/null +++ b/src/arm_float_to_q23.h @@ -0,0 +1,22 @@ +#pragma once + +#include "arm_math_types.h" + +typedef int32_t q23_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * @brief Converts the elements of the floating-point vector to Q23 vector. + * @param[in] pSrc points to the floating-point input vector + * @param[out] pDst points to the Q23 output vector + * @param[in] blockSize length of the input vector + */ +void arm_float_to_q23(const float32_t * pSrc, q23_t * pDst, uint32_t blockSize); + +#ifdef __cplusplus +} +#endif diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 1cca446..cbed9f9 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -29,6 +29,7 @@ #include #include #include +#include "arm_float_to_q23.h" const char WLANFirmwarePath[] = "SD:firmware/"; const char WLANConfigFile[] = "SD:wpa_supplicant.conf"; @@ -359,7 +360,7 @@ bool CMiniDexed::Initialize (void) return false; } - m_pSoundDevice->SetWriteFormat (SoundFormatSigned16, Channels); + m_pSoundDevice->SetWriteFormat (SoundFormatSigned24_32, Channels); m_nQueueSizeFrames = m_pSoundDevice->GetQueueSizeFrames (); @@ -1260,8 +1261,8 @@ void CMiniDexed::ProcessSound (void) m_pTG[0]->getSamples (SampleBuffer, nFrames); // Convert single float array (mono) to int16 array - int16_t tmp_int[nFrames]; - arm_float_to_q15(SampleBuffer,tmp_int,nFrames); + int32_t tmp_int[nFrames]; + arm_float_to_q23(SampleBuffer,tmp_int,nFrames); if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int)) { @@ -1328,7 +1329,7 @@ void CMiniDexed::ProcessSound (void) // 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]; + int32_t tmp_int[nFrames*Channels]; if(nMasterVolume > 0.0) { @@ -1350,11 +1351,11 @@ void CMiniDexed::ProcessSound (void) } } } - arm_float_to_q15(tmp_float,tmp_int,nFrames*Channels); + arm_float_to_q23(tmp_float,tmp_int,nFrames*Channels); } else { - arm_fill_q15(0, tmp_int, nFrames*Channels); + arm_fill_q31(0, tmp_int, nFrames*Channels); } // Prevent PCM510x analog mute from kicking in @@ -1378,7 +1379,7 @@ void CMiniDexed::ProcessSound (void) // BEGIN TG mixing float32_t tmp_float[nFrames*2]; - int16_t tmp_int[nFrames*2]; + int32_t tmp_int[nFrames*2]; if(nMasterVolume > 0.0) { @@ -1444,11 +1445,11 @@ void CMiniDexed::ProcessSound (void) tmp_float[(i*2)+1]=SampleBuffer[indexR][i]; } } - arm_float_to_q15(tmp_float,tmp_int,nFrames*2); + arm_float_to_q23(tmp_float,tmp_int,nFrames*2); } else { - arm_fill_q15(0, tmp_int, nFrames * 2); + arm_fill_q31(0, tmp_int, nFrames * 2); } // Prevent PCM510x analog mute from kicking in From 23a3730d5099c7e592d302820e043fa8f78fb873 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 19:03:05 +0200 Subject: [PATCH 14/27] NetworkSyslogEnabled and minor network related fixes (#876) Add `NetworkSyslogEnabled=1` to `minidexed.ini`, increase vebosity, add `syslogserver.py` --- src/config.cpp | 2 +- src/mididevice.cpp | 1 + src/minidexed.cpp | 9 +++++++ src/minidexed.ini | 1 + src/net/mdnspublisher.cpp | 6 +++++ submod.sh | 8 +++--- syslogserver.py | 57 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 syslogserver.py diff --git a/src/config.cpp b/src/config.cpp index 672d7cc..00d4d2d 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -209,7 +209,7 @@ void CConfig::Load (void) 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_bSyslogEnabled = m_Properties.GetNumber ("NetworkSyslogEnabled", 0) != 0; m_INetworkDNSServer = m_Properties.GetIPAddress("NetworkDNSServer") != 0; const u8 *pSyslogServerIP = m_Properties.GetIPAddress ("NetworkSyslogServerIPAddress"); diff --git a/src/mididevice.cpp b/src/mididevice.cpp index e13709f..c5e23ae 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -265,6 +265,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign else { // Ignore any other CC messages at this time + LOGNOTE("Ignoring CC %d (%d) on Performance Select Channel %d\n", pMessage[1], pMessage[2], nPerfCh); } } } diff --git a/src/minidexed.cpp b/src/minidexed.cpp index cbed9f9..7664e99 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -2307,6 +2307,7 @@ void CMiniDexed::UpdateNetwork() if (m_pConfig->GetSyslogEnabled()) { + LOGNOTE ("Syslog server is enabled in configuration"); CIPAddress ServerIP = m_pConfig->GetNetworkSyslogServerIPAddress(); if (ServerIP.IsSet () && !ServerIP.IsNull ()) { @@ -2318,6 +2319,14 @@ void CMiniDexed::UpdateNetwork() new CSysLogDaemon (m_pNet, ServerIP, usServerPort); } + else + { + LOGNOTE ("Syslog server IP not set"); + } + } + else + { + LOGNOTE ("Syslog server is not enabled in configuration"); } m_bNetworkReady = true; } diff --git a/src/minidexed.ini b/src/minidexed.ini index 2291f4f..d7de8ce 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -161,6 +161,7 @@ NetworkIPAddress=0 NetworkSubnetMask=0 NetworkDefaultGateway=0 NetworkDNSServer=0 +NetworkSyslogEnabled=1 NetworkSyslogServerIPAddress=0 # Performance diff --git a/src/net/mdnspublisher.cpp b/src/net/mdnspublisher.cpp index 23052db..fbeb549 100644 --- a/src/net/mdnspublisher.cpp +++ b/src/net/mdnspublisher.cpp @@ -117,10 +117,13 @@ boolean CmDNSPublisher::UnpublishService (const char *pServiceName) return FALSE; } LOGDBG ("Unpublish service %s", (const char *) pService->ServiceName); + SendResponse (pService, FALSE); + /* if (!SendResponse (pService, TRUE)) { LOGWARN ("Send failed"); } + */ for (unsigned i = 0; i < pService->nTextRecords; i++) { delete pService->ppText[i]; @@ -172,10 +175,13 @@ void CmDNSPublisher::Run (void) TService *pService = static_cast (CPtrList::GetPtr (pElement)); assert (pService); + SendResponse (pService, FALSE); + /* if (!SendResponse (pService, FALSE)) { LOGWARN ("Send failed"); } + */ pElement = m_ServiceList.GetNext (pElement); } m_Mutex.Release (); diff --git a/submod.sh b/submod.sh index 6685bef..f9d3251 100755 --- a/submod.sh +++ b/submod.sh @@ -2,13 +2,13 @@ set -ex # Update top-level modules as a baseline -git submodule update --init --recursive +git submodule update --init --recursive -f # Use fixed master branch of circle-stdlib then re-update cd circle-stdlib/ git reset --hard -git checkout 1111eee # Matches Circle Step49 -git submodule update --init --recursive +git checkout 1111eee -f # Matches Circle Step49 +git submodule update --init --recursive -f cd - # Optional update submodules explicitly @@ -23,5 +23,5 @@ cd - # Use fixed master branch of Synth_Dexed cd Synth_Dexed/ git reset --hard -git checkout c9f5274 +git checkout c9f5274 -f cd - diff --git a/syslogserver.py b/syslogserver.py new file mode 100644 index 0000000..c07c1ea --- /dev/null +++ b/syslogserver.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Syslog server to receive and display syslog messages from MiniDexed. +""" + +import socket +import time +import threading + +class SyslogServer: + def __init__(self, host='0.0.0.0', port=8514): + self.host = host + self.port = port + self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.server.bind((self.host, self.port)) + self.start_time = None + self.running = True + + def start(self): + ip_address = socket.gethostbyname(socket.gethostname()) + print(f"Syslog server listening on {ip_address}:{self.port}") + input_thread = threading.Thread(target=self.wait_for_input) + input_thread.daemon = True + input_thread.start() + while self.running: + try: + data, address = self.server.recvfrom(1024) + self.handle_message(data) + except KeyboardInterrupt: + self.running = False + + def handle_message(self, data): + message = data[2:].decode('utf-8').strip() + + if self.start_time is None: + self.start_time = time.time() + relative_time = "0:00:00.000" + else: + elapsed_time = time.time() - self.start_time + hours = int(elapsed_time // 3600) + minutes = int((elapsed_time % 3600) // 60) + seconds = int(elapsed_time % 60) + milliseconds = int((elapsed_time % 1) * 1000) + relative_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}" + + print(f"{relative_time} {message}") + + def wait_for_input(self): + input("Press any key to exit...") + self.running = False + +if __name__ == "__main__": + server = SyslogServer() + server.start() + print("Syslog server stopped.") \ No newline at end of file From c0d73e362976f1d6d3363be23e03d8e49134f585 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 19:05:46 +0200 Subject: [PATCH 15/27] NetworkSyslogEnabled=0 by default [ci skip] --- src/minidexed.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/minidexed.ini b/src/minidexed.ini index d7de8ce..7d0fd31 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -161,7 +161,7 @@ NetworkIPAddress=0 NetworkSubnetMask=0 NetworkDefaultGateway=0 NetworkDNSServer=0 -NetworkSyslogEnabled=1 +NetworkSyslogEnabled=0 NetworkSyslogServerIPAddress=0 # Performance From 22a7d54251b10d7311f730ae7a7417915ef58e69 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 19:51:08 +0200 Subject: [PATCH 16/27] Put all files into 64bit artifact (#873) Closes https://github.com/probonopd/MiniDexed/discussions/869#discussioncomment-12905515 --- .github/workflows/build.yml | 67 +++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a17d88d..5248d4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,8 @@ -# Build 32-bit and 64-bit separately - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_INFO: "" on: push: @@ -19,14 +18,16 @@ jobs: artifact-path: ${{ steps.upload64.outputs.artifact-path }} steps: - uses: actions/checkout@v2 + - name: Compute Git Info for Artifact Name + run: echo "GIT_INFO=$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - 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 - + sed -i "s/Loading.../${{ env.GIT_INFO }}/g" src/userinterface.cpp + # Install 64-bit toolchain (aarch64) - name: Install 64-bit toolchain run: | @@ -65,15 +66,30 @@ jobs: 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 ./src/config.txt ./src/minidexed.ini ./src/performance.ini sdcard/ cp ./getsysex.sh sdcard/ echo "usbspeed=full" > sdcard/cmdline.txt - + # Performances + git clone https://github.com/Banana71/Soundplantage --depth 1 # depth 1 means only the latest commit + cp -r ./Soundplantage/performance ./Soundplantage/*.pdf ./sdcard/ + # Hardware configuration + cd hwconfig + sh -ex ./customize.sh + cd - + mkdir -p ./sdcard/hardware/ + cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/ + # WLAN firmware + mkdir -p sdcard/firmware + cp circle-stdlib/libs/circle/addon/wlan/sample/hello_wlan/wpa_supplicant.conf sdcard/ + cd sdcard/firmware + make -f ../../circle-stdlib/libs/circle/addon/wlan/firmware/Makefile + cd - + - name: Upload 64-bit artifacts id: upload64 uses: actions/upload-artifact@v4 with: - name: build64-artifacts + name: MiniDexed_${{ github.run_number }}_${{ env.GIT_INFO }}_64bit path: sdcard/* build32: @@ -83,14 +99,16 @@ jobs: artifact-path: ${{ steps.upload32.outputs.artifact-path }} steps: - uses: actions/checkout@v2 + - name: Compute Git Info for Artifact Name + run: echo "GIT_INFO=$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - 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 - + sed -i "s/Loading.../${{ env.GIT_INFO }}/g" src/userinterface.cpp + # Install 32-bit toolchain (arm-none-eabi) - name: Install 32-bit toolchain run: | @@ -116,7 +134,7 @@ jobs: id: upload32 uses: actions/upload-artifact@v4 with: - name: build32-artifacts + name: MiniDexed_${{ github.run_number }}_${{ env.GIT_INFO }}_32bit path: sdcard/* combine: @@ -124,32 +142,23 @@ jobs: runs-on: ubuntu-22.04 needs: [ build64, build32 ] steps: - - name: Download 64-bit artifacts - uses: actions/download-artifact@v4 - with: - name: build64-artifacts - path: combined - - name: Download 32-bit artifacts + - name: Download artifacts uses: actions/download-artifact@v4 with: - name: build32-artifacts + pattern: MiniDexed_* + merge-multiple: true 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 . + zip -r ../MiniDexed_${{ github.run_number }}_${{ env.GIT_INFO }}.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 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 + 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 b383f4ce39409fa25c6b7c9ea5b29c7d5b7e89a2 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 20:04:25 +0200 Subject: [PATCH 17/27] Fix filename on GitHub Releases --- .github/workflows/build.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5248d4b..795e189 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,6 @@ name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_INFO: "" on: push: @@ -10,23 +9,28 @@ on: pull_request: jobs: - build64: name: Build 64-bit kernels runs-on: ubuntu-22.04 outputs: artifact-path: ${{ steps.upload64.outputs.artifact-path }} + git_info: ${{ steps.gitinfo.outputs.git_info }} steps: - uses: actions/checkout@v2 + - name: Compute Git Info for Artifact Name - run: echo "GIT_INFO=$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV + id: gitinfo + run: echo "::set-output name=git_info::$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" + - 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.../${{ env.GIT_INFO }}/g" src/userinterface.cpp + sed -i "s/Loading.../${{ steps.gitinfo.outputs.git_info }}/g" src/userinterface.cpp # Install 64-bit toolchain (aarch64) - name: Install 64-bit toolchain @@ -70,7 +74,7 @@ jobs: cp ./getsysex.sh sdcard/ echo "usbspeed=full" > sdcard/cmdline.txt # Performances - git clone https://github.com/Banana71/Soundplantage --depth 1 # depth 1 means only the latest commit + git clone https://github.com/Banana71/Soundplantage --depth 1 cp -r ./Soundplantage/performance ./Soundplantage/*.pdf ./sdcard/ # Hardware configuration cd hwconfig @@ -89,9 +93,9 @@ jobs: id: upload64 uses: actions/upload-artifact@v4 with: - name: MiniDexed_${{ github.run_number }}_${{ env.GIT_INFO }}_64bit + name: MiniDexed_${{ github.run_number }}_${{ steps.gitinfo.outputs.git_info }}_64bit path: sdcard/* - + build32: name: Build 32-bit kernels runs-on: ubuntu-22.04 @@ -99,12 +103,16 @@ jobs: artifact-path: ${{ steps.upload32.outputs.artifact-path }} steps: - uses: actions/checkout@v2 + - name: Compute Git Info for Artifact Name run: echo "GIT_INFO=$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV + - 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.../${{ env.GIT_INFO }}/g" src/userinterface.cpp @@ -148,11 +156,13 @@ jobs: pattern: MiniDexed_* merge-multiple: true path: combined + - name: Create combined ZIP file run: | cd combined - zip -r ../MiniDexed_${{ github.run_number }}_${{ env.GIT_INFO }}.zip . + zip -r ../MiniDexed_${{ github.run_number }}_${{ needs.build64.outputs.git_info }}.zip . cd .. + - name: Upload to GitHub Releases (only when building from main branch) if: ${{ github.ref == 'refs/heads/main' }} run: | From 26e258b25a36d5b43ffff515763481b5558032dc Mon Sep 17 00:00:00 2001 From: soyer Date: Tue, 22 Apr 2025 20:14:41 +0200 Subject: [PATCH 18/27] Update Synth_Dexed to 65d8383ad5 (#871) * use std::min and std::max in Compressor * update Synth_Dexed to 65d8383ad5 Contains various fixes: https://codeberg.org/dcoredump/Synth_Dexed/pulls/11 https://codeberg.org/dcoredump/Synth_Dexed/pulls/12 https://codeberg.org/dcoredump/Synth_Dexed/pulls/13 And sostenuto/hold: https://codeberg.org/dcoredump/Synth_Dexed/pulls/10 https://codeberg.org/dcoredump/Synth_Dexed/pulls/7 * use the corrected named functions of Synth_Dexed https://codeberg.org/dcoredump/Synth_Dexed/pulls/12 --- src/effect_compressor.cpp | 9 +++++---- src/minidexed.cpp | 4 ++-- submod.sh | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/effect_compressor.cpp b/src/effect_compressor.cpp index d2aaec7..1b35c26 100644 --- a/src/effect_compressor.cpp +++ b/src/effect_compressor.cpp @@ -12,6 +12,7 @@ MIT License. use at your own risk. */ +#include #include #include #include "effect_compressor.h" @@ -203,7 +204,7 @@ void Compressor::setPreGain_dB(float32_t gain_dB) void Compressor::setCompressionRatio(float32_t cr) { - comp_ratio = max(0.001f, cr); //limit to positive values + comp_ratio = std::max(0.001f, cr); //limit to positive values updateThresholdAndCompRatioConstants(); } @@ -213,7 +214,7 @@ void Compressor::setAttack_sec(float32_t a, float32_t fs_Hz) attack_const = expf(-1.0f / (attack_sec * fs_Hz)); //expf() is much faster than exp() //also update the time constant for the envelope extraction - setLevelTimeConst_sec(min(attack_sec,release_sec) / 5.0, fs_Hz); //make the level time-constant one-fifth the gain time constants + setLevelTimeConst_sec(std::min(attack_sec,release_sec) / 5.0, fs_Hz); //make the level time-constant one-fifth the gain time constants } void Compressor::setRelease_sec(float32_t r, float32_t fs_Hz) @@ -222,13 +223,13 @@ void Compressor::setRelease_sec(float32_t r, float32_t fs_Hz) release_const = expf(-1.0f / (release_sec * fs_Hz)); //expf() is much faster than exp() //also update the time constant for the envelope extraction - setLevelTimeConst_sec(min(attack_sec,release_sec) / 5.0, fs_Hz); //make the level time-constant one-fifth the gain time constants + setLevelTimeConst_sec(std::min(attack_sec,release_sec) / 5.0, fs_Hz); //make the level time-constant one-fifth the gain time constants } void Compressor::setLevelTimeConst_sec(float32_t t_sec, float32_t fs_Hz) { const float32_t min_t_sec = 0.002f; //this is the minimum allowed value - level_lp_sec = max(min_t_sec,t_sec); + level_lp_sec = std::max(min_t_sec,t_sec); level_lp_const = expf(-1.0f / (level_lp_sec * fs_Hz)); //expf() is much faster than exp() } diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 7664e99..de48840 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -1237,7 +1237,7 @@ std::string CMiniDexed::GetVoiceName (unsigned nTG) if (nTG < m_nToneGenerators) { assert (m_pTG[nTG]); - m_pTG[nTG]->setName (VoiceName); + m_pTG[nTG]->getName (VoiceName); } std::string Result (VoiceName); return Result; @@ -2078,7 +2078,7 @@ void CMiniDexed::SetVoiceName (const std::string &VoiceName, unsigned nTG) char Name[11]; strncpy(Name, VoiceName.c_str(),10); Name[10] = '\0'; - m_pTG[nTG]->getName (Name); + m_pTG[nTG]->setName (Name); } bool CMiniDexed::DeletePerformance(unsigned nID) diff --git a/submod.sh b/submod.sh index f9d3251..6ef767a 100755 --- a/submod.sh +++ b/submod.sh @@ -23,5 +23,5 @@ cd - # Use fixed master branch of Synth_Dexed cd Synth_Dexed/ git reset --hard -git checkout c9f5274 -f +git checkout 65d8383ad5 -f cd - From bebf9cec95c6cd6b3271b24390c0197d6c042c2a Mon Sep 17 00:00:00 2001 From: soyer Date: Tue, 22 Apr 2025 20:15:51 +0200 Subject: [PATCH 19/27] Fix op ordering and reenabling (#870) * fix op ordering The ops are in reverse order in the OPMask as well * reset OPMask after voice load If you change a voice, Dexed enables all operator. Do the same in MiniDexed. --- src/minidexed.cpp | 25 +++++++++++++++++-------- src/minidexed.h | 1 + 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index de48840..ebe9e1b 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -658,6 +658,7 @@ void CMiniDexed::ProgramChange (unsigned nProgram, unsigned nTG) assert (m_pTG[nTG]); m_pTG[nTG]->loadVoiceParameters (Buffer); + setOPMask(0b111111, nTG); if (m_pConfig->GetMIDIAutoVoiceDumpOnPC()) { @@ -1178,23 +1179,22 @@ void CMiniDexed::SetVoiceParameter (uint8_t uchOffset, uint8_t uchValue, unsigne if (nOP < 6) { + nOP = 5 - nOP; // OPs are in reverse order + if (uchOffset == DEXED_OP_ENABLE) { if (uchValue) { - m_uchOPMask[nTG] |= 1 << nOP; + setOPMask(m_uchOPMask[nTG] | 1 << nOP, nTG); } else { - m_uchOPMask[nTG] &= ~(1 << nOP); + setOPMask(m_uchOPMask[nTG] & ~(1 << nOP), nTG); } - m_pTG[nTG]->setOPAll (m_uchOPMask[nTG]); return; - } - - nOP = 5 - nOP; // OPs are in reverse order + } } uchOffset += nOP * 21; @@ -1213,12 +1213,12 @@ uint8_t CMiniDexed::GetVoiceParameter (uint8_t uchOffset, unsigned nOP, unsigned if (nOP < 6) { + nOP = 5 - nOP; // OPs are in reverse order + if (uchOffset == DEXED_OP_ENABLE) { return !!(m_uchOPMask[nTG] & (1 << nOP)); } - - nOP = 5 - nOP; // OPs are in reverse order } uchOffset += nOP * 21; @@ -1792,6 +1792,8 @@ void CMiniDexed::loadVoiceParameters(const uint8_t* data, uint8_t nTG) m_pTG[nTG]->loadVoiceParameters(&voice[6]); m_pTG[nTG]->doRefreshVoice(); + setOPMask(0b111111, nTG); + m_UI.ParameterChanged (); } @@ -1848,6 +1850,12 @@ void CMiniDexed::getSysExVoiceDump(uint8_t* dest, uint8_t nTG) dest[162] = 0xF7; // SysEx end } +void CMiniDexed::setOPMask(uint8_t uchOPMask, uint8_t nTG) +{ + m_uchOPMask[nTG] = uchOPMask; + m_pTG[nTG]->setOPAll (m_uchOPMask[nTG]); +} + void CMiniDexed::setMasterVolume(float32_t vol) { if (vol < 0.0) @@ -2022,6 +2030,7 @@ void CMiniDexed::LoadPerformanceParameters(void) { uint8_t* tVoiceData = m_PerformanceConfig.GetVoiceDataFromTxt(nTG); m_pTG[nTG]->loadVoiceParameters(tVoiceData); + setOPMask(0b111111, nTG); } setMonoMode(m_PerformanceConfig.GetMonoMode(nTG) ? 1 : 0, nTG); SetReverbSend (m_PerformanceConfig.GetReverbSend (nTG), nTG); diff --git a/src/minidexed.h b/src/minidexed.h index b382f7e..4193ad8 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -122,6 +122,7 @@ public: void loadVoiceParameters(const uint8_t* data, uint8_t nTG); void setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG); void getSysExVoiceDump(uint8_t* dest, uint8_t nTG); + void setOPMask(uint8_t uchOPMask, uint8_t nTG); void setModController (unsigned controller, unsigned parameter, uint8_t value, uint8_t nTG); unsigned getModController (unsigned controller, unsigned parameter, uint8_t nTG); From d68e1a87390b288a0180404e1e47e73efb15f6ed Mon Sep 17 00:00:00 2001 From: soyer Date: Tue, 22 Apr 2025 20:25:45 +0200 Subject: [PATCH 20/27] Do not refesh the display unnecessarily when updating a parameter (#874) --- src/minidexed.cpp | 2 ++ src/uimenu.cpp | 7 +++++++ src/uimenu.h | 1 + src/userinterface.cpp | 5 +++++ src/userinterface.h | 1 + 5 files changed, 16 insertions(+) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index ebe9e1b..674fbb3 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -2056,6 +2056,8 @@ void CMiniDexed::LoadPerformanceParameters(void) SetParameter (ParameterReverbLowPass, m_PerformanceConfig.GetReverbLowPass ()); SetParameter (ParameterReverbDiffusion, m_PerformanceConfig.GetReverbDiffusion ()); SetParameter (ParameterReverbLevel, m_PerformanceConfig.GetReverbLevel ()); + + m_UI.DisplayChanged (); } std::string CMiniDexed::GetNewPerformanceDefaultName(void) diff --git a/src/uimenu.cpp b/src/uimenu.cpp index d7ef12a..5bf7285 100644 --- a/src/uimenu.cpp +++ b/src/uimenu.cpp @@ -563,6 +563,7 @@ void CUIMenu::EditGlobalParameter (CUIMenu *pUIMenu, TMenuEvent Event) switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: @@ -608,6 +609,7 @@ void CUIMenu::EditVoiceBankNumber (CUIMenu *pUIMenu, TMenuEvent Event) switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: @@ -652,6 +654,7 @@ void CUIMenu::EditProgramNumber (CUIMenu *pUIMenu, TMenuEvent Event) switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: @@ -741,6 +744,7 @@ void CUIMenu::EditTGParameter (CUIMenu *pUIMenu, TMenuEvent Event) switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: @@ -794,6 +798,7 @@ void CUIMenu::EditTGParameter2 (CUIMenu *pUIMenu, TMenuEvent Event) // second me switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: @@ -847,6 +852,7 @@ void CUIMenu::EditVoiceParameter (CUIMenu *pUIMenu, TMenuEvent Event) switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: @@ -900,6 +906,7 @@ void CUIMenu::EditOPParameter (CUIMenu *pUIMenu, TMenuEvent Event) switch (Event) { case MenuEventUpdate: + case MenuEventUpdateParameter: break; case MenuEventStepDown: diff --git a/src/uimenu.h b/src/uimenu.h index d9dc3ee..6ff6571 100644 --- a/src/uimenu.h +++ b/src/uimenu.h @@ -39,6 +39,7 @@ public: enum TMenuEvent { MenuEventUpdate, + MenuEventUpdateParameter, MenuEventSelect, MenuEventBack, MenuEventHome, diff --git a/src/userinterface.cpp b/src/userinterface.cpp index edb5687..77b75f4 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -211,6 +211,11 @@ void CUserInterface::Process (void) } void CUserInterface::ParameterChanged (void) +{ + m_Menu.EventHandler (CUIMenu::MenuEventUpdateParameter); +} + +void CUserInterface::DisplayChanged (void) { m_Menu.EventHandler (CUIMenu::MenuEventUpdate); } diff --git a/src/userinterface.h b/src/userinterface.h index e61f621..6290d86 100644 --- a/src/userinterface.h +++ b/src/userinterface.h @@ -45,6 +45,7 @@ public: void Process (void); void ParameterChanged (void); + void DisplayChanged (void); // Write to display in this format: // +----------------+ From f6882a922a0b2af023ae86ca190e7bc5ca792e93 Mon Sep 17 00:00:00 2001 From: soyer Date: Tue, 22 Apr 2025 20:51:10 +0200 Subject: [PATCH 21/27] Add Portamento, Sostenuto, Hold2 MIDI CC-s (#878) * handle Portamento MIDI CC-s * add sostenuto * add hold mode --- src/midi.h | 4 ++++ src/mididevice.cpp | 16 ++++++++++++++++ src/minidexed.cpp | 18 ++++++++++++++++++ src/minidexed.h | 2 ++ 4 files changed, 40 insertions(+) diff --git a/src/midi.h b/src/midi.h index 4538df7..e106a3a 100644 --- a/src/midi.h +++ b/src/midi.h @@ -30,11 +30,15 @@ #define MIDI_CC_MODULATION 1 #define MIDI_CC_BREATH_CONTROLLER 2 #define MIDI_CC_FOOT_PEDAL 4 +#define MIDI_CC_PORTAMENTO_TIME 5 #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_PORTAMENTO 65 +#define MIDI_CC_SOSTENUTO 66 +#define MIDI_CC_HOLD2 69 #define MIDI_CC_RESONANCE 71 #define MIDI_CC_FREQUENCY_CUTOFF 74 #define MIDI_CC_REVERB_LEVEL 91 diff --git a/src/mididevice.cpp b/src/mididevice.cpp index c5e23ae..7d3b4a0 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -386,6 +386,10 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign m_pSynthesizer->ControllersRefresh (nTG); break; + case MIDI_CC_PORTAMENTO_TIME: + m_pSynthesizer->setPortamentoTime (maplong (pMessage[2], 0, 127, 0, 99), nTG); + break; + case MIDI_CC_BREATH_CONTROLLER: m_pSynthesizer->setBreathController (pMessage[2], nTG); m_pSynthesizer->ControllersRefresh (nTG); @@ -417,6 +421,18 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign case MIDI_CC_BANK_SUSTAIN: m_pSynthesizer->setSustain (pMessage[2] >= 64, nTG); break; + + case MIDI_CC_SOSTENUTO: + m_pSynthesizer->setSostenuto (pMessage[2] >= 64, nTG); + break; + + case MIDI_CC_PORTAMENTO: + m_pSynthesizer->setPortamentoMode (pMessage[2] >= 64, nTG); + break; + + case MIDI_CC_HOLD2: + m_pSynthesizer->setHoldMode (pMessage[2] >= 64, nTG); + break; case MIDI_CC_RESONANCE: m_pSynthesizer->SetResonance (maplong (pMessage[2], 0, 127, 0, 99), nTG); diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 674fbb3..0063b11 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -899,6 +899,24 @@ void CMiniDexed::setSustain(bool sustain, unsigned nTG) m_pTG[nTG]->setSustain (sustain); } +void CMiniDexed::setSostenuto(bool sostenuto, unsigned nTG) +{ + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + + assert (m_pTG[nTG]); + m_pTG[nTG]->setSostenuto (sostenuto); +} + +void CMiniDexed::setHoldMode(bool holdmode, unsigned nTG) +{ + assert (nTG < CConfig::AllToneGenerators); + if (nTG >= m_nToneGenerators) return; // Not an active TG + + assert (m_pTG[nTG]); + m_pTG[nTG]->setHold (holdmode); +} + void CMiniDexed::panic(uint8_t value, unsigned nTG) { assert (nTG < CConfig::AllToneGenerators); diff --git a/src/minidexed.h b/src/minidexed.h index 4193ad8..b1622d0 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -93,6 +93,8 @@ public: void keydown (int16_t pitch, uint8_t velocity, unsigned nTG); void setSustain (bool sustain, unsigned nTG); + void setSostenuto (bool sostenuto, unsigned nTG); + void setHoldMode(bool holdmode, unsigned nTG); void panic (uint8_t value, unsigned nTG); void notesOff (uint8_t value, unsigned nTG); void setModWheel (uint8_t value, unsigned nTG); From 389708fdadeff49091f9d72cf215762a1ff5075d Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 21:43:02 +0200 Subject: [PATCH 22/27] Put git hash in startup message w/o "-" Because there is no space for them --- .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 795e189..3f3a2fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - name: Put git hash in startup message run: | - sed -i "s/Loading.../${{ steps.gitinfo.outputs.git_info }}/g" src/userinterface.cpp + 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 From a22e3f375bf3059387a83c28d9ce153c283d04ae Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 21:46:31 +0200 Subject: [PATCH 23/27] Make FTP opt-in, increase timeout, protect wpa_supplicant better (#879) * Increase timeout * Protect wpa_supplicant more effectively * Make FTP opt-in with NetworkFTPEnabled --- src/config.cpp | 6 ++++++ src/config.h | 2 ++ src/minidexed.cpp | 25 +++++++++++++++---------- src/minidexed.ini | 1 + src/net/ftpworker.cpp | 18 ++++++++++++------ 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 00d4d2d..65cd68c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -211,6 +211,7 @@ void CConfig::Load (void) m_INetworkDefaultGateway = m_Properties.GetIPAddress("NetworkDefaultGateway") != 0; m_bSyslogEnabled = m_Properties.GetNumber ("NetworkSyslogEnabled", 0) != 0; m_INetworkDNSServer = m_Properties.GetIPAddress("NetworkDNSServer") != 0; + m_bNetworkFTPEnabled = m_Properties.GetNumber("NetworkFTPEnabled", 0) != 0; const u8 *pSyslogServerIP = m_Properties.GetIPAddress ("NetworkSyslogServerIPAddress"); if (pSyslogServerIP) @@ -792,3 +793,8 @@ CIPAddress CConfig::GetNetworkSyslogServerIPAddress (void) const { return m_INetworkSyslogServerIPAddress; } + +bool CConfig::GetNetworkFTPEnabled (void) const +{ + return m_bNetworkFTPEnabled; +} diff --git a/src/config.h b/src/config.h index fcb8cca..aaf2476 100644 --- a/src/config.h +++ b/src/config.h @@ -253,6 +253,7 @@ public: CIPAddress GetNetworkDNSServer (void) const; bool GetSyslogEnabled (void) const; CIPAddress GetNetworkSyslogServerIPAddress (void) const; + bool GetNetworkFTPEnabled (void) const; private: CPropertiesFatFsFile m_Properties; @@ -382,6 +383,7 @@ private: CIPAddress m_INetworkDNSServer; bool m_bSyslogEnabled; CIPAddress m_INetworkSyslogServerIPAddress; + bool m_bNetworkFTPEnabled; }; #endif diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 0063b11..134d241 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -2305,18 +2305,23 @@ void CMiniDexed::UpdateNetwork() m_UDPMIDI->Initialize(); } - m_pFTPDaemon = new CFTPDaemon(FTPUSERNAME, FTPPASSWORD); + if (m_pConfig->GetNetworkFTPEnabled()) { + 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"); + if (!m_pFTPDaemon->Initialize()) + { + LOGERR("Failed to init FTP daemon"); + delete m_pFTPDaemon; + m_pFTPDaemon = nullptr; + } + else + { + LOGNOTE("FTP daemon initialized"); + } + } else { + LOGNOTE("FTP daemon not started (NetworkFTPEnabled=0)"); } + m_UI.DisplayWrite (IPString, "", "TG1", 0, 1); m_pmDNSPublisher = new CmDNSPublisher (m_pNet); diff --git a/src/minidexed.ini b/src/minidexed.ini index 7d0fd31..c176e85 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -161,6 +161,7 @@ NetworkIPAddress=0 NetworkSubnetMask=0 NetworkDefaultGateway=0 NetworkDNSServer=0 +NetworkFTPEnabled=0 NetworkSyslogEnabled=0 NetworkSyslogServerIPAddress=0 diff --git a/src/net/ftpworker.cpp b/src/net/ftpworker.cpp index 6f19f8a..28415e2 100644 --- a/src/net/ftpworker.cpp +++ b/src/net/ftpworker.cpp @@ -40,7 +40,7 @@ constexpr u16 PassivePortBase = 9000; constexpr size_t TextBufferSize = 512; -constexpr unsigned int SocketTimeout = 20; +constexpr unsigned int SocketTimeout = 60; constexpr unsigned int NumRetries = 3; #ifndef MT32_PI_VERSION @@ -48,7 +48,7 @@ constexpr unsigned int NumRetries = 3; #endif const char MOTDBanner[] = "Welcome to the MiniDexed " MT32_PI_VERSION " embedded FTP server!"; -const char* exclude_filename = "SD:/wpa_supplicant.conf"; +const char* exclude_filename = "wpa_supplicant.conf"; enum class TDirectoryListEntryType { @@ -616,10 +616,16 @@ bool CFTPWorker::Retrieve(const char* pArgs) 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) + + // Disallow any file named wpa_supplicant.conf (case-insensitive) in any directory + const char* pathStr = Path; + const char* lastSep = nullptr; + for (const char* p = pathStr; *p; ++p) { + if (*p == '/' || *p == ':') lastSep = p; + } + const char* filename = lastSep ? lastSep + 1 : pathStr; + // Case-insensitive compare using strcasecmp if available + if (strcasecmp(filename, "wpa_supplicant.conf") == 0) { SendStatus(TFTPStatus::FileNameNotAllowed, "Reading this file is not allowed"); return false; From d8cdfe4f43202dd09151c77e3730cf03ae91fcae Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 22 Apr 2025 23:46:34 +0200 Subject: [PATCH 24/27] Updater for MiniDexed (#880) Run on your desktop computer to update MiniDexed devices on the network. --- updater.py | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 updater.py diff --git a/updater.py b/updater.py new file mode 100644 index 0000000..80c1f6c --- /dev/null +++ b/updater.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Updater for MiniDexed + +import os +import sys +import tempfile +import zipfile +import requests +import ftplib +import socket +import atexit +import re +import argparse + +try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf +except ImportError: + print("Please install the zeroconf library to use mDNS functionality.") + print("You can install it using: pip install zeroconf") + sys.exit(1) + +class MyListener(ServiceListener): + def __init__(self, ip_list, name_list): + self.ip_list = ip_list + self.name_list = name_list + + def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: + print(f"Service {name} updated") + + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + print(f"Service {name} removed") + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + info = zc.get_service_info(type_, name) + print(f"Service {name} added, service info: {info}") + if info and info.addresses: + ip = socket.inet_ntoa(info.addresses[0]) + if ip not in self.ip_list: + self.ip_list.append(ip) + self.name_list.append(info.server.rstrip('.')) + + +# Constants +TEMP_DIR = tempfile.gettempdir() + +# Register cleanup function for temp files +zip_path = None +extract_path = None +def cleanup_temp_files(): + if zip_path and os.path.exists(zip_path): + os.remove(zip_path) + if extract_path and os.path.exists(extract_path): + for root, dirs, files in os.walk(extract_path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(extract_path) + print("Cleaned up temporary files.") +atexit.register(cleanup_temp_files) + +# Function to download the latest release +def download_latest_release(url): + response = requests.get(url, stream=True) + if response.status_code == 200: + zip_path = os.path.join(TEMP_DIR, "MiniDexed_latest.zip") + with open(zip_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return zip_path + return None + +# Function to extract the downloaded zip file +def extract_zip(zip_path): + extract_path = os.path.join(TEMP_DIR, "MiniDexed") + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(extract_path) + return extract_path + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="MiniDexed Updater") + parser.add_argument("-v", action="store_true", help="Enable verbose FTP debug output") + args = parser.parse_args() + + import time + # Ask user which release to download (numbered choices) + release_options = [ + ("Latest official release", "https://github.com/probonopd/MiniDexed/releases/expanded_assets/latest"), + ("Continuous (experimental) build", "https://github.com/probonopd/MiniDexed/releases/expanded_assets/continuous") + ] + print("Which release do you want to download?") + for idx, (desc, _) in enumerate(release_options): + print(f" [{idx+1}] {desc}") + while True: + choice = input(f"Enter the number of your choice (1-{len(release_options)}): ").strip() + if choice.isdigit() and 1 <= int(choice) <= len(release_options): + github_url = release_options[int(choice)-1][1] + break + print("Invalid selection. Please enter a valid number.") + + # Using mDNS to find the IP address of the device(s) that advertise the FTP service "_ftp._tcp." + ip_addresses = [] + device_names = [] + zeroconf = Zeroconf() + listener = MyListener(ip_addresses, device_names) + browser = ServiceBrowser(zeroconf, "_ftp._tcp.local.", listener) + try: + print("Searching for devices...") + time.sleep(5) + if ip_addresses: + print("Devices found:") + for idx, (name, ip) in enumerate(zip(device_names, ip_addresses)): + print(f" [{idx+1}] {name} ({ip})") + while True: + selection = input(f"Enter the number of the device to upload to (1-{len(ip_addresses)}): ").strip() + if selection.isdigit() and 1 <= int(selection) <= len(ip_addresses): + selected_ip = ip_addresses[int(selection)-1] + selected_name = device_names[int(selection)-1] + break + print("Invalid selection. Please enter a valid number.") + else: + print("No devices found.") + sys.exit(1) + finally: + zeroconf.close() + print("Devices found:", list(zip(device_names, ip_addresses))) + + # Use the selected GitHub URL for release + def get_release_url(github_url): + print(f"Fetching release page: {github_url}") + response = requests.get(github_url) + print(f"HTTP status code: {response.status_code}") + if response.status_code == 200: + print("Successfully fetched release page. Scanning for MiniDexed*.zip links...") + # Find all tags with a MiniDexed*.zip + pattern = re.compile(r']+href=["\']([^"\']+\.zip)["\'][^>]*>\s*]*class=["\']Truncate-text text-bold["\'][^>]*>(MiniDexed[^<]*?\.zip)', re.IGNORECASE) + matches = pattern.findall(response.text) + print(f"Found {len(matches)} candidate .zip links.") + for href, filename in matches: + print(f"Examining link: href={href}, filename={filename}") + if filename.startswith("MiniDexed") and filename.endswith(".zip"): + if href.startswith('http'): + print(f"Selected direct link: {href}") + return href + else: + full_url = f"https://github.com{href}" + print(f"Selected relative link, full URL: {full_url}") + return full_url + print("No valid MiniDexed*.zip link found.") + else: + print(f"Failed to fetch release page. Status code: {response.status_code}") + return None + + latest_release_url = get_release_url(github_url) + if latest_release_url: + print(f"Release URL: {latest_release_url}") + zip_path = download_latest_release(latest_release_url) + if zip_path: + print(f"Downloaded to: {zip_path}") + extract_path = extract_zip(zip_path) + print(f"Extracted to: {extract_path}") + else: + print("Failed to download the release.") + sys.exit(1) + else: + print("Failed to get the release URL.") + sys.exit(1) + + # Log into the selected device and upload the new version of MiniDexed + print(f"Connecting to {selected_name} ({selected_ip})...") + try: + ftp = ftplib.FTP() + if args.v: + ftp.set_debuglevel(2) + ftp.connect(selected_ip, 21, timeout=10) + ftp.login("admin", "admin") + ftp.set_pasv(True) + print(f"Connected to {selected_ip} (passive mode).") + for root, dirs, files in os.walk(extract_path): + for file in files: + if file.startswith("kernel") and file.endswith(".img"): + local_path = os.path.join(root, file) + remote_path = f"/SD/{file}" + # Check if file exists on FTP server + file_exists = False + try: + ftp.cwd("/SD") + if file in ftp.nlst(): + file_exists = True + except Exception as e: + print(f"Error checking for {file} on FTP server: {e}") + file_exists = False + if not file_exists: + print(f"Skipping {file}: does not exist on device.") + continue + filesize = os.path.getsize(local_path) + uploaded = [0] + def progress_callback(data): + uploaded[0] += len(data) + percent = uploaded[0] * 100 // filesize + print(f"\rUploading {file}: {percent}%", end="", flush=True) + with open(local_path, 'rb') as f: + ftp.storbinary(f'STOR {remote_path}', f, 8192, callback=progress_callback) + print(f"\nUploaded {file} to {selected_ip}.") + ftp.sendcmd("BYE") + print(f"Disconnected from {selected_ip}.") + except ftplib.all_errors as e: + print(f"FTP error: {e}") From eb03a10f9e65d9fddba6d275cc561b09e51a6a10 Mon Sep 17 00:00:00 2001 From: probonopd Date: Wed, 23 Apr 2025 00:20:06 +0200 Subject: [PATCH 25/27] Ask user if they want to update Performances (default no) [ci skip] --- updater.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/updater.py b/updater.py index 80c1f6c..7ee0960 100644 --- a/updater.py +++ b/updater.py @@ -168,6 +168,10 @@ if __name__ == "__main__": print("Failed to get the release URL.") sys.exit(1) + # Ask user if they want to update Performances (default no) + update_perf = input("Do you want to update the Performances? This will OVERWRITE all existing performances. [y/N]: ").strip().lower() + update_performances = update_perf == 'y' + # Log into the selected device and upload the new version of MiniDexed print(f"Connecting to {selected_name} ({selected_ip})...") try: @@ -178,6 +182,72 @@ if __name__ == "__main__": ftp.login("admin", "admin") ftp.set_pasv(True) print(f"Connected to {selected_ip} (passive mode).") + # --- Performances update logic --- + if update_performances: + print("Updating Performance: recursively deleting and uploading /SD/performance directory...") + def ftp_rmdirs(ftp, path): + try: + items = ftp.nlst(path) + except Exception as e: + print(f"[WARN] Could not list {path}: {e}") + return + for item in items: + if item in ['.', '..', path]: + continue + full_path = f"{path}/{item}" if not item.startswith(path) else item + try: + # Try to delete as a file first + ftp.delete(full_path) + print(f"Deleted file: {full_path}") + except Exception: + # If not a file, try as a directory + try: + ftp_rmdirs(ftp, full_path) + ftp.rmd(full_path) + print(f"Deleted directory: {full_path}") + except Exception as e: + print(f"[WARN] Could not delete {full_path}: {e}") + try: + ftp_rmdirs(ftp, '/SD/performance') + try: + ftp.rmd('/SD/performance') + print("Deleted /SD/performance on device.") + except Exception as e: + print(f"[WARN] Could not delete /SD/performance directory itself: {e}") + except Exception as e: + print(f"Warning: Could not delete /SD/performance: {e}") + # Upload extracted performance/ recursively + local_perf = os.path.join(extract_path, 'performance') + def ftp_mkdirs(ftp, path): + try: + ftp.mkd(path) + except Exception: + pass + def ftp_upload_dir(ftp, local_dir, remote_dir): + ftp_mkdirs(ftp, remote_dir) + for item in os.listdir(local_dir): + lpath = os.path.join(local_dir, item) + rpath = f"{remote_dir}/{item}" + if os.path.isdir(lpath): + ftp_upload_dir(ftp, lpath, rpath) + else: + with open(lpath, 'rb') as fobj: + ftp.storbinary(f'STOR {rpath}', fobj) + print(f"Uploaded {rpath}") + if os.path.isdir(local_perf): + ftp_upload_dir(ftp, local_perf, '/SD/performance') + print("Uploaded new /SD/performance directory.") + else: + print("No extracted performance/ directory found, skipping upload.") + # Upload performance.ini if it exists in extract_path + local_perfini = os.path.join(extract_path, 'performance.ini') + if os.path.isfile(local_perfini): + with open(local_perfini, 'rb') as fobj: + ftp.storbinary('STOR /SD/performance.ini', fobj) + print("Uploaded /SD/performance.ini.") + else: + print("No extracted performance.ini found, skipping upload.") + # Upload kernel files for root, dirs, files in os.walk(extract_path): for file in files: if file.startswith("kernel") and file.endswith(".img"): From 686f7e6dd585248aa6659fc908b5f0e397767b1c Mon Sep 17 00:00:00 2001 From: probonopd Date: Wed, 23 Apr 2025 01:04:28 +0200 Subject: [PATCH 26/27] Fix start screen on soft reboot e.g., caused by BYE via FTP --- src/userinterface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/userinterface.cpp b/src/userinterface.cpp index 77b75f4..43a94ff 100644 --- a/src/userinterface.cpp +++ b/src/userinterface.cpp @@ -154,7 +154,8 @@ bool CUserInterface::Initialize (void) m_pLCDBuffered = new CWriteBufferDevice (m_pLCD); assert (m_pLCDBuffered); - + // clear sceen and go to top left corner + LCDWrite ("\x1B[H\x1B[J"); // cursor home and clear screen LCDWrite ("\x1B[?25l\x1B""d+"); // cursor off, autopage mode LCDWrite ("MiniDexed\nLoading..."); m_pLCDBuffered->Update (); From 543fc1ea3e759cb66b78fbd82dd8f3e03143cdf7 Mon Sep 17 00:00:00 2001 From: probonopd Date: Wed, 23 Apr 2025 01:13:43 +0200 Subject: [PATCH 27/27] PerformanceSelectToLoad=0 by default --- src/config.cpp | 2 +- src/minidexed.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 65cd68c..1f817f6 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -198,7 +198,7 @@ void CConfig::Load (void) m_bMIDIDumpEnabled = m_Properties.GetNumber ("MIDIDumpEnabled", 0) != 0; m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0; - m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 1) != 0; + m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 0) != 0; m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); // Network diff --git a/src/minidexed.ini b/src/minidexed.ini index c176e85..4d0f389 100644 --- a/src/minidexed.ini +++ b/src/minidexed.ini @@ -166,4 +166,4 @@ NetworkSyslogEnabled=0 NetworkSyslogServerIPAddress=0 # Performance -PerformanceSelectToLoad=1 +PerformanceSelectToLoad=0