Merge branch 'main' into setVelocityScale

pull/446/head
probonopd 2 months ago committed by GitHub
commit 55536983da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 216
      .github/workflows/build.yml
  2. 84
      .github/workflows/pr-comment.yml
  3. 3
      .gitignore
  4. 87
      README.md
  5. 19
      build.sh
  6. 54
      getsysex.sh
  7. 46
      hwconfig/DT-DX.override
  8. 31
      hwconfig/chrissy-mt32-pi-midi-hat.override
  9. 45
      hwconfig/customize.sh
  10. 31
      hwconfig/diyelectromusic-RPi400MIDIAudio.override
  11. 31
      hwconfig/diyelectromusic-RpiMiniDexedHD44780.override
  12. 26
      hwconfig/diyelectromusic-RpiMiniDexedSSD1306.override
  13. 32
      hwconfig/diyelectromusic-RpiQuadDACMiniDexed.override
  14. 30
      hwconfig/diyelectromusic-RpiV1MiniDexedIOBoard.override
  15. 44
      hwconfig/dxeus_machina_eurorack.override
  16. 31
      hwconfig/genxnoise_desktop_module.override
  17. 32
      hwconfig/pirate_audio.override
  18. 33
      hwconfig/serdaco_mp32l.override
  19. 10
      src/Makefile
  20. 5
      src/Rules.mk
  21. 297
      src/config.cpp
  22. 144
      src/config.h
  23. 6
      src/config.txt
  24. 64
      src/kernel.cpp
  25. 4
      src/kernel.h
  26. 227
      src/mididevice.cpp
  27. 15
      src/mididevice.h
  28. 95
      src/midikeyboard.cpp
  29. 19
      src/midikeyboard.h
  30. 1019
      src/minidexed.cpp
  31. 129
      src/minidexed.h
  32. 71
      src/minidexed.ini
  33. 874
      src/net/applemidi.cpp
  34. 111
      src/net/applemidi.h
  35. 42
      src/net/byteorder.h
  36. 111
      src/net/ftpdaemon.cpp
  37. 47
      src/net/ftpdaemon.h
  38. 1218
      src/net/ftpworker.cpp
  39. 157
      src/net/ftpworker.h
  40. 345
      src/net/mdnspublisher.cpp
  41. 90
      src/net/mdnspublisher.h
  42. 89
      src/net/udpmidi.cpp
  43. 57
      src/net/udpmidi.h
  44. 193
      src/net/utility.h
  45. 669
      src/performanceconfig.cpp
  46. 97
      src/performanceconfig.h
  47. 10
      src/serialmididevice.cpp
  48. 3
      src/serialmididevice.h
  49. 17
      src/sysexfileloader.cpp
  50. 1
      src/sysexfileloader.h
  51. 90
      src/udpmididevice.cpp
  52. 55
      src/udpmididevice.h
  53. 98
      src/uibuttons.cpp
  54. 37
      src/uibuttons.h
  55. 539
      src/uimenu.cpp
  56. 11
      src/uimenu.h
  57. 18
      src/usbminidexedmidigadget.h
  58. 118
      src/userinterface.cpp
  59. 7
      src/userinterface.h
  60. 23
      submod.sh

@ -1,3 +1,5 @@
# Build 32-bit and 64-bit separately
name: Build
env:
@ -7,83 +9,147 @@ on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
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: 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 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 -
- uses: actions/upload-artifact@v3
with:
name: ${{ env.artifactName }} # Exported above
path: ./sdcard/*
- 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,64 +1,32 @@
# https://nightly.link/
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
# https://github.com/subsurface/subsurface/blob/master/.github/workflows/artifact-links.yml
name: Add artifact links to pull request
name: Comment on pull request
on:
workflow_run:
workflows: ['Build']
workflows: ["Build"]
types: [completed]
jobs:
pr_comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
artifacts-url-comments:
name: Add artifact links to PR and issues
runs-on: ubuntu-22.04
# Restrict permissions for the GITHUB_TOKEN, https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
permissions:
issues: write
pull-requests: write
actions: read
steps:
- uses: actions/github-script@v6
with:
# This snippet is public-domain, taken from
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
script: |
async function upsertComment(owner, repo, issue_number, purpose, body) {
const {data: comments} = await github.rest.issues.listComments(
{owner, repo, issue_number});
const marker = `<!-- bot: ${purpose} -->`;
body = marker + "\n" + body;
const existing = comments.filter((c) => c.body.includes(marker));
if (existing.length > 0) {
const last = existing[existing.length - 1];
core.info(`Updating comment ${last.id}`);
await github.rest.issues.updateComment({
owner, repo,
body,
comment_id: last.id,
});
} else {
core.info(`Creating a comment in issue / PR #${issue_number}`);
await github.rest.issues.createComment({issue_number, body, owner, repo});
}
}
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
if (!pull_requests.length) {
return core.error("This workflow doesn't match any pull requests!");
}
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
if (!artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request:\n`;
for (const art of artifacts) {
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
core.info("Review thread message body:", body);
for (const pr of pull_requests) {
await upsertComment(owner, repo, pr.number,
"nightly-link", body);
}
- name: Add artifact links to PR and issues
if: github.event.workflow_run.event == 'pull_request'
uses: tonyhallett/artifacts-url-comments@0965ff1a7ae03c5c1644d3c30f956effea4e05ef # v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
prefix: "Build for testing:"
suffix: "Use at your own risk."
format: name
addTo: pull
errorNoArtifacts: false

3
.gitignore vendored

@ -48,4 +48,5 @@ sdcard
CMSIS_5/
Synth_Dexed/
circle-stdlib/
circle-stdlib/
minidexed_*

@ -1,15 +1,19 @@
# MiniDexed ![](https://github.com/probonopd/MiniDexed/actions/workflows/build.yml/badge.svg)
# MiniDexed ![Github Build Status](https://github.com/probonopd/MiniDexed/actions/workflows/build.yml/badge.svg)
![minidexed](https://user-images.githubusercontent.com/2480569/161813414-bb156a1c-efec-44c0-802a-8926412a08e0.jpg)
MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA).
MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known Japanese manufacturer running on a bare metal Raspberry Pi (without a Linux kernel or operating system). On Raspberry Pi 2 and larger, it can run 8 tone generators, not unlike the TX816/TX802 (8 DX7 instances without the keyboard in one box). [Featured by HACKADAY](https://hackaday.com/2022/04/19/bare-metal-gives-this-pi-some-classic-synths/), [Adafruit](https://blog.adafruit.com/2022/04/25/free-yamaha-dx7-synth-emulator-on-a-raspberry-pi/), [The MagPi magazine](https://magpi.raspberrypi.com/articles/mini-dexed) (Issue 142 June 2024, [PDF](https://magpi.raspberrypi.com/issues/142)) and [Synth Geekery](https://www.youtube.com/watch?v=TDSy5nnm0jA).
## Demo songs
Listen to some examples made with MiniDexed by Banana71 [here](https://soundcloud.com/soundplantage/sets/minidexed2).
## Features
- [x] Uses [Synth_Dexed](https://codeberg.org/dcoredump/Synth_Dexed) with [circle-stdlib](https://github.com/smuehlst/circle-stdlib)
- [x] SD card contents can be downloaded from [GitHub Releases](../../releases)
- [x] Runs on all Raspberry Pi models (except Pico); see below for details
- [x] Produces sound on the headphone jack, HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) (better), or a [dedicated DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2c-dac) (best)
- [x] Produces sound on the headphone jack, HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) (better), or a [dedicated DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2s-dac) (best)
- [x] Supports multiple voices through Program Change and Bank Change LSB/MSB MIDI messages
- [x] Loads voices from `.syx` files from SD card (e.g., using `getsysex.sh` or from [Dexed_cart_1.0.zip](http://hsjp.eu/downloads/Dexed/Dexed_cart_1.0.zip))
- [x] Menu structure on optional [HD44780 display](https://www.berrybase.de/sensoren-module/displays/alphanumerische-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) and rotary encoder
@ -18,48 +22,49 @@ MiniDexed is a FM synthesizer closely modeled on the famous DX7 by a well-known
- [x] Allows to configure multiple Dexed instances through `performance.ini` files (e.g., [converted](https://github.com/BobanSpasic/MDX_Vault) from DX1, DX5, TX816, DX7II, TX802)
- [x] Compressor effect
- [x] Reverb effect
- [x] Voices can be edited over MIDI, e.g., using the [synthmata](https://synthmata.github.io/volca-fm/) online editor (requires [additional hardware](https://github.com/probonopd/MiniDexed/wiki/Hardware#usb-midi-device))
- [x] Voices can be edited over MIDI, e.g., using the [synthmata](https://synthmata.github.io/volca-fm/) online editor (requires [additional hardware](https://github.com/probonopd/MiniDexed/wiki/Hardware#usb-midi-devices))
## Introduction
Video about this project by [Floyd Steinberg](https://www.youtube.com/watch?v=Z3t94ceMHJo):
[![](https://i.ytimg.com/vi/Z3t94ceMHJo/sddefault.jpg)](https://www.youtube.com/watch?v=Z3t94ceMHJo)
[![YouTube Video about MiniDexed (Floyd Steinberg)](https://i.ytimg.com/vi/Z3t94ceMHJo/sddefault.jpg)](https://www.youtube.com/watch?v=Z3t94ceMHJo)
## System Requirements
* Raspberry Pi 1, 2, 3, 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out). On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8)
* A [PCM5102A or PCM5122 based DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2c-dac), HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) for good sound quality. If you don't have this, you can use the headphone jack on the Raspberry Pi but on anything but the Raspberry 4 the sound quality will be seriously limited
* Optionally (but highly recommended), an [LCDC1602 Display](https://www.berrybase.de/en/sensors-modules/displays/alphanumeric-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) (with or without i2c "backpack" board) and a [KY-040 rotary encoder](https://www.berrybase.de/en/components/passive-components/potentiometer/rotary-encoder/drehregler/rotary-encoder-mit-breakoutboard-ohne-gewinde-und-mutter)
- Raspberry Pi 1, 2, 3, 4, or 400. Raspberry Pi Zero and Zero 2 can be used but need HDMI or a supported i2s DAC for audio out. On Raspberry Pi 1 and on Raspberry Pi Zero there will be severely limited functionality (only one tone generator instead of 8)
- Raspberry Pi 5 can be used but currently support is experimental: HDMI sound and USB Gadget mode are not available yet, and it is not clear if there are implications for cooling from running MiniDexed. Also, MiniDexed is currently not taking advantage of the higher processing power of the Raspberry Pi 5 yet. *Hence, you may consider using one of the less expensive, older Raspberry Pi boards for your first build.*
- A [PCM5102A or PCM5122 based DAC](https://github.com/probonopd/MiniDexed/wiki/Hardware#i2s-dac), HDMI display or [audio extractor](https://github.com/probonopd/MiniDexed/wiki/Hardware#hdmi-to-audio) for good sound quality. If you don't have this, you can use the headphone jack on the Raspberry Pi but on anything but the Raspberry 4 the sound quality will be seriously limited
- Optionally (but highly recommended), an [LCDC1602 Display](https://www.berrybase.de/en/sensors-modules/displays/alphanumeric-displays/alphanumerisches-lcd-16x2-gr-252-n/gelb) (with or without i2c "backpack" board) and a [KY-040 rotary encoder](https://www.berrybase.de/en/components/passive-components/potentiometer/rotary-encoder/drehregler/rotary-encoder-mit-breakoutboard-ohne-gewinde-und-mutter)
## Usage
* In the case of Raspberry Pi 4, Update the firmware and bootloader to the latest version (not doing this may cause USB reliability issues)
* Download from [GitHub Releases](../../releases)
* Unzip
* Put the files into the root directory of a FAT32 formatted partition on SD/microSD card (Note for small SD cards which are no longer sold: If less than 65525 clusters, you may need to format as FAT16.)
* Put SD/microSD card into Raspberry Pi 1, 2, 3 or 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2c DAC for audio out)
* Attach headphones to the headphone jack using `SoundDevice=pwm` in `minidexed.ini` (default) (poor audio quality)
* Alternatively, attach a PCM5102A or PCM5122 based DAC and select i2c sound output using `SoundDevice=i2s` in `minidexed.ini` (best audio quality)
* Alternatively, attach a HDMI display with sound and select HDMI sound output using `SoundDevice=hdmi` in `minidexed.ini` (this may introduce slight latency)
* Attach a MIDI keyboard via USB (alternatively you can build a circuit that allows you to attach a "traditional" MIDI keyboard using a DIN connector, or use a DIN-MIDI-to-USB adapter)
* If you are using a LCDC1602 with an i2c "backpack" board, then you need to set `LCDI2CAddress=0x27` (or another address your i2c "backpack" board is set to) in `minidexed.ini`
* Boot
* Start playing
* If the system seems to become unresponsive after a few seconds, remove `usbspeed=full` from `cmdline.txt` and repeat ([details](https://github.com/probonopd/MiniDexed/issues/39))
* Optionally, put voices in `.syx` files onto the SD card (e.g., using `getsysex.sh`)
* See the Wiki for [Menu](https://github.com/probonopd/MiniDexed/wiki/Menu) operation
* For voice programming, use any DX series editor (using MIDI sysex), including Dexed
* For library management, use the dedicated [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software
* If something is unclear or does not work, don't hesitate to [ask](https://github.com/probonopd/MiniDexed/discussions/)!
- In the case of Raspberry Pi 4, Update the firmware and bootloader to the latest version (not doing this may cause USB reliability issues)
- Download from [GitHub Releases](../../releases)
- Unzip
- Put the files into the root directory of a FAT32 formatted partition on SD/microSD card (Note for small SD cards which are no longer sold: If less than 65525 clusters, you may need to format as FAT16.)
- Put SD/microSD card into Raspberry Pi 1, 2, 3 or 4, or 400 (Zero and Zero 2 can be used but need HDMI or a supported i2c DAC for audio out)
- Attach headphones to the headphone jack using `SoundDevice=pwm` in `minidexed.ini` (default) (poor audio quality)
- Alternatively, attach a PCM5102A or PCM5122 based DAC and select i2c sound output using `SoundDevice=i2s` in `minidexed.ini` (best audio quality)
- Alternatively, attach a HDMI display with sound and select HDMI sound output using `SoundDevice=hdmi` in `minidexed.ini` (this may introduce slight latency)
- Attach a MIDI keyboard via USB (alternatively you can build a circuit that allows you to attach a "traditional" MIDI keyboard using a DIN connector, or use a DIN-MIDI-to-USB adapter)
- If you are using a LCDC1602 with an i2c "backpack" board, then you need to set `LCDI2CAddress=0x27` (or another address your i2c "backpack" board is set to) in `minidexed.ini`
- Boot
- Start playing
- If the system seems to become unresponsive after a few seconds, remove `usbspeed=full` from `cmdline.txt` and repeat ([details](https://github.com/probonopd/MiniDexed/issues/39))
- Optionally, put voices in `.syx` files onto the SD card (e.g., using `getsysex.sh`)
- See the Wiki for [Menu](https://github.com/probonopd/MiniDexed/wiki/Menu) operation
- For voice programming, use any DX series editor (using MIDI sysex), including Dexed
- For library management, use the dedicated [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software
- If something is unclear or does not work, don't hesitate to [ask](https://github.com/probonopd/MiniDexed/discussions/)!
## Pinout
All devices on Raspberry Pi GPIOs are **optional**.
![](https://user-images.githubusercontent.com/2480569/166105580-da11481c-8fc7-4375-8ab1-3031ab5c6ad0.png)
![Raspberry Pi Pinout/GPIO Diagram](https://user-images.githubusercontent.com/2480569/166105580-da11481c-8fc7-4375-8ab1-3031ab5c6ad0.png)
Please the the [wiki](https://github.com/probonopd/MiniDexed/wiki) for more information.
Please see the [wiki](https://github.com/probonopd/MiniDexed/wiki) for more information.
## Downloading
@ -71,24 +76,30 @@ Please see the [wiki](https://github.com/probonopd/MiniDexed/wiki/Development#bu
## Contributing
This project lives from the contributions of skilled C++ developers, testers, writers, etc. Please see https://github.com/probonopd/MiniDexed/issues.
This project lives from the contributions of skilled C++ developers, testers, writers, etc. Please see <https://github.com/probonopd/MiniDexed/issues>.
## Discussions
We are happy to hear from you. Please join the discussions on https://github.com/probonopd/MiniDexed/discussions.
We are happy to hear from you. Please join the discussions on <https://github.com/probonopd/MiniDexed/discussions>.
## Documentation
Project documentation is at https://github.com/probonopd/MiniDexed/wiki.
Project documentation is at <https://github.com/probonopd/MiniDexed/wiki>.
## Acknowledgements
This project stands on the shoulders of giants. Special thanks to:
* [raphlinus](https://github.com/raphlinus) for the [MSFA](https://github.com/google/music-synthesizer-for-android) sound engine
* [asb2m10](https://github.com/asb2m10/dexed) for the [Dexed](https://github.com/asb2m10/dexed) software
* [dcoredump](https://github.com/dcoredump) for https://codeberg.org/dcoredump/Synth_Dexed, a port of Dexed for embedded systems
* [rsta2](https://github.com/rsta2) for https://github.com/rsta2/circle, the library to run code on bare metal Raspberry Pi (without a Linux kernel or operating system) and for the bulk of the MiniDexed code
* [smuehlst](https://github.com/smuehlst) for https://github.com/smuehlst/circle-stdlib, a version with Standard C and C++ library support
* [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed
* [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault)
- [raphlinus](https://github.com/raphlinus) for the [MSFA](https://github.com/google/music-synthesizer-for-android) sound engine
- [asb2m10](https://github.com/asb2m10/dexed) for the [Dexed](https://github.com/asb2m10/dexed) software
- [dcoredump](https://github.com/dcoredump) for [Synth Dexed](https://codeberg.org/dcoredump/Synth_Dexed), a port of Dexed for embedded systems
- [rsta2](https://github.com/rsta2) for [Circle](https://github.com/rsta2/circle), the library to run code on bare metal Raspberry Pi (without a Linux kernel or operating system) and for the bulk of the MiniDexed code
- [smuehlst](https://github.com/smuehlst) for [circle-stdlib](https://github.com/smuehlst/circle-stdlib), a version with Standard C and C++ library support
- [Banana71](https://github.com/Banana71) for the sound design of the [Soundplantage](https://github.com/Banana71/Soundplantage) performances shipped with MiniDexed
- [BobanSpasic](https://github.com/BobanSpasic) for the [MiniDexedLibrarian](https://github.com/BobanSpasic/MiniDexedLibrarian) software, [MiniDexed performance converter](https://github.com/BobanSpasic/MDX_PerfConv) and [collection of performances for MiniDexed](https://github.com/BobanSpasic/MDX_Vault)
- [diyelectromusic](https://github.com/diyelectromusic/) for many [contributions](https://github.com/probonopd/MiniDexed/commits?author=diyelectromusic)
- [dwhinham/mt32-pi](https://github.com/dwhinham/mt32-pi) for creating networking support for Circle
- [omersiar](https://github.com/omersiar) for porting networking support to MiniDexed
## Stargazers over time
[![Stargazers over time](https://starchart.cc/probonopd/MiniDexed.svg?variant=adaptive)](https://starchart.cc/probonopd/MiniDexed)

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

@ -11,33 +11,33 @@ DIR="https://yamahablackboxes.com/patches/dx7/factory"
# wget -c "${DIR}"/rom1b.syx -O sysex/voice/000001_rom1b.syx
# wget -c "${DIR}"/rom2a.syx -O sysex/voice/000002_rom2a.syx
# wget -c "${DIR}"/rom2b.syx -O sysex/voice/000003_rom2b.syx
wget -c "${DIR}"/rom3a.syx -O sysex/voice/000000_rom3a.syx
wget -c "${DIR}"/rom3b.syx -O sysex/voice/000001_rom3b.syx
wget -c "${DIR}"/rom4a.syx -O sysex/voice/000002_rom4a.syx
wget -c "${DIR}"/rom4b.syx -O sysex/voice/000003_rom4b.syx
wget -c "${DIR}"/rom3a.syx -O sysex/voice/000001_rom3a.syx
wget -c "${DIR}"/rom3b.syx -O sysex/voice/000002_rom3b.syx
wget -c "${DIR}"/rom4a.syx -O sysex/voice/000003_rom4a.syx
wget -c "${DIR}"/rom4b.syx -O sysex/voice/000004_rom4b.syx
DIR="https://yamahablackboxes.com/patches/dx7/vrc"
wget -c "${DIR}"/vrc101b.syx -O sysex/voice/000004_vrc101b.syx
wget -c "${DIR}"/vrc102a.syx -O sysex/voice/000005_vrc102a.syx
wget -c "${DIR}"/vrc102b.syx -O sysex/voice/000006_vrc102b.syx
wget -c "${DIR}"/vrc103a.syx -O sysex/voice/000007_vrc103a.syx
wget -c "${DIR}"/vrc103b.syx -O sysex/voice/000008_vrc103b.syx
wget -c "${DIR}"/vrc104a.syx -O sysex/voice/000009_vrc104a.syx
wget -c "${DIR}"/vrc104b.syx -O sysex/voice/000010_vrc104b.syx
wget -c "${DIR}"/vrc105a.syx -O sysex/voice/000011_vrc105a.syx
wget -c "${DIR}"/vrc105b.syx -O sysex/voice/000012_vrc105b.syx
wget -c "${DIR}"/vrc106a.syx -O sysex/voice/000013_vrc106a.syx
wget -c "${DIR}"/vrc106b.syx -O sysex/voice/000014_vrc106b.syx
wget -c "${DIR}"/vrc107a.syx -O sysex/voice/000015_vrc107a.syx
wget -c "${DIR}"/vrc107b.syx -O sysex/voice/000016_vrc107b.syx
wget -c "${DIR}"/vrc108a.syx -O sysex/voice/000017_vrc108a.syx
wget -c "${DIR}"/vrc108b.syx -O sysex/voice/000018_vrc108b.syx
wget -c "${DIR}"/vrc109a.syx -O sysex/voice/000019_vrc109a.syx
wget -c "${DIR}"/vrc109b.syx -O sysex/voice/000020_vrc109b.syx
wget -c "${DIR}"/vrc110a.syx -O sysex/voice/000021_vrc110a.syx
wget -c "${DIR}"/vrc110b.syx -O sysex/voice/000022_vrc110b.syx
wget -c "${DIR}"/vrc111a.syx -O sysex/voice/000023_vrc111a.syx
wget -c "${DIR}"/vrc111b.syx -O sysex/voice/000024_vrc111b.syx
wget -c "${DIR}"/vrc112a.syx -O sysex/voice/000025_vrc112a.syx
wget -c "${DIR}"/vrc112b.syx -O sysex/voice/000026_vrc112b.syx
wget -c "${DIR}"/vrc101b.syx -O sysex/voice/000005_vrc101b.syx
wget -c "${DIR}"/vrc102a.syx -O sysex/voice/000006_vrc102a.syx
wget -c "${DIR}"/vrc102b.syx -O sysex/voice/000007_vrc102b.syx
wget -c "${DIR}"/vrc103a.syx -O sysex/voice/000008_vrc103a.syx
wget -c "${DIR}"/vrc103b.syx -O sysex/voice/000009_vrc103b.syx
wget -c "${DIR}"/vrc104a.syx -O sysex/voice/000010_vrc104a.syx
wget -c "${DIR}"/vrc104b.syx -O sysex/voice/000011_vrc104b.syx
wget -c "${DIR}"/vrc105a.syx -O sysex/voice/000012_vrc105a.syx
wget -c "${DIR}"/vrc105b.syx -O sysex/voice/000013_vrc105b.syx
wget -c "${DIR}"/vrc106a.syx -O sysex/voice/000014_vrc106a.syx
wget -c "${DIR}"/vrc106b.syx -O sysex/voice/000015_vrc106b.syx
wget -c "${DIR}"/vrc107a.syx -O sysex/voice/000016_vrc107a.syx
wget -c "${DIR}"/vrc107b.syx -O sysex/voice/000017_vrc107b.syx
wget -c "${DIR}"/vrc108a.syx -O sysex/voice/000018_vrc108a.syx
wget -c "${DIR}"/vrc108b.syx -O sysex/voice/000019_vrc108b.syx
wget -c "${DIR}"/vrc109a.syx -O sysex/voice/000020_vrc109a.syx
wget -c "${DIR}"/vrc109b.syx -O sysex/voice/000021_vrc109b.syx
wget -c "${DIR}"/vrc110a.syx -O sysex/voice/000022_vrc110a.syx
wget -c "${DIR}"/vrc110b.syx -O sysex/voice/000023_vrc110b.syx
wget -c "${DIR}"/vrc111a.syx -O sysex/voice/000024_vrc111a.syx
wget -c "${DIR}"/vrc111b.syx -O sysex/voice/000025_vrc111b.syx
wget -c "${DIR}"/vrc112a.syx -O sysex/voice/000026_vrc112a.syx
wget -c "${DIR}"/vrc112b.syx -O sysex/voice/000027_vrc112b.syx

@ -0,0 +1,46 @@
# DTronics DT-DX
# https://www.dtronics.nl/dt-dx
SoundDevice=i2s
SampleRate=48000
ChunkSize=256
DACI2CAddress=0x0
ChannelsSwapped=1
LCDEnabled=1
LCDPinEnable=17
LCDPinRegisterSelect=27
LCDPinReadWrite=16
LCDPinData4=22
LCDPinData5=23
LCDPinData6=24
LCDPinData7=25
LCDI2CAddress=0x00
SSD1306LCDI2CAddress=0x00
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=16
LCDRows=2
ButtonPinPrev=0
ButtonActionPrev=0
ButtonPinNext=0
ButtonActionNext=0
ButtonPinBack=26
ButtonActionBack=longpress
ButtonPinSelect=26
ButtonActionSelect=click
ButtonPinHome=26
ButtonActionHome=doubleclick
ButtonPinShortcut=26
DoubleClickTimeout=400
LongPressTimeout=400
EncoderEnabled=1
EncoderPinClock=6
EncoderPinData=5

@ -0,0 +1,31 @@
# mt32-Pi-Midi-Hat by Chrissy version 1.7.2
# https://github.com/chris-jh/mt32-pi-midi-hat
SoundDevice=i2s
SampleRate=48000
DACI2CAddress=0
ChannelsSwapped=0
MIDIBaudRate=31250
MIDIThru=umidi1,ttyS1
SSD1306LCDI2CAddress=0x3c
SSD1306LCDWidth=128
SSD1306LCDHeight=64
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=4
ButtonPinPrev=22
ButtonActionPrev=click
ButtonPinNext=23
ButtonActionNext=click
ButtonPinBack=27
ButtonActionBack=click
ButtonPinSelect=17
ButtonActionSelect=click
ButtonPinHome=17
ButtonActionHome=longpress
ButtonPinShortcut=0

@ -0,0 +1,45 @@
#!/bin/sh
# This script creates a set of ini files from the *.override files
# to provide customized configurations for well-known hardware
# Find all files named *.override, and run the following on each of them
for file in *.override; do
# Copy the file minidexed.ini to the name of this file but with .ini extension instead
name_of_ini_file=minidexed_$(echo "$file" | sed 's/\.override$/.ini/')
cp ../src/minidexed.ini "$name_of_ini_file"
# Change the values in the ini file, leaving the rest of the file unchanged
while IFS='=' read -r key value; do
# Skip empty lines and comments
if [ -z "$key" ] || [ "${key#\#}" != "$key" ]; then
continue
fi
value=$(echo "$value" | tr -d '\r')
if [ -n "$value" ]; then
sed -i "s/^$key=.*/$key=$value/" "$name_of_ini_file"
fi
done < "$file"
# Process the last line of the override file separately, if it doesn't end with a newline
if [ -n "$key" ]; then
value=$(echo "$value" | tr -d '\r')
if [ -n "$value" ]; then
sed -i "s/^$key=.*/$key=$value/" "$name_of_ini_file"
fi
fi
# Configure genxnoise_desktop_module as USB gadget (as intended by the manufacturer)
case "$file" in
*genxnoise_desktop_module*)
echo "" >> "$name_of_ini_file"
echo "# CAUTION: To prevent hardware damage, DO NOT use the port labeled 'PWR'" >> "$name_of_ini_file"
echo "# (the microUSB port near the edge of the device) when USBGadget is set to 1!" >> "$name_of_ini_file"
echo "# You need to disable USBGadget if you would like to use that port!" >> "$name_of_ini_file"
echo "# See https://github.com/probonopd/MiniDexed/wiki/Hardware#usb-gadget-mode for more information" >> "$name_of_ini_file"
echo "USBGadget=1" >> "$name_of_ini_file"
;;
esac
echo "Created $name_of_ini_file"
done

