A stateful python interface for USB MIDI controllers.
Project description
A stateful Python interface for MIDI controllers that abstracts hardware differences behind a unified API with three fundamental control types: Toggle, Momentary, and Continuous.
Overview
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.
Features
- 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
Installation
Requires Python 3.12+.
pip install padbound
For 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 documentation
Quick Start
from 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()
Usage
Callback Registration
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.
Setting Control State (LED Feedback)
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}")
Configuration
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).
Progressive Discovery
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 vs. Permissive Mode
# 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)
Debug TUI
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:8765
The TUI displays a live view of all pads, knobs, faders, and buttons with real-time state updates, colors, and bank information.
Supported Controllers
| 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
Detailed Controller Information
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
Writing a Plugin
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.
Examples
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
Acknowledgements
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
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file padbound-0.3.0.tar.gz.
File metadata
- Download URL: padbound-0.3.0.tar.gz
- Upload date:
- Size: 273.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed20d3d98d296af6ef5970ba44de08c0f5e9f612d3410378d54377dfa1b06d39
|
|
| MD5 |
2e36d5567c9d54c3a102fc4a97db048b
|
|
| BLAKE2b-256 |
2d83fdbd3be11e7dfc2b81cde0c07afb93d8c936f0a0e9cd4d9e040cfdf115fb
|
Provenance
The following attestation bundles were made for padbound-0.3.0.tar.gz:
Publisher:
release-please.yml on uermel/padbound
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
padbound-0.3.0.tar.gz -
Subject digest:
ed20d3d98d296af6ef5970ba44de08c0f5e9f612d3410378d54377dfa1b06d39 - Sigstore transparency entry: 1107637809
- Sigstore integration time:
-
Permalink:
uermel/padbound@835a0334e7c71416084cf64a27f5598de4ff06b5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/uermel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@835a0334e7c71416084cf64a27f5598de4ff06b5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file padbound-0.3.0-py3-none-any.whl.
File metadata
- Download URL: padbound-0.3.0-py3-none-any.whl
- Upload date:
- Size: 134.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
65b5a7c9cce09ccd2b18ffb93eff7d328726c5320850f3e0640174adfbe8b32d
|
|
| MD5 |
b4dd519dd2b06a9f252f051a47ad660b
|
|
| BLAKE2b-256 |
39abdcf4dae09d9bdc8ccc2593d3ad3a5d1e253c8bfc02951e82ec9c29b7606e
|
Provenance
The following attestation bundles were made for padbound-0.3.0-py3-none-any.whl:
Publisher:
release-please.yml on uermel/padbound
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
padbound-0.3.0-py3-none-any.whl -
Subject digest:
65b5a7c9cce09ccd2b18ffb93eff7d328726c5320850f3e0640174adfbe8b32d - Sigstore transparency entry: 1107637810
- Sigstore integration time:
-
Permalink:
uermel/padbound@835a0334e7c71416084cf64a27f5598de4ff06b5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/uermel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@835a0334e7c71416084cf64a27f5598de4ff06b5 -
Trigger Event:
push
-
Statement type: