Bidirectional, config-driven CircuitPython firmware for Paint Audio MIDI Captain foot controllers.
Includes a GUI Config Editor!
This firmware transforms your MIDI Captain into a bidirectional MIDI controller where your host software (DAW, plugin host) can control the device's LEDs and display, not just receive button presses.
Button modes: Toggle, momentary, select (radio-button groups), and tap tempo visualization. Long-press detection for secondary actions.
Multi-command actions: Each button event (press, release, long-press, long-release) can send multiple MIDI commands in sequence — change amp channel and delay preset with one footswitch!
See here for all open features and issues.
- 🔄 Bidirectional MIDI — Host can update LEDs/display state with value-based scene matching
- � Banks/Pages System — Up to 8 banks per device with button/CC/PC switching for complex live setups
- �📺 Center display — Shows button names and MIDI info with smart timeout
- ⚡ Multi-command actions — Send multiple MIDI messages per button press/release, each with independent channel control- 🧠 Conditional Actions — If/Then/Else logic: buttons adapt based on state (other buttons, received MIDI, encoder, expression pedals)- 💾 Export/Import Configs — Save and share button configurations as JSON files with ⌘E/⌘I shortcuts
- 🎯 Device Profiles — Built-in MIDI mappings for popular devices (Quad Cortex, Helix, Kemper, Ableton, MainStage)
- ⚙️ Config-driven — Visual GUI Config Editor for all settings
- 🎨 Visual feedback — LEDs and LCD reflect actual host state
- 🔘 Flexible modes — Toggle, momentary, select groups, tap tempo with accurate LED visualization
- ⏱️ Long-press support — Secondary actions on hold
- 🎛️ Full input support — Footswitches, rotary encoder, expression pedals
- 🔁 Keytimes — Multi-press cycling through states (like OEM SuperMode)
- 🎸 Stage-ready — No unexpected resets, no crashes, no surprises
| Device | Status |
|---|---|
| MIDI Captain STD10 (10-switch) | ✅ Fully working |
| MIDI Captain Mini6 (6-switch) | ✅ Fully working |
| MIDI Captain Nano4 (4-switch) | ✅ Config validation working |
| MIDI Captain Duo2 (2-switch) | ✅ Config validation working |
| MIDI Captain One1 (1-switch) | ✅ Config validation working |
- Download the latest firmware.zip and appropriate GUI Config Editor
- Connect your MIDI Captain via USB (hold Button 1 while powering on)
- Copy all files and folders from the zip to the device drive (CIRCUITPY or MIDICAPTAIN)
- On mini6, rename
config-mini6.jsontoconfig.json, overwriting the existing one. - Power off/on or unplug and replug USB to restart
If you have multiple MIDI Captain devices, you can give each one a unique name! Edit the usb_drive_name field in config.json:
{
"device": "std10",
"usb_drive_name": "MYCAPTAIN",
...
}Requirements:
- Maximum 11 characters
- Letters, numbers, and underscores only
- Will be automatically converted to uppercase
The name persists across power cycles and USB disconnects. Change it anytime by editing config.json and restarting the device.
Customize your device with a boot splash image that displays during startup!
- Create a 240×240 pixel BMP image (your logo, band name, etc.)
- Name it
splash.bmp - Copy it to the root of the device drive
- Power cycle to see it on boot
Optional config (add to config.json):
{
"splash_screen": {
"enabled": true,
"duration_ms": 1500,
"idle_timeout_seconds": 60
}
}The splash can also act as a screensaver — set idle_timeout_seconds to show it after inactivity (0 = disabled).
See firmware/circuitpython/SPLASH_README.md for detailed instructions and design tips. Use the included tools/generate_splash.py script to create a simple text-based splash:
python3 tools/generate_splash.py "MY BAND" "Live Setup"The MIDI Captain MAX Config Editor is a desktop app that makes configuration easy!
Download for your platform:
- macOS:
MIDI-Captain-MAX-Config-Editor-[version].dmg - Windows:
MIDI-Captain-MAX-Config-Editor-[version].msiorMIDI-Captain-MAX-Config-Editor-[version]-setup.exe
Get the latest release from Releases
MacOS:
- Open the DMG and drag the app to your Applications folder
Windows:
- Run the MSI installer or setup.exe
- At this time, Windows builds are unsigned. Users will see a Windows SmartScreen warning.
- To continue installation, click "More Info" --> "Run Anyway".
- Signing certificates will be obtained in the near future.
- Launch the app and connect your MIDI Captain
- Edit button labels, CC numbers, and colors using the visual editor
- Configure Device Profiles for quick MIDI setup
- Save directly to the device — option to safely eject when done
- Power cycle the device to load the new settings.
- 🖱️ Visual editing — No JSON syntax to learn
- ✅ Real-time validation — Catch errors before saving
- 🎨 Color picker — Visual color selection
- 🔍 Device detection — Automatically detects connected MIDI Captain
- 🎯 Device Profiles — Quick setup with built-in MIDI mappings
- 📊 MIDI Monitor — Real-time message debugging with filtering, export, and professional monitoring tools
⏏️ Safe eject — Cleanly ejects device after saving (macOS/Linux)
The config editor includes built-in profiles for popular music production devices, making setup faster and eliminating MIDI reference lookups.
- Neural DSP Quad Cortex — Scene select, stomp/preset modes, tuner
- Line 6 Helix — Snapshots, stomps, tap tempo, tuner
- Line 6 HX Stomp — Snapshots, stomps (compact 6-switch layout)
- Kemper Profiler — Rig select, stomp modes, tap tempo, tuner
- Ableton Live — Track control, clip launch, scene select, transport
- Apple MainStage — Patch select, bypass, tap tempo
- Open a button in the editor
- Enable "Use Device Profile"
- Select your device from the dropdown
- Choose an action (e.g., "Scene A", "Snapshot 1")
- Assign to Press/Release/Long Press event
- Optional: Override the MIDI channel
The editor shows a live preview of the MIDI commands that will be sent. You can mix profile actions with custom MIDI commands on the same button!
You can also edit config.json directly on the device. The firmware uses an event-based format where each button can define multiple commands per action:
{
"device": "std10",
"global_channel": 0,
"usb_drive_name": "MYCAPTAIN",
"dev_mode": false,
"buttons": [
{
"label": "DELAY",
"color": "blue",
"mode": "toggle",
"off_mode": "dim",
"channel": 0,
"press": [
{"type": "cc", "cc": 20, "value": 127}
],
"release": [
{"type": "cc", "cc": 20, "value": 0}
]
},
{
"label": "DRIVE",
"color": "orange",
"mode": "momentary",
"press": [
{"type": "cc", "cc": 23, "value": 127},
{"type": "pc", "program": 5}
],
"release": [
{"type"<
992E
/span>: "cc", "cc": 23, "value": 0}
],
"long_press": [
{"type": "cc", "cc": 40, "value": 127, "threshold_ms": 700}
],
"long_release": [
{"type": "cc", "cc": 40, "value": 0}
]
},
{
"label": "CLEAN",
"color": "green",
"mode": "select",
"select_group": "channel",
"default_selected": true,
"press": [
{"type": "pc", "program": 0}
]
},
{
"label": "CRCH",
"color": "red",
"mode": "select",
"select_group": "channel",
"press": [
{"type": "pc", "program": 1}
]
},
{
"label": "TAP",
"color": "cyan",
"mode": "momentary",
"press": [
{"type": "cc", "cc": 44, "value": 127, "channel": 0},
{"type": "cc", "cc": 1, "value": 127, "channel": 1}
]
},
{
"label": "VERB",
"color": "blue",
"mode": "toggle",
"keytimes": 3,
"press": [
{"type": "cc", "cc": 20, "value": 64}
],
"states": [
{"cc": 20, "value": 64, "color": "blue", "label": "50%"},
{"cc": 20, "value": 96, "color": "cyan", "label": "75%"},
{"cc": 20, "value": 127, "color": "white", "label": "100%"}
]
},
{
"label": "SMART",
"color": "purple",
"mode": "momentary",
"press": [
{
"type": "conditional",
"if": {"type": "button_state", "button": 0, "state": "on"},
"then": [
{"type": "cc", "cc": 30, "value": 127},
{"type": "pc", "program": 10}
],
"else": [
{"type": "cc", "cc": 30, "value": 64}
]
}
]
}
],
"encoder": {
"enabled": true,
"cc": 11,
"label": "MOD",
"min": 0,
"max": 127,
"initial": 64,
"channel": 0,
"push": {
"enabled": true,
"mode": "toggle",
"label": "PUSH",
"cc": 14,
"cc_on": 127,
"cc_off": 0,
"channel": 0
}
},
"expression": {
"exp1": {
"enabled": true,
"cc": 12,
"label": "EXP1",
"min": 0,
"max": 127,
"polarity": "normal",
"threshold": 2,
"channel": 0
}
},
"display": {
"button_text_size": "medium",
"status_text_size": "medium",
"expression_text_size": "medium"
}
}This example demonstrates:
- Toggle mode with press/release (Button 1: DELAY)
- Momentary mode with long-press/long-release (Button 2: DRIVE)
- Multi-command actions sending CC + PC simultaneously (Button 2)
- Select groups for radio-button behavior (Buttons 3-4: channel switching)
- Default selected button activated on boot (Button 3: CLEAN)
- Per-command channels controlling multiple devices (Button 5: TAP)
- Keytimes with per-state overrides (Button 6: VERB cycling 3 reverb levels)
- Conditional actions with if/then/else logic based on other button states (Button 7: SMART)
- Encoder configuration with push button
- Expression pedal setup
- Display text size settings
| Field | Description | Default |
|---|---|---|
label |
Text shown on LCD (max 6 chars) | Button number |
color |
Named color: red, green, blue, yellow, cyan, magenta, orange, purple, white |
white |
mode |
Button behavior: toggle, momentary, select, tap |
toggle |
off_mode |
LED when OFF: dim (30% brightness) or off (completely off) |
dim |
channel |
MIDI channel (0-15) | 0 |
select_group |
String ID for radio-button groups (only one ON at a time) | none |
keytimes |
Number of states to cycle through (1-99) | 1 |
states |
Array of per-state overrides (for keytimes > 1) | [] |
press |
Array of commands sent on press | [] |
release |
Array of commands sent on release | [] |
long_press |
Array of commands sent on long press (with threshold_ms) |
[] |
long_release |
Array of commands sent on release after long press | [] |
long_press_label |
Custom label shown during long press (max 6 chars) | none |
long_press_color |
LED color override during long press (any named color) | none |
| Field | Description | Types |
|---|---|---|
type |
Command type | cc, note, pc, pc_inc, pc_dec |
channel |
MIDI channel (0-15, optional - defaults to button or global channel) | All |
cc |
CC number (0-127) | cc |
value |
CC value (0-127) | cc |
note |
MIDI note (0-127) | note |
velocity |
Note velocity (0-127) | note |
program |
Program number (0-127) | pc |
pc_step |
Step value for increment/decrement | pc_inc, pc_dec |
threshold_ms |
Long-press threshold in milliseconds | long_press (first command only) |
Per-Command Channels:
Each command can specify its own channel (0-15). This enables one button to control multiple devices:
{
"label": "TAP",
"press": [
{"type": "cc", "cc": 44, "value": 127, "channel": 0}, // Tap to amp on ch1
{"type": "cc", "cc": 1, "value": 127, "channel": 1} // Tap to delay on ch2
]
}If channel is omitted, the command uses the button's channel field, or falls back to global_channel (default 0).
Mode behaviors:
toggle: Alternates ON/OFF, sendspresswhen ON,releasewhen OFFmomentary: ON while held, sendspresson press,releaseon releaseselect: Always turns ON (never toggles OFF), use withselect_grouptap: Visual tap tempo, blinks on each press
One-shot pattern (trigger only):
To send MIDI only on press without any release message, simply omit the release field:
{
"label": "KICK",
"color": "red",
"mode": "momentary",
"press": [
{"type": "note", "note": 36, "velocity": 127} // Kick drum
]
// No "release" field = nothing sent on release!
}Behavior:
- Press → LED on + send MIDI (once)
- Hold → LED stays on, no repeated messages
- Release → LED off, nothing sent
- Press again → LED on + send MIDI
Perfect for drum pads! Momentary mode sends MIDI once on press, not continuously while held. With no release commands, you get clean one-shot triggers.
This works in any mode:
- Momentary mode: Trigger on press, silent on release (great for drum pads, one-shot samples)
- Toggle mode: Action only when turning ON, silent when turning OFF
- Select mode: Action only when selecting, silent when deselecting
The firmware checks if a release action is configured before sending — if it's empty or missing, the button release is silent.
Repeat pattern (same message every press):
To send the same MIDI message every time you press the button (not alternating), use toggle mode with identical commands in both press and release arrays:
{
"label": "TAP",
"color": "green",
"mode": "toggle",
"press": [
{"type": "cc", "cc": 64, "value": 127}
],
"release": [
{"type": "cc", "cc": 64, "value": 127} // Same as press!
]
}How it works:
- First press: Turns button ON, sends
presscommands (CC64=127) - Second press: Turns button OFF, sends
releasecommands (also CC64=127) - Third press: Turns button ON, sends
presscommands (CC64=127) - Result: Same message sent every time you press
Use cases:
- Tap tempo (send same CC repeatedly for BPM detection)
- Scene advance (increment scene on each press)
- MIDI clock nudge or sync
- Any device expecting repeated triggers of the same value
Select groups:
Buttons with the same select_group act like radio buttons — selecting one deselects others in the group. Works with both toggle and select modes.
Keytimes allows a button to cycle through multiple states on repeated presses, similar to the OEM SuperMode firmware. Each state can have different MIDI values and LED colors.
{
"label": "VERB",
"color": "blue",
"mode": "toggle",
"keytimes": 3,
"press": [
{"type": "cc", "cc": 20, "value": 64}
],
"release": [
{"type": "cc", "cc": 20, "value": 0}
],
"states": [
{"cc": 20, "value": 64, "color": "blue"}, // State 1: 50% wet
{"cc": 20, "value": 96, "color": "cyan"}, // State 2: 75% wet
{"cc": 20, "value": 127, "color": "white"} // State 3: 100% wet
]
}- First press: Sends CC20=64, LED shows blue
- Second press: Sends CC20=96, LED shows cyan
- Third press: Sends CC20=127, LED shows white
- Fourth press: Cycles back to state 1
Each state in the states array can override command values from the base press/release arrays:
cc,value: CC command overridesnote,velocity: Note command overridesprogram: PC command overridepc_step: PC inc/dec step overridecolor: LED color for this statelabel: Display label for this state
- Keytimes defaults to 1 (standard single-state behavior)
- Maximum 99 states per button
- Works with toggle, momentary, and select modes
- State overrides apply to the command values in
press/releasearrays
Each button action can send multiple MIDI commands in sequence. This enables complex macros with a single footswitch press.
{
"label": "CH2+DLY",
"color": "red",
"press": [
{"type": "cc", "cc": 30, "value": 127}, // Switch to channel 2
{"type": "pc", "program": 12} // Load delay preset
]
}{
"label": "SCENE3",
"color": "purple",
"press": [
{"type": "pc", "program": 3}, // Load scene 3
{"type": "cc", "cc": 12, "value": 64} // Reset expression to middle
]
}Buttons can execute different commands based on runtime con 7292 ditions like other button states, received MIDI values, expression pedal position, or encoder value. This enables intelligent, context-aware behavior.
{
"label": "SMART",
"color": "purple",
"press": [
{
"type": "conditional",
"if": {"type": "button_state", "button": 0, "state": "on"},
"then": [
{"type": "cc", "cc": 30, "value": 127},
{"type": "pc", "program": 10}
],
"else": [
{"type": "cc", "cc": 30, "value": 64}
]
}
]
}Logic: If Button 0 is ON, send CC30=127 + PC10; otherwise send CC30=64.
1. Button State Check if another button is on or off:
{"type": "button_state", "button": 2, "state": "on"}2. Button Keytime Check the current keytime state of a multi-state button:
{"type": "button_keytime", "button": 3, "keytime": 1}Note: keytime is 0-indexed (0 = first state)
3. Received MIDI CC Check value of incoming CC messages:
{"type": "received_midi", "channel": 0, "cc": 20, "operator": "gte", "value": 64}Operators: eq, ne, gt, lt, gte, lte
4. Expression Pedal Position Check expression pedal value:
{"type": "expression", "pedal": "exp1", "operator": "gt", "value": 100}5. Encoder Position Check rotary encoder value:
{"type": "encoder", "operator": "gte", "value": 64}Scene-Aware Boost Button:
{
"label": "BOOST",
"color": "yellow",
"press": [
{
"type": "conditional",
"if": {"type": "button_state", "button": 0, "state": "on"},
"then": [{"type": "cc", "cc": 50, "value": 127}],
"else": [{"type": "cc", "cc": 51, "value": 127}]
}
]
}If clean channel (button 0) is active, use boost CC50; if drive channel, use boost CC51.
Expression-Based Scene Switch:
{
"label": "AUTO",
"color": "blue",
"press": [
{
"type": "conditional",
"if": {"type": "expression", "pedal": "exp1", "operator": "lt", "value": 30},
"then": [{"type": "pc", "program": 0}],
"else": [{"type": "pc", "program": 1}]
}
]
}Load scene 0 if expression pedal is at heel, scene 1 if at toe.
Host-Responsive Button:
{
"label": "SYNC",
"color": "green",
"press": [
{
"type": "conditional",
"if": {"type": "received_midi", "channel": 0, "cc": 100, "operator": "eq", "value": 127},
"then": [{"type": "cc", "cc": 25, "value": 127}],
"else": [{"type": "cc", "cc": 25, "value": 0}]
}
]
}Button behavior changes based on what the host last sent (bidirectional sync).
- Conditionals can be mixed with regular MIDI commands in the same action array
- Supports nested logic: THEN/ELSE branches can contain multiple commands
- All operators work with MIDI value range (0-127)
- Conditions are evaluated at button press time using current device state
Before deploying a config to your device, you can validate it offline using the provided CLI tool:
python tools/validate_config.py firmware/circuitpython/config.jsonFeatures:
- Device-aware validation: Checks button count matches device type (STD10=10, Mini6=6, etc.)
- Multi-bank support: Validates bank switch buttons and all banked configurations
- Conditional detection: Recognizes conditional commands in multi-command arrays
- Encoder/expression defaults: Matches firmware behavior (enabled: true by default)
- Batch validation:
--check-allmode validates all device templates
Example output:
✓ Config validation passed for std10
• Device: std10
• Buttons: 10
• Encoder: enabled
• Expression pedals: exp1, exp2
• Banks: 0
Error example:
✗ Config validation FAILED
• Button channel 16 exceeds max (15)
• Button 11 exceeds device limit for mini6 (6 buttons)
The validator uses the same validation logic as the CircuitPython firmware (imports from firmware/circuitpython/core/config.py), catching config errors before you deploy to hardware.
{
"label": "DELAY",
"color": "blue",
"press": [
{"type": "cc", "cc": 20, "value": 127} // Short press: delay ON
],
"long_press": [
{"type": "cc", "cc": 40, "value": 127, "threshold_ms": 700}, // Hold 700ms: tap tempo ON
{"type": "cc", "cc": 41, "value": 64} // Reset tap rate
],
"long_release": [
{"type": "cc", "cc": 40, "value": 0} // Release: tap tempo OFF
]
}The firmware supports CC (Control Change), Note On/Off, and Program Change messages. All MIDI mappings are fully configurable via config.json.
These are the default CC numbers (fully customizable):
| Input | MIDI Message |
|---|---|
| Encoder wheel | CC 11 (0-127 position) |
| Encoder push | CC 14 (127=press, 0=release) |
| Footswitch 1-10 | CC 20-29 (127=ON, 0=OFF) |
| Expression 1 | CC 12 (0-127) |
| Expression 2 | CC 13 (0-127) |
The device responds to incoming MIDI to update button states. Send CC messages matching your button's configured press commands:
CC 20, value 127→ Button 1 turns ON (LED lights up)CC 20, value 0→ Button 1 turns OFF (LED off/dim)
Value-based scene matching: The firmware matches incoming CC number, channel, and value against button configurations. This enables scene switching on devices like the Quad Cortex:
{
"label": "SCENE1",
"press": [{"type": "cc", "cc": 43, "value": 0, "channel": 0}]
},
{
"label": "SCENE2",
"press": [{"type": "cc", "cc": 43, "value": 1, "channel": 0}]
},
{
"label": "SCENE3",
"press": [{"type": "cc", "cc": 43, "value": 2, "channel": 0}]
}When the Quad Cortex sends CC 43, value 1, channel 0, only the SCENE2 button lights up.
- Gig Performer / MainStage — Sync button states with plugin bypass
- Ableton Live — Control track mutes/solos with visual feedback
- Guitar Rig / Helix Native — Effect on/off with LED confirmation
- Any MIDI-capable host — Generic CC control with bidirectional sync
| Path | Purpose |
|---|---|
firmware/circuitpython/ |
CircuitPython firmware (production-ready) |
firmware/rust/ |
Rust+Embassy firmware (experimental) |
config-editor/ |
Desktop config editor app (Tauri + Svelte) |
firmware/circuitpython/original_helmut/ |
Helmut Keller's original code (reference) |
docs/ |
Hardware specs, design docs |
tools/ |
Helper scripts |
Copyright © 2026 Maximilian Cascone. All rights reserved.
You may use this firmware freely for personal or commercial performances. Redistribution of modified versions requires permission. See LICENSE for details.
This project builds on work by Helmut Keller (hfrk.de), whose original firmware demonstrated bidirectional MIDI on the MIDI Captain. His code is preserved in firmware/original_helmut/ as a reference.
Open an issue or check AGENTS.md for developer documentation.