@ -0,0 +1,31 @@
# diyelectromusic Raspberry Pi 400 MIDI and Audio Module (RPi400MIDIAudio)
# https://diyelectromusic.wordpress.com/2023/12/18/rpi-400-midi-and-audio-pcb-design/
# https://diyelectromusic.wordpress.com/2023/12/18/rpi-400-midi-and-audio-pcb-build-guide/
SoundDevice=i2s
LCDEnabled=1
SSD1306LCDI2CAddress=0x3C
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
ButtonPinPrev=0
ButtonActionPrev=
ButtonPinNext=0
ButtonActionNext=
ButtonPinBack=5
ButtonActionBack=click
ButtonPinSelect=11
ButtonActionSelect=click
ButtonPinHome=6
ButtonActionHome=click
ButtonPinShortcut=11
EncoderEnabled=1
EncoderPinClock=10
EncoderPinData=9

@ -0,0 +1,31 @@
# diyelectromusic Raspberry Pi MiniDexed IO Module (HD44780 Version) (RpiMiniDexedHD44780)
# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiMiniDexedHD44780
# https://diyelectromusic.wordpress.com/2022/08/16/minidexed-raspberry-pi-io-board-part-3/
SoundDevice=i2s
LCDEnabled=1
LCDPinEnable=10
LCDPinRegisterSelect=9
LCDPinReadWrite=0
LCDPinData4=22
LCDPinData5=27
LCDPinData6=17
LCDPinData7=4
LCDI2CAddress=0x00
SSD1306LCDI2CAddress=0
LCDColumns=16
LCDRows=2
EncoderEnabled=1
EncoderPinClock=24
EncoderPinData=23
For the two buttons, and the rotary encoder switch itself:
ButtonPinBack=25
ButtonActionBack=longpress
ButtonPinSelect=25
ButtonActionSelect=click
ButtonPinHome=25
ButtonActionHome=doubleclick
ButtonPinShortcut=25

@ -0,0 +1,26 @@
# diyelectromusic Raspberry Pi MiniDexed IO Module (SSD1306 Version) (RpiMiniDexedSSD1306)
# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiMiniDexedSSD1306
# https://diyelectromusic.com/2022/08/16/minidexed-raspberry-pi-io-board-part-2/
SoundDevice=i2s
LCDEnabled=1
SSD1306LCDI2CAddress=0x3C
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
ButtonPinBack=5
ButtonActionBack=click
ButtonPinSelect=11
ButtonActionSelect=click
ButtonPinHome=6
ButtonActionHome=click
ButtonPinShortcut=11
EncoderEnabled=1
EncoderPinClock=9
EncoderPinData=10

@ -0,0 +1,32 @@
# diyelectromusic MiniDexed Quad DAC (RpiQuadDACMiniDexed)
# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiQuadDACMiniDexed
# https://diyelectromusic.com/2024/06/09/minidexed-quad-dac-pcb-design/
# https://diyelectromusic.com/2024/06/09/minidexed-quad-dac-pcb-build-guide/
SoundDevice=i2s
QuadDAC8Chan=1
LCDEnabled=1
SSD1306LCDI2CAddress=0x3C
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=1
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
ButtonPinPrev=0
ButtonActionPrev=
ButtonPinNext=0
ButtonActionNext=
ButtonPinBack=5
ButtonActionBack=click
ButtonPinSelect=11
ButtonActionSelect=click
ButtonPinHome=6
ButtonActionHome=click
EncoderEnabled=1
EncoderPinClock=10
EncoderPinData=9

@ -0,0 +1,30 @@
# diyelectromusic MiniDexed Raspberry Pi V1 IO Board (RpiV1MiniDexedIOBoard)
# https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RpiV1MiniDexedIOBoard
# https://diyelectromusic.com/2023/02/28/minidexed-raspberry-pi-v1-io-board-part-2/
SoundDevice=i2s
LCDEnabled=1
SSD1306LCDI2CAddress=0x3C
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
ButtonPinPrev=0
ButtonActionPrev=
ButtonPinNext=0
ButtonActionNext=
ButtonPinBack=22
ButtonActionBack=click
ButtonPinSelect=11
ButtonActionSelect=click
ButtonPinHome=27
ButtonActionHome=click
ButtonPinShortcut=11
EncoderEnabled=1
EncoderPinClock=9
EncoderPinData=10

@ -0,0 +1,44 @@
# genXnoise dXeus machina
# https://www.genxnoise.com/product-page/dxeus-machina-minidexed-eurorack-format
SoundDevice=i2s
SampleRate=48000
DACI2CAddress=0
ChannelsSwapped=0
MIDIThru=ttyS1,ttyS1
LCDEnabled=1
LCDPinEnable=17
LCDPinRegisterSelect=4
LCDPinReadWrite=0
LCDPinData4=22
LCDPinData5=23
LCDPinData6=24
LCDPinData7=25
LCDI2CAddress=0x00
SSD1306LCDI2CAddress=0x3c
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
ButtonPinPrev=0
ButtonActionPrev=
ButtonPinNext=0
ButtonActionNext=
ButtonPinBack=11
ButtonActionBack=longpress
ButtonPinSelect=11
ButtonActionSelect=click
ButtonPinHome=11
ButtonActionHome=doubleclick
ButtonPinShortcut=11
EncoderEnabled=1
EncoderPinClock=9
EncoderPinData=10

@ -0,0 +1,31 @@
# genXnoise desktop module
# https://www.genxnoise.com/product-page/minidexed-midi-tone-module
SoundDevice=i2s
DACI2CAddress=0
ChannelsSwapped=0
LCDEnabled=1
LCDPinEnable=17
LCDPinRegisterSelect=4
LCDPinReadWrite=0
LCDPinData4=22
LCDPinData5=23
LCDPinData6=24
LCDPinData7=25
LCDI2CAddress=0x00
SSD1306LCDI2CAddress=0x3c
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
EncoderEnabled=1
EncoderPinClock=10
EncoderPinData=9
USBGadget=1

@ -0,0 +1,32 @@
# Pimoroni Pirate Audio (screen, buttons and audio output)
# https://shop.pimoroni.com/search?q=pirate%20audio
SoundDevice=i2s
LCDEnabled=1
SPIBus=0
ST7789Enabled=1
ST7789Data=9
ST7789Select=1
ST7789Reset=
ST7789Backlight=13
ST7789Width=240
ST7789Height=240
ST7789Rotation=90
LCDColumns=15
LCDRows=2
ButtonPinPrev=5
ButtonActionPrev=click
ButtonPinNext=16
ButtonActionNext=click
ButtonPinBack=24
ButtonActionBack=click
ButtonPinSelect=6
ButtonActionSelect=click
ButtonPinHome=24
ButtonActionHome=doubleclick
ButtonPinShortcut=0
EncoderEnabled=0

@ -0,0 +1,33 @@
# Serdaco MP32L
# https://www.serdashop.com/MP32L
SoundDevice=i2s
SampleRate=48000
DACI2CAddress=0
ChannelsSwapped=0
MIDIBaudRate=31250
MIDIThru=umidi1,ttyS1
SSD1306LCDI2CAddress=0x3c
SSD1306LCDWidth=128
SSD1306LCDHeight=32
SSD1306LCDRotate=1
SSD1306LCDMirror=0
LCDColumns=20
LCDRows=2
ButtonPinPrev=17
ButtonActionPrev=click
ButtonPinNext=27
ButtonActionNext=click
ButtonPinBack=22
ButtonActionBack=click
ButtonPinSelect=23
ButtonActionSelect=click
ButtonPinHome=23
ButtonActionHome=longpress
ButtonPinShortcut=0
EncoderEnabled=0

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

