Merge branch 'main' into more-midi-functions

more-midi-functions
probonopd 1 day ago committed by GitHub
commit 607de7e515
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 252
      .github/workflows/build.yml
  2. 19
      build.sh
  3. 11
      src/Makefile
  4. 5
      src/Rules.mk
  5. 3
      src/Synth_Dexed.mk
  6. 88
      src/arm_float_to_q23.c
  7. 22
      src/arm_float_to_q23.h
  8. 78
      src/config.cpp
  9. 31
      src/config.h
  10. 9
      src/effect_compressor.cpp
  11. 5
      src/kernel.cpp
  12. 2
      src/kernel.h
  13. 52
      src/midi.h
  14. 49
      src/mididevice.cpp
  15. 365
      src/minidexed.cpp
  16. 28
      src/minidexed.h
  17. 18
      src/minidexed.ini
  18. 874
      src/net/applemidi.cpp
  19. 111
      src/net/applemidi.h
  20. 42
      src/net/byteorder.h
  21. 111
      src/net/ftpdaemon.cpp
  22. 47
      src/net/ftpdaemon.h
  23. 1224
      src/net/ftpworker.cpp
  24. 157
      src/net/ftpworker.h
  25. 351
      src/net/mdnspublisher.cpp
  26. 90
      src/net/mdnspublisher.h
  27. 89
      src/net/udpmidi.cpp
  28. 57
      src/net/udpmidi.h
  29. 193
      src/net/utility.h
  30. 90
      src/udpmididevice.cpp
  31. 55
      src/udpmididevice.h
  32. 23
      src/uibuttons.cpp
  33. 2
      src/uibuttons.h
  34. 48
      src/uimenu.cpp
  35. 1
      src/uimenu.h
  36. 12
      src/userinterface.cpp
  37. 3
      src/userinterface.h
  38. 29
      submod.sh
  39. 57
      syslogserver.py
  40. 280
      updater.py

@ -9,100 +9,166 @@ on:
pull_request:
jobs:
Build:
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
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.../$(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/performance.ini sdcard/
cp ./getsysex.sh sdcard/
echo "usbspeed=full" > sdcard/cmdline.txt
# Performances
git clone https://github.com/Banana71/Soundplantage --depth 1
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: MiniDexed_${{ github.run_number }}_${{ steps.gitinfo.outputs.git_info }}_64bit
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: 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
# 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: MiniDexed_${{ github.run_number }}_${{ env.GIT_INFO }}_32bit
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
wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh
bash ./upload.sh ./MiniDexed*.zip
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: MiniDexed_*
merge-multiple: true
path: combined
- name: Create combined ZIP file
run: |
cd combined
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: |
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

@ -1,6 +1,6 @@
#!/bin/bash
set -e
set -e
set -x
if [ -z "${RPI}" ] ; then
@ -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 ..

@ -9,9 +9,18 @@ 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 \
arm_float_to_q23.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

@ -28,6 +28,9 @@ 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
-include $(DEPS)

@ -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

@ -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) */

@ -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

@ -198,8 +198,28 @@ 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
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 ("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)
{
m_INetworkSyslogServerIPAddress.Set (pSyslogServerIP);
}
m_nMasterVolume = m_Properties.GetNumber ("MasterVolume", 64);
}
unsigned CConfig::GetToneGenerators (void) const
@ -722,3 +742,59 @@ 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;
}
bool CConfig::GetNetworkFTPEnabled (void) const
{
return m_bNetworkFTPEnabled;
}

@ -23,6 +23,7 @@
#ifndef _config_h
#define _config_h
#include <circle/net/ipaddress.h>
#include <fatfs/ff.h>
#include <Properties/propertiesfatfsfile.h>
#include <circle/sysconfig.h>
@ -239,6 +240,21 @@ public:
bool GetPerformanceSelectToLoad (void) const;
unsigned GetPerformanceSelectChannel (void) const;
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;
bool GetNetworkFTPEnabled (void) const;
private:
CPropertiesFatFsFile m_Properties;
@ -353,6 +369,21 @@ private:
bool m_bProfileEnabled;
bool m_bPerformanceSelectToLoad;
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;
bool m_bNetworkFTPEnabled;
};
#endif

