Merge branch 'main' into OptimisePerformanceMenu

pull/834/head
probonopd 1 day ago committed by GitHub
commit 5209d1df8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 233
      .github/workflows/build.yml
  2. 19
      build.sh
  3. 4
      hwconfig/DT-DX.override
  4. 10
      src/Makefile
  5. 5
      src/Rules.mk
  6. 70
      src/config.cpp
  7. 29
      src/config.h
  8. 5
      src/kernel.cpp
  9. 2
      src/kernel.h
  10. 48
      src/midi.h
  11. 27
      src/mididevice.cpp
  12. 285
      src/minidexed.cpp
  13. 25
      src/minidexed.h
  14. 14
      src/minidexed.ini
  15. 874
      src/net/applemidi.cpp
  16. 111
      src/net/applemidi.h
  17. 42
      src/net/byteorder.h
  18. 111
      src/net/ftpdaemon.cpp
  19. 47
      src/net/ftpdaemon.h
  20. 1218
      src/net/ftpworker.cpp
  21. 157
      src/net/ftpworker.h
  22. 345
      src/net/mdnspublisher.cpp
  23. 90
      src/net/mdnspublisher.h
  24. 89
      src/net/udpmidi.cpp
  25. 57
      src/net/udpmidi.h
  26. 193
      src/net/utility.h
  27. 90
      src/udpmididevice.cpp
  28. 55
      src/udpmididevice.h
  29. 23
      src/uibuttons.cpp
  30. 2
      src/uibuttons.h
  31. 41
      src/uimenu.cpp
  32. 4
      src/userinterface.cpp
  33. 2
      src/userinterface.h
  34. 23
      submod.sh