@ -36,16 +36,39 @@ void CConfig::Load (void)
{
m_Properties.Load ();
m_bUSBGadgetMode = m_Properties.GetNumber ("USBGadget", 0) != 0;
// Number of Tone Generators and Polyphony
m_nToneGenerators = m_Properties.GetNumber ("ToneGenerators", DefToneGenerators);
m_nPolyphony = m_Properties.GetNumber ("Polyphony", DefaultNotes);
// At present there are only two options for tone generators: min or max
// and for the Pi 1,2,3 these are the same anyway.
if ((m_nToneGenerators != MinToneGenerators) && (m_nToneGenerators != AllToneGenerators))
{
m_nToneGenerators = DefToneGenerators;
}
if (m_nPolyphony > MaxNotes)
{
m_nPolyphony = DefaultNotes;
}
m_bUSBGadget = m_Properties.GetNumber ("USBGadget", 0) != 0;
m_nUSBGadgetPin = m_Properties.GetNumber ("USBGadgetPin", 0); // Default OFF
SetUSBGadgetMode(m_bUSBGadget); // Might get overriden later by USBGadgetPin state
m_SoundDevice = m_Properties.GetString ("SoundDevice", "pwm");
m_nSampleRate = m_Properties.GetNumber ("SampleRate", 48000);
m_bQuadDAC8Chan = m_Properties.GetNumber ("QuadDAC8Chan", 0) != 0;
if (m_SoundDevice == "hdmi") {
m_nChunkSize = m_Properties.GetNumber ("ChunkSize", 384*6);
}
else
{
#ifdef ARM_ALLOW_MULTI_CORE
m_nChunkSize = m_Properties.GetNumber ("ChunkSize", m_SoundDevice == "hdmi" ? 384*6 : 256);
m_nChunkSize = m_Properties.GetNumber ("ChunkSize", m_bQuadDAC8Chan ? 1024 : 256); // 128 per channel
#else
m_nChunkSize = m_Properties.GetNumber ("ChunkSize", m_SoundDevice == "hdmi" ? 384*6 : 1024);
m_nChunkSize = m_Properties.GetNumber ("ChunkSize", 1024);
#endif
}
m_nDACI2CAddress = m_Properties.GetNumber ("DACI2CAddress", 0);
m_bChannelsSwapped = m_Properties.GetNumber ("ChannelsSwapped", 0) != 0;
@ -92,9 +115,14 @@ void CConfig::Load (void)
m_bMIDIRXProgramChange = m_Properties.GetNumber ("MIDIRXProgramChange", 1) != 0;
m_bIgnoreAllNotesOff = m_Properties.GetNumber ("IgnoreAllNotesOff", 0) != 0;
m_bMIDIAutoVoiceDumpOnPC = m_Properties.GetNumber ("MIDIAutoVoiceDumpOnPC", 1) != 0;
m_bMIDIAutoVoiceDumpOnPC = m_Properties.GetNumber ("MIDIAutoVoiceDumpOnPC", 0) != 0;
m_bHeaderlessSysExVoices = m_Properties.GetNumber ("HeaderlessSysExVoices", 0) != 0;
m_bExpandPCAcrossBanks = m_Properties.GetNumber ("ExpandPCAcrossBanks", 1) != 0;
m_nMIDISystemCCVol = m_Properties.GetNumber ("MIDISystemCCVol", 0);
m_nMIDISystemCCPan = m_Properties.GetNumber ("MIDISystemCCPan", 0);
m_nMIDISystemCCDetune = m_Properties.GetNumber ("MIDISystemCCDetune", 0);
m_nMIDIGlobalExpression = m_Properties.GetNumber ("MIDIGlobalExpression", 0);
m_bLCDEnabled = m_Properties.GetNumber ("LCDEnabled", 0) != 0;
m_nLCDPinEnable = m_Properties.GetNumber ("LCDPinEnable", 4);
@ -112,6 +140,20 @@ void CConfig::Load (void)
m_bSSD1306LCDRotate = m_Properties.GetNumber ("SSD1306LCDRotate", 0) != 0;
m_bSSD1306LCDMirror = m_Properties.GetNumber ("SSD1306LCDMirror", 0) != 0;
m_nSPIBus = m_Properties.GetNumber ("SPIBus", SPI_INACTIVE); // Disabled by default
m_nSPIMode = m_Properties.GetNumber ("SPIMode", SPI_DEF_MODE);
m_nSPIClockKHz = m_Properties.GetNumber ("SPIClockKHz", SPI_DEF_CLOCK);
m_bST7789Enabled = m_Properties.GetNumber ("ST7789Enabled", 0) != 0;
m_nST7789Data = m_Properties.GetNumber ("ST7789Data", 0);
m_nST7789Select = m_Properties.GetNumber ("ST7789Select", 0);
m_nST7789Reset = m_Properties.GetNumber ("ST7789Reset", 0); // optional
m_nST7789Backlight = m_Properties.GetNumber ("ST7789Backlight", 0); // optional
m_nST7789Width = m_Properties.GetNumber ("ST7789Width", 240);
m_nST7789Height = m_Properties.GetNumber ("ST7789Height", 240);
m_nST7789Rotation = m_Properties.GetNumber ("ST7789Rotation", 0);
m_bST7789SmallFont = m_Properties.GetNumber ("ST7789SmallFont", 0) != 0;
m_nLCDColumns = m_Properties.GetNumber ("LCDColumns", 16);
m_nLCDRows = m_Properties.GetNumber ("LCDRows", 2);
@ -133,11 +175,15 @@ void CConfig::Load (void)
m_nButtonPinPgmUp = m_Properties.GetNumber ("ButtonPinPgmUp", 0);
m_nButtonPinPgmDown = m_Properties.GetNumber ("ButtonPinPgmDown", 0);
m_nButtonPinBankUp = m_Properties.GetNumber ("ButtonPinBankUp", 0);
m_nButtonPinBankDown = m_Properties.GetNumber ("ButtonPinBankDown", 0);
m_nButtonPinTGUp = m_Properties.GetNumber ("ButtonPinTGUp", 0);
m_nButtonPinTGDown = m_Properties.GetNumber ("ButtonPinTGDown", 0);
m_ButtonActionPgmUp = m_Properties.GetString ("ButtonActionPgmUp", "");
m_ButtonActionPgmDown = m_Properties.GetString ("ButtonActionPgmDown", "");
m_ButtonActionBankUp = m_Properties.GetString ("ButtonActionBankUp", "");
m_ButtonActionBankDown = m_Properties.GetString ("ButtonActionBankDown", "");
m_ButtonActionTGUp = m_Properties.GetString ("ButtonActionTGUp", "");
m_ButtonActionTGDown = m_Properties.GetString ("ButtonActionTGDown", "");
@ -151,6 +197,8 @@ void CConfig::Load (void)
m_nMIDIButtonPgmUp = m_Properties.GetNumber ("MIDIButtonPgmUp", 0);
m_nMIDIButtonPgmDown = m_Properties.GetNumber ("MIDIButtonPgmDown", 0);
m_nMIDIButtonBankUp = m_Properties.GetNumber ("MIDIButtonBankUp", 0);
m_nMIDIButtonBankDown = m_Properties.GetNumber ("MIDIButtonBankDown", 0);
m_nMIDIButtonTGUp = m_Properties.GetNumber ("MIDIButtonTGUp", 0);
m_nMIDIButtonTGDown = m_Properties.GetNumber ("MIDIButtonTGDown", 0);
@ -163,6 +211,77 @@ void CConfig::Load (void)
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
{
return m_nToneGenerators;
}
unsigned CConfig::GetPolyphony (void) const
{
return m_nPolyphony;
}
unsigned CConfig::GetTGsCore1 (void) const
{
#ifndef ARM_ALLOW_MULTI_CORE
return 0;
#else
if (m_nToneGenerators > MinToneGenerators)
{
return TGsCore1 + TGsCore1Opt;
}
else
{
return TGsCore1;
}
#endif
}
unsigned CConfig::GetTGsCore23 (void) const
{
#ifndef ARM_ALLOW_MULTI_CORE
return 0;
#else
if (m_nToneGenerators > MinToneGenerators)
{
return TGsCore23 + TGsCore23Opt;
}
else
{
return TGsCore23;
}
#endif
}
bool CConfig::GetUSBGadget (void) const
{
return m_bUSBGadget;
}
unsigned CConfig::GetUSBGadgetPin (void) const
{
return m_nUSBGadgetPin;
}
bool CConfig::GetUSBGadgetMode (void) const
@ -170,6 +289,11 @@ bool CConfig::GetUSBGadgetMode (void) const
return m_bUSBGadgetMode;
}
void CConfig::SetUSBGadgetMode (bool USBGadgetMode)
{
m_bUSBGadgetMode = USBGadgetMode;
}
const char *CConfig::GetSoundDevice (void) const
{
return m_SoundDevice.c_str ();
@ -205,6 +329,11 @@ unsigned CConfig::GetEngineType (void) const
return m_EngineType;
}
bool CConfig::GetQuadDAC8Chan (void) const
{
return m_bQuadDAC8Chan;
}
unsigned CConfig::GetMIDIBaudRate (void) const
{
return m_nMIDIBaudRate;
@ -245,6 +374,26 @@ bool CConfig::GetExpandPCAcrossBanks (void) const
return m_bExpandPCAcrossBanks;
}
unsigned CConfig::GetMIDISystemCCVol (void) const
{
return m_nMIDISystemCCVol;
}
unsigned CConfig::GetMIDISystemCCPan (void) const
{
return m_nMIDISystemCCPan;
}
unsigned CConfig::GetMIDISystemCCDetune (void) const
{
return m_nMIDISystemCCDetune;
}
unsigned CConfig::GetMIDIGlobalExpression (void) const
{
return m_nMIDIGlobalExpression;
}
bool CConfig::GetLCDEnabled (void) const
{
return m_bLCDEnabled;
@ -315,6 +464,65 @@ bool CConfig::GetSSD1306LCDMirror (void) const
return m_bSSD1306LCDMirror;
}
unsigned CConfig::GetSPIBus (void) const
{
return m_nSPIBus;
}
unsigned CConfig::GetSPIMode (void) const
{
return m_nSPIMode;
}
unsigned CConfig::GetSPIClockKHz (void) const
{
return m_nSPIClockKHz;
}
bool CConfig::GetST7789Enabled (void) const
{
return m_bST7789Enabled;
}
unsigned CConfig::GetST7789Data (void) const
{
return m_nST7789Data;
}
unsigned CConfig::GetST7789Select (void) const
{
return m_nST7789Select;
}
unsigned CConfig::GetST7789Reset (void) const
{
return m_nST7789Reset;
}
unsigned CConfig::GetST7789Backlight (void) const
{
return m_nST7789Backlight;
}
unsigned CConfig::GetST7789Width (void) const
{
return m_nST7789Width;
}
unsigned CConfig::GetST7789Height (void) const
{
return m_nST7789Height;
}
unsigned CConfig::GetST7789Rotation (void) const
{
return m_nST7789Rotation;
}
bool CConfig::GetST7789SmallFont (void) const
{
return m_bST7789SmallFont;
}
unsigned CConfig::GetLCDColumns (void) const
{
return m_nLCDColumns;
@ -400,6 +608,16 @@ unsigned CConfig::GetButtonPinPgmDown (void) const
return m_nButtonPinPgmDown;
}
unsigned CConfig::GetButtonPinBankUp (void) const
{
return m_nButtonPinBankUp;
}
unsigned CConfig::GetButtonPinBankDown (void) const
{
return m_nButtonPinBankDown;
}
unsigned CConfig::GetButtonPinTGUp (void) const
{
return m_nButtonPinTGUp;
@ -420,6 +638,16 @@ const char *CConfig::GetButtonActionPgmDown (void) const
return m_ButtonActionPgmDown.c_str();
}
const char *CConfig::GetButtonActionBankUp (void) const
{
return m_ButtonActionBankUp.c_str();
}
const char *CConfig::GetButtonActionBankDown (void) const
{
return m_ButtonActionBankDown.c_str();
}
const char *CConfig::GetButtonActionTGUp (void) const
{
return m_ButtonActionTGUp.c_str();
@ -475,6 +703,16 @@ unsigned CConfig::GetMIDIButtonPgmDown (void) const
return m_nMIDIButtonPgmDown;
}
unsigned CConfig::GetMIDIButtonBankUp (void) const
{
return m_nMIDIButtonBankUp;
}
unsigned CConfig::GetMIDIButtonBankDown (void) const
{
return m_nMIDIButtonBankDown;
}
unsigned CConfig::GetMIDIButtonTGUp (void) const
{
return m_nMIDIButtonTGUp;
@ -519,3 +757,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,26 +23,62 @@
#ifndef _config_h
#define _config_h
#include <circle/net/ipaddress.h>
#include <fatfs/ff.h>
#include <Properties/propertiesfatfsfile.h>
#include <circle/sysconfig.h>
#include <string>
#define SPI_INACTIVE 255
#define SPI_DEF_CLOCK 15000 // kHz
#define SPI_DEF_MODE 0 // Default mode (0,1,2,3)
class CConfig // Configuration for MiniDexed
{
public:
// Set maximum, minimum and default numbers of tone generators, depending on Pi version.
// Actual number in can be changed via config settings for some Pis.
#ifndef ARM_ALLOW_MULTI_CORE
static const unsigned ToneGenerators = 1;
// Pi V1 or Zero (single core)
static const unsigned MinToneGenerators = 1;
static const unsigned AllToneGenerators = 1;
static const unsigned DefToneGenerators = AllToneGenerators;
#else
#if (RASPPI==4 || RASPPI==5)
// Pi 4 and 5 quad core
// These are max values, default is to support 8 in total with optional 16 TGs
static const unsigned TGsCore1 = 2; // process 2 TGs on core 1
static const unsigned TGsCore23 = 3; // process 3 TGs on core 2 and 3 each
static const unsigned ToneGenerators = TGsCore1 + 2*TGsCore23;
static const unsigned TGsCore1Opt = 2; // process optional additional 2 TGs on core 1
static const unsigned TGsCore23Opt = 3; // process optional additional 3 TGs on core 2 and 3 each
static const unsigned MinToneGenerators = TGsCore1 + 2*TGsCore23;
static const unsigned AllToneGenerators = TGsCore1 + TGsCore1Opt + 2*TGsCore23 + 2*TGsCore23Opt;
static const unsigned DefToneGenerators = MinToneGenerators;
#else
// Pi 2 or 3 quad core
static const unsigned TGsCore1 = 2; // process 2 TGs on core 1
static const unsigned TGsCore23 = 3; // process 3 TGs on core 2 and 3 each
static const unsigned TGsCore1Opt = 0;
static const unsigned TGsCore23Opt = 0;
static const unsigned MinToneGenerators = TGsCore1 + 2*TGsCore23;
static const unsigned AllToneGenerators = MinToneGenerators;
static const unsigned DefToneGenerators = AllToneGenerators;
#endif
#endif
// Set maximum polyphony, depending on PI version. This can be changed via config settings
#if RASPPI == 1
static const unsigned MaxNotes = 8; // polyphony
static const unsigned MaxNotes = 8;
static const unsigned DefaultNotes = 8;
#elif RASPPI == 4
static const unsigned MaxNotes = 32;
static const unsigned DefaultNotes = 24;
#elif RASPPI == 5
static const unsigned MaxNotes = 32;
static const unsigned DefaultNotes = 32;
#else
static const unsigned MaxNotes = 16;
static const unsigned DefaultNotes = 16;
#endif
static const unsigned MaxChunkSize = 4096;
@ -63,8 +99,17 @@ public:
void Load (void);
// TGs and Polyphony
unsigned GetToneGenerators (void) const;
unsigned GetPolyphony (void) const;
unsigned GetTGsCore1 (void) const;
unsigned GetTGsCore23 (void) const;
// USB Mode
bool GetUSBGadgetMode (void) const; // true if in USB gadget mode
bool GetUSBGadget (void) const;
unsigned GetUSBGadgetPin (void) const;
bool GetUSBGadgetMode (void) const; // true if in USB gadget mode depending on USBGadget and USBGadgetPin
void SetUSBGadgetMode (bool USBGadgetMode);
// Sound device
const char *GetSoundDevice (void) const;
@ -76,6 +121,7 @@ public:
unsigned GetVelocityScale (void) const;
unsigned GetEngineType (void) const;
bool GetQuadDAC8Chan (void) const; // false if not specified
// MIDI
@ -84,9 +130,13 @@ public:
const char *GetMIDIThruOut (void) const; // "" if not specified
bool GetMIDIRXProgramChange (void) const; // true if not specified
bool GetIgnoreAllNotesOff (void) const;
bool GetMIDIAutoVoiceDumpOnPC (void) const; // true if not specified
bool GetMIDIAutoVoiceDumpOnPC (void) const; // false if not specified
bool GetHeaderlessSysExVoices (void) const; // false if not specified
bool GetExpandPCAcrossBanks (void) const; // true if not specified
unsigned GetMIDISystemCCVol (void) const;
unsigned GetMIDISystemCCPan (void) const;
unsigned GetMIDISystemCCDetune (void) const;
unsigned GetMIDIGlobalExpression (void) const;
// HD44780 LCD
// GPIO pin numbers are chip numbers, not header positions
@ -107,6 +157,22 @@ public:
bool GetSSD1306LCDRotate (void) const;
bool GetSSD1306LCDMirror (void) const;
// SPI support
unsigned GetSPIBus (void) const;
unsigned GetSPIMode (void) const;
unsigned GetSPIClockKHz (void) const;
// ST7789 LCD
bool GetST7789Enabled (void) const;
unsigned GetST7789Data (void) const;
unsigned GetST7789Select (void) const;
unsigned GetST7789Reset (void) const;
unsigned GetST7789Backlight (void) const;
unsigned GetST7789Width (void) const;
unsigned GetST7789Height (void) const;
unsigned GetST7789Rotation (void) const;
bool GetST7789SmallFont (void) const;
unsigned GetLCDColumns (void) const;
unsigned GetLCDRows (void) const;
@ -134,12 +200,16 @@ public:
// GPIO pin numbers are chip numbers, not header positions
unsigned GetButtonPinPgmUp (void) const;
unsigned GetButtonPinPgmDown (void) const;
unsigned GetButtonPinBankUp (void) const;
unsigned GetButtonPinBankDown (void) const;
unsigned GetButtonPinTGUp (void) const;
unsigned GetButtonPinTGDown (void) const;
// Action type for buttons: "click", "doubleclick", "longpress", ""
const char *GetButtonActionPgmUp (void) const;
const char *GetButtonActionPgmDown (void) const;
const char *GetButtonActionBankUp (void) const;
const char *GetButtonActionBankDown (void) const;
const char *GetButtonActionTGUp (void) const;
const char *GetButtonActionTGDown (void) const;
@ -155,6 +225,8 @@ public:
// MIDI Button Program and TG Selection
unsigned GetMIDIButtonPgmUp (void) const;
unsigned GetMIDIButtonPgmDown (void) const;
unsigned GetMIDIButtonBankUp (void) const;
unsigned GetMIDIButtonBankDown (void) const;
unsigned GetMIDIButtonTGUp (void) const;
unsigned GetMIDIButtonTGDown (void) const;
@ -172,9 +244,28 @@ 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;
unsigned m_nToneGenerators;
unsigned m_nPolyphony;
bool m_bUSBGadget;
unsigned m_nUSBGadgetPin;
bool m_bUSBGadgetMode;
std::string m_SoundDevice;
@ -186,6 +277,7 @@ private:
unsigned m_VelocityScale;
unsigned m_EngineType;
bool m_bQuadDAC8Chan;
unsigned m_nMIDIBaudRate;
std::string m_MIDIThruIn;
@ -195,6 +287,10 @@ private:
bool m_bMIDIAutoVoiceDumpOnPC;
bool m_bHeaderlessSysExVoices;
bool m_bExpandPCAcrossBanks;
unsigned m_nMIDISystemCCVol;
unsigned m_nMIDISystemCCPan;
unsigned m_nMIDISystemCCDetune;
unsigned m_nMIDIGlobalExpression;
bool m_bLCDEnabled;
unsigned m_nLCDPinEnable;
@ -211,7 +307,21 @@ private:
unsigned m_nSSD1306LCDHeight;
bool m_bSSD1306LCDRotate;
bool m_bSSD1306LCDMirror;
unsigned m_nSPIBus;
unsigned m_nSPIMode;
unsigned m_nSPIClockKHz;
bool m_bST7789Enabled;
unsigned m_nST7789Data;
unsigned m_nST7789Select;
unsigned m_nST7789Reset;
unsigned m_nST7789Backlight;
unsigned m_nST7789Width;
unsigned m_nST7789Height;
unsigned m_nST7789Rotation;
unsigned m_bST7789SmallFont;
unsigned m_nLCDColumns;
unsigned m_nLCDRows;
@ -223,6 +333,8 @@ private:
unsigned m_nButtonPinShortcut;
unsigned m_nButtonPinPgmUp;
unsigned m_nButtonPinPgmDown;
unsigned m_nButtonPinBankUp;
unsigned m_nButtonPinBankDown;
unsigned m_nButtonPinTGUp;
unsigned m_nButtonPinTGDown;
@ -233,6 +345,8 @@ private:
std::string m_ButtonActionHome;
std::string m_ButtonActionPgmUp;
std::string m_ButtonActionPgmDown;
std::string m_ButtonActionBankUp;
std::string m_ButtonActionBankDown;
std::string m_ButtonActionTGUp;
std::string m_ButtonActionTGDown;
@ -248,6 +362,8 @@ private:
unsigned m_nMIDIButtonHome;
unsigned m_nMIDIButtonPgmUp;
unsigned m_nMIDIButtonPgmDown;
unsigned m_nMIDIButtonBankUp;
unsigned m_nMIDIButtonBankDown;
unsigned m_nMIDIButtonTGUp;
unsigned m_nMIDIButtonTGDown;
@ -259,6 +375,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

@ -10,7 +10,7 @@ gpu_mem=16
disable_overscan=0
#
# Use 64-bit for RPi 3, 4, 400 and Zero 2, and 32-bit for all other models
# Use 64-bit for RPi 3, 4, 400, 5 and Zero 2, and 32-bit for all other models
#
[pi3]
@ -24,3 +24,7 @@ kernel=kernel8-rpi4.img
# Zero 2 W
[pi02]
arm_64bit=1
[pi5]
arm_64bit=1
kernel=kernel_2712.img

@ -20,19 +20,24 @@
#include "kernel.h"
#include <circle/logger.h>
#include <circle/synchronize.h>
#include <circle/gpiopin.h>
#include <assert.h>
#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),
m_pSPIMaster (nullptr),
m_pDexed (0)
{
s_pThis = this;
@ -66,23 +71,76 @@ bool CKernel::Initialize (void)
m_Config.Load ();
if (m_Config.GetUSBGadgetMode())
unsigned nSPIMaster = m_Config.GetSPIBus();
unsigned nSPIMode = m_Config.GetSPIMode();
unsigned long nSPIClock = 1000 * m_Config.GetSPIClockKHz();
#if RASPPI<4
// By default older RPI versions use SPI 0.
// It is possible to build circle to support SPI 1 for
// devices that use the 40-pin header, but that isn't
// enabled at present...
if (nSPIMaster == 0)
#else
// RPI 4+ has several possible SPI Bus Configurations.
// As mentioned above, SPI 1 is not built by default.
// See circle/include/circle/spimaster.h
if (nSPIMaster == 0 || nSPIMaster == 3 || nSPIMaster == 4 || nSPIMaster == 5 || nSPIMaster == 6)
#endif
{
unsigned nCPHA = (nSPIMode & 1) ? 1 : 0;
unsigned nCPOL = (nSPIMode & 2) ? 1 : 0;
m_pSPIMaster = new CSPIMaster (nSPIClock, nCPOL, nCPHA, nSPIMaster);
if (!m_pSPIMaster->Initialize())
{
delete (m_pSPIMaster);
m_pSPIMaster = nullptr;
}
}
bool bUSBGadgetMode = false;
if (m_Config.GetUSBGadget())
{
unsigned nUSBGadgetPin = m_Config.GetUSBGadgetPin();
if (nUSBGadgetPin == 0)
{
// No hardware config option
bUSBGadgetMode = true;
}
else
{
// State of USB Gadget Mode determined by state of the pin.
// Pulled down = enable USB Gadget mode
CGPIOPin usbGadgetPin(nUSBGadgetPin, GPIOModeInputPullUp);
if (usbGadgetPin.Read() == 0)
{
bUSBGadgetMode = true;
}
}
}
if (bUSBGadgetMode)
{
#if RASPPI==5
#warning No support for USB Gadget Mode on RPI 5 yet
#else
// Run the USB stack in USB Gadget (device) mode
m_pUSB = new CUSBMiniDexedMIDIGadget (&mInterrupt);
#endif
}
else
{
// Run the USB stack in USB Host (default) mode
m_pUSB = new CUSBHCIDevice (&mInterrupt, &mTimer, TRUE);
}
m_Config.SetUSBGadgetMode(bUSBGadgetMode);
if (!m_pUSB->Initialize ())
{
return FALSE;
}
m_pDexed = new CMiniDexed (&m_Config, &mInterrupt, &m_GPIOManager, &m_I2CMaster,
m_pDexed = new CMiniDexed (&m_Config, &mInterrupt, &m_GPIOManager, &m_I2CMaster, m_pSPIMaster,
&mFileSystem);
assert (m_pDexed);

@ -24,7 +24,9 @@
#include <circle/cputhrottle.h>
#include <circle/gpiomanager.h>
#include <circle/i2cmaster.h>
#include <circle/spimaster.h>
#include <circle/usb/usbcontroller.h>
#include <circle/sched/scheduler.h>
#include "config.h"
#include "minidexed.h"
@ -54,8 +56,10 @@ private:
CCPUThrottle m_CPUThrottle;
CGPIOManager m_GPIOManager;
CI2CMaster m_I2CMaster;
CSPIMaster *m_pSPIMaster;
CMiniDexed *m_pDexed;
CUSBController *m_pUSB;
CScheduler m_Scheduler;
static CKernel *s_pThis;
};

@ -37,14 +37,15 @@ LOGMODULE ("mididevice");
#define MIDI_CHANNEL_AFTERTOUCH 0b1101 // right now Synth_Dexed just manage Channel Aftertouch not Polyphonic AT -> 0b1010
#define MIDI_CONTROL_CHANGE 0b1011
#define MIDI_CC_BANK_SELECT_MSB 0
#define MIDI_CC_MODULATION 1
#define MIDI_CC_MODULATION 1
#define MIDI_CC_BREATH_CONTROLLER 2
#define MIDI_CC_FOOT_PEDAL 4
#define MIDI_CC_VOLUME 7
#define MIDI_CC_VOLUME 7
#define MIDI_CC_PAN_POSITION 10
#define MIDI_CC_EXPRESSION 11
#define MIDI_CC_BANK_SELECT_LSB 32
#define MIDI_CC_BANK_SUSTAIN 64
#define MIDI_CC_RESONANCE 71
#define MIDI_CC_RESONANCE 71
#define MIDI_CC_FREQUENCY_CUTOFF 74
#define MIDI_CC_REVERB_LEVEL 91
#define MIDI_CC_DETUNE_LEVEL 94
@ -53,6 +54,21 @@ LOGMODULE ("mididevice");
#define MIDI_PROGRAM_CHANGE 0b1100
#define MIDI_PITCH_BEND 0b1110
// MIDI "System" level (i.e. all TG) custom CC maps
// Note: Even if number of TGs is not 8, there are only 8
// available to be used in the mappings here.
#define NUM_MIDI_CC_MAPS 8
const unsigned MIDISystemCCMap[NUM_MIDI_CC_MAPS][8] = {
{0,0,0,0,0,0,0,0}, // 0 = disabled
{16,17,18,19,80,81,82,83}, // 1 = General Purpose Controllers 1-8
{20,21,22,23,24,25,26,27},
{52,53,54,55,56,57,58,59},
{102,103,104,105,106,107,108,109},
{110,111,112,113,114,115,116,117},
{3,9,14,15,28,29,30,31},
{35,41,46,47,60,61,62,63}
};
#define MIDI_SYSTEM_EXCLUSIVE_BEGIN 0xF0
#define MIDI_SYSTEM_EXCLUSIVE_END 0xF7
#define MIDI_TIMING_CLOCK 0xF8
@ -65,10 +81,47 @@ CMIDIDevice::CMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInter
m_pConfig (pConfig),
m_pUI (pUI)
{
for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++)
for (unsigned nTG = 0; nTG < CConfig::AllToneGenerators; nTG++)
{
m_ChannelMap[nTG] = Disabled;
}
m_nMIDISystemCCVol = m_pConfig->GetMIDISystemCCVol();
m_nMIDISystemCCPan = m_pConfig->GetMIDISystemCCPan();
m_nMIDISystemCCDetune = m_pConfig->GetMIDISystemCCDetune();
m_MIDISystemCCBitmap[0] = 0;
m_MIDISystemCCBitmap[1] = 0;
m_MIDISystemCCBitmap[2] = 0;
m_MIDISystemCCBitmap[3] = 0;
m_nMIDIGlobalExpression = m_pConfig->GetMIDIGlobalExpression();
// convert from config channels 1..16 to internal channels
if ((m_nMIDIGlobalExpression >= 1) && (m_nMIDIGlobalExpression <= 16)) {
m_nMIDIGlobalExpression = m_nMIDIGlobalExpression - 1;
} else {
// Either disabled or OMNI means disabled
m_nMIDIGlobalExpression = Disabled;
}
for (int tg=0; tg<8; tg++)
{
if (m_nMIDISystemCCVol != 0) {
u8 cc = MIDISystemCCMap[m_nMIDISystemCCVol][tg];
m_MIDISystemCCBitmap[cc>>5] |= (1<<(cc%32));
}
if (m_nMIDISystemCCPan != 0) {
u8 cc = MIDISystemCCMap[m_nMIDISystemCCPan][tg];
m_MIDISystemCCBitmap[cc>>5] |= (1<<(cc%32));
}
if (m_nMIDISystemCCDetune != 0) {
u8 cc = MIDISystemCCMap[m_nMIDISystemCCDetune][tg];
m_MIDISystemCCBitmap[cc>>5] |= (1<<(cc%32));
}
}
if (m_pConfig->GetMIDIDumpEnabled ()) {
LOGNOTE("MIDI System CC Map: %08X %08X %08X %08X", m_MIDISystemCCBitmap[3],m_MIDISystemCCBitmap[2],m_MIDISystemCCBitmap[1],m_MIDISystemCCBitmap[0]);
}
}
CMIDIDevice::~CMIDIDevice (void)
@ -78,13 +131,13 @@ CMIDIDevice::~CMIDIDevice (void)
void CMIDIDevice::SetChannel (u8 ucChannel, unsigned nTG)
{
assert (nTG < CConfig::ToneGenerators);
assert (nTG < CConfig::AllToneGenerators);
m_ChannelMap[nTG] = ucChannel;
}
u8 CMIDIDevice::GetChannel (unsigned nTG) const
{
assert (nTG < CConfig::ToneGenerators);
assert (nTG < CConfig::AllToneGenerators);
return m_ChannelMap[nTG];
}
@ -101,35 +154,36 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
if ( pMessage[0] != MIDI_TIMING_CLOCK
&& pMessage[0] != MIDI_ACTIVE_SENSING)
{
printf ("MIDI%u: %02X\n", nCable, (unsigned) pMessage[0]);
fprintf (stderr, "MIDI%u: %02X\n", nCable, (unsigned) pMessage[0]);
}
break;
case 2:
printf ("MIDI%u: %02X %02X\n", nCable,
fprintf (stderr, "MIDI%u: %02X %02X\n", nCable,
(unsigned) pMessage[0], (unsigned) pMessage[1]);
break;
case 3:
printf ("MIDI%u: %02X %02X %02X\n", nCable,
fprintf (stderr, "MIDI%u: %02X %02X %02X\n", nCable,
(unsigned) pMessage[0], (unsigned) pMessage[1],
(unsigned) pMessage[2]);
break;
default:
switch(pMessage[0])
{
case MIDI_SYSTEM_EXCLUSIVE_BEGIN:
printf("MIDI%u: SysEx data length: [%d]:",nCable, uint16_t(nLength));
fprintf(stderr, "MIDI%u: SysEx data length: [%d]:",nCable, uint16_t(nLength));
for (uint16_t i = 0; i < nLength; i++)
{
if((i % 16) == 0)
printf("\n%04d:",i);
printf(" 0x%02x",pMessage[i]);
fprintf(stderr, "\n%04d:",i);
fprintf(stderr, " 0x%02x",pMessage[i]);
}
printf("\n");
fprintf(stderr, "\n");
break;
default:
printf("MIDI%u: Unhandled MIDI event type %0x02x\n",nCable,pMessage[0]);
fprintf(stderr, "MIDI%u: Unhandled MIDI event type %0x02x\n",nCable,pMessage[0]);
}
break;
}
@ -175,18 +229,83 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
u8 ucType = ucStatus >> 4;
// GLOBAL MIDI SYSEX
if (pMessage[0] == MIDI_SYSTEM_EXCLUSIVE_BEGIN && pMessage[3] == 0x04 && pMessage[4] == 0x01 && pMessage[nLength-1] == MIDI_SYSTEM_EXCLUSIVE_END) // MASTER VOLUME
//
// Master Volume is set using a MIDI SysEx message as follows:
// F0 Start of SysEx
// 7F System Realtime SysEx
// 7F SysEx "channel" - 7F = all devices
// 04 Device Control
// 01 Master Volume Device Control
// LL Low 7-bits of 14-bit volume
// HH High 7-bits of 14-bit volume
// F7 End SysEx
//
// See MIDI Specification "Device Control"
// "Master Volume and Master Balance"
//
// Need to scale the volume parameter to fit
// a 14-bit value: 0..16383
// and then split into LSB/MSB.
if (nLength == 8 &&
pMessage[0] == MIDI_SYSTEM_EXCLUSIVE_BEGIN &&
pMessage[1] == 0x7F &&
pMessage[2] == 0x7F &&
pMessage[3] == 0x04 &&
pMessage[4] == 0x01 &&
// pMessage[5] and pMessage[6] = LSB+MSB
pMessage[7] == MIDI_SYSTEM_EXCLUSIVE_END
) // MASTER VOLUME
{
float32_t nMasterVolume=((pMessage[5] & 0x7c) & ((pMessage[6] & 0x7c) <<7))/(1<<14);
LOGNOTE("Master volume: %f",nMasterVolume);
m_pSynthesizer->setMasterVolume(nMasterVolume);
// Convert LSB/MSB to 14-bit integer volume
uint32_t nMasterVolume=((pMessage[5] & 0x7F) | ((pMessage[6] & 0x7F) <<7));
// Convert to value between 0.0 and 1.0
float32_t fMasterVolume = (float32_t)nMasterVolume / 16384.0;
//printf("Master volume: %f (%d)\n",fMasterVolume, nMasterVolume);
m_pSynthesizer->setMasterVolume(fMasterVolume);
}
else
{
// Perform any MiniDexed level MIDI handling before specific Tone Generators
unsigned nPerfCh = m_pSynthesizer->GetPerformanceSelectChannel();
switch (ucType)
{
case MIDI_CONTROL_CHANGE:
// Check for performance PC messages
if (nPerfCh != Disabled)
{
if ((ucChannel == nPerfCh) || (nPerfCh == OmniMode))
{
if (pMessage[1] == MIDI_CC_BANK_SELECT_MSB)
{
m_pSynthesizer->BankSelectMSBPerformance (pMessage[2]);
}
else if (pMessage[1] == MIDI_CC_BANK_SELECT_LSB)
{
m_pSynthesizer->BankSelectLSBPerformance (pMessage[2]);
}
else
{
// Ignore any other CC messages at this time
}
}
}
if (m_nMIDIGlobalExpression != Disabled)
{
// Expression is global so check for expression MIDI channel
// NB: OMNI not supported
if (ucChannel == m_nMIDIGlobalExpression) {
// Send to all TGs regardless of their own channel
for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators(); nTG++) {
m_pSynthesizer->SetExpression (pMessage[2], nTG);
}
}
}
if (nLength == 3)
{
m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]);
}
break;
case MIDI_NOTE_OFF:
case MIDI_NOTE_ON:
if (nLength < 3)
@ -195,11 +314,11 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
}
m_pUI->UIMIDICmdHandler (ucChannel, ucStatus & 0xF0, pMessage[1], pMessage[2]);
break;
case MIDI_PROGRAM_CHANGE:
// Check for performance PC messages
if( m_pConfig->GetMIDIRXProgramChange() )
{
unsigned nPerfCh = m_pSynthesizer->GetPerformanceSelectChannel();
if( nPerfCh != Disabled)
{
if ((ucChannel == nPerfCh) || (nPerfCh == OmniMode))
@ -212,8 +331,10 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
break;
}
// Process MIDI for each Tone Generator
for (unsigned nTG = 0; nTG < CConfig::ToneGenerators; nTG++)
// Process MIDI for each active Tone Generator
bool bSystemCCHandled = false;
bool bSystemCCChecked = false;
for (unsigned nTG = 0; nTG < m_pConfig->GetToneGenerators() && !bSystemCCHandled; nTG++)
{
if (ucStatus == MIDI_SYSTEM_EXCLUSIVE_BEGIN)
{
@ -298,6 +419,13 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
m_pSynthesizer->SetPan (pMessage[2], nTG);
break;
case MIDI_CC_EXPRESSION:
if (m_nMIDIGlobalExpression == Disabled) {
// Expression is per channel only
m_pSynthesizer->SetExpression (pMessage[2], nTG);
}
break;
case MIDI_CC_BANK_SELECT_MSB:
m_pSynthesizer->BankSelectMSB (pMessage[2], nTG);
break;
@ -347,6 +475,17 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign
m_pSynthesizer->notesOff (pMessage[2], nTG);
}
break;
default:
// Check for system-level, cross-TG MIDI Controls, but only do it once.
// Also, if successfully handled, then no need to process other TGs,
// so it is possible to break out of the main TG loop too.
// Note: We handle this here so we get the TG MIDI channel checking.
if (!bSystemCCChecked) {
bSystemCCHandled = HandleMIDISystemCC(pMessage[1], pMessage[2]);
bSystemCCChecked = true;
}
break;
}
break;
@ -392,6 +531,52 @@ void CMIDIDevice::AddDevice (const char *pDeviceName)
s_DeviceMap.insert (std::pair<std::string, CMIDIDevice *> (pDeviceName, this));
}
bool CMIDIDevice::HandleMIDISystemCC(const u8 ucCC, const u8 ucCCval)
{
// This only makes sense when there are at least 8 TGs.
// Note: If more than 8 TGs then only 8 TGs are controllable this way.
if (m_pConfig->GetToneGenerators() < 8) {
return false;
}
// Quickly reject any CCs not in the configured maps
if ((m_MIDISystemCCBitmap[ucCC>>5] & (1<<(ucCC%32))) == 0) {
// Not in the map
return false;
}
// Not looking for duplicate CCs so return once handled
for (unsigned tg=0; tg<8; tg++) {
if (m_nMIDISystemCCVol != 0) {
if (ucCC == MIDISystemCCMap[m_nMIDISystemCCVol][tg]) {
m_pSynthesizer->SetVolume (ucCCval, tg);
return true;
}
}
if (m_nMIDISystemCCPan != 0) {
if (ucCC == MIDISystemCCMap[m_nMIDISystemCCPan][tg]) {
m_pSynthesizer->SetPan (ucCCval, tg);
return true;
}
}
if (m_nMIDISystemCCDetune != 0) {
if (ucCC == MIDISystemCCMap[m_nMIDISystemCCDetune][tg]) {
if (ucCCval == 0)
{
m_pSynthesizer->SetMasterTune (0, tg);
}
else
{
m_pSynthesizer->SetMasterTune (maplong (ucCCval, 1, 127, -99, 99), tg);
}
return true;
}
}
}
return false;
}
void CMIDIDevice::HandleSystemExclusive(const uint8_t* pMessage, const size_t nLength, const unsigned nCable, const uint8_t nTG)
{
int16_t sysex_return;

@ -30,6 +30,9 @@
#include <circle/spinlock.h>
#include "userinterface.h"
#define MAX_DX7_SYSEX_LENGTH 4104
#define MAX_MIDI_MESSAGE MAX_DX7_SYSEX_LENGTH
class CMiniDexed;
class CMIDIDevice
@ -57,12 +60,22 @@ protected:
void MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsigned nCable = 0);
void AddDevice (const char *pDeviceName);
void HandleSystemExclusive(const uint8_t* pMessage, const size_t nLength, const unsigned nCable, const uint8_t nTG);
private:
bool HandleMIDISystemCC(const u8 ucCC, const u8 ucCCval);
private:
CMiniDexed *m_pSynthesizer;
CConfig *m_pConfig;
CUserInterface *m_pUI;
u8 m_ChannelMap[CConfig::ToneGenerators];
u8 m_ChannelMap[CConfig::AllToneGenerators];
unsigned m_nMIDISystemCCVol;
unsigned m_nMIDISystemCCPan;
unsigned m_nMIDISystemCCDetune;
u32 m_MIDISystemCCBitmap[4]; // to allow for 128 bit entries
unsigned m_nMIDIGlobalExpression;
std::string m_DeviceName;

@ -25,24 +25,12 @@
#include <cstring>
#include <assert.h>
CMIDIKeyboard *CMIDIKeyboard::s_pThis[MaxInstances] = {0};
TMIDIPacketHandler * const CMIDIKeyboard::s_pMIDIPacketHandler[MaxInstances] =
{
MIDIPacketHandler0,
MIDIPacketHandler1,
MIDIPacketHandler2,
MIDIPacketHandler3
};
CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance)
: CMIDIDevice (pSynthesizer, pConfig, pUI),
m_nSysExIdx (0),
m_nInstance (nInstance),
m_pMIDIDevice (0)
{
assert (m_nInstance < MaxInstances);
s_pThis[m_nInstance] = this;
m_DeviceName.Format ("umidi%u", nInstance+1);
AddDevice (m_DeviceName);
@ -50,8 +38,6 @@ CMIDIKeyboard::CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserI
CMIDIKeyboard::~CMIDIKeyboard (void)
{
assert (m_nInstance < MaxInstances);
s_pThis[m_nInstance] = 0;
}
void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated)
@ -80,8 +66,7 @@ void CMIDIKeyboard::Process (boolean bPlugAndPlayUpdated)
(CUSBMIDIDevice *) CDeviceNameService::Get ()->GetDevice (m_DeviceName, FALSE);
if (m_pMIDIDevice != 0)
{
assert (m_nInstance < MaxInstances);
m_pMIDIDevice->RegisterPacketHandler (s_pMIDIPacketHandler[m_nInstance]);
m_pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler, this);
m_pMIDIDevice->RegisterRemovedHandler (DeviceRemovedHandler, this);
}
@ -100,28 +85,72 @@ void CMIDIKeyboard::Send (const u8 *pMessage, size_t nLength, unsigned nCable)
m_SendQueue.push (Entry);
}
void CMIDIKeyboard::MIDIPacketHandler0 (unsigned nCable, u8 *pPacket, unsigned nLength)
// Most packets will be passed straight onto the main MIDI message handler
// but SysEx messages are multiple USB packets and so will need building up
// before parsing.
void CMIDIKeyboard::USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable, unsigned nDevice)
{
assert (s_pThis[0] != 0);
s_pThis[0]->MIDIMessageHandler (pPacket, nLength, nCable);
}
assert (nDevice == m_nInstance + 1);
void CMIDIKeyboard::MIDIPacketHandler1 (unsigned nCable, u8 *pPacket, unsigned nLength)
{
assert (s_pThis[1] != 0);
s_pThis[1]->MIDIMessageHandler (pPacket, nLength, nCable);
if ((pPacket[0] == 0xF0) && (m_nSysExIdx == 0))
{
// Start of SysEx message
//printf("SysEx Start Idx=%d, (%d)\n", m_nSysExIdx, nLength);
for (unsigned i=0; i<USB_SYSEX_BUFFER_SIZE; i++) {
m_SysEx[i] = 0;
}
for (unsigned i=0; i<nLength; i++) {
m_SysEx[m_nSysExIdx++] = pPacket[i];
}
}
else if (m_nSysExIdx != 0)
{
// Continue processing SysEx message
//printf("SysEx Packet Idx=%d, (%d)\n", m_nSysExIdx, nLength);
for (unsigned i=0; i<nLength; i++) {
if (pPacket[i] == 0xF8 || pPacket[i] == 0xFA || pPacket[i] == 0xFB || pPacket[i] == 0xFC || pPacket[i] == 0xFE || pPacket[i] == 0xFF) {
// Singe-byte System Realtime Messages can happen at any time!
MIDIMessageHandler (&pPacket[i], 1, nCable);
}
else if (m_nSysExIdx >= USB_SYSEX_BUFFER_SIZE) {
// Run out of space, so reset and ignore rest of the message
m_nSysExIdx = 0;
break;
}
else if (pPacket[i] == 0xF7) {
// End of SysEx message
m_SysEx[m_nSysExIdx++] = pPacket[i];
//printf ("SysEx End Idx=%d\n", m_nSysExIdx);
MIDIMessageHandler (m_SysEx, m_nSysExIdx, nCable);
// Reset ready for next time
m_nSysExIdx = 0;
}
else if ((pPacket[i] & 0x80) != 0) {
// Received another command, so reset processing as something has gone wrong
//printf ("SysEx Reset\n");
m_nSysExIdx = 0;
break;
}
else
{
// Store the byte
m_SysEx[m_nSysExIdx++] = pPacket[i];
}
}
}
else
{
// Assume it is a standard message
MIDIMessageHandler (pPacket, nLength, nCable);
}
}
void CMIDIKeyboard::MIDIPacketHandler2 (unsigned nCable, u8 *pPacket, unsigned nLength)
void CMIDIKeyboard::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam)
{
assert (s_pThis[2] != 0);
s_pThis[2]->MIDIMessageHandler (pPacket, nLength, nCable);
}
CMIDIKeyboard *pThis = static_cast<CMIDIKeyboard *> (pParam);
assert (pThis != 0);
void CMIDIKeyboard::MIDIPacketHandler3 (unsigned nCable, u8 *pPacket, unsigned nLength)
{
assert (s_pThis[3] != 0);
s_pThis[3]->MIDIMessageHandler (pPacket, nLength, nCable);
pThis->USBMIDIMessageHandler (pPacket, nLength, nCable, nDevice);
}
void CMIDIKeyboard::DeviceRemovedHandler (CDevice *pDevice, void *pContext)