@ -12,6 +12,7 @@
MIT License. use at your own risk.
*/
#include <algorithm>
#include <circle/logger.h>
#include <cstdlib>
#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()
}

@ -25,12 +25,15 @@
#include <circle/usb/usbhcidevice.h>
#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),

@ -26,6 +26,7 @@
#include <circle/i2cmaster.h>
#include <circle/spimaster.h>
#include <circle/usb/usbcontroller.h>
#include <circle/sched/scheduler.h>
#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;
};

@ -0,0 +1,52 @@
//
// 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 <http://www.gnu.org/licenses/>.
//
#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_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
#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

@ -27,37 +27,11 @@
#include "config.h"
#include <stdio.h>
#include <assert.h>
#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_CC_OMNI_MODE_OFF 124
#define MIDI_CC_OMNI_MODE_ON 125
#define MIDI_CC_MONO_MODE_ON 126
#define MIDI_CC_POLY_MODE_ON 127
#define MIDI_PROGRAM_CHANGE 0b1100
#define MIDI_PITCH_BEND 0b1110
// MIDI "System" level (i.e. all TG) custom CC maps
// Note: Even if number of TGs is not 8, there are only 8
// available to be used in the mappings here.
@ -297,6 +271,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);
}
}
}
@ -313,7 +288,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;
@ -323,7 +298,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:
@ -417,6 +392,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);
@ -448,6 +427,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);

@ -23,10 +23,18 @@
#include <circle/sound/pwmsoundbasedevice.h>
#include <circle/sound/i2ssoundbasedevice.h>
#include <circle/sound/hdmisoundbasedevice.h>
#include <circle/net/syslogdaemon.h>
#include <circle/net/ipaddress.h>
#include <circle/gpiopin.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include "arm_float_to_q23.h"
const char WLANFirmwarePath[] = "SD:firmware/";
const char WLANConfigFile[] = "SD:wpa_supplicant.conf";
#define FTPUSERNAME "admin"
#define FTPPASSWORD "admin"
LOGMODULE ("minidexed");
@ -51,6 +59,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),
@ -218,7 +234,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<CConfig::AllToneGenerators>(pConfig->GetChunkSize()/2);
@ -243,8 +260,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);
@ -333,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 ();
@ -345,21 +372,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);
@ -367,6 +400,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated)
if (m_bUseSerial)
{
m_SerialMIDI.Process ();
pScheduler->Yield();
}
m_UI.Process ();
@ -376,12 +410,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)
@ -399,6 +435,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated)
{
DoSetFirstPerformance();
}
pScheduler->Yield();
}
if (m_bSetNewPerformance && !m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy)
@ -408,18 +445,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
@ -613,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())
{
@ -769,6 +815,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...
@ -849,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);
@ -1129,23 +1197,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;
@ -1164,12 +1231,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;
@ -1188,7 +1255,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;
@ -1212,8 +1279,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))
{
@ -1280,7 +1347,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)
{
@ -1302,13 +1369,22 @@ 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
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");
@ -1321,7 +1397,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)
{
@ -1387,13 +1463,19 @@ 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
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");
@ -1701,7 +1783,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();
@ -1728,6 +1810,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 ();
}
@ -1739,7 +1823,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 ();
}
@ -1785,14 +1868,23 @@ void CMiniDexed::getSysExVoiceDump(uint8_t* dest, uint8_t nTG)
dest[162] = 0xF7; // SysEx end
}
void CMiniDexed::setMasterVolume (float32_t vol)
void CMiniDexed::setOPMask(uint8_t uchOPMask, uint8_t nTG)
{
if(vol < 0.0)
vol = 0.0;
else if(vol > 1.0)
vol = 1.0;
m_uchOPMask[nTG] = uchOPMask;
m_pTG[nTG]->setOPAll (m_uchOPMask[nTG]);
}
nMasterVolume=vol;
void CMiniDexed::setMasterVolume(float32_t vol)
{
if (vol < 0.0)
vol = 0.0;
else if (vol > 1.0)
vol = 1.0;
// Apply logarithmic scaling to match perceived loudness
vol = powf(vol, 2.0f);
nMasterVolume = vol;
}
std::string CMiniDexed::GetPerformanceFileName(unsigned nID)
@ -1956,6 +2048,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);
@ -1981,6 +2074,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)
@ -2012,7 +2107,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)
@ -2184,3 +2279,215 @@ 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();
}
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");
}
} else {
LOGNOTE("FTP daemon not started (NetworkFTPEnabled=0)");
}
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())
{
LOGNOTE ("Syslog server is enabled in configuration");
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);
}
else
{
LOGNOTE ("Syslog server IP not set");
}
}
else
{
LOGNOTE ("Syslog server is not enabled in configuration");
}
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;
}
}

