A stateful Python interface for MIDI controllers that abstracts hardware differences behind a unified API with three fundamental control types: Toggle, Momentary, and Continuous.
Padbound lets applications work with MIDI controllers using abstract control IDs ("pad_1", "knob_3") and high-level state (on/off, colors, normalized values) instead of raw MIDI messages. A plugin system handles the translation for each controller, so your application code works across different hardware without changes.
- Three Control Types — Toggle (on/off switch), Momentary (press-and-release trigger), and Continuous (knobs/faders with 0.0–1.0 range)
- Progressive State Discovery — Continuous controls start in "unknown" state until first interaction, honestly representing hardware limitations
- Capability-Based API — Validates hardware support before attempting operations; strict mode raises errors, permissive mode logs warnings
- Thread-Safe — Immutable state snapshots (Pydantic frozen models) and lock-protected internals for safe concurrent access
- Plugin Architecture — 7 built-in plugins covering popular controllers, with a straightforward base class for adding more
- Callback System — Five callback levels (global, per-control, per-type, per-category, per-bank) with error isolation and signal-type filtering
- Bank Support — Handles hardware-managed bank switching with per-bank configuration
- LED Feedback — Set pad colors (RGB or named), LED animation modes (solid/pulse/blink), and on/off states from code
- Configuration Hierarchy — Per-bank, per-control settings for types, colors, and LED modes with wildcard pattern matching
- Debug TUI — Real-time terminal visualization of controller state via WebSocket
Requires Python 3.12+.
pip install padboundFor development and debugging tools:
pip install padbound[debug] # Debug TUI + WebSocket server
pip install padbound[dev] # Linting, formatting, notebooks
pip install padbound[test] # pytest + coverage
pip install padbound[docs] # MkDocs documentationfrom padbound import Controller, ControlType
# Auto-detect and connect to controller
with Controller(plugin='auto', auto_connect=True) as controller:
# Register callback for a specific pad
controller.on_control('pad_1', lambda state: print(f"Pad 1: {state.is_on}"))
# Register callback for all continuous controls (knobs/faders)
def on_continuous(control_id, state):
if state.is_discovered:
print(f"{control_id}: {state.normalized_value:.2f}")
controller.on_type(ControlType.CONTINUOUS, on_continuous)
# Main loop
while True:
controller.process_events()Padbound provides five levels of callback registration, from most specific to broadest:
from padbound import Controller, ControlType
with Controller(plugin='auto', auto_connect=True) as controller:
# Per-control callback — fires only for pad_1
controller.on_control('pad_1', lambda state: print(f"Pad 1: {state.is_on}"))
# Per-type callback — fires for all toggles, all continuous, etc.
controller.on_type(ControlType.TOGGLE, lambda cid, state: print(f"{cid} toggled"))
# Per-category callback — fires for all controls in a category (e.g., transport)
controller.on_category('transport', lambda cid, state: print(f"Transport: {cid}"))
# Global callback — fires for every control change
controller.on_global(lambda cid, state: print(f"Any control: {cid}"))
# Bank change callback — fires when a bank switches
controller.on_bank_change('pad', lambda bank_id: print(f"Switched to {bank_id}"))
while True:
controller.process_events()Callbacks can also filter by MIDI signal type for controllers with multi-signal pads:
# Only fires for note messages, not CC or program change
controller.on_control('pad_1', my_callback, signal_type='note')All callbacks are error-isolated — if one callback raises an exception, other callbacks and the main loop continue unaffected.
from padbound import Controller, StateUpdate
with Controller(plugin='auto', auto_connect=True) as controller:
# Set a single pad's LED color and state
update = StateUpdate(is_on=True, color='red')
if controller.can_set_state('pad_1', update):
controller.set_state('pad_1', update)
# Batch update — plugin can optimize into fewer MIDI messages
controller.set_states([
('pad_1', StateUpdate(is_on=True, color='red')),
('pad_2', StateUpdate(is_on=True, color='green')),
('pad_3', StateUpdate(is_on=False)),
])
# Query current state
state = controller.get_state('pad_1')
if state:
print(f"Pad 1 is {'on' if state.is_on else 'off'}, color: {state.color}")Configure control types, colors, and LED modes per bank and per control:
from padbound import Controller, ControllerConfig, BankConfig, ControlConfig, ControlType
config = ControllerConfig(banks={
'bank_1': BankConfig(controls={
'pad_1': ControlConfig(type=ControlType.TOGGLE, on_color='red', off_color='dim_red'),
'pad_2': ControlConfig(type=ControlType.MOMENTARY, on_color='green'),
'pad_*': ControlConfig(on_color='blue'), # Wildcard — applies to all unmatched pads
})
})
with Controller(plugin='auto', config=config, auto_connect=True) as controller:
while True:
controller.process_events()Configuration can also be updated at runtime with controller.reconfigure(new_config).
Continuous controls (knobs, faders, encoders) have no way to report their physical position until the user moves them. Padbound makes this explicit:
state = controller.get_state('knob_1')
if state.is_discovered:
print(f"Knob 1 value: {state.normalized_value:.2f}")
else:
print("Knob 1: position unknown (not yet moved)")
# Get lists of discovered/undiscovered controls
print("Ready:", controller.get_discovered_controls())
print("Waiting:", controller.get_undiscovered_controls())# Strict mode (default) — raises CapabilityError for unsupported operations
controller = Controller(plugin='auto', strict_mode=True)
# Permissive mode — logs warnings instead of raising
controller = Controller(plugin='auto', strict_mode=False)Padbound includes a real-time terminal UI for visualizing controller state. Enable the WebSocket server on the controller, then connect with the TUI client:
# In your application
controller = Controller(plugin='auto', auto_connect=True, debug_server=True)
print(f"Debug URL: {controller.debug_url}")# In another terminal
padbound-debug --url ws://127.0.0.1:8765The TUI displays a live view of all pads, knobs, faders, and buttons with real-time state updates, colors, and bank information.
| Controller | Pads | Knobs/Encoders | Faders | Buttons | RGB LEDs | LED Modes | Banks | Persistent Config | Special Features |
|---|---|---|---|---|---|---|---|---|---|
| AKAI LPD8 MK2 | 8 | 8 knobs | — | — | Full | Solid | 4 (HW) | SysEx | Multi-signal pads (NOTE/CC/PC) |
| AKAI APC mini MK2 | 64 | — | 9 | 17 | Full | Solid/Pulse/Blink | 1 | — | Fader position discovery |
| AKAI MPD218 | 16 | 6 encoders | — | 6 | — | — | 3+3 (HW) | SysEx | Multi-signal pads, 16 presets, pressure sensing |
| PreSonus ATOM | 16 | 4 encoders | — | 20 | Full | Solid/Pulse/Blink | 8 (HW) | — | Native Control mode, encoder acceleration |
| Synido TempoPad P16 | 16 | 4 encoders | — | 6 | Full | — | 3 (HW) | SysEx | RGB color config via SysEx, dual working modes |
| Xjam | 16 | 6 knobs | — | — | — | — | 3 (HW) | SysEx | Multi-signal pads, multiple encoder modes |
| X-Touch Mini | 16 | 8 + buttons | 1 | — | Single | Solid | 2 (HW) | — | Auto-reflecting encoder rings |
Legend:
- HW = Hardware-managed bank switching
- RGB LEDs: Full = true RGB color support, Single = on/off only, — = hardware-managed or none
- LED Modes: Animation/behavior modes supported from software
- Persistent Config: Device stores configuration in non-volatile memory via SysEx
AKAI LPD8 MK2
Control Surface: 8 RGB pads + 8 knobs
Banks: 4 banks with hardware-based switching
Capabilities:
- Pad LED Feedback: Full RGB via SysEx
- Pad LED Modes: Solid
- Pad Modes: Toggle or momentary (global per bank)
- Knob Feedback: None (read-only)
- Configuration: Persistent (SysEx)
AKAI APC mini MK2
Control Surface: 8x8 RGB pad grid + 9 faders + 17 buttons
Banks: Single layer
Capabilities:
- Pad LED Feedback: Full RGB via SysEx
- Pad LED Modes: Solid, pulse, blink
- Pad Modes: Toggle or momentary (per pad)
- Fader Feedback: None (read-only, initial position discovered)
- Button LED Feedback: Single-color (red for track, green for scene)
- Configuration: Volatile
AKAI MPD218
Control Surface: 16 velocity/pressure-sensitive pads + 6 encoders + 6 buttons
Banks: 3 pad banks + 3 control banks with hardware switching (48 pads, 18 knobs total)
Capabilities:
- Pad LED Feedback: None (red backlit, hardware-managed)
- Pad Modes: Toggle or momentary (per pad via SysEx preset)
- Pad Signals: NOTE, Program Change, or Bank messages
- Encoder Feedback: None (read-only)
- Configuration: Persistent (SysEx, 16 presets)
PreSonus ATOM
Control Surface: 16 RGB pads (4x4) + 4 encoders + 20 buttons
Banks: 8 hardware-managed banks (not software-accessible)
Capabilities:
- Pad LED Feedback: Full RGB via Native Control mode
- Pad LED Modes: Solid, pulse, breathe
- Pad Modes: Toggle or momentary (per pad)
- Encoder Type: Relative with acceleration
- Encoder Feedback: None (read-only)
- Button LED Feedback: Single-color
- Configuration: Volatile
Synido TempoPad P16
Control Surface: 16 RGB pads (4x4) + 4 encoders + 6 transport buttons
Banks: 3 pad/encoder banks with hardware switching
Capabilities:
- Pad LED Feedback: RGB colors via SysEx (stored in device memory)
- Pad LED State: Hardware-managed (no real-time software control)
- Pad Modes: Toggle or momentary (per pad in user-defined mode)
- Encoder Feedback: None (read-only)
- Configuration: Persistent (SysEx)
- Working Modes: Keyboard mode (red LED) and User-Defined mode (green LED)
Xjam (ESI/Artesia Pro)
Control Surface: 16 pads + 6 knobs per bank
Banks: 3 banks (Green, Yellow, Red) with synchronized pad/knob switching
Capabilities:
- Pad LED Feedback: None (hardware-managed)
- Pad Modes: Toggle or momentary (global)
- Knob Type: Configurable (absolute or 3 relative modes)
- Knob Feedback: None (read-only)
- Configuration: Persistent (SysEx)
Behringer X-Touch Mini
Control Surface: 8 encoders with buttons + 16 pads + 1 fader
Banks: 2 layers (A, B) with hardware switching
Capabilities:
- Pad LED Feedback: Single-color
- Pad LED Modes: Solid
- Pad Modes: Toggle or momentary (per pad)
- Encoder Type: Absolute
- Encoder Feedback: LED ring auto-reflects value
- Encoder Button Feedback: Single-color
- Fader Feedback: None (read-only)
- Configuration: Volatile
To add support for a new controller, subclass ControllerPlugin and implement the required methods:
from padbound import ControllerPlugin, ControlDefinition, plugin_registry
from padbound.plugin import MIDIMapping
class MyControllerPlugin(ControllerPlugin):
port_patterns = ["My Controller"] # For auto-detection from MIDI port names
@property
def name(self) -> str:
return "My Controller"
def get_control_definitions(self) -> list[ControlDefinition]:
# Define all pads, knobs, buttons with their capabilities
...
def get_input_mappings(self) -> dict[str, MIDIMapping]:
# Map MIDI messages to control IDs
...
def init(self, send_message, receive_message):
# Initialize controller to a known state
...
def translate_feedback_batch(self, updates):
# Convert state updates to MIDI messages for LED feedback
...
# Register the plugin
plugin_registry.register(MyControllerPlugin)See src/padbound/plugins/example_midi_controller.py for a complete reference implementation.
The examples/ directory contains runnable demos for each supported controller:
demo_akai_lpd8.py— AKAI LPD8 MK2demo_akai_apc_mini_mk2.py— AKAI APC mini MK2demo_akai_mpd218.py— AKAI MPD218demo_presonus_atom.py— PreSonus ATOMdemo_synido_tempopad.py— Synido TempoPad P16demo_xjam.py— Xjamdemo_x_touch_mini.py— Behringer X-Touch Mini
Some protocol information for supported controllers was gathered from:
- AKAI LPD8 MK2: stephensrmmartin/lpd8mk2
- PreSonus ATOM: EMATech/AtomCtrl
- Behringer X-Touch Mini: AndreasPantle/X-Touch-Mini-HandsOn
MIT License