@ -31,13 +31,12 @@
#include <circle/types.h>
#include <queue>
#define USB_SYSEX_BUFFER_SIZE (MAX_DX7_SYSEX_LENGTH+128) // Allow a bit spare to handle unexpected SysEx messages
class CMiniDexed;
class CMIDIKeyboard : public CMIDIDevice
{
public:
static const unsigned MaxInstances = 4;
public:
CMIDIKeyboard (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI, unsigned nInstance = 0);
~CMIDIKeyboard (void);
@ -47,12 +46,10 @@ public:
void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) override;
private:
static void MIDIPacketHandler0 (unsigned nCable, u8 *pPacket, unsigned nLength);
static void MIDIPacketHandler1 (unsigned nCable, u8 *pPacket, unsigned nLength);
static void MIDIPacketHandler2 (unsigned nCable, u8 *pPacket, unsigned nLength);
static void MIDIPacketHandler3 (unsigned nCable, u8 *pPacket, unsigned nLength);
static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength, unsigned nDevice, void *pParam);
static void DeviceRemovedHandler (CDevice *pDevice, void *pContext);
void USBMIDIMessageHandler (u8 *pPacket, unsigned nLength, unsigned nCable, unsigned nDevice);
private:
struct TSendQueueEntry
@ -61,6 +58,8 @@ private:
size_t nLength;
unsigned nCable;
};
uint8_t m_SysEx[USB_SYSEX_BUFFER_SIZE];
unsigned m_nSysExIdx;
private:
unsigned m_nInstance;
@ -69,10 +68,6 @@ private:
CUSBMIDIDevice * volatile m_pMIDIDevice;
std::queue<TSendQueueEntry> m_SendQueue;
static CMIDIKeyboard *s_pThis[MaxInstances];
static TMIDIPacketHandler * const s_pMIDIPacketHandler[MaxInstances];
};
#endif

