build(firmware): migrate to PlatformIO and vendor memllib (plan §5)

One cut, no dual path. Closes ALIGNMENT defect 3 ("Arduino-CLI build
machinery is actively hostile") and vision bullet 4.

platformio.ini carries 16 [env:], one per variant, each passing
-DMEMLNAUT_MODE_TYPE; selftest passes -DNISPS_SELFTEST=1 instead. The env list
IS the registry now — the .ino comment-registry and the NISPS_ST_* token-paste
table are deleted rather than migrated. L12 noted that table was already
silently missing the currently-shipped SLPWorkshop variant, which is the whole
argument against having a second list.

Also deleted: the Python/sed machinery that rewrote the COMMITTED .ino on every
build, the sketch symlink forest, the global TFT_eSPI User_Setup.h mutation
(now -D flags — TFT_eSPI's own documented PlatformIO recipe), the UF2
boot-mount detection stack (upload_protocol=picotool talks to the bootloader
directly), and build-firmware-arch.sh entirely. Scripts 683 -> 435 lines.

memllib is vendored at lib/memllib/ from upstream e291192; no submodules
remain. VENDORED.md records provenance and the re-sync procedure.

S9: a firmware-build CI job compiles three representative envs against a cached
toolchain and reports per-variant flash/RAM. Firmware is in an automated gate
for the FIRST time. The old ci.yml comment justified excluding it as "low
verification value" — an assessment that did not survive contact, since the
SelfTest variant sat broken for an unknown period calling a DisplayDriver
method that did not exist at the pinned memllib commit, and nothing noticed
because nothing built it.

Verified: all 16 envs build from an empty cache, each within ~520 bytes of the
arduino-cli binary it replaces, flash and RAM. Measured as .text+.rodata /
.data+.bss+vector+uninitialized — NOT PlatformIO's console line, which
double-counts .data on this board. This does not prove the hardware boots; no
flash+smoke test was possible and that stays an operator chokepoint.

  slpworkshop 248232/145028   pafsynth 256880/149716   selftest 216228/17960
  (all 16 in the CI log format; none exceeds 2% of a 16 MB flash)

Two traps recorded so nobody rediscovers them: vendoring memllib's subdirs
without a src/ wrapper makes PlatformIO's library builder silently compile
NOTHING while still linking; and project build_flags land BEFORE the
framework's own -std=gnu++17 -Os, so build_unflags is required.

CORRECTION carried in this commit: the firmware sizes in c19d846's message and
the first version of the memllib recon doc were wrong — SLPWorkshop 145348,
PAFSynth 145300, SelfTest 141840. They came from building variants in sequence
through a SHARED incremental arduino-cli build directory, which reused stale
objects and under-reported by ~75 KB. Clean-cache rebuilds of the identical
commit give 216736/18492 for SelfTest. The real cost of the memllib upstream
bump is +216 bytes flash, not +316. Never measure firmware size through a
reused build dir.

HISTORY NOTE: this commit and the docs commit before it were rebuilt (force-push,
2026-07-21) so that each contains only what its message describes. The first
versions had the firmware deletions stranded in the docs commit by a shared-index
race between concurrent agents; content is byte-identical to the originals.

Gates: run-all-tests.sh ALL GREEN (nisps/ untouched by this change beyond
include paths); 16/16 pio envs build.
This commit is contained in:
monkey-w1n5t0n 2026-07-21 20:17:58 +02:00
parent 9ad1f78ddd
commit 68d4cc4017
135 changed files with 18955 additions and 739 deletions

View file

@ -2,17 +2,23 @@ name: CI
# Stream 11 verification pipeline.
#
# Two parallel jobs:
# Three parallel jobs:
# * cpp-tests — builds nisps host C++ tests, builds nisps.wasm, runs
# the parity check, runs the lint script.
# * manifold-tests — typechecks the React manifold app, runs bun unit
# tests, builds the production bundle, runs Playwright
# e2e tests.
#
# Firmware compilation is NOT included in this workflow. Arduino-cli +
# rp2040 board package add ~2 minutes per run, and the verification value
# is low compared to the time cost; firmware build is documented as a
# manual `scripts/build-firmware.sh` step in README.md / CLAUDE.md.
# * firmware-build — compiles three representative PlatformIO envs for the
# RP2350 target.
#
# Firmware entered CI for the first time with the Phase 4 PlatformIO migration
# (plan §5, S9). It was previously excluded because arduino-cli + the rp2040
# board package cost ~2 minutes per run for "low verification value" — an
# assessment that did not survive contact: the SelfTest variant sat broken for
# an unknown period, calling a DisplayDriver method that did not exist at the
# pinned memllib commit, and nothing noticed because nothing built it. PIO's
# toolchain caches cleanly, so the cost is now a cache restore.
on:
push:
@ -28,8 +34,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build deps
run: |
@ -155,3 +159,56 @@ jobs:
name: playwright-report
path: manifold/playwright-report/
retention-days: 7
firmware-build:
name: Firmware (RP2350, PlatformIO)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# The platform wrapper and the arduino-pico framework are fetched from
# git and total ~1-2 GB. Key on platformio.ini because that file pins
# both versions — change a pin, get a fresh toolchain.
- name: Cache PlatformIO toolchain
uses: actions/cache@v4
with:
path: |
~/.platformio
~/.cache/pip
key: pio-${{ runner.os }}-${{ hashFiles('firmware/MEMLNaut-NISPS/platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Install PlatformIO
run: pip install --upgrade platformio
# Three representative envs rather than all sixteen: they differ only in
# which mode type is instantiated, so a compile break is almost always
# common to all. slpworkshop is the shipped default, pafsynth is the
# heaviest RAM user, and selftest takes the separate NISPS_SELFTEST fork
# through main.cpp — the one that broke unnoticed before.
- name: Build representative firmware variants
working-directory: firmware/MEMLNaut-NISPS
run: pio run -e slpworkshop -e pafsynth -e selftest
# Sizes are reported, not asserted. A threshold would either be slack
# enough to be meaningless or tight enough to fail on unrelated work;
# this puts the numbers in the log so a jump is visible in review.
# Flash = .text + .rodata, RAM = .data + .bss + vector table +
# uninitialized — matching arduino-cli's convention, NOT PlatformIO's
# own console line, which double-counts .data on this board.
- name: Report flash/RAM per variant
working-directory: firmware/MEMLNaut-NISPS
run: |
SIZE=$(find ~/.platformio/packages -name 'arm-none-eabi-size' | head -1)
for e in slpworkshop pafsynth selftest; do
"$SIZE" -A ".pio/build/$e/firmware.elf" | awk -v e="$e" '
/^\.text/{t=$2} /^\.rodata/{r=$2} /^\.data/{d=$2}
/^\.bss/{b=$2} /^\.ram_vector_table/{v=$2}
/^\.uninitialized_data/{u=$2}
END{printf "%-14s flash=%-8d ram=%-8d\n", e, t+r, d+b+v+u}'
done

6
.gitignore vendored
View file

@ -33,6 +33,12 @@
.ai
build
# PlatformIO build artifacts (firmware/MEMLNaut-NISPS) — bootstrapped platform,
# toolchain, and libraries live outside the repo in ~/.platformio; only
# .pio/build (per-env objects/elf/uf2) and .pio/libdeps (fetched lib_deps)
# land here, both disposable.
.pio
# OSC bridge compiled binaries (built via compile.sh or CI)
manifold/osc-bridge/dist/
manifold/osc-bridge/node_modules/

10
.gitmodules vendored
View file

@ -1,10 +0,0 @@
[submodule "src/memllib"]
path = src/memllib
# Upstream (the lab's shared library), not the monkey-w1n5t0n fork.
# Phase 0 pointed this at the fork because the pin b37fc53 existed on no
# remote. Those three fork commits touch only `examples/`, which the
# firmware never compiles (it is not in the sketch symlink forest) and whose
# content is already ported into nisps/ml/{jolt,ou_noise,feedback,geo_push}.
# With the pin moved to an upstream commit, upstream is the correct source.
# The fork's feat/nisps-core-swap branch is still pushed; nothing is lost.
url = https://github.com/MusicallyEmbodiedML/memllib.git

View file

@ -28,15 +28,7 @@ The clean-slate rewrite (2026-04-29) consolidated everything into one C++20 code
**Rough cost.** Product-model decision first (plan §7.6), then incremental: picker is days; the curated-preset model seeds from `backends/presets.ts` + schemas; disclosure via per-drawer depth levels. Deleting the decorative stratum is part of the Phase-1 sweep.
### 3. Arduino-CLI build machinery is actively hostile — vision 4 unstarted (2026-07-21)
**What.** The build script sed-mutates the committed `.ino` to select variants (polluting history), the mode list is triple-bookkept (a `NISPS_ST_*` token-paste table is already silently missing the currently-active SLPWorkshop variant), a symlink forest works around Arduino's include rules, and the toolchain globally mutates the installed TFT_eSPI library. Firmware compilation is in no automated gate anywhere.
**Why it blocks the mission.** Fragile-by-design builds are the opposite of "confident agentic changes"; the vision names PlatformIO explicitly. `firmware/useq-celium/` already proves the PIO pattern in-repo.
**Rough cost.** 23 days, one cut (plan §5): env-per-variant `platformio.ini`, delete ~400 lines of hackery, then a firmware CI job. Gated on the memllib ownership decision (plan §7.5).
### 4. Manifold-as-hardware-editor is a facade (2026-07-21)
### 3. Manifold-as-hardware-editor is a facade (2026-07-21)
**What.** Vision bullet 5 exists as a 237-line Web Serial shell: sound connect lifecycle, zero protocol (`saveModel`/`restoreModel`/`getSettings` are literal stubs), and firmware has no serial command surface or on-device persistence to talk to.
@ -44,7 +36,7 @@ The clean-slate rewrite (2026-04-29) consolidated everything into one C++20 code
**Rough cost.** Week+, spec-first (plan §6.5d). The right discipline already exists in-repo: useq-celium's C-header wire truth + TS mirror + parity test; settings payloads should derive from schema codegen.
### 5. Dead mass and registry sprawl across every layer (2026-07-21)
### 4. Dead mass and registry sprawl across every layer (2026-07-21)
**What.** *Phase 1 landed 2026-07-21 and removed the bulk of this:* the dead focus/altitude UI system, the decorative control stratum, 12 dead WASM API entries across the 5-file registration chain, the vendored daisysp tree, retired-playground artifacts and root planning relics, 5 unused primitives, the duplicate backend editor and catalogue, `voice_space.hpp`, `fixed_buffer.hpp`, the dead perf-macro regime, and the OSC bridge twin. What remains is the *registry* half: mode identity spread across ~6 hand-maintained registries with demonstrated drift, MLP dims typed twice, per-mode schema blocks hand-written in C++, and assorted stale specs presenting a deleted world as present tense.
@ -52,13 +44,13 @@ The clean-slate rewrite (2026-04-29) consolidated everything into one C++20 code
**Rough cost.** Plan phase 3 (~23 days, codegen takes ownership) plus the docs disposition pass (§8). The behaviour bugs found en route (dataset-cap divergence, VCV 2-D input truncation, VCV audio-thread race and JSON) were fixed in phase 2 on 2026-07-21.
### 6. No performance measurement despite a performance-defined mission (2026-07-21)
### 5. No performance measurement despite a performance-defined mission (2026-07-21)
**What.** The "super performance-sensitive" constraint is enforced only by static discipline (no-heap lint — itself with proven false negatives — and section attrs, 3/5 of which are dead macros). No benchmark, no CPU-load assertion, no flash/RAM size report on either target; the 16 KB dead buffer was found by reading, not by any gate.
**What.** The "super performance-sensitive" constraint is enforced only by static discipline (the no-heap lint — false negatives closed in Phase 2 — and section attrs). *Half-closed 2026-07-21:* the Phase 4 firmware CI job now reports per-variant flash/RAM on every push, so size regressions are at least visible. **Still missing: any measure of time.** No benchmark, no CPU-load assertion, no blocks-per-second number on either target — nothing would catch an engine getting 3x slower.
**Rough cost.** ~A day for a host-side blocks-per-second benchmark + a per-variant size report in `build-firmware.sh` (plan §6.5f).
**Rough cost.** ~Half a day now: a host-side blocks-per-second benchmark for `engine_process_block`, native + WASM (plan §6.5f).
### 7. Training-health telemetry: decided, not yet built (2026-07-21)
### 6. Training-health telemetry: decided, not yet built (2026-07-21)
**What.** Four fragments of one feature. Fragment 3 (decorative gradient-health UI) was deleted in
Phase 1. The other three stand: a 16 KB loss-history buffer in every firmware MLP that nothing
@ -68,7 +60,7 @@ reads (`nisps/ml/mlp.hpp`); a WASM worker faking a **1-element** loss history
**Decisions are now complete** (operator, §7.3 + L25): telemetry becomes **real, browser-only,
behind a feature flag**; the fakes go; and the **firmware buffer stays** — it is the on-device
record the hardware editor (defect 4) will want, and it costs flash we demonstrably have (16% RAM
record the hardware editor (defect 3) will want, and it costs flash we demonstrably have (16% RAM
on the largest variant). So the remaining work is one coherent job, not a judgement call: plumb
`loss_history` through the C API (`Drawers.tsx:263` already marks the gap), replace the worker's
fake with it, and put the display plus `get_layer_stats` behind the advanced-mode flag.
@ -78,7 +70,7 @@ it *looks* answered while being fabricated — worse than absent.
**Rough cost.** ~A day, spec-light: plan §6.5e, no longer gated on anything.
### 8. RMSProp still deferred from `nisps/ml/` (2026-04-29; reaffirmed 2026-07-21)
### 7. RMSProp still deferred from `nisps/ml/` (2026-04-29; reaffirmed 2026-07-21)
**What.** `training.hpp` ships SGD only; the legacy firmware used RMSProp for `TrainBatch`. Optimizer choice is a research axis. Not blocking current fits; will matter for harder loss landscapes. Port target: upstream MusicallyEmbodiedML `memlp` (the in-repo `src/memlp` copy is deleted; use the GitHub remote or archive branch).
@ -105,10 +97,10 @@ Operator decision: **vendor**, self-contained in this repo. The inventory
subset — it is all of memllib bar `examples/` (~1.8 MB, 24/24 compiled TUs link). The fork is
dissolved: its three commits touch only `examples/`, which the firmware never compiles and whose
content already lives in `nisps/ml/{jolt,ou_noise,feedback,geo_push}.hpp`, so the submodule now
points at upstream and is pinned to current `main` (verified: all three variants build, +316 bytes
flash, and it brings the `l r input swap` hardware fix plus the `NavigateToView` the SelfTest
variant was already written against). **Remaining: the vendoring copy itself**, which lands with the
PlatformIO cut (plan §5). Delete this entry when it does.
points at upstream and is pinned to current `main`. Verified by building: it brings the
`l r input swap` hardware fix plus the `NavigateToView` the SelfTest variant was already written
against, and costs **+216 bytes of a 16 MB flash**. **Remaining: the vendoring copy itself**, which
lands with the PlatformIO cut (plan §5). Delete this entry when it does.
### Q5: Legacy feedback modes — delete or keep for A/B? (2026-07-21)
@ -124,6 +116,13 @@ PlatformIO cut (plan §5). Delete this entry when it does.
## Recently resolved (delete after a few weeks)
- 2026-07-21: Full-repo simplification audit landed (recon + plan + this rewrite). Superseded entries removed: "browser-only engines incomplete" (→ defect 2/plan 5b), "loss curve not plumbed" (→ defect 8), "NISPS_AUDIO_FUNC misshapen" (→ plan Phase 1, S21/L13), stale "VCV not currently maintained" note (vcv/ is active and consumes `nisps/` directly post-P6).
- 2026-07-21: **Arduino-CLI build machinery (old defect 3) is gone.** Phase 4 replaced it with a
PlatformIO project: one `[env:]` per variant is now the only variant registry, the `.ino`-mutating
Python/sed machinery and the `NISPS_ST_*` token-paste table and the sketch symlink forest and the
global TFT_eSPI mutation are all deleted, memllib is vendored (no submodule), and firmware finally
entered CI — three representative envs per run, which is what would have caught the SelfTest
variant sitting broken. All 16 envs build; sizes match arduino-cli within ~520 bytes.
- 2026-07-21: Full-repo simplification audit landed (recon + plan + this rewrite). Superseded entries removed: "browser-only engines incomplete" (→ defect 2/plan 5b), "loss curve not plumbed" (→ defect 7), "NISPS_AUDIO_FUNC misshapen" (→ plan Phase 1, S21/L13), stale "VCV not currently maintained" note (vcv/ is active and consumes `nisps/` directly post-P6).
- 2026-07-18: Browser curve maths unified onto the canonical `nisps/core/math.hpp` catalog at P4; four silently-divergent TS curves re-baselined.
- 2026-07-14: WASM MLP fixed-architecture defect resolved by P2 (`MLPCore<Storage>`; browser runtime-shaped, firmware zero-heap fixed).

19
MAP.md
View file

@ -15,17 +15,18 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
- `nisps/midi/generated/midi_devices.hpp` — codegen output: no-heap `constexpr` external-MIDI-synth templates (`nisps::midi::generated`; `MidiParam`/`MidiDevice` + `kMidiDevices` registry). Source = `schemas/midi_devices/`; do not edit by hand.
- `nisps/CMakeLists.txt` + `nisps/build/` — host-target builds + ctest.
### `firmware/` — Arduino sketch + hardware glue
- `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` — entry point. Selects active mode at compile time via `#define MEMLNAUT_MODE_TYPE`. Forks on `NISPS_SELFTEST`: normal modes run the engine/ML path; the `SelfTest` variant delegates all four entry points to `glue/selftest.hpp`.
### `firmware/` — PlatformIO project + hardware glue
- `firmware/MEMLNaut-NISPS/platformio.ini`**the variant registry**: one `[env:<alias>]` per firmware variant (16 of them), each passing `-DMEMLNAUT_MODE_TYPE=<alias>`; `selftest` passes `-DNISPS_SELFTEST=1` instead. There is no second list to keep in sync. Shared `[env]` base pins the platform wrapper + arduino-pico framework, sets `-std=gnu++20 -O3` (via `build_unflags`, because the framework appends its own `-std=gnu++17 -Os` AFTER project flags), reaches `nisps/` with `-I${PROJECT_DIR}/../..`, and carries the TFT_eSPI panel config as `-D` flags. Build: `pio run -e <alias>`, or `scripts/build-firmware.sh [--all]`.
- `firmware/MEMLNaut-NISPS/src/main.cpp` — entry point (was `MEMLNaut-NISPS.ino`). Forks on `NISPS_SELFTEST`: normal modes run the engine/ML path; the `SelfTest` variant delegates all four entry points to `glue/selftest.hpp`.
- `firmware/MEMLNaut-NISPS/glue/` — hardware bindings:
- `audio_driver.hpp` — bridges memllib `AudioDriver` callback → `Mode::process(stereosample_t)`.
- `peripherals.hpp` — joystick / pots / buttons → `Mode::set_input` and ML primitives. Wires the shared `FeedbackController` ExploreAndPlace lifecycle (MomA1 = enter/exit explore, MomA2 = freeze/place, TogB2 = commit; MomB1/MomB2 = reroll/nudge while exploring **or** grab/drop *reposition* while idle) plus the adaptive-learning gestures: **TogB1** = Jolt (held weight morph), **RVX1** = exploration amount (OU output walk). Reposition relocates an existing positive example's output to a new input position (`feedback.hpp` `begin_reposition`/`commit_reposition`) — no scratchpad, no weight restore.
- `midi_io.hpp` — MIDI in → mode `note_on`/`update_bpm`/`set_playing`; drains `ControlEvent` ring → MIDI UART.
- `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line. Includes the six `MEMLNautModeExtSynth*` external-synth variants (one per device template in `nisps/midi`, e.g. `MEMLNautModeExtSynthSub37`). Also defines the `MEMLNautModeSelfTest` pseudo-variant (tag type) + the `NISPS_ST_*`/`NISPS_ST_CAT` token-paste macros the `.ino` uses to compute `NISPS_SELFTEST`. Note: `src/nisps/` exposes each referenced top-level nisps subdir as a symlink — `midi` was added alongside `core/dsp/engines/ml/modes`.
- `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types, selected by the `-D` from platformio.ini. Includes the six `MEMLNautModeExtSynth*` external-synth variants (one per device template in `nisps/midi`, e.g. `MEMLNautModeExtSynthSub37`). The `NISPS_ST_*`/`NISPS_ST_CAT` token-paste table and the `SelfTestRig` tag type are GONE — selftest is now just an env with its own `-D`.
- `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`.
- `output_router.hpp` — top-level `drain_outputs()` entry point. (Inputs are wired directly by `peripherals.hpp`'s `bind_peripherals()`.)
- `settings_view.hpp``wire_settings(mode)`: adds on-device settings views to the MEMLNaut display carousel (TFT + rotary encoder). Joystick Dual/Single toggle for the 4-input ("two 2-D joystick") modes — "Single" pins ML input channels 2,3 to neutral via `ModeBase::set_input_pinned` (no net rebuild). Registered in the `.ino` after `addSystemInfoView()`.
- `firmware/MEMLNaut-NISPS/src/{memllib,nisps}` — symlinks (Arduino-CLI requires sketch-tree includes; preprocessor refuses `..` in headers).
- `firmware/MEMLNaut-NISPS/lib/memllib/` — **vendored** memllib (was the `src/memllib` submodule): hardware abstraction (audio driver, TFT display, MIDI, peripherals), ~1.9 MB / 100 files, `examples/` dropped. `VENDORED.md` records the upstream commit and the re-sync procedure; `LICENSE` is MPL-2.0, copied verbatim. **Sources must sit under `lib/memllib/src/`** — PlatformIO's library builder falls back to a flat root-only scan without it and silently compiles nothing while still linking (see VENDORED.md).
- `firmware/README.md` — structure + build instructions.
- `firmware/useq-celium/` — standalone RP2040 firmware (PlatformIO, Arduino-Pico core) that turns a uSEQ module + CV expander into a USB→CV/gate converter driven by the manifold `cvgate` backend. `shared/protocol.h` is the v2 wire-protocol single source of truth (mirrored by `manifold/src/backends/useq-protocol.ts`); `main/` (USB serial → CV13 + GATE13, I2C → expander) and `expander/` (I2C slave → CV411). Wire spec: `docs/specs/useq-cv-protocol.md`. Restored from the April-2026 "uSEQ-Celium" mode.
@ -127,7 +128,7 @@ includes; no `nisps-core`.
- `ci.yml` — GitHub Actions: cmake build + ctest + WASM build + parity check + lint + Playwright (cpp-tests + manifold-tests jobs). Firmware compile is documented as manual.
### `src/` — submodule + vendored trees
- `src/memllib/` — hardware abstraction (audio driver, peripherals, MIDI), the only true submodule. **Not auto-initialized** — fresh clones need `git submodule update --init --recursive`. Pinned to `monkey-w1n5t0n/memllib` branch `feat/nisps-core-swap` (the operator's fork; upstream is `MusicallyEmbodiedML/memllib`). Ownership decision — vendor the load-bearing subset into this repo — lands with the PlatformIO migration (plan §5, §7.5).
- **There are no submodules.** `src/memllib` was one until the Phase 4 PlatformIO migration; it is now vendored at `firmware/MEMLNaut-NISPS/lib/memllib/`. Fresh clones need no `git submodule` step.
### Top-level docs
- `CLAUDE.md` — long-form architecture narrative.
@ -150,7 +151,7 @@ includes; no `nisps-core`.
## Conventions
- Firmware mode selection is compile-time only — `#define MEMLNAUT_MODE_TYPE` in the `.ino`.
- Firmware mode selection is compile-time only — one `-DMEMLNAUT_MODE_TYPE` per `[env:]` in `platformio.ini`.
- `nisps/` follows Chris's RP2350 perf rules: no heap, `static const float` for non-trivial constants, strict `.f` suffix. `perf.hpp` now carries only `NISPS_HOT`/`NISPS_FORCE_INLINE`; the three dead/misshapen SRAM-section macros were deleted in the 2026-07 sweep (S21/L13).
- C++ identifiers: `PascalCase` types, `snake_case` functions/variables, `kPascalCase` constexpr. JSON keys `snake_case`. TS types `PascalCase`, components `PascalCase.tsx`, modules `kebab-case.ts`.
- `Curve` enum lives in `nisps/core/math.hpp` (lowercase: `linear/exp/log/square/sqrt/sigmoid/cubic`, plus the parameterised `centered_power` free function); generated mode headers re-export via `using Curve = ::nisps::Curve;`. Since P4 there is NO TS mirror — the browser samples the WASM catalog (`nisps_curve_apply(+batch)`).
@ -160,10 +161,10 @@ includes; no `nisps-core`.
## Gotchas
- `src/memllib` submodule is not auto-checked-out.
- Firmware sketch path is `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` (Arduino-CLI requires sketch dir name == sketch file name); `firmware/MEMLNaut-NISPS/src/{memllib,nisps}` are symlinks because Arduino's preprocessor refuses `..` in includes from sketch headers.
- Firmware needs PlatformIO: `nix-shell -p platformio-core`. Use `platformio-core`, NOT `platformio` — the latter is nixpkgs' bubblewrap-wrapped FHS build and fails without a working user namespace. First build pulls ~1-2 GB into `~/.platformio`.
- `firmware/MEMLNaut-NISPS/glue/mode_select.hpp` `#undef`s Arduino macros (`sq`, `min`, `max`, `abs`, `round`) before pulling nisps headers — engines use those identifiers as method names.
- `nisps_firmware::g_active_mode_bridge` is `extern` in `glue/audio_driver.hpp` and defined in the `.ino`; combining `inline` with `__not_in_flash` produces a comdat conflict at link time.
- `nisps_firmware::g_active_mode_bridge` is `extern` in `glue/audio_driver.hpp` and defined in `src/main.cpp`; combining `inline` with `__not_in_flash` produces a comdat conflict at link time.
- `pio run`'s own "Flash: NN%" console line double-counts `.data` on this board (PlatformIO's generic size checker counts every PROGBITS+ALLOC section). Compare `arm-none-eabi-size -A` — flash = `.text+.rodata` — before believing a size regression.
- `nisps_modes_tests` builds against generated schemas under `nisps/modes/generated/`; if you add a new mode, regenerate via `bun run codegen/generate.ts` before building.
## Smells / strategic concerns

View file

@ -95,23 +95,32 @@ DESCRIPTIVE — it documents that engine voice spaces already square the value i
- **S26** Schema surface honesty: per-field wire-or-delete pass (default_learning_rate/max_iterations both-platforms-or-neither; input_channels; per-param curves) per the verifier's corrected list.
- **L38** one TS `applyCurve` (mapping.ts's spec-anchored formula survives); **ST4** one GROUP_COLOR source; **L8** extract shared `ratio_seq`/SeqClock/EventQueue for the sequencer engines; **L17** BaseBackend mirroring BaseSource for status/throttle plumbing; **ST12** shared codegen `lib.ts`; **L39** delete the clobbering seed script (git keeps it); **ST13** move `synth-midi-cc.json` under `schemas/midi_devices/`.
## §5 Phase 4 — PlatformIO migration (vision 4) (~23 days) — A6, S2, L12, S9
## §5 Phase 4 — PlatformIO migration (vision 4) — A6, S2, L12, S9
One cut, no dual path. `firmware/useq-celium/` already proves the PIO + arduino-pico pattern in-repo.
**BURNED DOWN 2026-07-21.** One cut, no dual path, as specified.
- `platformio.ini` with one `[env]` per firmware variant passing `-DMEMLNAUT_MODE_TYPE=<alias>`; selftest becomes a plain `-DNISPS_SELFTEST=1` env. Deletes: the sed/python machinery in `firmware-common.sh` that **mutates the committed .ino**, the `.ino` comment-registry, the entire `NISPS_ST_*` token-paste table (already silently missing the currently-active SLPWorkshop variant — L12), the sketch-tree symlink forest, and the global TFT_eSPI library mutation (handled via PIO lib config/build flags instead).
- **memllib consumption decision** (§7.5): **SETTLED — the submodule bump landed; the vendoring copy
is what remains.** See `../recon/memllib-usage-inventory.md`. Result: all 24 compiled TUs link, so
the vendoring surface is ~1.8 MB / 84 files — all of memllib bar `examples/`; there is no small
subset to lift. The operator chose "rebase then vendor", but on inspection **there was no rebase to
do**: all three fork commits touch only `examples/`, which is not in the sketch symlink forest and
is never compiled, and whose content is already ported into `nisps/ml/`. So the fork is dissolved
and the submodule is repointed at upstream, pinned to current `main` — verified by building all
three variants (+316 bytes flash, one `constexpr`→`const` fix in the `.ino` because upstream made
`kSampleRate` runtime-settable). That bump brings the `l r input swap` hardware fix and the
`NavigateToView` the SelfTest variant had already been written against. **Vendor from this
snapshot**, recording the upstream commit so a re-sync stays a documented diff.
- Then **S9**: a CI job compiling 23 representative envs with cached toolchain — firmware enters an automated gate for the first time.
- `firmware/MEMLNaut-NISPS/platformio.ini`: 16 `[env:]`, one per variant, each passing
`-DMEMLNAUT_MODE_TYPE`; `selftest` passes `-DNISPS_SELFTEST=1`. **The env list IS the registry**
the `.ino` comment-registry and the `NISPS_ST_*` token-paste table (L12, already silently missing
the shipped variant) are gone, not migrated.
- Deleted: the Python/sed `.ino`-mutation machinery, the sketch symlink forest, the global TFT_eSPI
`User_Setup.h` mutation (now `-D` flags, TFT_eSPI's own documented recipe), the UF2 boot-mount
detection stack (`upload_protocol = picotool` talks to the bootloader directly), and
`build-firmware-arch.sh` entirely. Scripts 683 → 435 lines.
- memllib **vendored** at `lib/memllib/` from upstream `e291192`; the submodule is gone. Provenance
and re-sync procedure in `VENDORED.md`.
- **S9 done**: a `firmware-build` CI job compiles three representative envs with a cached toolchain
and reports per-variant flash/RAM. Firmware is in an automated gate for the first time.
Verified: **all 16 envs build from an empty cache**, and every one lands within ~520 bytes of the
arduino-cli binary it replaces (flash and RAM), measured as `.text+.rodata` / `.data+.bss+…` rather
than PlatformIO's console line, which double-counts `.data` on this board. What this does NOT prove
is that the hardware boots — no flash+smoke test was possible. That remains an operator chokepoint.
Two traps worth remembering, both recorded in `VENDORED.md` / `platformio.ini`: vendoring memllib's
subdirs without a `src/` wrapper makes PlatformIO's library builder silently compile **nothing**
while still linking; and project `build_flags` land *before* the framework's own `-std=gnu++17 -Os`,
so `build_unflags` is required — appending our own flags is not enough.
## §6 Phase 5 — Vision-facing architecture (each item spec-first, own session)

View file

@ -83,22 +83,36 @@ lowest level. Every mode on the pinned commit sees its stereo input backwards.
## Verified: current upstream builds, and costs almost nothing
Submodule moved to `e291192` (upstream `main`), all three variants built with `arduino-cli`:
Submodule moved to `e291192` (upstream `main`).
| variant | flash | Δ vs pin | RAM | Δ |
|---|---|---|---|---|
| SLPWorkshop | 145348 | +320 | 87388 | +4 |
| PAFSynth | 145300 | +312 | 107060 | +4 |
| SelfTest | 141840 | +320 | 12028 | +4 |
**CORRECTION (2026-07-21, same day).** The first version of this table, and the numbers in commit
`c19d846`'s message, were WRONG — SLPWorkshop 145348, PAFSynth 145300, SelfTest 141840, with a
"+316 uniform delta". They were produced by building three variants in a row through a **shared
incremental `arduino-cli` build directory** (`/tmp/memlnaut-firmware-build`), which reused stale
objects and under-reported by roughly 75 KB. The error surfaced when the PlatformIO migration could
not reproduce them; a clean-cache rebuild of the identical commit in a throwaway worktree confirmed
it. **Never measure firmware size through a reused build directory.**
Measured properly (clean build dir per measurement, SelfTest variant):
| memllib pin | flash | RAM |
|---|---|---|
| `b37fc53` (old pin, pre-bump) | 216520 | 18480 |
| `e291192` (upstream main) | 216736 | 18492 |
| **delta** | **+216** | **+12** |
The conclusion is unchanged and if anything stronger: the 31-commit upstream bump costs ~216 bytes
of a 16 MB flash. The bulky new upstream code (`GrainDelayI16`, `ReverbI16`, `ModFXI16`,
`CCSelectView`, `NameInputView`, `RLView`, `VUMeterView`, `PSRAMManager`) is header-only and
unreferenced, so the linker drops all of it; the delta is the AudioDriver/DisplayDriver changes.
Absolute sizes for every variant, post-bump, are in the Phase 4 migration commit — all 16 build,
and none exceeds 2% of flash.
Exactly **one** compile error had to be fixed: `MEMLNaut-NISPS.ino:169` used `kSampleRate` in a
`constexpr`, and upstream `1997699 "mode sample rate"` made it a runtime `extern size_t` so a mode
can choose its own rate. `constexpr``const`; it is a once-per-second diagnostic print.
The +316-byte uniform delta is the AudioDriver/DisplayDriver changes. The bulky new upstream code
(`GrainDelayI16`, `ReverbI16`, `ModFXI16`, `CCSelectView`, `NameInputView`, `RLView`, `VUMeterView`,
`PSRAMManager`) is header-only and unreferenced, so the linker drops all of it.
## Where this leaves the vendoring
Vendor **from current upstream**, not from the old pin. Take the five linked subdirs +

View file

@ -17,9 +17,9 @@
#include <Arduino.h>
#include "../src/nisps/core/perf.hpp"
#include "../src/nisps/core/types.hpp"
#include "../src/memllib/audio/AudioDriver.hpp"
#include "nisps/core/perf.hpp"
#include "nisps/core/types.hpp"
#include "audio/AudioDriver.hpp"
namespace nisps_firmware {

View file

@ -26,8 +26,8 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include "../src/nisps/modes/base.hpp"
#include "../src/memllib/interface/MIDIInOut.hpp"
#include "nisps/modes/base.hpp"
#include "interface/MIDIInOut.hpp"
namespace nisps_firmware {

View file

@ -1,18 +1,12 @@
// firmware/glue/mode_select.hpp — Compile-time mode selection.
//
// The active firmware mode is chosen at compile time via `MEMLNAUT_MODE_TYPE`
// (the .ino's master macro). The macro expands to one of the canonical mode
// type aliases below; build scripts rewrite the active line.
// The active firmware mode is chosen at compile time via `MEMLNAUT_MODE_TYPE`,
// defined per-variant as a PlatformIO build flag (one `[env:...]` per variant
// in platformio.ini, e.g. `-DMEMLNAUT_MODE_TYPE=MEMLNautModePAFSynth`) — there
// is no in-source registry and no build-time file rewriting.
//
// Each alias maps a short human-readable name (`MEMLNautModePAFSynth`) to a
// concrete `nisps::modes::*Mode` C++ type. The legacy
// `modes/MEMLNautMode*.hpp` wrappers are gone; the same identifier now refers
// to the new platform-agnostic mode type.
//
// This indirection (via using-aliases) lets the build script's mode-rewrite
// logic stay near-identical: it still sees lines of the form
// #define MEMLNAUT_MODE_TYPE MEMLNautModePAFSynth
// and rewrites between alternatives.
// concrete `nisps::modes::*Mode` C++ type.
#pragma once
@ -38,16 +32,16 @@
# undef round
#endif
#include "../src/nisps/modes/breakor.hpp"
#include "../src/nisps/modes/channel_strip.hpp"
#include "../src/nisps/modes/elysiamorf.hpp"
#include "../src/nisps/modes/external_synth_midi.hpp"
#include "../src/nisps/modes/memlcelium.hpp"
#include "../src/nisps/modes/paf_synth.hpp"
#include "../src/nisps/modes/slp_workshop.hpp"
#include "../src/nisps/modes/sound_analysis_midi.hpp"
#include "../src/nisps/modes/verb_fx.hpp"
#include "../src/nisps/modes/xiasri.hpp"
#include "nisps/modes/breakor.hpp"
#include "nisps/modes/channel_strip.hpp"
#include "nisps/modes/elysiamorf.hpp"
#include "nisps/modes/external_synth_midi.hpp"
#include "nisps/modes/memlcelium.hpp"
#include "nisps/modes/paf_synth.hpp"
#include "nisps/modes/slp_workshop.hpp"
#include "nisps/modes/sound_analysis_midi.hpp"
#include "nisps/modes/verb_fx.hpp"
#include "nisps/modes/xiasri.hpp"
// ---- Public name → nisps type aliases ----
// Build script greps for `MEMLNautMode*` lines, so we keep the prefix.
@ -75,32 +69,9 @@ using MEMLNautModeExtSynthAnalogKeys = ::nisps::modes::ExternalSynthMIDIMode<::n
using MEMLNautModeExtSynthHydrasynth = ::nisps::modes::ExternalSynthMIDIMode<::nisps::midi::generated::kAsmHydrasynth, 8u>;
using MEMLNautModeExtSynthJD800 = ::nisps::modes::ExternalSynthMIDIMode<::nisps::midi::generated::kRolandJd800, 8u>;
// ---- SelfTest pseudo-variant ----
// A standalone guided hardware self-test (see glue/selftest.hpp). It is NOT a
// nisps Mode — it drives the display + raw peripherals directly. We expose a
// tiny tag type so the `MEMLNautModeSelfTest` alias type-checks and the build
// script's `MEMLNautMode*` grep discovers the variant; the .ino forks on
// `NISPS_SELFTEST` and never instantiates this tag.
namespace nisps_firmware { namespace selftest { struct SelfTestRig {}; } }
using MEMLNautModeSelfTest = ::nisps_firmware::selftest::SelfTestRig;
// Compile-time guard: NISPS_SELFTEST expands to 1 iff MEMLNAUT_MODE_TYPE is
// MEMLNautModeSelfTest, else 0. Computed in the .ino *after* the mode #define.
// Two-level CAT so MEMLNAUT_MODE_TYPE expands before the paste.
#define NISPS_ST_MEMLNautModePAFSynth 0
#define NISPS_ST_MEMLNautModeChannelStrip 0
#define NISPS_ST_MEMLNautModeXIASRI 0
#define NISPS_ST_MEMLNautModeSoundAnalysisMIDI 0
#define NISPS_ST_MEMLNautModeBreakOr 0
#define NISPS_ST_MEMLNautModeVerbFX 0
#define NISPS_ST_MEMLNautModeElysiamorfs 0
#define NISPS_ST_MEMLNautModeMEMLCelium 0
#define NISPS_ST_MEMLNautModeExtSynthSub37 0
#define NISPS_ST_MEMLNautModeExtSynthSubPhatty 0
#define NISPS_ST_MEMLNautModeExtSynthPro12 0
#define NISPS_ST_MEMLNautModeExtSynthAnalogKeys 0
#define NISPS_ST_MEMLNautModeExtSynthHydrasynth 0
#define NISPS_ST_MEMLNautModeExtSynthJD800 0
#define NISPS_ST_MEMLNautModeSelfTest 1
#define NISPS_ST_CAT_(x) NISPS_ST_##x
#define NISPS_ST_CAT(x) NISPS_ST_CAT_(x)
// ---- SelfTest ----
// The guided hardware self-test (see glue/selftest.hpp) is NOT a nisps Mode —
// it drives the display + raw peripherals directly and is selected by its own
// `selftest` PlatformIO env, which defines `NISPS_SELFTEST=1` and leaves
// `MEMLNAUT_MODE_TYPE` undefined (src/main.cpp's `#if !NISPS_SELFTEST` fork
// never instantiates `ActiveMode` in that env, so no alias is needed here).

View file

@ -46,9 +46,9 @@
#include <cstddef>
#include <cstdint>
#include "../src/nisps/core/perf.hpp"
#include "../src/nisps/ml/feedback.hpp"
#include "../src/memllib/hardware/memlnaut/MEMLNaut.hpp"
#include "nisps/core/perf.hpp"
#include "nisps/ml/feedback.hpp"
#include "hardware/memlnaut/MEMLNaut.hpp"
namespace nisps_firmware {

View file

@ -15,8 +15,9 @@
//
// This logic is INHERENTLY hardware-coupled (it draws to the TFT and reads raw
// pins), so it lives firmware-side here in glue/, NOT under platform-agnostic
// nisps/. It is selected at build time via the `SelfTest` variant (see
// mode_select.hpp + the NISPS_SELFTEST fork in MEMLNaut-NISPS.ino).
// nisps/. It is selected at build time via the `selftest` PlatformIO env (see
// platformio.ini, which defines NISPS_SELFTEST=1; the fork lives in
// src/main.cpp).
//
// Threading: core 0 owns the step state machine (driven from MEMLNaut's
// loopCallback, ~5 ms) and the display. core 1 owns the audio sweep block
@ -48,14 +49,14 @@
#include <cstdio>
#include <functional>
#include "../src/memllib/PicoDefs.hpp"
#include "../src/memllib/utils/perf.hpp"
#include "../src/memllib/audio/AudioDriver.hpp"
#include "../src/memllib/interface/MIDIInOut.hpp"
#include "../src/memllib/hardware/memlnaut/MEMLNaut.hpp"
#include "../src/memllib/hardware/memlnaut/Pins.hpp"
#include "../src/memllib/hardware/memlnaut/display/View.hpp"
#include "../src/nisps/dsp/osc.hpp"
#include "PicoDefs.hpp"
#include "utils/perf.hpp"
#include "audio/AudioDriver.hpp"
#include "interface/MIDIInOut.hpp"
#include "hardware/memlnaut/MEMLNaut.hpp"
#include "hardware/memlnaut/Pins.hpp"
#include "hardware/memlnaut/display/View.hpp"
#include "nisps/dsp/osc.hpp"
// Inter-core boot-handshake flags. Defined in the .ino (shared by both the
// normal-mode and self-test build paths); declared here so this header is

View file

@ -3,7 +3,7 @@
// Adds entries to the MEMLNaut display carousel (the same DisplayDriver the
// SystemView and SelfTest use). Navigation: rotate to move between views;
// press to focus a view; rotate-while-focused to change its value; press
// again to unfocus. See src/memllib/.../display/DisplayDriver.hpp.
// again to unfocus. See lib/memllib/hardware/memlnaut/display/DisplayDriver.hpp.
//
// Currently provides:
// * Joystick: Dual / Single — for the 4-input ("two 2-D joystick") modes,
@ -24,8 +24,8 @@
#include <memory>
#include <span>
#include "../src/memllib/hardware/memlnaut/MEMLNaut.hpp"
#include "../src/memllib/hardware/memlnaut/display/SingleSelectView.hpp"
#include "hardware/memlnaut/MEMLNaut.hpp"
#include "hardware/memlnaut/display/SingleSelectView.hpp"
namespace nisps_firmware {

View file

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -0,0 +1,86 @@
# memllib — vendored, not a submodule
This directory is a **vendored copy** of the load-bearing subset of
[`MusicallyEmbodiedML/memllib`](https://github.com/MusicallyEmbodiedML/memllib), the
hardware-abstraction library for the MEMLNaut board (audio driver, TFT display, MIDI
I/O, peripherals). It replaced the `src/memllib` git submodule during the Phase 4
PlatformIO migration (2026-07-21).
## Provenance
- **Upstream repo**: `https://github.com/MusicallyEmbodiedML/memllib.git`
- **Vendored at commit**: `e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd` (upstream `main`,
"l r input swap")
- **License**: MPL-2.0 (`LICENSE` in this directory, copied verbatim from upstream)
## What was copied, what was dropped
Copied verbatim, directory structure unchanged, under `src/`: `audio/`, `hardware/`,
`interface/`, `synth/`, `utils/`, `PicoDefs.hpp`. `LICENSE` sits at this directory's
root (metadata, not source). These are exactly the subdirectories the firmware sketch
used to reach via its symlink forest (`firmware/MEMLNaut-NISPS/src/memllib` before this
migration) — only the wrapping `src/` folder and the `library.properties` manifest are
new, both required for PlatformIO to discover and recursively compile this tree (see
below).
Dropped: `examples/` (17 files — never compiled; the firmware never referenced it, and
its content that mattered was already ported into `nisps/ml/{jolt,ou_noise,feedback,
geo_push}.hpp` per the pre-Phase-4 submodule-bump decision), `.git` (submodule gitlink),
`.gitignore` (build-artifact patterns, meaningless once vendored — this repo's own
`.gitignore` covers it), `README.md` (described the old Arduino-IDE TFT_eSPI
`User_Setup_Select.h` copy-paste workflow, which PlatformIO replaces with
`-D USER_SETUP_LOADED=1` + explicit build flags in `platformio.ini` — see there).
98 files, ~1.9 MB total — all of memllib bar `examples/`; there is no smaller subset to
lift (every one of the 24 `.cpp` translation units here is reached by at least one
compiled firmware variant).
## Internal include convention (do not break)
Files inside this tree include each other with paths relative to `src/` as the root
(e.g. `src/hardware/memlnaut/MEMLNaut.cpp` does `#include "../PicoDefs.hpp"`,
`src/hardware/memlnaut/display/View.cpp` does `#include "../../PicoDefs.hpp"`).
This directory is consumed as a PlatformIO Arduino-format library (`lib/memllib/`, with
`library.properties` + a `src/` subfolder — the standard 1.5 Arduino library layout).
PlatformIO's Library Dependency Finder therefore adds `lib/memllib/src` (not
`lib/memllib` itself) to the include search path and recursively compiles every source
file under `src/`.
**Do not vendor these five subdirectories directly under `lib/memllib/`** (i.e. without
the `src/` wrapper) — that was tried first and silently compiles nothing: PlatformIO's
`ArduinoLibBuilder`, when it finds no `src/` subfolder, falls back to a *non-recursive*
"files directly in the library root" scan (the historical Arduino 1.0 library format,
which only special-cases a `utility/` subfolder). Nested folders like `audio/` or
`hardware/` are silently invisible to the build under that fallback — it links, or
rather fails to link, with `undefined reference to MEMLNaut::...` for every symbol in
this library. The `src/` subfolder switches PlatformIO onto the recursive path.
Firmware code outside this tree (`../../src/main.cpp`, `../../glue/*.hpp`) includes
headers here relative to `src/` as the root, e.g. `#include "audio/AudioDriver.hpp"`,
`#include "hardware/memlnaut/MEMLNaut.hpp"` — no `memllib/` or `src/` prefix, because
`lib/memllib/src/` *is* the include root PlatformIO adds.
## Re-syncing with upstream
There is no submodule to bump anymore, so a re-sync is a manual, documented diff:
1. Clone upstream at the desired commit: `git clone
https://github.com/MusicallyEmbodiedML/memllib.git /tmp/memllib-upstream`
2. Diff the five subdirs + `PicoDefs.hpp` against this directory's `src/`, e.g.:
```
diff -ru /tmp/memllib-upstream/audio firmware/MEMLNaut-NISPS/lib/memllib/src/audio
# ...repeat for hardware/ interface/ synth/ utils/ PicoDefs.hpp
```
3. Copy over the changed files (`cp -a`), re-run `diff -ru` both ways to confirm nothing
outside the tracked subset leaked in and nothing was silently dropped.
4. Update the "Vendored at commit" line above to the new upstream SHA + its subject
line.
5. Rebuild every `platformio.ini` env (`pio run`) and diff flash/RAM sizes against the
previous vendored commit's numbers — a size jump with no corresponding upstream
feature is a signal something unexpected changed.
6. Commit the vendored-file changes and this doc update together.
If upstream ever restructures these directories (renames, new cross-subdir relative
includes), the internal-include convention above may need re-verification — grep for
`#include "\.\./` inside this tree and confirm every relative path still resolves.

View file

@ -0,0 +1,9 @@
name=memllib
version=0.0.0
author=MusicallyEmbodiedML
maintainer=MusicallyEmbodiedML <https://github.com/MusicallyEmbodiedML/memllib>
sentence=Hardware abstraction for the MEMLNaut board (audio driver, TFT display, MIDI I/O, peripherals).
paragraph=Vendored subset of MusicallyEmbodiedML/memllib at commit e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd. See VENDORED.md in this directory for provenance and the re-sync procedure.
category=Device Control
url=https://github.com/MusicallyEmbodiedML/memllib
architectures=rp2040

View file

@ -0,0 +1,85 @@
#ifndef __MEML_PICO_HPP__
#define __MEML_PICO_HPP__
#include "pico.h"
#include <memory>
#define AUDIO_FUNC(x) __not_in_flash_func(x) ///< Macro to make audio function load from mem
#define AUDIO_MEM __not_in_flash("audio") ///< Macro to make variable load from mem
//#define AUDIO_MEM_2 __not_in_flash("audio2")
#define APP_SRAM __not_in_flash("app")
#define ML_BUFFER_MEM __attribute__((section(".scratch_x")))
#define AUDIO_BUFFER_MEM __attribute__((section(".scratch_y")))
#define PERIODIC_DEBUG(COUNT, FUNC) \
static size_t ct=0; \
if (ct++ % COUNT == 0) { \
FUNC \
}
// Add these macros near other globals
#define MEMORY_BARRIER() __sync_synchronize()
#define WRITE_VOLATILE(var, val) do { MEMORY_BARRIER(); (var) = (val); MEMORY_BARRIER(); } while (0)
#define READ_VOLATILE(var) ({ MEMORY_BARRIER(); typeof(var) __temp = (var); MEMORY_BARRIER(); __temp; })
template<typename T>
inline void write_volatile_struct(volatile T& dest, const T& src) {
MEMORY_BARRIER();
memcpy((void*)&dest, &src, sizeof(T));
MEMORY_BARRIER();
}
#define WRITE_VOLATILE_STRUCT(var, val) write_volatile_struct((var), (val))
// Add this template function after write_volatile_struct:
template<typename T>
inline T read_volatile_struct(const volatile T& src) {
MEMORY_BARRIER();
T temp;
memcpy(&temp, (const void*)&src, sizeof(T));
MEMORY_BARRIER();
return temp;
}
#define READ_VOLATILE_STRUCT(var) read_volatile_struct(var)
//#define ALLOW_DEBUG
#ifdef ALLOW_DEBUG
#define DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__); Serial.flush()
#define DEBUG_PRINT(...) Serial.print(__VA_ARGS__); Serial.flush()
#define DEBUG_PRINTLN(...) Serial.println(__VA_ARGS__); Serial.flush()
#else
#define DEBUG_PRINT(...)
#define DEBUG_PRINTLN(...)
#define DEBUG_PRINTF(...)
#endif
#define PERIODIC_RUN(code, freq_ms) \
{ \
static size_t lastUpdate = 0; \
size_t now = millis(); \
if (now - lastUpdate > (freq_ms)) { \
lastUpdate = now; \
code; \
} \
}
#define PERIODIC_RUN_US(code, freq_us) \
{ \
static size_t lastUpdate = 0; \
size_t now = micros(); \
if (now - lastUpdate > (freq_us)) { \
lastUpdate = now; \
code; \
} \
}
template<typename T>
std::shared_ptr<T> make_non_owning(T& obj) {
return std::shared_ptr<T>(&obj, [](T*){});
}
#endif // __MEML_PICO_HPP__

View file

@ -0,0 +1,33 @@
#include "AnalysisParams.hpp"
#include "pico/mutex.h"
#include "CoreMutex.h"
#include "Arduino.h"
#include "../PicoDefs.hpp"
static mutex_t mutex_0to1_;
static std::vector<float> params_mem_;
void AnalysisParamsSetup(size_t n_params) {
params_mem_.resize(n_params, -1.f);
mutex_init(&mutex_0to1_);
};
void AUDIO_FUNC(AnalysisParamsWrite)(std::vector<float> &params) {
{ // Acquire
CoreMutex acquire_1to0(&mutex_0to1_);
for (unsigned int n=0; n < params_mem_.size(); n++) {
if (n >= params.size()) {
DEBUG_PRINTLN("PANIK! Too many params for AnalysisParams");
}
params_mem_[n] = params[n];
}
} // Release
}
void AnalysisParamsRead(std::vector<float> &params) {
{ // Acquire
CoreMutex acquire_1to0(&mutex_0to1_);
params = params_mem_;
} // Release
}

View file

@ -0,0 +1,12 @@
#ifndef __ANALYSIS_PARAMS_HPP__
#define __ANALYSIS_PARAMS_HPP__
#include <stddef.h>
#include <vector>
void AnalysisParamsSetup(size_t n_params);
void AnalysisParamsWrite(std::vector<float> &params);
void AnalysisParamsRead(std::vector<float> &params);
#endif // __ANALYSIS_PARAMS_HPP__

View file

@ -0,0 +1,67 @@
#ifndef __AUDIO_APP_BASE_HPP__
#define __AUDIO_APP_BASE_HPP__
#include "AudioDriver.hpp"
#include "../interface/InterfaceBase.hpp"
#include <functional>
#include <memory>
template<size_t NPARAMS>
class AudioAppBase {
protected:
float sample_rate_;
std::shared_ptr<InterfaceBase> interface_;
static std::function<stereosample_t(stereosample_t)> callback_;
std::array<float, NPARAMS> paramsFromQueue;
public:
virtual AudioDriver::codec_config_t GetDriverConfig() const {
return {
.mic_input = false,
.line_level = 3,
.mic_gain_dB = 0,
.output_volume = 0.55f
};
}
AudioAppBase() = default;
virtual ~AudioAppBase() = default;
virtual stereosample_t Process(const stereosample_t x) {
return x;
}
static stereosample_t audioCallback(const stereosample_t x) {
return callback_(x);
}
virtual void Setup(float sample_rate, std::shared_ptr<InterfaceBase> interface) {
sample_rate_ = sample_rate;
interface_ = interface;
callback_ = [this](stereosample_t x) { return Process(x); };
AudioDriver::SetCallback(audioCallback);
}
virtual void ProcessParams(const std::array<float, NPARAMS>& params) {
// Default implementation does nothing
}
virtual void loop() {
if (!interface_) {
DEBUG_PRINTLN("AudioAppBase::loop - Error: Interface is null");
return; // Early return if interface is null
}
// std::vector<float> x;
if (interface_->ReceiveParamsFromQueue(paramsFromQueue.data())) {
ProcessParams(paramsFromQueue);
}
}
};
template<size_t NPARAMS>
std::function<stereosample_t(stereosample_t)> AudioAppBase<NPARAMS>::callback_ = nullptr;
#endif // __AUDIO_APP_BASE_HPP__

View file

@ -0,0 +1,48 @@
/* Audio Library for Teensy 3.X
* Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
*
* Development of this audio library was funded by PJRC.COM, LLC by sales of
* Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop
* open source software by purchasing Teensy or other PJRC products.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice, development funding notice, and this permission
* notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef AudioControl_h_
#define AudioControl_h_
#include <stdint.h>
// A base class for all Codecs, DACs and ADCs, so at least the
// most basic functionality is consistent.
#define AUDIO_INPUT_LINEIN 0
#define AUDIO_INPUT_MIC 1
class AudioControl
{
public:
virtual bool enable(void) = 0;
virtual bool disable(void) = 0;
virtual bool volume(float volume) = 0; // volume 0.0 to 1.0
virtual bool inputLevel(float volume) = 0; // volume 0.0 to 1.0
virtual bool inputSelect(int n) = 0;
};
#endif

View file

@ -0,0 +1,298 @@
#include "AudioDriver.hpp"
#include <Arduino.h>
#include "Wire.h"
#include "control_sgtl5000.h"
#include "../synth/maximilian.h"
#include "hardware/dma.h"
#include "hardware/clocks.h"
#include "../PicoDefs.hpp"
#include "../utils/perf.hpp"
#define TEST_TONES 0
#define PASSTHROUGH 0
extern "C" {
size_t kSampleRate = 48000;
float kSampleRateRcpr = 1.0f / 48000.0f;
}
PERF_DECLARE(AUDIOLOOP);
AUDIO_MEM uint32_t AUDIOLOOP_MEAN=0;
int sampleRate = (int)kSampleRate;
constexpr int bitsPerSample = 32;
constexpr float amplitude = 1 << (bitsPerSample - 2); // amplitude of square wave = 1/2 of maximum
constexpr float neg_amplitude = -amplitude; // amplitude of square wave = 1/2 of maximum
constexpr float one_over_amplitude = 1.f / amplitude;
static int32_t AUDIO_MEM sample = amplitude; // current sample value
static AUDIO_MEM AudioControlSGTL5000 codecCtl;
audiocallback_fptr_t AUDIO_MEM audio_callback_ = nullptr;
audiocallback_block_fptr_t AUDIO_MEM audio_callback_block_ = nullptr;
//define AUDIO_BUFFER_MEM externally
static AUDIO_BUFFER_MEM float input_buffer[kNChannels][kBufferSize];
static AUDIO_BUFFER_MEM float output_buffer[kNChannels][kBufferSize];
static __attribute__((aligned(8))) AUDIO_MEM pio_i2s i2s;
volatile bool AUDIO_MEM dsp_overload;
float master_volume_ = 0;
#if TEST_TONES
maxiOsc osc, osc2;
float f1=20, f2=2000;
#endif // TEST_TONES
inline float __attribute__((always_inline)) _scale_down(float x) {
// Convert from int32 range to [-1.0, 1.0]
return x * (1.0f / (float)(1LL << 31)); // Divide by 2^31 for proper scaling
}
inline float __attribute__((always_inline)) _scale_and_saturate(float x) {
// Convert from [-1.0, 1.0] back to int32 range with saturation
const float scaled = x * (float)(1LL << 31);
if (scaled > INT32_MAX) return INT32_MAX;
if (scaled < INT32_MIN) return INT32_MIN;
return scaled;
}
#if TEST_TONES
static inline __attribute__((always_inline)) void AUDIO_FUNC(process_test_tones)(
int32_t* output, size_t i) {
output[i*2] = osc.sinewave(f1) * sample;
output[(i*2) + 1] = osc2.sinewave(f2) * sample;
f1 *= 1.00001;
f2 *= 1.00001;
if (f1 > 15000) {
f1 = 20.0;
}
if (f2 > 15000) {
f2 = 20.0;
}
}
#endif
#if PASSTHROUGH
static inline __attribute__((always_inline)) void AUDIO_FUNC(process_passthrough)(
const int32_t* input, int32_t* output, size_t i) {
output[i*2] = input[i*2];
output[(i*2) + 1] = input[(i*2) + 1];
}
#endif
static inline __attribute__((always_inline)) void AUDIO_FUNC(process_normal)(
const int32_t* input, int32_t* output, size_t i,
const size_t indexL, const size_t indexR) {
stereosample_t y {
_scale_down(static_cast<float>(input[indexL])),
_scale_down(static_cast<float>(input[indexR]))
};
y = audio_callback_(y); // y should now be in [-1.0, 1.0] range
output[indexL] = static_cast<int32_t>(_scale_and_saturate(y.L * master_volume_));
output[indexR] = static_cast<int32_t>(_scale_and_saturate(y.R * master_volume_));
}
static void AUDIO_FUNC(process_audio)(const int32_t* input, int32_t* output, size_t num_frames) {
PERF_BEGIN(AUDIOLOOP);
if (audio_callback_block_ != nullptr) {
// Convert from interleaved int32_t to deinterleaved float.
// Channels are swapped here to correct a hardware layout issue: the physical
// L/R input sockets map to the opposite codec ADC channels. Swapping at this
// lowest level means every mode sees x.L/x.R matching the labelled sockets.
for (size_t i = 0; i < num_frames; i++) {
const size_t indexL = i << 1;
const size_t indexR = indexL + 1;
input_buffer[0][i] = _scale_down(static_cast<float>(input[indexR]));
input_buffer[1][i] = _scale_down(static_cast<float>(input[indexL]));
}
// Serial.println(input_buffer[0][0]);
// Serial.println(input[0]);
// Call block callback
audio_callback_block_(input_buffer, output_buffer, kNChannels, num_frames);
// Convert from deinterleaved float to interleaved int32_t
for (size_t i = 0; i < num_frames; i++) {
const size_t indexL = i << 1;
const size_t indexR = indexL + 1;
// TODO find way to perform only one for loop
output[indexL] = static_cast<int32_t>(_scale_and_saturate(output_buffer[0][i] * master_volume_));
output[indexR] = static_cast<int32_t>(_scale_and_saturate(output_buffer[1][i] * master_volume_));
}
} else {
for (size_t i = 0; i < num_frames; i++) {
const size_t indexL = i << 1;
const size_t indexR = indexL + 1;
#if TEST_TONES
process_test_tones(output, i);
#else
#if PASSTHROUGH
process_passthrough(input, output, i);
#else
process_normal(input, output, i, indexL, indexR);
#endif
#endif
}
}
PERF_END(AUDIOLOOP);
AUDIOLOOP_MEAN = PERF_GET_MEAN(AUDIOLOOP);
}
static void __isr dma_i2s_in_handler(void) {
/* We're double buffering using chained TCBs. By checking which buffer the
* DMA is currently reading from, we can identify which buffer it has just
* finished reading (the completion of which has triggered this interrupt).
*/
if (*(int32_t**)dma_hw->ch[i2s.dma_ch_in_ctrl].read_addr == i2s.input_buffer) {
// It is inputting to the second buffer so we can overwrite the first
process_audio(i2s.input_buffer, i2s.output_buffer, AUDIO_BUFFER_FRAMES);
} else {
// It is currently inputting the first buffer, so we write to the second
process_audio(&i2s.input_buffer[STEREO_BUFFER_SIZE], &i2s.output_buffer[STEREO_BUFFER_SIZE], AUDIO_BUFFER_FRAMES);
}
dma_hw->ints0 = 1u << i2s.dma_ch_in_data; // clear the IRQ
}
void __isr AudioDriver::i2sOutputCallback() {
// for(size_t i=0; i < kBufferSize; i++) {
// stereosample_t y { 0 };
// y = audio_callback_(y);
// stereosample_t y_scaled {
// _scale_and_saturate(y.L),
// _scale_and_saturate(y.R),
// };
// i2s.write32(static_cast<int32_t>(y_scaled.L), static_cast<int32_t>(y_scaled.R));
// }
// Timing end
// auto elapsed = micros() - ts;
// static constexpr float quantumLength = 1.f/
// ((static_cast<float>(kBufferSize)/static_cast<float>(kSampleRate))
// * 1000000.f);
// float dspload = elapsed * quantumLength;
// // Report DSP overload if needed
// static volatile bool dsp_overload = false;
// if (dspload > 0.95 and !dsp_overload) {
// dsp_overload = true;
// } else if (dspload < 0.9) {
// dsp_overload = false;
// }
}
bool AudioDriver::Setup(const codec_config_t &config) {
if (nullptr == audio_callback_) {
audio_callback_ = &silence_;
}
DEBUG_PRINTF("AUDIO- Setup - Audio callback address: 0x%x\n", audio_callback_);
dsp_overload = false;
master_volume_ = 0;
// Zero out float buffers
for (size_t ch = 0; ch < kNChannels; ch++) {
for (size_t i = 0; i < kBufferSize; i++) {
input_buffer[ch][i] = 0;
output_buffer[ch][i] = 0;
}
}
maxiSettings::setup(kSampleRate, 2, kBufferSize);
if (!Wire.setSDA(i2c_sgt5000Data) ||
!Wire.setSCL(i2c_sgt5000Clk)) {
DEBUG_PRINTLN("AUDIO- Failed to setup I2C with codec!");
}
// set_sys_clock_khz(132000*2, true);
// set_sys_clock_khz(129600, true);
DEBUG_PRINTF("System Clock: %lu\n", clock_get_hz(clk_sys));
size_t sys_clk_hz = clock_get_hz(clk_sys);
if (sys_clk_hz != AudioDriver::GetSysClockSpeed() * 1000) {
DEBUG_PRINTLN("Error: audio driver: system clock must be set externally (see ::GetDesiredClockSpeed)");
DEBUG_PRINTLN("After 'setup()) {', add: 'set_sys_clock_khz(AudioDriver::GetSysClockSpeed(), true);'");
}
i2s_config picoI2SConfig {
kSampleRate, // 48000,
256,
bitsPerSample, // 32,
i2s_pMCLK, // 10,
i2s_pDIN, // 6,
i2s_pDOUT, // 7,
i2s_pBCLK, // 8,
true
};
i2s_program_start_synched(pio0, &picoI2SConfig, dma_i2s_in_handler, &i2s);
setDACVolume(3.0f);
// init i2c
codecCtl.enable();
DEBUG_PRINTLN("AUDIO - Codec enabled");
DEBUG_PRINTF("config.output_volume = %f\n", config.output_volume);
codecCtl.volume(config.output_volume > 0.99 ? 0.99 : config.output_volume);
DEBUG_PRINTF("config.mic_input = %d\n", config.mic_input);
codecCtl.inputSelect(config.mic_input ? AUDIO_INPUT_MIC : AUDIO_INPUT_LINEIN);
DEBUG_PRINTF("config.line_level = %d\n", config.line_level);
codecCtl.lineInLevel(config.line_level);
DEBUG_PRINTF("config.mic_gain_dB = %d\n", config.mic_gain_dB);
if (config.mic_input) {
codecCtl.micGain(config.mic_gain_dB);
}
codecCtl.lineOutLevel(20);
return true;
}
bool AudioDriver::Setup() {
codec_config_t config;
config.mic_input = false;
config.line_level = 3;
config.mic_gain_dB = 0;
config.output_volume = 0.8;
return Setup(config);
}
stereosample_t AudioDriver::silence_(stereosample_t x) {
x.L = 0;
x.R = 0;
return x;
}
//breaking change
void AudioDriver::setDACVolume(float n) {
codecCtl.dacVolume(n);
}
void AudioDriver::SetSampleRate(size_t rate) {
kSampleRate = rate;
kSampleRateRcpr = 1.0f / (float)rate;
}

View file

@ -0,0 +1,132 @@
#ifndef __AUDIO_DRIVER_HPP__
#define __AUDIO_DRIVER_HPP__
#include <Arduino.h>
#include <stddef.h>
#include "../PicoDefs.hpp"
#include "i2s_pio/i2s.h"
extern "C" {
const size_t kBufferSize = AUDIO_BUFFER_FRAMES;
extern size_t kSampleRate;
extern float kSampleRateRcpr;
const size_t kNChannels = 2;
struct stereosample_t {
float L;
float R;
__force_inline stereosample_t operator+(const stereosample_t& other) const {
return {L + other.L, R + other.R};
}
__force_inline stereosample_t& operator+=(const stereosample_t& other) {
L += other.L;
R += other.R;
return *this;
}
__force_inline stereosample_t operator*(float scalar) const {
return {L * scalar, R * scalar};
}
__force_inline stereosample_t& operator*=(float scalar) {
L *= scalar;
R *= scalar;
return *this;
}
__force_inline stereosample_t operator-() const {
return {-L, -R};
}
__force_inline stereosample_t operator-(const stereosample_t& other) const {
return {L - other.L, R - other.R};
}
__force_inline stereosample_t& operator-=(const stereosample_t& other) {
L -= other.L;
R -= other.R;
return *this;
}
__force_inline float operator[](size_t index) const {
return index == 0 ? L : R;
}
};
using audiocallback_fptr_t = stereosample_t (*)(stereosample_t);
using audiocallback_block_fptr_t = void (*)(float[][kBufferSize], float[][kBufferSize], size_t, size_t);
}
extern audiocallback_fptr_t audio_callback_;
extern audiocallback_block_fptr_t audio_callback_block_;
extern uint32_t AUDIOLOOP_MEAN;
enum PinConfig_i2c {
i2c_sgt5000Data = 0,
i2c_sgt5000Clk = 1,
i2s_pDIN = 6,
i2s_pDOUT = 7,
i2s_pBCLK = 8,
i2s_pWS = 9,
i2s_pMCLK = 10,
};
extern volatile bool AUDIO_MEM dsp_overload;
extern float master_volume_;
class AudioDriver {
public:
typedef struct {
bool mic_input;
size_t line_level;
size_t mic_gain_dB;
float output_volume;
} codec_config_t;
static bool Setup();
static bool Setup(const codec_config_t& config);
static inline void SetCallback(audiocallback_fptr_t callback) {
audio_callback_ = callback;
DEBUG_PRINT("AUDIO_DRIVER - Callback address: ");
DEBUG_PRINTF("%p\n", audio_callback_);
}
static inline void SetBlockCallback(audiocallback_block_fptr_t callback) {
audio_callback_block_ = callback;
DEBUG_PRINT("AUDIO_DRIVER - Block Callback address: ");
DEBUG_PRINTF("%p\n", audio_callback_block_);
}
static inline void SetMasterVolume(float volume) {
if (volume > 1.0f) {
volume = 1.0f;
} else if (volume < 0) {
volume = 0;
}
master_volume_ = volume;
}
static void SetSampleRate(size_t rate);
static inline size_t GetSampleRate() { return kSampleRate; }
static size_t GetSysClockSpeed() {
if (kSampleRate == 48000) {
return 132000 * 2;
} else if (kSampleRate == 44100) {
return 135475 * 2;
} else if (kSampleRate == 32000) {
return 132000 * 2;
} else if (kSampleRate == 24000) {
return 132000 * 2;
} else {
panic("Unsupported sample rate for SGTL5000");
}
}
AudioDriver() = delete;
static void i2sOutputCallback(void);
static stereosample_t silence_(stereosample_t);
private:
static void setDACVolume(float n);
};
#endif // __AUDIO_DRIVER_HPP__

View file

@ -0,0 +1,71 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <WString.h>
// FocusManager — selective parameter latching for the Focus system.
//
// Interaction model:
// selectedMask == 0 → all params live (default, no-op)
// selectedMask != 0 → a param is live if any of its groups is in selectedMask,
// otherwise it uses its latch buffer value.
//
// Latch snapshot: setFocus() only captures params transitioning live→latched,
// so the freeze point is the exact moment a group leaves the selection.
//
// Multi-group membership: assign (kGroupA | kGroupB) to a param — it stays
// live if either group is selected.
template<size_t NPARAMS, size_t NGROUPS>
class FocusManager {
public:
std::array<String, NGROUPS> groupNames = {};
std::array<uint32_t, NPARAMS> paramGroupMask = {};
FocusManager() {
latchBuffer.fill(0.f);
}
void setGroupName(size_t groupIdx, const String& name) {
if (groupIdx < NGROUPS) groupNames[groupIdx] = name;
}
void setParamGroups(const std::array<uint32_t, NPARAMS>& masks) {
paramGroupMask = masks;
}
// Change focus selection. Snapshots only params transitioning live→latched.
void setFocus(uint32_t newMask, const std::vector<float>& live) {
for (size_t i = 0; i < NPARAMS && i < live.size(); i++) {
const bool wasLive = (selectedMask == 0) || ((paramGroupMask[i] & selectedMask) != 0);
const bool willBeLive = (newMask == 0) || ((paramGroupMask[i] & newMask) != 0);
if (wasLive && !willBeLive) {
latchBuffer[i] = live[i];
}
}
selectedMask = newMask;
}
// Apply focus filter in place. No-op when selectedMask == 0.
void applyInPlace(std::vector<float>& params) const {
if (selectedMask == 0) return;
for (size_t i = 0; i < NPARAMS && i < params.size(); i++) {
if ((paramGroupMask[i] & selectedMask) == 0) {
params[i] = latchBuffer[i];
}
}
}
uint32_t getSelectedMask() const { return selectedMask; }
bool isGroupSelected(size_t groupIdx) const {
return (selectedMask & (1u << groupIdx)) != 0;
}
private:
std::array<float, NPARAMS> latchBuffer = {};
uint32_t selectedMask = 0;
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,145 @@
/* Audio Library for Teensy 3.X
* Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
*
* Development of this audio library was funded by PJRC.COM, LLC by sales of
* Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop
* open source software by purchasing Teensy or other PJRC products.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice, development funding notice, and this permission
* notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef control_sgtl5000_h_
#define control_sgtl5000_h_
// #include <AudioStream.h> // github.com/PaulStoffregen/cores/blob/master/teensy4/AudioStream.h
#include "AudioControl.h"
// SGTL5000-specific defines for headphones
#define AUDIO_HEADPHONE_DAC 0
#define AUDIO_HEADPHONE_LINEIN 1
#define AUDIO_SAMPLE_RATE_EXACT 48000
class AudioControlSGTL5000 : public AudioControl
{
public:
AudioControlSGTL5000(void) : i2c_addr(0x0A) { }
void setAddress(uint8_t level);
bool enable(void);//For Teensy LC the SGTL acts as master, for all other Teensys as slave.
bool enable(const unsigned extMCLK, const uint32_t pllFreq = (4096.0l * AUDIO_SAMPLE_RATE_EXACT) ); //With extMCLK > 0, the SGTL acts as Master
bool disable(void) { return false; }
bool volume(float n) { return volumeInteger(n * 129 + 0.499f); }
bool inputLevel(float n) {return false;}
bool muteHeadphone(void) { return write(0x0024, ana_ctrl | (1<<4)); }
bool unmuteHeadphone(void) { return write(0x0024, ana_ctrl & ~(1<<4)); }
bool muteLineout(void) { return write(0x0024, ana_ctrl | (1<<8)); }
bool unmuteLineout(void) { return write(0x0024, ana_ctrl & ~(1<<8)); }
bool inputSelect(int n) {
if (n == AUDIO_INPUT_LINEIN) {
return write(0x0020, 0x055) // +7.5dB gain (1.3Vp-p full scale)
&& write(0x0024, ana_ctrl | (1<<2)); // enable linein
} else if (n == AUDIO_INPUT_MIC) {
return write(0x002A, 0x0173) // mic preamp gain = +40dB
&& write(0x0020, 0x088) // input gain +12dB (is this enough?)
&& write(0x0024, ana_ctrl & ~(1<<2)); // enable mic
} else {
return false;
}
}
bool headphoneSelect(int n) {
if (n == AUDIO_HEADPHONE_DAC) {
return write(0x0024, ana_ctrl | (1<<6)); // route DAC to headphones out
} else if (n == AUDIO_HEADPHONE_LINEIN) {
return write(0x0024, ana_ctrl & ~(1<<6)); // route linein to headphones out
} else {
return false;
}
}
bool volume(float left, float right);
bool micGain(unsigned int dB);
bool lineInLevel(uint8_t n) { return lineInLevel(n, n); }
bool lineInLevel(uint8_t left, uint8_t right);
unsigned short lineOutLevel(uint8_t n);
unsigned short lineOutLevel(uint8_t left, uint8_t right);
unsigned short dacVolume(float n);
unsigned short dacVolume(float left, float right);
bool dacVolumeRamp();
bool dacVolumeRampLinear();
bool dacVolumeRampDisable();
unsigned short adcHighPassFilterEnable(void);
unsigned short adcHighPassFilterFreeze(void);
unsigned short adcHighPassFilterDisable(void);
unsigned short audioPreProcessorEnable(void);
unsigned short audioPostProcessorEnable(void);
unsigned short audioProcessorDisable(void);
unsigned short eqFilterCount(uint8_t n);
unsigned short eqSelect(uint8_t n);
unsigned short eqBand(uint8_t bandNum, float n);
void eqBands(float bass, float mid_bass, float midrange, float mid_treble, float treble);
void eqBands(float bass, float treble);
void eqFilter(uint8_t filterNum, int *filterParameters);
unsigned short autoVolumeControl(uint8_t maxGain, uint8_t lbiResponse, uint8_t hardLimit, float threshold, float attack, float decay);
unsigned short autoVolumeEnable(void);
unsigned short autoVolumeDisable(void);
unsigned short enhanceBass(float lr_lev, float bass_lev);
unsigned short enhanceBass(float lr_lev, float bass_lev, uint8_t hpf_bypass, uint8_t cutoff);
unsigned short enhanceBassEnable(void);
unsigned short enhanceBassDisable(void);
unsigned short surroundSound(uint8_t width);
unsigned short surroundSound(uint8_t width, uint8_t select);
unsigned short surroundSoundEnable(void);
unsigned short surroundSoundDisable(void);
void killAutomation(void) { semi_automated=false; }
void setMasterMode(uint32_t freqMCLK_in);
protected:
bool muted;
bool volumeInteger(unsigned int n); // range: 0x00 to 0x80
uint16_t ana_ctrl;
uint8_t i2c_addr;
unsigned char calcVol(float n, unsigned char range);
unsigned int read(unsigned int reg);
bool write(unsigned int reg, unsigned int val);
unsigned int modify(unsigned int reg, unsigned int val, unsigned int iMask);
unsigned short dap_audio_eq_band(uint8_t bandNum, float n);
private:
bool semi_automated;
void automate(uint8_t dap, uint8_t eq);
void automate(uint8_t dap, uint8_t eq, uint8_t filterCount);
};
//For Filter Type: 0 = LPF, 1 = HPF, 2 = BPF, 3 = NOTCH, 4 = PeakingEQ, 5 = LowShelf, 6 = HighShelf
#define FILTER_LOPASS 0
#define FILTER_HIPASS 1
#define FILTER_BANDPASS 2
#define FILTER_NOTCH 3
#define FILTER_PARAEQ 4
#define FILTER_LOSHELF 5
#define FILTER_HISHELF 6
//For frequency adjustment
#define FLAT_FREQUENCY 0
#define PARAMETRIC_EQUALIZER 1
#define TONE_CONTROLS 2
#define GRAPHIC_EQUALIZER 3
void calcBiquad(uint8_t filtertype, float fC, float dB_Gain, float Q, uint32_t quantization_unit, uint32_t fS, int *coef);
#endif

View file

@ -0,0 +1,236 @@
/* i2s.c
*
* Author: Daniel Collins
* Date: 2022-02-25
*
* Copyright (c) 2022 Daniel Collins
*
* This file is part of rp2040_i2s_example.
*
* rp2040_i2s_example is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3 as published by the
* Free Software Foundation.
*
* rp2040_i2s_example 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
* rp2040_i2s_example. If not, see <https://www.gnu.org/licenses/>.
*/
#include "i2s.h"
#include <math.h>
#include "hardware/clocks.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "i2s_pio_lib.h"
#include <Arduino.h>
// const i2s_config i2s_config_default = {48000, 256, 32, 10, 6, 7, 8, true};
static float pio_div(float freq, uint16_t* div, uint8_t* frac) {
float clk = (float)clock_get_hz(clk_sys);
float ratio = clk / freq;
float d;
float f = modff(ratio, &d);
*div = (uint16_t)d;
*frac = (uint8_t)(f * 256.0f);
// Use post-converted values to get actual freq after any rounding
float result = clk / ((float)*div + ((float)*frac / 256.0f));
return result;
}
static void calc_clocks(const i2s_config* config, pio_i2s_clocks* clocks) {
// Try to get a precise ratio between SCK and BCK regardless of how
// perfect the system_clock divides. First, see what sck we can actually get:
float sck_desired = (float)config->fs * (float)config->sck_mult * (float)i2s_sck_program_pio_mult;
float sck_attained = pio_div(sck_desired, &clocks->sck_d, &clocks->sck_f);
clocks->fs_attained = sck_attained / (float)config->sck_mult / (float)i2s_sck_program_pio_mult;
// Now that we have the closest fs our dividers will give us, we can
// re-calculate SCK and BCK as correct ratios of this adjusted fs:
float sck_hz = clocks->fs_attained * (float)config->sck_mult;
clocks->sck_pio_hz = pio_div(sck_hz * (float)i2s_sck_program_pio_mult, &clocks->sck_d, &clocks->sck_f);
float bck_hz = clocks->fs_attained * (float)config->bit_depth * 2.0f;
clocks->bck_pio_hz = pio_div(bck_hz * (float)i2s_out_master_program_pio_mult, &clocks->bck_d, &clocks->bck_f);
}
static bool validate_sck_bck_sync(pio_i2s_clocks* clocks) {
float ratio = clocks->sck_pio_hz / clocks->bck_pio_hz;
float actual_sck = clocks->sck_pio_hz / (float)i2s_sck_program_pio_mult;
float actual_bck = clocks->bck_pio_hz / (float)i2s_out_master_program_pio_mult;
DEBUG_PRINTF("Clock speed for SCK: %f (PIO %f Hz with divider %d.%d)\n", actual_sck, clocks->sck_pio_hz, clocks->sck_d, clocks->sck_f);
DEBUG_PRINTF("Clock speed for BCK: %f (PIO %f Hz with divider %d.%d)\n", actual_bck, clocks->bck_pio_hz, clocks->bck_d, clocks->bck_f);
DEBUG_PRINTF("Clock Ratio: %f\n", ratio);
float whole_ratio;
float fractional_ratio = modff(ratio, &whole_ratio);
return (fractional_ratio == 0.0f);
}
static void dma_double_buffer_init(pio_i2s* i2s, void (*dma_handler)(void)) {
// Set up DMA for PIO I2s - two channels, in and out
i2s->dma_ch_in_ctrl = dma_claim_unused_channel(true);
i2s->dma_ch_out_ctrl = dma_claim_unused_channel(true);
i2s->dma_ch_out_data = dma_claim_unused_channel(true);
i2s->dma_ch_in_data = dma_claim_unused_channel(true);
// Control blocks support double-buffering with interrupts on buffer change
i2s->in_ctrl_blocks[0] = i2s->input_buffer;
i2s->in_ctrl_blocks[1] = &i2s->input_buffer[STEREO_BUFFER_SIZE];
i2s->out_ctrl_blocks[0] = i2s->output_buffer;
i2s->out_ctrl_blocks[1] = &i2s->output_buffer[STEREO_BUFFER_SIZE];
// DMA I2S OUT control channel - wrap read address every 8 bytes (2 words)
// Transfer 1 word at a time, to the out channel read address and trigger.
dma_channel_config c = dma_channel_get_default_config(i2s->dma_ch_out_ctrl);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
channel_config_set_ring(&c, false, 3);
channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
dma_channel_configure(i2s->dma_ch_out_ctrl, &c, &dma_hw->ch[i2s->dma_ch_out_data].al3_read_addr_trig, i2s->out_ctrl_blocks, 1, false);
c = dma_channel_get_default_config(i2s->dma_ch_out_data);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
channel_config_set_chain_to(&c, i2s->dma_ch_out_ctrl);
channel_config_set_dreq(&c, pio_get_dreq(i2s->pio, i2s->sm_dout, true));
dma_channel_configure(i2s->dma_ch_out_data,
&c,
&i2s->pio->txf[i2s->sm_dout], // Destination pointer
NULL, // Source pointer, will be set by ctrl channel
STEREO_BUFFER_SIZE, // Number of transfers
false // Start immediately
);
c = dma_channel_get_default_config(i2s->dma_ch_in_ctrl);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
channel_config_set_ring(&c, false, 3);
channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
dma_channel_configure(i2s->dma_ch_in_ctrl, &c, &dma_hw->ch[i2s->dma_ch_in_data].al2_write_addr_trig, i2s->in_ctrl_blocks, 1, false);
c = dma_channel_get_default_config(i2s->dma_ch_in_data);
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_chain_to(&c, i2s->dma_ch_in_ctrl);
channel_config_set_dreq(&c, pio_get_dreq(i2s->pio, i2s->sm_din, false));
dma_channel_configure(i2s->dma_ch_in_data,
&c,
NULL, // Will be set by ctrl chan
&i2s->pio->rxf[i2s->sm_din], // Source pointer
STEREO_BUFFER_SIZE, // Number of transfers
false // Don't start yet
);
// Input channel triggers the DMA interrupt handler, hopefully these stay
// in perfect sync with the output.
dma_channel_set_irq0_enabled(i2s->dma_ch_in_data, true);
irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
irq_set_enabled(DMA_IRQ_0, true);
// Enable all the dma channels
dma_channel_start(i2s->dma_ch_out_ctrl); // This will trigger-start the out chan
dma_channel_start(i2s->dma_ch_in_ctrl); // This will trigger-start the in chan
}
/* Initializes an I2S block (of 3 state machines) on the designated PIO.
* NOTE! This does NOT START the PIO units. You must call i2s_program_start
* with the resulting i2s object!
*/
static void i2s_slave_program_init(PIO pio, const i2s_config* config, pio_i2s* i2s) {
DEBUG_PRINTLN("Slave init");
uint offset = 0;
i2s->pio = pio;
i2s->sm_mask = 0;
pio_i2s_clocks clocks;
calc_clocks(config, &clocks);
if (config->sck_enable) {
// SCK block
i2s->sm_sck = pio_claim_unused_sm(pio, true);
i2s->sm_mask |= (1u << i2s->sm_sck);
offset = pio_add_program(pio, &i2s_sck_program);
i2s_sck_program_init(pio, i2s->sm_sck, offset, config->sck_pin);
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_sck, clocks.sck_d, clocks.sck_f);
}
// Bi-Di I2S block, clocked with SCK
i2s->sm_din = pio_claim_unused_sm(pio, true);
i2s->sm_dout = i2s->sm_din;
i2s->sm_mask |= (1u << i2s->sm_din);
offset = pio_add_program(pio, &i2s_bidi_slave_program);
i2s_bidi_slave_program_init(pio, i2s->sm_din, offset, config->dout_pin, config->din_pin);
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_din, clocks.sck_d, clocks.sck_f);
}
/* Initializes an I2S block (of 3 state machines) on the designated PIO.
* NOTE! This does NOT START the PIO units. You must call i2s_program_start
* with the resulting i2s object!
*/
static void i2s_sync_program_init(PIO pio, const i2s_config* config, pio_i2s* i2s) {
uint offset = 0;
i2s->pio = pio;
i2s->sm_mask = 0;
pio_i2s_clocks clocks;
calc_clocks(config, &clocks);
if (config->sck_enable) {
// Check that SCK and BCK are in perfect whole ratio
if (!validate_sck_bck_sync(&clocks)) {
/* There are lots of possible causes for this, a few are:
* - You are running a system clock frequency that doesn't divide well at all into SCK or BCK
* - You are running a 24-bit I2S with a 256x SCK multiplier (RP2040 cannot support this)
* - You have mucked with the PIO ratios or done something silly.
*/
DEBUG_PRINTLN("SCK and BCK are not in sync.");
}
// SCK block
i2s->sm_sck = pio_claim_unused_sm(pio, true);
i2s->sm_mask |= (1u << i2s->sm_sck);
offset = pio_add_program(pio, &i2s_sck_program);
i2s_sck_program_init(pio, i2s->sm_sck, offset, config->sck_pin);
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_sck, clocks.sck_d, clocks.sck_f);
}
// In block, clocked with SCK
i2s->sm_din = pio_claim_unused_sm(pio, true);
i2s->sm_mask |= (1u << i2s->sm_din);
offset = pio_add_program(pio, &i2s_in_slave_program);
i2s_in_slave_program_init(pio, i2s->sm_din, offset, config->din_pin);
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_din, clocks.sck_d, clocks.sck_f);
// Out block, clocked with BCK
i2s->sm_dout = pio_claim_unused_sm(pio, true);
i2s->sm_mask |= (1u << i2s->sm_dout);
offset = pio_add_program(pio, &i2s_out_master_program);
i2s_out_master_program_init(pio, i2s->sm_dout, offset, config->bit_depth, config->dout_pin, config->clock_pin_base);
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_dout, clocks.bck_d, clocks.bck_f);
}
void i2s_program_start_slaved(PIO pio, const i2s_config* config, void (*dma_handler)(void), pio_i2s* i2s) {
if (((uint32_t)i2s & 0x7) != 0) {
DEBUG_PRINTLN("pio_i2s argument must be 8-byte aligned!");
}
i2s_slave_program_init(pio, config, i2s);
dma_double_buffer_init(i2s, dma_handler);
pio_enable_sm_mask_in_sync(i2s->pio, i2s->sm_mask);
}
void i2s_program_start_synched(PIO pio, const i2s_config* config, void (*dma_handler)(void), pio_i2s* i2s) {
// DEBUG_PRINTLN("Start synched");
if (((uint32_t)i2s & 0x7) != 0) {
DEBUG_PRINTLN("pio_i2s argument must be 8-byte aligned!");
}
i2s_sync_program_init(pio, config, i2s);
dma_double_buffer_init(i2s, dma_handler);
pio_enable_sm_mask_in_sync(i2s->pio, i2s->sm_mask);
}

View file

@ -0,0 +1,82 @@
/* i2s.h
*
* Author: Daniel Collins
* Date: 2022-02-25
*
* Copyright (c) 2022 Daniel Collins
*
* This file is part of rp2040_i2s_example.
*
* rp2040_i2s_example is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3 as published by the
* Free Software Foundation.
*
* rp2040_i2s_example 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
* rp2040_i2s_example. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include "hardware/pio.h"
#include "../../PicoDefs.hpp"
#ifndef I2S_TEST_I2S_H
#define I2S_TEST_I2S_H
#define AUDIO_BUFFER_FRAMES 48
#define STEREO_BUFFER_SIZE AUDIO_BUFFER_FRAMES * 2 // roughly 1ms, 48 L + R words
typedef struct i2s_config {
uint32_t fs;
uint32_t sck_mult;
uint8_t bit_depth;
uint8_t sck_pin;
uint8_t dout_pin;
uint8_t din_pin;
uint8_t clock_pin_base;
bool sck_enable;
} i2s_config;
typedef struct pio_i2s_clocks {
// Clock computation results
float fs_attained;
float sck_pio_hz;
float bck_pio_hz;
// PIO divider ratios to obtain the computed clocks above
uint16_t sck_d;
uint8_t sck_f;
uint16_t bck_d;
uint8_t bck_f;
} pio_i2s_clocks;
// NOTE: Use __attribute__ ((aligned(8))) on this struct or the DMA wrap won't work!
typedef struct pio_i2s {
PIO pio;
uint8_t sm_mask;
uint8_t sm_sck;
uint8_t sm_dout;
uint8_t sm_din;
uint dma_ch_in_ctrl;
uint dma_ch_in_data;
uint dma_ch_out_ctrl;
uint dma_ch_out_data;
int32_t* in_ctrl_blocks[2]; // Control blocks MUST have 8-byte alignment.
int32_t* out_ctrl_blocks[2];
int32_t input_buffer[STEREO_BUFFER_SIZE * 2];
int32_t output_buffer[STEREO_BUFFER_SIZE * 2];
i2s_config config;
} pio_i2s;
// extern const i2s_config i2s_config_default;
const i2s_config i2s_config_default = {48000, 256, 32, 10, 6, 7, 8, true};
void i2s_program_start_slaved(PIO pio, const i2s_config* config, void (*dma_handler)(void), pio_i2s* i2s);
void i2s_program_start_synched(PIO pio, const i2s_config* config, void (*dma_handler)(void), pio_i2s* i2s);
#endif // I2S_TEST_I2S_H

View file

@ -0,0 +1,73 @@
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
/*
; I2S audio bidirectional (input/output) block, both in slave mode.
; Requires external BCK and LRCK, usually from the codec directly.
; This block provides both the output and input components together,
; so should not be used in combination with either of the other output or
; input units on the same I2S bus.
;
; Input pin order: DIN, BCK, LRCK
; Set JMP pin to LRCK.
;
; Clock synchronously with the system clock, or *at least* 4x the usual
; bit clock for a given fs (e.g. for 48kHz 24-bit, clock at at least
; (48000 * 24 * 2 (stereo)) * 4 = 9.216 MHz. Ideally 8x or more.
*/
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// -------------- //
// i2s_bidi_slave //
// -------------- //
#define i2s_bidi_slave_wrap_target 0
#define i2s_bidi_slave_wrap 23
static const uint16_t i2s_bidi_slave_program_instructions[] = {
// .wrap_target
0x20a1, // 0: wait 1 pin, 1
0x4001, // 1: in pins, 1
0x8080, // 2: pull noblock
0x8000, // 3: push noblock
0x2021, // 4: wait 0 pin, 1
0x2022, // 5: wait 0 pin, 2
0x6001, // 6: out pins, 1
0x20a1, // 7: wait 1 pin, 1
0x4001, // 8: in pins, 1
0x2021, // 9: wait 0 pin, 1
0x6001, // 10: out pins, 1
0x00cd, // 11: jmp pin, 13
0x0007, // 12: jmp 7
0x20a1, // 13: wait 1 pin, 1
0x4001, // 14: in pins, 1
0x8080, // 15: pull noblock
0x8000, // 16: push noblock
0x2021, // 17: wait 0 pin, 1
0x6001, // 18: out pins, 1
0x20a1, // 19: wait 1 pin, 1
0x4001, // 20: in pins, 1
0x2021, // 21: wait 0 pin, 1
0x6001, // 22: out pins, 1
0x00d3, // 23: jmp pin, 19
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program i2s_bidi_slave_program = {
.instructions = i2s_bidi_slave_program_instructions,
.length = 24,
.origin = -1,
};
static inline pio_sm_config i2s_bidi_slave_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + i2s_bidi_slave_wrap_target, offset + i2s_bidi_slave_wrap);
return c;
}
#endif

View file

@ -0,0 +1,65 @@
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
/*
; I2S Audio Input - Slave or Synchronous with Output Master
; Inputs must be sequential in order: DIN, BCK, LRCK
; Must run at same speed of SCK block, or at least 4x BCK.
;
; NOTE: Set JMP pin to LRCK pin.
; NOTE: The very first word read is potentially corrupt, since there is a
; chance to start in the middle of an L frame and only read part of it.
; Nevertheless, the frame order should be synchronized (first word is L,
; second is R, etc.
*/
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// ------------ //
// i2s_in_slave //
// ------------ //
#define i2s_in_slave_wrap_target 0
#define i2s_in_slave_wrap 17
static const uint16_t i2s_in_slave_program_instructions[] = {
// .wrap_target
0x20a1, // 0: wait 1 pin, 1
0x4001, // 1: in pins, 1
0x8000, // 2: push noblock
0x2021, // 3: wait 0 pin, 1
0x2022, // 4: wait 0 pin, 2
0x20a1, // 5: wait 1 pin, 1
0x4001, // 6: in pins, 1
0x2021, // 7: wait 0 pin, 1
0x00ca, // 8: jmp pin, 10
0x0005, // 9: jmp 5
0x20a1, // 10: wait 1 pin, 1
0x4001, // 11: in pins, 1
0x8000, // 12: push noblock
0x2021, // 13: wait 0 pin, 1
0x20a1, // 14: wait 1 pin, 1
0x4001, // 15: in pins, 1
0x2021, // 16: wait 0 pin, 1
0x00ce, // 17: jmp pin, 14
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program i2s_in_slave_program = {
.instructions = i2s_in_slave_program_instructions,
.length = 18,
.origin = -1,
};
static inline pio_sm_config i2s_in_slave_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + i2s_in_slave_wrap_target, offset + i2s_in_slave_wrap);
return c;
}
#endif

View file

@ -0,0 +1,59 @@
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
// ; I2S audio output block. Synchronous with clock and input.
// ; Must run at BCK * 2.
// ;
// ; This block also outputs the word clock (also called frame or LR clock) and
// ; the bit clock.
// ;
// ; Set register x to (bit depth - 2) (e.g. for 24 bit audio, set to 22).
// ; Note that if this is needed to be synchronous with the SCK module,
// ; it is not possible to run 24-bit frames with an SCK of 256x fs. You must either
// ; run SCK at 384x fs (if your codec permits this) or use 32-bit frames, which
// ; work fine with 24-bit codecs.
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// -------------- //
// i2s_out_master //
// -------------- //
#define i2s_out_master_wrap_target 0
#define i2s_out_master_wrap 7
#define i2s_out_master_offset_entry_point 0u
static const uint16_t i2s_out_master_program_instructions[] = {
// .wrap_target
0xe03e, // 0: set x, 30 side 0
0x8880, // 1: pull noblock side 1
0x6001, // 2: out pins, 1 side 0
0x0842, // 3: jmp x--, 2 side 1
0xf03e, // 4: set x, 30 side 2
0x9880, // 5: pull noblock side 3
0x7001, // 6: out pins, 1 side 2
0x1846, // 7: jmp x--, 6 side 3
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program i2s_out_master_program = {
.instructions = i2s_out_master_program_instructions,
.length = 8,
.origin = -1,
};
static inline pio_sm_config i2s_out_master_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + i2s_out_master_wrap_target, offset + i2s_out_master_wrap);
sm_config_set_sideset(&c, 2, false, false);
return c;
}
#endif

View file

@ -0,0 +1,96 @@
#pragma once
#include "i2s_sck.pio.h"
#include "i2s_out_master.pio.h"
#include "i2s_bidi_slave.pio.h"
#include "i2s_in_slave.pio.h"
// These constants are the I2S clock to pio clock ratio
const int i2s_sck_program_pio_mult = 2;
const int i2s_out_master_program_pio_mult = 2;
/*
* System ClocK (SCK) is only required by some I2S peripherals.
* This outputs it at 1 SCK per 2 PIO clocks, so scale the dividers correctly
* first.
* NOTE: Most peripherals require that this is *perfectly* aligned in ratio,
* if not phase, to the bit and word clocks of any master peripherals.
* It is up to you to ensure that the divider config is set up for a
* precise (not approximate) ratio between the BCK, LRCK, and SCK outputs.
*/
static void i2s_sck_program_init(PIO pio, uint8_t sm, uint8_t offset, uint8_t sck_pin) {
pio_gpio_init(pio, sck_pin);
pio_sm_config sm_config = i2s_sck_program_get_default_config(offset);
sm_config_set_set_pins(&sm_config, sck_pin, 1);
uint pin_mask = (1u << sck_pin);
pio_sm_set_pins_with_mask(pio, sm, 0, pin_mask); // zero output
pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask);
pio_sm_init(pio, sm, offset, &sm_config);
}
static inline void i2s_out_master_program_init(PIO pio, uint8_t sm, uint8_t offset, uint8_t bit_depth, uint8_t dout_pin, uint8_t clock_pin_base) {
pio_gpio_init(pio, dout_pin);
pio_gpio_init(pio, clock_pin_base);
pio_gpio_init(pio, clock_pin_base + 1);
pio_sm_config sm_config = i2s_out_master_program_get_default_config(offset);
sm_config_set_out_pins(&sm_config, dout_pin, 1);
sm_config_set_sideset_pins(&sm_config, clock_pin_base);
sm_config_set_out_shift(&sm_config, false, false, bit_depth);
sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX);
pio_sm_init(pio, sm, offset, &sm_config);
uint32_t pin_mask = (1u << dout_pin) | (3u << clock_pin_base);
pio_sm_set_pins_with_mask(pio, sm, 0, pin_mask); // zero output
pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask);
}
static inline void i2s_bidi_slave_program_init(PIO pio, uint8_t sm, uint8_t offset, uint8_t dout_pin, uint8_t in_pin_base) {
pio_gpio_init(pio, dout_pin);
pio_gpio_init(pio, in_pin_base);
pio_gpio_init(pio, in_pin_base + 1);
pio_gpio_init(pio, in_pin_base + 2);
pio_sm_config sm_config = i2s_bidi_slave_program_get_default_config(offset);
sm_config_set_out_pins(&sm_config, dout_pin, 1);
sm_config_set_in_pins(&sm_config, in_pin_base);
sm_config_set_jmp_pin(&sm_config, in_pin_base + 2);
sm_config_set_out_shift(&sm_config, false, false, 0);
sm_config_set_in_shift(&sm_config, false, false, 0);
pio_sm_init(pio, sm, offset, &sm_config);
// Setup output pins
uint32_t pin_mask = (1u << dout_pin);
pio_sm_set_pins_with_mask(pio, sm, 0, pin_mask); // zero output
pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask);
// Setup input pins
pin_mask = (7u << in_pin_base); // Three input pins
pio_sm_set_pindirs_with_mask(pio, sm, 0, pin_mask);
}
/*
* Designed to be used with output master module, requiring overlapping pins:
* din_pin_base + 0 = input pin
* din_pin_base + 1 = out_master clock_pin_base
* din_pin_base + 2 = out_master clock_pin_base + 1
*
* Intended to be run at SCK rate (4x BCK), so clock same as SCK module if using
* it, or 4x the BCK frequency (BCK is 64x fs, so 256x fs).
*/
static inline void i2s_in_slave_program_init(PIO pio, uint8_t sm, uint8_t offset, uint8_t din_pin_base) {
pio_gpio_init(pio, din_pin_base);
gpio_set_pulls(din_pin_base, false, false);
gpio_set_dir(din_pin_base, GPIO_IN);
pio_sm_config sm_config = i2s_in_slave_program_get_default_config(offset);
sm_config_set_in_pins(&sm_config, din_pin_base);
sm_config_set_in_shift(&sm_config, false, false, 0);
sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_RX);
sm_config_set_jmp_pin(&sm_config, din_pin_base + 2);
pio_sm_init(pio, sm, offset, &sm_config);
uint32_t pin_mask = (7u << din_pin_base); // Three input pins
pio_sm_set_pindirs_with_mask(pio, sm, 0, pin_mask);
}

View file

@ -0,0 +1,37 @@
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// ------- //
// i2s_sck //
// ------- //
#define i2s_sck_wrap_target 0
#define i2s_sck_wrap 1
static const uint16_t i2s_sck_program_instructions[] = {
// .wrap_target
0xe001, // 0: set pins, 1
0xe000, // 1: set pins, 0
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program i2s_sck_program = {
.instructions = i2s_sck_program_instructions,
.length = 2,
.origin = -1,
};
static inline pio_sm_config i2s_sck_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + i2s_sck_wrap_target, offset + i2s_sck_wrap);
return c;
}
#endif

View file

@ -0,0 +1,23 @@
#ifndef FLASH_FS_HPP
#define FLASH_FS_HPP
#include <Arduino.h>
#include <VFS.h>
#include <LittleFS.h>
namespace FlashFS {
void begin()
{
LittleFS.begin();
VFS.root(LittleFS);
DEBUG_PRINTLN("LittleFS initialized.");
}
bool exists(const char* filename)
{
return LittleFS.exists(filename);
}
} // namespace FlashFS
#endif // FLASH_FS_HPP

View file

@ -0,0 +1,359 @@
#include "MEMLNaut.hpp"
#include "../../audio/AudioDriver.hpp"
#include "Arduino.h"
#include "pico/util/queue.h"
MEMLNaut* MEMLNaut::instance = nullptr;
queue_t queue_buttons_;
#define FAST_MEM __not_in_flash("memlnaut")
// struct repeating_timer FAST_MEM timerDisplay;
// inline bool __not_in_flash_func(displayUpdate)(__unused struct repeating_timer *t) {
// MEMLNaut::Instance()->disp->Draw();
// return true;
// }
// struct repeating_timer FAST_MEM timerTouch;
// inline bool __not_in_flash_func(touchUpdate)(__unused struct repeating_timer *t) {
// // scr->update();
// MEMLNaut::Instance()->disp->PollTouch();
// return true;
// }
// Static interrupt handlers implementation
#define HANDLE_BUTTON_MACRO(handler_name, pin_name, callback_name, debouncer_index) \
void __not_in_flash_func(MEMLNaut::handle##handler_name)() { \
if (instance) { \
bool current_value = digitalRead(Pins::pin_name) == LOW; \
/*Serial.println(String("Button ") + #pin_name + " interrupt, value: " + String(current_value));*/ \
if (instance->debouncers[debouncer_index].debounce(current_value)) { \
/*Serial.println("Valid input!");*/ \
if (instance->debouncers[debouncer_index].getState()) { \
/*Serial.println(String("Pin ") + #pin_name + " getState() = 1");*/ \
if (instance->callback_name##Callback) { \
const size_t pin = Pins::pin_name; \
queue_add_blocking(&queue_buttons_, &pin); \
/*Serial.println(String("Pin ") + #pin_name + " callback!");*/ \
/*instance->callback_name##Callback();*/ \
} \
} else { \
/*Serial.println(String("Pin ") + #pin_name + " getState() = 0");*/ \
} \
} \
/*Serial.println("---");*/ \
} \
}
HANDLE_BUTTON_MACRO(MomA1, MOM_A1, momA1, 0)
HANDLE_BUTTON_MACRO(MomA2, MOM_A2, momA2, 1)
HANDLE_BUTTON_MACRO(MomB1, MOM_B1, momB1, 2)
HANDLE_BUTTON_MACRO(MomB2, MOM_B2, momB2, 3)
HANDLE_BUTTON_MACRO(ReSW, RE_SW, reSW, 4)
void __not_in_flash_func(MEMLNaut::handleTogA1)() {
if (instance) {
bool should_update = instance->toggleDebouncers[0].debounce(digitalRead(Pins::TOG_A1) == LOW);
if (should_update && instance->togA1Callback) {
bool val = instance->toggleDebouncers[0].getState();
instance->togA1Callback(val);
}
}
}
void __not_in_flash_func(MEMLNaut::handleTogA2)() {
if (instance) {
bool should_update = instance->toggleDebouncers[1].debounce(digitalRead(Pins::TOG_A2) == LOW);
if (should_update && instance->togA2Callback) {
bool val = instance->toggleDebouncers[1].getState();
instance->togA2Callback(val);
}
}
}
void __not_in_flash_func(MEMLNaut::handleTogB1)() {
if (instance) {
bool should_update = instance->toggleDebouncers[2].debounce(digitalRead(Pins::TOG_B1) == LOW);
if (should_update && instance->togB1Callback) {
bool val = instance->toggleDebouncers[2].getState();
instance->togB1Callback(val);
}
}
}
void __not_in_flash_func(MEMLNaut::handleTogB2)() {
if (instance) {
bool should_update = instance->toggleDebouncers[3].debounce(digitalRead(Pins::TOG_B2) == LOW);
if (should_update && instance->togB2Callback) {
bool val = instance->toggleDebouncers[3].getState();
instance->togB2Callback(val);
}
}
}
void __not_in_flash_func(MEMLNaut::handleJoySW)() {
if (instance) {
bool should_update = instance->toggleDebouncers[4].debounce(digitalRead(Pins::JOY_SW) == LOW);
if (should_update && instance->joySWCallback) {
bool val = instance->toggleDebouncers[4].getState();
instance->joySWCallback(val);
}
}
}
int8_t __not_in_flash_func(read_rotary)(uint8_t &prevNextCode, uint16_t &store, int a_pin, int b_pin) {
static int8_t FAST_MEM rot_enc_table[] = { 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0 };
prevNextCode <<= 2;
if (digitalRead(b_pin)) prevNextCode |= 0x02;
if (digitalRead(a_pin)) prevNextCode |= 0x01;
prevNextCode &= 0x0f;
// Serial.println(prevNextCode);
// If valid then store as 16 bit data.
if (rot_enc_table[prevNextCode]) {
store <<= 4;
store |= prevNextCode;
if ((store & 0xff) == 0x2b) return -1;
if ((store & 0xff) == 0x17) return 1;
}
return 0;
}
static uint8_t FAST_MEM enc1Code = 0;
static uint16_t FAST_MEM enc1Store = 0;
void __isr MEMLNaut::encoder1_callback() {
int change = read_rotary(enc1Code, enc1Store, Pins::RE_A, Pins::RE_B);
DEBUG_PRINTLN("Encoder1 change: " + String(change));
if (instance && instance->rotEncCallback && change != 0) {
instance->rotEncCallback(change);
}
}
MEMLNaut::MEMLNaut(bool old_display) {
instance = this;
loopCallback = nullptr;
// Initialize median filters
for(auto& filter : adcFilters) {
filter.init(FILTER_SIZE);
}
// Initialise all pins
Pins::initializePins();
// Attach momentary switch interrupts (FALLING edge)
attachInterrupt(digitalPinToInterrupt(Pins::MOM_A1), handleMomA1, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::MOM_A2), handleMomA2, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::MOM_B1), handleMomB1, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::MOM_B2), handleMomB2, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::RE_SW), handleReSW, CHANGE);
// attachInterrupt(digitalPinToInterrupt(Pins::RE_A), handleReA, FALLING);
// attachInterrupt(digitalPinToInterrupt(Pins::RE_B), handleReB, FALLING);
// Attach toggle switch interrupts (CHANGE)
attachInterrupt(digitalPinToInterrupt(Pins::TOG_A1), handleTogA1, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::TOG_A2), handleTogA2, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::TOG_B1), handleTogB1, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::TOG_B2), handleTogB2, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::JOY_SW), handleJoySW, CHANGE);
// Force RVGain to be master volume
setRVGain1Volume(DEFAULT_THRESHOLD);
//rotary encoder setup
attachInterrupt(digitalPinToInterrupt(Pins::RE_A), encoder1_callback,
CHANGE);
attachInterrupt(digitalPinToInterrupt(Pins::RE_B), encoder1_callback,
CHANGE);
SPI1.setRX(Pins::SD_MISO);
SPI1.setTX(Pins::SD_MOSI);
SPI1.setSCK(Pins::SD_SCK);
if (!old_display) {
disp = std::make_unique<DisplayDriver>();
disp->Setup();
} else {
disp = nullptr;
}
setRotaryEncoderCallback([this](int delta) {
Serial.printf("Rotary encoder moved: %d\n", delta);
if (disp)
disp->RotaryIncEvent(delta);
});
setReSWCallback([this]() {
if (disp)
disp->RotarySwitchEvent();
});
queue_init(&queue_buttons_, sizeof(size_t), 1);
}
// Momentary switch callback setters
void MEMLNaut::setMomA1Callback(ButtonCallback cb) { momA1Callback = cb; }
void MEMLNaut::setMomA2Callback(ButtonCallback cb) { momA2Callback = cb; }
void MEMLNaut::setMomB1Callback(ButtonCallback cb) { momB1Callback = cb; }
void MEMLNaut::setMomB2Callback(ButtonCallback cb) { momB2Callback = cb; }
void MEMLNaut::setReSWCallback(ButtonCallback cb) { reSWCallback = cb; }
// Toggle switch callback setters
void MEMLNaut::setTogA1Callback(ToggleCallback cb) { togA1Callback = cb; }
void MEMLNaut::setTogA2Callback(ToggleCallback cb) { togA2Callback = cb; }
void MEMLNaut::setTogB1Callback(ToggleCallback cb) { togB1Callback = cb; }
void MEMLNaut::setTogB2Callback(ToggleCallback cb) { togB2Callback = cb; }
void MEMLNaut::setJoySWCallback(ToggleCallback cb) { joySWCallback = cb; }
// ADC callback setters
void MEMLNaut::setJoyXCallback(AnalogCallback cb, uint16_t threshold) {
adcStates[0] = {analogRead(Pins::JOY_X) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setJoyYCallback(AnalogCallback cb, uint16_t threshold) {
adcStates[1] = {analogRead(Pins::JOY_Y) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setJoyZCallback(AnalogCallback cb, uint16_t threshold) {
adcStates[2] = {analogRead(Pins::JOY_Z) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setADC3Callback(AnalogCallback cb, uint16_t threshold) {
adcStates[7] = {analogRead(Pins::ADC3) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setRVGain1Callback(AnalogCallback cb, uint16_t threshold) {
//adcStates[3] = {analogRead(Pins::RV_GAIN1) / ADC_SCALE, threshold, cb};
// DEBUG_PRINTLN("RVGain1 overridden - only controls audio volume");
//this overrides the volume control
adcStates[3] = {analogRead(Pins::RV_GAIN1) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setRVGain1Volume(uint16_t threshold) {
adcStates[3] = {
analogRead(Pins::RV_GAIN1) / ADC_SCALE,
threshold,
[] (float value) {
AudioDriver::SetMasterVolume(value);
}
};
}
void MEMLNaut::setRVZ1Callback(AnalogCallback cb, uint16_t threshold) {
adcStates[4] = {analogRead(Pins::RV_Z1) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setRVY1Callback(AnalogCallback cb, uint16_t threshold) {
adcStates[5] = {analogRead(Pins::RV_Y1) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setRVX1Callback(AnalogCallback cb, uint16_t threshold) {
adcStates[6] = {analogRead(Pins::RV_X1) / ADC_SCALE, threshold, cb};
}
void MEMLNaut::setRotaryEncoderCallback(RotaryEncoderCallback cb) {
rotEncCallback = cb;
}
void MEMLNaut::setLoopCallback(LoopCallback cb) {
loopCallback = cb;
}
size_t displayTS=0;
void MEMLNaut::loop() {
static bool first_run = true;
if (first_run) {
first_run = false;
SyncOnBoot();
}
// Process buttons
size_t button_index;
while (queue_try_remove(&queue_buttons_, &button_index)) {
switch (button_index) {
case Pins::MOM_A1:
if (momA1Callback) momA1Callback();
//Serial.println("+++ MOM_A1 asynchronous call!");
break;
case Pins::MOM_A2:
if (momA2Callback) momA2Callback();
//Serial.println("+++ MOM_A2 asynchronous call!");
break;
case Pins::MOM_B1:
if (momB1Callback) momB1Callback();
//Serial.println("+++ MOM_B1 asynchronous call!");
break;
case Pins::MOM_B2:
if (momB2Callback) momB2Callback();
//Serial.println("+++ MOM_B2 asynchronous call!");
break;
case Pins::RE_SW:
if (reSWCallback) reSWCallback();
//Serial.println("+++ RE_SW asynchronous call!");
break;
default:
DEBUG_PRINTLN("MEMLNaut- Unknown button asynchronous call!");
break;
}
}
const uint8_t adcPins[NUM_ADCS] = {
Pins::JOY_X, Pins::JOY_Y, Pins::JOY_Z,
Pins::RV_GAIN1, Pins::RV_Z1, Pins::RV_Y1, Pins::RV_X1,
Pins::ADC3
};
for (size_t i = 0; i < NUM_ADCS; i++) {
uint16_t rawValue = analogRead(adcPins[i]);
uint16_t filteredValue = adcFilters[i].process(rawValue);
float currentValue = filteredValue / ADC_SCALE;
auto& state = adcStates[i];
if (state.callback && abs(static_cast<int>(filteredValue - (state.lastValue * ADC_SCALE))) > state.threshold) {
state.callback(currentValue);
state.lastValue = currentValue;
}
}
if (loopCallback) {
loopCallback();
}
if (disp) {
PERIODIC_RUN(MEMLNaut::Instance()->disp->PollTouch();, 30);
PERIODIC_RUN(MEMLNaut::Instance()->disp->Draw();, 39);
}
}
void MEMLNaut::SyncOnBoot() {
// Synchronize ADCs
const uint8_t adcPins[NUM_ADCS] = {
Pins::JOY_X, Pins::JOY_Y, Pins::JOY_Z,
Pins::RV_GAIN1, Pins::RV_Z1, Pins::RV_Y1, Pins::RV_X1
};
for (size_t i = 0; i < NUM_ADCS; i++) {
uint16_t rawValue = analogRead(adcPins[i]);
adcFilters[i].reset(rawValue); // Reset filter to current value
float currentValue = rawValue / ADC_SCALE;
adcStates[i].lastValue = currentValue;
if (adcStates[i].callback) {
adcStates[i].callback(currentValue);
}
}
// Synchronize toggle switches
const uint8_t togglePins[NUM_TOGGLES] = {
Pins::TOG_A1, Pins::TOG_A2, Pins::TOG_B1, Pins::TOG_B2, Pins::JOY_SW
};
ToggleCallback toggleCallbacks[NUM_TOGGLES] = {
togA1Callback, togA2Callback, togB1Callback, togB2Callback, joySWCallback
};
for (size_t i = 0; i < NUM_TOGGLES; i++) {
bool state = (digitalRead(togglePins[i]) == LOW);
toggleDebouncers[i].setState(state);
if (toggleCallbacks[i]) {
toggleCallbacks[i](state);
}
}
}

View file

@ -0,0 +1,173 @@
#ifndef __MEMLNAUT_HPP__
#define __MEMLNAUT_HPP__
#include "Pins.hpp"
#include "../../utils/Debounce.hpp"
#include "../../utils/MedianFilter.h"
#include <functional>
#include <array>
#include "display/DisplayDriver.hpp"
#include "display/SystemView.hpp"
#include "SD.h"
class MEMLNaut {
public:
using ButtonCallback = std::function<void(void)>;
using ToggleCallback = std::function<void(bool)>;
using AnalogCallback = std::function<void(float)>;
using LoopCallback = std::function<void(void)>;
using RotaryEncoderCallback = std::function<void(int)>;
private:
static MEMLNaut* __not_in_flash("memlnaut") instance;
static constexpr size_t NUM_ADCS = 8;
static constexpr uint16_t DEFAULT_THRESHOLD = 40;
static constexpr size_t FILTER_SIZE = 5;
static constexpr float ADC_SCALE = 4128.7f;
static constexpr size_t NUM_BUTTONS = 7;
static constexpr size_t NUM_TOGGLES = 5;
struct ADCState {
float lastValue = 0.0f;
uint16_t threshold = DEFAULT_THRESHOLD;
AnalogCallback callback = nullptr;
};
std::array<ADCState, NUM_ADCS> adcStates;
std::array<MedianFilter<uint16_t>, NUM_ADCS> adcFilters;
std::array<ToggleDebounce, NUM_BUTTONS> debouncers;
std::array<ToggleDebounce, NUM_TOGGLES> toggleDebouncers;
// Callback storage
ButtonCallback momA1Callback;
ButtonCallback momA2Callback;
ButtonCallback momB1Callback;
ButtonCallback momB2Callback;
ButtonCallback reSWCallback;
RotaryEncoderCallback rotEncCallback;
ToggleCallback togA1Callback;
ToggleCallback togA2Callback;
ToggleCallback togB1Callback;
ToggleCallback togB2Callback;
ToggleCallback joySWCallback;
LoopCallback loopCallback;
// Static interrupt handlers
static void handleMomA1();
static void handleMomA2();
static void handleMomB1();
static void handleMomB2();
static void handleReSW();
// static void handleReA();
// static void handleReB();
static void handleTogA1();
static void handleTogA2();
static void handleTogB1();
static void handleTogB2();
static void handleJoySW();
//encoder
static void encoder1_callback();
public:
static inline __attribute__((always_inline)) MEMLNaut* Instance() {
return instance;
}
static void Initialize(bool old_display = false) {
if (!instance) {
instance = new MEMLNaut(old_display);
}
}
// Delete copy constructor and assignment operator
MEMLNaut(const MEMLNaut&) = delete;
MEMLNaut& operator=(const MEMLNaut&) = delete;
MEMLNaut(bool old_display = false);
void addSystemInfoView() {
if (disp) {
sysView = std::make_shared<SystemView>("System Info");
disp->AddView(sysView);
}
}
bool getMOMA1State() const {return digitalRead(Pins::MOM_A1) == LOW;}
bool getMOMA2State() const {return digitalRead(Pins::MOM_A2) == LOW;}
bool getMOMB1State() const {return digitalRead(Pins::MOM_B1) == LOW;}
bool getMOMB2State() const {return digitalRead(Pins::MOM_B2) == LOW;}
bool getMOMJOYSWState() const {return digitalRead(Pins::JOY_SW) == LOW;}
// Set callbacks for momentary switches
void setMomA1Callback(ButtonCallback cb);
void setMomA2Callback(ButtonCallback cb);
void setMomB1Callback(ButtonCallback cb);
void setMomB2Callback(ButtonCallback cb);
void setReSWCallback(ButtonCallback cb);
// Set callbacks for toggle switches
void setTogA1Callback(ToggleCallback cb);
void setTogA2Callback(ToggleCallback cb);
void setTogB1Callback(ToggleCallback cb);
void setTogB2Callback(ToggleCallback cb);
void setJoySWCallback(ToggleCallback cb);
// ADC callback setters
void setJoyXCallback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setJoyYCallback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setJoyZCallback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setADC3Callback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setRVGain1Callback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setRVGain1Volume(uint16_t threshold = DEFAULT_THRESHOLD);
void setRVZ1Callback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setRVY1Callback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
void setRVX1Callback(AnalogCallback cb, uint16_t threshold = DEFAULT_THRESHOLD);
// Main loop callback setter
void setLoopCallback(LoopCallback cb);
void setRotaryEncoderCallback(RotaryEncoderCallback cb);
/**
* @brief Read all pots and switches at once. Synchronise with the
* state of the hardware panel. Run once after all callbacks are
* correctly created and all interfaces and references are set up.
*
*/
void SyncOnBoot();
void loop();
//display
std::unique_ptr<DisplayDriver> disp;
std::shared_ptr<SystemView> sysView;
// SD
bool startSD() {
if (!SD.begin(Pins::SD_CS, SPI1)) {
return false;
}
return true;
}
bool stopSD() {
SD.end();
Serial.println("SD card stopped");
return true;
}
};
#endif // __MEMLNAUT_HPP__

View file

@ -0,0 +1,60 @@
#include "MEMLNaut.hpp"
#include <Arduino.h>
// Button callback functions
void onMomA1() { DEBUG_PRINTLN("MOM_A1 pressed"); }
void onMomA2() { DEBUG_PRINTLN("MOM_A2 pressed"); }
void onMomB1() { DEBUG_PRINTLN("MOM_B1 pressed"); }
void onMomB2() { DEBUG_PRINTLN("MOM_B2 pressed"); }
void onReSW() { DEBUG_PRINTLN("RE_SW pressed"); }
void onReA() { DEBUG_PRINTLN("RE_A triggered"); }
void onReB() { DEBUG_PRINTLN("RE_B triggered"); }
// Toggle callback functions
void onTogA1(bool state) { DEBUG_PRINTF("TOG_A1: %s\n", state ? "ON" : "OFF"); }
void onTogA2(bool state) { DEBUG_PRINTF("TOG_A2: %s\n", state ? "ON" : "OFF"); }
void onTogB1(bool state) { DEBUG_PRINTF("TOG_B1: %s\n", state ? "ON" : "OFF"); }
void onTogB2(bool state) { DEBUG_PRINTF("TOG_B2: %s\n", state ? "ON" : "OFF"); }
void onJoySW(bool state) { DEBUG_PRINTF("JOY_SW: %s\n", state ? "ON" : "OFF"); }
// ADC callback functions
void onJoyX(float value) { DEBUG_PRINTF("JOY_X: %.3f\n", value); }
void onJoyY(float value) { DEBUG_PRINTF("JOY_Y: %.3f\n", value); }
void onJoyZ(float value) { DEBUG_PRINTF("JOY_Z: %.3f\n", value); }
void onRVGain1(float value) { DEBUG_PRINTF("RV_GAIN1: %.3f\n", value); }
void onRVZ1(float value) { DEBUG_PRINTF("RV_Z1: %.3f\n", value); }
void onRVY1(float value) { DEBUG_PRINTF("RV_Y1: %.3f\n", value); }
void onRVX1(float value) { DEBUG_PRINTF("RV_X1: %.3f\n", value); }
namespace MEMLNautTest {
void Setup() {
Serial.begin(115200);
while (!Serial) delay(10);
DEBUG_PRINTLN("MEMLNaut Test Starting...");
// Set up momentary switch callbacks
MEMLNaut::Instance()->setMomA1Callback(onMomA1);
MEMLNaut::Instance()->setMomA2Callback(onMomA2);
MEMLNaut::Instance()->setMomB1Callback(onMomB1);
MEMLNaut::Instance()->setMomB2Callback(onMomB2);
MEMLNaut::Instance()->setReSWCallback(onReSW);
// Set up toggle switch callbacks
MEMLNaut::Instance()->setTogA1Callback(onTogA1);
MEMLNaut::Instance()->setTogA2Callback(onTogA2);
MEMLNaut::Instance()->setTogB1Callback(onTogB1);
MEMLNaut::Instance()->setTogB2Callback(onTogB2);
MEMLNaut::Instance()->setJoySWCallback(onJoySW);
// Set up ADC callbacks with default threshold
MEMLNaut::Instance()->setJoyXCallback(onJoyX);
MEMLNaut::Instance()->setJoyYCallback(onJoyY);
MEMLNaut::Instance()->setJoyZCallback(onJoyZ);
MEMLNaut::Instance()->setRVGain1Callback(onRVGain1);
MEMLNaut::Instance()->setRVZ1Callback(onRVZ1);
MEMLNaut::Instance()->setRVY1Callback(onRVY1);
MEMLNaut::Instance()->setRVX1Callback(onRVX1);
DEBUG_PRINTLN("MEMLNaut Test Setup Complete!");
}
}

View file

@ -0,0 +1,8 @@
#ifndef __MEMLNAUT_TEST_HPP__
#define __MEMLNAUT_TEST_HPP__
namespace MEMLNautTest {
void Setup();
}
#endif // __MEMLNAUT_TEST_HPP__

View file

@ -0,0 +1,66 @@
#ifndef PSRAM_MANAGER_HPP
#define PSRAM_MANAGER_HPP
#include <hardware/structs/qmi.h>
#include <hardware/structs/xip.h>
#include <hardware/clocks.h>
// PSRAM memory map on RP2350B
// 0x11000000 — cached via XIP L2 (best for hot read-mostly buffers)
// 0x15000000 — uncached/noalloc (direct QMI, no coherence concerns on write)
#define PSRAM_BASE (reinterpret_cast<uint8_t *>(0x11000000u))
#define PSRAM_BASE_NOCACHE (reinterpret_cast<uint8_t *>(0x15000000u))
// Place PSRAM-backed objects with: uint8_t PSRAM_ATTR myBuf[N];
#define PSRAM_ATTR __attribute__((section(".psram")))
class PSRAMManager {
public:
// Detect PSRAM and apply fastest stable QMI timing.
// Call after set_sys_clock_khz(), before any PSRAM access.
// Returns true if PSRAM was found and configured.
static bool init() {
_size = rp2040.getPSRAMSize();
if (!_size) return false;
_applyOptimalTiming();
return true;
}
static bool available() { return _size > 0; }
static uint32_t size() { return _size; }
static uint8_t* base() { return PSRAM_BASE; }
static uint8_t* baseUncached() { return PSRAM_BASE_NOCACHE; }
static uint32_t psramClockMHz() { return _psramMHz; }
private:
inline static uint32_t _size = 0;
inline static uint32_t _psramMHz = 0;
// RXDELAY is counted in sys_clk cycles, not PSRAM cycles. At 264 MHz,
// rxd=3 is only 11.4 ns — insufficient for board trace delays — so even
// div=2 (66 MHz PSRAM) fails. Empirically verified configurations:
// 200 MHz sys_clk: div=1 rxd=3 → 100 MHz PSRAM, 33 MB/s
// 264 MHz sys_clk: div=3 rxd=2 → 44 MHz PSRAM, 17 MB/s
// Staying at 200 MHz gives substantially better PSRAM throughput.
static void _applyOptimalTiming() {
const uint32_t sys_mhz = clock_get_hz(clk_sys) / 1000000u;
uint32_t div, rxd;
if (sys_mhz <= 200u) {
div = 1u; rxd = 3u; // 100 MHz PSRAM at 200 MHz, sweep-verified
} else if (sys_mhz <= 264u) {
div = 3u; rxd = 2u; // 44 MHz PSRAM at 264 MHz, sweep-verified
} else {
div = 4u; rxd = 3u; // conservative fallback for unknown clocks
}
hw_set_bits(&xip_ctrl_hw->ctrl, XIP_CTRL_WRITABLE_M1_BITS);
uint32_t t = qmi_hw->m[1].timing;
t &= ~(QMI_M1_TIMING_CLKDIV_BITS | QMI_M1_TIMING_RXDELAY_BITS);
t |= (div << QMI_M1_TIMING_CLKDIV_LSB) | (rxd << QMI_M1_TIMING_RXDELAY_LSB);
qmi_hw->m[1].timing = t;
_psramMHz = sys_mhz / 2u / div;
}
};
#endif // PSRAM_MANAGER_HPP

View file

@ -0,0 +1,157 @@
/**
* @file Pins.hpp
* @brief Pin definitions and initialization for the FM Synth RL project
*
* @copyright Copyright (c) 2024. This Source Code Form is subject to the terms
* of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#ifndef __PINS_HPP__
#define __PINS_HPP__
#include <Arduino.h>
#include "../../PicoDefs.hpp"
/**
* @brief Class containing all pin definitions and initialization functions
*/
class Pins {
public:
/**
* @name Momentary Switches
* Digital inputs with internal pull-up resistors
* @{
*/
static constexpr uint8_t MOM_A1 = 24; ///< Momentary switch A1
static constexpr uint8_t MOM_A2 = 25; ///< Momentary switch A2
static constexpr uint8_t MOM_B1 = 28; ///< Momentary switch B1
static constexpr uint8_t MOM_B2 = 29; ///< Momentary switch B2
static constexpr uint8_t RE_SW = 23; ///< Rotary encoder switch
static constexpr uint8_t RE_B = 17; ///< Rotary encoder B signal
static constexpr uint8_t RE_A = 11; ///< Rotary encoder A signal
static constexpr uint8_t JOY_SW = 32; ///< Joystick switch
/** @} */
/**
* @name Toggle Switches
* Digital inputs with internal pull-up resistors
* @{
*/
static constexpr uint8_t TOG_A1 = 26; ///< Toggle switch A1
static constexpr uint8_t TOG_A2 = 27; ///< Toggle switch A2
static constexpr uint8_t TOG_B1 = 30; ///< Toggle switch B1
static constexpr uint8_t TOG_B2 = 31; ///< Toggle switch B2
/** @} */
/**
* @name ADC Inputs
* 12-bit resolution analog inputs
* @{
*/
static constexpr uint8_t JOY_X = 40; ///< Joystick X-axis (ADC0)
static constexpr uint8_t JOY_Y = 41; ///< Joystick Y-axis (ADC1)
static constexpr uint8_t JOY_Z = 42; ///< Joystick Z-axis (ADC2)
static constexpr uint8_t ADC3 = 43; ///< Joystick Z-axis (ADC3)
static constexpr uint8_t RV_GAIN1 = 47; ///< Gain potentiometer (ADC7)
static constexpr uint8_t RV_Z1 = 46; ///< Z1 potentiometer (ADC6)
static constexpr uint8_t RV_Y1 = 45; ///< Y1 potentiometer (ADC5)
static constexpr uint8_t RV_X1 = 44; ///< X1 potentiometer (ADC4)
/** @} */
/**
* @name LED Pins
* Digital output pins for LEDs
* @{
*/
static constexpr uint8_t LED = 33; ///< Status LED
static constexpr uint8_t LED_TIMING = 43; ///< Timing LED
/** @} */
/**
* @name UART Pins
* Digital pins used for UART communication
* @{
*/
static constexpr uint8_t DAISY_TX = 36; ///< One-way TX to Daisy (PIO software serial)
static constexpr uint8_t SENSOR_TX = 34; ///< Sensor UART TX (Serial1)
static constexpr uint8_t SENSOR_RX = 35; ///< Sensor UART RX (Serial1)
static constexpr uint8_t MIDI_TX = 4; ///< MIDI UART TX (Serial2)
static constexpr uint8_t MIDI_RX = 5; ///< MIDI UART RX (Serial2)
/** @} */
/**
* @name SPI Pins
* Digital pins used for SPI communication with SD card
* @{
*/
static constexpr uint8_t SD_CS = 13; ///< SD card chip select
static constexpr uint8_t SD_SCK = 14; ///< SD card clock
static constexpr uint8_t SD_MISO = 12; ///< SD card MISO
static constexpr uint8_t SD_MOSI = 15; ///< SD card MOSI
/** @} */
/**
* @name I2C Pins
* Digital pins used for I2C communication
* @{
*/
static constexpr uint8_t USEQ_SDA = 38; ///< USeq I2C SDA (CV output)
static constexpr uint8_t USEQ_SCL = 39; ///< USeq I2C SCL (CV output)
/** @} */
/**
* @brief Initialize all pins with their respective modes
*/
static void initializePins() {
// Initialize momentary switches
pinMode(MOM_A1, INPUT_PULLUP);
pinMode(MOM_A2, INPUT_PULLUP);
pinMode(MOM_B1, INPUT_PULLUP);
pinMode(MOM_B2, INPUT_PULLUP);
pinMode(RE_SW, INPUT_PULLUP);
pinMode(RE_B, INPUT_PULLUP);
pinMode(RE_A, INPUT_PULLUP);
pinMode(JOY_SW, INPUT_PULLUP);
// Initialize toggle switches
pinMode(TOG_A1, INPUT_PULLUP);
pinMode(TOG_A2, INPUT_PULLUP);
pinMode(TOG_B1, INPUT_PULLUP);
pinMode(TOG_B2, INPUT_PULLUP);
// Initialize ADC pins
pinMode(JOY_X, INPUT);
pinMode(JOY_Y, INPUT);
pinMode(JOY_Z, INPUT);
pinMode(RV_GAIN1, INPUT);
pinMode(RV_Z1, INPUT);
pinMode(RV_Y1, INPUT);
pinMode(RV_X1, INPUT);
// Initialize LED pins
pinMode(LED, OUTPUT);
pinMode(LED_TIMING, OUTPUT);
// Initialize UART pins (just as digital IO)
pinMode(DAISY_TX, OUTPUT);
pinMode(SENSOR_TX, OUTPUT);
pinMode(SENSOR_RX, INPUT);
pinMode(MIDI_TX, OUTPUT);
pinMode(MIDI_RX, INPUT);
// Initialize I2C pins (just as digital IO)
pinMode(USEQ_SDA, OUTPUT);
pinMode(USEQ_SCL, OUTPUT);
// Set ADC resolution to 12 bits
analogReadResolution(12);
DEBUG_PRINTLN("Pins initialized.");
}
};
#endif // __PINS_HPP__

View file

@ -0,0 +1,187 @@
// USER DEFINED SETTINGS
// Set driver type, fonts to be loaded, pins used and SPI control method etc.
//
// See the User_Setup_Select.h file if you wish to be able to define multiple
// setups and then easily select which setup file is used by the compiler.
//
// If this file is edited correctly then all the library example sketches should
// run without the need to make any more changes for a particular hardware setup!
// Note that some sketches are designed for a particular TFT pixel width/height
#define USER_SETUP_ID 60
// ##################################################################################
//
// Section 1. Call up the right driver file and any options for it
//
// ##################################################################################
// Tell the library to use 8-bit parallel mode (otherwise SPI is assumed)
//#define TFT_PARALLEL_8_BIT
// Display type - only define if RPi display
//#define RPI_DISPLAY_TYPE // 20MHz maximum SPI
// Only define one driver, the other ones must be commented out
#define ILI9341_DRIVER
//#define ST7735_DRIVER // Define additional parameters below for this display
//#define ILI9163_DRIVER // Define additional parameters below for this display
//#define S6D02A1_DRIVER
//#define RPI_ILI9486_DRIVER // 20MHz maximum SPI
//#define HX8357D_DRIVER
//#define ILI9481_DRIVER
//#define ILI9486_DRIVER
//#define ILI9488_DRIVER // WARNING: Do not connect ILI9488 display SDO to MISO if other devices share the SPI bus (TFT SDO does NOT tristate when CS is high)
//#define ST7789_DRIVER // Full configuration option, define additional parameters below for this display
//#define ST7789_2_DRIVER // Minimal configuration option, define additional parameters below for this display
//#define R61581_DRIVER
//#define RM68140_DRIVER
//#define ST7796_DRIVER
//#define SSD1963_480_DRIVER
//#define SSD1963_800_DRIVER
//#define SSD1963_800ALT_DRIVER
//#define ILI9225_DRIVER
// Some displays support SPI reads via the MISO pin, other displays have a single
// bi-directional SDA pin and the library will try to read this via the MOSI line.
// To use the SDA line for reading data from the TFT uncomment the following line:
// #define TFT_SDA_READ // This option is for ESP32 ONLY, tested with ST7789 display only
// For ST7735, ST7789 and ILI9341 ONLY, define the colour order IF the blue and red are swapped on your display
// Try ONE option at a time to find the correct colour order for your display
// #define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue
// #define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
// For ST7789, ST7735 and ILI9163 ONLY, define the pixel width and height in portrait orientation
// #define TFT_WIDTH 80
// #define TFT_WIDTH 128
// #define TFT_WIDTH 240 // ST7789 240 x 240 and 240 x 320
// #define TFT_HEIGHT 160
// #define TFT_HEIGHT 128
// #define TFT_HEIGHT 240 // ST7789 240 x 240
// #define TFT_HEIGHT 320 // ST7789 240 x 320
// For ST7735 ONLY, define the type of display, originally this was based on the
// colour of the tab on the screen protector film but this is not always true, so try
// out the different options below if the screen does not display graphics correctly,
// e.g. colours wrong, mirror images, or tray pixels at the edges.
// Comment out ALL BUT ONE of these options for a ST7735 display driver, save this
// this User_Setup file, then rebuild and upload the sketch to the board again:
// #define ST7735_INITB
// #define ST7735_GREENTAB
// #define ST7735_GREENTAB2
// #define ST7735_GREENTAB3
// #define ST7735_GREENTAB128 // For 128 x 128 display
// #define ST7735_GREENTAB160x80 // For 160 x 80 display (BGR, inverted, 26 offset)
// #define ST7735_REDTAB
// #define ST7735_BLACKTAB
// #define ST7735_REDTAB160x80 // For 160 x 80 display with 24 pixel offset
// If colours are inverted (white shows as black) then uncomment one of the next
// 2 lines try both options, one of the options should correct the inversion.
// #define TFT_INVERSION_ON
// #define TFT_INVERSION_OFF
// ##################################################################################
//
// Section 2. Define the pins that are used to interface with the display here
//
// ##################################################################################
// If a backlight control signal is available then define the TFT_BL pin in Section 2
// below. The backlight will be turned ON when tft.begin() is called, but the library
// needs to know if the LEDs are ON with the pin HIGH or LOW. If the LEDs are to be
// driven with a PWM signal or turned OFF/ON then this must be handled by the user
// sketch. e.g. with digitalWrite(TFT_BL, LOW);
// #define TFT_BL 32 // LED back-light control pin
// #define TFT_BACKLIGHT_ON HIGH // Level to turn ON back-light (HIGH or LOW)
// We must use hardware SPI, a minimum of 3 GPIO pins is needed.
// Typical setup for the RP2040 is :
//
// Display SDO/MISO to RP2040 pin D0 (or leave disconnected if not reading TFT)
// Display LED to RP2040 pin 3V3 or 5V
// Display SCK to RP2040 pin D2
// Display SDI/MOSI to RP2040 pin D3
// Display DC (RS/AO)to RP2040 pin D18 (can use another pin if desired)
// Display RESET to RP2040 pin D19 (can use another pin if desired)
// Display CS to RP2040 pin D20 (can use another pin if desired, or GND, see below)
// Display GND to RP2040 pin GND (0V)
// Display VCC to RP2040 5V or 3.3V (5v if display has a 5V to 3.3V regulator fitted)
//
// The DC (Data Command) pin may be labelled AO or RS (Register Select)
//
// With some displays such as the ILI9341 the TFT CS pin can be connected to GND if no more
// SPI devices (e.g. an SD Card) are connected, in this case comment out the #define TFT_CS
// line below so it is NOT defined. Other displays such at the ST7735 require the TFT CS pin
// to be toggled during setup, so in these cases the TFT_CS line must be defined and connected.
// For the Pico use these #define lines
#define TFT_MISO 16
#define TFT_MOSI 3
#define TFT_SCLK 2
#define TFT_CS 20 // Chip select control pin
#define TFT_DC 18 // Data Command control pin
#define TFT_RST 22 // Reset pin (could connect to Arduino RESET pin)
//#define TFT_BL // LED back-light
#define TOUCH_CS 21 // Chip select pin (T_CS) of touch screen
// ##################################################################################
//
// Section 3. Define the fonts that are to be used here
//
// ##################################################################################
// Comment out the #defines below with // to stop that font being loaded
// The ESP8366 and ESP32 have plenty of memory so commenting out fonts is not
// normally necessary. If all fonts are loaded the extra FLASH space required is
// about 17Kbytes. To save FLASH space only enable the fonts you need!
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
// Comment out the #define below to stop the SPIFFS filing system and smooth font code being loaded
// this will save ~20kbytes of FLASH
#define SMOOTH_FONT
// ##################################################################################
//
// Section 4. Other options
//
// ##################################################################################
// For the RP2040 processor define the SPI port channel used, default is 0
// #define TFT_SPI_PORT 1 // Set to 0 if SPI0 pins are used, or 1 if spi1 pins used
// Define the SPI clock frequency, this affects the graphics rendering speed. Too
// fast and the TFT driver will not keep up and display corruption appears.
// With an ILI9341 display 40MHz works OK, 80MHz sometimes fails
// With a ST7735 display more than 27MHz may not work (spurious pixels and lines)
// With an ILI9163 display 27 MHz works OK.
// #define SPI_FREQUENCY 1000000
// #define SPI_FREQUENCY 5000000
// #define SPI_FREQUENCY 10000000
// #define SPI_FREQUENCY 20000000
// #define SPI_FREQUENCY 32000000
#define SPI_FREQUENCY 70000000
// Optional reduced SPI frequency for reading TFT
#define SPI_READ_FREQUENCY 20000000
// The XPT2046 requires a lower SPI clock rate of 2.5MHz so we define that here:
#define SPI_TOUCH_FREQUENCY 2500000

View file

@ -0,0 +1,6 @@
#ifndef MEMLLIB_HARDWARE_MEMLNAUT_COMMON_HPP
#define MEMLLIB_HARDWARE_MEMLNAUT_COMMON_HPP
#define MEMLLIB_VERSION "1.1.0"
#endif // MEMLLIB_HARDWARE_MEMLNAUT_COMMON_HPP

View file

@ -0,0 +1,4 @@
#include "display.hpp"
TFT_eSPI tft = TFT_eSPI(); // Invoke custom library

View file

@ -0,0 +1,94 @@
#ifndef __DISPLAY_H
#define __DISPLAY_H
#include <Arduino.h>
#include <TFT_eSPI.h>
#include <deque>
#define DISPLAY_MEM __not_in_flash("display")
extern TFT_eSPI tft; // Invoke custom library
class display {
public:
display() : textSprite(&tft),
status_text{ "NoValue", "NoValue", "NoValue",
"NoValue", "NoValue", "NoValue" } {
}
void setup() {
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
textSprite.setFreeFont(&FreeMono9pt7b);
// tft.setTextFont(4);
// tft.setTextColor(0xFC9F);
textSprite.createSprite(320, 40);
}
void post(String str) {
redraw = true;
lines.push_back(str);
if(lines.size() > 10) {
lines.pop_front();
}
}
void statusPost(String str, size_t pos) {
if (pos >= kNStatuses) {
return; // Invalid position
}
status_text[pos] = str;
status_redraw = true;
}
void update() {
if (redraw) {
redraw = false;
status_redraw = true;
tft.fillScreen(TFT_BLACK);
constexpr int32_t lineheight = 20;
// tft.fillRect(10, 10, 250, 30, TFT_BLUE);
// tft.fillRect(100, 100, 200, 100, TFT_RED);
for(size_t i=0; i < lines.size(); i++) {
textSprite.fillRect(0,0,320,20,TFT_BLACK);
textSprite.drawString(lines[i].c_str(), 0, 0);
textSprite.pushSprite(4,i*lineheight);
}
}
if (status_redraw) {
status_redraw = false;
tft.fillRect(0, 200, 320, 40, TFT_BLACK);
textSprite.fillRect(0,0,320,40,TFT_BLACK);
static constexpr unsigned int status_colours[kNStatuses] = {
TFT_WHITE, TFT_RED, TFT_GREEN,
TFT_MAGENTA, TFT_YELLOW, TFT_CYAN
};
static constexpr unsigned int status_x[kNStatuses] = {
0, 104, 208, 0, 104, 208
};
static constexpr unsigned int status_y[kNStatuses] = {
0, 0, 0, 20, 20, 20
};
for (unsigned int n = 0; n < kNStatuses; ++n) {
textSprite.setTextColor(status_colours[n], TFT_BLACK);
textSprite.drawString(status_text[n].c_str(), status_x[n], status_y[n]);
}
textSprite.pushSprite(4,200);
textSprite.setTextColor(TFT_WHITE, TFT_BLACK);
}
}
private:
std::deque<String> lines;
bool redraw=false;
bool status_redraw = false;
static constexpr size_t kNStatuses = 6;
String status_text[kNStatuses];
TFT_eSprite textSprite;
};
#endif

View file

@ -0,0 +1,164 @@
#ifndef __BARGRAPH_VIEW_HPP__
#define __BARGRAPH_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
class BarGraphView : public ViewBase {
public:
BarGraphView(String name, size_t nOutputs, int barwidth = 2, int colour = TFT_GREEN, float rangeLow=0.f, float rangeHigh=1.f)
: ViewBase(name), colour(colour), barwidth(barwidth), oldValues(nOutputs, 0.0f), newValues(nOutputs, 0.0f),
rangeLow(rangeLow), rangeHigh(rangeHigh), runningMax(nOutputs,0), runningMin(nOutputs,0)
{
}
void OnSetup() override {
offsetX = 5;
offsetY = 5;
barSectionWidth = (area.w - (2 * offsetX)) / static_cast<float>(newValues.size());
barSectionHeight = area.h - (2 * offsetY);
rangeTotal = rangeHigh - rangeLow;
rangeTotalInv = 1.f / rangeTotal;
}
void OnDisplay() override {
};
void setSpectrumColors(uint16_t low, uint16_t high) {
spectrumLow_ = low;
spectrumHigh_ = high;
useSpectrum_ = true;
}
void setNumDisplayBars(size_t n) {
if (n == 0 || n == newValues.size()) return;
newValues.assign(n, 0.f);
oldValues.assign(n, 0.f);
runningMax.assign(n, 0.f);
runningMin.assign(n, 0.f);
if (area.w > 0) {
barSectionWidth = (area.w - (2 * offsetX)) / static_cast<float>(n);
if (scr) scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLACK);
}
redraw();
}
void UpdateValues(const std::vector<float>& values, bool resetMinMax=false) {
size_t n = std::min(values.size(), newValues.size());
for(size_t i=0; i < n; i++) {
newValues[i] = values[i];
}
if (resetMinMax) {
runningMax = values;
runningMin = values;
}
redraw();
}
void OnDraw() override {
for(size_t i=0; i < newValues.size(); i++) {
float newVal = newValues[i];
// if (newVal < rangeLow) {
// newVal = rangeLow;
// } else if (newVal > rangeHigh) {
// newVal = rangeHigh;
// }
float normalizedNewValue = (newVal - rangeLow) * rangeTotalInv;
int newBarHeight = static_cast<int>(normalizedNewValue * barSectionHeight);
float oldVal = oldValues[i];
// if (newVal < rangeLow) {
// newVal = rangeLow;
// } else if (newVal > rangeHigh) {
// newVal = rangeHigh;
// }
float normalizedOldValue = (oldVal - rangeLow) * rangeTotalInv;
int oldBarHeight = static_cast<int>(normalizedOldValue * barSectionHeight);
int x = area.x + offsetX + static_cast<int>(i * barSectionWidth);
int newy = area.y + area.h - offsetY - newBarHeight;
int oldy = area.y + area.h - offsetY - oldBarHeight;
// if (newy < oldy) {
// scr->fillRect(x,oldy,barwidth,oldy-newy,TFT_BLACK);
// }else{
// scr->fillRect(x,newy,barwidth,newy-oldy,colour);
// }
// scr->drawLine(x,oldy, x+barwidth, oldy, TFT_BLACK);
// scr->drawLine(x,newy, x+barwidth, newy, colour);
uint16_t barColour = useSpectrum_
? lerpRGB565(spectrumLow_, spectrumHigh_, normalizedNewValue)
: static_cast<uint16_t>(colour);
scr->fillRect(x, oldy, barwidth, 4, TFT_BLACK);
scr->fillRect(x, newy, barwidth, 4, barColour);
// Update running max/min
//TODO: what happens after reset?
// if (newValues[i] > runningMax[i]) {
// float normalizedOldMaxValue = (runningMax[i] - rangeLow) * rangeTotalInv;
// int oldMaxBarHeight = static_cast<int>(normalizedOldMaxValue * barSectionHeight);
// int oldmaxy = area.y + area.h - offsetY - oldMaxBarHeight;
// scr->drawLine(x,oldmaxy, x+barwidth, oldmaxy, TFT_BLACK);
// runningMax[i] = newValues[i];
// normalizedOldMaxValue = (runningMax[i] - rangeLow) * rangeTotalInv;
// oldMaxBarHeight = static_cast<int>(normalizedOldMaxValue * barSectionHeight);
// oldmaxy = area.y + area.h - offsetY - oldMaxBarHeight;
// scr->drawLine(x,oldmaxy, x+barwidth, oldmaxy, TFT_PINK);
// }
// if (newValues[i] < runningMin[i]) {
// float normalizedOldMinValue = (runningMin[i] - rangeLow) * rangeTotalInv;
// int oldMinBarHeight = static_cast<int>(normalizedOldMinValue * barSectionHeight);
// int oldminy = area.y + area.h - offsetY - oldMinBarHeight;
// scr->drawLine(x,oldminy, x+barwidth, oldminy, TFT_BLACK);
// runningMin[i] = newValues[i];
// normalizedOldMinValue = (runningMin[i] - rangeLow) * rangeTotalInv;
// oldMinBarHeight = static_cast<int>(normalizedOldMinValue * barSectionHeight);
// oldminy = area.y + area.h - offsetY - oldMinBarHeight;
// scr->drawLine(x,oldminy, x+barwidth, oldminy, TFT_PINK);
// }
}
oldValues = newValues; // Update old values after drawing
drawnValues = true;
}
private:
static uint16_t lerpRGB565(uint16_t c1, uint16_t c2, float t) {
int r = ((c1 >> 11) & 0x1F) + static_cast<int>((static_cast<int>((c2 >> 11) & 0x1F) - static_cast<int>((c1 >> 11) & 0x1F)) * t);
int g = ((c1 >> 5) & 0x3F) + static_cast<int>((static_cast<int>((c2 >> 5) & 0x3F) - static_cast<int>((c1 >> 5) & 0x3F)) * t);
int b = ( c1 & 0x1F) + static_cast<int>((static_cast<int>( c2 & 0x1F) - static_cast<int>( c1 & 0x1F)) * t);
return (static_cast<uint16_t>(r) << 11) | (static_cast<uint16_t>(g) << 5) | static_cast<uint16_t>(b);
}
std::vector<float> newValues, oldValues;
int colour = TFT_GREEN;
int barwidth = 2;
bool useSpectrum_{false};
uint16_t spectrumLow_{TFT_GREEN};
uint16_t spectrumHigh_{TFT_BLUE};
float rangeLow = 0.0f;
float rangeHigh = 1.0f;
float rangeTotal=1.f;
float rangeTotalInv=1.f;
int offsetX, offsetY;
float barSectionWidth, barSectionHeight;
bool drawnValues = false;
std::vector<float> runningMax, runningMin;
};
#endif

View file

@ -0,0 +1,119 @@
#ifndef __BLOCK_SELECT_VIEW_HPP__
#define __BLOCK_SELECT_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include "ButtonView.hpp"
class BlockSelectView : public ViewBase {
public:
using OnSelectCallback = std::function<void(size_t)>;
BlockSelectView(String name, int _buttonColour_ = TFT_BLUE, size_t nButtons_=8, size_t buttonWidth_=60, size_t buttonHeight_=60, int fontColour_=TFT_WHITE, std::vector<String> buttonNames_ = {}, int buttonAltColour_=TFT_BLUE, uint8_t buttonFontNum_=4, int altFontColour_=TFT_MAROON, int32_t altBorderWidth_=3)
: ViewBase(name), buttonColour(_buttonColour_), buttonAltColour(buttonAltColour_),
nButtons(nButtons_), buttonWidth(buttonWidth_), buttonHeight(buttonHeight_), fontColour(fontColour_), altFontColour(altFontColour_), altBorderWidth(altBorderWidth_), buttonFontNum(buttonFontNum_)
{
rows = (nButtons > 4) ? 2 : 1;
cols = (nButtons + 1) / rows;
if (!buttonNames_.empty() && buttonNames_.size() == nButtons_) {
buttonNames = buttonNames_;
} else {
for(size_t i = 1; i <= nButtons; i++) {
buttonNames.push_back(String(i));
}
}
altColour.resize(nButtons, false);
}
void SetOnSelectCallback(OnSelectCallback _cb_) {
cb = _cb_;
}
void setAltColour(size_t index, bool on) {
altColour[index] = on;
}
void toggleAlt(size_t index) {
altColour[index] = !altColour[index];
buttons[index]->setFillColour(altColour[index] ? buttonAltColour : buttonColour);
buttons[index]->setFontColour(altColour[index] ? altFontColour : fontColour);
buttons[index]->setBorderWidth(altColour[index] ? altBorderWidth : 1);
}
void setAltState(size_t index, bool on) {
if (index >= buttons.size()) return;
altColour[index] = on;
buttons[index]->setFillColour(on ? buttonAltColour : buttonColour);
buttons[index]->setFontColour(on ? altFontColour : fontColour);
buttons[index]->setBorderWidth(on ? altBorderWidth : 1);
}
void OnSetup() override {
int idx=1;
for(int i=0; i < cols; i++) {
for(int j=0; j < rows; j++) {
if (idx <= nButtons) {
auto button = std::make_shared<ButtonView>(buttonNames[idx-1], idx, altColour[idx-1] ? buttonAltColour : buttonColour, fontColour, buttonFontNum);
rect bounds = { static_cast<int>(area.x + 10 + (i * (buttonWidth+10))), static_cast<int>(area.y + 10 + (j*(buttonHeight+10))), static_cast<int>(buttonWidth), static_cast<int>(buttonHeight) };
AddSubView(button, bounds);
button->SetReleaseCallback([this](size_t id) {
if (cb) {
cb(id);
}
});
buttons.push_back(button);
idx++;
}
}
}
}
void OnDraw() override {
TFT_eSprite textSprite(scr);
textSprite.createSprite(320, 20);
textSprite.setTextFont(2);
scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLACK);
constexpr int32_t lineheight = 20;
textSprite.setTextColor(TFT_WHITE, TFT_BLACK);
textSprite.fillRect(0,0,320,20,TFT_BLACK);
textSprite.drawString(msg, 3, 0);
textSprite.pushSprite(area.x,area.y + area.h - 25);
}
void updateButtonName(size_t idx, const String& newName) {
if (idx < buttons.size()) {
buttons[idx]->name_ = newName;
buttons[idx]->redraw();
}
}
void SetMessage(const String &__msg) {
msg = __msg;
redraw();
}
private:
std::vector<std::shared_ptr<ButtonView>> buttons;
int buttonColour;
int buttonAltColour;
OnSelectCallback cb = nullptr;
String msg;
size_t nButtons;
size_t rows;
size_t cols;
size_t buttonWidth = 50;
size_t buttonHeight = 50;
int fontColour = TFT_WHITE;
int altFontColour = TFT_MAROON;
int32_t altBorderWidth = 3;
std::vector<String> buttonNames;
std::vector<bool> altColour;
uint8_t buttonFontNum = 4;
};
#endif

View file

@ -0,0 +1,86 @@
#ifndef __BUTTON_VIEW_HPP__
#define __BUTTON_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
class ButtonView : public ViewBase {
public:
using ButtonCallback = std::function<void(size_t)>;
ButtonView(String name, size_t _id_, int _fillcolour_ = TFT_BLUE, int _fontcolour_ = TFT_WHITE, uint8_t fontNum_ = 4)
: ViewBase(name), fillColour(_fillcolour_), fontColour(_fontcolour_),
id(_id_), fontNum(fontNum_)
{
}
void SetReleaseCallback(ButtonCallback __callback) {
callback = __callback;
}
void OnSetup() override {
}
void setFillColour(int newCol) {
fillColour = newCol;
redraw();
}
void setFontColour(int newCol) {
fontColour = newCol;
redraw();
}
void setBorderWidth(int w) {
borderWidth = w;
redraw();
}
void OnDraw() override {
TFT_eSprite sprite(scr);
sprite.createSprite(area.w, area.h);
sprite.fillSprite(fillColour);
int32_t bw = pressed ? 1 : borderWidth;
int32_t col = pressed ? TFT_RED : TFT_WHITE;
for (int32_t i = 0; i < bw; i++) {
sprite.drawRect(i, i, area.w - 2*i, area.h - 2*i, col);
}
sprite.setTextColor(fontColour);
sprite.setTextFont(fontNum);
sprite.drawString(this->name_, 10, 10);
sprite.pushSprite(area.x, area.y);
// scr->drawString("1", area.x + 10, area.y + 10);
}
void OnTouchDown(size_t x, size_t y) override {
pressed = true;
redraw();
// Check if the touch is within the button area
}
void OnTouchUp(size_t x, size_t y) override {
if (callback) {
callback(id);
}
pressed = false;
redraw();
}
private:
int fillColour;
int fontColour;
size_t id;
ButtonCallback callback = nullptr;
bool pressed = false;
uint8_t fontNum = 4;
int32_t borderWidth = 1;
};
#endif

View file

@ -0,0 +1,407 @@
#ifndef __CC_SELECT_VIEW_HPP__
#define __CC_SELECT_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include <vector>
#include <map>
#include <algorithm>
#include <functional>
struct CCOption {
uint8_t num;
String name;
};
// Scrollable named-CC selector.
// Shows a list of CCOption entries; allows selecting up to maxActive_.
// Selection order is preserved — index in selectedCCs_ == NN output index.
// Navigate with rotary encoder (scroll) or touch (tap row). Press encoder to toggle.
class CCSelectView : public ViewBase {
public:
using OnChangeCallback = std::function<void(const std::vector<uint8_t>&)>;
using OnHomeChangeCallback = std::function<void()>;
using OnSaveCallback = std::function<void()>;
CCSelectView(size_t maxActive, String name = "MIDI CC Out")
: ViewBase(name), maxActive_(maxActive) { lastLiveDrawn_.assign(maxActive_, -999); }
void setOptions(std::vector<CCOption> opts) {
options_ = std::move(opts);
}
void setOnChangeCallback(OnChangeCallback cb) { cb_ = cb; }
void setOnHomeChangeCallback(OnHomeChangeCallback cb) { homeCb_ = cb; }
// Fires when the user leaves the page or exits focus, and only if something changed.
// Use this to persist to flash — avoids slow per-edit flash writes.
void setOnSaveCallback(OnSaveCallback cb) { saveCb_ = cb; }
// Clear the unsaved-changes flag (e.g. after programmatically applying a mapping, so
// it isn't treated as a user edit needing save-on-exit).
void resetDirty() { dirty_ = false; }
// Opt-in: show + allow editing the per-CC home value column. Off by default so
// modes that don't use home values render unchanged.
void setShowHome(bool v) { showHome_ = v; }
// Opt-in live-output column: point at an external array of the values actually being
// sent (one per NN output slot, [0..1]). Enables the "out" column.
void setLiveValues(const float* values) { liveValues_ = values; }
// Repaint only the changed live-output cells. Drive this each loop from the mode;
// it self-throttles and only redraws cells whose value changed (no flicker).
void refreshLiveColumn() {
if (!scr || liveValues_ == nullptr) return;
if (!IsVisible()) { liveWasVisible_ = false; return; }
unsigned long now = millis();
if (liveWasVisible_ && (now - lastLiveRefreshMs_) < kLiveRefreshMs) return;
lastLiveRefreshMs_ = now;
if (!liveWasVisible_) { // force full repaint when re-shown
std::fill(lastLiveDrawn_.begin(), lastLiveDrawn_.end(), -999);
liveWasVisible_ = true;
}
for (int i = 0; i < kVisibleRows; i++) {
size_t optIdx = scrollOffset_ + (size_t)i;
if (optIdx >= options_.size()) break;
int slot = slotOf(options_[optIdx].num);
if (slot <= 0) continue;
int val = (int)(clamp01(liveValues_[slot - 1]) * 127.f + 0.5f);
if (slot - 1 < (int)lastLiveDrawn_.size() && val == lastLiveDrawn_[slot - 1]) continue;
drawLiveCell(optIdx);
}
}
size_t getMaxActive() const { return maxActive_; }
const std::vector<uint8_t>& getSelectedCCs() const { return selectedCCs_; }
void setSelectedCCs(const std::vector<uint8_t>& ccs) {
selectedCCs_.clear();
for (uint8_t cc : ccs) {
if (selectedCCs_.size() >= maxActive_) break;
selectedCCs_.push_back(cc);
}
std::sort(selectedCCs_.begin(), selectedCCs_.end());
std::fill(lastLiveDrawn_.begin(), lastLiveDrawn_.end(), -999);
}
// Per-CC "home" values, in [0..1], keyed by CC number so they survive sorting.
// getHomeValues() returns them aligned to getSelectedCCs() (== NN output order).
std::vector<float> getHomeValues() const {
std::vector<float> out;
out.reserve(selectedCCs_.size());
for (uint8_t cc : selectedCCs_) {
auto it = homeByCC_.find(cc);
out.push_back(it != homeByCC_.end() ? it->second : 0.f);
}
return out;
}
void setHomeValues(const std::vector<float>& homes) {
for (size_t i = 0; i < selectedCCs_.size() && i < homes.size(); i++)
homeByCC_[selectedCCs_[i]] = clamp01(homes[i]);
}
// Set the home of the CC currently under the cursor (if it's selected), using
// soft pickup: when the cursor moves to a new CC the knob must first cross that
// CC's stored home value before it takes control (avoids value jumps).
// Returns true if a home value was actually changed.
bool setHomeForCursor(float v) {
if (cursorRow_ >= options_.size()) return false; // Done row
uint8_t cc = options_[cursorRow_].num;
if (slotOf(cc) == 0) return false; // not a selected CC
auto it = homeByCC_.find(cc);
float target = (it != homeByCC_.end()) ? it->second : 0.f;
if ((int)cursorRow_ != homePickupRow_) { // arm pickup for this CC
homePickupRow_ = (int)cursorRow_;
homePickupCaught_ = false;
lastHomeKnob_ = v;
}
if (!homePickupCaught_) {
constexpr float eps = 0.5f / 127.f;
bool crossed = ((v - target) <= 0.f) != ((lastHomeKnob_ - target) <= 0.f);
float dist = (v > target) ? (v - target) : (target - v);
if (crossed || dist <= eps) homePickupCaught_ = true;
}
lastHomeKnob_ = v;
if (!homePickupCaught_) return false; // still seeking — don't move it
homeByCC_[cc] = clamp01(v);
dirty_ = true;
drawHomeCell(cursorRow_); // partial redraw — just the value, no full-screen flicker
if (homeCb_) homeCb_();
return true;
}
void OnSetup() override {
// If no named options were provided, generate a generic numbered list (CC 1127)
if (options_.empty()) {
for (uint8_t i = 1; i <= 127; i++)
options_.push_back({i, "CC " + String(i)});
}
}
void OnDraw() override {
scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLACK);
// Header
scr->setTextColor(TFT_WHITE, TFT_BLACK);
scr->setTextFont(2);
String hdr = String(selectedCCs_.size()) + "/" + String(maxActive_) + " assigned";
scr->drawString(hdr, area.x + 4, area.y + 4);
// Right-side column labels: set (home) | out (live) | cc
scr->setTextFont(1);
scr->setTextColor(TFT_SILVER, TFT_BLACK);
if (showHome_) scr->drawString("home", area.x + area.w - 116, area.y + 8);
if (liveValues_) scr->drawString("out", area.x + area.w - 78, area.y + 8);
scr->drawString("cc", area.x + area.w - 40, area.y + 8);
scr->setTextFont(2);
// Rows (options + one trailing Done entry)
size_t totalItems = options_.size() + 1;
for (int i = 0; i < kVisibleRows; i++) {
size_t optIdx = scrollOffset_ + (size_t)i;
if (optIdx >= totalItems) break;
int ry = area.y + kHeaderH + i * kRowH;
bool isCursor = (optIdx == cursorRow_);
bool isDone = (optIdx == options_.size());
if (isDone) {
uint16_t bg = isCursor ? (uint16_t)TFT_DARKGREEN : (uint16_t)TFT_BLACK;
scr->fillRect(area.x, ry, area.w, kRowH - 1, bg);
scr->setTextFont(2);
scr->setTextColor(TFT_WHITE, bg);
scr->drawString("[ Done ]", area.x + 36, ry + 4);
continue;
}
int slot = slotOf(options_[optIdx].num);
bool isSel = slot > 0;
uint16_t bg = (isSel && isCursor) ? (uint16_t)0x0660 // bright green: selected + cursor
: isSel ? (uint16_t)0x0340 // dark green: selected
: isCursor ? (uint16_t)TFT_NAVY // navy: cursor only
: (uint16_t)TFT_BLACK;
scr->fillRect(area.x, ry, area.w, kRowH - 1, bg);
scr->setTextFont(2);
if (isSel) {
scr->setTextColor(TFT_YELLOW, bg);
scr->drawString("[" + String(slot) + "]", area.x + 2, ry + 4);
} else if (isCursor) {
scr->setTextColor(TFT_YELLOW, bg);
scr->drawString(" > ", area.x + 2, ry + 4);
}
scr->setTextColor(isSel ? TFT_WHITE : (isCursor ? TFT_YELLOW : TFT_SILVER), bg);
scr->drawString(options_[optIdx].name, area.x + 36, ry + 4);
// Home value column (0..127), shown for every CC when enabled.
// Drawn via drawHomeCell so the partial-update path renders identically.
if (showHome_) drawHomeCell(optIdx);
// Live output value column (0..127), shown for selected CCs when enabled.
if (liveValues_) drawLiveCell(optIdx);
scr->setTextColor(0x7BEF, bg);
scr->drawString("CC" + String(options_[optIdx].num), area.x + area.w - 40, ry + 4);
}
// Scroll indicator
if (totalItems > (size_t)kVisibleRows) {
int barH = area.h - kHeaderH;
int indicatorH = std::max(8, (int)(barH * kVisibleRows / (int)totalItems));
int indicatorY = area.y + kHeaderH +
(int)(scrollOffset_ * (barH - indicatorH) / (totalItems - kVisibleRows));
scr->fillRect(area.x + area.w - 4, area.y + kHeaderH, 4, barH, TFT_DARKGREY);
scr->fillRect(area.x + area.w - 4, indicatorY, 4, indicatorH, TFT_WHITE);
}
}
bool acceptsFocus() override { return true; }
bool setFocus() override {
redraw();
return ViewBase::setFocus();
}
void removeFocus() override {
ViewBase::removeFocus();
flushSave(); // exiting edit mode (e.g. "Done") — persist now
redraw();
}
void OnHide() override {
flushSave(); // navigated away from the page — persist now
}
void HandleRotaryEncChange(int inc) override {
size_t totalItems = options_.size() + 1;
if (inc > 0 && cursorRow_ + 1 < totalItems) {
cursorRow_++;
if (cursorRow_ >= scrollOffset_ + (size_t)kVisibleRows)
scrollOffset_ = cursorRow_ - kVisibleRows + 1;
} else if (inc < 0 && cursorRow_ > 0) {
cursorRow_--;
if (cursorRow_ < scrollOffset_)
scrollOffset_ = cursorRow_;
}
resetHomePickup(); // moving the cursor re-arms soft pickup
redraw();
}
void HandleRotaryEncSwitch() override {
if (cursorRow_ == options_.size()) {
removeFocus();
redraw();
} else {
toggleAt(cursorRow_);
}
}
void OnTouchUp(size_t tx, size_t ty) override {
size_t totalItems = options_.size() + 1;
for (int i = 0; i < kVisibleRows; i++) {
int ry = area.y + kHeaderH + i * kRowH;
if ((int)ty >= ry && (int)ty < ry + kRowH) {
size_t optIdx = scrollOffset_ + (size_t)i;
if (optIdx >= totalItems) return;
cursorRow_ = optIdx;
if (optIdx == options_.size()) {
removeFocus();
redraw();
} else {
toggleAt(optIdx);
}
return;
}
}
}
private:
static constexpr int kRowH = 28;
static constexpr int kHeaderH = 22;
static constexpr int kVisibleRows = 6;
size_t maxActive_;
std::vector<CCOption> options_;
std::vector<uint8_t> selectedCCs_; // order = NN output index
std::map<uint8_t, float> homeByCC_; // per-CC home value [0..1]
bool showHome_ = false; // render/edit the home column
// Soft-pickup state for gain-knob home editing
int homePickupRow_ = -1; // armed cursor row, -1 = not armed
bool homePickupCaught_ = false; // knob has crossed the stored value
float lastHomeKnob_ = -1.f; // previous knob value (crossing detection)
// Live-output column
const float* liveValues_ = nullptr; // external [0..1] values per NN slot
std::vector<int> lastLiveDrawn_; // last drawn 0..127 per slot (change tracking)
bool liveWasVisible_ = false;
unsigned long lastLiveRefreshMs_ = 0;
static constexpr unsigned long kLiveRefreshMs = 50; // ~20 Hz live readout
size_t scrollOffset_ = 0;
size_t cursorRow_ = 0;
OnChangeCallback cb_;
OnHomeChangeCallback homeCb_;
OnSaveCallback saveCb_;
bool dirty_ = false; // unsaved selection/home changes
static float clamp01(float v) { return v < 0.f ? 0.f : (v > 1.f ? 1.f : v); }
// Draw only the home-value cell for one option row, without clearing the screen.
// Used by OnDraw and as the partial-update path for live gain-knob edits (both run
// in MEMLNaut::loop(), so direct drawing here doesn't race the periodic Draw()).
void drawHomeCell(size_t optIdx) {
if (!scr || !showHome_) return;
if (optIdx >= options_.size()) return;
if (optIdx < scrollOffset_ || optIdx >= scrollOffset_ + (size_t)kVisibleRows) return;
uint8_t cc = options_[optIdx].num;
int slot = slotOf(cc);
bool isSel = slot > 0;
bool isCursor = (optIdx == cursorRow_);
// Match the row background used in OnDraw so the cell blends seamlessly.
uint16_t bg = (isSel && isCursor) ? (uint16_t)0x0660 // selected + cursor
: isSel ? (uint16_t)0x0340 // selected
: isCursor ? (uint16_t)TFT_NAVY // cursor only
: (uint16_t)TFT_BLACK;
int ry = area.y + kHeaderH + (int)(optIdx - scrollOffset_) * kRowH;
int hx = area.x + area.w - 116;
auto it = homeByCC_.find(cc);
float home = (it != homeByCC_.end()) ? it->second : 0.f;
int homeInt = (int)(home * 127.f + 0.5f);
// High-contrast on every background: white when set, red while the gain knob
// is still seeking pickup, dim grey for unselected CCs.
bool seeking = isSel && isCursor && !homePickupCaught_;
uint16_t col = seeking ? (uint16_t)TFT_RED
: isSel ? (uint16_t)TFT_WHITE
: (uint16_t)TFT_DARKGREY;
scr->fillRect(hx, ry, 36, kRowH - 1, bg);
scr->setTextFont(2);
scr->setTextColor(col, bg);
scr->drawString(String(homeInt), hx, ry + 4);
}
// Draw only the live-output cell for one option row (selected CCs only).
// Shares the run-context rules of drawHomeCell (callable outside OnDraw).
void drawLiveCell(size_t optIdx) {
if (!scr || liveValues_ == nullptr) return;
if (optIdx >= options_.size()) return;
if (optIdx < scrollOffset_ || optIdx >= scrollOffset_ + (size_t)kVisibleRows) return;
uint8_t cc = options_[optIdx].num;
int slot = slotOf(cc);
if (slot <= 0) return; // only selected CCs have a live output value
bool isCursor = (optIdx == cursorRow_);
uint16_t bg = isCursor ? (uint16_t)0x0660 : (uint16_t)0x0340; // selected row bg
int ry = area.y + kHeaderH + (int)(optIdx - scrollOffset_) * kRowH;
int lx = area.x + area.w - 78;
int val = (int)(clamp01(liveValues_[slot - 1]) * 127.f + 0.5f);
scr->fillRect(lx, ry, 36, kRowH - 1, bg);
scr->setTextFont(2);
scr->setTextColor(TFT_CYAN, bg);
scr->drawString(String(val), lx, ry + 4);
if (slot - 1 < (int)lastLiveDrawn_.size()) lastLiveDrawn_[slot - 1] = val;
}
// Returns 1-based slot number, 0 if not selected
int slotOf(uint8_t cc) const {
for (size_t i = 0; i < selectedCCs_.size(); i++)
if (selectedCCs_[i] == cc) return (int)i + 1;
return 0;
}
void toggleAt(size_t optIdx) {
uint8_t ccNum = options_[optIdx].num;
auto it = std::find(selectedCCs_.begin(), selectedCCs_.end(), ccNum);
if (it != selectedCCs_.end()) {
selectedCCs_.erase(it);
homeByCC_.erase(ccNum);
} else {
if (selectedCCs_.size() < maxActive_) {
selectedCCs_.push_back(ccNum);
std::sort(selectedCCs_.begin(), selectedCCs_.end());
homeByCC_[ccNum] = 0.f; // default home
}
}
resetHomePickup(); // selection changed — re-arm soft pickup
std::fill(lastLiveDrawn_.begin(), lastLiveDrawn_.end(), -999);
dirty_ = true;
redraw();
if (cb_) cb_(selectedCCs_);
}
void resetHomePickup() {
homePickupRow_ = -1;
homePickupCaught_ = false;
}
// Persist (via saveCb_) only if something changed since the last save.
void flushSave() {
if (dirty_ && saveCb_) saveCb_();
dirty_ = false;
}
};
#endif // __CC_SELECT_VIEW_HPP__

View file

@ -0,0 +1,219 @@
#include "DisplayDriver.hpp"
void DisplayDriver::Setup() {
// Clear screen
Serial.println("display setup");
Serial.println("init tft...");
tft_.init();
tft_.setRotation(1);
tft_.fillScreen(TFT_BLACK);
tft_initialized_ = true;
Serial.println("display init");
// Set up touch
tft_.setTouch(calData_);
isTouchPressed_ = false;
Serial.println("touch init");
// Set up grid dimensions
screenWidth_ = tft_.width();
screenHeight_ = tft_.height();
grid_.widthElements = kGridWidthElements;
grid_.heightElements = kGridHeightElements;
grid_.widthStep = screenWidth_ / grid_.widthElements;
grid_.heightStep = screenHeight_ / grid_.heightElements;
// Top bar is drawn directly in Draw() (no sprite buffers) to save RAM.
tft_.fillScreen(TFT_BLACK);
mainArea = {0, topBarHeight + 5, screenWidth_, screenHeight_ - topBarHeight - 5};
// Set up views
currentViewIndex_ = 0;
for (auto &view : views_) {
view->SetGrid(grid_);
view->Setup(&tft_, mainArea);
}
// Set up internal view
// Trigger initial redraw
redraw_internal_ = true;
}
void DisplayDriver::Draw() {
// Serial.println("display draw");
lastDrawTime_ = millis();
// Check if any of the views need redrawing
bool needRedraw = false;
if (redraw_internal_) {
// Clear screen
tft_.fillScreen(TFT_BLACK);
// tft_.setTextColor(TFT_WHITE);
tft_.fillRect(0, 0, tft_.width(), 30, TFT_WHITE);
// Clear the redraw flag now that screen is cleared
// This allows rapid view changes while ensuring old content is removed
redraw_internal_ = false;
// tft_.setFreeFont(&FreeSansBoldOblique24pt7b);
// tft_.setTextFont(4);
// Top bar drawn directly onto the (already white-filled) bar — no sprites.
tft_.setTextFont(4);
tft_.setTextColor(TFT_BLUE, TFT_WHITE);
tft_.setTextDatum(TL_DATUM);
if (dialogView_) {
// Dialog active: no nav arrows, show dialog title
tft_.drawString(dialogView_->GetName().c_str(), 43, 3);
dialogView_->redraw();
} else {
// Back arrow if not on first view
if (currentViewIndex_ > 0) {
tft_.drawString("<", 3, 3);
}
// Forward arrow if not on last view
if (currentViewIndex_ < views_.size() - 1) {
tft_.drawString(">", tft_.width() - 27, 3);
}
// Title of current view, between the arrows
if (currentViewIndex_ < views_.size()) {
tft_.drawString(views_[currentViewIndex_]->GetName().c_str(), 43, 3);
} else {
tft_.drawString("No View", 43, 3);
}
views_[currentViewIndex_]->redraw();
}
}
if (dialogView_) {
dialogView_->Draw();
} else if (currentViewIndex_ < views_.size()) {
views_[currentViewIndex_]->Draw();
}
}
void DisplayDriver::NavigateToView(const std::shared_ptr<ViewBase>& target) {
auto it = std::find(views_.begin(), views_.end(), target);
if (it == views_.end()) return;
size_t targetIndex = static_cast<size_t>(it - views_.begin());
if (targetIndex == currentViewIndex_) return;
views_[currentViewIndex_]->setVisible(false);
views_[currentViewIndex_]->removeFocus();
views_[currentViewIndex_]->OnHide();
currentViewIndex_ = targetIndex;
views_[currentViewIndex_]->setVisible(true);
views_[currentViewIndex_]->OnDisplay();
redraw_internal_ = true;
}
void DisplayDriver::ShowDialog(const std::shared_ptr<ViewBase>& dialog) {
dialogView_ = dialog;
dialog->OnDisplay();
redraw_internal_ = true;
}
void DisplayDriver::DismissDialog() {
if (dialogView_) {
dialogView_->OnHide();
dialogView_ = nullptr;
redraw_internal_ = true;
}
}
void DisplayDriver::ChangeView(int delta) {
if (dialogView_) return;
if (!redraw_internal_) { //wait until the current redraw is finished
auto lastViewIndex = currentViewIndex_;
bool viewChange = false;
if (delta < 0 && currentViewIndex_ > 0) {
currentViewIndex_--;
viewChange = true;
} else if (delta > 0 && currentViewIndex_ < views_.size() - 1) {
currentViewIndex_++;
viewChange = true;
}
if (viewChange) {
// If view changed, redraw the new view
redraw_internal_ = true;
views_[lastViewIndex]->setVisible(false);
views_[lastViewIndex]->removeFocus();
views_[lastViewIndex]->OnHide(); // Call OnHide for the old view
views_[currentViewIndex_]->setVisible(true);
views_[currentViewIndex_]->OnDisplay(); // Call OnDisplay for the new view
}
}
}
void DisplayDriver::PollTouch() {
// lastTouchTime_ = millis();
uint16_t x, y;
bool pressed = tft_.getTouch(&x, &y, 20);
if(pressed) {
lastTouchX = x;
lastTouchY = y;
}
if (currentViewIndex_ < views_.size()) {
bool viewChange = false;
if (pressed && !isTouchPressed_) {
auto lastViewIndex = currentViewIndex_;
// If within first row, handle navigation
if (y <= topBarHeight) {
// if (x < leftButton.width() && currentViewIndex_ > 0) {
// // Navigate to previous view
// currentViewIndex_--;
// redraw_internal_ = true;
// viewChange = true;
// }
// else if (x > tft_.width() - rightButton.width() && currentViewIndex_ < views_.size() - 1) {
// // Navigate to next view
// currentViewIndex_++;
// redraw_internal_ = true;
// viewChange = true;
// }
if (!dialogView_) {
constexpr int kNavButtonWidth = 30; // nav-arrow touch zone (was sprite width)
if (x < kNavButtonWidth) {
ChangeView(-1);
}
else if (x > tft_.width() - kNavButtonWidth) {
ChangeView(1);
}
}
} else {
auto& activeView = dialogView_ ? dialogView_ : views_[currentViewIndex_];
activeView->HandleTouch(lastTouchX, lastTouchY);
}
// if (viewChange) {
// // If view changed, redraw the new view
// views_[lastViewIndex]->setVisible(false);
// views_[lastViewIndex]->OnHide(); // Call OnHide for the old view
// views_[currentViewIndex_]->setVisible(true);
// views_[currentViewIndex_]->OnDisplay(); // Call OnDisplay for the new view
// }
isTouchPressed_ = true;
} else if (isTouchPressed_) {
//drag event
}
if (!pressed && isTouchPressed_) {
// If touch was released, handle release
// views_[currentViewIndex_]->HandleRelease();
// If released, check if any button was released
auto& activeView = dialogView_ ? dialogView_ : views_[currentViewIndex_];
activeView->HandleTouchRelease(lastTouchX, lastTouchY);
Serial.println("Touch released");
Serial.print("x: ");
Serial.print(x);
Serial.print(", y: ");
Serial.println(y);
isTouchPressed_ = false;
}
}
}

View file

@ -0,0 +1,124 @@
#ifndef __DISPLAY_DRIVER_HPP__
#define __DISPLAY_DRIVER_HPP__
#include "View.hpp"
#include <vector>
#include <memory>
#include <algorithm>
#include <cstdint>
#include <cstddef>
#include <TFT_eSPI.h>
#include <TFT_eWidget.h>
class DisplayDriver {
public:
DisplayDriver() {}
void Setup();
void Draw();
inline void AddView(const std::shared_ptr<ViewBase> &view)
{
views_.push_back(view);
view->SetGrid(grid_);
if (tft_initialized_) {
view->Setup(&tft_, mainArea);
}
redraw_internal_ = true;
}
inline void RegisterDialog(const std::shared_ptr<ViewBase>& view) {
view->SetGrid(grid_);
if (tft_initialized_) {
view->Setup(&tft_, mainArea);
}
}
inline void InsertViewAfter(const std::shared_ptr<ViewBase> &existingView, const std::shared_ptr<ViewBase> &newView)
{
auto it = std::find(views_.begin(), views_.end(), existingView);
if (it != views_.end()) {
views_.insert(it + 1, newView);
newView->SetGrid(grid_);
if (tft_initialized_) {
newView->Setup(&tft_, mainArea);
}
redraw_internal_ = true;
}
}
void PollTouch();
unsigned long GetLastTouchTime() const { return lastTouchTime_; }
unsigned long GetLastDrawTime() const { return lastDrawTime_; }
void ChangeView(int delta);
void NavigateToView(const std::shared_ptr<ViewBase>& target);
void ShowDialog(const std::shared_ptr<ViewBase>& dialog);
void DismissDialog();
void RotaryIncEvent(int delta) {
if (dialogView_) {
dialogView_->HandleRotaryEncChange(delta);
return;
}
if (currentViewIndex_ < views_.size()) {
auto& currentView = views_[currentViewIndex_];
if (currentView->isFocused()) {
currentView->HandleRotaryEncChange(delta);
}else{
ChangeView(delta);
}
}
}
void RotarySwitchEvent() {
if (dialogView_) {
dialogView_->HandleRotaryEncSwitch();
return;
}
if (currentViewIndex_ < views_.size()) {
auto& currentView = views_[currentViewIndex_];
if (currentView->isFocused()) {
currentView->HandleRotaryEncSwitch();
} else {
if (currentView->setFocus()) {
}
}
}
}
private:
// Internal TFT hardware instance
TFT_eSPI tft_;
// Views
std::vector<std::shared_ptr<ViewBase>> views_;
size_t currentViewIndex_;
std::shared_ptr<ViewBase> dialogView_{nullptr};
// Calibration
uint16_t calData_[5] = { 421, 3470, 270, 3492, 7 };
// Grid
static constexpr size_t kGridWidthElements = 16;
static constexpr size_t kGridHeightElements = 12;
int screenWidth_;
int screenHeight_;
GridDef grid_;
bool redraw_internal_{false};
unsigned long lastTouchTime_{0};
unsigned long lastDrawTime_{0};
bool tft_initialized_{false};
bool isTouchPressed_{false};
//top bar (drawn directly to the TFT — no sprite buffers, saves ~14 KB RAM)
rect mainArea;
int lastTouchX, lastTouchY;
const int topBarHeight = 30;
};
#endif // __DISPLAY_DRIVER_HPP__

View file

@ -0,0 +1,161 @@
#ifndef __GRAPH_VIEW_HPP__
#define __GRAPH_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
template<size_t NPOINTS=128>
class GraphView : public ViewBase {
public:
TFT_eSprite *sprite = nullptr;
TFT_eSprite *sprTitle = nullptr;
TFT_eSprite *sprMax = nullptr;
GraphView(String name, int _fillcolour_ = TFT_BLUE)
: ViewBase(name), fillColour(_fillcolour_)
{
}
void OnSetup() override {
graphHeight = area.h - 20;
xstep = static_cast<float>(area.w) / static_cast<float>(NPOINTS - 1);
sprite = new TFT_eSprite(scr);
sprite->createSprite(area.w, graphHeight);
sprTitle = new TFT_eSprite(scr);
sprTitle->createSprite(100,20);
sprMax = new TFT_eSprite(scr);
sprMax->createSprite(100,15);
sprTitle->fillSprite(TFT_BLACK);
sprTitle->setTextColor(TFT_SILVER, TFT_BLACK);
sprTitle->setTextFont(2);
sprTitle->drawString(this->name_, 0, 0);
}
void OnDraw() override {
// scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLUE);
// if (pressed) {
// scr->drawRect(area.x, area.y, area.w, area.h, TFT_RED);
// } else {
// scr->drawRect(area.x, area.y, area.w, area.h, TFT_WHITE);
// }
// scr->setTextColor(TFT_WHITE);
// scr->setTextFont(4);
// scr->drawString(this->name_, area.x + 10, area.y + 10);
// scr->drawString("1", area.x + 10, area.y + 10);
// for(size_t i = 0; i < NPOINTS-1; i++) {
// scr->drawLine(oldGraphPoints[i].x, oldGraphPoints[i].y, oldGraphPoints[i+1].x, oldGraphPoints[i+1].y, TFT_BLACK);
// }
sprite->fillSprite(TFT_BLACK);
for(size_t i = 0; i < NPOINTS-1; i++) {
sprite->drawLine(graphPoints[i].x, graphPoints[i].y, graphPoints[i+1].x, graphPoints[i+1].y, fillColour);
}
sprite->pushSprite(area.x, area.y+20);
if (redrawMax) {
sprMax->fillSprite(TFT_BLACK);
sprMax->setTextColor(TFT_SILVER, TFT_BLACK);
sprMax->setTextFont(1);
sprMax->drawString((String("max: ") + String(ymax)).c_str(), 0, 0);
sprMax->pushSprite(area.x+area.w-110, area.y);
redrawMax = false;
}
if (redrawTitle) {
sprTitle->pushSprite(area.x, area.y);
redrawTitle = false;
}
sprTitle->pushSprite(area.x, area.y);
}
void OnTouchDown(size_t x, size_t y) override {
}
void OnTouchUp(size_t x, size_t y) override {
}
void addDataPoint(float value) {
if (IsVisible()) {
dataPoints[dataPointIndex] = value;
ymax = -std::numeric_limits<float>::infinity();
ymin = std::numeric_limits<float>::infinity();
for(size_t i = 0; i < NPOINTS; i++) {
if (dataPoints[i] > ymax) {
ymax = dataPoints[i];
}
// if (dataPoints[i] < xmin) {
// xmin = dataPoints[i];
// }
}
redrawMax = oldyMax != ymax;
oldyMax = ymax;
ymin = 0.f;
yrange = ymax - ymin;
if (yrange < 0.00001f) {
yrange = 0.00001f;
}
yrangeInv = 1.f / yrange;
for(size_t i = 0; i < NPOINTS; i++) {
oldGraphPoints[i] = {graphPoints[i].x, graphPoints[i].y};
size_t index = (dataPointIndex + i + 1) % NPOINTS;
int graphX = static_cast<int>(static_cast<float>(i) * xstep);
int graphY = graphHeight - static_cast<int>((dataPoints[index]-ymin) * yrangeInv * graphHeight);
graphPoints[i] = { graphX, graphY };
}
dataPointIndex = (dataPointIndex + 1) % NPOINTS;
// for(size_t i=0; i < 10; i++) {
// Serial.printf("(%d,%d),", oldGraphPoints[i].x, oldGraphPoints[i].y);
// }
// Serial.println(" ");
// for(size_t i=0; i < 10; i++) {
// Serial.printf("(%d,%d),", graphPoints[i].x, graphPoints[i].y);
// }
// Serial.println("\n --------- ");
redraw();
}
}
void OnDisplay() override {
ViewBase::OnDisplay();
for(size_t i = 0; i < NPOINTS; i++) {
graphPoints[i] = { static_cast<int>(i*xstep), 0 };
}
redrawMax = true;
redrawTitle = true;
redraw();
};
private:
struct Point {
int x=0;
int y=0;
};
int fillColour;
std::array<float, NPOINTS> dataPoints{0.0f};
std::array<Point, NPOINTS> graphPoints{}, oldGraphPoints{};
size_t dataPointIndex = 0;
float xstep=1;
float ymax, ymin,yrange, yrangeInv;
float graphHeight = 1;
float oldyMax = 0.f;
bool redrawMax=true;
bool redrawTitle=true;
};
#endif

View file

@ -0,0 +1,63 @@
#ifndef __MESSAGE_VIEW_HPP__
#define __MESSAGE_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include <deque>
class MessageView : public ViewBase {
public:
MessageView(String name)
: ViewBase(name)
{}
void OnSetup() override {
}
void OnDraw() override {
TFT_eSprite textSprite(scr);
textSprite.createSprite(320, 20);
textSprite.setTextFont(2);
scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLACK);
constexpr int32_t lineheight = 20;
textSprite.setTextColor(TFT_WHITE, TFT_BLACK);
for(size_t i=0; i < lines.size(); i++) {
textSprite.fillRect(0,0,320,20,TFT_BLACK);
textSprite.drawString(lines[i].c_str(), 0, 0);
textSprite.pushSprite(area.x + 10,area.y + (i*lineheight));
}
}
void post(String str) {
lines.push_back(str);
if(lines.size() > maxLines) {
lines.pop_front();
}
redraw();
}
void setMaxLines(size_t max) {
maxLines = max;
if (lines.size() > maxLines) {
lines.resize(maxLines);
}
redraw();
}
void setLineWidth(int width) {
lineWidth = width;
redraw();
}
private:
std::deque<String> lines;
size_t maxLines = 9; // Maximum number of lines to display
int lineWidth = 320; // Width of each line in pixels
};
#endif

View file

@ -0,0 +1,118 @@
#ifndef __NAME_INPUT_VIEW_HPP__
#define __NAME_INPUT_VIEW_HPP__
#include "View.hpp"
#include "ButtonView.hpp"
#include <functional>
class NameInputView : public ViewBase {
public:
using ConfirmCallback = std::function<void(const String&)>;
using CancelCallback = std::function<void()>;
NameInputView(String name) : ViewBase(name) {}
void setCallbacks(ConfirmCallback confirm, CancelCallback cancel) {
onConfirm = confirm;
onCancel = cancel;
}
void reset(const String& initial = "") {
currentName = initial.substring(0, 5);
redraw();
}
void OnSetup() override {
static const char kChars[20] = {
'A','B','C','D','E',
'F','G','H','I','J',
'0','1','2','3','4',
'5','6','7','8','9'
};
constexpr int kBtnW = 60;
constexpr int kBtnH = 30;
constexpr int kColGap = 4;
constexpr int kRowGap = 3;
constexpr int kRowTop = 37; // below filename display
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 5; c++) {
int idx = r * 5 + c;
String label = String(kChars[idx]);
auto btn = std::make_shared<ButtonView>(label, static_cast<size_t>(idx + 1), TFT_DARKGREY, TFT_WHITE, 2);
rect bounds = {
area.x + 2 + c * (kBtnW + kColGap),
area.y + kRowTop + r * (kBtnH + kRowGap),
kBtnW,
kBtnH
};
AddSubView(btn, bounds);
btn->SetReleaseCallback([this, label](size_t) {
if (currentName.length() < 5) {
currentName += label;
redraw();
}
});
}
}
// Bottom control row
constexpr int kCtrlY = 170;
constexpr int kCtrlH = 26;
auto delBtn = std::make_shared<ButtonView>("DEL", 21, TFT_ORANGE, TFT_WHITE, 2);
AddSubView(delBtn, {area.x + 2, area.y + kCtrlY, 90, kCtrlH});
delBtn->SetReleaseCallback([this](size_t) {
if (currentName.length() > 0) {
currentName.remove(currentName.length() - 1);
redraw();
}
});
auto cancelBtn = std::make_shared<ButtonView>("CANCEL", 22, TFT_RED, TFT_WHITE, 2);
AddSubView(cancelBtn, {area.x + 115, area.y + kCtrlY, 90, kCtrlH});
cancelBtn->SetReleaseCallback([this](size_t) {
if (onCancel) onCancel();
});
auto okBtn = std::make_shared<ButtonView>("OK", 23, TFT_DARKGREEN, TFT_WHITE, 2);
AddSubView(okBtn, {area.x + 228, area.y + kCtrlY, 90, kCtrlH});
okBtn->SetReleaseCallback([this](size_t) {
if (onConfirm) onConfirm(currentName);
});
}
void OnDraw() override {
scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLACK);
// Filename display: 5 boxes near the top
constexpr int kBoxW = 40;
constexpr int kBoxH = 30;
constexpr int kBoxGap = 4;
const int totalBoxW = 5 * kBoxW + 4 * kBoxGap;
const int startX = area.x + (area.w - totalBoxW) / 2;
const int startY = area.y + 3;
for (int i = 0; i < 5; i++) {
int bx = startX + i * (kBoxW + kBoxGap);
scr->drawRect(bx, startY, kBoxW, kBoxH, TFT_WHITE);
if (i < (int)currentName.length()) {
scr->setTextColor(TFT_YELLOW, TFT_BLACK);
scr->setTextFont(4);
scr->drawChar(currentName[i], bx + 10, startY + 5);
} else {
scr->setTextColor(TFT_DARKGREY, TFT_BLACK);
scr->setTextFont(2);
scr->drawChar('_', bx + 14, startY + 9);
}
}
}
private:
String currentName;
ConfirmCallback onConfirm;
CancelCallback onCancel;
};
#endif // __NAME_INPUT_VIEW_HPP__

View file

@ -0,0 +1,77 @@
#ifndef __RLSTATSVIEW_VIEW_HPP__
#define __RLSTATSVIEW_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include "GraphView.hpp"
class RLStatsView : public ViewBase {
public:
static constexpr size_t NPOINTS = 50;
RLStatsView(String name)
: ViewBase(name)
{
}
void OnSetup() override {
// rect bounds = { area.x + 10 + (i * 60), area.y + 10 + (j*60), 50, 50 };
graphActorGNorm = std::make_shared<GraphView<NPOINTS>>("Loss", TFT_BLUE);
// graphCriticLoss = std::make_shared<GraphView<NPOINTS>>("Critic Loss", TFT_RED);
AddSubView(graphActorGNorm, { area.x + 10, area.y + 10, 300, 90 });
// AddSubView(graphCriticLoss, { area.x + 10, area.y + 10+100, 300, 90 });
}
void OnDisplay() override {
};
void OnDraw() override {
// TFT_eSprite textSprite(scr);
// textSprite.createSprite(120, 20);
// textSprite.setTextFont(2);
// auto drawText = [&textSprite, this](size_t x, size_t y, const String& item, const int colour) {
// scr->fillRect(area.x + x,area.y+y,120,20,TFT_BLACK);
// textSprite.fillSprite(TFT_BLACK);
// textSprite.setTextColor(colour, TFT_BLACK);
// textSprite.drawString(item.c_str(), 0, 0);
// textSprite.pushSprite(area.x + x,area.y + y);
// };
// constexpr int32_t lineheight = 20;
// drawText(10, 1 * lineheight, "Actor gnorm:", itemColour);
// drawText(150, 1 * lineheight, String(actorGradNorm).c_str(), valueColour);
// drawText(10, 2 * lineheight, "Critic Loss:", itemColour);
// drawText(150, 2 * lineheight, String(criticLoss).c_str(), valueColour);
}
void setLoss(float v) {
// actorGradNorm = v;
graphActorGNorm->addDataPoint(v);
redraw();
}
void setCriticLoss(float loss) {
// criticLoss = loss;
graphCriticLoss->addDataPoint(loss);
redraw();
}
private:
const int itemColour = TFT_WHITE;
const int valueColour = TFT_YELLOW;
float actorGradNorm = 0.f;
float criticLoss = 0.f;
// #define GRAPHSIZE 100
// std::array<float, GRAPHSIZE> histGnorm, histCriticLoss;
// size_t indexGnorm=0, indexCriticLoss=0;
std::shared_ptr<GraphView<NPOINTS>> graphActorGNorm, graphCriticLoss;
};
#endif

View file

@ -0,0 +1,106 @@
#ifndef __RL_VIEW_HPP__
#define __RL_VIEW_HPP__
#include "View.hpp"
#include "BarGraphView.hpp"
class RLView : public ViewBase {
public:
static constexpr int kStatusBarHeight = 20;
RLView(String name, size_t nOutputs, int barwidth = 2, int colour = TFT_GREEN,
float rangeLow = 0.f, float rangeHigh = 1.f)
: ViewBase(name)
, barGraph(std::make_shared<BarGraphView>(name, nOutputs, barwidth, colour, rangeLow, rangeHigh))
{}
void OnSetup() override {
rect barArea = getBarGraphArea();
AddSubView(barGraph, barArea);
barGraph->setSpectrumColors(TFT_GREEN, TFT_BLUE);
}
// View re-shown (driver cleared the screen): repaint every field.
void OnDisplay() override {
lossDirty_ = countsDirty_ = actionDirty_ = true;
needRedraw_ = true;
for (auto& subview : subviews) subview->OnDisplay();
}
void OnDraw() override {
// No field marked => this OnDraw was triggered by an external full repaint
// (driver fillScreen + redraw()), so redraw all three fields. Otherwise only
// the field whose value changed is repainted, avoiding whole-line flicker.
if (!lossDirty_ && !countsDirty_ && !actionDirty_)
lossDirty_ = countsDirty_ = actionDirty_ = true;
const int barY = area.y + area.h - kStatusBarHeight;
scr->setTextFont(1);
scr->setTextColor(TFT_WHITE, TFT_BLACK);
if (lossDirty_) {
scr->fillRect(area.x, barY, 100, kStatusBarHeight, TFT_BLACK);
scr->drawString(("L:" + String(loss_, 4)).c_str(), area.x + 2, barY + 4);
lossDirty_ = false;
}
if (countsDirty_) {
scr->fillRect(area.x + 100, barY, 100, kStatusBarHeight, TFT_BLACK);
scr->drawString(("y:" + String(posCount_) + " n:" + String(negCount_)).c_str(), area.x + 100, barY + 4);
countsDirty_ = false;
}
if (actionDirty_) {
scr->fillRect(area.x + 200, barY, area.w - 200, kStatusBarHeight, TFT_BLACK);
scr->drawString(lastAction_.c_str(), area.x + 200, barY + 4);
actionDirty_ = false;
}
}
void UpdateValues(const std::vector<float>& values, bool resetMinMax = false) {
barGraph->UpdateValues(values, resetMinMax);
}
// Safe to call from ISR — only updates state and sets dirty/redraw flags, no direct SPI.
// Each setter marks only its own field so OnDraw repaints just that part of the bar.
void setLoss(float v) {
if (v != loss_) { loss_ = v; lossDirty_ = true; needRedraw_ = true; }
}
void setMemoryCounts(size_t pos, size_t neg) {
if (pos != posCount_ || neg != negCount_) {
posCount_ = pos;
negCount_ = neg;
countsDirty_ = true;
needRedraw_ = true;
}
}
void setLastAction(const String& a) {
if (a != lastAction_) { lastAction_ = a; actionDirty_ = true; needRedraw_ = true; }
}
void setNoiseActive(bool active) {
if (active != noiseActive_) {
noiseActive_ = active;
if (active) {
barGraph->setSpectrumColors(TFT_RED, TFT_YELLOW);
} else {
barGraph->setSpectrumColors(TFT_GREEN, TFT_BLUE);
}
barGraph->redraw();
}
}
protected:
virtual rect getBarGraphArea() const {
return {area.x, area.y, area.w, area.h - kStatusBarHeight};
}
private:
std::shared_ptr<BarGraphView> barGraph;
float loss_{0.f};
size_t posCount_{0};
size_t negCount_{0};
String lastAction_{""};
bool noiseActive_{false};
bool lossDirty_{true};
bool countsDirty_{true};
bool actionDirty_{true};
};
#endif // __RL_VIEW_HPP__

View file

@ -0,0 +1,152 @@
#ifndef __ROTARY_SELECT_VIEW_HPP__
#define __ROTARY_SELECT_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
class RotarySelectView : public ViewBase {
public:
TFT_eSprite *sprite = nullptr;
TFT_eSprite *sprTitle = nullptr;
TFT_eSprite *sprMax = nullptr;
using NewSelectionCallback = std::function<void(size_t)>;
RotarySelectView(String name, int _fillcolour_ = TFT_BLUE)
: ViewBase(name), fillColour(_fillcolour_)
{
}
void setNewSelectionCallback(NewSelectionCallback cb) {
newSelCB = cb;
}
void OnSetup() override {
if (options.empty()) {
options.push_back("VoiceSpace 1");
options.push_back("VoiceSpace 2");
options.push_back("VoiceSpace 3");
options.push_back("VoiceSpace 4");
options.push_back("VoiceSpace 5");
options.push_back("VoiceSpace 6");
options.push_back("VoiceSpace 7");
options.push_back("VoiceSpace 8");
}
}
void OnDraw() override {
scr->drawLine(area.x, area.y, area.x, area.y + area.h, isFocused() ? TFT_GREEN : TFT_BLUE);
TFT_eSprite textSprite(scr);
constexpr int32_t lineheight = 25;
textSprite.createSprite(area.w-50, lineheight);
constexpr size_t sizes[] = {1,2,2,2,1};
constexpr size_t heights[] = {15,25,25,25,15};
constexpr size_t indents[] = {5,10,15,10,5};
constexpr size_t colours[] = {TFT_SILVER, TFT_SILVER, TFT_WHITE, TFT_SILVER, TFT_SILVER};
// Serial.printf("RotarySelectView::OnDraw called, hasFocus=%d\n", isFocused());
int heightAccum = 0;
for(int i=0; i < 5; i++) {
int itemIndex = selectedIndex - 2 + i;
textSprite.fillSprite(TFT_BLACK);
if (itemIndex < 0 || itemIndex >= options.size()) {
//
}else{
if (isFocused() && (itemIndex == selectedIndex)) {
textSprite.setTextColor(TFT_YELLOW, TFT_BLACK);
} else {
textSprite.setTextColor(colours[i], TFT_BLACK);
}
textSprite.setTextFont(sizes[i]);
textSprite.drawString(options[itemIndex].c_str(), indents[i], 0);
}
textSprite.pushSprite(area.x + 10,area.y + 20 + heightAccum);
heightAccum += heights[i];
}
}
void OnTouchDown(size_t x, size_t y) override {
}
void OnTouchUp(size_t x, size_t y) override {
constexpr size_t heights[] = {15, 25, 25, 25, 15};
int heightAccum = (int)area.y + 20;
for (int i = 0; i < 5; i++) {
if ((int)y >= heightAccum && (int)y < heightAccum + (int)heights[i]) {
int delta = i - 2;
int dir = delta > 0 ? 1 : -1;
for (int s = 0; s < abs(delta); s++) HandleRotaryEncChange(dir);
break;
}
heightAccum += (int)heights[i];
}
}
void OnDisplay() override {
ViewBase::OnDisplay();
};
void setOptions(std::span<String> newOptions) {
options.assign(newOptions.begin(), newOptions.end());
}
void setSelection(size_t idx) {
if (idx < options.size()) selectedIndex = idx;
}
bool acceptsFocus() override {
return true;
}
bool setFocus() override {
// Serial.println("RotarySelectView::setFocus called");
redraw();
return ViewBase::setFocus();
}
void removeFocus() override{
ViewBase::removeFocus();
redraw();
}
void HandleRotaryEncSwitch() override {
removeFocus();
redraw();
}
void HandleRotaryEncChange(int inc) override {
// Serial.printf("RotarySelectView::HandleRotaryEncChange called with inc=%d\n", inc);
if (inc > 0) {
if (selectedIndex < options.size() - 1) {
selectedIndex++;
if (newSelCB) newSelCB(selectedIndex);
redraw();
}
} else if (inc < 0) {
if (selectedIndex > 0) {
selectedIndex--;
if (newSelCB) newSelCB(selectedIndex);
redraw();
}
}
}
private:
int fillColour;
std::vector<String> options;
size_t selectedIndex = 0;
NewSelectionCallback newSelCB;
};
#endif

View file

@ -0,0 +1,78 @@
#ifndef __SINGLESELECT_VIEW_HPP__
#define __SINGLESELECT_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include "../common.hpp"
#include "RotarySelectView.hpp"
class SingleSelectView : public ViewBase {
public:
using NewSelectionCallback = std::function<void(size_t)>;
SingleSelectView(String name)
: ViewBase(name)
{}
void OnSetup() override {
selector = std::make_shared<RotarySelectView>("Selector", TFT_WHITE);
AddSubView(selector, { area.x + 20, area.y + 20, 280, 140 });
}
void setNewVoiceCallback(NewSelectionCallback cb) {
selector->setNewSelectionCallback(
[cb](size_t idx) {
cb(idx);
}
);
}
void OnDisplay() override {
};
void OnDraw() override {
}
bool acceptsFocus() override {
return true;
}
bool setFocus() override {
selector->setFocus();
redraw();
return ViewBase::setFocus();
}
void removeFocus() override{
hasFocus = false;
}
void HandleRotaryEncChange(int inc) override {
selector->HandleRotaryEncChange(inc);
}
virtual void HandleRotaryEncSwitch() override{
if (isFocused()) {
selector->removeFocus();
removeFocus();
}
}
void setOptions(std::span<String> newOptions) {
selector->setOptions(newOptions);
}
private:
std::shared_ptr<RotarySelectView> selector;
};
#endif

View file

@ -0,0 +1,66 @@
#ifndef __SYSTEM_VIEW_HPP__
#define __SYSTEM_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include <deque>
#include "../common.hpp"
class SystemView : public ViewBase {
public:
SystemView(String name)
: ViewBase(name)
{}
void OnSetup() override {
}
void OnDisplay() override {
freeHeap = rp2040.getFreeHeap();
totalHeap = rp2040.getTotalHeap();
usedHeap = totalHeap - freeHeap;
sys_clk = clock_get_hz(clk_sys);
};
void OnDraw() override {
TFT_eSprite textSprite(scr);
textSprite.createSprite(320, 20);
textSprite.setTextFont(2);
scr->fillRect(area.x, area.y, area.w, area.h, TFT_BLACK);
constexpr int32_t lineheight = 20;
textSprite.setTextColor(TFT_WHITE, TFT_BLACK);
std::deque<String> lines;
lines.push_back(String("MEMLNaut ") + String(MEMLLIB_VERSION));
lines.push_back("");
lines.push_back("Info: https://musicallyembodiedml.github.io");
lines.push_back("");
lines.push_back("Build: " + String(__DATE__) + " " + String(__TIME__));
lines.push_back("System Clock: " + String(sys_clk/1000000.f) + " MHz");
lines.push_back("Heap: " + String(freeHeap/1024) + "k free, " +
String(totalHeap/1024) + "k total, " +
String(usedHeap/1024) + "k used");
lines.push_back("");
lines.push_back("Made by Chris Kiefer and Andrea Martelloni");
lines.push_back("Emute Lab, University of Sussex, UK");
for(size_t i=0; i < lines.size(); i++) {
textSprite.fillRect(0,0,320,20,TFT_BLACK);
textSprite.drawString(lines[i].c_str(), 0, 0);
textSprite.pushSprite(area.x + 10,area.y + (i*lineheight));
}
}
private:
uint32_t freeHeap = 0;
uint32_t totalHeap = 0;
uint32_t usedHeap = 0;
uint32_t sys_clk=0;
};
#endif

View file

@ -0,0 +1,79 @@
#include "TextView.hpp"
TextView::TextView(String name, const char* content, uint16_t color)
: ViewBase(name)
, button("Press Me", 0x041f, false)
, toggle("Toggle Me", 0x041f, true)
, val("Value", 0, 100, 1)
, fl("Float", 0.1f, 5.0f, 0.01f)
, content_(content)
, color_(color)
{}
void TextView::OnSetup() {
// button.SetGrid(grid_, 0, 3);
// button.Setup(tft_);
// button.SetCallback([this](bool state) { OnButtonPressed_(state); });
// toggle.SetGrid(grid_, 3, 3);
// toggle.Setup(tft_);
// toggle.SetCallback([this](bool state) { OnTogglePressed_(state); });
// val.SetGrid(grid_, 2, 1);
// val.Setup(tft_);
// val.SetCallback([this](size_t value) {
// Serial.print("Value changed: ");
// Serial.println(value);
// });
// fl.SetGrid(grid_, 1, 1);
// fl.Setup(tft_);
// fl.SetCallback([this](float value) {
// Serial.print("Float changed: ");
// Serial.println(value);
// });
}
void TextView::OnDraw() {
scr->setTextColor(color_);
// Draw the content at the third row, first column of the grid
size_t gridX = 0;
size_t gridY = 4; // Third row
scr->drawString(content_, gridX * grid_.widthStep, gridY * grid_.heightStep, 2);
needRedraw_ = false;
// // Draw the button
// button.Draw();
// // Draw the toggle button
// toggle.Draw();
// // Draw the value element
// val.Draw();
// // Draw the float element
// fl.Draw();
}
void TextView::OnTouchDown(size_t x, size_t y) {
// Serial.print("TextView HandleTouch at: ");
// Serial.print(x);
// Serial.print(", ");
// Serial.println(y);
// Pass touch coordinates to the button for interaction
button.Interact(x, y);
toggle.Interact(x, y);
val.Interact(x, y);
fl.Interact(x, y);
}
void TextView::OnTouchUp(size_t x, size_t y) {
// Serial.println("TextView HandleRelease");
// Handle release events for the button
//button_.Interact(0, 0); // Pass dummy coordinates since release doesn't need them
button.Release();
toggle.Release();
val.Release();
fl.Release();
}
void TextView::OnButtonPressed_(bool state) {
Serial.print("Button pressed: ");
Serial.println(state ? "TRUE" : "FALSE");
}

View file

@ -0,0 +1,34 @@
#ifndef __TEXT_VIEW_HPP__
#define __TEXT_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
class TextView : public ViewBase {
public:
TextView(String name, // Changed parameter type
const char* content, // Changed parameter type
uint16_t color);
void OnSetup() override; // Changed from Setup to OnSetup
void OnDraw() override; // No longer takes TFT pointer
void OnTouchDown(size_t x, size_t y) override;
void OnTouchUp(size_t x, size_t y) override;
Button button{"Press Me", 0x041F, false}; // Navy blue to deep blue, non-toggle
Button toggle{"Toggle Me", 0x041F, true}; // Navy blue to deep blue, toggle
Value<size_t> val{"Value", 0, 100, 1}; // Value element with range 0-100 and step 1
Value<float> fl{"Float", 0.1, 5.0, 0.01};
private:
void OnButtonPressed_(bool state);
void OnTogglePressed_(bool state) {
Serial.print("Toggle pressed: ");
Serial.println(state ? "TRUE" : "FALSE");
}
const char* content_; // 1st initialized
uint16_t color_; // 2nd initialized
};
#endif // __TEXT_VIEW_HPP__

View file

@ -0,0 +1,69 @@
#include "UIElements.hpp"
Button::Button(const char* label, uint16_t color, bool is_toggle)
: UIElementBase(label)
, color_(color)
, is_toggle_(is_toggle)
, pressedCallback_()
, pressed_(false)
, toggleState_(false)
, button_(nullptr)
, current_x_(0)
, current_y_(0)
, current_w_(0)
, current_h_(0)
{
strncpy(labelBuffer_, label, sizeof(labelBuffer_) - 1);
labelBuffer_[sizeof(labelBuffer_) - 1] = '\0';
}
void Button::Draw() {
if (button_) {
button_->drawSmoothButton(toggleState_, kBorder, TFT_BLACK);
}
}
void Button::OnSetup() {
current_x_ = grid_.widthStep * pos_x_;
current_y_ = grid_.heightStep * pos_y_;
current_w_ = grid_.widthStep;
current_h_ = grid_.heightStep;
button_ = std::make_unique<ButtonWidget>(scr);
button_->initButtonUL(current_x_, current_y_, current_w_, current_h_,
TFT_WHITE, color_, TFT_BLACK, labelBuffer_, kTextSize);
button_->drawSmoothButton(pressed_, kBorder, TFT_BLACK);
}
void Button::SetCallback(std::function<void(bool)> callback) {
pressedCallback_ = callback;
}
void Button::Interact(size_t x, size_t y) {
if (button_) {
if (button_->contains(x, y)) {
if (!pressed_) {
pressed_ = true;
if (is_toggle_) {
toggleState_ = !toggleState_;
} else {
toggleState_ = true;
}
button_->drawSmoothButton(toggleState_, kBorder, TFT_BLACK);
if (pressedCallback_) {
pressedCallback_(toggleState_);
}
}
}
}
}
void Button::Release() {
if (button_) {
pressed_ = false;
if (!is_toggle_) {
toggleState_ = false;
}
button_->drawSmoothButton(toggleState_, kBorder, TFT_BLACK);
}
}

View file

@ -0,0 +1,214 @@
#ifndef __UI_ELEMENTS_HPP__
#define __UI_ELEMENTS_HPP__
#include <TFT_eSPI.h>
#include <TFT_eWidget.h>
#include <functional>
#include <string>
#include "../../../utils/Format.hpp"
struct GridDef {
size_t widthElements;
size_t heightElements;
size_t widthStep;
size_t heightStep;
};
struct rect {
int x, y, w, h;
};
class UIElementBase {
public:
UIElementBase() = delete; // Prevent instantiation of base class
virtual ~UIElementBase() = default; // Virtual destructor
void Setup(TFT_eSPI* tft) { // Changed to non-virtual like ViewBase
scr = tft;
OnSetup(); // Call virtual setup hook
}
virtual void OnSetup() = 0; // New virtual setup hook
virtual void Draw() = 0; // No longer needs TFT parameter
virtual void Interact(size_t x, size_t y) = 0; // Handle interaction events
virtual void Release() = 0; // Handle release events
void SetGrid(const GridDef &grid, size_t pos_x, size_t pos_y) {
grid_ = grid;
pos_x_ = pos_x;
pos_y_ = pos_y;
}
protected:
explicit UIElementBase(const char* label) : label_(label), scr(nullptr) {} // Changed to const char*
GridDef grid_; // Grid definition for layout
size_t pos_x_{0}; // Position in grid X
size_t pos_y_{0}; // Position in grid Y
const char* label_; // Changed to const char*
TFT_eSPI* scr; // Added tft pointer
};
class Button : public UIElementBase {
public:
static constexpr size_t kBorder = 3;
static constexpr size_t kTextSize = 1;
Button(const char* label, uint16_t color, bool is_toggle = false);
~Button() = default;
void Draw() override;
void OnSetup() override;
void SetCallback(std::function<void(bool)> callback);
void Interact(size_t x, size_t y) override;
void Release() override;
protected:
uint16_t color_;
bool is_toggle_;
std::function<void(bool)> pressedCallback_;
bool pressed_;
bool toggleState_{false}; // Track toggle state
std::unique_ptr<ButtonWidget> button_;
char labelBuffer_[32]; // Fixed buffer for button label
uint16_t current_x_{0};
uint16_t current_y_{0};
uint16_t current_w_{0};
uint16_t current_h_{0};
};
/**
* @brief Class showing a value (name and value) on the display.
* It can be incremented or decremented by a step value, e.g. by an
* encoder or a button.
*
* @tparam T
*/
template <typename T>
class Value : public UIElementBase {
public:
static constexpr size_t kTextSize = 1;
static constexpr size_t kValueSize = 2;
static constexpr size_t kSpacingTop = 16;
static constexpr size_t kSpacingBottom = 5;
Value(const char* label, T min, T max, T step)
: UIElementBase(label)
, value_(min)
, min_(min)
, max_(max)
, step_(step)
{
snprintf(labelBuffer_, sizeof(labelBuffer_), "%s", label);
}
~Value() = default;
void Draw() override
{
if (scr == nullptr) {
return; // Ensure tft is initialized
}
// Calculate background color
uint16_t bgColor = selected_ ? scr->color565(64, 80, 96) : TFT_BLACK;
// Fill background
scr->fillRect(current_x_, current_y_, current_w_, current_h_, bgColor);
// Draw the label with consistent background
scr->setTextSize(kTextSize);
scr->setTextColor(TFT_WHITE, bgColor);
scr->setTextDatum(TC_DATUM); // Center text
scr->drawString(labelBuffer_, current_x_ + (current_w_ >> 1), current_y_ + kSpacingTop);
scr->setTextSize(kValueSize);
// Draw the value in the line below
char valueBuffer[6];
formatNumber(valueBuffer, value_); // Format the value
size_t y = current_y_ + kSpacingTop + kTextSize * 8 + kSpacingBottom;
scr->drawString(valueBuffer, current_x_ + (current_w_ >> 1), y);
scr->setTextSize(1); // Reset text size
scr->setTextDatum(TL_DATUM); // Reset text datum
}
void OnSetup() override
{
value_ = min_; // Initialize value to min
current_x_ = pos_x_ * grid_.widthStep;
current_y_ = pos_y_ * grid_.heightStep;
current_w_ = grid_.widthStep;
current_h_ = grid_.heightStep;
selected_ = false; // Default not selected
};
void SetValue(T value)
{
if (value < min_) {
value = min_;
} else if (value > max_) {
value = max_;
}
if (value == value_) {
return; // No change
}
value_ = value;
// Trigger callback
if (valueChangedCallback_) {
valueChangedCallback_(value_);
}
Draw();
}
T GetValue() const { return value_; }
void Increment(bool up = true)
{
if (!selected_) {
return; // Only increment if selected
}
if (up) {
SetValue(value_ + step_);
} else {
// Check that decrementing does not go below min
// (robust to unsigned types)
if (value_ <= min_) {
return; // Do not decrement below min
}
SetValue(value_ - step_);
}
}
void SetCallback(std::function<void(T)> callback)
{
valueChangedCallback_ = std::move(callback);
}
void Interact(size_t x, size_t y) override
{
bool wasSelected = selected_; // Store previous selection state
// Select the value element if within bounds
if (x >= current_x_ && x < current_x_ + current_w_ &&
y >= current_y_ && y < current_y_ + current_h_) {
selected_ = true; // Mark as selected
} else {
selected_ = false; // Deselect if outside bounds
}
if (selected_ != wasSelected) {
// Redraw if selection state changed
Draw();
}
}
void Release() override
{
// Nothing for now
}
protected:
T value_;
T min_;
T max_;
T step_;
bool selected_ = false; // Moved initialization to declaration
std::function<void(T)> valueChangedCallback_; // Default constructor is fine
char labelBuffer_[32];
uint16_t current_x_{0};
uint16_t current_y_{0};
uint16_t current_w_{0};
uint16_t current_h_{0};
};
#endif // __UI_ELEMENTS_HPP__

View file

@ -0,0 +1,87 @@
#ifndef __VUMETER_VIEW_HPP__
#define __VUMETER_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include <vector>
#include <functional>
#include <cmath>
// Vertical VU meters fed from an external published level buffer (filled on the audio core).
// The view arms/disarms that measurement via onActive() as it comes on/off screen, and
// self-refreshes (~25 fps) only while it is the current view — so it costs nothing when hidden.
class VUMeterView : public ViewBase {
public:
using OnActiveCallback = std::function<void(bool)>;
VUMeterView(String name, std::vector<String> labels,
const volatile float* levels, OnActiveCallback onActive)
: ViewBase(name), labels_(std::move(labels)), levels_(levels),
onActive_(std::move(onActive)) {}
void OnSetup() override {
nBars_ = labels_.size();
slotW_ = nBars_ ? area.w / (int)nBars_ : area.w;
barW_ = 10;
meterTop_ = area.y + topPad_;
meterH_ = area.h - topPad_ - labelH_;
}
void OnDisplay() override {
ViewBase::OnDisplay();
if (onActive_) onActive_(true);
drawLabels_ = true;
redraw();
}
void OnHide() override {
if (onActive_) onActive_(false);
}
void OnDraw() override {
const int bottom = meterTop_ + meterH_;
const float gThr = 0.6f, yThr = 0.85f; // green / yellow / red zone boundaries
for (size_t i = 0; i < nBars_; ++i) {
const int slotX = area.x + (int)i * slotW_;
const int x = slotX + (slotW_ - barW_) / 2;
float lvl = levels_ ? levels_[i] : 0.f;
if (lvl < 0.f) lvl = 0.f; else if (lvl > 1.f) lvl = 1.f;
// Erase the full column, then paint the lit zones from the bottom up.
scr->fillRect(x, meterTop_, barW_, meterH_, TFT_BLACK);
const int gPx = (int)(fminf(lvl, gThr) * meterH_);
const int yPx = (int)(fmaxf(0.f, fminf(lvl, yThr) - gThr) * meterH_);
const int rPx = (int)(fmaxf(0.f, lvl - yThr) * meterH_);
if (gPx > 0) scr->fillRect(x, bottom - gPx, barW_, gPx, TFT_GREEN);
if (yPx > 0) scr->fillRect(x, bottom - gPx - yPx, barW_, yPx, TFT_YELLOW);
if (rPx > 0) scr->fillRect(x, bottom - gPx - yPx - rPx, barW_, rPx, TFT_RED);
if (drawLabels_) {
scr->setTextFont(2);
scr->setTextDatum(TC_DATUM);
scr->setTextColor(TFT_SILVER, TFT_BLACK);
scr->drawString(labels_[i].c_str(), slotX + slotW_ / 2, bottom + 3);
}
}
drawLabels_ = false;
redraw(); // keep refreshing while we are the active view
}
private:
std::vector<String> labels_;
const volatile float* levels_;
OnActiveCallback onActive_;
size_t nBars_ = 0;
int slotW_ = 0, barW_ = 0;
int meterTop_ = 0, meterH_ = 0;
const int topPad_ = 6;
const int labelH_ = 20;
bool drawLabels_ = true;
};
#endif

View file

@ -0,0 +1,14 @@
#include "View.hpp"
void ViewBase::Setup(TFT_eSPI* tft, rect bounds) {
scr = tft;
needRedraw_ = true;
area = bounds;
OnSetup();
}
bool ViewBase::NeedRedraw() {
bool ret = needRedraw_;
needRedraw_ = false;
return ret;
}

View file

@ -0,0 +1,147 @@
#ifndef __VIEW_HPP__
#define __VIEW_HPP__
#include <TFT_eSPI.h>
#include "UIElements.hpp"
class ViewBase {
public:
ViewBase() = delete; // Prevent instantiation of base class
virtual ~ViewBase() = default; // Virtual destructor
void Setup(TFT_eSPI* tft, rect bounds); // No longer virtual
virtual void OnSetup() = 0; // New virtual setup hook
virtual void OnDraw() = 0;
virtual void OnTouchDown(size_t x, size_t y) {
};
virtual void OnTouchUp(size_t x, size_t y) {
};
//called when a view is displayed
virtual void OnDisplay() {
for(auto& subview: subviews) {
subview->OnDisplay();
}
};
//called when a view is hidden
virtual void OnHide() {
};
bool NeedRedraw();
inline String GetName() const { return name_; } // Changed return type
inline void SetGrid(const GridDef &grid) { grid_ = grid; }
void setBounds(rect newBounds) {
area = newBounds;
}
inline void redraw() {
needRedraw_ = true;
for(auto& subview: subviews) {
subview->redraw();
}
}
void Draw() {
if (NeedRedraw()) {
OnDraw();
}
for(auto& subview: subviews) {
subview->Draw();
}
}
void HandleTouch(size_t x, size_t y) {
for(auto& subview: subviews) {
if (subview->area.x <= x && x < subview->area.x + subview->area.w &&
subview->area.y <= y && y < subview->area.y + subview->area.h) {
Serial.println("Subview touched");
subview->HandleTouch(x,y);
}
}
OnTouchDown(x,y);
}
void HandleTouchRelease(size_t x, size_t y) {
Serial.print("HandleTouchRelease at: ");
Serial.print(x);
Serial.print(", ");
Serial.println(y);
for(auto& subview: subviews) {
if (subview->area.x <= x && x < subview->area.x + subview->area.w &&
subview->area.y <= y && y < subview->area.y + subview->area.h) {
Serial.println("Subview touch released");
subview->HandleTouchRelease(x,y);
}
}
OnTouchUp(x,y);
}
void AddSubView(const std::shared_ptr<ViewBase> &view, rect bounds)
{
subviews.push_back(view);
if (scr) {
view->Setup(scr, bounds);
}
redraw();
}
String name_; // 1st initialized
bool IsVisible() {
return viewIsVisible;
}
void setVisible(const bool v) {
viewIsVisible = v;
// Serial.printf("setVisible called on view %s (addr: %p) to %d\n",
// name_.c_str(), (void*)this, v);
for(auto& subview: subviews) {
subview->setVisible(v);
}
}
//focus on view with rotary encoder switch
virtual bool acceptsFocus() {
return false;
}
virtual bool setFocus() {
hasFocus = acceptsFocus();
return hasFocus;
}
virtual void removeFocus() {
hasFocus = false;
}
bool isFocused() {
return hasFocus;
}
virtual void HandleRotaryEncChange(int inc) {
// Override in subclass if needed
}
virtual void HandleRotaryEncSwitch(){
// Override in subclass if needed
}
protected:
explicit ViewBase(String &name) // Changed parameter type
: name_(name)
, grid_({0, 0, 0, 0}) // Default grid definition
, needRedraw_(true)
, scr(nullptr),
area{0,0,1,1} {} // Initialize TFT pointer
GridDef grid_; // 2nd initialized
bool needRedraw_; // 3rd initialized
TFT_eSPI* scr; // 4th initialized
rect area;
std::vector<std::shared_ptr<ViewBase>> subviews;
bool viewIsVisible = false;
bool hasFocus = false;
};
#endif // __VIEW_HPP__

View file

@ -0,0 +1,92 @@
#ifndef __VOICESPACESELECT_VIEW_HPP__
#define __VOICESPACESELECT_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
#include "../common.hpp"
#include "RotarySelectView.hpp"
class VoiceSpaceSelectView : public ViewBase {
public:
using NewVoiceCallback = std::function<void(size_t)>;
VoiceSpaceSelectView(String name)
: ViewBase(name)
{}
void OnSetup() override {
voiceSpaceSelector = std::make_shared<RotarySelectView>("VoiceSpace", TFT_WHITE);
AddSubView(voiceSpaceSelector, { area.x + 20, area.y + 20, 280, 140 });
}
void setNewVoiceCallback(NewVoiceCallback cb) {
voiceSpaceSelector->setNewSelectionCallback(
[cb](size_t idx) {
cb(idx);
}
);
}
void OnDisplay() override {
};
void OnDraw() override {
TFT_eSprite textSprite(scr);
textSprite.createSprite(area.w, 20);
textSprite.fillSprite(TFT_BLACK);
textSprite.setTextColor(TFT_WHITE, TFT_BLACK);
textSprite.setTextFont(2);
textSprite.drawString(isFocused() ? "Press to confirm" : "Press to select", 10, 0);
textSprite.pushSprite(area.x, area.y + 165);
}
bool acceptsFocus() override {
return true;
}
bool setFocus() override {
// Serial.println("VoiceSpaceSelectView::setFocus called");
voiceSpaceSelector->setFocus();
redraw();
return ViewBase::setFocus();
}
void removeFocus() override{
hasFocus = false;
redraw();
}
void HandleRotaryEncChange(int inc) override {
voiceSpaceSelector->HandleRotaryEncChange(inc);
}
virtual void HandleRotaryEncSwitch() override{
if (isFocused()) {
voiceSpaceSelector->removeFocus();
removeFocus();
}
}
void setOptions(std::span<String> newOptions) {
voiceSpaceSelector->setOptions(newOptions);
}
void setSelection(size_t idx) {
voiceSpaceSelector->setSelection(idx);
redraw();
}
private:
std::shared_ptr<RotarySelectView> voiceSpaceSelector;
};
#endif

View file

@ -0,0 +1,82 @@
#ifndef __XYPADVIEW_VIEW_HPP__
#define __XYPADVIEW_VIEW_HPP__
#include "View.hpp"
#include "UIElements.hpp"
class XYPadView : public ViewBase {
public:
using OnTouchCallback = std::function<void(float, float)>;
using OnTouchReleaseCallback = std::function<void(float, float)>;
XYPadView(String name, int colour = TFT_GREEN)
: ViewBase(name), colour(colour)
{
}
void SetOnTouchCallback(OnTouchCallback _cb_) {
cb = _cb_;
}
void SetOnTouchReleaseCallback(OnTouchCallback _cb_) {
cbRelease = _cb_;
}
void OnSetup() override {
}
void OnDisplay() override {
};
void OnDraw() override {
if (touchReleaseFlag) {
touchReleaseFlag = false;
if (y > area.y) {
scr->fillRect(x, y, 10,10, TFT_BLACK);
}
}else if (touchDownFlag) {
if (y > area.y) {
scr->fillRect(x, y, 10,10, colour);
}
touchDownFlag = false;
}
Serial.printf("XYPadView OnDraw at: %d, %d\n", x, y);
}
void OnTouchDown(size_t x, size_t y) override {
// Update position based on touch coordinates
this->x = x;
this->y = y;
touching = true;
touchDownFlag = true;
if(cb) {
cb(x / (float)area.w, 1-(y / (float)(area.h - area.y)));
}
redraw();
};
void OnTouchUp(size_t x, size_t y) override {
touching = false;
touchReleaseFlag = true;
if(cbRelease) {
cbRelease(x / (float)area.w, 1-(y / (float)(area.h - area.y)));
}
redraw();
};
private:
size_t x,y;
bool touching;
int colour = TFT_GREEN;
bool touchReleaseFlag = false;
bool touchDownFlag = false;
OnTouchCallback cb = nullptr;
OnTouchReleaseCallback cbRelease = nullptr;
};
#endif

View file

@ -0,0 +1,61 @@
# Pin configuration
## Momentary switches or buttons:
| Pin Number | Pin Name | Type |
|------------|----------|------|
| 24 | MOM_A1 | INPUT_PULLUP |
| 25 | MOM_A2 | INPUT_PULLUP |
| 28 | MOM_B1 | INPUT_PULLUP |
| 29 | MOM_B2 | INPUT_PULLUP |
| 23 | RE_SW | INPUT_PULLUP |
| 17 | RE_B | INPUT_PULLUP |
| 11 | RE_A | INPUT_PULLUP |
## Toggle switches:
| Pin Number | Pin Name | Type |
|------------|----------|------|
| 26 | TOG_A1 | INPUT_PULLUP |
| 27 | TOG_A2 | INPUT_PULLUP |
| 30 | TOG_B1 | INPUT_PULLUP |
| 31 | TOG_B2 | INPUT_PULLUP |
| 32 | JOY_SW | INPUT_PULLUP |
## ADCs:
| Pin Number | Pin Name | ADC Channel | Type | Resolution |
|------------|----------|-------------|------|------------|
| 40 | JOY_X | 0 | INPUT | 12-bit |
| 41 | JOY_Y | 1 | INPUT | 12-bit |
| 42 | JOY_Z | 2 | INPUT | 12-bit |
| 47 | RV_Gain1 | 7 | INPUT | 12-bit |
| 46 | RV_Z1 | 6 | INPUT | 12-bit |
| 45 | RV_Y1 | 5 | INPUT | 12-bit |
| 44 | RV_X1 | 4 | INPUT | 12-bit |
## LED:
| Pin Number | Pin Name | Type | Description |
|------------|----------|------|-------------|
| 33 | LED | OUTPUT | Status LED |
| 43 | LED_Timing | OUTPUT | Timing LED |
## UART:
| UART Name | TX Pin | RX Pin | Description |
|-----------|--------|--------|-------------|
| DaisyPIO | 36 | N/A | One-way communication to Daisy |
| SensorUART | 34 | 35 | Bidirectional sensor communication |
| MIDI | 4 | 5 | MIDI in/out |
## SPI:
| SPI Name | CS Pin | SCK Pin | MISO Pin | MOSI Pin | Description |
|----------|--------|---------|----------|----------|-------------|
| SDCard | 13 | 14 | 12 | 15 | SPI interface to onboard SD card |
## I2C:
| I2C Name | SDA Pin | SCL Pin | Description |
|----------|---------|---------|-------------|
| USeqI2C | 38 | 39 | USeq output for CV |

View file

@ -0,0 +1,54 @@
#include "InterfaceBase.hpp"
#include <Arduino.h>
InterfaceBase::InterfaceBase() :
init_done_(false),
n_inputs_(0),
n_outputs_(0),
midi_(nullptr)
{
}
InterfaceBase::~InterfaceBase()
{
}
void InterfaceBase::setup(size_t n_inputs, size_t n_outputs)
{
queue_init(&queue_audioparam_, sizeof(float)*n_outputs, 1);
n_inputs_ = n_inputs;
n_outputs_ = n_outputs;
init_done_ = true;
}
void InterfaceBase::SendParamsToQueue(const std::vector<float>& data) {
if (!init_done_) {
DEBUG_PRINTLN("InterfaceBase::SendParamsToQueue - Error: Interface not initialized");
return;
}
if (data.size() != n_outputs_) {
DEBUG_PRINTLN("InterfaceBase::SendParamsToQueue - Error: data size mismatch");
DEBUG_PRINTF("Expected: %zu, Received: %zu\n", n_outputs_, data.size());
return;
}
queue_try_add(&queue_audioparam_, data.data());
if (paramOutputHook) {
paramOutputHook(data);
} else if (midi_) {
midi_->SendParamsAsMIDICC(data);
} else {
DEBUG_PRINTLN("Warning: MIDI interface not set");
}
}
bool InterfaceBase::ReceiveParamsFromQueue(float *data) {
if (!init_done_) {
DEBUG_PRINTLN("InterfaceBase::ReceiveParamsFromQueue - Error: Interface not initialized");
return false;
}
// if (data.size() != n_outputs_) {
// data.resize(n_outputs_);
// }
return queue_try_remove(&queue_audioparam_, data);
}

View file

@ -0,0 +1,61 @@
#ifndef __INTERFACE_BASE_HPP__
#define __INTERFACE_BASE_HPP__
#include "pico/util/queue.h"
#include <vector>
#include <functional>
#include "MIDIInOut.hpp"
#include <memory>
#include <span>
class InterfaceBase
{
protected:
InterfaceBase();
// Member variables
bool init_done_;
size_t n_inputs_;
size_t n_outputs_;
queue_t queue_audioparam_;
std::shared_ptr<MIDIInOut> midi_;
public:
~InterfaceBase();
// Disable copy constructor and assignment operator
InterfaceBase(const InterfaceBase&) = delete;
InterfaceBase& operator=(const InterfaceBase&) = delete;
// Disable move constructor and assignment operator
InterfaceBase(InterfaceBase&&) = delete;
InterfaceBase& operator=(InterfaceBase&&) = delete;
// Virtual functions with default implementations
virtual void setup(size_t n_inputs, size_t n_outputs);
inline void SetMIDIInterface(std::shared_ptr<MIDIInOut> midi) {
midi_ = midi;
}
// Optional transform applied to params before queuing and before storing as training action.
// Set this to apply FocusManager::applyInPlace or similar.
std::function<void(std::vector<float>&)> paramTransformHook;
// Optional output hook — when set, replaces the default SendParamsAsMIDICC() call.
// Use this to send SysEx or other non-CC output. If null, CC output is used as normal.
std::function<void(std::span<const float>)> paramOutputHook;
// Queue management
void SendParamsToQueue(const std::vector<float>& data);
//todo: this should be std::array and this class should be templated
bool ReceiveParamsFromQueue(float *data);
virtual void readAnalysisParameters(std::vector<float> params) {
// Default implementation does nothing
}
};
#endif// __INTERFACE_BASE_HPP__

View file

@ -0,0 +1,847 @@
#include "MIDIInOut.hpp"
#include <Arduino.h>
#include "../PicoDefs.hpp"
#include <unordered_set>
#include "hardware/dma.h"
struct CustomMIDISettings : public midi::DefaultSettings {
static const bool UseRunningStatus = false;
static const bool Use1ByteParsing = false; // Add this line
static const long BaudRate = 31250;
};
MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial2, MIDI, CustomMIDISettings);
#ifdef MIDI_USB_CLIENT
#include <Adafruit_TinyUSB.h>
Adafruit_USBD_MIDI usb_midi;
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, USBMIDI);
#endif
MIDIInOut* MIDIInOut::instance_ = nullptr;
MIDIInOut::MIDIInOut() : n_outputs_(0),
cc_callback_(nullptr),
note_callback_(nullptr),
send_channel_(1),
note_channel_(1),
refresh_uart_(false),
use_advanced_mappings_(false),
tx_dma_channel_(-1),
dma_busy_(false),
midi_tx_pin_(0),
rx_dma_channel_(-1),
rx_read_pos_(0),
parser_state_(0),
parser_status_(0),
parser_index_(0),
running_status_(0),
msg_write_pos_(0),
msg_read_pos_(0),
max_messages_per_poll_(16),
max_bytes_per_poll_(64),
track_changes_(true),
queue_write_pos_(0) {
instance_ = this;
memset(tx_dma_buffer_, 0, sizeof(tx_dma_buffer_));
memset(rx_dma_buffer_, 0, sizeof(rx_dma_buffer_));
memset(parser_data_, 0, sizeof(parser_data_));
memset(msg_queue_, 0, sizeof(msg_queue_));
memset(midi_queue_buffer_, 0, sizeof(midi_queue_buffer_));
#ifdef MIDI_USB_CLIENT
// TinyUSB setup - must be before Serial
TinyUSBDevice.setManufacturerDescriptor("ELI");
TinyUSBDevice.setProductDescriptor("MEMLNaut MIDI");
#endif
}
MIDIInOut::~MIDIInOut() {
// Clean up DMA channels if allocated
if (tx_dma_channel_ >= 0) {
dma_channel_abort(tx_dma_channel_);
dma_channel_unclaim(tx_dma_channel_);
tx_dma_channel_ = -1;
}
if (rx_dma_channel_ >= 0) {
dma_channel_abort(rx_dma_channel_);
dma_channel_unclaim(rx_dma_channel_);
rx_dma_channel_ = -1;
}
}
void MIDIInOut::Setup(size_t n_outputs,
bool midi_through, uint8_t midi_tx, uint8_t midi_rx, bool use_dma_rx) {
n_outputs_ = n_outputs;
cc_numbers_.resize(n_outputs);
// Initialize Serial2 first
Serial2.end(); // Reset Serial2 state
delay(10);
Serial2.setFIFOSize(32); // Set larger FIFO
Serial2.setTX(midi_tx);
Serial2.setRX(midi_rx);
Serial2.begin(31250); // Start MIDI baud rate
while(!Serial2) { delay(1); } // Wait for Serial2
delay(100); // Allow port to stabilize
// Check DMA channel availability before claiming
int available_channels = 0;
for (int i = 0; i < NUM_DMA_CHANNELS; i++) {
if (!dma_channel_is_claimed(i)) {
available_channels++;
}
}
DEBUG_PRINTF("DMA channels available: %d / %d\n", available_channels, NUM_DMA_CHANNELS);
// Setup DMA for TX (Serial2 uses uart1 on RP2040)
midi_tx_pin_ = midi_tx;
if (!setupTxDMA(uart1)) {
DEBUG_PRINTLN("Warning: DMA TX setup failed, falling back to regular writes");
} else {
DEBUG_PRINTF("DMA TX initialized (channel %d)\n", tx_dma_channel_);
}
// Setup DMA for RX only if explicitly requested (to avoid DMA channel conflicts)
if (use_dma_rx) {
if (!setupRxDMA(uart1)) {
DEBUG_PRINTLN("Warning: DMA RX setup failed, falling back to MIDI.read()");
} else {
DEBUG_PRINTF("DMA RX initialized (channel %d)\n", rx_dma_channel_);
}
} else {
DEBUG_PRINTLN("MIDI RX using Arduino MIDI library (DMA RX disabled to preserve channels)");
}
// Initialize CC numbers to default values [0 .. (n_outputs-1)]
std::vector<int> midiShitList = {0, 6,7, 8, 10, 32, 38, 39,41, 43};
std::unordered_set<int> excludeSet(midiShitList.begin(), midiShitList.end());
std::vector<int> ccnums;
ccnums.reserve(127 - midiShitList.size()); // pre-allocate
for(int i = 0; i < 127; i++) {
if(excludeSet.find(i) == excludeSet.end()) {
ccnums.push_back(i);
}
}
cc_numbers_.resize(ccnums.size());
for(size_t i=0; i < ccnums.size();i++) {
cc_numbers_[i] = static_cast<uint8_t>(ccnums[i]);
}
// Initialize change tracking with invalid values to force first send
last_sent_values_.resize(n_outputs_, 0xFF);
if (midi_through) {
// Enable MIDI thru if requested
MIDI.turnThruOn();
} else {
// Disable MIDI thru to prevent loop-back
MIDI.turnThruOff();
}
// Setup static callbacks
MIDI.setHandleControlChange(handleControlChange);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
MIDI.begin(MIDI_CHANNEL_OMNI);
delay(100); // Allow MIDI to initialize
// // Test sending messages with memory barriers
// MEMORY_BARRIER();
// MIDI.sendNoteOn(60, 127, 1);
// Serial2.flush();
// delay(1);
// MIDI.sendNoteOff(60, 0, 1);
// Serial2.flush();
// delay(1);
// MIDI.sendControlChange(102, 127, 1);
// Serial2.flush();
// delay(1);
// MIDI.sendControlChange(102, 0, 1);
// Serial2.flush();
// MEMORY_BARRIER();
#ifdef MIDI_USB_CLIENT
USBMIDI.begin(MIDI_CHANNEL_OMNI);
// Set up callbacks for incoming MIDI
USBMIDI.setHandleNoteOn(handleNoteOn);
USBMIDI.setHandleNoteOff(handleNoteOff);
MIDI.setHandleControlChange(handleControlChange);
#endif
DEBUG_PRINTLN("MIDI setup complete");
// Serial2.flush(); // Ensure the message is sent completely
}
void MIDIInOut::Poll()
{
// Use DMA-based processing if available
if (rx_dma_channel_ >= 0) {
// Process incoming bytes from DMA buffer
processRxBuffer();
// Process queued messages with rate limiting
processQueuedMessages();
} else {
// Non-DMA path: Read from Serial2 directly with rate limiting
// Process limited number of bytes per poll to prevent blocking
uint32_t bytes_processed = 0;
while (Serial2.available() && bytes_processed < max_bytes_per_poll_) {
uint8_t byte = Serial2.read();
processMidiByte(byte);
bytes_processed++;
}
// Process queued messages with rate limiting
processQueuedMessages();
}
// USBMIDI.read();
}
void MIDIInOut::warnSizeMismatch(const char* function_name, size_t expected, size_t actual) const {
DEBUG_PRINTF("Warning: %s size mismatch (expected %d, got %d)\n", function_name, expected, actual);
}
void MIDIInOut::SetParamCCNumbers(const std::vector<uint8_t>& cc_numbers) {
if (cc_numbers.size() != n_outputs_) {
warnSizeMismatch("SetParamCCNumbers", n_outputs_, cc_numbers.size());
}
cc_numbers_ = cc_numbers;
}
void MIDIInOut::SendParamsAsMIDICC(std::span<const float> params) {
// Optimized: Build directly into DMA buffer, skip unchanged values, use running status
size_t buf_idx = 0;
if (use_advanced_mappings_) {
// Use advanced mappings with individual channels and custom scaling
size_t send_count = std::min(params.size(), std::min(advanced_mappings_.size(), n_outputs_));
uint8_t last_channel = 0; // Track channel for running status
for (size_t i = 0; i < send_count; i++) {
uint8_t value = scaleValue(params[i], advanced_mappings_[i]);
// Skip if value hasn't changed
if (track_changes_ && value == last_sent_values_[i]) {
continue;
}
last_sent_values_[i] = value;
// Use running status: only send status byte when channel changes
uint8_t channel = advanced_mappings_[i].channel;
if (channel != last_channel || buf_idx == 0) {
tx_dma_buffer_[buf_idx++] = 0xB0 | ((channel - 1) & 0x0F);
last_channel = channel;
}
tx_dma_buffer_[buf_idx++] = advanced_mappings_[i].cc_number & 0x7F;
tx_dma_buffer_[buf_idx++] = value & 0x7F;
// Check buffer limit
if (buf_idx >= DMA_BUFFER_SIZE - 3) break;
}
} else {
// Use simple mappings (all on same channel - maximum running status benefit)
size_t send_count = std::min(params.size(), std::min(cc_numbers_.size(), n_outputs_));
uint8_t status_byte = 0xB0 | ((send_channel_ - 1) & 0x0F);
bool status_sent = false;
for (size_t i = 0; i < send_count; i++) {
// Fast clamping and scaling
float clamped = params[i];
clamped = (clamped > 1.0f) ? 1.0f : ((clamped < 0.0f) ? 0.0f : clamped);
uint8_t value = static_cast<uint8_t>(clamped * 127.0f + 0.5f);
// Skip if value hasn't changed
if (track_changes_ && value == last_sent_values_[i]) {
continue;
}
last_sent_values_[i] = value;
// Send status byte only once (running status)
if (!status_sent) {
tx_dma_buffer_[buf_idx++] = status_byte;
status_sent = true;
}
tx_dma_buffer_[buf_idx++] = cc_numbers_[i] & 0x7F;
tx_dma_buffer_[buf_idx++] = value & 0x7F;
// Check buffer limit
if (buf_idx >= DMA_BUFFER_SIZE - 3) break;
}
}
// Only send if we have data
if (buf_idx > 0) {
if (tx_dma_channel_ >= 0) {
sendViaDMADirect(buf_idx); // No memcpy - buffer already built in place
} else {
Serial2.write(tx_dma_buffer_, buf_idx);
}
}
}
void MIDIInOut::SetAdvancedParamMappings(const std::vector<CCMapping>& mappings) {
if (mappings.size() != n_outputs_) {
warnSizeMismatch("SetAdvancedParamMappings", n_outputs_, mappings.size());
}
advanced_mappings_ = mappings;
// Validate and fix mappings for efficiency
for (size_t i = 0; i < advanced_mappings_.size(); i++) {
// Ensure valid ranges
if (advanced_mappings_[i].channel < 1 || advanced_mappings_[i].channel > 16) {
advanced_mappings_[i].channel = 1;
}
if (advanced_mappings_[i].cc_number > 127) {
advanced_mappings_[i].cc_number = 127;
}
if (advanced_mappings_[i].min_value > 127) {
advanced_mappings_[i].min_value = 127;
}
if (advanced_mappings_[i].max_value > 127) {
advanced_mappings_[i].max_value = 127;
}
// Recompute scale factor in case values were clamped
uint8_t range = advanced_mappings_[i].max_value - advanced_mappings_[i].min_value;
advanced_mappings_[i].scale_factor = range;
}
use_advanced_mappings_ = true;
}
void MIDIInOut::SetParamMapping(size_t index, uint8_t cc_number, uint8_t channel, uint8_t min_value, uint8_t max_value) {
if (index >= n_outputs_) {
DEBUG_PRINTF("Warning: Parameter index %d out of range (max %d)\n", index, n_outputs_ - 1);
return;
}
// Initialize advanced_mappings_ if not already done
if (advanced_mappings_.size() != n_outputs_) {
advanced_mappings_.resize(n_outputs_);
}
// Validate and clamp values using bit operations for efficiency
channel = (channel < 1) ? 1 : ((channel > 16) ? 16 : channel);
cc_number = (cc_number > 127) ? 127 : cc_number;
min_value = (min_value > 127) ? 127 : min_value;
max_value = (max_value > 127) ? 127 : max_value;
advanced_mappings_[index] = CCMapping(cc_number, channel, min_value, max_value);
use_advanced_mappings_ = true;
}
void MIDIInOut::ClearAdvancedMappings() {
use_advanced_mappings_ = false;
}
void MIDIInOut::SetMIDISendChannel(uint8_t channel) {
// Ensure channel is in valid range (1-16)
if (channel < 1 || channel > 16) {
DEBUG_PRINTF("Warning: Invalid MIDI channel %d (must be 1-16)\n", channel);
return;
}
send_channel_ = channel;
}
void MIDIInOut::SetMIDINoteChannel(uint8_t channel) {
// Ensure channel is in valid range (1-16)
if (channel < 1 || channel > 16) {
DEBUG_PRINTF("Warning: Invalid MIDI note channel %d (must be 1-16)\n", channel);
return;
}
note_channel_ = channel;
}
void MIDIInOut::SetCCCallback(midi_cc_callback_t callback) {
cc_callback_ = callback;
}
void MIDIInOut::SetNoteCallback(midi_note_callback_t callback) {
note_callback_ = callback;
}
// Static callback implementations
void MIDIInOut::handleControlChange(byte channel, byte number, byte value) {
if (instance_ && instance_->cc_callback_) {
instance_->cc_callback_(number, value);
}
}
void MIDIInOut::handleNoteOn(byte channel, byte note, byte velocity) {
if (instance_ && instance_->note_callback_) {
instance_->note_callback_(true, note, velocity);
}
}
void MIDIInOut::handleNoteOff(byte channel, byte note, byte velocity) {
if (instance_ && instance_->note_callback_) {
instance_->note_callback_(false, note, velocity);
}
}
void MIDIInOut::RefreshUART_(void) {
if (!READ_VOLATILE(refresh_uart_)) {
Serial2.end(); // Reset Serial2 state
//delay(10);
Serial2.setFIFOSize(32); // Set larger FIFO
Serial2.setTX(Pins::MIDI_TX);
Serial2.setRX(Pins::MIDI_RX);
Serial2.begin(31250); // Start MIDI baud rate
while(!Serial2) {} // Wait for Serial2
WRITE_VOLATILE(refresh_uart_, true);
DEBUG_PRINTLN("MIDI UART refreshed.");
}
}
bool MIDIInOut::sendNoteOn(uint8_t note_number, uint8_t velocity) {
if (note_number > 127 || velocity > 127) {
return false;
}
// RefreshUART_(); // Refresh UART if needed
// while(!Serial2) { delay(1); } // Wait for Serial2
MIDI.sendNoteOn(note_number, velocity, note_channel_);
// MEMORY_BARRIER();
return true;
}
bool MIDIInOut::sendNoteOff(uint8_t note_number, uint8_t velocity) {
if (note_number > 127 || velocity > 127) {
return false;
}
RefreshUART_(); // Refresh UART if needed
while(!Serial2) { delay(1); } // Wait for Serial2
MIDI.sendNoteOff(note_number, velocity, note_channel_);
MEMORY_BARRIER();
return true;
}
// Buffered MIDI queue implementation
bool MIDIInOut::queueNoteOn(uint8_t note, uint8_t velocity) {
if (note > 127 || velocity > 127) {
return false;
}
// Auto-flush if not enough space
if (queue_write_pos_ + 3 > MIDI_QUEUE_BUFFER_SIZE) {
flushQueue();
}
midi_queue_buffer_[queue_write_pos_++] = 0x90 | ((note_channel_ - 1) & 0x0F);
midi_queue_buffer_[queue_write_pos_++] = note & 0x7F;
midi_queue_buffer_[queue_write_pos_++] = velocity & 0x7F;
return true;
}
bool MIDIInOut::queueNoteOff(uint8_t note, uint8_t velocity) {
if (note > 127 || velocity > 127) {
return false;
}
// Auto-flush if not enough space
if (queue_write_pos_ + 3 > MIDI_QUEUE_BUFFER_SIZE) {
flushQueue();
}
midi_queue_buffer_[queue_write_pos_++] = 0x80 | ((note_channel_ - 1) & 0x0F);
midi_queue_buffer_[queue_write_pos_++] = note & 0x7F;
midi_queue_buffer_[queue_write_pos_++] = velocity & 0x7F;
return true;
}
bool MIDIInOut::queueClock() {
if (queue_write_pos_ + 1 > MIDI_QUEUE_BUFFER_SIZE) {
flushQueue();
}
midi_queue_buffer_[queue_write_pos_++] = 0xF8 ;
return true;
}
bool MIDIInOut::queueClockStart() {
if (queue_write_pos_ + 1 > MIDI_QUEUE_BUFFER_SIZE) {
flushQueue();
}
midi_queue_buffer_[queue_write_pos_++] = 0xFA ;
return true;
}
bool MIDIInOut::queueClockStop() {
if (queue_write_pos_ + 1 > MIDI_QUEUE_BUFFER_SIZE) {
flushQueue();
}
midi_queue_buffer_[queue_write_pos_++] = 0xFC ;
return true;
}
bool MIDIInOut::queueCC(uint8_t cc_number, uint8_t value) {
if (cc_number > 127 || value > 127) {
return false;
}
// Auto-flush if not enough space
if (queue_write_pos_ + 3 > MIDI_QUEUE_BUFFER_SIZE) {
flushQueue();
}
midi_queue_buffer_[queue_write_pos_++] = 0xB0 | ((send_channel_ - 1) & 0x0F);
midi_queue_buffer_[queue_write_pos_++] = cc_number & 0x7F;
midi_queue_buffer_[queue_write_pos_++] = value & 0x7F;
return true;
}
size_t MIDIInOut::flushQueue() {
if (queue_write_pos_ == 0) {
return 0; // Nothing to send
}
size_t bytes_to_send = queue_write_pos_;
// Wait for any existing DMA to complete
waitForDMA();
// Copy queue to DMA buffer
memcpy(tx_dma_buffer_, midi_queue_buffer_, bytes_to_send);
// Send via DMA or fallback to Serial2
if (tx_dma_channel_ >= 0) {
sendViaDMADirect(bytes_to_send);
} else {
Serial2.write(tx_dma_buffer_, bytes_to_send);
}
// Reset queue position
queue_write_pos_ = 0;
return bytes_to_send;
}
// DMA Implementation
bool MIDIInOut::setupTxDMA(uart_inst_t* uart) {
// Claim DMA channel (don't panic on failure)
tx_dma_channel_ = dma_claim_unused_channel(false);
if (tx_dma_channel_ < 0) {
return false; // No DMA channel available
}
// Configure TX data channel
dma_channel_config c = dma_channel_get_default_config(tx_dma_channel_);
channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
channel_config_set_read_increment(&c, true); // Increment read address
channel_config_set_write_increment(&c, false); // Always write to UART DR
channel_config_set_dreq(&c, uart_get_dreq(uart, true)); // UART TX DREQ
dma_channel_configure(
tx_dma_channel_,
&c,
&uart_get_hw(uart)->dr, // Write to UART data register
NULL, // Read address set later
0, // Transfer count set later
false // Don't start yet
);
dma_busy_ = false;
return true;
}
void MIDIInOut::sendViaDMA(const uint8_t* data, size_t length) {
if (tx_dma_channel_ < 0 || length == 0 || length > DMA_BUFFER_SIZE) {
return; // DMA not available or invalid length
}
// Wait for previous transfer to complete
waitForDMA();
// Copy data to DMA buffer
memcpy(tx_dma_buffer_, data, length);
// Start DMA transfer
dma_busy_ = true;
dma_channel_set_read_addr(tx_dma_channel_, tx_dma_buffer_, false);
dma_channel_set_trans_count(tx_dma_channel_, length, true); // Start immediately
}
void MIDIInOut::sendViaDMADirect(size_t length) {
if (tx_dma_channel_ < 0 || length == 0 || length > DMA_BUFFER_SIZE) {
return; // DMA not available or invalid length
}
// Wait for previous transfer to complete
waitForDMA();
// Start DMA transfer directly from buffer (no memcpy needed)
dma_busy_ = true;
dma_channel_set_read_addr(tx_dma_channel_, tx_dma_buffer_, false);
dma_channel_set_trans_count(tx_dma_channel_, length, true); // Start immediately
}
void MIDIInOut::waitForDMA() {
if (tx_dma_channel_ < 0 || !dma_busy_) {
return;
}
// Wait for DMA to complete
while (dma_channel_is_busy(tx_dma_channel_)) {
tight_loop_contents();
}
dma_busy_ = false;
}
void MIDIInOut::sendRawBytes(const uint8_t* data, size_t length) {
if (length == 0) return;
if (tx_dma_channel_ >= 0) {
sendViaDMA(data, length);
} else {
Serial2.write(data, length);
}
}
// RX DMA Implementation
bool MIDIInOut::setupRxDMA(uart_inst_t* uart) {
// Claim DMA channel for RX (don't panic on failure)
rx_dma_channel_ = dma_claim_unused_channel(false);
if (rx_dma_channel_ < 0) {
return false; // No DMA channel available
}
// Configure RX DMA with ring buffer
dma_channel_config c = dma_channel_get_default_config(rx_dma_channel_);
channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
channel_config_set_read_increment(&c, false); // Always read from UART DR
channel_config_set_write_increment(&c, true); // Increment write address
channel_config_set_dreq(&c, uart_get_dreq(uart, false)); // UART RX DREQ
// Enable ring buffer on write (wraps at RX_BUFFER_SIZE)
uint32_t ring_size = 0;
uint32_t size = RX_BUFFER_SIZE;
while (size >>= 1) ring_size++;
channel_config_set_ring(&c, true, ring_size); // true = wrap on write
// Start the DMA channel
dma_channel_configure(
rx_dma_channel_,
&c,
rx_dma_buffer_, // Write to rx_buffer
&uart_get_hw(uart)->dr, // Read from UART data register
0xFFFFFFFF, // Transfer count (infinite with ring)
true // Start immediately
);
return true;
}
uint32_t MIDIInOut::getRxWritePos() {
// Calculate current write position from DMA transfer count
uint32_t remaining = dma_channel_hw_addr(rx_dma_channel_)->transfer_count;
return (0xFFFFFFFF - remaining) & (RX_BUFFER_SIZE - 1);
}
void MIDIInOut::processRxBuffer() {
uint32_t write_pos = getRxWritePos();
// Process all available bytes
while (rx_read_pos_ != write_pos) {
uint8_t byte = rx_dma_buffer_[rx_read_pos_];
rx_read_pos_ = (rx_read_pos_ + 1) & (RX_BUFFER_SIZE - 1);
processMidiByte(byte);
}
}
unsigned long medianOf3(unsigned long a, unsigned long b, unsigned long c) {
if (a > b) std::swap(a, b);
if (b > c) std::swap(b, c);
if (a > b) std::swap(a, b);
return b;
}
int counter=0;
void MIDIInOut::updateTempoEstimate() {
unsigned long now = micros();
unsigned long delta = now - midiClockTS;
if (delta < 100)
{
delta = deltaTMinus1; // Ignore unrealistic short intervals (could be due to jitter)
}
unsigned long deltaSmoothed = medianOf3(deltaTMinus1, deltaTMinus2, delta);
if (deltaSmoothed > 0.f) {
float ticksPerSecond = 1000000.f/deltaSmoothed;
static float lastBPM = 140.f;
float bpm = ticksPerSecond * 60.f / 24.f;
if (fabs(bpm - lastBPM) > 0.1f) {
lastBPM = bpm;
if (bpm_callback_) {
bpm_callback_(bpm);
}
}
}
counter++;
if (counter >= 12) {
// Serial.printf("---------Estimated BPM: %.2f, %f\n", bpm, deltaSmoothed);
counter = 0;
};
deltaTMinus2 = deltaTMinus1;
deltaTMinus1 = delta;
midiClockTS = now;
}
void MIDIInOut::processMidiByte(uint8_t byte) {
// Check if this is a status byte
if (byte & 0x80) {
// System Real-Time messages (single byte, can interrupt other messages)
if (byte == 0xF8) {
updateTempoEstimate();
return;
}
if (byte == 0xFA) {
if (transport_callback_) {
transport_callback_(true); // Start
}
return;
}
if (byte == 0xFC) {
if (transport_callback_) {
transport_callback_(false); // Stop
}
return;
}
// System Common messages or Channel Voice messages
parser_status_ = byte;
parser_index_ = 0;
// Determine expected data bytes
uint8_t msg_type = byte & 0xF0;
if (msg_type == 0xF0) {
// System messages - ignore SysEx for this implementation
if (byte == 0xF0) {
parser_state_ = 0xFF; // Ignore until 0xF7
}
return;
}
// Channel voice message
running_status_ = parser_status_;
} else if (parser_status_ == 0 && running_status_ != 0) {
// Use running status
parser_status_ = running_status_;
parser_index_ = 0;
}
// Ignore if in SysEx ignore mode
if (parser_state_ == 0xFF) {
if (byte == 0xF7) parser_state_ = 0;
return;
}
// Process data bytes
if (!(byte & 0x80) && parser_status_ != 0) {
uint8_t msg_type = parser_status_ & 0xF0;
// Determine how many data bytes we need
uint8_t expected_bytes = 2;
if (msg_type == 0xC0 || msg_type == 0xD0) { // Program Change or Channel Aftertouch
expected_bytes = 1;
}
parser_data_[parser_index_++] = byte;
if (parser_index_ >= expected_bytes) {
// Complete message received - queue it
uint8_t channel = (parser_status_ & 0x0F) + 1; // 1-16
uint8_t data2 = (expected_bytes > 1) ? parser_data_[1] : 0;
queueMessage(msg_type, channel, parser_data_[0], data2);
// Reset for next message (but keep running status)
parser_index_ = 0;
}
}
}
void MIDIInOut::queueMessage(uint8_t type, uint8_t channel, uint8_t data1, uint8_t data2) {
// Add message to queue
uint32_t next_pos = (msg_write_pos_ + 1) % MSG_QUEUE_SIZE;
// Check if queue is full (drop message if full)
if (next_pos == msg_read_pos_) {
return; // Queue full, drop message
}
msg_queue_[msg_write_pos_].type = type;
msg_queue_[msg_write_pos_].channel = channel;
msg_queue_[msg_write_pos_].data1 = data1;
msg_queue_[msg_write_pos_].data2 = data2;
msg_write_pos_ = next_pos;
}
void MIDIInOut::processQueuedMessages() {
uint32_t processed = 0;
// Process messages up to the limit
while (msg_read_pos_ != msg_write_pos_) {
// Check rate limit
if (max_messages_per_poll_ > 0 && processed >= max_messages_per_poll_) {
break; // Hit rate limit, process more next time
}
const MIDIMessage& msg = msg_queue_[msg_read_pos_];
msg_read_pos_ = (msg_read_pos_ + 1) % MSG_QUEUE_SIZE;
processed++;
// Call appropriate callback
switch (msg.type) {
case 0x90: // Note On
if (msg.data2 == 0) {
// Velocity 0 = Note Off
if (note_callback_) {
note_callback_(false, msg.data1, 0);
}
} else {
if (note_callback_) {
note_callback_(true, msg.data1, msg.data2);
}
}
break;
case 0x80: // Note Off
if (note_callback_) {
note_callback_(false, msg.data1, msg.data2);
}
break;
case 0xB0: // Control Change
if (cc_callback_) {
cc_callback_(msg.data1, msg.data2);
}
break;
// Add more message types as needed
}
}
}

View file

@ -0,0 +1,356 @@
#ifndef __MIDI_IN_OUT_HPP__
#define __MIDI_IN_OUT_HPP__
#include <Arduino.h>
#include <MIDI.h>
#include <memory>
#include "../hardware/memlnaut/Pins.hpp"
#include <functional>
#include <span>
#include "hardware/dma.h"
#include "hardware/uart.h"
//#define MIDI_USB_CLIENT
class MIDIInOut
{
public:
/**
* @brief Constructor (only instantiates memory and member variables). *
*/
MIDIInOut();
/**
* @brief Destructor - cleans up DMA resources
*/
~MIDIInOut();
/**
* @brief Sets up the MIDI interface on the required pins.
* The CC numbers are set to the default values [0 .. (n_outputs-1)].
*
* @param n_outputs Number of output parameters that will be expected by SendParamsAsMIDICC().
* @param midi_through Enable MIDI thru
* @param midi_tx TX pin the MIDI device is connected to (default: Pins::MIDI_TX).
* @param midi_rx RX pin the MIDI device is connected to (default: Pins::MIDI_RX).
* @param use_dma_rx Enable DMA for RX (default: false to avoid conflicts with audio/neural net DMA)
*/
void Setup(size_t n_outputs,
bool midi_through = false,
uint8_t midi_tx = Pins::MIDI_TX,
uint8_t midi_rx = Pins::MIDI_RX,
bool use_dma_rx = false);
/**
* @brief Set the MIDI channel to send messages on.
*
* @param channel MIDI channel (1-16).
* @note The channel is set to 1-16, but the library uses 0-15 internally.
*/
void SetMIDISendChannel(uint8_t channel);
/**
* @brief Set the MIDI channel to send note messages on.
*
* @param channel MIDI channel (1-16).
* @note The channel is set to 1-16, but the library uses 0-15 internally.
*/
void SetMIDINoteChannel(uint8_t channel);
/**
* @brief Send a MIDI Note On message
*
* @param note_number MIDI note number (0-127)
* @param velocity Note velocity (0-127)
* @return true if message sent successfully, false otherwise
*/
bool sendNoteOn(uint8_t note_number, uint8_t velocity);
/**
* @brief Send a MIDI Note Off message
*
* @param note_number MIDI note number (0-127)
* @param velocity Note velocity (0-127, typically 0)
* @return true if message sent successfully, false otherwise
*/
bool sendNoteOff(uint8_t note_number, uint8_t velocity = 0);
/**
* @brief Queue a MIDI Note On message for buffered transmission
* @param note MIDI note number (0-127)
* @param velocity Note velocity (0-127)
* @return true if queued successfully, false if invalid params
*/
bool queueNoteOn(uint8_t note, uint8_t velocity);
/**
* @brief Queue a MIDI Note Off message for buffered transmission
* @param note MIDI note number (0-127)
* @param velocity Note velocity (0-127, typically 0)
* @return true if queued successfully, false if invalid params
*/
bool queueNoteOff(uint8_t note, uint8_t velocity);
/**
* @brief Queue a MIDI Control Change message for buffered transmission
* @param cc_number CC number (0-127)
* @param value CC value (0-127)
* @return true if queued successfully, false if invalid params
*/
bool queueCC(uint8_t cc_number, uint8_t value);
bool queueClock();
bool queueClockStart();
bool queueClockStop();
unsigned long midiClockTS=0;
unsigned long deltaTMinus1=0, deltaTMinus2=0;
void updateTempoEstimate();
/**
* @brief Flush all queued MIDI messages via DMA
* @return Number of bytes sent, 0 if queue was empty
*/
size_t flushQueue();
/**
* @brief Send arbitrary bytes directly (e.g. SysEx). Uses DMA when available.
*/
void sendRawBytes(const uint8_t* data, size_t length);
/**
* @brief Poll input. Put in a regular loop.
*/
void Poll();
/**
* @brief Set the CC numbers that the parameters are sent to.
*
* @param cc_numbers Vector of MIDI CC numbers (0-127) to send the parameters to.
* cc.numbers.size() must be equal to n_outputs.
* @note The CC numbers are sent as MIDI messages, so they must be in the range 0-127.
*/
void SetParamCCNumbers(const std::vector<uint8_t> &cc_numbers);
/**
* @brief Send the given vector of parameters as MIDI CC messages.
*
* @param params Vector of parameters to send as MIDI CC messages.
* The size of the vector must be equal to n_outputs.
* @note The parameters will be scaled from [0 .. 1] to [0 .. 127].
*/
void SendParamsAsMIDICC(std::span<const float> params);
/**
* @typedef midi_cc_callback_t
* @brief Function pointer type for MIDI CC callbacks
*
* @param cc_number The MIDI CC number (0-127)
* @param cc_value The value of the MIDI CC message (0-127)
*
* This callback type is used for handling incoming MIDI Control Change messages.
* The function takes two parameters: the CC number and its corresponding value,
* both in the MIDI standard range of 0-127.
*/
using midi_cc_callback_t = std::function<void(const uint8_t, const uint8_t)>;
/**
* @typedef midi_note_callback_t
* @brief Function pointer type for MIDI note callbacks
*
* @param note_on True for note-on events, false for note-off events
* @param note_number The MIDI note number (0-127)
* @param velocity Note velocity/intensity (0-127)
*
* This callback type is used for handling incoming MIDI note messages.
* The function takes three parameters: a boolean indicating note on/off status,
* the note number, and velocity. Note number and velocity follow the MIDI
* standard range of 0-127.
*/
using midi_note_callback_t = std::function<void(const bool, const uint8_t, const uint8_t)>;
using midi_bpm_callback_t = std::function<void(const float)>;
void SetBPMCallback(midi_bpm_callback_t callback) {
bpm_callback_ = callback;
};
using midi_transport_callback_t = std::function<void(const bool)>;
void SetTransportCallback(midi_transport_callback_t callback) {
transport_callback_ = callback;
};
/**
* @brief Set the callback to be called when a MIDI CC message is received.
*
* @param callback Callback that accepts a CC number and value.
*/
void SetCCCallback(midi_cc_callback_t callback);
/**
* @brief Set the callback to be called when a MIDI note message is received.
*
* @param callback Callback that accepts a note on/off status, note number, and velocity.
*/
void SetNoteCallback(midi_note_callback_t callback);
/**
* @brief Structure for advanced CC parameter mapping
*/
struct CCMapping {
uint8_t cc_number;
uint8_t channel;
uint8_t min_value;
uint8_t max_value;
float scale_factor; // Precomputed for efficiency: (max - min)
constexpr CCMapping() : cc_number(0), channel(1), min_value(0), max_value(127), scale_factor(127.0f) {}
constexpr CCMapping(uint8_t cc, uint8_t ch, uint8_t min_val, uint8_t max_val)
: cc_number(cc), channel(ch), min_value(min_val), max_value(max_val),
scale_factor(max_val - min_val) {}
};
/**
* @brief Set advanced parameter mappings with individual CC, channel, and range settings
*
* @param mappings Vector of CCMapping structures (must equal n_outputs_ size)
*/
void SetAdvancedParamMappings(const std::vector<CCMapping>& mappings);
/**
* @brief Set mapping for a single parameter
*
* @param index Parameter index
* @param cc_number CC number (0-127)
* @param channel MIDI channel (1-16)
* @param min_value Minimum output value (0-127)
* @param max_value Maximum output value (0-127)
*/
void SetParamMapping(size_t index, uint8_t cc_number, uint8_t channel, uint8_t min_value, uint8_t max_value);
/**
* @brief Clear advanced mappings and revert to simple mode
*/
void ClearAdvancedMappings();
/**
* @brief Enable/disable change tracking optimization
*
* When enabled, only sends CC messages when values change.
* Disable if you need to force send all values every time.
*
* @param enable True to track changes (default), false to always send
*/
void SetChangeTracking(bool enable) { track_changes_ = enable; }
/**
* @brief Set maximum messages to process per Poll() call
*
* Limits how many incoming MIDI messages are processed per poll,
* preventing message floods from blocking the main loop.
*
* @param max_messages Maximum messages per poll (default: 16, 0 = unlimited)
*/
void SetMaxMessagesPerPoll(uint32_t max_messages) { max_messages_per_poll_ = max_messages; }
/**
* @brief Set maximum bytes to read from Serial per Poll() call (non-DMA mode)
*
* Limits how many bytes are read from the UART per poll to prevent blocking.
*
* @param max_bytes Maximum bytes per poll (default: 64)
*/
void SetMaxBytesPerPoll(uint32_t max_bytes) { max_bytes_per_poll_ = max_bytes; }
size_t getParamCount() const { return n_outputs_; }
protected:
std::vector<uint8_t> cc_numbers_;
size_t n_outputs_;
midi_cc_callback_t cc_callback_;
midi_note_callback_t note_callback_;
midi_bpm_callback_t bpm_callback_ = nullptr;
midi_transport_callback_t transport_callback_ = nullptr;
uint8_t send_channel_; // Store the MIDI send channel (1-16)
uint8_t note_channel_; // Store the MIDI note channel (1-16)
bool refresh_uart_;
static MIDIInOut* instance_; // Add static instance pointer
// Advanced mapping support
std::vector<CCMapping> advanced_mappings_;
bool use_advanced_mappings_;
// Helper for size mismatch warnings
void warnSizeMismatch(const char* function_name, size_t expected, size_t actual) const;
private:
// Static callback handlers
static void handleControlChange(byte channel, byte number, byte value);
static void handleNoteOn(byte channel, byte note, byte velocity);
static void handleNoteOff(byte channel, byte note, byte velocity);
void RefreshUART_(void);
// DMA output support
static constexpr size_t DMA_BUFFER_SIZE = 1024;
int tx_dma_channel_;
uint8_t tx_dma_buffer_[DMA_BUFFER_SIZE];
volatile bool dma_busy_;
uint8_t midi_tx_pin_;
// DMA input support
static constexpr size_t RX_BUFFER_SIZE = 512;
int rx_dma_channel_;
uint8_t rx_dma_buffer_[RX_BUFFER_SIZE] __attribute__((aligned(RX_BUFFER_SIZE)));
uint32_t rx_read_pos_;
// MIDI parser state
uint8_t parser_state_;
uint8_t parser_status_;
uint8_t parser_data_[2];
uint8_t parser_index_;
uint8_t running_status_;
// Message buffering for rate limiting
struct MIDIMessage {
uint8_t type;
uint8_t channel;
uint8_t data1;
uint8_t data2;
};
static constexpr size_t MSG_QUEUE_SIZE = 64;
MIDIMessage msg_queue_[MSG_QUEUE_SIZE];
volatile uint32_t msg_write_pos_;
volatile uint32_t msg_read_pos_;
uint32_t max_messages_per_poll_;
uint32_t max_bytes_per_poll_;
// Optimization: Track last sent values to skip unchanged CCs
std::vector<uint8_t> last_sent_values_;
bool track_changes_;
bool setupTxDMA(uart_inst_t* uart);
bool setupRxDMA(uart_inst_t* uart);
void sendViaDMA(const uint8_t* data, size_t length);
void sendViaDMADirect(size_t length); // Send buffer directly without memcpy
void waitForDMA();
// RX processing
uint32_t getRxWritePos();
void processRxBuffer();
void processMidiByte(uint8_t byte);
void queueMessage(uint8_t type, uint8_t channel, uint8_t data1, uint8_t data2);
void processQueuedMessages();
// Inline helper for efficient value scaling
inline uint8_t scaleValue(float param, const CCMapping& mapping) const {
float clamped = param > 1.0f ? 1.0f : (param < 0.0f ? 0.0f : param);
return static_cast<uint8_t>(clamped * mapping.scale_factor + mapping.min_value + 0.5f);
}
// Buffered MIDI queue support
static constexpr size_t MIDI_QUEUE_BUFFER_SIZE = 1024;
uint8_t midi_queue_buffer_[MIDI_QUEUE_BUFFER_SIZE];
size_t queue_write_pos_; // No volatile needed - single-threaded user code only
};
#endif // __MIDI_IN_OUT_HPP__

View file

@ -0,0 +1,92 @@
#include "SDCard.hpp"
#include <SPI.h>
#include "SdFat.h"
SDCard::SDCard(int miso, int mosi, int cs, int sck)
: miso_(miso), mosi_(mosi), cs_(cs), sck_(sck), cardPresent_(false), cardEventCb_(nullptr) {
SPI1.setRX(miso_);
SPI1.setTX(mosi_);
SPI1.setSCK(sck_);
Poll();
}
void SDCard::Poll() {
bool newStatus = sd_.begin(cs_);
if (newStatus != cardPresent_) {
cardPresent_ = newStatus;
if (cardEventCb_) {
cardEventCb_(cardPresent_);
}
}
}
SDCard::CardInfo SDCard::GetCardInfo() {
CardInfo info = {CardType::UNKNOWN, 0, 0, 0, 0, 0, 0};
if (!cardPresent_) return info;
FsVolume* volume = sd_.vol();
if (volume && volume->fatType() != 0) {
info.type = CardType::SD2;
info.blockSize = 512; // Standard for SD cards
info.blocksPerCluster = volume->sectorsPerCluster();
info.clusterSize = info.blockSize * info.blocksPerCluster;
info.totalBytes = (uint64_t)volume->clusterCount() * info.clusterSize;
info.fatType = volume->fatType();
// Calculate used space
uint32_t freeClusters = volume->freeClusterCount();
if (freeClusters != 0xFFFFFFFF) {
uint64_t freeBytes = (uint64_t)freeClusters * info.clusterSize;
info.usedBytes = info.totalBytes - freeBytes;
} else {
info.usedBytes = info.totalBytes / 2; // Fallback estimate
}
}
return info;
}
bool SDCard::MKDir(const char* path, bool exist_ok) {
if (!cardPresent_) return false;
if (sd_.exists(path)) {
return exist_ok;
}
return sd_.mkdir(path);
}
bool SDCard::Write(const char* filename, const std::vector<char>& data) {
if (!cardPresent_) return false;
FatFile file;
if (!file.open(filename, O_RDWR | O_CREAT | O_TRUNC)) return false;
size_t written = file.write(data.data(), data.size());
file.close();
return written == data.size();
}
bool SDCard::Read(const char* filename, std::vector<char>& data, size_t size) {
if (!cardPresent_) return false;
FatFile file;
if (!file.open(filename, O_READ)) return false;
size_t fileSize = file.fileSize();
size_t bytesToRead = (size == 0 || size > fileSize) ? fileSize : size;
data.resize(bytesToRead);
size_t bytesRead = file.read(data.data(), bytesToRead);
file.close();
return bytesRead == bytesToRead;
}
bool SDCard::Touch(const char* filename) {
if (!cardPresent_) return false;
if (sd_.exists(filename)) return false;
FatFile file;
if (!file.open(filename, O_RDWR | O_CREAT)) return false;
file.close();
return true;
}

View file

@ -0,0 +1,122 @@
#ifndef __SD_CARD_HPP__
#define __SD_CARD_HPP__
#include <cstddef>
#include <vector>
#include <functional>
#include <cstdint>
#include "../hardware/memlnaut/Pins.hpp"
#include "SdFat.h"
class SDCard {
public:
using CardEventCallback = std::function<void(bool)>; // Callback type for card events (inserted/removed)
enum class CardType {
UNKNOWN = -1,
SD1 = 0,
SD2 = 1,
SDHC_SDXC = 3
};
struct CardInfo {
CardType type;
uint32_t clusterSize;
uint32_t blocksPerCluster;
uint32_t blockSize;
uint64_t totalBytes;
uint64_t usedBytes;
uint8_t fatType;
};
/**
* @brief Constructor initialises the hardware SPI interface
* @param miso MISO pin number
* @param mosi MOSI pin number
* @param cs CS pin number
* @param sck SCK pin number
*/
SDCard(int miso = Pins::SD_MISO,
int mosi = Pins::SD_MOSI,
int cs = Pins::SD_CS,
int sck = Pins::SD_SCK);
/**
* @brief Set callback for card insertion/removal events
* @param cb Callback function receiving bool (true=inserted, false=removed)
*/
void SetCardEventCallback(CardEventCallback cb) { cardEventCb_ = cb; }
/**
* @brief Get current card information
* @return CardInfo struct with card details
*/
CardInfo GetCardInfo(); // Remove const
/**
* @brief Check if card is currently available
* @return true if card is inserted and initialized
*/
bool IsCardPresent() const { return cardPresent_; }
/**
* @brief Creates directory structure if it doesn't exist
* @param path Directory path to create
* @param exist_ok
* @return true if successful (returns true if file already exists
* and exist_ok is true,
* false if file already exists and exist_ok is false)
*/
bool MKDir(const char* path, bool exist_ok = false);
/**
* @brief Writes bytes to file path. If file exists,
* data will be overwritten.
*
* @param filename File path to write to
* @param data Data to write
* @return true Write successful
* @return false Write unsuccessful
*/
bool Write(const char* filename, const std::vector<char> &data);
/**
* @brief Reads bytes from file path. If file does not exist,
* data will be empty and false is returned.
*
* @param filename File path to read from
* @param data Data to read
* @param size Size of data to read
* @return true Read successful
* @return false Read unsuccessful
*/
bool Read(const char* filename, std::vector<char> &data, size_t size = 0);
/**
* @brief Creates empty file if file does not exist. Also recursively
* creates directories in the path if they do not exist.
*
* @param filename File path to create
* @return true File created successfully
* @return false File already exists or creation failed
*/
bool Touch(const char* filename);
/**
* @brief Check card status and trigger callbacks if changed
* Should be called periodically from main Arduino loop
*/
void Poll();
private:
const int miso_;
const int mosi_;
const int cs_;
const int sck_;
bool cardPresent_;
CardEventCallback cardEventCb_;
mutable SdFs sd_; // Make sd_ mutable to allow vol() calls in const methods
FatFile root_;
};
#endif // __SD_CARD_HPP__

View file

@ -0,0 +1,156 @@
#include "SerialUSBInput.hpp"
#include "../utils/SLIP.hpp"
#include "SerialUSBOutput.hpp"
SerialUSBOutput usbSerialOut;
SerialUSBInput::SerialUSBInput(size_t n_inputs, std::shared_ptr<display> dispptr, size_t baud_rate) :
slipBuffer{ 0 },
value_states_(n_inputs, 0),
spiState(SPISTATES::WAITFOREND),
spiIdx(0),
callback_(nullptr),
refresh_uart_(false),
n_inputs_(n_inputs),
baud_rate_(baud_rate)
{
disp = dispptr;
//assume Serial has already begun()
}
float bytes2float_union(const uint8_t* bytes) {
union {
uint32_t i;
float f;
} converter;
converter.i = bytes[0] |
(bytes[1] << 8) |
(bytes[2] << 16) |
(bytes[3] << 24);
return converter.f;
}
void SerialUSBInput::Poll()
{
if (!refresh_uart_) {
// What baud rate is the UART running at?
// Print debug info about PIO Serial state
DEBUG_PRINT("PIO Serial Status - Initialized: ");
DEBUG_PRINT(Serial ? "Yes" : "No");
DEBUG_PRINT(", Target Baud Rate: ");
DEBUG_PRINTLN(baud_rate_);
// Start at current baud rate
Serial1.begin(baud_rate_);
DEBUG_PRINTLN("PIO_UART refreshed.");
DEBUG_PRINT("Serial available: ");
DEBUG_PRINTLN(Serial.available());
refresh_uart_ = true;
}
while (true) {
int raw = Serial.read();
if (raw < 0) break; // no more data
uint8_t spiByte = static_cast<uint8_t>(raw);
switch(spiState) {
case SPISTATES::WAITFOREND:
if (spiByte == SLIP::END) {
slipBuffer[0] = SLIP::END;
spiState = SPISTATES::READBYTES;
}
break;
// case SPISTATES::ENDORBYTES:
// if (spiByte == SLIP::END) {
// spiIdx = 1;
// } else {
// slipBuffer[1] = spiByte;
// spiIdx = 2;
// }
// spiState = SPISTATES::READBYTES;
// break;
case SPISTATES::READBYTES:
if (spiIdx < static_cast<int>(kSlipBufferSize_)) {
slipBuffer[spiIdx++] = spiByte;
if (spiByte == SLIP::END) {
// Safe decode into fixed buffer
//disp->post("Received packet");
size_t maxOut = n_inputs_ * sizeof(float);
uint8_t outBuf[maxOut] = {0};
size_t got = SLIP::decode(slipBuffer, spiIdx, outBuf, maxOut);
if (got == maxOut) {
//disp->post("Decoded packet successfully");
//float f = bytes2float_union(outBuf);
//disp->post("Value: " + String(f, 8));
for (size_t n = 0; n < n_inputs_; n++) {
// Convert each 4-byte float in the buffer
float f = bytes2float_union(&outBuf[n * sizeof(float)]);
value_states_[n] = f; // Store the value
//if (n == 0) disp->post("USBIn0: "+ String(f, 8));
}
callback_(value_states_);
// std::vector<float> values(1);
// values[0] = f * 2.0f;
// usbSerialOut.SendFloatArray(values);
//disp->post("Sent value back: " + String(f, 8));
// spiMessage msg;
// memcpy(&msg, outBuf, maxOut);
// Parse_(msg);
}else{
disp->post("Invalid packet: " + String(got));
}
// Reset for next packet
spiState = SPISTATES::WAITFOREND;
spiIdx = 0;
}
} else {
DEBUG_PRINTLN("UARTInput: SLIP buffer overrun, dropping packet");
spiState = SPISTATES::WAITFOREND;
spiIdx = 0;
}
break;
}
}
}
// void UARTInput::Parse_(spiMessage msg)
// {
// static const float kEventThresh = 0.01;
// // Find if this message's index is in our tracked indexes
// auto it = std::find(sensor_indexes_.begin(), sensor_indexes_.end(), msg.msg);
// if (it != sensor_indexes_.end()) {
// size_t index = std::distance(sensor_indexes_.begin(), it);
// // Protect against infs and nans
// if (std::isnan(msg.value) || std::isinf(msg.value)) {
// msg.value = value_states_[index];
// }
// float filtered_value = filters_[index].process(msg.value);
// float prev_value = value_states_[index];
// // Trigger callback whenever any value has changed
// if (std::abs(filtered_value - prev_value) > kEventThresh) {
// if (callback_) callback_(msg.msg, filtered_value);
// }
// // Print the value (Arduino scope) if it's the observed channel
// if (kObservedChan == msg.msg) {
// DEBUG_PRINT("Low:0.00,High:1.00,Value:");
// DEBUG_PRINT(msg.value, 8);
// DEBUG_PRINT(",FilteredValue:");
// DEBUG_PRINTLN(filtered_value, 8);
// }
// value_states_[index] = filtered_value;
// }
// }

View file

@ -0,0 +1,80 @@
#ifndef __UART_INPUT_HPP__
#define __UART_INPUT_HPP__
#include <Arduino.h>
#include "../utils/MedianFilter.h"
#include "../hardware/memlnaut/Pins.hpp"
#include <SerialPIO.h>
#include <functional>
#include "../hardware/memlnaut/display.hpp" // Added include
#include <vector>
class SerialUSBInput // Forward declaration for SerialUSBInput
{
public:
// Maximum allowed channels (avoid unbounded resizes)
static constexpr size_t kMaxChannels = 8;
// Change to trigger debugging of single channel
static constexpr size_t kObservedChan = 9999;
using usb_uart_in_callback_t = std::function<void(std::vector<float>)>;
/**
* @brief Construct a new UARTInput object for communication
* with the MEML Sensor Board.
*
* @param sensor_indexes Vector of indexes to read (maximum 8, numbers 0-7).
* @param sensor_rx RX pin the Sensor Board is connected to (default: Pins::SENSOR_RX).
* @param sensor_tx TX pin the Sensor Board is connected to (default: Pins::SENSOR_TX).
*/
SerialUSBInput(size_t n_inputs, std::shared_ptr<display> disp, size_t baud_rate = 115200);
/**
* @brief Poll input. Put in a regular loop.
*/
void Poll();
/**
* @brief Set the callback to be called when sensor data changes.
*
* @param callback Callback that accepts a vector of sensor readings.
*/
inline void SetCallback(usb_uart_in_callback_t callback)
{
callback_ = callback;
}
protected:
static const size_t kSlipBufferSize_ = 512;
std::vector<size_t> sensor_indexes_;
uint8_t slipBuffer[kSlipBufferSize_];
// std::vector<MedianFilter<float>> filters_;
std::vector<float> value_states_;
usb_uart_in_callback_t callback_ = nullptr;
bool refresh_uart_;
size_t baud_rate_;
struct spiMessage
{
uint8_t msg;
float value;
};
enum SPISTATES
{
WAITFOREND,
ENDORBYTES,
READBYTES
};
SPISTATES spiState;
int spiIdx;
void Parse_(spiMessage msg);
private:
std::shared_ptr<display> disp;
size_t n_inputs_;
};
#endif // __UART_INPUT_HPP__

View file

@ -0,0 +1,47 @@
#include "SerialUSBOutput.hpp"
SerialUSBOutput::SerialUSBOutput()
{
//assume Serial has already begun()
}
void SerialUSBOutput::SendFloatArray(const std::vector<float> &params)
{
Serial.write(SLIP_END);
// Encode each float (4 bytes) via SLIP
for(const auto& f : params)
{
union {
float f;
uint8_t b[4];
} data;
data.f = f;
for(int i = 0; i < 4; i++)
{
slipSendByte(data.b[i]);
}
}
// Finally, write the SLIP_END marker
Serial.write(SLIP_END);
}
void SerialUSBOutput::slipSendByte(uint8_t b)
{
if(b == SLIP_END)
{
Serial.write(SLIP_ESC);
Serial.write(SLIP_ESC_END);
}
else if(b == SLIP_ESC)
{
Serial.write(SLIP_ESC);
Serial.write(SLIP_ESC_ESC);
}
else
{
Serial.write(b);
}
}

View file

@ -0,0 +1,46 @@
#ifndef __SERIALUSBUART_OUTPUT_HPP__
#define __SERIALUSBUART_OUTPUT_HPP__
#include <Arduino.h>
#include <SerialPIO.h>
#include "../hardware/memlnaut/Pins.hpp"
#include <vector>
/**
* @brief Sends float data to an external device using
* SLIP encoding over a PIO-based
* "software" UART on a single GPIO pin (txPin).
*/
class SerialUSBOutput
{
public:
/**
* @param txPin The GPIO pin on the Raspberry Pi Pico to use for TX.
* Must be a valid pin for PIO-based UART.
* For example, "16" for GP16.
*/
SerialUSBOutput();
/**
* @brief SLIP-encodes the float vector and sends it as one packet.
* @param params Vector of floats to send.
*/
void SendFloatArray(const std::vector<float> &params);
private:
/**
* @brief Helper to apply SLIP-escaping on a single byte.
*/
void slipSendByte(uint8_t b);
// SLIP special characters
static constexpr uint8_t SLIP_END = 0xC0; ///< End of SLIP packet
static constexpr uint8_t SLIP_ESC = 0xDB; ///< Escape character
static constexpr uint8_t SLIP_ESC_END = 0xDC; ///< Escaped END
static constexpr uint8_t SLIP_ESC_ESC = 0xDD; ///< Escaped ESC
};
#endif // __UART_OUTPUT_HPP__

View file

@ -0,0 +1,18 @@
#pragma once
#include "MIDIInOut.hpp"
#include <span>
#include <memory>
// Abstract base for synth-specific parameter output drivers.
// Subclass this to implement SysEx (or other) output for a specific synthesizer.
// Wire an instance into InterfaceBase::paramOutputHook in the mode's setupMIDI().
class SynthParamOutput {
public:
virtual void sendParams(std::span<const float> params) = 0;
virtual ~SynthParamOutput() = default;
protected:
SynthParamOutput(std::shared_ptr<MIDIInOut> midi) : midi_(midi) {}
std::shared_ptr<MIDIInOut> midi_;
};

View file

@ -0,0 +1,198 @@
#include "UARTInput.hpp"
#include "../utils/SLIP.hpp"
#include "../utils/Maths.hpp"
UARTInput::UARTInput(const std::vector<size_t>& sensor_indexes,
size_t sensor_rx,
size_t sensor_tx,
size_t baud_rate) :
sensor_indexes_(sensor_indexes),
sensor_rx_(sensor_rx),
sensor_tx_(sensor_tx),
slipBuffer{ 0 },
filters_(),
value_states_{ 0 },
spiState(SPISTATES::WAITFOREND),
spiIdx(0),
callback_(nullptr),
refresh_uart_(false),
baud_rate_(baud_rate)
{
// Reserve once to avoid heap fragmentation at runtime
filters_.reserve(kMaxChannels);
value_states_.reserve(kMaxChannels);
filters_.resize(kMaxChannels);
value_states_.resize(kMaxChannels, 0.5f);
Serial1.setTX(sensor_tx);
Serial1.setRX(sensor_rx);
Serial1.begin(baud_rate);
}
void UARTInput::ListenToSensorIndexes(const std::vector<size_t>& indexes)
{
sensor_indexes_ = indexes;
// Reserve to avoid repeated reallocs
filters_.reserve(indexes.size());
value_states_.reserve(indexes.size());
filters_.clear();
filters_.resize(indexes.size());
value_states_.clear();
value_states_.resize(indexes.size(), 0.5f);
}
void UARTInput::Poll()
{
if (!refresh_uart_) {
// What baud rate is the UART running at?
// Print debug info about PIO Serial state
DEBUG_PRINT("PIO Serial Status - Initialized: ");
DEBUG_PRINT(Serial1 ? "Yes" : "No");
DEBUG_PRINT(", Target Baud Rate: ");
DEBUG_PRINTLN(baud_rate_);
// Re-apply pin config before begin() — calling begin() without setTX/setRX
// would reset Serial1 to default pins, discarding the constructor's setup.
Serial1.setTX(sensor_tx_);
Serial1.setRX(sensor_rx_);
Serial1.begin(baud_rate_);
DEBUG_PRINTLN("PIO_UART refreshed.");
DEBUG_PRINT("Serial available: ");
DEBUG_PRINTLN(Serial1.available());
refresh_uart_ = true;
}
while (true) {
int raw = Serial1.read();
if (raw < 0) break; // no more data
uint8_t spiByte = static_cast<uint8_t>(raw);
//DEBUG_PRINTF("%02X ", spiByte);
switch(spiState) {
case SPISTATES::WAITFOREND:
if (spiByte == SLIP::END) {
slipBuffer[0] = SLIP::END;
spiState = SPISTATES::ENDORBYTES;
}
break;
case SPISTATES::ENDORBYTES:
if (spiByte == SLIP::END) {
spiIdx = 1;
} else {
slipBuffer[1] = spiByte;
spiIdx = 2;
}
spiState = SPISTATES::READBYTES;
break;
case SPISTATES::READBYTES:
if (spiIdx < static_cast<int>(kSlipBufferSize_)) {
slipBuffer[spiIdx++] = spiByte;
if (spiByte == SLIP::END) {
//DEBUG_PRINTLN(); // Newline after hex bytes
// Safe decode into fixed buffer
//uint8_t outBuf[sizeof(spiMessage)] = {0};
//size_t maxOut = sizeof(spiMessage);
//size_t got = SLIP::decode(slipBuffer, spiIdx, outBuf, maxOut);
float outBuf[kMaxChannels] = {0};
size_t maxOut = sizeof(outBuf);
size_t got = SLIP::decode(slipBuffer, spiIdx,
reinterpret_cast<uint8_t*>(outBuf), maxOut);
// DEBUG_PRINTF("Got %zu bytes\n", got);
{
//spiMessage msg;
//memcpy(&msg, outBuf, maxOut);
//Parse_(msg);
ParseBuf_(outBuf, got >> 2);
}
// Reset for next packet
spiState = SPISTATES::WAITFOREND;
spiIdx = 0;
}
} else {
DEBUG_PRINTLN("UARTInput: SLIP buffer overrun, dropping packet");
spiState = SPISTATES::WAITFOREND;
spiIdx = 0;
}
break;
}
}
}
// void UARTInput::Parse_(spiMessage msg)
// {
// static const float kEventThresh = 0.001;
// DEBUG_PRINTLN(String("-- Chan ") + msg.msg + String(" Value ") + msg.value);
// // Find if this message's index is in our tracked indexes
// auto index = where(sensor_indexes_, msg.msg);
// if (index >= 0) {
// // Protect against infs and nans
// if (std::isnan(msg.value) || std::isinf(msg.value)) {
// msg.value = value_states_[index];
// }
// float filtered_value = filters_[index].process(msg.value);
// float prev_value = value_states_[index];
// // Trigger callback whenever any value has changed
// if (std::abs(filtered_value - prev_value) > kEventThresh) {
// if (callback_) callback_(msg.msg, filtered_value);
// }
// // Print the value (Arduino scope) if it's the observed channel
// if (kObservedChan == msg.msg) {
// DEBUG_PRINT("Low:0.00,High:1.00,Value:");
// DEBUG_PRINT(msg.value, 8);
// DEBUG_PRINT(",FilteredValue:");
// DEBUG_PRINTLN(filtered_value, 8);
// }
// value_states_[index] = filtered_value;
// }
// }
void UARTInput::ParseBuf_(float* buf, size_t len)
{
static const float kEventThresh = 0.01f;
bool changed = false;
for (size_t i = 0; i < len; i++) {
//size_t chan = sensor_indexes_[i];
//if (chan < kMaxChannels) {
if (i < kMaxChannels) {
float raw_value = buf[i];
// Protect against infs and nans
if (std::isnan(raw_value) || std::isinf(raw_value)) {
raw_value = value_states_[i];
}
if (callback_) {
callback_(i, raw_value);
}
// float filtered_value = filters_[i].process(raw_value);
// float prev_value = value_states_[i];
// Trigger callback whenever any value has changed
// if (std::abs(filtered_value - prev_value) > kEventThresh) {
// changed = true;
// Serial.println("UART input: " + String(i) + " value: " + String(filtered_value));
// }
// // Print the value (Arduino scope) if it's the observed channel
// if (kObservedChan == i) {
// DEBUG_PRINT("Low:0.00,High:1.00,Value:");
// DEBUG_PRINT(raw_value, 8);
// DEBUG_PRINT(",FilteredValue:");
// DEBUG_PRINTLN(filtered_value, 8);
// }
value_states_[i] = raw_value;
}
}
}

View file

@ -0,0 +1,88 @@
#ifndef __UART_INPUT_HPP__
#define __UART_INPUT_HPP__
#include <Arduino.h>
#include "../utils/MedianFilter.h"
#include "../hardware/memlnaut/Pins.hpp"
#include <SerialPIO.h>
#include <functional>
#include <vector>
class UARTInput
{
public:
// Maximum allowed channels (avoid unbounded resizes)
static constexpr size_t kMaxChannels = 8;
// Change to trigger debugging of single channel
static constexpr size_t kObservedChan = 9999;
using uart_in_callback_t = std::function<void(size_t, float)>;
/**
* @brief Construct a new UARTInput object for communication
* with the MEML Sensor Board.
*
* @param sensor_indexes Vector of indexes to read (maximum 8, numbers 0-7).
* @param sensor_rx RX pin the Sensor Board is connected to (default: Pins::SENSOR_RX).
* @param sensor_tx TX pin the Sensor Board is connected to (default: Pins::SENSOR_TX).
*/
UARTInput(const std::vector<size_t> &sensor_indexes,
size_t sensor_rx = Pins::SENSOR_RX,
size_t sensor_tx = Pins::SENSOR_TX,
size_t baud_rate = 115200);
/**
* @brief Poll input. Put in a regular loop.
*/
void Poll();
/**
* @brief Set the callback to be called when sensor data changes.
*
* @param callback Callback that accepts a vector of sensor readings.
*/
inline void SetCallback(uart_in_callback_t callback)
{
callback_ = callback;
}
/**
* @brief Change the sensor indexes to listen to.
*
* @param indexes Vector of indexes to read (maximum 8, numbers 0-7).
*/
void ListenToSensorIndexes(const std::vector<size_t> &indexes);
protected:
static const size_t kSlipBufferSize_ = 128;
std::vector<size_t> sensor_indexes_;
size_t sensor_rx_;
size_t sensor_tx_;
uint8_t slipBuffer[kSlipBufferSize_];
std::vector<MedianFilter<float>> filters_;
std::vector<float> value_states_;
uart_in_callback_t callback_ = nullptr;
bool refresh_uart_;
size_t baud_rate_;
struct spiMessage
{
uint8_t msg;
float value;
};
enum SPISTATES
{
WAITFOREND,
ENDORBYTES,
READBYTES
};
SPISTATES spiState;
int spiIdx;
// void Parse_(spiMessage msg);
void ParseBuf_(float* buf, size_t len = kMaxChannels);
private:
//SerialPIO pioSerial_;
};
#endif // __UART_INPUT_HPP__

View file

@ -0,0 +1,47 @@
#include "UARTOutput.hpp"
UARTOutput::UARTOutput(int txPin)
: pioSerial_(txPin, NOPIN) // TX only, no RX pin
{
// Start the PIO-based serial port at 115200 baud
pioSerial_.begin(115200);
}
void UARTOutput::SendParams(const std::vector<float> &params)
{
// Encode each float (4 bytes) via SLIP
for(const auto& f : params)
{
union {
float f;
uint8_t b[4];
} data;
data.f = f;
for(int i = 0; i < 4; i++)
{
slipSendByte(data.b[i]);
}
}
// Finally, write the SLIP_END marker
pioSerial_.write(SLIP_END);
}
void UARTOutput::slipSendByte(uint8_t b)
{
if(b == SLIP_END)
{
pioSerial_.write(SLIP_ESC);
pioSerial_.write(SLIP_ESC_END);
}
else if(b == SLIP_ESC)
{
pioSerial_.write(SLIP_ESC);
pioSerial_.write(SLIP_ESC_ESC);
}
else
{
pioSerial_.write(b);
}
}

View file

@ -0,0 +1,49 @@
#ifndef __UART_OUTPUT_HPP__
#define __UART_OUTPUT_HPP__
#include <Arduino.h>
#include <SerialPIO.h>
#include "../hardware/memlnaut/Pins.hpp"
#include <vector>
/**
* @brief Sends float data to an external device using
* SLIP encoding over a PIO-based
* "software" UART on a single GPIO pin (txPin).
*/
class UARTOutput
{
public:
/**
* @param txPin The GPIO pin on the Raspberry Pi Pico to use for TX.
* Must be a valid pin for PIO-based UART.
* For example, "16" for GP16.
*/
UARTOutput(int txPin = Pins::DAISY_TX);
/**
* @brief SLIP-encodes the float vector and sends it as one packet.
* @param params Vector of floats to send to the external board.
*/
void SendParams(const std::vector<float> &params);
private:
/**
* @brief Helper to apply SLIP-escaping on a single byte.
*/
void slipSendByte(uint8_t b);
// PIO-based Serial
SerialPIO pioSerial_;
// SLIP special characters
static constexpr uint8_t SLIP_END = 0xC0; ///< End of SLIP packet
static constexpr uint8_t SLIP_ESC = 0xDB; ///< Escape character
static constexpr uint8_t SLIP_ESC_END = 0xDC; ///< Escaped END
static constexpr uint8_t SLIP_ESC_ESC = 0xDD; ///< Escaped ESC
};
#endif // __UART_OUTPUT_HPP__

View file

@ -0,0 +1,105 @@
/**
* @file USeqI2C.cpp
* @brief USeq I2C interface implementation
*
* @copyright Copyright (c) 2024. This Source Code Form is subject to the terms
* of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "USeqI2C.hpp"
USeqI2C::USeqI2C(uint8_t slave_address)
: slave_address_(slave_address), initialized_(false) {
// Initialize send buffer to zeros
memset(send_buffer_, 0, sizeof(send_buffer_));
}
USeqI2C::~USeqI2C() {
if (initialized_) {
Wire1.end();
}
}
bool USeqI2C::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t frequency) {
if (initialized_) {
Wire1.end();
}
// Initialize Wire1 with specified pins (matching reference pattern exactly)
Wire1.setSDA(sda_pin);
Wire1.setSCL(scl_pin);
Wire1.begin();
Wire1.setTimeout(2); // 2 ms timeout to avoid blocking core 0 on unresponsive device
delay(10); // Match reference implementation delay
// Only set clock if frequency is different from default
// if (frequency != 100000) {
Wire1.setClock(frequency);
// }
initialized_ = true;
DEBUG_PRINT("USeqI2C initialized - SDA: ");
DEBUG_PRINT(sda_pin);
DEBUG_PRINT(", SCL: ");
DEBUG_PRINT(scl_pin);
DEBUG_PRINT(", Frequency: ");
DEBUG_PRINT(frequency);
DEBUG_PRINT(" Hz, Slave Address: ");
DEBUG_PRINTLN(slave_address_);
return true;
}
bool USeqI2C::sendValues(const std::vector<float>& values) {
if (!initialized_) {
return false;
}
if (values.empty()) {
return false;
}
size_t count = values.size();
if (count > kMaxValues) {
count = kMaxValues;
}
// Copy values to internal buffer
for (size_t i = 0; i < count; i++) {
send_buffer_[i] = values[i];
}
return transmit_(send_buffer_, count * sizeof(float));
}
bool USeqI2C::sendValues(const float* values, size_t count) {
if (!initialized_) {
return false;
}
if (values == nullptr || count == 0) {
return false;
}
if (count > kMaxValues) {
count = kMaxValues;
}
// Copy values to internal buffer
memcpy(send_buffer_, values, count * sizeof(float));
return transmit_(send_buffer_, count * sizeof(float));
}
bool USeqI2C::transmit_(const void* data, size_t size) {
// Serial.printf("Transmitting %d bytes to I2C slave 0x%02X\n", size, slave_address_);
Wire1.beginTransmission(slave_address_);
size_t written = Wire1.write(static_cast<const uint8_t*>(data), size);
int result = Wire1.endTransmission(true);
return (result == 0 && written == size);
}

View file

@ -0,0 +1,105 @@
/**
* @file USeqI2C.hpp
* @brief USeq I2C interface for sending CV data
*
* @copyright Copyright (c) 2024. This Source Code Form is subject to the terms
* of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#ifndef __USEQ_I2C_HPP__
#define __USEQ_I2C_HPP__
#include <Arduino.h>
#include <Wire.h>
#include <vector>
#include "../hardware/memlnaut/Pins.hpp"
/**
* @brief USeq I2C interface class for sending CV data
*
* This class manages I2C communication using Wire1 interface to send
* float arrays for CV output. Designed to work with euclidean rhythm
* generators and other musical applications.
*/
class USeqI2C {
public:
/**
* @brief Default I2C slave address for USeq device
*/
static constexpr uint8_t kDefaultSlaveAddress = 1;
/**
* @brief Maximum number of values that can be sent in one transmission
*/
static constexpr size_t kMaxValues = 8;
/**
* @brief Constructor
* @param slave_address I2C slave address (default: 1)
*/
explicit USeqI2C(uint8_t slave_address = kDefaultSlaveAddress);
/**
* @brief Destructor
*/
~USeqI2C();
/**
* @brief Initialize the I2C interface
* @param sda_pin SDA pin number (default: USEQ_SDA from Pins.hpp)
* @param scl_pin SCL pin number (default: USEQ_SCL from Pins.hpp)
* @param frequency I2C frequency in Hz (default: 100000)
* @return true if initialization successful, false otherwise
*/
bool begin(uint8_t sda_pin = Pins::USEQ_SDA, uint8_t scl_pin = Pins::USEQ_SCL, uint32_t frequency = 1000000);
/**
* @brief Send a vector of float values via I2C
* @param values Vector of float values to send
* @return true if transmission successful, false otherwise
*/
bool sendValues(const std::vector<float>& values);
/**
* @brief Send an array of float values via I2C
* @param values Pointer to float array
* @param count Number of values to send
* @return true if transmission successful, false otherwise
*/
bool sendValues(const float* values, size_t count);
/**
* @brief Check if I2C interface is initialized
* @return true if initialized, false otherwise
*/
bool isInitialized() const { return initialized_; }
/**
* @brief Get the current slave address
* @return Current I2C slave address
*/
uint8_t getSlaveAddress() const { return slave_address_; }
/**
* @brief Set a new slave address
* @param slave_address New I2C slave address
*/
void setSlaveAddress(uint8_t slave_address) { slave_address_ = slave_address; }
private:
uint8_t slave_address_; ///< I2C slave address
bool initialized_; ///< Initialization status
float send_buffer_[kMaxValues]; ///< Internal buffer for sending data
float read_buffer_[kMaxValues]; ///< Internal buffer for sending data
/**
* @brief Internal function to perform I2C transmission
* @param data Pointer to data to send
* @param size Size of data in bytes
* @return true if transmission successful, false otherwise
*/
bool transmit_(const void* data, size_t size);
};
#endif // __USEQ_I2C_HPP__

View file

@ -0,0 +1,6 @@
#ifndef MEMLLIB_SYNTH_ADSRLITE_HPP
#define MEMLLIB_SYNTH_ADSRLITE_HPP
#endif // MEMLLIB_SYNTH_ADSRLITE_HPP

View file

@ -0,0 +1,466 @@
#pragma once
#include <vector>
#include <array>
#include <cstdint>
#include <cmath>
#include <algorithm>
#include "pico/critical_section.h"
#include "pico/time.h"
class DynamicSliceSelector {
public:
static constexpr int kN_Params = 4;
static constexpr int kMaxSlices = 64; // Maximum number of slices
static constexpr int kGridResolution = 16; // 16th note grid
static constexpr int kPhraseLength = 16; // 16-beat phrase structure
struct SlicePoint {
uint32_t start_sample;
uint32_t length_samples;
bool active;
float probability;
};
struct DrumLoop {
const float* samples;
uint32_t length_samples;
uint32_t sample_rate;
};
private:
// Parameters (0-1 range)
float slice_probability_; // Overall probability of slicing
float clustering_factor_; // How much slices cluster together
float phrase_awareness_; // How much to respect phrase structure
float grid_tightness_; // How strictly to follow the grid
// Thread-safe double buffer for slice points
std::array<SlicePoint, kMaxSlices> slice_points_a_;
std::array<SlicePoint, kMaxSlices> slice_points_b_;
const std::array<SlicePoint, kMaxSlices>* active_slices_;
std::array<SlicePoint, kMaxSlices>* update_slices_;
// Playback state (accessed in ISR)
uint32_t playback_position_; // Current position in drum loop
uint32_t current_slice_idx_; // Current slice being played
uint32_t slice_start_position_; // Where current slice started
uint32_t slice_end_position_; // Where current slice ends
bool slice_transition_pending_; // Flag for slice boundary crossing
// Thread synchronization
critical_section_t cs_;
volatile bool update_pending_; // Flag that new slices are ready
volatile int active_num_slices_; // Thread-safe copy of slice count
// Crossfade state for smooth transitions
static constexpr int kCrossfadeSamples = 64; // Short crossfade for glitch-free transitions
float crossfade_buffer_[kCrossfadeSamples];
int crossfade_position_;
bool crossfading_;
// Loop management
uint32_t loop_start_time_; // For tempo-sync if needed
bool loop_active_;
DrumLoop drum_loop_;
// Phrase awareness weights (higher = more likely to slice)
static constexpr std::array<float, kPhraseLength> phrase_weights_ = {
1.0f, 0.6f, 0.8f, 0.6f, // Beat 1: strong, off-beats weaker
0.9f, 0.5f, 0.7f, 0.5f, // Beat 2: backbeat emphasis
0.8f, 0.6f, 0.9f, 0.6f, // Beat 3: syncopation
1.0f, 0.7f, 0.8f, 0.9f // Beat 4: build to next phrase
};
// Simple PRNG for deterministic behavior
uint32_t rng_state_;
float FastRandom() {
rng_state_ = rng_state_ * 1664525u + 1013904223u;
return (rng_state_ & 0x00FFFFFF) / float(0x01000000);
}
// Fast approximation of exponential function for clustering
float FastExp(float x) {
if (x < -5.0f) return 0.0f;
if (x > 5.0f) return 1.0f;
// Taylor series approximation for small x
float x2 = x * x;
return 1.0f + x + 0.5f * x2 + 0.166667f * x2 * x;
}
float CalculateClusteringWeight(int grid_pos, int last_slice_pos) {
if (last_slice_pos == -1) return 1.0f;
float distance = abs(grid_pos - last_slice_pos);
float cluster_strength = clustering_factor_ * 4.0f - 2.0f; // Map to [-2, 2]
// Exponential decay/growth based on distance
float weight = FastExp(-cluster_strength * distance * 0.25f);
// Clamp to reasonable range
return std::max(0.1f, std::min(2.0f, weight));
}
float CalculatePhraseWeight(int grid_pos) {
int phrase_pos = grid_pos % kPhraseLength;
float base_weight = phrase_weights_[phrase_pos];
// Blend with uniform distribution based on phrase_awareness
return base_weight * phrase_awareness_ + (1.0f - phrase_awareness_);
}
void UpdatePlaybackState() {
// Check if we need to handle slice updates (called from ISR)
if (update_pending_) {
// Find current slice based on new slice configuration
bool found_current_slice = false;
for (int i = 0; i < active_num_slices_; ++i) {
if ((*active_slices_)[i].active) {
uint32_t slice_start = (*active_slices_)[i].start_sample;
uint32_t slice_end = slice_start + (*active_slices_)[i].length_samples;
// Check if current playback position falls within this slice
if (playback_position_ >= slice_start && playback_position_ < slice_end) {
// Only update if we're actually changing slices
if (current_slice_idx_ != i) {
current_slice_idx_ = i;
slice_start_position_ = slice_start;
slice_end_position_ = slice_end;
// Start crossfade if we're mid-playback
if (playback_position_ > slice_start + kCrossfadeSamples) {
crossfading_ = true;
crossfade_position_ = 0;
}
}
found_current_slice = true;
break;
}
}
}
// If no slice found, default to first slice
if (!found_current_slice && active_num_slices_ > 0) {
current_slice_idx_ = 0;
slice_start_position_ = (*active_slices_)[0].start_sample;
slice_end_position_ = slice_start_position_ + (*active_slices_)[0].length_samples;
playback_position_ = slice_start_position_;
}
update_pending_ = false;
}
}
float GetCrossfadedSample(uint32_t pos_a, uint32_t pos_b, float mix) {
float sample_a = (pos_a < drum_loop_.length_samples) ? drum_loop_.samples[pos_a] : 0.0f;
float sample_b = (pos_b < drum_loop_.length_samples) ? drum_loop_.samples[pos_b] : 0.0f;
return sample_a * (1.0f - mix) + sample_b * mix;
}
void GenerateSlicePoints() {
// Work on the update buffer
auto& slice_points = *update_slices_;
int num_slices = 0;
if (drum_loop_.samples == nullptr || drum_loop_.length_samples == 0) {
return;
}
// Calculate samples per grid position
float samples_per_grid = drum_loop_.length_samples / float(kGridResolution);
int last_slice_pos = -1;
// Evaluate each grid position
for (int grid_pos = 0; grid_pos < kGridResolution && num_slices < kMaxSlices - 1; ++grid_pos) {
// Calculate base probability factors
float clustering_weight = CalculateClusteringWeight(grid_pos, last_slice_pos);
float phrase_weight = CalculatePhraseWeight(grid_pos);
// Combined probability
float final_probability = slice_probability_ * clustering_weight * phrase_weight;
// Clamp probability
final_probability = std::max(0.0f, std::min(1.0f, final_probability));
// Probabilistic decision
if (FastRandom() < final_probability) {
// Calculate slice start position with optional grid looseness
float base_sample = grid_pos * samples_per_grid;
float jitter = (FastRandom() - 0.5f) * samples_per_grid * 0.1f * (1.0f - grid_tightness_);
uint32_t start_sample = uint32_t(std::max(0.0f, base_sample + jitter));
// Ensure we don't exceed bounds
if (start_sample < drum_loop_.length_samples) {
slice_points[num_slices].start_sample = start_sample;
slice_points[num_slices].active = true;
slice_points[num_slices].probability = final_probability;
last_slice_pos = grid_pos;
num_slices++;
}
}
}
// Always ensure we have at least one slice at the beginning
if (num_slices == 0) {
slice_points[0].start_sample = 0;
slice_points[0].active = true;
slice_points[0].probability = 1.0f;
num_slices = 1;
}
// Calculate slice lengths
for (int i = 0; i < num_slices; ++i) {
uint32_t next_start = (i + 1 < num_slices) ?
slice_points[i + 1].start_sample :
drum_loop_.length_samples;
slice_points[i].length_samples = next_start - slice_points[i].start_sample;
}
// Mark remaining slices as inactive
for (int i = num_slices; i < kMaxSlices; ++i) {
slice_points[i].active = false;
}
// Atomically update the active buffer
critical_section_enter_blocking(&cs_);
std::swap(active_slices_, update_slices_);
active_num_slices_ = num_slices;
update_pending_ = true;
critical_section_exit(&cs_);
}
public:
DynamicSliceSelector() :
slice_probability_(0.5f),
clustering_factor_(0.5f),
phrase_awareness_(0.7f),
grid_tightness_(0.8f),
active_slices_(&slice_points_a_),
update_slices_(&slice_points_b_),
playback_position_(0),
current_slice_idx_(0),
slice_start_position_(0),
slice_end_position_(0),
slice_transition_pending_(false),
active_num_slices_(0),
update_pending_(false),
crossfade_position_(0),
crossfading_(false),
loop_start_time_(0),
loop_active_(false),
drum_loop_{nullptr, 0, 44100},
rng_state_(12345) {
// Initialize critical section
critical_section_init(&cs_);
// Initialize slice points
for (auto& slice : slice_points_a_) {
slice.start_sample = 0;
slice.length_samples = 0;
slice.active = false;
slice.probability = 0.0f;
}
for (auto& slice : slice_points_b_) {
slice.start_sample = 0;
slice.length_samples = 0;
slice.active = false;
slice.probability = 0.0f;
}
// Initialize crossfade buffer
for (int i = 0; i < kCrossfadeSamples; ++i) {
crossfade_buffer_[i] = 0.0f;
}
}
~DynamicSliceSelector() {
critical_section_deinit(&cs_);
}
void SetDrumLoop(const float* samples, uint32_t length_samples, uint32_t sample_rate = 44100) {
// This should be called from main thread only
critical_section_enter_blocking(&cs_);
drum_loop_.samples = samples;
drum_loop_.length_samples = length_samples;
drum_loop_.sample_rate = sample_rate;
// Reset playback state
playback_position_ = 0;
current_slice_idx_ = 0;
slice_start_position_ = 0;
slice_end_position_ = length_samples;
crossfading_ = false;
loop_active_ = true;
critical_section_exit(&cs_);
// Regenerate slices with current parameters
GenerateSlicePoints();
}
void ProcessParams(const std::vector<float>& params) {
if (params.size() != kN_Params) return;
slice_probability_ = params[0];
clustering_factor_ = params[1];
phrase_awareness_ = params[2];
grid_tightness_ = params[3];
// Regenerate slice points with new parameters
GenerateSlicePoints();
}
// Real-time audio processing function - called from DMA interrupt
float Process() {
if (!loop_active_ || drum_loop_.samples == nullptr || drum_loop_.length_samples == 0) {
return 0.0f;
}
// Update playback state if parameters changed
UpdatePlaybackState();
float output_sample = 0.0f;
// Handle crossfading between slices
if (crossfading_) {
// Get current sample and previous slice sample for crossfade
float current_sample = (playback_position_ < drum_loop_.length_samples) ?
drum_loop_.samples[playback_position_] : 0.0f;
float fade_ratio = float(crossfade_position_) / float(kCrossfadeSamples);
// Simple linear crossfade
output_sample = crossfade_buffer_[crossfade_position_] * (1.0f - fade_ratio) +
current_sample * fade_ratio;
crossfade_position_++;
if (crossfade_position_ >= kCrossfadeSamples) {
crossfading_ = false;
crossfade_position_ = 0;
}
} else {
// Normal playback
output_sample = (playback_position_ < drum_loop_.length_samples) ?
drum_loop_.samples[playback_position_] : 0.0f;
}
// Advance playback position
playback_position_++;
// Check if we've reached the end of current slice
if (playback_position_ >= slice_end_position_) {
// Find next slice
bool found_next_slice = false;
// First, try to find a slice that starts at or after current position
for (int i = 0; i < active_num_slices_; ++i) {
if ((*active_slices_)[i].active &&
(*active_slices_)[i].start_sample >= playback_position_) {
// Prepare crossfade buffer with current slice end
for (int j = 0; j < kCrossfadeSamples && j < slice_end_position_ - slice_start_position_; ++j) {
uint32_t pos = slice_end_position_ - kCrossfadeSamples + j;
crossfade_buffer_[j] = (pos < drum_loop_.length_samples) ?
drum_loop_.samples[pos] : 0.0f;
}
// Jump to new slice
current_slice_idx_ = i;
slice_start_position_ = (*active_slices_)[i].start_sample;
slice_end_position_ = slice_start_position_ + (*active_slices_)[i].length_samples;
playback_position_ = slice_start_position_;
crossfading_ = true;
crossfade_position_ = 0;
found_next_slice = true;
break;
}
}
// If no slice found ahead, loop back to beginning
if (!found_next_slice) {
// Prepare crossfade buffer
for (int j = 0; j < kCrossfadeSamples; ++j) {
uint32_t pos = slice_end_position_ - kCrossfadeSamples + j;
crossfade_buffer_[j] = (pos < drum_loop_.length_samples) ?
drum_loop_.samples[pos] : 0.0f;
}
// Go to first slice
current_slice_idx_ = 0;
if (active_num_slices_ > 0 && (*active_slices_)[0].active) {
slice_start_position_ = (*active_slices_)[0].start_sample;
slice_end_position_ = slice_start_position_ + (*active_slices_)[0].length_samples;
} else {
slice_start_position_ = 0;
slice_end_position_ = drum_loop_.length_samples;
}
playback_position_ = slice_start_position_;
crossfading_ = true;
crossfade_position_ = 0;
}
}
return output_sample;
}
// Get current slice configuration (thread-safe)
const SlicePoint* GetSlicePoints() const {
return active_slices_->data();
}
int GetNumActiveSlices() const {
return active_num_slices_;
}
// Get current playback info (for debugging/visualization)
uint32_t GetPlaybackPosition() const {
return playback_position_;
}
int GetCurrentSliceIndex() const {
return current_slice_idx_;
}
// Get slice at specific position (for external playback systems)
const SlicePoint* GetSliceAtPosition(uint32_t sample_position) const {
for (int i = 0; i < active_num_slices_; ++i) {
if ((*active_slices_)[i].active &&
sample_position >= (*active_slices_)[i].start_sample &&
sample_position < (*active_slices_)[i].start_sample + (*active_slices_)[i].length_samples) {
return &(*active_slices_)[i];
}
}
return nullptr;
}
// Debug info (thread-safe)
void PrintSliceInfo() const {
critical_section_enter_blocking(const_cast<critical_section_t*>(&cs_));
// Only implement if you have debug output capability
// for (int i = 0; i < active_num_slices_; ++i) {
// printf("Slice %d: start=%u, length=%u, prob=%.2f\n",
// i, (*active_slices_)[i].start_sample,
// (*active_slices_)[i].length_samples,
// (*active_slices_)[i].probability);
// }
// printf("Playback: pos=%u, slice=%d, crossfade=%d\n",
// playback_position_, current_slice_idx_, crossfading_);
critical_section_exit(const_cast<critical_section_t*>(&cs_));
}
};

View file

@ -0,0 +1,182 @@
#include "FMSynth.hpp"
#include <cmath>
#include <random>
#include <cstdlib>
#include <vector>
void FMSynth::GenParams(std::vector<float> &param_vector)
{
#if 0
std::random_device rd; // Will be used to obtain a seed for the random number engine
std::mt19937 gen(rd()); // Standard mersenne_twister_engine seeded with rd()
std::uniform_real_distribution<float> dis(0.f, 1.0f);
#else
float rand_scale = 1.f / static_cast<float>(RAND_MAX);
#endif
//printf("Calling FMSynth::GenParams\n");
for(size_t i=0; i < kN_synthparams; i++) {
param_vector[i] = std::rand() * rand_scale;
//printf(".");
}
//printf("\n");
}
void FMSynth::UpdateParams() {
op1.UpdateParams();
op2.UpdateParams();
op3.UpdateParams();
op4.UpdateParams();
}
FMSynth::FMSynth(float sample_rate) :
smoother_(100.f, sample_rate),
envelope_smoother_(10.f, sample_rate),
note_freq_(0),
note_amplitude_(0),
play_note_(false),
midi_enabled_(false)
{
// std::srand(0);
maxiSettings::setup(sample_rate, 1, 16);
UpdateParams();
std::vector<float> randParams(kN_synthparams);
GenParams(randParams);
mapParameters(randParams);
}
void FMSynth::mapParameters(const std::vector<float> &params) {
const float *params_ptr = params.data();
float *dest_ptr = synthparams.data();
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 5000);
++params_ptr;
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 3000);
++params_ptr;
*dest_ptr++ = (*(params_ptr++) * 200);
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 5000);
++params_ptr;
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 5000);
++params_ptr;
*dest_ptr++ = (*(params_ptr++) * 200);
*dest_ptr++ = (*(params_ptr++) * 200);
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 5000);
++params_ptr;
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 1000);
++params_ptr;
*dest_ptr++ = (params[9] * 200);
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 5000);
++params_ptr;
*dest_ptr++ = 20 + ((*(params_ptr) * *(params_ptr)) * 800);
++params_ptr;
*dest_ptr++ = (*(params_ptr++) * 100);
*dest_ptr++ = (*(params_ptr++) * 200);
}
inline float midiNoteToFrequency(int midiNote) {
// Constants
constexpr float A440 = 440.0f; // Frequency of A4
constexpr float SEMITONE_RATIO = 1.059463094359f; // 2^(1/12), the ratio between adjacent semitones
// Middle C (C4) is MIDI note 60, A4 is MIDI note 69
int semitoneOffset = midiNote - 69;
// Calculate the frequency
return A440 * std::pow(SEMITONE_RATIO, semitoneOffset);
}
size_t sampleIdx=0;
maxiOsc tmposc;
float FMSynth::process()
{
// Smooth all parameters before using them
smoother_.Process(synthparams.data(), synthparams_smoothed.data());
float carrier_1, carrier_2, envelope;
// Handle MIDI
#if 1
if (midi_enabled_) {
#else
if (false) {
#endif
carrier_1 = note_freq_;
carrier_2 = carrier_1;
if (play_note_ == false) {
// No notes to play
envelope = 0;
} else {
// One note to play!
envelope = note_amplitude_;
}
// Smooth envelope
float envelope_smoothed;
envelope_smoother_.Process(&envelope, &envelope_smoothed);
envelope = envelope_smoothed;
} else {
carrier_1 = synthparams_smoothed[0] * 0.2f;
carrier_2 = synthparams_smoothed[7] * 0.6;
envelope = 1.0f;
}
#if 1
float w = op1.play(carrier_1 +
(op2.play(synthparams_smoothed[3],synthparams_smoothed[4],synthparams_smoothed[5]) * synthparams_smoothed[6]),
synthparams_smoothed[1], synthparams_smoothed[2]);
float w2 = op3.play(carrier_2 +
(op4.play(synthparams_smoothed[10],synthparams_smoothed[11],synthparams_smoothed[12]) * synthparams_smoothed[13]),
synthparams_smoothed[8], synthparams_smoothed[9]);
float y = (w + w2) * envelope;
// float y = op1.play(500,1, 1);
// if (sampleIdx++ %1000 == 0) {
// DEBUG_PRINTLN(y);
// }
return std::tanh(y);
#else
return op1.play(carrier_1, 0, 0) * envelope;
#endif
}
int32_t FMSynth::processInt()
{
static const float scaling = std::pow(2.f, 31.f) - 1000.f;
return static_cast<int32_t>(process() * scaling);
}
// void FMSynth::EnableMIDI(bool en)
// {
// midi_enabled_ = en;
// }
// void FMSynth::AddMIDINote(ts_midi_note note)
// {
// if (midi_enabled_) {
// if (note.velocity > 0) {
// // note_buffer_.push_back(note);
// note_freq_ = midiNoteToFrequency(note.note_number);
// note_amplitude_ = note.velocity;
// play_note_ = true;
// } else {
// // #if 0
// // note_buffer_.RemoveNote(note);
// // #else
// // note_buffer_.clear();
// // #endif
// play_note_ = false;
// }
// }
// }

View file

@ -0,0 +1,143 @@
#ifndef _FM_HPP
#define _FM_HPP
#include <Arduino.h>
#include <cstdint>
#include "maximilian.h"
#include <vector>
#include <array>
#include <cmath>
#include "OnePoleSmoother.hpp"
const size_t kN_synthparams = 14;
using synthparams_array = std::array<float, kN_synthparams>;
static constexpr size_t kN_notes = 4;
template <typename T, std::size_t Capacity>
class RingBuffer {
public:
RingBuffer() : head_(0), tail_(0), size_(0) {}
// Push an element to the back of the queue
bool push_back(const T& value) {
if (size_ < Capacity) {
buffer_[tail_] = value;
tail_ = (tail_ + 1) % Capacity;
++size_;
return true;
} else {
// Overwrite the oldest element if at full capacity
buffer_[tail_] = value;
tail_ = (tail_ + 1) % Capacity;
head_ = (head_ + 1) % Capacity;
return false;
}
}
// Remove an element at a specific position
bool remove_at(std::size_t index) {
if (index >= size_) return false;
std::size_t actual_index = (head_ + index) % Capacity;
for (std::size_t i = actual_index; i != tail_; i = (i + 1) % Capacity) {
std::size_t next_index = (i + 1) % Capacity;
buffer_[i] = buffer_[next_index];
}
tail_ = (tail_ + Capacity - 1) % Capacity;
--size_;
return true;
}
// Remove all elements with the same `note`
void RemoveNote(const T& target) {
std::size_t current = 0;
while (current < size_) {
std::size_t actual_index = (head_ + current) % Capacity;
if (buffer_[actual_index].note_number == target.note_number) {
remove_at(current);
// Do not increment `current` as the elements after the removed one shift left
} else {
++current;
}
}
}
// Clear the queue
void clear() {
head_ = 0;
tail_ = 0;
size_ = 0;
}
// Get a pointer to the last element
T* back() {
if (size_ == 0) return nullptr;
std::size_t last_index = (tail_ + Capacity - 1) % Capacity;
return &buffer_[last_index];
}
// Check if the buffer is empty
bool empty() const {
return size_ == 0;
}
// Get the current size of the buffer
std::size_t size() const {
return size_;
}
private:
std::array<T, Capacity> buffer_;
std::size_t head_; // Points to the start of valid data
std::size_t tail_; // Points to the next insertion point
std::size_t size_; // Number of valid elements
};
class FMOperator {
public:
void UpdateParams(void) {
carrier.UpdateParams();
modulator.UpdateParams();
}
float play(MAXITYPE carrierFreq, MAXITYPE modFreq, MAXITYPE index) {
float mod = modulator.sinebuf(modFreq);
float car = carrier.sinebuf(carrierFreq + (mod * index)) ;
return car;
}
private:
maxiOsc carrier, modulator;
};
class FMSynth {
public:
static void GenParams(std::vector<float> &param_vector);
FMSynth(float sample_rate);
float process();
int32_t processInt();
void mapParameters(const std::vector<float> &params);
// void EnableMIDI(bool en);
// void AddMIDINote(ts_midi_note note);
void UpdateParams();
private:
FMOperator op1, op2, op3, op4;
synthparams_array synthparams;
synthparams_array synthparams_smoothed;
// FMOperator fmops[10];
OnePoleSmoother<kN_synthparams> smoother_;
// MIDI
// RingBuffer<ts_midi_note, kN_notes> note_buffer_;
OnePoleSmoother<1> envelope_smoother_;
float note_freq_;
float note_amplitude_;
bool play_note_;
bool midi_enabled_;
};
#endif // _FM_HPP

View file

@ -0,0 +1,346 @@
#pragma once
#include <cmath>
#include <cstddef>
#include "maximilian.h"
#include "../audio/AudioDriver.hpp"
template<size_t BUFSIZE = 16384, size_t NGRAINS = 4>
class GrainDelayI16 {
static_assert((BUFSIZE & (BUFSIZE - 1)) == 0, "BUFSIZE must be a power of 2");
static constexpr size_t kEnvSize = 512;
static constexpr float kBufSizeF = static_cast<float>(BUFSIZE);
static constexpr float kNGrainGain = 2.0f / static_cast<float>(NGRAINS);
public:
void setup(float sample_rate) {
sample_rate_ = sample_rate;
for (size_t i = 0; i < kEnvSize; ++i) {
float phase = static_cast<float>(i) / static_cast<float>(kEnvSize);
env_[i] = 0.5f * (1.f - cosf(2.f * M_PI * phase));
}
for (size_t g = 0; g < NGRAINS; ++g) {
phase_[g] = static_cast<float>(g) / static_cast<float>(NGRAINS);
read_pos_[g] = 0.f;
}
updateGrainPitches();
}
float __force_inline process(float input, float externalFb = 0.f) {
if (!frozen_) buf_.write(input + out_ * feedback_ + externalFb + tap_out_ * tap_feedback_);
float sum = 0.f;
const float write_idx = static_cast<float>(buf_.getWriteIndex());
if (hasTap_) {
float tap_pos = write_idx - start_time_;
if (tap_pos < 0.f) tap_pos += kBufSizeF;
tap_out_ = buf_.readAbsolute(tap_pos);
}
for (size_t g = 0; g < NGRAINS; ++g) {
phase_[g] += phase_inc_;
if (phase_[g] >= 1.0f) {
phase_[g] -= 1.0f;
read_pos_[g] = write_idx - start_time_;
if (read_pos_[g] < 0.f) read_pos_[g] += kBufSizeF;
}
const size_t env_idx = static_cast<size_t>(phase_[g] * kEnvSize) & (kEnvSize - 1);
sum += buf_.readAbsolute(read_pos_[g]) * env_[env_idx];
read_pos_[g] += grain_pitch_[g];
if (read_pos_[g] >= kBufSizeF) read_pos_[g] -= kBufSizeF;
if (read_pos_[g] < 0.f) read_pos_[g] += kBufSizeF;
}
out_ = sum * kNGrainGain; // grains only — keeps tap out of the grain feedback path
return out_ + tap_out_ * tap_level_;
}
stereosample_t __force_inline processStereo(float input) {
if (!frozen_) buf_.write(input + out_ * feedback_ + tap_out_ * tap_feedback_);
float sumL = 0.f, sumR = 0.f;
const float write_idx = static_cast<float>(buf_.getWriteIndex());
if (hasTap_) {
float tap_pos = write_idx - start_time_;
if (tap_pos < 0.f) tap_pos += kBufSizeF;
tap_out_ = buf_.readAbsolute(tap_pos);
}
for (size_t g = 0; g < NGRAINS; ++g) {
phase_[g] += phase_inc_;
if (phase_[g] >= 1.0f) {
phase_[g] -= 1.0f;
read_pos_[g] = write_idx - start_time_;
if (read_pos_[g] < 0.f) read_pos_[g] += kBufSizeF;
}
const size_t env_idx = static_cast<size_t>(phase_[g] * kEnvSize) & (kEnvSize - 1);
const float grainOut = buf_.readAbsolute(read_pos_[g]) * env_[env_idx];
if (g & 1) sumR += grainOut;
else sumL += grainOut;
read_pos_[g] += grain_pitch_[g];
if (read_pos_[g] >= kBufSizeF) read_pos_[g] -= kBufSizeF;
if (read_pos_[g] < 0.f) read_pos_[g] += kBufSizeF;
}
out_ = (sumL + sumR) * kNGrainGain;
const float tapContrib = tap_out_ * tap_level_;
return { sumL * kNGrainGain + tapContrib,
sumR * kNGrainGain + tapContrib };
}
void setGrainLengthSamples(float samples) { phase_inc_ = 1.0f / samples; }
void setGrainLengthMs(float ms) { setGrainLengthSamples(ms * 0.001f * sample_rate_); }
void setStartTimeSamples(float s) { start_time_ = s; }
void setStartTimeMs(float ms) { setStartTimeSamples(ms * 0.001f * sample_rate_); }
void setFeedback(float fb) { feedback_ = fb; }
void setPitch(float ratio) { pitch_ = ratio; updateGrainPitches(); }
void setPitchSpread(float spread){ pitch_spread_ = spread; updateGrainPitches(); }
void setTapLevel(float level) { tap_level_ = level; hasTap_ = (tap_level_ > 0.f || tap_feedback_ > 0.f); }
void setTapFeedback(float fb) { tap_feedback_ = fb; hasTap_ = (tap_level_ > 0.f || tap_feedback_ > 0.f); }
void setFreeze(bool freeze) { frozen_ = freeze; }
void fillWithSaw(float freqHz) {
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float saw = phase * 2.f - 1.f;
saw -= polyBlep(phase, inc, inv_period);
cycle[i] = static_cast<int16_t>(saw * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
void fillWithSquare(float freqHz, float duty = 0.5f) {
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float sq = (phase < duty) ? 1.f : -1.f;
sq += polyBlep(phase, inc, inv_period);
float t2 = phase - duty;
if (t2 < 0.f) t2 += 1.f;
sq -= polyBlep(t2, inc, inv_period);
cycle[i] = static_cast<int16_t>(sq * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
void fillWithTriangle(float freqHz) {
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float tri = 2.f * fabsf(2.f * phase - 1.f) - 1.f;
tri += polyBlamp(phase, inc, inv_period);
tri -= polyBlamp(phase - 0.5f < 0.f ? phase + 0.5f : phase - 0.5f, inc, inv_period);
cycle[i] = static_cast<int16_t>(tri * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
void fillWithFallingSaw(float freqHz) {
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float saw = 1.f - phase * 2.f;
saw += polyBlep(phase, inc, inv_period);
cycle[i] = static_cast<int16_t>(saw * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
void fillWithSine(float freqHz) {
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float phase_inc = 2.f * static_cast<float>(M_PI) / static_cast<float>(period);
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
cycle[i] = static_cast<int16_t>(sinf(phase) * 32767.f);
phase += phase_inc;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
// Half-rectified sine: positive half of sine, zero for negative half.
// PolyBLAMP at both zero crossings (both are positive slope jumps).
void fillWithHalfRectSine(float freqHz) {
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
const float s = sinf(phase * 2.f * static_cast<float>(M_PI));
float val = s > 0.f ? s : 0.f;
val += polyBlamp(phase, inc, inv_period);
const float t2 = phase >= 0.5f ? phase - 0.5f : phase + 0.5f;
val += polyBlamp(t2, inc, inv_period);
cycle[i] = static_cast<int16_t>(val * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
// Trapezoidal: square with sloped edges. rise=0 → square, rise=0.5 → triangle.
// PolyBLAMP at all 4 corners.
void fillWithTrapezoid(float freqHz, float rise = 0.2f) {
rise = fmaxf(0.01f, fminf(0.49f, rise));
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float trap;
if (phase < rise) trap = phase / rise * 2.f - 1.f;
else if (phase < 0.5f) trap = 1.f;
else if (phase < 0.5f + rise) trap = 1.f - (phase - 0.5f) / rise * 2.f;
else trap = -1.f;
// corners: 0(+), rise(-), 0.5(-), 0.5+rise(+)
trap += polyBlamp(phase, inc, inv_period);
float t1 = phase - rise; if (t1 < 0.f) t1 += 1.f;
trap -= polyBlamp(t1, inc, inv_period);
float t2 = phase - 0.5f; if (t2 < 0.f) t2 += 1.f;
trap -= polyBlamp(t2, inc, inv_period);
float t3 = phase - (0.5f + rise); if (t3 < 0.f) t3 += 1.f;
trap += polyBlamp(t3, inc, inv_period);
cycle[i] = static_cast<int16_t>(trap * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
// Asymmetric triangle: peak position 0..1 (0.5 = symmetric triangle,
// 0 → falling saw, 1 → rising saw). PolyBLAMP at both corners.
void fillWithAsymTriangle(float freqHz, float peak = 0.5f) {
peak = fmaxf(0.01f, fminf(0.99f, peak));
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float tri = (phase < peak)
? (phase / peak * 2.f - 1.f)
: (1.f - (phase - peak) / (1.f - peak) * 2.f);
tri += polyBlamp(phase, inc, inv_period);
float t1 = phase - peak; if (t1 < 0.f) t1 += 1.f;
tri -= polyBlamp(t1, inc, inv_period);
cycle[i] = static_cast<int16_t>(tri * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
// Staircase: quantised rising saw. PolyBLEP corrects the wrap discontinuity;
// individual step transitions are small (2/(steps-1)) so their aliasing is mild.
void fillWithStaircase(float freqHz, size_t steps = 8) {
steps = std::max(size_t(2), std::min(steps, size_t(32)));
const size_t period = periodSamples(freqHz);
static int16_t cycle[kMaxPeriod];
const float inv_period = static_cast<float>(period);
const float inc = 1.f / inv_period;
const float fsteps = static_cast<float>(steps);
float phase = 0.f;
for (size_t i = 0; i < period; ++i) {
float saw = phase * 2.f - 1.f;
saw -= polyBlep(phase, inc, inv_period);
const float stair = roundf((saw + 1.f) * 0.5f * (fsteps - 1.f))
* 2.f / (fsteps - 1.f) - 1.f;
cycle[i] = static_cast<int16_t>(stair * 32767.f);
phase += inc;
if (phase >= 1.f) phase -= 1.f;
}
buf_.fillRepeating(cycle, period);
resetGrains();
}
private:
DynamicDelayI16<BUFSIZE> buf_;
float env_[kEnvSize] = {};
float phase_[NGRAINS] = {};
float read_pos_[NGRAINS] = {};
float phase_inc_ = 1.0f / 4800.f; // default 100ms at 48kHz
float start_time_ = 8000.f; // default ~167ms
float feedback_ = 0.3f;
float pitch_ = 1.0f;
float pitch_spread_ = 0.f;
float grain_pitch_[NGRAINS] = {};
float tap_level_ = 0.f;
float tap_feedback_= 0.f;
float tap_out_ = 0.f;
float out_ = 0.f;
bool frozen_ = false;
bool hasTap_ = false;
float sample_rate_= 48000.f;
static constexpr size_t kMaxPeriod = 4096;
size_t periodSamples(float freqHz) const {
return std::min(static_cast<size_t>(sample_rate_ / freqHz + 0.5f), kMaxPeriod);
}
void resetGrains() {
const float wi = static_cast<float>(buf_.getWriteIndex());
for (size_t g = 0; g < NGRAINS; ++g) {
read_pos_[g] = wi - start_time_;
if (read_pos_[g] < 0.f) read_pos_[g] += kBufSizeF;
phase_[g] = static_cast<float>(g) / static_cast<float>(NGRAINS);
}
out_ = 0.f;
tap_out_ = 0.f;
}
static float polyBlep(float t, float dt, float inv_dt) {
if (t < dt) { t *= inv_dt; return t + t - t*t - 1.f; }
else if (t > 1.f - dt) { t = (t - 1.f) * inv_dt; return t*t + t + t + 1.f; }
return 0.f;
}
// PolyBLAMP: corrects slope discontinuities (triangle wave corners)
static float polyBlamp(float t, float dt, float inv_dt) {
if (t < dt) { t *= inv_dt; return dt * ( t*t*t/3.f - t*t + 1.f); }
else if (t > 1.f - dt) { t = (t - 1.f) * inv_dt; return dt * (t*t*t/3.f + t*t + 1.f); }
return 0.f;
}
void updateGrainPitches() {
for (size_t g = 0; g < NGRAINS; ++g) {
float t = NGRAINS > 1
? static_cast<float>(g) / static_cast<float>(NGRAINS - 1)
: 0.5f;
grain_pitch_[g] = pitch_ + pitch_spread_ * (t - 0.5f);
}
}
};

View file

@ -0,0 +1,179 @@
#pragma once
#include "maximilian.h"
// ─────────────────────────────────────────────────────────────────────────────
// FlangerI16
// Short modulated delay (0BUFSIZE samples).
// process(input, delayBaseSamples, depth, ratHz, feedback, mix)
// mix 0 = dry, mix 1 = classic 50/50 flange
// ─────────────────────────────────────────────────────────────────────────────
template<size_t BUFSIZE = 1024>
class FlangerI16 {
static_assert((BUFSIZE & (BUFSIZE - 1)) == 0, "BUFSIZE must be a power of 2");
public:
float __force_inline process(float input, float delayBase,
float depth, float rate,
float feedback, float mix)
{
const float lfoVal = lfo_.sinewave(rate);
const float delayTime = fmaxf(1.f, delayBase * (1.f + depth * lfoVal));
const float out = buf_.read(delayTime);
buf_.write(input + out * feedback);
// mix=0 → dry; mix=1 → 50% dry + 50% delayed
return input * (1.f - mix * 0.5f) + out * (mix * 0.5f);
}
private:
DynamicDelayI16<BUFSIZE> buf_;
maxiOsc lfo_;
};
// ─────────────────────────────────────────────────────────────────────────────
// ChorusI16
// Two-voice chorus. Second LFO starts 180° offset and runs at a slightly
// different rate for a richer, detuned character.
// process(input, delayBaseSamples, depth, rateHz, feedback, mix)
// mix 0 = dry, mix 1 = 1/3 dry + 1/3 voice1 + 1/3 voice2
// ─────────────────────────────────────────────────────────────────────────────
template<size_t BUFSIZE = 4096>
class ChorusI16 {
static_assert((BUFSIZE & (BUFSIZE - 1)) == 0, "BUFSIZE must be a power of 2");
public:
ChorusI16() { lfo2_.phase = 0.5f; }
float __force_inline process(float input, float delayBase,
float depth, float rate,
float feedback, float mix)
{
const float lfo1 = lfo1_.sinewave(rate);
const float lfo2 = lfo2_.sinewave(rate * 1.02f);
const float out1 = buf1_.read(fmaxf(1.f, delayBase * (1.f + depth * lfo1)));
const float out2 = buf2_.read(fmaxf(1.f, delayBase * (1.f + depth * lfo2 * 0.97f)));
buf1_.write(input + out1 * feedback);
buf2_.write(input + out2 * feedback * 0.98f);
const float wet = (input + out1 + out2) * 0.3333f;
return input * (1.f - mix) + wet * mix;
}
private:
DynamicDelayI16<BUFSIZE> buf1_, buf2_;
maxiOsc lfo1_, lfo2_;
};
// ─────────────────────────────────────────────────────────────────────────────
// RingMod
// Multiply input by a sine carrier. mix=0 dry, mix=1 full ring mod.
// process(input, freqHz, mix)
// ─────────────────────────────────────────────────────────────────────────────
class RingMod {
public:
// Advance oscillator once and return carrier value — use for stereo.
float __force_inline carrier(float freqHz) { return osc_.sinewave(freqHz); }
float __force_inline process(float input, float freqHz, float mix)
{
const float c = carrier(freqHz);
return input * (1.f - mix) + (input * c) * mix;
}
private:
maxiOsc osc_;
};
// ─────────────────────────────────────────────────────────────────────────────
// StutterGate
// Rhythmic gate synced to a pre-computed period in samples.
// process(input, periodSamples, dutyCycle, mix)
// dutyCycle: 0-1 fraction of period that is open
// mix: 0 = dry (gate bypassed), 1 = full stutter
// ─────────────────────────────────────────────────────────────────────────────
class StutterGate {
public:
// Advance phase once and return gate multiplier (0 or 1) — use for stereo.
float __force_inline gateValue(float periodSamples, float dutyCycle)
{
phase_ += 1.f;
if (phase_ >= periodSamples) phase_ -= periodSamples;
return (phase_ / periodSamples < dutyCycle) ? 1.f : 0.f;
}
float __force_inline process(float input, float periodSamples, float dutyCycle, float mix)
{
const float gate = gateValue(periodSamples, dutyCycle);
return input * (1.f - mix) + input * gate * mix;
}
private:
float phase_{0.f};
};
// ─────────────────────────────────────────────────────────────────────────────
// BitCrusher
// Combines bit-depth reduction and sample-rate reduction.
// process(input, bits, rateDiv, mix)
// bits : effective bit depth (e.g. 4.0 = 4-bit)
// rateDiv: sample-hold period in samples (1 = off, 32 = extreme)
// mix : 0 = dry, 1 = full crush
// ─────────────────────────────────────────────────────────────────────────────
class BitCrusher {
public:
float __force_inline process(float input, float bits, float rateDiv, float mix)
{
if (++phase_ >= static_cast<int>(rateDiv)) {
phase_ = 0;
heldL_ = input;
}
const float levels = powf(2.f, bits) - 1.f;
return input * (1.f - mix) + roundf(heldL_ * levels) / levels * mix;
}
// Stereo version: advances phase once, holds L and R independently.
void __force_inline processStereo(float& L, float& R, float bits, float rateDiv, float mix)
{
if (++phase_ >= static_cast<int>(rateDiv)) {
phase_ = 0;
heldL_ = L;
heldR_ = R;
}
const float levels = powf(2.f, bits) - 1.f;
L = L * (1.f - mix) + roundf(heldL_ * levels) / levels * mix;
R = R * (1.f - mix) + roundf(heldR_ * levels) / levels * mix;
}
private:
float heldL_{0.f}, heldR_{0.f};
int phase_{0};
};
// ─────────────────────────────────────────────────────────────────────────────
// AllpassI16
// Schroeder allpass: y[n] = -g*x[n] + x[n-D] + g*y[n-D]
// Unity gain across all frequencies; adds diffuse phase dispersion.
// process(input, delaySamples, g)
// g: 01, typical 0.50.7
// ─────────────────────────────────────────────────────────────────────────────
template<size_t BUFSIZE = 4096>
class AllpassI16 {
static_assert((BUFSIZE & (BUFSIZE - 1)) == 0, "BUFSIZE must be a power of 2");
static constexpr float kBufF = static_cast<float>(BUFSIZE);
public:
float __force_inline process(float input, float delaySamples, float g)
{
const float dt = fminf(delaySamples, kBufF - 1.f);
float readPos = static_cast<float>(buf_.getWriteIndex()) - dt;
if (readPos < 0.f) readPos += kBufF;
const float delayed = buf_.readAbsolute(readPos);
const float v = input + g * delayed;
buf_.write(v);
return -g * v + delayed;
}
private:
DynamicDelayI16<BUFSIZE> buf_;
};

View file

@ -0,0 +1,47 @@
#ifndef ONEPOLESMOOTHER_H
#define ONEPOLESMOOTHER_H
#include <cmath>
template<size_t n_channels>
class OnePoleSmoother {
public:
OnePoleSmoother() : sample_rate_(48000), y_{0} {}
OnePoleSmoother(float time_ms, float sample_rate) :
sample_rate_(sample_rate),
y_ { 0 } {
SetTimeMs(time_ms);
}
void Setup(float time_ms, float sample_rate) {
sample_rate_ = sample_rate;
SetTimeMs(time_ms);
}
void SetTimeMs(float time_ms) {
//b1_ = std::exp(
// std::log(0.01) /
// time_ms * sample_rate_ * 0.001
//);
b1_ = powf(0.1f, 1.f/ (time_ms * 0.001f * sample_rate_));
}
inline __attribute__((always_inline)) void Process(const float * x_ptr, float *y_ptr) {
float *y2_ptr = y_;
for (unsigned int c = 0; c < n_channels; c++) {
const float x = *x_ptr;
*y2_ptr = *y_ptr = x + b1_ * (*y2_ptr - x);
++x_ptr;
++y_ptr;
++y2_ptr;
}
}
protected:
float sample_rate_;
float b1_;
float y_[n_channels];
};
#endif

View file

@ -0,0 +1,124 @@
#ifndef __PLAYLOOP_HPP__
#define __PLAYLOOP_HPP__
#include "stdint.h"
#include "../PicoDefs.hpp"
// Flash memory address where audio data is loaded
#define AUDIO_FLASH_ADDRESS 0x10200000U
#define AUDIO_MAGIC 0x4F434950U // 'PICO'
#define AUDIO_VERSION 1U
class PlayLoop {
public:
PlayLoop(const char *filename) :
sample_info_{0},
phase_(0.f)
{
if (!get_sample_info(filename, &sample_info_)) {
DEBUG_PRINTLN("Error: Sample not found in audio data.");
}else{
DEBUG_PRINTLN("Sample found: " + String(sample_info_.name) + ", count: " + String(sample_info_.sample_count) + ", duration: " + String(sample_info_.duration));
}
}
float __force_inline Process() {
if (!sample_info_.found || !sample_info_.samples || sample_info_.sample_count == 0) {
return 0.f; // Return silence if sample not found
}
// Read sample at current phase
float y = sample_info_.samples[phase_];
// Update phase
phase_++;
if (phase_ >= sample_info_.sample_count) {
phase_ = 0;
}
return y;
}
protected:
// Sample information structure
typedef struct {
const char* name; // File name
const float* samples; // Pointer to audio samples
uint32_t sample_count; // Number of samples
float duration; // Duration in seconds
bool found; // Whether the file was found
} sample_info_t;
// Binary format structures
typedef struct {
uint32_t magic; // 'PICO' magic number
uint32_t version; // Format version
uint32_t file_count; // Number of audio files
uint32_t sample_rate; // Sample rate in Hz
} audio_header_t;
typedef struct {
char name[16]; // Null-terminated filename
uint32_t offset; // Offset to audio data
uint32_t sample_count; // Number of samples
float duration; // Duration in seconds
uint32_t reserved; // Reserved for future use
} audio_file_entry_t;
sample_info_t sample_info_;
unsigned int phase_;
/**
* Get sample information by filename (without .wav extension)
*
* @param filename Name of the file without .wav extension (e.g., "intro", "beep")
* @param info Pointer to sample_info_t structure to fill
* @return true if file found, false otherwise
*/
static bool get_sample_info(const char* filename, sample_info_t* info) {
DEBUG_PRINTLN("get_sample_info called with filename: " + String(filename));
if (!filename || !info) {
return false;
}
// Initialize info structure
memset(info, 0, sizeof(sample_info_t));
// Read pointers from memory based on flash address
const uint8_t* binary_data = (const uint8_t*)AUDIO_FLASH_ADDRESS;
const audio_header_t* header = (const audio_header_t*)binary_data;
const audio_file_entry_t* file_table = (const audio_file_entry_t*)(binary_data + 16);
// Verify binary is valid
if (header->magic != AUDIO_MAGIC) {
return false;
}
// Search for the file by name
for (uint32_t i = 0; i < header->file_count; i++) {
if (strcmp(file_table[i].name, filename) == 0) {
// Found the file!
info->name = file_table[i].name;
info->samples = (const float*)(binary_data + file_table[i].offset);
info->sample_count = file_table[i].sample_count;
info->duration = file_table[i].duration;
info->found = true;
return true;
}
}
// File not found
info->found = false;
return false;
}
};
#endif // __PLAYLOOP_HPP__

View file

@ -0,0 +1,324 @@
#pragma once
#include "maximilian.h"
#include <utility>
#include <cmath>
// Freeverb-style reverb using int16 delay lines.
// 4 parallel LP-filtered feedback combs → 2 serial Schroeder allpasses → stereo out.
// COMB_SIZE must be a power of 2 (DynamicDelayI16 bitmask requirement).
// At default COMB_SIZE=4096: ~40 KB RAM, ~75 ops/sample ≈ 1.5% CPU at 200MHz/48kHz.
template<size_t COMB_SIZE = 4096>
class ReverbI16 {
static_assert((COMB_SIZE & (COMB_SIZE - 1)) == 0, "COMB_SIZE must be a power of 2");
static constexpr size_t AP_SIZE = COMB_SIZE / 4; // 1024
static constexpr size_t PRE_SIZE = COMB_SIZE / 2; // 2048 (~42ms at 48kHz)
static constexpr size_t DECOR_SIZE = 64;
// Comb bases as fraction of sample rate; scale range [0.5, 1.1] keeps max < COMB_SIZE.
// (Read only at control rate in setSize, so flash residence is fine here.)
static constexpr float kCombBases[4] = {0.0500f, 0.0561f, 0.0625f, 0.0688f};
// Allpass fixed times live as SRAM statics in processCore() (hot path) — see note there.
DynamicDelayI16<COMB_SIZE> combs_[4];
DynamicDelayI16<AP_SIZE> aps_[2];
DynamicDelayI16<PRE_SIZE> preDelay_;
DynamicDelayI16<DECOR_SIZE> decorR_;
float dampState_[4] = {};
float hpfLpState_ = 0.f;
float lfo1_ = 0.f;
float lfo2_ = 0.5f;
// Written by ProcessParams (Core 1 bg), read by process() ISR (Core 1 hi).
// Same-core aligned float reads/writes are safe on Cortex-M33.
float combTimes_[4] = {};
float feedbackGain_ = 0.60f;
float dampCoeff_ = 0.50f;
float apGain_ = 0.50f;
float modDepth_ = 0.f;
float modInc_ = 0.f;
float preDelaySamples_ = 0.f;
float hpfCoeff_ = 0.f;
float width_ = 0.7f;
float satDrive_ = 0.f;
float sampleRate_ = 48000.f;
// Cheap cubic soft-clip (no divide): ~unity for |x|<1, smoothly reaches ±1 at ±1.5,
// hard-limits beyond. Applied to the recirculating writes so overload saturates gently
// instead of the int16 delay line's harsh ±1 hard-clamp.
static float __force_inline softLimit(float x) {
// Constants in SRAM (non-const static) to avoid flash literal-pool reads in the hot path.
static float kLim = 1.5f;
static float kCub = 0.148148f; // 4/27
if (x > kLim) return 1.f;
if (x < -kLim) return -1.f;
return x - x * x * x * kCub;
}
public:
void setup(float sr) {
sampleRate_ = sr;
// Fast smoothing on combs so LFO modulation tracks correctly
for (auto& c : combs_) c.setSmoothCoeff(0.99f);
setSize(0.5f);
}
// 0-1 → scale 0.5-1.1; combTimes_ derived from base × scale × sr
void setSize(float v) {
const float scale = 0.5f + v * 0.6f;
const float maxT = static_cast<float>(COMB_SIZE - 2);
for (int i = 0; i < 4; ++i)
combTimes_[i] = fminf(kCombBases[i] * sampleRate_ * scale, maxT);
}
void setDecay(float v) { feedbackGain_ = 0.30f + v * 0.67f; }
void setDamping(float v) { dampCoeff_ = v * 0.92f; }
void setDiffusion(float v) { apGain_ = 0.30f + v * 0.45f; }
void setModDepth(float v) { modDepth_ = v * 20.f; }
void setModRate(float v) { modInc_ = (0.1f + v * 4.9f) / sampleRate_; }
void setPreDelay(float v) { preDelaySamples_ = v * static_cast<float>(PRE_SIZE - 1); }
void setLowCut(float v) { hpfCoeff_ = v * 0.05f; }
void setStereoWidth(float v) { width_ = v; }
void setSaturation(float v) { satDrive_ = v * 4.f; }
// Shared mono reverb core: HPF → predelay → 4 LP-combs → saturation → 2 allpasses.
// Returns the wet mono signal; process()/processMono() build their output from it.
float __force_inline processCore(float in) {
// Hot-path constants kept in SRAM (non-const static) so the per-sample loop reads them
// from RAM rather than the flash literal pool (XIP reads are far slower). NOT const.
static float kAPTimes[2] = {605.f, 480.f}; // Schroeder allpass delay times
static float kLfo2Rate = 1.3f;
static float kTriA = 4.f;
static float kTriB = 3.f;
static float kCombScale = 0.25f; // average of 4 combs
static float kSatA = 27.f;
static float kSatB = 9.f;
// Input HPF (1-pole: y_lp += coeff*(x - y_lp); y_hp = x - y_lp).
// coeff IS the cutoff: 0 => y_lp frozen => y_hp = x (no cut, full signal).
hpfLpState_ += hpfCoeff_ * (in - hpfLpState_);
float sig = in - hpfLpState_;
// Pre-delay
if (preDelaySamples_ >= 1.f) {
const float pd = preDelay_.read(preDelaySamples_);
preDelay_.write(softLimit(sig));
sig = pd;
}
// Dual triangle LFOs (antiphase: lfo2 offset by 0.5 period, runs at 1.3× rate)
lfo1_ += modInc_;
if (lfo1_ >= 1.f) lfo1_ -= 1.f;
lfo2_ += modInc_ * kLfo2Rate;
if (lfo2_ >= 1.f) lfo2_ -= 1.f;
const float tri1 = lfo1_ < 0.5f ? kTriA * lfo1_ - 1.f : kTriB - kTriA * lfo1_;
const float tri2 = lfo2_ < 0.5f ? kTriA * lfo2_ - 1.f : kTriB - kTriA * lfo2_;
// 4 parallel LP-comb filters
float combSum = 0.f;
for (int i = 0; i < 4; ++i) {
const float lfo = (i < 2) ? tri1 : tri2;
const float y = combs_[i].read(combTimes_[i] + lfo * modDepth_);
dampState_[i] = dampState_[i] * dampCoeff_ + y * (1.f - dampCoeff_);
combs_[i].write(softLimit(sig + dampState_[i] * feedbackGain_));
combSum += dampState_[i];
}
combSum *= kCombScale;
// Optional tail saturation (normalised fasttanh)
if (satDrive_ > 0.f) {
const float xd = combSum * satDrive_;
const float x2 = xd * xd;
combSum = (xd * (kSatA + x2) / (kSatA + kSatB * x2)) / satDrive_;
}
// 2 serial Schroeder allpasses
for (int i = 0; i < 2; ++i) {
const float delayed = aps_[i].read(kAPTimes[i]);
const float v = combSum - delayed * apGain_;
aps_[i].write(softLimit(v));
combSum = v * apGain_ + delayed;
}
return combSum;
}
// Wet-only MONO output — cheapest path (skips the stereo decorrelation tail). Use when
// the caller only needs a mono reverb (copies to L/R itself).
float __force_inline processMono(float in) { return processCore(in); }
// Wet-only stereo pair; caller applies wet/dry crossfade.
std::pair<float, float> __force_inline process(float in) {
const float combSum = processCore(in);
// Stereo decorrelation on R (fixed 23-sample delay) + width (mid-side)
const float L = combSum;
const float R = decorR_.read(23.f);
decorR_.write(combSum);
const float mid = (L + R) * 0.5f;
const float side = (L - R) * 0.5f;
return { mid + side * width_, mid - side * width_ };
}
};
// Larger, denser, true-stereo variant. Same public API as ReverbI16 (drop-in), but:
// input diffusion (2 allpasses) → 8 LP-combs split into L/R banks → per-channel
// allpass chains → width. ~2x the RAM/CPU of ReverbI16 (~78 KB, ~3% CPU at COMB_SIZE
// 4096). Pick per mode by choosing the class: DJFX uses this; SaxFX uses ReverbI16.
template<size_t COMB_SIZE = 4096>
class ReverbI16Large {
static_assert((COMB_SIZE & (COMB_SIZE - 1)) == 0, "COMB_SIZE must be a power of 2");
// AP/PRE/DIFF are fixed (independent of COMB_SIZE) so the allpass/predelay/diffuser
// times always fit even when COMB_SIZE is small (e.g. 2048). Only the combs — which
// set the room size and dominate RAM — scale with COMB_SIZE.
static constexpr size_t AP_SIZE = 1024; // holds <=556
static constexpr size_t PRE_SIZE = 2048; // ~42ms predelay
static constexpr size_t DIFF_SIZE = 512; // input diffusers (holds <=142)
// 8 comb bases (Freeverb tunings ×2 for a bigger room), as fraction of sample rate.
// Longest = 0.0733·48k·1.1 ≈ 3870 < COMB_SIZE, so they fit at max size.
static constexpr float kCombBases[8] = {
0.0506f, 0.0539f, 0.0579f, 0.0615f, 0.0645f, 0.0676f, 0.0706f, 0.0733f
};
// Per-sample-read allpass/diffuser times + gain live as SRAM statics in process()
// (avoid flash literal-pool reads in the hot loop). kCombBases stays — read only in setSize.
DynamicDelayI16<COMB_SIZE> combs_[8];
DynamicDelayI16<AP_SIZE> apsL_[2];
DynamicDelayI16<AP_SIZE> apsR_[2];
DynamicDelayI16<DIFF_SIZE> inDiff_[2];
DynamicDelayI16<PRE_SIZE> preDelay_;
float dampState_[8] = {};
float hpfLpState_ = 0.f;
float lfo1_ = 0.f, lfo2_ = 0.5f;
float combTimes_[8] = {};
float feedbackGain_ = 0.60f;
float dampCoeff_ = 0.50f;
float apGain_ = 0.50f;
float modDepth_ = 0.f;
float modInc_ = 0.f;
float preDelaySamples_ = 0.f;
float hpfCoeff_ = 0.f;
float width_ = 0.7f;
float satDrive_ = 0.f;
float sampleRate_ = 48000.f;
float __force_inline saturate(float x) const {
static float kA = 27.f, kB = 9.f; // SRAM, not flash literals
const float xd = x * satDrive_;
const float x2 = xd * xd;
return (xd * (kA + x2) / (kA + kB * x2)) / satDrive_;
}
// Cheap cubic soft-clip (no divide): ~unity for |x|<1, smoothly reaches ±1 at ±1.5,
// hard-limits beyond. Applied to the reverb's recirculating writes so overload becomes
// gentle saturation instead of the int16 delay line's harsh ±1 hard-clamp.
static float __force_inline softLimit(float x) {
// Constants in SRAM (non-const static) to avoid flash literal-pool reads in the hot path.
static float kLim = 1.5f;
static float kCub = 0.148148f; // 4/27
if (x > kLim) return 1.f;
if (x < -kLim) return -1.f;
return x - x * x * x * kCub;
}
public:
void setup(float sr) {
sampleRate_ = sr;
for (auto& c : combs_) c.setSmoothCoeff(0.99f);
setSize(0.5f);
}
void setSize(float v) {
// Comb base tunings are sized for COMB_SIZE=4096; scale them by COMB_SIZE/4096 so
// they fit (and stay mutually distinct) at any buffer size — a smaller COMB_SIZE
// simply gives a smaller room.
const float scale = (0.5f + v * 0.6f) * (static_cast<float>(COMB_SIZE) / 4096.f);
const float maxT = static_cast<float>(COMB_SIZE - 2);
for (int i = 0; i < 8; ++i)
combTimes_[i] = fminf(kCombBases[i] * sampleRate_ * scale, maxT);
}
void setDecay(float v) { feedbackGain_ = 0.30f + v * 0.685f; } // up to ~0.985
void setDamping(float v) { dampCoeff_ = v * 0.92f; }
void setDiffusion(float v) { apGain_ = 0.30f + v * 0.45f; }
void setModDepth(float v) { modDepth_ = v * 20.f; }
void setModRate(float v) { modInc_ = (0.1f + v * 4.9f) / sampleRate_; }
void setPreDelay(float v) { preDelaySamples_ = v * static_cast<float>(PRE_SIZE - 1); }
void setLowCut(float v) { hpfCoeff_ = v * 0.05f; }
void setStereoWidth(float v) { width_ = v; }
void setSaturation(float v) { satDrive_ = v * 4.f; }
std::pair<float, float> __force_inline process(float in) {
// Hot-path constants in SRAM (non-const static) — avoid flash literal-pool reads.
static float kInDiffTimes[2] = {142.f, 107.f};
static float kAPTimesL[2] = {556.f, 441.f};
static float kAPTimesR[2] = {341.f, 225.f};
static float kInDiffGain = 0.7f;
static float kLfo2Rate = 1.3f;
static float kTriA = 4.f;
static float kTriB = 3.f;
static float kCombScale = 0.25f; // 4 combs per channel
// Input HPF (see ReverbI16 for the 1-pole derivation)
hpfLpState_ += hpfCoeff_ * (in - hpfLpState_);
float sig = in - hpfLpState_;
// Pre-delay
if (preDelaySamples_ >= 1.f) {
const float pd = preDelay_.read(preDelaySamples_);
preDelay_.write(softLimit(sig));
sig = pd;
}
// Input diffusion — 2 allpasses smear transients into a dense wash
for (int k = 0; k < 2; ++k) {
const float d = inDiff_[k].read(kInDiffTimes[k]);
const float v = sig - d * kInDiffGain;
inDiff_[k].write(softLimit(v));
sig = v * kInDiffGain + d;
}
// Dual triangle LFOs (antiphase, lfo2 at 1.3× rate)
lfo1_ += modInc_; if (lfo1_ >= 1.f) lfo1_ -= 1.f;
lfo2_ += modInc_ * kLfo2Rate; if (lfo2_ >= 1.f) lfo2_ -= 1.f;
const float tri1 = lfo1_ < 0.5f ? kTriA * lfo1_ - 1.f : kTriB - kTriA * lfo1_;
const float tri2 = lfo2_ < 0.5f ? kTriA * lfo2_ - 1.f : kTriB - kTriA * lfo2_;
// 8 LP-combs, split into L (even) / R (odd) banks for true stereo
float combL = 0.f, combR = 0.f;
for (int i = 0; i < 8; ++i) {
const float lfo = (i < 4) ? tri1 : tri2;
const float y = combs_[i].read(combTimes_[i] + lfo * modDepth_);
dampState_[i] = dampState_[i] * dampCoeff_ + y * (1.f - dampCoeff_);
combs_[i].write(softLimit(sig + dampState_[i] * feedbackGain_));
if (i & 1) combR += dampState_[i]; else combL += dampState_[i];
}
combL *= kCombScale;
combR *= kCombScale;
if (satDrive_ > 0.f) { combL = saturate(combL); combR = saturate(combR); }
// Per-channel allpass chains (stereo diffusion)
for (int k = 0; k < 2; ++k) {
const float d = apsL_[k].read(kAPTimesL[k]);
const float v = combL - d * apGain_;
apsL_[k].write(softLimit(v));
combL = v * apGain_ + d;
}
for (int k = 0; k < 2; ++k) {
const float d = apsR_[k].read(kAPTimesR[k]);
const float v = combR - d * apGain_;
apsR_[k].write(softLimit(v));
combR = v * apGain_ + d;
}
// Width (mid-side)
const float mid = (combL + combR) * 0.5f;
const float side = (combL - combR) * 0.5f;
return { mid + side * width_, mid - side * width_ };
}
};

View file

@ -0,0 +1,149 @@
#include "SaxAnalysis.hpp"
#include <cmath>
#include "../hardware/memlnaut/Pins.hpp"
#include "../utils/Maths.hpp"
#include "../PicoDefs.hpp"
SaxAnalysis::SaxAnalysis(const float sample_rate) :
sample_rate_(sample_rate),
one_over_sample_rate_(1.0f / sample_rate),
zc_median_filter_(kZC_MedianFilterSize) {
// Initialize filters and detectors
common_hpf_.set(maxiBiquad::filterTypes::HIGHPASS, 100.f, 0.707f, 0);
// Zero crossing
zc_lpf_.set(maxiBiquad::filterTypes::LOWPASS, 800.0f, 0.707f, 0);
elapsed_samples_ = 0;
// Envelope follower
ef_follower_.setAttack(10.0f);
ef_follower_.setRelease(100.0f);
ef_deriv_y_ = 0;
// Brightness
br_lpf1_.set(maxiBiquad::filterTypes::LOWPASS, 1000.0f, 0.707f, 0);
br_hpf2_.set(maxiBiquad::filterTypes::HIGHPASS, 1000.0f, 0.707f, 0);
br_lpf2_.set(maxiBiquad::filterTypes::LOWPASS, 4000.0f, 0.707f, 0);
for (size_t i = 0; i < kBR_NBands; ++i) {
br_follower_[i].setAttack(10.f);
br_follower_[i].setRelease(100.f);
}
}
inline float logEnvelopeFast(float linearEnv) {
// -60 dBFS corresponds to a linear amplitude ratio of 10^(-60/20) = 10^(-3) = 0.001
static constexpr float MIN_ENV = 1e-3f; // 10^(-60dB/20dB) = 0.001 linear
// Mathematical derivation:
// We want to map linear amplitude [0.001, 1.0] to normalized range [0, 1]
// where 0.001 corresponds to -60 dBFS and 1.0 corresponds to 0 dBFS
//
// Using logarithmic mapping: output = (log2(input) - log2(min)) / (log2(max) - log2(min))
// log2(0.001) = log2(10^-3) = -3 * log2(10) ≈ -9.966
// log2(1.0) = 0
// Range = 0 - (-9.966) = 9.966
static constexpr float LOG2_MIN_ENV = -3.0f * 3.321928095f; // -3 * log2(10) ≈ -9.966
static constexpr float LOG2_MAX_ENV = 0.0f; // log2(1.0) = 0
static constexpr float LOG_RANGE = LOG2_MAX_ENV - LOG2_MIN_ENV; // 9.966
static constexpr float INV_LOG_RANGE = 1.0f / LOG_RANGE; // 1 / 9.966 ≈ 0.1003
// Clamp input to minimum envelope value
linearEnv = (linearEnv > MIN_ENV) ? linearEnv : MIN_ENV;
// Precise logarithmic conversion
float log2_val = std::log2f(linearEnv);
// Map to [0,1]: (log2_val - log2_min) / (log2_max - log2_min)
float y = (log2_val - LOG2_MIN_ENV) * INV_LOG_RANGE;
// Clamp to [0,1] range (should be unnecessary given our math, but safety first)
y = (y > 1.0f) ? 1.0f : y;
y = (y < 0.0f) ? 0.0f : y;
return y;
}
SaxAnalysis::parameters_t AUDIO_FUNC(SaxAnalysis::Process)(const float x) {
parameters_t params = {};
// Pre-filter
float pre_filtered = common_hpf_.play(x);
// Zero crossing detection
float zc_y = zc_lpf_.play(pre_filtered);
bool positive_zero_crossing = zc_detector_.zx(zc_y);
if (positive_zero_crossing) {
size_t median_elapsed_samples = zc_median_filter_.process(elapsed_samples_);
zc_buffer_.push(median_elapsed_samples);
elapsed_samples_ = 0;
}
// Convert zero crossing value to pitch
size_t zc_value = zc_buffer_[zc_buffer_.size() - 1];
float pitch = 1.0f / (zc_value * one_over_sample_rate_);
// Map [100..800] hz to [0..1] range
float normalized_pitch;
if (pitch <= kPitchMin) {
normalized_pitch = 0.0f;
} else if (pitch >= kPitchMax) {
normalized_pitch = 1.0f;
} else {
normalized_pitch = (pitch - kPitchMin) * kPitchScale;
}
elapsed_samples_++;
// Aperiodicity calculation
float zc_copy[kZC_ZCBufferSize];
// Copy contents of circular buffer to float array
for (size_t i = 0; i < kZC_ZCBufferSize; ++i) {
zc_copy[i] = static_cast<float>(zc_buffer_[i]);
}
float mad = meanAbsoluteDeviation(zc_copy, kZC_ZCBufferSize);
// Scale MAD relative to median period
float medianPeriod = static_cast<float>(zc_value);
float relativeMad = mad / (medianPeriod + 1.0f); // +1 to avoid div/0
// Typical relative MAD ranges from 0 to 0.3 for musical sounds
static constexpr float ONE_OVER_RELATIVE_MAD_MAX = 1/0.3f;
float normalizedAperiodicity = std::min(1.0f, relativeMad * ONE_OVER_RELATIVE_MAD_MAX);
// Envelope follower
float ef_y = ef_follower_.play(pre_filtered);
// Convert to log
ef_y = logEnvelopeFast(ef_y);
float ef_d_dy = ef_y - ef_deriv_y_;
ef_deriv_y_ = ef_y;
// Half-wave rectify the derivative
if (ef_d_dy < 0) {
ef_d_dy = 0;
}
// Scale to [0..1] range
ef_d_dy = std::min(ef_d_dy * 10.0f, 1.0f);
// Brightness calculation
float br_low = br_lpf1_.play(pre_filtered);
float br_high = br_hpf2_.play(pre_filtered);
br_high = br_lpf2_.play(br_high);
br_low = br_follower_[0].play(br_low);
br_high = br_follower_[1].play(br_high);
// brightness = high_band_energy / (low_band_energy + high_band_energy)
float br_energy = br_low + br_high;
if (br_energy > 0.0f) {
br_high /= br_energy;
} else {
br_high = 0.0f; // Avoid division by zero
}
// Fill parameters
params.pitch = normalized_pitch;
params.aperiodicity = normalizedAperiodicity;
params.energy = ef_y;
params.attack = ef_d_dy;
params.brightness = br_high;
params.energy_crude = std::abs(x);
return params;
}

View file

@ -0,0 +1,59 @@
#ifndef __SAX_ANALYSIS_HPP__
#define __SAX_ANALYSIS_HPP__
#include "../audio/AudioDriver.hpp"
#include "../utils/MedianFilter.h"
#include "../utils/CircularBuffer.hpp"
#include "maximilian.h"
#include <cmath>
class SaxAnalysis {
public:
struct parameters_t {
float pitch;
float aperiodicity;
float energy;
float attack;
float brightness;
float energy_crude;
};
static constexpr size_t kN_Params = sizeof(parameters_t) / sizeof(float);
SaxAnalysis(const float sample_rate);
parameters_t Process(const float x);
protected:
const float sample_rate_;
const float one_over_sample_rate_;
// Pre-filter
maxiBiquad common_hpf_;
// Zero crossing
static constexpr size_t kZC_MedianFilterSize = 16;
static constexpr size_t kZC_ZCBufferSize = 32;
static constexpr float kPitchMin = 100.0f;
static constexpr float kPitchMax = 800.0f;
static constexpr float kPitchRange = kPitchMax - kPitchMin; // 700.0f
static constexpr float kPitchScale = 1.0f / kPitchRange; // 1/700
maxiBiquad zc_lpf_;
maxiZeroCrossingDetector zc_detector_;
size_t elapsed_samples_;
MedianFilter<size_t> zc_median_filter_;
CircularBuffer<size_t, kZC_ZCBufferSize> zc_buffer_;
// Envelope follower
maxiEnvelopeFollowerF ef_follower_;
float ef_deriv_y_;
// Brightness
static constexpr size_t kBR_NBands = 2;
maxiBiquad br_lpf1_;
maxiBiquad br_hpf2_;
maxiBiquad br_lpf2_;
maxiEnvelopeFollowerF br_follower_[kBR_NBands];
};
#endif // __SAX_ANALYSIS_HPP__

View file

@ -0,0 +1,309 @@
#ifndef MAXIPAF_HPP
#define MAXIPAF_HPP
#include "maximilian.h"
constexpr size_t LOGTABSIZE = 10;
constexpr size_t LOGTABSIZEINV = 32 - LOGTABSIZE;
constexpr size_t TABSIZE = (1 << LOGTABSIZE);
constexpr size_t TABRANGE = 3;
typedef struct _tabpoint
{
float p_y;
float p_diff;
} t_tabpoint;
typedef struct _linenv
{
double l_current;
double l_biginc;
float l_1overn;
float l_target;
float l_msectodsptick;
int l_ticks;
} t_linenv;
constexpr size_t UNITBIT32 = 1572864.f; /* 3*2^19 -- bit 32 has value 1 */
constexpr float TABFRACSHIFT = (UNITBIT32/TABSIZE);
//little endian
#define HIOFFSET 1
#define LOWOFFSET 0
#include <sys/types.h>
#define int32 u_int32_t
#define LINENV_RUN(linenv, current, incr) \
if (linenv.l_ticks > 0) \
{ \
current = linenv.l_current; \
incr = linenv.l_biginc * linenv.l_1overn; \
linenv.l_ticks--; \
linenv.l_current += linenv.l_biginc; \
} \
else \
{ \
linenv.l_current = current = linenv.l_target; \
incr = 0; \
}
constexpr float PAFA1 = 4 * (3.14159265/2);
constexpr float PAFA3 = ((64 * (2.5 - 3.14159265)));
constexpr float PAFA5 ((1024 * ((3.14159265/2) - 1.5)));
/* value of Cauchy distribution at TABRANGE */
constexpr float CAUCHYVAL = (1./ (1. + TABRANGE * TABRANGE));
/* first derivative of Cauchy distribution at TABRANGE */
constexpr float CAUCHYSLOPE = ((-2. * TABRANGE) * CAUCHYVAL * CAUCHYVAL);
constexpr float ADDSQ = (- CAUCHYSLOPE / (2 * TABRANGE));
constexpr float HALFSINELIM = (0.997 * TABRANGE);
constexpr float TABRANGERCPR = 1.f/TABRANGE;
static t_tabpoint __not_in_flash("paf") paf_gauss[TABSIZE];
static t_tabpoint __not_in_flash("paf") paf_cauchy[TABSIZE];
static bool tabsGenerated = false;
class maxiPAFOperator {
public:
void linenv_init(t_linenv &l)
{
l.l_current = l.l_biginc = 0;
l.l_1overn = l.l_target = l.l_msectodsptick = 0;
l.l_ticks = 0;
}
void init()
{
int i;
if (!tabsGenerated) {
const float CAUCHYFAKEAT3 =
(CAUCHYVAL + ADDSQ * TABRANGE * TABRANGE);
const float CAUCHYRESIZE = (1./ (1. - CAUCHYFAKEAT3));
for (i = 0; i <= TABSIZE; i++)
{
float f = i * ((float)TABRANGE/(float)TABSIZE);
float gauss = expf(-f * f);
float cauchygenuine = 1.f / (1.f + f * f);
float cauchyfake = cauchygenuine + ADDSQ * f * f;
float cauchyrenorm = (cauchyfake - 1.) * CAUCHYRESIZE + 1.;
if (i != TABSIZE)
{
paf_gauss[i].p_y = gauss;
paf_cauchy[i].p_y = cauchyrenorm;
/* post("%f", cauchyrenorm); */
}
if (i != 0)
{
paf_gauss[i-1].p_diff = gauss - paf_gauss[i-1].p_y;
paf_cauchy[i-1].p_diff = cauchyrenorm - paf_cauchy[i-1].p_y;
}
}
}
// linenv_init(x_freqenv);
// linenv_init(x_cfenv);
// linenv_init(x_bwenv);
// linenv_init(x_ampenv);
// linenv_init(x_vibenv);
// linenv_init(x_vfrenv);
// linenv_init(x_shiftenv);
// x_freqenv.l_target = x_freqenv.l_current = 1.0;
//TODO: use correct sample rate
x_isr = maxiSettings::one_over_sampleRate; //1.f/44100.f;
x_held_freq = 1.f;
x_held_intcar = 0.f;
x_held_fraccar = 0.f;
x_held_bwquotient = 0.f;
x_phase = 0.;
x_shiftphase = 0.;
x_vibphase = 0.;
x_triggerme = 0;
}
void linenv_setsr(t_linenv &l, const float sr, const int vecsize)
{
l.l_msectodsptick = sr / (1000.f * ((float)vecsize));
l.l_1overn = 1.f/(float)vecsize;
}
void setsr(const float sr, const int vecsize)
{
x_isr = 1.f/sr;
// linenv_setsr(x_freqenv, sr, vecsize);
// linenv_setsr(x_cfenv, sr, vecsize);
// linenv_setsr(x_bwenv, sr, vecsize);
// linenv_setsr(x_ampenv, sr, vecsize);
// linenv_setsr(x_vibenv, sr, vecsize);
// linenv_setsr(x_vfrenv, sr, vecsize);
// linenv_setsr(x_shiftenv, sr, vecsize);
}
inline void linenv_set(t_linenv &l, const float target, const long timdel)
{
if (timdel > 0)
{
l.l_ticks = ((float)timdel) * l.l_msectodsptick;
if (!l.l_ticks) l.l_ticks = 1;
l.l_target = target;
l.l_biginc = (l.l_target - l.l_current)/l.l_ticks;
}
else
{
l.l_ticks = 0;
l.l_current = l.l_target = target;
l.l_biginc = 0;
}
}
inline void phase(const float mainphase, const float shiftphase,
const float vibphase)
{
x_phase = mainphase;
x_shiftphase = shiftphase;
x_vibphase = vibphase;
x_triggerme = 1;
}
void __force_inline play(float *out1, int n, float freqval, const float cfval, const float bwval,
const float vibval,
const float vfrval,
float shiftval,
const bool x_cauchy=false)
{
float bwquotient, bwqincr;
// float held_freq = x_held_freq;
float held_intcar = x_held_intcar;
float held_fraccar = x_held_fraccar;
float held_bwquotient = x_held_bwquotient;
float sinvib, vibphase;
t_tabpoint *paf_table = (x_cauchy ? paf_cauchy : paf_gauss);
x_shiftphase -= floorf(x_shiftphase);
/* fake line envelope for quotient of bw and frequency */
bwquotient = bwval/freqval;
float future_vib_phase = x_vibphase + n * x_isr * vfrval;
future_vib_phase = future_vib_phase - floorf(future_vib_phase);
x_vibphase = vibphase = future_vib_phase;
if (vibphase > 0.5f)
sinvib = 1.0f - 16.0f * (0.75f-vibphase) * (0.75f - vibphase);
else sinvib = -1.0f + 16.0f * (0.25f-vibphase) * (0.25f - vibphase);
freqval = freqval * (1.0f + vibval * sinvib);
const float inv_freqval = 1.0f / freqval;
shiftval *= x_isr;
if (x_phase ==0.f || x_triggerme)
{
float cf_over_freq = cfval * inv_freqval;
x_held_freq = freqval * x_isr;
held_intcar = (float)((int)cf_over_freq);
held_fraccar = cf_over_freq - held_intcar;
held_bwquotient = bwquotient;
x_triggerme = 0;
}
while (n--)
{
float g,halfsine;
float new_x_phase = x_phase + x_held_freq;
new_x_phase = new_x_phase - floorf(new_x_phase);
float fracnewphase = new_x_phase;
const float fphase = 2.0f * ((fracnewphase)) - 1.0f;
if (new_x_phase < x_phase) [[unlikely]]
{
float cf_over_freq = cfval * inv_freqval;
x_held_freq = freqval * x_isr;
held_intcar = floorf(cf_over_freq);
held_fraccar = cf_over_freq - held_intcar;
held_bwquotient = bwquotient;
}
x_phase = new_x_phase;
float fcarphase1 = fracnewphase * held_intcar + x_shiftphase;
fcarphase1 -= floorf(fcarphase1);
float fcarphase2 = fcarphase1 + fracnewphase;
fcarphase2 -= floorf(fcarphase2);
x_shiftphase += shiftval;
g = (fcarphase1 > 0.5f) ? (fcarphase1 - 0.75f) : (0.25f - fcarphase1);
const float g2a = g * g;
const float g3a = g * g2a;
const float cosine1 = g * PAFA1 + g3a * PAFA3 + g2a * g3a * PAFA5;
g = (fcarphase2 > 0.5f) ? (fcarphase2 - 0.75f) : (0.25f - fcarphase2);
const float g2b = g * g;
const float g3b = g * g2b;
const float cosine2 = g * PAFA1 + g3b * PAFA3 + g2b * g3b * PAFA5;
const float carrier = cosine1 + held_fraccar * (cosine2-cosine1);
halfsine = held_bwquotient * (1.0f - fphase * fphase);
halfsine = fminf(halfsine, HALFSINELIM);
constexpr float TABSCALE = (TABSIZE * TABRANGERCPR);
float halfsineScaled = halfsine * TABSCALE;
// Extract integer and fractional parts
int table_index = static_cast<int>(halfsineScaled);
const float tabfrac = (halfsineScaled) - table_index;
// Bounds check
table_index = min(TABSIZE-2, table_index);
table_index = max(0, table_index);
// Linear interpolation
const t_tabpoint *p = paf_table + table_index;
const float mod = carrier * (p->p_y + tabfrac * p->p_diff);
*out1++ = mod;
}
// x_held_freq = held_freq;
x_held_intcar = held_intcar;
x_held_fraccar = held_fraccar;
x_held_bwquotient = held_bwquotient;
}
private:
float x_isr;
float x_held_freq;
float x_held_intcar;
float x_held_fraccar;
float x_held_bwquotient;
float x_phase;
float x_shiftphase;
float x_vibphase;
int x_triggerme;
};
#endif // MAXIPAF_HPP

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more