@ -1,3 +1,5 @@
# Build 32-bit and 64-bit separately
name: Build
env:
@ -9,98 +11,145 @@ on:
pull_request:
jobs:
Build:
runs-on: ubuntu-20.04
build64:
name: Build 64-bit kernels
runs-on: ubuntu-22.04
outputs:
artifact-path: ${{ steps.upload64.outputs.artifact-path }}
steps:
- uses: actions/checkout@v2
- name: Get specific commits of git submodules
run: sh -ex ./submod.sh
- name: Create sdcard directory
run: mkdir -p ./sdcard/
- name: Put git hash in startup message
run: |
sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp
# Install 64-bit toolchain (aarch64)
- name: Install 64-bit toolchain
run: |
set -ex
wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz
tar xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz
- name: Build for Raspberry Pi 5 (64-bit)
run: |
set -ex
export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH
RPI=5 bash -ex build.sh
cp ./src/kernel*.img ./sdcard/
- name: Build for Raspberry Pi 4 (64-bit)
run: |
set -ex
export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH
RPI=4 bash -ex build.sh
cp ./src/kernel*.img ./sdcard/
- name: Build for Raspberry Pi 3 (64-bit)
run: |
set -ex
export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH
RPI=3 bash -ex build.sh
cp ./src/kernel*.img ./sdcard/
- name: Prepare SD card content for 64-bit
run: |
set -ex
export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin):$PATH
cd ./circle-stdlib/libs/circle/boot
make
make armstub64
cd -
cp -r ./circle-stdlib/libs/circle/boot/* sdcard
rm -rf sdcard/config*.txt sdcard/README sdcard/Makefile sdcard/armstub sdcard/COPYING.linux
cp ./src/config.txt ./src/minidexed.ini ./src/*img ./src/performance.ini sdcard/
cp ./getsysex.sh sdcard/
echo "usbspeed=full" > sdcard/cmdline.txt
- name: Upload 64-bit artifacts
id: upload64
uses: actions/upload-artifact@v4
with:
name: build64-artifacts
path: sdcard/*
build32:
name: Build 32-bit kernels
runs-on: ubuntu-22.04
outputs:
artifact-path: ${{ steps.upload32.outputs.artifact-path }}
steps:
- uses: actions/checkout@v2
- name: Get specific commits of git submodules
run: sh -ex ./submod.sh
- name: Create sdcard directory
run: mkdir -p ./sdcard/
- name: Put git hash in startup message
run: |
sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp
# Install 32-bit toolchain (arm-none-eabi)
- name: Install 32-bit toolchain
run: |
set -ex
wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz
tar xf gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz
- name: Build for Raspberry Pi 2 (32-bit)
run: |
set -ex
export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-arm-none-eabi/bin):$PATH
RPI=2 bash -ex build.sh
cp ./src/kernel*.img ./sdcard/
- name: Build for Raspberry Pi 1 (32-bit)
run: |
set -ex
export PATH=$(readlink -f ./gcc-arm-10.3-2021.07-x86_64-arm-none-eabi/bin):$PATH
RPI=1 bash -ex build.sh
cp ./src/kernel*.img ./sdcard/
- name: Upload 32-bit artifacts
id: upload32
uses: actions/upload-artifact@v4
with:
name: build32-artifacts
path: sdcard/*
combine:
name: Combine Artifacts
runs-on: ubuntu-22.04
needs: [ build64, build32 ]
steps:
- uses: actions/checkout@v2
- name: Get specific commits of git submodules
run: |
sh -ex ./submod.sh
- name: Apply patches
run: |
# Put git hash in startup message
sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp
- name: Install toolchains
run: |
set -ex
wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz
tar xf *-aarch64-none-elf.tar.xz
wget -q https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-none-eabi.tar.xz
tar xf *-arm-none-eabi.tar.xz
mkdir -p kernels
- name: Build for Raspberry Pi 5
run: |
set -ex
export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH
RPI=5 bash -ex build.sh
cp ./src/kernel*.img ./kernels/
- name: Build for Raspberry Pi 4
run: |
set -ex
export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH
RPI=4 bash -ex build.sh
cp ./src/kernel*.img ./kernels/
- name: Build for Raspberry Pi 3
run: |
set -ex
export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH
RPI=3 bash -ex build.sh
cp ./src/kernel*.img ./kernels/
- name: Build for Raspberry Pi 2
run: |
set -ex
export PATH=$(readlink -f ./gcc-*arm-none*/bin/):$PATH
RPI=2 bash -ex build.sh
cp ./src/kernel*.img ./kernels/
- name: Build for Raspberry Pi 1
run: |
set -ex
export PATH=$(readlink -f ./gcc-*arm-none*/bin/):$PATH
RPI=1 bash -ex build.sh
cp ./src/kernel*.img ./kernels/
- name: Get Raspberry Pi boot files
run: |
set -ex
export PATH=$(readlink -f ./gcc-*aarch64-none*/bin/):$PATH
cd ./circle-stdlib/libs/circle/boot
make
make armstub64
cd -
mkdir -p sdcard
cp -r ./circle-stdlib/libs/circle/boot/* sdcard
rm -rf sdcard/config*.txt sdcard/README sdcard/Makefile sdcard/armstub sdcard/COPYING.linux
cp ./src/config.txt ./src/minidexed.ini ./src/*img ./src/performance.ini sdcard/
cp ./getsysex.sh sdcard/
echo "usbspeed=full" > sdcard/cmdline.txt
cd sdcard
cp ../kernels/* . || true
cd -
- name: Get performance files
run: |
git clone https://github.com/Banana71/Soundplantage --depth 1 # depth 1 means only the latest commit
cp -r ./Soundplantage/performance ./Soundplantage/*.pdf ./sdcard/
cd sdcard
zip -r ../MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip *
echo "artifactName=MiniDexed_$GITHUB_RUN_NUMBER_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
cd -
- name: Hardware configration files
run: |
cd hwconfig
sh -ex ./customize.sh
cd -
mkdir -p ./sdcard/hardware/
cp -r ./hwconfig/minidexed_* ./sdcard/minidexed.ini ./sdcard/hardware/
- 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 64-bit artifacts
uses: actions/download-artifact@v4
with:
name: build64-artifacts
path: combined
- name: Download 32-bit artifacts
uses: actions/download-artifact@v4
with:
name: build32-artifacts
path: combined
- name: Create combined ZIP file
run: |
cd combined
zip -r ../MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip .
cd ..
- name: Upload combined ZIP artifact
uses: actions/upload-artifact@v4
with:
name: combined-artifact
path: MiniDexed_${GITHUB_RUN_NUMBER}_$(date +%Y-%m-%d)-$(git rev-parse --short HEAD).zip
- name: Upload to GitHub Releases (only 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 ..

@ -2,7 +2,7 @@
# https://www.dtronics.nl/dt-dx
SoundDevice=i2s
SampleRate=22000
SampleRate=48000
ChunkSize=256
DACI2CAddress=0x0
ChannelsSwapped=1
@ -43,4 +43,4 @@ LongPressTimeout=400
EncoderEnabled=1
EncoderPinClock=6
EncoderPinData=5
EncoderPinData=5

@ -9,9 +9,17 @@ CMSIS_DIR = ../CMSIS_5/CMSIS
OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \
mididevice.o midikeyboard.o serialmididevice.o pckeyboard.o \
sysexfileloader.o performanceconfig.o perftimer.o \
effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o
effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \
net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o net/mdnspublisher.o udpmididevice.o
OPTIMIZE = -O3
include ./Synth_Dexed.mk
include ./Rules.mk
# Clean target
.PHONY: clean
clean:
@echo "Cleaning up..."
rm -f $(OBJS) *.o *.d *~ core

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

@ -200,6 +200,25 @@ void CConfig::Load (void)
m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0;
m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 1) != 0;
m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0);
// Network
m_bNetworkEnabled = m_Properties.GetNumber ("NetworkEnabled", 0) != 0;
m_bNetworkDHCP = m_Properties.GetNumber ("NetworkDHCP", 0) != 0;
m_NetworkType = m_Properties.GetString ("NetworkType", "wlan");
m_NetworkHostname = m_Properties.GetString ("NetworkHostname", "MiniDexed");
m_INetworkIPAddress = m_Properties.GetIPAddress("NetworkIPAddress") != 0;
m_INetworkSubnetMask = m_Properties.GetIPAddress("NetworkSubnetMask") != 0;
m_INetworkDefaultGateway = m_Properties.GetIPAddress("NetworkDefaultGateway") != 0;
m_bSyslogEnabled = m_Properties.GetNumber ("SyslogEnabled", 0) != 0;
m_INetworkDNSServer = m_Properties.GetIPAddress("NetworkDNSServer") != 0;
const u8 *pSyslogServerIP = m_Properties.GetIPAddress ("NetworkSyslogServerIPAddress");
if (pSyslogServerIP)
{
m_INetworkSyslogServerIPAddress.Set (pSyslogServerIP);
}
m_nMasterVolume = m_Properties.GetNumber ("MasterVolume", 64);
}
unsigned CConfig::GetToneGenerators (void) const
@ -722,3 +741,54 @@ unsigned CConfig::GetPerformanceSelectChannel (void) const
{
return m_bPerformanceSelectChannel;
}
// Network
bool CConfig::GetNetworkEnabled (void) const
{
return m_bNetworkEnabled;
}
bool CConfig::GetNetworkDHCP (void) const
{
return m_bNetworkDHCP;
}
const char *CConfig::GetNetworkType (void) const
{
return m_NetworkType.c_str();
}
const char *CConfig::GetNetworkHostname (void) const
{
return m_NetworkHostname.c_str();
}
CIPAddress CConfig::GetNetworkIPAddress (void) const
{
return m_INetworkIPAddress;
}
CIPAddress CConfig::GetNetworkSubnetMask (void) const
{
return m_INetworkSubnetMask;
}
CIPAddress CConfig::GetNetworkDefaultGateway (void) const
{
return m_INetworkDefaultGateway;
}
CIPAddress CConfig::GetNetworkDNSServer (void) const
{
return m_INetworkDNSServer;
}
bool CConfig::GetSyslogEnabled (void) const
{
return m_bSyslogEnabled;
}
CIPAddress CConfig::GetNetworkSyslogServerIPAddress (void) const
{
return m_INetworkSyslogServerIPAddress;
}

@ -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,20 @@ 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;
private:
CPropertiesFatFsFile m_Properties;
@ -353,6 +368,20 @@ 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;
};
#endif

@ -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,48 @@
//
// midi.h
//
// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
// Copyright (C) 2025 The MiniDexed Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <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_VOLUME 7
#define MIDI_CC_PAN_POSITION 10
#define MIDI_CC_EXPRESSION 11
#define MIDI_CC_BANK_SELECT_LSB 32
#define MIDI_CC_BANK_SUSTAIN 64
#define MIDI_CC_RESONANCE 71
#define MIDI_CC_FREQUENCY_CUTOFF 74
#define MIDI_CC_REVERB_LEVEL 91
#define MIDI_CC_DETUNE_LEVEL 94
#define MIDI_CC_ALL_SOUND_OFF 120
#define MIDI_CC_ALL_NOTES_OFF 123
#define MIDI_PROGRAM_CHANGE 0b1100
#define MIDI_PITCH_BEND 0b1110
#endif

@ -27,32 +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_PROGRAM_CHANGE 0b1100
#define MIDI_PITCH_BEND 0b1110
// MIDI "System" level (i.e. all TG) custom CC maps
// Note: Even if number of TGs is not 8, there are only 8
@ -302,7 +281,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
}
if (nLength == 3)
{
m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]);
m_pUI->UIMIDICmdHandler (ucChannel, ucType, pMessage[1], pMessage[2]);
}
break;
@ -312,7 +291,7 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
{
break;
}
m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]);
m_pUI->UIMIDICmdHandler (ucChannel, ucType, pMessage[1], pMessage[2]);
break;
case MIDI_PROGRAM_CHANGE:

@ -23,11 +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>
const char WLANFirmwarePath[] = "SD:firmware/";
const char WLANConfigFile[] = "SD:wpa_supplicant.conf";
#define FTPUSERNAME "admin"
#define FTPPASSWORD "admin"
LOGMODULE ("minidexed");
CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt,
@ -51,6 +58,14 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt,
m_GetChunkTimer ("GetChunk",
1000000U * pConfig->GetChunkSize ()/2 / pConfig->GetSampleRate ()),
m_bProfileEnabled (m_pConfig->GetProfileEnabled ()),
m_pNet(nullptr),
m_pNetDevice(nullptr),
m_WLAN(nullptr),
m_WPASupplicant(nullptr),
m_bNetworkReady(false),
m_bNetworkInit(false),
m_UDPMIDI(nullptr),
m_pmDNSPublisher (nullptr),
m_bSavePerformance (false),
m_bSavePerformanceNewFile (false),
m_bSetNewPerformance (false),
@ -218,7 +233,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 +259,18 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt,
SetParameter (ParameterPerformanceBank, 0);
};
CMiniDexed::~CMiniDexed (void)
{
delete m_WLAN;
delete m_WPASupplicant;
delete m_UDPMIDI;
delete m_pFTPDaemon;
delete m_pmDNSPublisher;
}
bool CMiniDexed::Initialize (void)
{
LOGNOTE("CMiniDexed::Initialize called");
assert (m_pConfig);
assert (m_pSoundDevice);
@ -345,21 +371,27 @@ bool CMiniDexed::Initialize (void)
{
return false;
}
InitNetwork(); // returns bool but we continue even if something goes wrong
LOGNOTE("CMiniDexed::Initialize: InitNetwork() called");
#endif
return true;
}
void CMiniDexed::Process (bool bPlugAndPlayUpdated)
{
CScheduler* const pScheduler = CScheduler::Get();
#ifndef ARM_ALLOW_MULTI_CORE
ProcessSound ();
pScheduler->Yield();
#endif
for (unsigned i = 0; i < CConfig::MaxUSBMIDIDevices; i++)
{
assert (m_pMIDIKeyboard[i]);
m_pMIDIKeyboard[i]->Process (bPlugAndPlayUpdated);
pScheduler->Yield();
}
m_PCKeyboard.Process (bPlugAndPlayUpdated);
@ -367,6 +399,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated)
if (m_bUseSerial)
{
m_SerialMIDI.Process ();
pScheduler->Yield();
}
m_UI.Process ();
@ -376,12 +409,14 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated)
DoSavePerformance ();
m_bSavePerformance = false;
pScheduler->Yield();
}
if (m_bSavePerformanceNewFile)
{
DoSavePerformanceNewFile ();
m_bSavePerformanceNewFile = false;
pScheduler->Yield();
}
if (m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy)
@ -399,6 +434,7 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated)
{
DoSetFirstPerformance();
}
pScheduler->Yield();
}
if (m_bSetNewPerformance && !m_bSetNewPerformanceBank && !m_bLoadPerformanceBusy && !m_bLoadPerformanceBankBusy)
@ -408,18 +444,26 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated)
{
m_bSetNewPerformance = false;
}
pScheduler->Yield();
}
if(m_bDeletePerformance)
{
DoDeletePerformance ();
m_bDeletePerformance = false;
pScheduler->Yield();
}
if (m_bProfileEnabled)
{
m_GetChunkTimer.Dump ();
pScheduler->Yield();
}
if (m_pNet) {
UpdateNetwork();
}
// Allow other tasks to run
pScheduler->Yield();
}
#ifdef ARM_ALLOW_MULTI_CORE
@ -769,6 +813,10 @@ void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG)
{
m_SerialMIDI.SetChannel (uchChannel, nTG);
}
if (m_UDPMIDI)
{
m_UDPMIDI->SetChannel (uchChannel, nTG);
}
#ifdef ARM_ALLOW_MULTI_CORE
/* This doesn't appear to be used anywhere...
@ -1309,6 +1357,15 @@ void CMiniDexed::ProcessSound (void)
arm_fill_q15(0, tmp_int, nFrames*Channels);
}
// Prevent PCM510x analog mute from kicking in
for (uint8_t tg = 0; tg < Channels; tg++)
{
if (tmp_int[(nFrames - 1) * Channels + tg] == 0)
{
tmp_int[(nFrames - 1) * Channels + tg]++;
}
}
if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int))
{
LOGERR ("Sound data dropped");
@ -1394,6 +1451,12 @@ void CMiniDexed::ProcessSound (void)
arm_fill_q15(0, tmp_int, nFrames * 2);
}
// Prevent PCM510x analog mute from kicking in
if (tmp_int[nFrames * 2 - 1] == 0)
{
tmp_int[nFrames * 2 - 1]++;
}
if (m_pSoundDevice->Write (tmp_int, sizeof(tmp_int)) != (int) sizeof(tmp_int))
{
LOGERR ("Sound data dropped");
@ -1672,7 +1735,7 @@ void CMiniDexed::setBreathControllerTarget(uint8_t target, uint8_t nTG)
assert (m_pTG[nTG]);
m_nBreathControlTarget[nTG]=target;
m_nBreathControlTarget[nTG] = target;
m_pTG[nTG]->setBreathControllerTarget(constrain(target, 0, 7));
m_pTG[nTG]->ControllersRefresh();
@ -1701,7 +1764,7 @@ void CMiniDexed::setAftertouchTarget(uint8_t target, uint8_t nTG)
assert (m_pTG[nTG]);
m_nAftertouchTarget[nTG]=target;
m_nAftertouchTarget[nTG] = target;
m_pTG[nTG]->setAftertouchTarget(constrain(target, 0, 7));
m_pTG[nTG]->ControllersRefresh();
@ -1739,7 +1802,6 @@ void CMiniDexed::setVoiceDataElement(uint8_t data, uint8_t number, uint8_t nTG)
assert (m_pTG[nTG]);
m_pTG[nTG]->setVoiceDataElement(constrain(data, 0, 155),constrain(number, 0, 99));
//m_pTG[nTG]->doRefreshVoice();
m_UI.ParameterChanged ();
}
@ -1785,14 +1847,17 @@ void CMiniDexed::getSysExVoiceDump(uint8_t* dest, uint8_t nTG)
dest[162] = 0xF7; // SysEx end
}
void CMiniDexed::setMasterVolume (float32_t vol)
void CMiniDexed::setMasterVolume(float32_t vol)
{
if(vol < 0.0)
vol = 0.0;
else if(vol > 1.0)
vol = 1.0;
if (vol < 0.0)
vol = 0.0;
else if (vol > 1.0)
vol = 1.0;
// Apply logarithmic scaling to match perceived loudness
vol = powf(vol, 2.0f);
nMasterVolume=vol;
nMasterVolume = vol;
}
std::string CMiniDexed::GetPerformanceFileName(unsigned nID)
@ -2186,3 +2251,201 @@ unsigned CMiniDexed::getModController (unsigned controller, unsigned parameter,
}
}
void CMiniDexed::UpdateNetwork()
{
if (!m_pNet) {
LOGNOTE("CMiniDexed::UpdateNetwork: m_pNet is nullptr, returning early");
return;
}
bool bNetIsRunning = m_pNet->IsRunning();
if (m_pNetDevice->GetType() == NetDeviceTypeEthernet)
bNetIsRunning &= m_pNetDevice->IsLinkUp();
else if (m_pNetDevice->GetType() == NetDeviceTypeWLAN)
bNetIsRunning &= (m_WPASupplicant && m_WPASupplicant->IsConnected());
if (!m_bNetworkInit && bNetIsRunning)
{
LOGNOTE("CMiniDexed::UpdateNetwork: Network became ready, initializing network services");
m_bNetworkInit = true;
CString IPString;
m_pNet->GetConfig()->GetIPAddress()->Format(&IPString);
if (m_UDPMIDI)
{
m_UDPMIDI->Initialize();
}
m_pFTPDaemon = new CFTPDaemon(FTPUSERNAME, FTPPASSWORD);
if (!m_pFTPDaemon->Initialize())
{
LOGERR("Failed to init FTP daemon");
delete m_pFTPDaemon;
m_pFTPDaemon = nullptr;
}
else
{
LOGNOTE("FTP daemon initialized");
}
m_UI.DisplayWrite (IPString, "", "TG1", 0, 1);
m_pmDNSPublisher = new CmDNSPublisher (m_pNet);
assert (m_pmDNSPublisher);
if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), CmDNSPublisher::ServiceTypeAppleMIDI,
5004))
{
LOGPANIC ("Cannot publish mdns service");
}
static constexpr const char *ServiceTypeFTP = "_ftp._tcp";
if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), ServiceTypeFTP, 21))
{
LOGPANIC ("Cannot publish mdns service");
}
if (m_pConfig->GetSyslogEnabled())
{
CIPAddress ServerIP = m_pConfig->GetNetworkSyslogServerIPAddress();
if (ServerIP.IsSet () && !ServerIP.IsNull ())
{
static const u16 usServerPort = 8514;
CString IPString;
ServerIP.Format (&IPString);
LOGNOTE ("Sending log messages to syslog server %s:%u",
(const char *) IPString, (unsigned) usServerPort);
new CSysLogDaemon (m_pNet, ServerIP, usServerPort);
}
}
m_bNetworkReady = true;
}
if (m_bNetworkReady && !bNetIsRunning)
{
LOGNOTE("CMiniDexed::UpdateNetwork: Network disconnected");
m_bNetworkReady = false;
m_pmDNSPublisher->UnpublishService (m_pConfig->GetNetworkHostname());
LOGNOTE("Network disconnected.");
}
else if (!m_bNetworkReady && bNetIsRunning)
{
LOGNOTE("CMiniDexed::UpdateNetwork: Network connection reestablished");
m_bNetworkReady = true;
if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), CmDNSPublisher::ServiceTypeAppleMIDI,
5004))
{
LOGPANIC ("Cannot publish mdns service");
}
static constexpr const char *ServiceTypeFTP = "_ftp._tcp";
if (!m_pmDNSPublisher->PublishService (m_pConfig->GetNetworkHostname(), ServiceTypeFTP, 21))
{
LOGPANIC ("Cannot publish mdns service");
}
m_bNetworkReady = true;
LOGNOTE("Network connection reestablished.");
}
}
bool CMiniDexed::InitNetwork()
{
LOGNOTE("CMiniDexed::InitNetwork called");
assert(m_pNet == nullptr);
TNetDeviceType NetDeviceType = NetDeviceTypeUnknown;
if (m_pConfig->GetNetworkEnabled())
{
LOGNOTE("CMiniDexed::InitNetwork: Network is enabled in configuration");
LOGNOTE("CMiniDexed::InitNetwork: Network type set in configuration: %s", m_pConfig->GetNetworkType());
if (strcmp(m_pConfig->GetNetworkType(), "wlan") == 0)
{
LOGNOTE("CMiniDexed::InitNetwork: Initializing WLAN");
NetDeviceType = NetDeviceTypeWLAN;
m_WLAN = new CBcm4343Device(WLANFirmwarePath);
if (m_WLAN && m_WLAN->Initialize())
{
LOGNOTE("CMiniDexed::InitNetwork: WLAN initialized");
}
else
{
LOGERR("CMiniDexed::InitNetwork: Failed to initialize WLAN, maybe firmware files are missing?");
delete m_WLAN; m_WLAN = nullptr;
return false;
}
}
else if (strcmp(m_pConfig->GetNetworkType(), "ethernet") == 0)
{
LOGNOTE("CMiniDexed::InitNetwork: Initializing Ethernet");
NetDeviceType = NetDeviceTypeEthernet;
}
else
{
LOGERR("CMiniDexed::InitNetwork: Network type is not set, please check your minidexed configuration file.");
NetDeviceType = NetDeviceTypeUnknown;
}
if (NetDeviceType != NetDeviceTypeUnknown)
{
LOGNOTE("CMiniDexed::InitNetwork: Creating CNetSubSystem");
if (m_pConfig->GetNetworkDHCP())
m_pNet = new CNetSubSystem(0, 0, 0, 0, m_pConfig->GetNetworkHostname(), NetDeviceType);
else
m_pNet = new CNetSubSystem(
m_pConfig->GetNetworkIPAddress().Get(),
m_pConfig->GetNetworkSubnetMask().Get(),
m_pConfig->GetNetworkDefaultGateway().Get(),
m_pConfig->GetNetworkDNSServer().Get(),
m_pConfig->GetNetworkHostname(),
NetDeviceType
);
if (!m_pNet || !m_pNet->Initialize(false)) // Check if m_pNet allocation succeeded
{
LOGERR("CMiniDexed::InitNetwork: Failed to initialize network subsystem");
delete m_pNet; m_pNet = nullptr; // Clean up if failed
delete m_WLAN; m_WLAN = nullptr; // Clean up WLAN if allocated
return false; // Return false as network init failed
}
// WPASupplicant needs to be started after netdevice available
if (NetDeviceType == NetDeviceTypeWLAN)
{
LOGNOTE("CMiniDexed::InitNetwork: Initializing WPASupplicant");
m_WPASupplicant = new CWPASupplicant(WLANConfigFile); // Allocate m_WPASupplicant
if (!m_WPASupplicant || !m_WPASupplicant->Initialize())
{
LOGERR("CMiniDexed::InitNetwork: Failed to initialize WPASupplicant, maybe wlan config is missing?");
delete m_WPASupplicant; m_WPASupplicant = nullptr; // Clean up if failed
// Continue without supplicant? Or return false? Decided to continue for now.
}
}
m_pNetDevice = CNetDevice::GetNetDevice(NetDeviceType);
// Allocate UDP MIDI device now that network might be up
m_UDPMIDI = new CUDPMIDIDevice(this, m_pConfig, &m_UI); // Allocate m_UDPMIDI
if (!m_UDPMIDI) {
LOGERR("CMiniDexed::InitNetwork: Failed to allocate UDP MIDI device");
// Clean up other network resources if needed, or handle error appropriately
} else {
// Synchronize UDP MIDI channels with current assignments
for (unsigned nTG = 0; nTG < m_nToneGenerators; ++nTG)
m_UDPMIDI->SetChannel(m_nMIDIChannel[nTG], nTG);
}
}
LOGNOTE("CMiniDexed::InitNetwork: returning %d", m_pNet != nullptr);
return m_pNet != nullptr;
}
else
{
LOGNOTE("CMiniDexed::InitNetwork: Network is not enabled in configuration");
return false;
}
}

@ -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);
@ -229,11 +237,15 @@ public:
void setMasterVolume (float32_t vol);
bool InitNetwork();
void UpdateNetwork();
private:
int16_t ApplyNoteLimits (int16_t pitch, unsigned nTG); // returns < 0 to ignore note
uint8_t m_uchOPMask[CConfig::AllToneGenerators];
void LoadPerformanceParameters(void);
void ProcessSound (void);
const char* GetNetworkDeviceShortName() const;
#ifdef ARM_ALLOW_MULTI_CORE
enum TCoreStatus
@ -325,6 +337,17 @@ private:
CSpinLock m_ReverbSpinLock;
// Network
CNetSubSystem* m_pNet;
CNetDevice* m_pNetDevice;
CBcm4343Device* m_WLAN; // Changed to pointer
CWPASupplicant* m_WPASupplicant; // Changed to pointer
bool m_bNetworkReady;
bool m_bNetworkInit;
CUDPMIDIDevice* m_UDPMIDI; // Changed to pointer
CFTPDaemon* m_pFTPDaemon;
CmDNSPublisher *m_pmDNSPublisher;
bool m_bSavePerformance;
bool m_bSavePerformanceNewFile;
bool m_bSetNewPerformance;

@ -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,17 @@ EncoderPinData=9
MIDIDumpEnabled=0
ProfileEnabled=0
# Network
NetworkEnabled=0
NetworkDHCP=1
# NetworkType ( wlan ; ethernet )
NetworkType=wlan
NetworkHostname=MiniDexed
NetworkIPAddress=0
NetworkSubnetMask=0
NetworkDefaultGateway=0
NetworkDNSServer=0
NetworkSyslogServerIPAddress=0
# Performance
PerformanceSelectToLoad=1

@ -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,345 @@
//
// 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);
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);
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},
@ -705,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);
}
}
@ -1780,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)
@ -2003,5 +2016,3 @@ void CUIMenu::EditTGParameterModulation (CUIMenu *pUIMenu, TMenuEvent Event)
nValue > rParam.Minimum, nValue < rParam.Maximum);
}

@ -401,7 +401,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)
{
@ -416,7 +416,7 @@ void CUserInterface::UIMIDICmdHandler (unsigned nMidiCh, unsigned nMidiCmd, unsi
if (m_pUIButtons)
{
m_pUIButtons->BtnMIDICmdHandler (nMidiCmd, nMidiData1, nMidiData2);
m_pUIButtons->BtnMIDICmdHandler (nMidiType, nMidiData1, nMidiData2);
}
}

@ -56,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
#
# Use fixed master branch of circle-stdlib then re-update
cd circle-stdlib/
git checkout 3bd135d
git reset --hard
git checkout 1111eee # Matches Circle Step49
git submodule update --init --recursive
cd -
#
# Optional update submodules explicitly
cd circle-stdlib/libs/circle
git checkout tags/Step49
cd -
cd circle-stdlib/libs/circle-newlib
#cd circle-stdlib/libs/circle
#git reset --hard
#git checkout tags/Step49
#cd -
#cd circle-stdlib/libs/circle-newlib
#git checkout develop
cd -
#
#cd -
# Use fixed master branch of Synth_Dexed
cd Synth_Dexed/
git reset --hard
git checkout c9f5274
cd -

Loading…
Cancel
Save