File diff suppressed because it is too large Load Diff

@ -36,14 +36,22 @@
#include <circle/interrupt.h>
#include <circle/gpiomanager.h>
#include <circle/i2cmaster.h>
#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
@ -51,7 +59,8 @@ class CMiniDexed
{
public:
CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt,
CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, FATFS *pFileSystem);
CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, FATFS *pFileSystem);
~CMiniDexed (void); // Add destructor
bool Initialize (void);
@ -62,13 +71,18 @@ public:
#endif
CSysExFileLoader *GetSysExFileLoader (void);
CPerformanceConfig *GetPerformanceConfig (void);
void BankSelect (unsigned nBank, unsigned nTG);
void BankSelectPerformance (unsigned nBank);
void BankSelectMSB (unsigned nBankMSB, unsigned nTG);
void BankSelectMSBPerformance (unsigned nBankMSB);
void BankSelectLSB (unsigned nBankLSB, unsigned nTG);
void BankSelectLSBPerformance (unsigned nBankLSB);
void ProgramChange (unsigned nProgram, unsigned nTG);
void ProgramChangePerformance (unsigned nProgram);
void SetVolume (unsigned nVolume, unsigned nTG);
void SetExpression (unsigned nExpression, unsigned nTG);
void SetPan (unsigned nPan, unsigned nTG); // 0 .. 127
void SetMasterTune (int nMasterTune, unsigned nTG); // -99 .. 99
void SetCutoff (int nCutoff, unsigned nTG); // 0 .. 99
@ -117,17 +131,27 @@ public:
std::string GetPerformanceFileName(unsigned nID);
std::string GetPerformanceName(unsigned nID);
unsigned GetLastPerformance();
unsigned GetPerformanceBank();
unsigned GetLastPerformanceBank();
unsigned GetActualPerformanceID();
void SetActualPerformanceID(unsigned nID);
unsigned GetActualPerformanceBankID();
void SetActualPerformanceBankID(unsigned nBankID);
bool SetNewPerformance(unsigned nID);
bool SetNewPerformanceBank(unsigned nBankID);
void SetFirstPerformance(void);
void DoSetFirstPerformance(void);
bool SavePerformanceNewFile ();
bool DoSavePerformanceNewFile (void);
bool DoSetNewPerformance (void);
bool DoSetNewPerformanceBank (void);
bool GetPerformanceSelectToLoad(void);
bool SavePerformance (bool bSaveAsDeault);
unsigned GetPerformanceSelectChannel (void);
void SetPerformanceSelectChannel (unsigned uCh);
bool IsValidPerformance(unsigned nID);
bool IsValidPerformanceBank(unsigned nBankID);
// Must match the order in CUIMenu::TParameter
enum TParameter
@ -141,6 +165,7 @@ public:
ParameterReverbDiffusion,
ParameterReverbLevel,
ParameterPerformanceSelectChannel,
ParameterPerformanceBank,
ParameterUnknown
};
@ -148,8 +173,8 @@ public:
int GetParameter (TParameter Parameter);
std::string GetNewPerformanceDefaultName(void);
void SetNewPerformanceName(std::string nName);
void SetVoiceName (std::string VoiceName, unsigned nTG);
void SetNewPerformanceName(const std::string &Name);
void SetVoiceName (const std::string &VoiceName, unsigned nTG);
bool DeletePerformance(unsigned nID);
bool DoDeletePerformance(void);
@ -212,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::ToneGenerators];
uint8_t m_uchOPMask[CConfig::AllToneGenerators];
void LoadPerformanceParameters(void);
void ProcessSound (void);
const char* GetNetworkDeviceShortName() const;
#ifdef ARM_ALLOW_MULTI_CORE
enum TCoreStatus
@ -233,39 +262,45 @@ private:
CConfig *m_pConfig;
int m_nParameter[ParameterUnknown]; // global (non-TG) parameters
CDexedAdapter *m_pTG[CConfig::ToneGenerators];
unsigned m_nVoiceBankID[CConfig::ToneGenerators];
unsigned m_nVoiceBankIDMSB[CConfig::ToneGenerators];
unsigned m_nProgram[CConfig::ToneGenerators];
unsigned m_nVolume[CConfig::ToneGenerators];
unsigned m_nPan[CConfig::ToneGenerators];
int m_nMasterTune[CConfig::ToneGenerators];
int m_nCutoff[CConfig::ToneGenerators];
int m_nResonance[CConfig::ToneGenerators];
unsigned m_nMIDIChannel[CConfig::ToneGenerators];
unsigned m_nPitchBendRange[CConfig::ToneGenerators];
unsigned m_nPitchBendStep[CConfig::ToneGenerators];
unsigned m_nPortamentoMode[CConfig::ToneGenerators];
unsigned m_nPortamentoGlissando[CConfig::ToneGenerators];
unsigned m_nPortamentoTime[CConfig::ToneGenerators];
bool m_bMonoMode[CConfig::ToneGenerators];
unsigned m_nToneGenerators;
unsigned m_nPolyphony;
CDexedAdapter *m_pTG[CConfig::AllToneGenerators];
unsigned m_nVoiceBankID[CConfig::AllToneGenerators];
unsigned m_nVoiceBankIDMSB[CConfig::AllToneGenerators];
unsigned m_nVoiceBankIDPerformance;
unsigned m_nVoiceBankIDMSBPerformance;
unsigned m_nProgram[CConfig::AllToneGenerators];
unsigned m_nVolume[CConfig::AllToneGenerators];
unsigned m_nExpression[CConfig::AllToneGenerators];
unsigned m_nPan[CConfig::AllToneGenerators];
int m_nMasterTune[CConfig::AllToneGenerators];
int m_nCutoff[CConfig::AllToneGenerators];
int m_nResonance[CConfig::AllToneGenerators];
unsigned m_nMIDIChannel[CConfig::AllToneGenerators];
unsigned m_nPitchBendRange[CConfig::AllToneGenerators];
unsigned m_nPitchBendStep[CConfig::AllToneGenerators];
unsigned m_nPortamentoMode[CConfig::AllToneGenerators];
unsigned m_nPortamentoGlissando[CConfig::AllToneGenerators];
unsigned m_nPortamentoTime[CConfig::AllToneGenerators];
bool m_bMonoMode[CConfig::AllToneGenerators];
unsigned m_nModulationWheelRange[CConfig::ToneGenerators];
unsigned m_nModulationWheelTarget[CConfig::ToneGenerators];
unsigned m_nFootControlRange[CConfig::ToneGenerators];
unsigned m_nFootControlTarget[CConfig::ToneGenerators];
unsigned m_nBreathControlRange[CConfig::ToneGenerators];
unsigned m_nBreathControlTarget[CConfig::ToneGenerators];
unsigned m_nAftertouchRange[CConfig::ToneGenerators];
unsigned m_nAftertouchTarget[CConfig::ToneGenerators];
unsigned m_nModulationWheelRange[CConfig::AllToneGenerators];
unsigned m_nModulationWheelTarget[CConfig::AllToneGenerators];
unsigned m_nFootControlRange[CConfig::AllToneGenerators];
unsigned m_nFootControlTarget[CConfig::AllToneGenerators];
unsigned m_nBreathControlRange[CConfig::AllToneGenerators];
unsigned m_nBreathControlTarget[CConfig::AllToneGenerators];
unsigned m_nAftertouchRange[CConfig::AllToneGenerators];
unsigned m_nAftertouchTarget[CConfig::AllToneGenerators];
unsigned m_nNoteLimitLow[CConfig::ToneGenerators];
unsigned m_nNoteLimitHigh[CConfig::ToneGenerators];
int m_nNoteShift[CConfig::ToneGenerators];
unsigned m_nNoteLimitLow[CConfig::AllToneGenerators];
unsigned m_nNoteLimitHigh[CConfig::AllToneGenerators];
int m_nNoteShift[CConfig::AllToneGenerators];
unsigned m_nReverbSend[CConfig::ToneGenerators];
unsigned m_nReverbSend[CConfig::AllToneGenerators];
uint8_t m_nRawVoiceData[156];
@ -280,34 +315,50 @@ private:
CPCKeyboard m_PCKeyboard;
CSerialMIDIDevice m_SerialMIDI;
bool m_bUseSerial;
bool m_bQuadDAC8Chan;
CSoundBaseDevice *m_pSoundDevice;
bool m_bChannelsSwapped;
unsigned m_nQueueSizeFrames;
#ifdef ARM_ALLOW_MULTI_CORE
unsigned m_nActiveTGsLog2;
// unsigned m_nActiveTGsLog2;
volatile TCoreStatus m_CoreStatus[CORES];
volatile unsigned m_nFramesToProcess;
float32_t m_OutputLevel[CConfig::ToneGenerators][CConfig::MaxChunkSize];
float32_t m_OutputLevel[CConfig::AllToneGenerators][CConfig::MaxChunkSize];
#endif
CPerformanceTimer m_GetChunkTimer;
bool m_bProfileEnabled;
AudioEffectPlateReverb* reverb;
AudioStereoMixer<CConfig::ToneGenerators>* tg_mixer;
AudioStereoMixer<CConfig::ToneGenerators>* reverb_send_mixer;
AudioStereoMixer<CConfig::AllToneGenerators>* tg_mixer;
AudioStereoMixer<CConfig::AllToneGenerators>* reverb_send_mixer;
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;
unsigned m_nSetNewPerformanceID;
bool m_bSetNewPerformanceBank;
unsigned m_nSetNewPerformanceBankID;
bool m_bSetFirstPerformance;
bool m_bDeletePerformance;
unsigned m_nDeletePerformanceID;
bool m_bLoadPerformanceBusy;
bool m_bLoadPerformanceBankBusy;
bool m_bSaveAsDeault;
};

@ -14,12 +14,15 @@ ChannelsSwapped=0
VelocityScale=0
# Engine Type ( 1=Modern ; 2=Mark I ; 3=OPL )
EngineType=1
QuadDAC8Chan=0
# Master Volume (0-127)
MasterVolume=64
# MIDI
MIDIBaudRate=31250
#MIDIThru=umidi1,ttyS1
IgnoreAllNotesOff=0
MIDIAutoVoiceDumpOnPC=1
MIDIAutoVoiceDumpOnPC=0
HeaderlessSysExVoices=0
# Program Change enable
# 0 = Ignore all Program Change messages.
@ -57,6 +60,27 @@ SSD1306LCDHeight=32
SSD1306LCDRotate=0
SSD1306LCDMirror=0
# ST7789 LCD
# SPIBus=0 for any RPi (GPIO 10,11,8,7).
# Note: Leave blank (default) if no SPI device required.
# Select=0|1 for CE0 or CE1
# Data = GPIO pin number
# Optional: Reset, Backlight = GPIO pin numbers
# Rotation=0,90,180,270
# SmallFont=0 (default), 1
#
# For a 240 wide display set LCDColumns=15 with LCDRows=2
SPIBus=
ST7789Enabled=0
ST7789Data=
ST7789Select=
ST7789Reset=
ST7789Backlight=
ST7789Width=240
ST7789Height=240
ST7789Rotation=0
ST7789SmallFont=0
# Default is 16x2 display (e.g. HD44780)
LCDColumns=16
LCDRows=2
@ -76,12 +100,16 @@ ButtonActionHome=doubleclick
ButtonPinShortcut=11
# (Shortcut doesn't have an action)
# GPIO Program/TG Selection
# GPIO Program/Bank/TG Selection
# Any buttons set to 0 will be ignored
ButtonPinPgmUp=0
ButtonActionPgmUp=
ButtonPinPgmDown=0
ButtonActionPgmDown=
ButtonPinBankUp=0
ButtonActionBankUp=
ButtonPinBankDown=0
ButtonActionBankDown=
ButtonPinTGUp=0
ButtonActionTGUp=
ButtonPinTGDown=0
@ -97,17 +125,24 @@ LongPressTimeout=400
# CC channel: 0=OFF; 1-16 MIDI Ch; >16 Omni
# If MIDIButtonNotes>0 then treat MIDIButton numbers as MIDI
# Note numbers, triggered with NoteOn/NoteOff, not CC numbers.
MIDIButtonCh=0
MIDIButtonCh=17
MIDIButtonNotes=0
MIDIButtonPrev=0
MIDIButtonNext=0
MIDIButtonBack=0
MIDIButtonSelect=0
MIDIButtonHome=0
MIDIButtonPgmUp=0
MIDIButtonPgmDown=0
MIDIButtonTGUp=0
MIDIButtonTGDown=0
# Arrow left
MIDIButtonPrev=46
# Arrow right
MIDIButtonNext=47
# Arrow up
MIDIButtonBack=48
# Arrow down
MIDIButtonSelect=49
# Home button
MIDIButtonHome=50
MIDIButtonPgmUp=51
MIDIButtonPgmDown=52
MIDIButtonBankUp=53
MIDIButtonBankDown=54
MIDIButtonTGUp=55
MIDIButtonTGDown=56
# KY-040 Rotary Encoder
EncoderEnabled=1
@ -118,5 +153,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

File diff suppressed because it is too large Load Diff

