AudioMixer
July 29, 2026 · View on GitHub
Firmware for the physical knob controller that drives the Dialed desktop app. The board reads pots/encoders and reports them to the PC over USB serial; the PC sets per-app Windows volumes and (on display-capable boards) echoes back state to render on screen.
There are three firmware tiers so the same project works on different hardware. Pick the
folder that matches your board, open its .ino in the Arduino IDE (or build with arduino-cli),
and flash it.
| Folder | Board | Display | Features |
|---|---|---|---|
mixer/ | ESP32 (e.g. WROOM) | GC9A01 240×240 round TFT (TFT_eSPI) | Full per-app icons, accent color, animated arc + idle |
mixer_nano/ | Arduino Nano / Uno / Pro Mini (ATmega328P) | 1.3" 128×64 I2C OLED (SH1106, U8g2) | App name + % + bar gauge (monochrome). No icons. |
arduino/ | Any (ESP32/AVR) | none | Knobs only — minimal sketch, no screen |
display_test/ | ESP32 | GC9A01 | Display bring-up test only |
The tiers exist because of RAM: the ESP32 build stores 64×64 RGB565 icons (32 KB) and a frame sprite (~16 KB), which cannot fit an ATmega328P's 2 KB SRAM. The Nano build is a slimmed copy of the same module layout that drops icons/sprites and discards the icon data the PC sends.
Serial protocol (shared by all tiers)
115200 baud, newline-terminated, CultureInfo.InvariantCulture floats.
Board → PC
knob1:0.42— potentiometer absolute level, 0.00–1.00knob1:up/knob1:down— encoder detentknob1:press— encoder push switch
PC → Board (display tiers; knobs-only/Nano ignore what they can't use)
vol:knob1:0.42— authoritative volume echo; drives the on-device gaugeassign:knob1:RRGGBB:AppName— label + accent color for a knobicon:knob1:<base64>— 64×64 RGB565 icon (~11 KB). ESP32 only; the Nano build discards these lines without buffering them.
Knob ids are 1-based (knob1…knobN); MAX_KNOBS in firmware must cover the highest index used.
mixer_nano/ — Arduino Nano (ATmega328P)
Libraries: U8g2 (install via Library Manager).
Wiring (defaults in the sketch):
| Function | Pin(s) | Notes |
|---|---|---|
| OLED SDA / SCL | A4 / A5 | Hardware I2C (fixed on the 328P). OLED VDD→5V, GND→GND. |
| Pots | A0, A1 (…A3) | Add more in pots[]; A6/A7 are also ADC-capable. |
| Encoder CLK / DT | D2 / D3 | The only interrupt pins on the 328P. (when USE_ENCODER 1) |
| Encoder switch | D4 | Any spare digital pin. |
Choose pots vs encoders with #define USE_ENCODER at the top of knobs.cpp (0 = pots, 1 =
encoder). Edit pots[] / encoders[] to match your build.
Display note: 1.3" panels are usually SH1106. If text is shifted ~2px or garbled on the right
edge, your panel is an SSD1306 — change the constructor in display.cpp to
U8G2_SSD1306_128X64_NONAME_F_HW_I2C.
Build:
arduino-cli compile --fqbn arduino:avr:nano:cpu=atmega328 Arduino/mixer_nano
Use cpu=atmega328old if upload fails on an older bootloader. Watch the SRAM report — the U8g2
full buffer is ~1 KB; if you run low, switch display.cpp to a page-buffer constructor
(..._2_HW_I2C) and a firstPage()/nextPage() render loop.
mixer/ — ESP32
Libraries: TFT_eSPI configured for GC9A01. Copy mixer/User_Setup.h into your TFT_eSPI
library folder (it overrides the library's default; see the header comment for pin mapping).
Wiring (defaults in the sketch, WROOM DevKit — 4 encoders + output toggle):
| Function | GPIO | Notes |
|---|---|---|
| Display SCLK / MOSI | 18 / 23 | SPI to the GC9A01. Set in User_Setup.h. |
| Display CS / DC / RST | 14 / 27 / 4 | VCC→3.3V, GND→GND, BL→3.3V if the screen stays dark. |
| Encoder 1 CLK / DT / SW | 17 / 16 / 5 | SW uses INPUT_PULLUP (button→GND). GPIO5 is a strapping pin. |
| Encoder 2 CLK / DT / SW | 19 / 13 / 21 | Edit encoders[] in knobs.cpp to match your build. |
| Encoder 3 CLK / DT / SW | 22 / 25 / 26 | |
| Encoder 4 CLK / DT / SW | 32 / 33 / 15 | GPIO15 is a strapping pin — don't hold this button while booting. |
| Output toggle (SPDT) COM | 34 | Center lug→GPIO34, the two throws→3.3V and GND. Input-only pin: no pull-up needed since the toggle drives the line both ways. Emits switch:0/switch:1. |
All encoder/display + lines share the 3.3V rail and all GND lines share the ground rail. Avoid
GPIO 6–11 (flash), 1/3 (USB serial), and 12 (strapping, must be LOW at boot). Choose pots vs
encoders with #define USE_ENCODER at the top of knobs.cpp (0 = pots, 1 = encoder).
Build:
arduino-cli compile --fqbn esp32:esp32:esp32 Arduino/mixer
Firmware versioning & bundled image
The version lives in Arduino/mixer/version.h (FW_BOARD/FW_VERSION), reported over
serial as fw:<board>:<version>. It is the single source of truth for the firmware
version. Bump it (SemVer) in the same commit as any change under mixer/.
It is deliberately independent of the app's v* release tag — tagging v1.4.0
may legitimately ship esp32-mixer-1.0.0.bin if the firmware did not change.
Nothing enforces the bump: if you change the sketch without bumping, two
different binaries ship under one version string and the app's
fw:<board>:<version> check will not notice a connected device is stale.
On every tagged v* release, .github/workflows/release.yml regenerates the bundled
image by running Arduino/tools/build-firmware.ps1 (using the pinned arduino-cli and
the pinned tools/esptool/esptool.exe — see tools/esptool/README.md), producing
firmware/esp32/<board>-<version>.bin and firmware/esp32/manifest.json, then attaches
both to the Release. Dialed.csproj bundles them next to the exe when present; the app
degrades gracefully if they're absent. You can run the same script locally to produce
the same outputs for local testing, but they are not committed to the repo.