@ -39,12 +39,19 @@
#include <circle/spimaster.h>
#include <circle/multicore.h>
#include <circle/sound/soundbasedevice.h>
#include <circle/sched/scheduler.h>
#include <circle/net/netsubsystem.h>
#include <wlan/bcm4343.h>
#include <wlan/hostap/wpa_supplicant/wpasupplicant.h>
#include "net/mdnspublisher.h"
#include <circle/spinlock.h>
#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);
@ -85,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);
@ -114,6 +124,7 @@ public:
void loadVoiceParameters(const uint8_t* data, uint8_t nTG);
void setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG);
void getSysExVoiceDump(uint8_t* dest, uint8_t nTG);
void 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);
@ -229,11 +240,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 +340,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;

@ -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
@ -149,5 +151,19 @@ 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
NetworkFTPEnabled=0
NetworkSyslogEnabled=0
NetworkSyslogServerIPAddress=0
# Performance
PerformanceSelectToLoad=1
PerformanceSelectToLoad=0

@ -0,0 +1,874 @@
//
// applemidi.cpp
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#include <circle/logger.h>
#include <circle/macros.h>
#include <circle/net/in.h>
#include <circle/net/netsubsystem.h>
#include <circle/sched/scheduler.h>
#include <circle/timer.h>
#include <circle/util.h>
#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<const TAppleMIDISession*>(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, "<unknown>", sizeof(pOutPacket->Name));
return true;
}
bool ParseEndSessionPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket)
{
const TAppleMIDISession* const pInPacket = reinterpret_cast<const TAppleMIDISession*>(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<const TAppleMIDISync*>(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<const TRTPMIDI*>(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<const char*>(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<size_t>(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));
}

@ -0,0 +1,111 @@
//
// applemidi.h
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#ifndef _applemidi_h
#define _applemidi_h
#include <circle/bcmrandom.h>
#include <circle/net/ipaddress.h>
#include <circle/net/socket.h>
#include <circle/sched/task.h>
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

@ -0,0 +1,42 @@
//
// byteorder.h
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#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

@ -0,0 +1,111 @@
//
// ftpdaemon.cpp
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#include <circle/logger.h>
#include <circle/net/in.h>
#include <circle/net/ipaddress.h>
#include <circle/net/netsubsystem.h>
#include <circle/string.h>
#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<const char*>(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);
}
}

@ -0,0 +1,47 @@
//
// ftpdaemon.h
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#ifndef _ftpdaemon_h
#define _ftpdaemon_h
#include <circle/net/socket.h>
#include <circle/sched/task.h>
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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,157 @@
//
// ftpworker.h
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#ifndef _ftpworker_h
#define _ftpworker_h
#include <circle/net/ipaddress.h>
#include <circle/net/socket.h>
#include <circle/sched/task.h>
#include <circle/string.h>
// 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

@ -0,0 +1,351 @@
//
// mdnspublisher.cpp
//
// Circle - A C++ bare metal environment for Raspberry Pi
// Copyright (C) 2024 R. Stange <rsta2@o2online.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "mdnspublisher.h"
#include <circle/sched/scheduler.h>
#include <circle/net/in.h>
#include <circle/logger.h>
#include <circle/util.h>
#include <assert.h>
#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<TService *> (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);
SendResponse (pService, FALSE);
/*
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<TService *> (CPtrList::GetPtr (pElement));
assert (pService);
SendResponse (pService, FALSE);
/*
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<u8> (*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<u16> (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<u16 *> (m_pDataLen) = le2be16 (m_pWritePtr - m_pDataLen - sizeof (u16));
m_pDataLen = nullptr;
}

@ -0,0 +1,90 @@
//
// mdnspublisher.h
//
// Circle - A C++ bare metal environment for Raspberry Pi
// Copyright (C) 2024 R. Stange <rsta2@o2online.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#ifndef _circle_net_mdnspublisher_h
#define _circle_net_mdnspublisher_h
#include <circle/sched/task.h>
#include <circle/sched/mutex.h>
#include <circle/sched/synchronizationevent.h>
#include <circle/net/netsubsystem.h>
#include <circle/net/socket.h>
#include <circle/net/ipaddress.h>
#include <circle/ptrlist.h>
#include <circle/string.h>
#include <circle/types.h>
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

@ -0,0 +1,89 @@
//
// udpmidi.cpp
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#include <circle/logger.h>
#include <circle/net/in.h>
#include <circle/net/netsubsystem.h>
#include <circle/sched/scheduler.h>
#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();
}
}

@ -0,0 +1,57 @@
//
// udpmidi.h
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#ifndef _udpmidi_h
#define _udpmidi_h
#include <circle/net/ipaddress.h>
#include <circle/net/socket.h>
#include <circle/sched/task.h>
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

@ -0,0 +1,193 @@
//
// utility.h
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
#ifndef _utility_h
#define _utility_h
#include <circle/string.h>
#include <circle/util.h>
// 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 <class T>
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 <class T>
constexpr T Min(const T& nLHS, const T& nRHS)
{
return nLHS < nRHS ? nLHS : nRHS;
}
// Templated function for taking the maximum of two values
template <class T>
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 <class T, size_t N>
constexpr size_t ArraySize(const T(&)[N]) { return N; }
// Returns whether some value is a power of 2
template <class T>
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 <class T>
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 <class T>
constexpr T MillisToTicks(const T& nMillis)
{
return nMillis * 1000;
}
template <class T>
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<class T>
using TComparator = bool (*)(const T&, const T&);
template<class T>
inline bool LessThan(const T& ObjectA, const T& ObjectB)
{
return ObjectA < ObjectB;
}
template<class T>
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<class T>
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<class T>
size_t Partition(T* Items, Comparator::TComparator<T> 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 <class T>
void QSort(T* Items, Comparator::TComparator<T> 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

@ -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 <rsta2@o2online.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include <circle/logger.h>
#include <cstring>
#include "udpmididevice.h"
#include <assert.h>
#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);
}

@ -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 <rsta2@o2online.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#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

@ -22,6 +22,7 @@
#include <assert.h>
#include <circle/timer.h>
#include <string.h>
#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; i<MAX_BUTTONS; i++) {
if (m_buttons[i].getPinNumber() == midiPin) {
if (nMidiCmd == 0x80) {
// NoteOff = Button OFF
if (nMidiType == MIDI_NOTE_OFF) {
// Button OFF
m_buttons[i].Write (0);
} else if ((nMidiCmd == 0x90) && (nMidiData2 == 0)) {
// NoteOn with Vel == 0 = Button OFF
} else if ((nMidiType == MIDI_NOTE_ON) && (nMidiData2 == 0)) {
// Button OFF (MIDI_NOTE_ON with Vel == 0)
m_buttons[i].Write (0);
} else if (nMidiCmd == 0x90) {
// NoteOn = Button ON
} else if (nMidiType == MIDI_NOTE_ON) {
// Button ON
m_buttons[i].Write (127);
} else {
// Ignore other MIDI commands
@ -513,9 +514,9 @@ void CUIButtons::BtnMIDICmdHandler (unsigned nMidiCmd, unsigned nMidiData1, unsi
}
}
} else {
// LOGDBG("BtnMIDICmdHandler (CC): %x %x %x)", nMidiCmd, nMidiData1, nMidiData2);
// LOGDBG("BtnMIDICmdHandler (CC): %x %x %x)", nMidiType, nMidiData1, nMidiData2);
// Using MIDI CC messages for MIDI buttons
if (nMidiCmd == 0xB0) { // Control Message
if (nMidiType == MIDI_CONTROL_CHANGE) {
unsigned midiPin = ccToMidiPin(nMidiData1);
for (unsigned i=0; i<MAX_BUTTONS; i++) {
if (m_buttons[i].getPinNumber() == midiPin) {
@ -523,5 +524,5 @@ void CUIButtons::BtnMIDICmdHandler (unsigned nMidiCmd, unsigned nMidiData1, unsi
}
}
}
}
}
}

@ -124,7 +124,7 @@ public:
void ResetButton (unsigned pinNumber);
void BtnMIDICmdHandler (unsigned nMidiCmd, unsigned nMidiData1, unsigned nMidiData2);
void BtnMIDICmdHandler (unsigned nMidiType, unsigned nMidiData1, unsigned nMidiData2);
private:
CConfig *m_pConfig;

@ -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},
@ -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:
@ -702,15 +705,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);
}
}
@ -726,6 +744,7 @@ void CUIMenu::EditTGParameter (CUIMenu *pUIMenu, TMenuEvent Event)
switch (Event)
{
case MenuEventUpdate:
case MenuEventUpdateParameter:
break;
case MenuEventStepDown:
@ -779,6 +798,7 @@ void CUIMenu::EditTGParameter2 (CUIMenu *pUIMenu, TMenuEvent Event) // second me
switch (Event)
{
case MenuEventUpdate:
case MenuEventUpdateParameter:
break;
case MenuEventStepDown:
@ -832,6 +852,7 @@ void CUIMenu::EditVoiceParameter (CUIMenu *pUIMenu, TMenuEvent Event)
switch (Event)
{
case MenuEventUpdate:
case MenuEventUpdateParameter:
break;
case MenuEventStepDown:
@ -885,6 +906,7 @@ void CUIMenu::EditOPParameter (CUIMenu *pUIMenu, TMenuEvent Event)
switch (Event)
{
case MenuEventUpdate:
case MenuEventUpdateParameter:
break;
case MenuEventStepDown:
@ -1773,9 +1795,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)
@ -1996,5 +2016,3 @@ void CUIMenu::EditTGParameterModulation (CUIMenu *pUIMenu, TMenuEvent Event)
nValue > rParam.Minimum, nValue < rParam.Maximum);
}

@ -39,6 +39,7 @@ public:
enum TMenuEvent
{
MenuEventUpdate,
MenuEventUpdateParameter,
MenuEventSelect,
MenuEventBack,
MenuEventHome,

@ -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 ();
@ -211,6 +212,11 @@ void CUserInterface::Process (void)
}
void CUserInterface::ParameterChanged (void)
{
m_Menu.EventHandler (CUIMenu::MenuEventUpdateParameter);
}
void CUserInterface::DisplayChanged (void)
{
m_Menu.EventHandler (CUIMenu::MenuEventUpdate);
}
@ -396,7 +402,7 @@ void CUserInterface::UIButtonsEventStub (CUIButton::BtnEvent Event, void *pParam
pThis->UIButtonsEventHandler (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 +417,7 @@ void CUserInterface::UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiCmd, unsi
if (m_pUIButtons)
{
m_pUIButtons->BtnMIDICmdHandler (nMidiCmd, nMidiData1, nMidiData2);
m_pUIButtons->BtnMIDICmdHandler (nMidiType, nMidiData1, nMidiData2);
}
}

@ -45,6 +45,7 @@ public:
void Process (void);
void ParameterChanged (void);
void DisplayChanged (void);
// Write to display in this format:
// +----------------+
@ -55,7 +56,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

@ -1,24 +1,27 @@
#!/bin/bash
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 checkout 3bd135d
git submodule update --init --recursive
git reset --hard
git checkout 1111eee -f # Matches Circle Step49
git submodule update --init --recursive -f
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 checkout c9f5274
git reset --hard
git checkout 65d8383ad5 -f
cd -

@ -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.")

@ -0,0 +1,280 @@
#!/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 <a ... href="..."> tags with a <span class="Truncate-text text-bold">MiniDexed*.zip</span>
pattern = re.compile(r'<a[^>]+href=["\']([^"\']+\.zip)["\'][^>]*>\s*<span[^>]*class=["\']Truncate-text text-bold["\'][^>]*>(MiniDexed[^<]*?\.zip)</span>', 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)
# 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:
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).")
# --- 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"):
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}")
Loading…
Cancel
Save