@ -27,14 +27,16 @@
#include <fatfs/ff.h>
#include <Properties/propertiesfatfsfile.h>
#define NUM_VOICE_PARAM 156
#define PERFORMANCE_DIR "performance"
#define NUM_PERFORMANCES 256
#define NUM_PERFORMANCES 128
#define NUM_PERFORMANCE_BANKS 128
class CPerformanceConfig // Performance configuration
{
public:
CPerformanceConfig (FATFS *pFileSystem);
~CPerformanceConfig (void);
bool Init (unsigned nToneGenerators);
bool Load (void);
@ -122,59 +124,76 @@ public:
bool ListPerformances();
//std::string m_DirName;
void SetNewPerformance (unsigned nID);
unsigned FindFirstPerformance (void);
std::string GetPerformanceFileName(unsigned nID);
std::string GetPerformanceFullFilePath(unsigned nID);
std::string GetPerformanceName(unsigned nID);
unsigned GetLastPerformance();
unsigned GetLastPerformanceBank();
void SetActualPerformanceID(unsigned nID);
unsigned GetActualPerformanceID();
void SetActualPerformanceBankID(unsigned nBankID);
unsigned GetActualPerformanceBankID();
bool CreateNewPerformanceFile(void);
bool GetInternalFolderOk();
std::string GetNewPerformanceDefaultName(void);
void SetNewPerformanceName(std::string nName);
void SetNewPerformanceName(const std::string &Name);
bool DeletePerformance(unsigned nID);
bool CheckFreePerformanceSlot(void);
std::string AddPerformanceBankDirName(unsigned nBankID);
bool IsValidPerformance(unsigned nID);
bool ListPerformanceBanks(void);
void SetNewPerformanceBank(unsigned nBankID);
unsigned GetPerformanceBank(void);
std::string GetPerformanceBankName(unsigned nBankID);
bool IsValidPerformanceBank(unsigned nBankID);
private:
CPropertiesFatFsFile m_Properties;
unsigned m_nBankNumber[CConfig::ToneGenerators];
unsigned m_nVoiceNumber[CConfig::ToneGenerators];
unsigned m_nMIDIChannel[CConfig::ToneGenerators];
unsigned m_nVolume[CConfig::ToneGenerators];
unsigned m_nPan[CConfig::ToneGenerators];
int m_nDetune[CConfig::ToneGenerators];
unsigned m_nCutoff[CConfig::ToneGenerators];
unsigned m_nResonance[CConfig::ToneGenerators];
unsigned m_nNoteLimitLow[CConfig::ToneGenerators];
unsigned m_nNoteLimitHigh[CConfig::ToneGenerators];
int m_nNoteShift[CConfig::ToneGenerators];
int m_nReverbSend[CConfig::ToneGenerators];
unsigned m_nPitchBendRange[CConfig::ToneGenerators];
unsigned m_nPitchBendStep[CConfig::ToneGenerators];
unsigned m_nPortamentoMode[CConfig::ToneGenerators];
unsigned m_nPortamentoGlissando[CConfig::ToneGenerators];
unsigned m_nPortamentoTime[CConfig::ToneGenerators];
std::string m_nVoiceDataTxt[CConfig::ToneGenerators];
bool m_bMonoMode[CConfig::ToneGenerators];
unsigned m_nModulationWheelRange[CConfig::ToneGenerators];
unsigned m_nModulationWheelTarget[CConfig::ToneGenerators];
unsigned m_nFootControlRange[CConfig::ToneGenerators];
unsigned m_nFootControlTarget[CConfig::ToneGenerators];
unsigned m_nBreathControlRange[CConfig::ToneGenerators];
unsigned m_nBreathControlTarget[CConfig::ToneGenerators];
unsigned m_nAftertouchRange[CConfig::ToneGenerators];
unsigned m_nAftertouchTarget[CConfig::ToneGenerators];
unsigned nLastPerformance;
unsigned nLastFileIndex;
unsigned nActualPerformance = 0;
unsigned m_nToneGenerators;
unsigned m_nBankNumber[CConfig::AllToneGenerators];
unsigned m_nVoiceNumber[CConfig::AllToneGenerators];
unsigned m_nMIDIChannel[CConfig::AllToneGenerators];
unsigned m_nVolume[CConfig::AllToneGenerators];
unsigned m_nPan[CConfig::AllToneGenerators];
int m_nDetune[CConfig::AllToneGenerators];
unsigned m_nCutoff[CConfig::AllToneGenerators];
unsigned m_nResonance[CConfig::AllToneGenerators];
unsigned m_nNoteLimitLow[CConfig::AllToneGenerators];
unsigned m_nNoteLimitHigh[CConfig::AllToneGenerators];
int m_nNoteShift[CConfig::AllToneGenerators];
int m_nReverbSend[CConfig::AllToneGenerators];
unsigned m_nPitchBendRange[CConfig::AllToneGenerators];
unsigned m_nPitchBendStep[CConfig::AllToneGenerators];
unsigned m_nPortamentoMode[CConfig::AllToneGenerators];
unsigned m_nPortamentoGlissando[CConfig::AllToneGenerators];
unsigned m_nPortamentoTime[CConfig::AllToneGenerators];
std::string m_nVoiceDataTxt[CConfig::AllToneGenerators];
bool m_bMonoMode[CConfig::AllToneGenerators];
unsigned m_nModulationWheelRange[CConfig::AllToneGenerators];
unsigned m_nModulationWheelTarget[CConfig::AllToneGenerators];
unsigned m_nFootControlRange[CConfig::AllToneGenerators];
unsigned m_nFootControlTarget[CConfig::AllToneGenerators];
unsigned m_nBreathControlRange[CConfig::AllToneGenerators];
unsigned m_nBreathControlTarget[CConfig::AllToneGenerators];
unsigned m_nAftertouchRange[CConfig::AllToneGenerators];
unsigned m_nAftertouchTarget[CConfig::AllToneGenerators];
unsigned m_nLastPerformance;
unsigned m_nActualPerformance = 0;
unsigned m_nActualPerformanceBank = 0;
unsigned m_nPerformanceBank;
unsigned m_nLastPerformanceBank;
bool m_bPerformanceDirectoryExists;
//unsigned nMenuSelectedPerformance = 0;
std::string m_nPerformanceFileName[NUM_PERFORMANCES];
std::string m_PerformanceFileName[NUM_PERFORMANCES];
std::string m_PerformanceBankName[NUM_PERFORMANCE_BANKS];
FATFS *m_pFileSystem;
bool nInternalFolderOk=false;
bool nExternalFolderOk=false; // for future USB implementation
std::string NewPerformanceName="";
bool m_bCompressorEnable;

@ -28,11 +28,15 @@
LOGMODULE("serialmididevice");
// There are several UART options - see circle/include/serial.h
// 0 corresponds to GP14/GP15 on all RPi versions.
#define SERIAL_MIDI_DEVICE 0
CSerialMIDIDevice::CSerialMIDIDevice (CMiniDexed *pSynthesizer, CInterruptSystem *pInterrupt,
CConfig *pConfig, CUserInterface *pUI)
: CMIDIDevice (pSynthesizer, pConfig, pUI),
m_pConfig (pConfig),
m_Serial (pInterrupt, TRUE),
m_Serial (pInterrupt, TRUE, SERIAL_MIDI_DEVICE),
m_nSerialState (0),
m_nSysEx (0),
m_SendBuffer (&m_Serial)
@ -71,7 +75,7 @@ void CSerialMIDIDevice::Process (void)
return;
}
if (m_pConfig->GetMIDIDumpEnabled ())
/* if (m_pConfig->GetMIDIDumpEnabled ())
{
printf("Incoming MIDI data:");
for (uint16_t i = 0; i < nResult; i++)
@ -81,7 +85,7 @@ void CSerialMIDIDevice::Process (void)
printf(" 0x%02x",Buffer[i]);
}
printf("\n");
}
}*/
// Process MIDI messages
// See: https://www.midi.org/specifications/item/table-1-summary-of-midi-message

@ -30,9 +30,6 @@
#include <circle/writebuffer.h>
#include <circle/types.h>
#define MAX_DX7_SYSEX_LENGTH 4104
#define MAX_MIDI_MESSAGE MAX_DX7_SYSEX_LENGTH
class CMiniDexed;
class CSerialMIDIDevice : public CMIDIDevice

@ -273,6 +273,23 @@ std::string CSysExFileLoader::GetBankName (unsigned nBankID)
return "NO NAME";
}
std::string CSysExFileLoader::GetVoiceName (unsigned nBankID, unsigned nVoiceID)
{
if ((nBankID <= MaxVoiceBankID) && (nVoiceID < VoicesPerBank))
{
if (IsValidBank(nBankID))
{
// The name is the last 10 characters of the voice data
char sVoiceName[11];
strncpy (sVoiceName, (char *)((char *)&(m_pVoiceBank[nBankID]->Voice[nVoiceID]) + SizePackedVoice - 10), 10);
sVoiceName[10] = 0;
std::string result(sVoiceName);
return result;
}
}
return "INIT VOICE";
}
unsigned CSysExFileLoader::GetNextBankUp (unsigned nBankID)
{
// Find the next loaded bank "up" from the provided bank ID

@ -60,6 +60,7 @@ public:
void Load (bool bHeaderlessSysExVoices = false);
std::string GetBankName (unsigned nBankID); // 0 .. MaxVoiceBankID
std::string GetVoiceName (unsigned nBankID, unsigned nVoice); // 0 .. MaxVoiceBankID, 0 .. VoicesPerBank-1
unsigned GetNumHighestBank (); // 0 .. MaxVoiceBankID
bool IsValidBank (unsigned nBankID);
unsigned GetNextBankUp (unsigned nBankID);

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

@ -257,50 +257,8 @@ CUIButton::BtnTrigger CUIButton::triggerTypeFromString(const char* triggerString
}
CUIButtons::CUIButtons (
unsigned prevPin, const char *prevAction,
unsigned nextPin, const char *nextAction,
unsigned backPin, const char *backAction,
unsigned selectPin, const char *selectAction,
unsigned homePin, const char *homeAction,
unsigned pgmUpPin, const char *pgmUpAction,
unsigned pgmDownPin, const char *pgmDownAction,
unsigned TGUpPin, const char *TGUpAction,
unsigned TGDownPin, const char *TGDownAction,
unsigned doubleClickTimeout, unsigned longPressTimeout,
unsigned notesMidi, unsigned prevMidi, unsigned nextMidi, unsigned backMidi, unsigned selectMidi, unsigned homeMidi,
unsigned pgmUpMidi, unsigned pgmDownMidi, unsigned TGUpMidi, unsigned TGDownMidi
)
: m_doubleClickTimeout(doubleClickTimeout),
m_longPressTimeout(longPressTimeout),
m_prevPin(prevPin),
m_prevAction(CUIButton::triggerTypeFromString(prevAction)),
m_nextPin(nextPin),
m_nextAction(CUIButton::triggerTypeFromString(nextAction)),
m_backPin(backPin),
m_backAction(CUIButton::triggerTypeFromString(backAction)),
m_selectPin(selectPin),
m_selectAction(CUIButton::triggerTypeFromString(selectAction)),
m_homePin(homePin),
m_homeAction(CUIButton::triggerTypeFromString(homeAction)),
m_pgmUpPin(pgmUpPin),
m_pgmUpAction(CUIButton::triggerTypeFromString(pgmUpAction)),
m_pgmDownPin(pgmDownPin),
m_pgmDownAction(CUIButton::triggerTypeFromString(pgmDownAction)),
m_TGUpPin(TGUpPin),
m_TGUpAction(CUIButton::triggerTypeFromString(TGUpAction)),
m_TGDownPin(TGDownPin),
m_TGDownAction(CUIButton::triggerTypeFromString(TGDownAction)),
m_notesMidi(notesMidi),
m_prevMidi(ccToMidiPin(prevMidi)),
m_nextMidi(ccToMidiPin(nextMidi)),
m_backMidi(ccToMidiPin(backMidi)),
m_selectMidi(ccToMidiPin(selectMidi)),
m_homeMidi(ccToMidiPin(homeMidi)),
m_pgmUpMidi(ccToMidiPin(pgmUpMidi)),
m_pgmDownMidi(ccToMidiPin(pgmDownMidi)),
m_TGUpMidi(ccToMidiPin(TGUpMidi)),
m_TGDownMidi(ccToMidiPin(TGDownMidi)),
CUIButtons::CUIButtons (CConfig *pConfig)
: m_pConfig(pConfig),
m_eventHandler (0),
m_lastTick (0)
{
@ -312,6 +270,46 @@ CUIButtons::~CUIButtons (void)
boolean CUIButtons::Initialize (void)
{
assert (m_pConfig);
// Read the button configuration
m_doubleClickTimeout = m_pConfig->GetDoubleClickTimeout ();
m_longPressTimeout = m_pConfig->GetLongPressTimeout ();
m_prevPin = m_pConfig->GetButtonPinPrev ();
m_prevAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionPrev ());
m_nextPin = m_pConfig->GetButtonPinNext ();
m_nextAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionNext ());
m_backPin = m_pConfig->GetButtonPinBack ();
m_backAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionBack ());
m_selectPin = m_pConfig->GetButtonPinSelect ();
m_selectAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionSelect ());
m_homePin = m_pConfig->GetButtonPinHome ();
m_homeAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionHome ());
m_pgmUpPin = m_pConfig->GetButtonPinPgmUp ();
m_pgmUpAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionPgmUp ());
m_pgmDownPin = m_pConfig->GetButtonPinPgmDown ();
m_pgmDownAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionPgmDown ());
m_BankUpPin = m_pConfig->GetButtonPinBankUp ();
m_BankUpAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionBankUp ());
m_BankDownPin = m_pConfig->GetButtonPinBankDown ();
m_BankDownAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionBankDown ());
m_TGUpPin = m_pConfig->GetButtonPinTGUp ();
m_TGUpAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionTGUp ());
m_TGDownPin = m_pConfig->GetButtonPinTGDown ();
m_TGDownAction = CUIButton::triggerTypeFromString( m_pConfig->GetButtonActionTGDown ());
m_notesMidi = ccToMidiPin( m_pConfig->GetMIDIButtonNotes ());
m_prevMidi = ccToMidiPin( m_pConfig->GetMIDIButtonPrev ());
m_nextMidi = ccToMidiPin( m_pConfig->GetMIDIButtonNext ());
m_backMidi = ccToMidiPin( m_pConfig->GetMIDIButtonBack ());
m_selectMidi = ccToMidiPin( m_pConfig->GetMIDIButtonSelect ());
m_homeMidi = ccToMidiPin( m_pConfig->GetMIDIButtonHome ());
m_pgmUpMidi = ccToMidiPin( m_pConfig->GetMIDIButtonPgmUp ());
m_pgmDownMidi = ccToMidiPin( m_pConfig->GetMIDIButtonPgmDown ());
m_BankUpMidi = ccToMidiPin( m_pConfig->GetMIDIButtonBankUp ());
m_BankDownMidi = ccToMidiPin( m_pConfig->GetMIDIButtonBankDown ());
m_TGUpMidi = ccToMidiPin( m_pConfig->GetMIDIButtonTGUp ());
m_TGDownMidi = ccToMidiPin( m_pConfig->GetMIDIButtonTGDown ());
// First sanity check and convert the timeouts:
// Internally values are in tenths of a millisecond, but config values
// are in milliseconds
@ -332,16 +330,16 @@ boolean CUIButtons::Initialize (void)
// longpress. We may not initialise all of the buttons.
// MIDI buttons only support a single click.
unsigned pins[MAX_BUTTONS] = {
m_prevPin, m_nextPin, m_backPin, m_selectPin, m_homePin, m_pgmUpPin, m_pgmDownPin, m_TGUpPin, m_TGDownPin,
m_prevMidi, m_nextMidi, m_backMidi, m_selectMidi, m_homeMidi, m_pgmUpMidi, m_pgmDownMidi, m_TGUpMidi, m_TGDownMidi
m_prevPin, m_nextPin, m_backPin, m_selectPin, m_homePin, m_pgmUpPin, m_pgmDownPin, m_BankUpPin, m_BankDownPin, m_TGUpPin, m_TGDownPin,
m_prevMidi, m_nextMidi, m_backMidi, m_selectMidi, m_homeMidi, m_pgmUpMidi, m_pgmDownMidi, m_BankUpMidi, m_BankDownMidi, m_TGUpMidi, m_TGDownMidi
};
CUIButton::BtnTrigger triggers[MAX_BUTTONS] = {
// Normal buttons
m_prevAction, m_nextAction, m_backAction, m_selectAction, m_homeAction,
m_pgmUpAction, m_pgmDownAction, m_TGUpAction, m_TGDownAction,
m_pgmUpAction, m_pgmDownAction, m_BankUpAction, m_BankDownAction, m_TGUpAction, m_TGDownAction,
// MIDI Buttons only support a single click (at present)
CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick,
CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick
CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick
};
CUIButton::BtnEvent events[MAX_BUTTONS] = {
// Normal buttons
@ -352,6 +350,8 @@ boolean CUIButtons::Initialize (void)
CUIButton::BtnEventHome,
CUIButton::BtnEventPgmUp,
CUIButton::BtnEventPgmDown,
CUIButton::BtnEventBankUp,
CUIButton::BtnEventBankDown,
CUIButton::BtnEventTGUp,
CUIButton::BtnEventTGDown,
// MIDI buttons
@ -362,6 +362,8 @@ boolean CUIButtons::Initialize (void)
CUIButton::BtnEventHome,
CUIButton::BtnEventPgmUp,
CUIButton::BtnEventPgmDown,
CUIButton::BtnEventBankUp,
CUIButton::BtnEventBankDown,
CUIButton::BtnEventTGUp,
CUIButton::BtnEventTGDown
};

@ -26,9 +26,9 @@
#include "config.h"
#define BUTTONS_UPDATE_NUM_TICKS 100
#define DEBOUNCE_TIME 100
#define MAX_GPIO_BUTTONS 9 // 5 UI buttons, 4 Program/TG Select buttons
#define MAX_MIDI_BUTTONS 9
#define DEBOUNCE_TIME 20
#define MAX_GPIO_BUTTONS 11 // 5 UI buttons, 6 Program/Bank/TG Select buttons
#define MAX_MIDI_BUTTONS 11
#define MAX_BUTTONS (MAX_GPIO_BUTTONS+MAX_MIDI_BUTTONS)
class CUIButtons;
@ -54,9 +54,11 @@ public:
BtnEventHome = 5,
BtnEventPgmUp = 6,
BtnEventPgmDown = 7,
BtnEventTGUp = 8,
BtnEventTGDown = 9,
BtnEventUnknown = 10
BtnEventBankUp = 8,
BtnEventBankDown = 9,
BtnEventTGUp = 10,
BtnEventTGDown = 11,
BtnEventUnknown = 12
};
CUIButton (void);
@ -111,20 +113,7 @@ public:
typedef void BtnEventHandler (CUIButton::BtnEvent Event, void *param);
public:
CUIButtons (
unsigned prevPin, const char *prevAction,
unsigned nextPin, const char *nextAction,
unsigned backPin, const char *backAction,
unsigned selectPin, const char *selectAction,
unsigned homePin, const char *homeAction,
unsigned pgmUpPin, const char *pgmUpAction,
unsigned pgmDownPin, const char *pgmDownAction,
unsigned TGUpPin, const char *TGUpAction,
unsigned TGDownPin, const char *TGDownAction,
unsigned doubleClickTimeout, unsigned longPressTimeout,
unsigned notesMidi, unsigned prevMidi, unsigned nextMidi, unsigned backMidi, unsigned selectMidi, unsigned homeMidi,
unsigned pgmUpMidi, unsigned pgmDownMidi, unsigned TGUpMidi, unsigned TGDownMidi
);
CUIButtons (CConfig *pConfig);
~CUIButtons (void);
boolean Initialize (void);
@ -138,6 +127,8 @@ public:
void BtnMIDICmdHandler (unsigned nMidiCmd, unsigned nMidiData1, unsigned nMidiData2);
private:
CConfig *m_pConfig;
// Array of normal GPIO buttons and "MIDI buttons"
CUIButton m_buttons[MAX_BUTTONS];
@ -163,6 +154,10 @@ private:
CUIButton::BtnTrigger m_pgmUpAction;
unsigned m_pgmDownPin;
CUIButton::BtnTrigger m_pgmDownAction;
unsigned m_BankUpPin;
CUIButton::BtnTrigger m_BankUpAction;
unsigned m_BankDownPin;
CUIButton::BtnTrigger m_BankDownAction;
unsigned m_TGUpPin;
CUIButton::BtnTrigger m_TGUpAction;
unsigned m_TGDownPin;
@ -178,6 +173,8 @@ private:
unsigned m_pgmUpMidi;
unsigned m_pgmDownMidi;
unsigned m_BankUpMidi;
unsigned m_BankDownMidi;
unsigned m_TGUpMidi;
unsigned m_TGDownMidi;

@ -51,6 +51,16 @@ const CUIMenu::TMenuItem CUIMenu::s_MainMenu[] =
{"TG6", MenuHandler, s_TGMenu, 5},
{"TG7", MenuHandler, s_TGMenu, 6},
{"TG8", MenuHandler, s_TGMenu, 7},
#if (RASPPI==4 || RASPPI==5)
{"TG9", MenuHandler, s_TGMenu, 8},
{"TG10", MenuHandler, s_TGMenu, 9},
{"TG11", MenuHandler, s_TGMenu, 10},
{"TG12", MenuHandler, s_TGMenu, 11},
{"TG13", MenuHandler, s_TGMenu, 12},
{"TG14", MenuHandler, s_TGMenu, 13},
{"TG15", MenuHandler, s_TGMenu, 14},
{"TG16", MenuHandler, s_TGMenu, 15},
#endif
#endif
{"Effects", MenuHandler, s_EffectsMenu},
{"Performance", MenuHandler, s_PerformanceMenu},
@ -64,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},
@ -214,7 +224,8 @@ const CUIMenu::TParameter CUIMenu::s_GlobalParameter[CMiniDexed::ParameterUnknow
{0, 99, 1}, // ParameterReverbLowPass
{0, 99, 1}, // ParameterReverbDiffusion
{0, 99, 1}, // ParameterReverbLevel
{0, CMIDIDevice::ChannelUnknown-1, 1, ToMIDIChannel} // ParameterPerformanceSelectChannel
{0, CMIDIDevice::ChannelUnknown-1, 1, ToMIDIChannel}, // ParameterPerformanceSelectChannel
{0, NUM_PERFORMANCE_BANKS, 1} // ParameterPerformanceBank
};
// must match CMiniDexed::TTGParameter
@ -327,14 +338,16 @@ const CUIMenu::TMenuItem CUIMenu::s_PerformanceMenu[] =
{"Load", PerformanceMenu, 0, 0},
{"Save", MenuHandler, s_SaveMenu},
{"Delete", PerformanceMenu, 0, 1},
{"Bank", EditPerformanceBankNumber, 0, 0},
{"PCCH", EditGlobalParameter, 0, CMiniDexed::ParameterPerformanceSelectChannel},
{0}
};
CUIMenu::CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed)
CUIMenu::CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed, CConfig *pConfig)
: m_pUI (pUI),
m_pMiniDexed (pMiniDexed),
m_pConfig (pConfig),
m_pParentMenu (s_MenuRoot),
m_pCurrentMenu (s_MainMenu),
m_nCurrentMenuItem (0),
@ -342,23 +355,27 @@ CUIMenu::CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed)
m_nCurrentParameter (0),
m_nCurrentMenuDepth (0)
{
#ifndef ARM_ALLOW_MULTI_CORE
// If there is just one core, then there is only a single
// tone generator so start on the TG1 menu...
m_pParentMenu = s_MainMenu;
m_pCurrentMenu = s_TGMenu;
m_nCurrentMenuItem = 0;
m_nCurrentSelection = 0;
m_nCurrentParameter = 0;
m_nCurrentMenuDepth = 1;
assert (m_pConfig);
m_nToneGenerators = m_pConfig->GetToneGenerators();
// Place the "root" menu at the top of the stack
m_MenuStackParent[0] = s_MenuRoot;
m_MenuStackMenu[0] = s_MainMenu;
m_nMenuStackItem[0] = 0;
m_nMenuStackSelection[0] = 0;
m_nMenuStackParameter[0] = 0;
#endif
if (m_nToneGenerators == 1)
{
// If there is just one core, then there is only a single
// tone generator so start on the TG1 menu...
m_pParentMenu = s_MainMenu;
m_pCurrentMenu = s_TGMenu;
m_nCurrentMenuItem = 0;
m_nCurrentSelection = 0;
m_nCurrentParameter = 0;
m_nCurrentMenuDepth = 1;
// Place the "root" menu at the top of the stack
m_MenuStackParent[0] = s_MenuRoot;
m_MenuStackMenu[0] = s_MainMenu;
m_nMenuStackItem[0] = 0;
m_nMenuStackSelection[0] = 0;
m_nMenuStackParameter[0] = 0;
}
}
void CUIMenu::EventHandler (TMenuEvent Event)
@ -381,28 +398,31 @@ void CUIMenu::EventHandler (TMenuEvent Event)
break;
case MenuEventHome:
#ifdef ARM_ALLOW_MULTI_CORE
m_pParentMenu = s_MenuRoot;
m_pCurrentMenu = s_MainMenu;
m_nCurrentMenuItem = 0;
m_nCurrentSelection = 0;
m_nCurrentParameter = 0;
m_nCurrentMenuDepth = 0;
#else
// "Home" is the TG0 menu if only one TG active
m_pParentMenu = s_MainMenu;
m_pCurrentMenu = s_TGMenu;
m_nCurrentMenuItem = 0;
m_nCurrentSelection = 0;
m_nCurrentParameter = 0;
m_nCurrentMenuDepth = 1;
// Place the "root" menu at the top of the stack
m_MenuStackParent[0] = s_MenuRoot;
m_MenuStackMenu[0] = s_MainMenu;
m_nMenuStackItem[0] = 0;
m_nMenuStackSelection[0] = 0;
m_nMenuStackParameter[0] = 0;
#endif
if (m_nToneGenerators == 1)
{
// "Home" is the TG0 menu if only one TG active
m_pParentMenu = s_MainMenu;
m_pCurrentMenu = s_TGMenu;
m_nCurrentMenuItem = 0;
m_nCurrentSelection = 0;
m_nCurrentParameter = 0;
m_nCurrentMenuDepth = 1;
// Place the "root" menu at the top of the stack
m_MenuStackParent[0] = s_MenuRoot;
m_MenuStackMenu[0] = s_MainMenu;
m_nMenuStackItem[0] = 0;
m_nMenuStackSelection[0] = 0;
m_nMenuStackParameter[0] = 0;
}
else
{
m_pParentMenu = s_MenuRoot;
m_pCurrentMenu = s_MainMenu;
m_nCurrentMenuItem = 0;
m_nCurrentSelection = 0;
m_nCurrentParameter = 0;
m_nCurrentMenuDepth = 0;
}
EventHandler (MenuEventUpdate);
break;
@ -411,6 +431,11 @@ void CUIMenu::EventHandler (TMenuEvent Event)
PgmUpDownHandler(Event);
break;
case MenuEventBankUp:
case MenuEventBankDown:
BankUpDownHandler(Event);
break;
case MenuEventTGUp:
case MenuEventTGDown:
TGUpDownHandler(Event);
@ -451,7 +476,30 @@ void CUIMenu::MenuHandler (CUIMenu *pUIMenu, TMenuEvent Event)
break;
case MenuEventStepDown:
if (pUIMenu->m_nCurrentSelection > 0)
if (pUIMenu->m_nCurrentSelection == 0)
{
// If in main mennu, wrap around
if (pUIMenu->m_pCurrentMenu == s_MainMenu)
{
// Find last entry with a name
while (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name)
{
pUIMenu->m_nCurrentSelection++;
}
}
}
else if (pUIMenu->m_nCurrentSelection > 0)
{
pUIMenu->m_nCurrentSelection--;
}
// Might need to trim menu if number of TGs is configured to be less than the maximum supported
while ((pUIMenu->m_pCurrentMenu == s_MainMenu) && (pUIMenu->m_nCurrentSelection > 0) &&
( // Skip any unused menus
(pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].MenuItem == s_TGMenu) &&
(pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter >= pUIMenu->m_nToneGenerators) &&
(pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter < CConfig::AllToneGenerators)
)
)
{
pUIMenu->m_nCurrentSelection--;
}
@ -461,7 +509,27 @@ void CUIMenu::MenuHandler (CUIMenu *pUIMenu, TMenuEvent Event)
++pUIMenu->m_nCurrentSelection;
if (!pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Name) // more entries?
{
pUIMenu->m_nCurrentSelection--;
if (pUIMenu->m_pCurrentMenu == s_MainMenu)
{
// If in main mennu, wrap around
pUIMenu->m_nCurrentSelection = 0;
}
else
{
// Return to last known good item
pUIMenu->m_nCurrentSelection--;
}
}
// Might need to trim menu if number of TGs is configured to be less than the maximum supported
while ((pUIMenu->m_pCurrentMenu == s_MainMenu) && (pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name) &&
( // Skip any unused TG menus
(pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].MenuItem == s_TGMenu) &&
(pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter >= pUIMenu->m_nToneGenerators) &&
(pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Parameter < CConfig::AllToneGenerators)
)
)
{
pUIMenu->m_nCurrentSelection++;
}
break;
@ -471,12 +539,13 @@ void CUIMenu::MenuHandler (CUIMenu *pUIMenu, TMenuEvent Event)
if (pUIMenu->m_pCurrentMenu) // if this is another menu?
{
bool bIsMainMenu = pUIMenu->m_pCurrentMenu == s_MainMenu;
pUIMenu->m_pUI->DisplayWrite (
pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name,
"",
pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection].Name,
pUIMenu->m_nCurrentSelection > 0,
!!pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name);
pUIMenu->m_nCurrentSelection > 0 || bIsMainMenu,
!!pUIMenu->m_pCurrentMenu[pUIMenu->m_nCurrentSelection+1].Name || bIsMainMenu);
}
else
{
@ -633,15 +702,30 @@ void CUIMenu::EditProgramNumber (CUIMenu *pUIMenu, TMenuEvent Event)
CUIMenu::EditProgramNumber (pUIMenu, MenuEventStepDown);
}
} else {
string TG ("TG");
TG += to_string (nTG+1);
string Value = to_string (nValue+1) + "=" + pUIMenu->m_pMiniDexed->GetVoiceName (nTG);
pUIMenu->m_pUI->DisplayWrite (TG.c_str (),
pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name,
Value.c_str (),
nValue > 0, nValue < (int) CSysExFileLoader::VoicesPerBank-1);
// Format: 000:000 TG1 (bank:voice padded, TGx right-aligned)
int nBank = pUIMenu->m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG);
std::string left = "000";
left += std::to_string(nBank+1);
left = left.substr(left.length()-3,3);
left += ":";
std::string voiceNum = "000";
voiceNum += std::to_string(nValue+1);
voiceNum = voiceNum.substr(voiceNum.length()-3,3);
left += voiceNum;
std::string tgLabel = "TG" + std::to_string(nTG+1);
unsigned lcdCols = pUIMenu->m_pConfig->GetLCDColumns();
unsigned pad = 0;
if (lcdCols > left.length() + tgLabel.length())
pad = lcdCols - (unsigned)(left.length() + tgLabel.length());
std::string topLine = left + std::string(pad, ' ') + tgLabel;
std::string Value = pUIMenu->m_pMiniDexed->GetVoiceName (nTG);
pUIMenu->m_pUI->DisplayWrite (topLine.c_str(),
"",
Value.c_str(),
nValue > 0, nValue < (int) CSysExFileLoader::VoicesPerBank);
}
}
@ -1149,7 +1233,7 @@ void CUIMenu::TGShortcutHandler (TMenuEvent Event)
assert (m_nCurrentMenuDepth >= 2);
assert (m_MenuStackMenu[0] = s_MainMenu);
unsigned nTG = m_nMenuStackSelection[0];
assert (nTG < CConfig::ToneGenerators);
assert (nTG < CConfig::AllToneGenerators);
assert (m_nMenuStackItem[1] == nTG);
assert (m_nMenuStackParameter[1] == nTG);
@ -1164,7 +1248,7 @@ void CUIMenu::TGShortcutHandler (TMenuEvent Event)
nTG++;
}
if (nTG < CConfig::ToneGenerators)
if (nTG < m_nToneGenerators)
{
m_nMenuStackSelection[0] = nTG;
m_nMenuStackItem[1] = nTG;
@ -1211,24 +1295,43 @@ void CUIMenu::PgmUpDownHandler (TMenuEvent Event)
// Program Up/Down acts on performances
unsigned nLastPerformance = m_pMiniDexed->GetLastPerformance();
unsigned nPerformance = m_pMiniDexed->GetActualPerformanceID();
unsigned nStart = nPerformance;
//LOGNOTE("Performance actual=%d, last=%d", nPerformance, nLastPerformance);
if (Event == MenuEventPgmDown)
{
if (nPerformance > 0)
do
{
m_nSelectedPerformanceID = nPerformance-1;
m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID);
//LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance);
}
if (nPerformance == 0)
{
// Wrap around
nPerformance = nLastPerformance;
}
else if (nPerformance > 0)
{
--nPerformance;
}
} while ((m_pMiniDexed->IsValidPerformance(nPerformance) != true) && (nPerformance != nStart));
m_nSelectedPerformanceID = nPerformance;
m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID);
//LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance);
}
else
else // MenuEventPgmUp
{
if (nPerformance < nLastPerformance-1)
do
{
m_nSelectedPerformanceID = nPerformance+1;
m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID);
//LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance);
}
if (nPerformance == nLastPerformance)
{
// Wrap around
nPerformance = 0;
}
else if (nPerformance < nLastPerformance)
{
++nPerformance;
}
} while ((m_pMiniDexed->IsValidPerformance(nPerformance) != true) && (nPerformance != nStart));
m_nSelectedPerformanceID = nPerformance;
m_pMiniDexed->SetNewPerformance(m_nSelectedPerformanceID);
//LOGNOTE("Performance new=%d, last=%d", m_nSelectedPerformanceID, nLastPerformance);
}
}
else
@ -1241,62 +1344,145 @@ void CUIMenu::PgmUpDownHandler (TMenuEvent Event)
if (m_MenuStackMenu[0] == s_MainMenu && (m_pCurrentMenu == s_TGMenu) || (m_MenuStackMenu[1] == s_TGMenu)) {
nTG = m_nMenuStackSelection[0];
}
assert (nTG < CConfig::ToneGenerators);
assert (nTG < CConfig::AllToneGenerators);
if (nTG < m_nToneGenerators)
{
int nPgm = m_pMiniDexed->GetTGParameter (CMiniDexed::TGParameterProgram, nTG);
int nPgm = m_pMiniDexed->GetTGParameter (CMiniDexed::TGParameterProgram, nTG);
assert (Event == MenuEventPgmDown || Event == MenuEventPgmUp);
if (Event == MenuEventPgmDown)
{
//LOGNOTE("PgmDown");
if (--nPgm < 0)
{
// Switch down a voice bank and set to the last voice
nPgm = CSysExFileLoader::VoicesPerBank-1;
int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG);
nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankDown(nVB);
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG);
}
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG);
}
else
{
//LOGNOTE("PgmUp");
if (++nPgm > (int) CSysExFileLoader::VoicesPerBank-1)
{
// Switch up a voice bank and reset to voice 0
nPgm = 0;
int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG);
nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankUp(nVB);
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG);
}
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG);
}
assert (Event == MenuEventPgmDown || Event == MenuEventPgmUp);
if (Event == MenuEventPgmDown)
{
//LOGNOTE("PgmDown");
if (--nPgm < 0)
// Skip empty voices.
// Use same criteria in EditProgramNumber () too.
string voiceName = m_pMiniDexed->GetVoiceName (nTG);
if (voiceName == "EMPTY "
|| voiceName == " "
|| voiceName == "----------"
|| voiceName == "~~~~~~~~~~" )
{
// Switch down a voice bank and set to the last voice
nPgm = CSysExFileLoader::VoicesPerBank-1;
int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG);
nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankDown(nVB);
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG);
if (Event == MenuEventPgmUp) {
PgmUpDownHandler (MenuEventPgmUp);
}
if (Event == MenuEventPgmDown) {
PgmUpDownHandler (MenuEventPgmDown);
}
}
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG);
}
else
}
}
void CUIMenu::BankUpDownHandler (TMenuEvent Event)
{
if (m_pMiniDexed->GetParameter (CMiniDexed::ParameterPerformanceSelectChannel) != CMIDIDevice::Disabled)
{
// Bank Up/Down acts on performances
unsigned nLastPerformanceBank = m_pMiniDexed->GetLastPerformanceBank();
unsigned nPerformanceBank = m_nSelectedPerformanceBankID;
unsigned nStartBank = nPerformanceBank;
//LOGNOTE("Performance Bank actual=%d, last=%d", nPerformanceBank, nLastPerformanceBank);
if (Event == MenuEventBankDown)
{
//LOGNOTE("PgmUp");
if (++nPgm > (int) CSysExFileLoader::VoicesPerBank-1)
do
{
// Switch up a voice bank and reset to voice 0
nPgm = 0;
int nVB = m_pMiniDexed->GetTGParameter(CMiniDexed::TGParameterVoiceBank, nTG);
nVB = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankUp(nVB);
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nVB, nTG);
}
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterProgram, nPgm, nTG);
if (nPerformanceBank == 0)
{
// Wrap around
nPerformanceBank = nLastPerformanceBank;
}
else if (nPerformanceBank > 0)
{
--nPerformanceBank;
}
} while ((m_pMiniDexed->IsValidPerformanceBank(nPerformanceBank) != true) && (nPerformanceBank != nStartBank));
m_nSelectedPerformanceBankID = nPerformanceBank;
// Switch to the new bank and select the first performance voice
m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nPerformanceBank);
m_pMiniDexed->SetFirstPerformance();
//LOGNOTE("Performance Bank new=%d, last=%d", m_nSelectedPerformanceBankID, nLastPerformanceBank);
}
// Skip empty voices.
// Use same criteria in EditProgramNumber () too.
string voiceName = m_pMiniDexed->GetVoiceName (nTG);
if (voiceName == "EMPTY "
|| voiceName == " "
|| voiceName == "----------"
|| voiceName == "~~~~~~~~~~" )
else // MenuEventBankUp
{
do
{
if (nPerformanceBank == nLastPerformanceBank)
{
// Wrap around
nPerformanceBank = 0;
}
else if (nPerformanceBank < nLastPerformanceBank)
{
++nPerformanceBank;
}
} while ((m_pMiniDexed->IsValidPerformanceBank(nPerformanceBank) != true) && (nPerformanceBank != nStartBank));
m_nSelectedPerformanceBankID = nPerformanceBank;
m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nPerformanceBank);
m_pMiniDexed->SetFirstPerformance();
//LOGNOTE("Performance Bank new=%d, last=%d", m_nSelectedPerformanceBankID, nLastPerformanceBank);
}
}
else
{
// Bank Up/Down acts on voices within a TG.
// If we're not in the root menu, then see if we are already in a TG menu,
// then find the current TG number. Otherwise assume TG1 (nTG=0).
unsigned nTG = 0;
if (m_MenuStackMenu[0] == s_MainMenu && (m_pCurrentMenu == s_TGMenu) || (m_MenuStackMenu[1] == s_TGMenu)) {
nTG = m_nMenuStackSelection[0];
}
assert (nTG < CConfig::AllToneGenerators);
if (nTG < m_nToneGenerators)
{
if (Event == MenuEventPgmUp) {
PgmUpDownHandler (MenuEventPgmUp);
int nBank = m_pMiniDexed->GetTGParameter (CMiniDexed::TGParameterVoiceBank, nTG);
assert (Event == MenuEventBankDown || Event == MenuEventBankUp);
if (Event == MenuEventBankDown)
{
//LOGNOTE("BankDown");
nBank = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankDown(nBank);
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nBank, nTG);
}
if (Event == MenuEventPgmDown) {
PgmUpDownHandler (MenuEventPgmDown);
else
{
//LOGNOTE("BankUp");
nBank = m_pMiniDexed->GetSysExFileLoader ()->GetNextBankUp(nBank);
m_pMiniDexed->SetTGParameter (CMiniDexed::TGParameterVoiceBank, nBank, nTG);
}
}
}
}
void CUIMenu::TGUpDownHandler (TMenuEvent Event)
{
// This will update the menus to position it for the next TG up or down
unsigned nTG = 0;
if (CConfig::ToneGenerators <= 1) {
if (m_nToneGenerators <= 1) {
// Nothing to do if only a single TG
return;
}
@ -1307,7 +1493,7 @@ void CUIMenu::TGUpDownHandler (TMenuEvent Event)
nTG = m_nMenuStackSelection[0];
}
assert (nTG < CConfig::ToneGenerators);
assert (nTG < CConfig::AllToneGenerators);
assert (Event == MenuEventTGDown || Event == MenuEventTGUp);
if (Event == MenuEventTGDown)
{
@ -1319,7 +1505,7 @@ void CUIMenu::TGUpDownHandler (TMenuEvent Event)
else
{
//LOGNOTE("TGUp");
if (nTG < CConfig::ToneGenerators - 1) {
if (nTG < m_nToneGenerators - 1) {
nTG++;
}
}
@ -1366,7 +1552,15 @@ void CUIMenu::TimerHandlerNoBack (TKernelTimerHandle hTimer, void *pParam, void
void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event)
{
bool bPerformanceSelectToLoad = pUIMenu->m_pMiniDexed->GetPerformanceSelectToLoad();
unsigned nLastPerformance = pUIMenu->m_pMiniDexed->GetLastPerformance();
unsigned nValue = pUIMenu->m_nSelectedPerformanceID;
unsigned nStart = nValue;
if (pUIMenu->m_pMiniDexed->IsValidPerformance(nValue) != true)
{
// A bank change has left the selected performance out of sync
nValue = pUIMenu->m_pMiniDexed->GetActualPerformanceID();
pUIMenu->m_nSelectedPerformanceID = nValue;
}
std::string Value;
if (Event == MenuEventUpdate)
@ -1380,17 +1574,25 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event)
}
if(!pUIMenu->m_bPerformanceDeleteMode)
{
{
switch (Event)
{
case MenuEventUpdate:
break;
case MenuEventStepDown:
if (nValue > 0)
do
{
--nValue;
}
if (nValue == 0)
{
// Wrap around
nValue = nLastPerformance;
}
else if (nValue > 0)
{
--nValue;
}
} while ((pUIMenu->m_pMiniDexed->IsValidPerformance(nValue) != true) && (nValue != nStart));
pUIMenu->m_nSelectedPerformanceID = nValue;
if (!bPerformanceSelectToLoad && pUIMenu->m_nCurrentParameter==0)
{
@ -1399,10 +1601,18 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event)
break;
case MenuEventStepUp:
if (++nValue > (unsigned) pUIMenu->m_pMiniDexed->GetLastPerformance()-1)
do
{
nValue = pUIMenu->m_pMiniDexed->GetLastPerformance()-1;
}
if (nValue == nLastPerformance)
{
// Wrap around
nValue = 0;
}
else if (nValue < nLastPerformance)
{
++nValue;
}
} while ((pUIMenu->m_pMiniDexed->IsValidPerformance(nValue) != true) && (nValue != nStart));
pUIMenu->m_nSelectedPerformanceID = nValue;
if (!bPerformanceSelectToLoad && pUIMenu->m_nCurrentParameter==0)
{
@ -1421,7 +1631,7 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event)
break;
case 1:
if (pUIMenu->m_nSelectedPerformanceID != 0)
if (pUIMenu->m_pMiniDexed->IsValidPerformance(pUIMenu->m_nSelectedPerformanceID))
{
pUIMenu->m_bPerformanceDeleteMode=true;
pUIMenu->m_bConfirmDeletePerformance=false;
@ -1474,17 +1684,24 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event)
if(!pUIMenu->m_bPerformanceDeleteMode)
{
Value = pUIMenu->m_pMiniDexed->GetPerformanceName(nValue);
unsigned nBankNum = pUIMenu->m_pMiniDexed->GetPerformanceBank();
std::string nPSelected = "";
std::string nPSelected = "000";
nPSelected += std::to_string(nBankNum+1); // Convert to user-facing bank number rather than index
nPSelected = nPSelected.substr(nPSelected.length()-3,3);
std::string nPPerf = "000";
nPPerf += std::to_string(nValue+1); // Convert to user-facing performance number rather than index
nPPerf = nPPerf.substr(nPPerf.length()-3,3);
nPSelected += ":"+nPPerf;
if(nValue == pUIMenu->m_pMiniDexed->GetActualPerformanceID())
{
nPSelected= "[L]";
nPSelected += " [L]";
}
pUIMenu->m_pUI->DisplayWrite (pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, nPSelected.c_str(),
Value.c_str (),
(int) nValue > 0, (int) nValue < (int) pUIMenu->m_pMiniDexed->GetLastPerformance()-1);
Value.c_str (), true, true);
// (int) nValue > 0, (int) nValue < (int) pUIMenu->m_pMiniDexed->GetLastPerformance());
}
else
{
@ -1492,6 +1709,88 @@ void CUIMenu::PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event)
}
}
void CUIMenu::EditPerformanceBankNumber (CUIMenu *pUIMenu, TMenuEvent Event)
{
bool bPerformanceSelectToLoad = pUIMenu->m_pMiniDexed->GetPerformanceSelectToLoad();
unsigned nLastPerformanceBank = pUIMenu->m_pMiniDexed->GetLastPerformanceBank();
unsigned nValue = pUIMenu->m_nSelectedPerformanceBankID;
unsigned nStart = nValue;
std::string Value;
switch (Event)
{
case MenuEventUpdate:
break;
case MenuEventStepDown:
do
{
if (nValue == 0)
{
// Wrap around
nValue = nLastPerformanceBank;
}
else if (nValue > 0)
{
--nValue;
}
} while ((pUIMenu->m_pMiniDexed->IsValidPerformanceBank(nValue) != true) && (nValue != nStart));
pUIMenu->m_nSelectedPerformanceBankID = nValue;
if (!bPerformanceSelectToLoad)
{
// Switch to the new bank and select the first performance voice
pUIMenu->m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nValue);
pUIMenu->m_pMiniDexed->SetFirstPerformance();
}
break;
case MenuEventStepUp:
do
{
if (nValue == nLastPerformanceBank)
{
// Wrap around
nValue = 0;
}
else if (nValue < nLastPerformanceBank)
{
++nValue;
}
} while ((pUIMenu->m_pMiniDexed->IsValidPerformanceBank(nValue) != true) && (nValue != nStart));
pUIMenu->m_nSelectedPerformanceBankID = nValue;
if (!bPerformanceSelectToLoad)
{
pUIMenu->m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nValue);
pUIMenu->m_pMiniDexed->SetFirstPerformance();
}
break;
case MenuEventSelect:
if (bPerformanceSelectToLoad)
{
pUIMenu->m_pMiniDexed->SetParameter (CMiniDexed::ParameterPerformanceBank, nValue);
pUIMenu->m_pMiniDexed->SetFirstPerformance();
}
break;
default:
return;
}
Value = pUIMenu->m_pMiniDexed->GetPerformanceConfig ()->GetPerformanceBankName(nValue);
std::string nPSelected = "000";
nPSelected += std::to_string(nValue+1); // Convert to user-facing number rather than index
nPSelected = nPSelected.substr(nPSelected.length()-3,3);
if(nValue == (unsigned)pUIMenu->m_pMiniDexed->GetParameter (CMiniDexed::ParameterPerformanceBank))
{
nPSelected += " [L]";
}
pUIMenu->m_pUI->DisplayWrite (pUIMenu->m_pParentMenu[pUIMenu->m_nCurrentMenuItem].Name, nPSelected.c_str(),
Value.c_str (), true, true);
}
void CUIMenu::InputTxt (CUIMenu *pUIMenu, TMenuEvent Event)
{
unsigned nTG=0;
@ -1710,5 +2009,3 @@ void CUIMenu::EditTGParameterModulation (CUIMenu *pUIMenu, TMenuEvent Event)
nValue > rParam.Minimum, nValue < rParam.Maximum);
}

@ -25,6 +25,7 @@
#include <string>
#include <circle/timer.h>
#include "config.h"
class CMiniDexed;
class CUserInterface;
@ -47,13 +48,15 @@ public:
MenuEventPressAndStepUp,
MenuEventPgmUp,
MenuEventPgmDown,
MenuEventBankUp,
MenuEventBankDown,
MenuEventTGUp,
MenuEventTGDown,
MenuEventUnknown
};
public:
CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed);
CUIMenu (CUserInterface *pUI, CMiniDexed *pMiniDexed, CConfig *pConfig);
void EventHandler (TMenuEvent Event);
@ -91,6 +94,7 @@ private:
static void EditTGParameterModulation (CUIMenu *pUIMenu, TMenuEvent Event);
static void PerformanceMenu (CUIMenu *pUIMenu, TMenuEvent Event);
static void SavePerformanceNewFile (CUIMenu *pUIMenu, TMenuEvent Event);
static void EditPerformanceBankNumber (CUIMenu *pUIMenu, TMenuEvent Event);
static std::string GetGlobalValueString (unsigned nParameter, int nValue);
static std::string GetTGValueString (unsigned nTGParameter, int nValue);
@ -117,6 +121,7 @@ private:
void OPShortcutHandler (TMenuEvent Event);
void PgmUpDownHandler (TMenuEvent Event);
void BankUpDownHandler (TMenuEvent Event);
void TGUpDownHandler (TMenuEvent Event);
static void TimerHandler (TKernelTimerHandle hTimer, void *pParam, void *pContext);
@ -127,6 +132,9 @@ private:
private:
CUserInterface *m_pUI;
CMiniDexed *m_pMiniDexed;
CConfig *m_pConfig;
unsigned m_nToneGenerators;
const TMenuItem *m_pParentMenu;
const TMenuItem *m_pCurrentMenu;
@ -169,6 +177,7 @@ private:
bool m_bPerformanceDeleteMode=false;
bool m_bConfirmDeletePerformance=false;
unsigned m_nSelectedPerformanceID =0;
unsigned m_nSelectedPerformanceBankID =0;
bool m_bSplashShow=false;
};

@ -22,6 +22,23 @@
#ifndef _usbminidexedmidigadget_h
#define _usbminidexedmidigadget_h
#if RASPPI==5
#include <circle/sysconfig.h>
#include <assert.h>
#warning No support for USB Gadget Mode on RPI 5 yet
class CUSBMiniDexedMIDIGadget
{
public:
CUSBMiniDexedMIDIGadget (CInterruptSystem *pInterruptSystem)
{
}
~CUSBMiniDexedMIDIGadget (void)
{
}
};
#else
#include <circle/usb/gadget/usbmidigadget.h>
#include <circle/usb/gadget/usbmidigadgetendpoint.h>
#include <circle/sysconfig.h>
@ -82,5 +99,6 @@ protected:
return CUSBMIDIGadget::GetDescriptor(wValue, wIndex, pLength);
}
};
#endif
#endif

@ -27,17 +27,18 @@
LOGMODULE ("ui");
CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CConfig *pConfig)
CUserInterface::CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, CConfig *pConfig)
: m_pMiniDexed (pMiniDexed),
m_pGPIOManager (pGPIOManager),
m_pI2CMaster (pI2CMaster),
m_pSPIMaster (pSPIMaster),
m_pConfig (pConfig),
m_pLCD (0),
m_pLCDBuffered (0),
m_pUIButtons (0),
m_pRotaryEncoder (0),
m_bSwitchPressed (false),
m_Menu (this, pMiniDexed)
m_Menu (this, pMiniDexed, pConfig)
{
}
@ -57,17 +58,69 @@ bool CUserInterface::Initialize (void)
{
unsigned i2caddr = m_pConfig->GetLCDI2CAddress ();
unsigned ssd1306addr = m_pConfig->GetSSD1306LCDI2CAddress ();
bool st7789 = m_pConfig->GetST7789Enabled ();
if (ssd1306addr != 0) {
m_pSSD1306 = new CSSD1306Device (m_pConfig->GetSSD1306LCDWidth (), m_pConfig->GetSSD1306LCDHeight (),
m_pI2CMaster, ssd1306addr,
m_pConfig->GetSSD1306LCDRotate (), m_pConfig->GetSSD1306LCDMirror ());
LOGDBG ("LCD: SSD1306");
if (!m_pSSD1306->Initialize ())
{
LOGDBG("LCD: SSD1306 initialization failed");
return false;
}
LOGDBG ("LCD: SSD1306");
m_pLCD = m_pSSD1306;
} else if (i2caddr == 0)
}
else if (st7789)
{
if (m_pSPIMaster == nullptr)
{
LOGDBG("LCD: ST7789 Enabled but SPI Initialisation Failed");
return false;
}
unsigned long nSPIClock = 1000 * m_pConfig->GetSPIClockKHz();
unsigned nSPIMode = m_pConfig->GetSPIMode();
unsigned nCPHA = (nSPIMode & 1) ? 1 : 0;
unsigned nCPOL = (nSPIMode & 2) ? 1 : 0;
LOGDBG("SPI: CPOL=%u; CPHA=%u; CLK=%u",nCPOL,nCPHA,nSPIClock);
m_pST7789Display = new CST7789Display (m_pSPIMaster,
m_pConfig->GetST7789Data(),
m_pConfig->GetST7789Reset(),
m_pConfig->GetST7789Backlight(),
m_pConfig->GetST7789Width(),
m_pConfig->GetST7789Height(),
nCPOL, nCPHA, nSPIClock,
m_pConfig->GetST7789Select());
if (m_pST7789Display->Initialize())
{
m_pST7789Display->SetRotation (m_pConfig->GetST7789Rotation());
bool bLargeFont = !(m_pConfig->GetST7789SmallFont());
m_pST7789 = new CST7789Device (m_pSPIMaster, m_pST7789Display, m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows (), Font8x16, bLargeFont, bLargeFont);
if (m_pST7789->Initialize())
{
LOGDBG ("LCD: ST7789");
m_pLCD = m_pST7789;
}
else
{
LOGDBG ("LCD: Failed to initalize ST7789 character device");
delete (m_pST7789);
delete (m_pST7789Display);
m_pST7789 = nullptr;
m_pST7789Display = nullptr;
return false;
}
}
else
{
LOGDBG ("LCD: Failed to initialize ST7789 display");
delete (m_pST7789Display);
m_pST7789Display = nullptr;
return false;
}
}
else if (i2caddr == 0)
{
m_pHD44780 = new CHD44780Device (m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows (),
m_pConfig->GetLCDPinData4 (),
@ -77,22 +130,24 @@ bool CUserInterface::Initialize (void)
m_pConfig->GetLCDPinEnable (),
m_pConfig->GetLCDPinRegisterSelect (),
m_pConfig->GetLCDPinReadWrite ());
LOGDBG ("LCD: HD44780");
if (!m_pHD44780->Initialize ())
{
LOGDBG("LCD: HD44780 initialization failed");
return false;
}
LOGDBG ("LCD: HD44780");
m_pLCD = m_pHD44780;
}
else
{
m_pHD44780 = new CHD44780Device (m_pI2CMaster, i2caddr,
m_pConfig->GetLCDColumns (), m_pConfig->GetLCDRows ());
LOGDBG ("LCD: HD44780 I2C");
if (!m_pHD44780->Initialize ())
{
LOGDBG("LCD: HD44780 (I2C) initialization failed");
return false;
}
LOGDBG ("LCD: HD44780 I2C");
m_pLCD = m_pHD44780;
}
assert (m_pLCD);
@ -107,37 +162,7 @@ bool CUserInterface::Initialize (void)
LOGDBG ("LCD initialized");
}
m_pUIButtons = new CUIButtons ( m_pConfig->GetButtonPinPrev (),
m_pConfig->GetButtonActionPrev (),
m_pConfig->GetButtonPinNext (),
m_pConfig->GetButtonActionNext (),
m_pConfig->GetButtonPinBack (),
m_pConfig->GetButtonActionBack (),
m_pConfig->GetButtonPinSelect (),
m_pConfig->GetButtonActionSelect (),
m_pConfig->GetButtonPinHome (),
m_pConfig->GetButtonActionHome (),
m_pConfig->GetButtonPinPgmUp (),
m_pConfig->GetButtonActionPgmUp (),
m_pConfig->GetButtonPinPgmDown (),
m_pConfig->GetButtonActionPgmDown (),
m_pConfig->GetButtonPinTGUp (),
m_pConfig->GetButtonActionTGUp (),
m_pConfig->GetButtonPinTGDown (),
m_pConfig->GetButtonActionTGDown (),
m_pConfig->GetDoubleClickTimeout (),
m_pConfig->GetLongPressTimeout (),
m_pConfig->GetMIDIButtonNotes (),
m_pConfig->GetMIDIButtonPrev (),
m_pConfig->GetMIDIButtonNext (),
m_pConfig->GetMIDIButtonBack (),
m_pConfig->GetMIDIButtonSelect (),
m_pConfig->GetMIDIButtonHome (),
m_pConfig->GetMIDIButtonPgmUp (),
m_pConfig->GetMIDIButtonPgmDown (),
m_pConfig->GetMIDIButtonTGUp (),
m_pConfig->GetMIDIButtonTGDown ()
);
m_pUIButtons = new CUIButtons ( m_pConfig );
assert (m_pUIButtons);
if (!m_pUIButtons->Initialize ())
@ -217,7 +242,7 @@ void CUserInterface::DisplayWrite (const char *pMenu, const char *pParam, const
CString Value (" ");
if (bArrowDown)
{
Value = "\x7F"; // arrow left character
Value = "<"; // arrow left character
}
Value.Append (pValue);
@ -232,7 +257,7 @@ void CUserInterface::DisplayWrite (const char *pMenu, const char *pParam, const
}
}
Value.Append ("\x7E"); // arrow right character
Value.Append (">"); // arrow right character
}
Msg.Append (Value);
@ -342,6 +367,14 @@ void CUserInterface::UIButtonsEventHandler (CUIButton::BtnEvent Event)
m_Menu.EventHandler (CUIMenu::MenuEventPgmDown);
break;
case CUIButton::BtnEventBankUp:
m_Menu.EventHandler (CUIMenu::MenuEventBankUp);
break;
case CUIButton::BtnEventBankDown:
m_Menu.EventHandler (CUIMenu::MenuEventBankDown);
break;
case CUIButton::BtnEventTGUp:
m_Menu.EventHandler (CUIMenu::MenuEventTGUp);
break;
@ -388,13 +421,16 @@ void CUserInterface::UISetMIDIButtonChannel (unsigned uCh)
if (uCh == 0)
{
m_nMIDIButtonCh = CMIDIDevice::Disabled;
LOGNOTE("MIDI Button channel not set");
}
else if (uCh < CMIDIDevice::Channels)
else if (uCh <= CMIDIDevice::Channels)
{
m_nMIDIButtonCh = uCh - 1;
LOGNOTE("MIDI Button channel set to: %d", m_nMIDIButtonCh+1);
}
else
{
m_nMIDIButtonCh = CMIDIDevice::OmniMode;
LOGNOTE("MIDI Button channel set to: OMNI");
}
}
}

@ -26,16 +26,18 @@
#include <sensor/ky040.h>
#include <display/hd44780device.h>
#include <display/ssd1306device.h>
#include <display/st7789device.h>
#include <circle/gpiomanager.h>
#include <circle/writebuffer.h>
#include <circle/i2cmaster.h>
#include <circle/spimaster.h>
class CMiniDexed;
class CUserInterface
{
public:
CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CConfig *pConfig);
CUserInterface (CMiniDexed *pMiniDexed, CGPIOManager *pGPIOManager, CI2CMaster *pI2CMaster, CSPIMaster *pSPIMaster, CConfig *pConfig);
~CUserInterface (void);
bool Initialize (void);
@ -68,11 +70,14 @@ private:
CMiniDexed *m_pMiniDexed;
CGPIOManager *m_pGPIOManager;
CI2CMaster *m_pI2CMaster;
CSPIMaster *m_pSPIMaster;
CConfig *m_pConfig;
CCharDevice *m_pLCD;
CHD44780Device *m_pHD44780;
CSSD1306Device *m_pSSD1306;
CST7789Display *m_pST7789Display;
CST7789Device *m_pST7789;
CWriteBufferDevice *m_pLCDBuffered;
CUIButtons *m_pUIButtons;

@ -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 695ab4a
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 fe09c4b
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