Skip to main content

A stateful python interface for USB MIDI controllers.

Project description

Padbound

PyPI Python License

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 MK2
  • demo_akai_apc_mini_mk2.py — AKAI APC mini MK2
  • demo_akai_mpd218.py — AKAI MPD218
  • demo_presonus_atom.py — PreSonus ATOM
  • demo_synido_tempopad.py — Synido TempoPad P16
  • demo_xjam.py — Xjam
  • demo_x_touch_mini.py — Behringer X-Touch Mini

Acknowledgements

Some protocol information for supported controllers was gathered from:

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

padbound-0.3.0.tar.gz (273.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

padbound-0.3.0-py3-none-any.whl (134.0 kB view details)

Uploaded Python 3

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

Hashes for padbound-0.3.0.tar.gz
Algorithm Hash digest
SHA256 ed20d3d98d296af6ef5970ba44de08c0f5e9f612d3410378d54377dfa1b06d39
MD5 2e36d5567c9d54c3a102fc4a97db048b
BLAKE2b-256 2d83fdbd3be11e7dfc2b81cde0c07afb93d8c936f0a0e9cd4d9e040cfdf115fb

See more details on using hashes here.

Provenance

The following attestation bundles were made for padbound-0.3.0.tar.gz:

Publisher: release-please.yml on uermel/padbound

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for padbound-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 65b5a7c9cce09ccd2b18ffb93eff7d328726c5320850f3e0640174adfbe8b32d
MD5 b4dd519dd2b06a9f252f051a47ad660b
BLAKE2b-256 39abdcf4dae09d9bdc8ccc2593d3ad3a5d1e253c8bfc02951e82ec9c29b7606e

See more details on using hashes here.

Provenance

The following attestation bundles were made for padbound-0.3.0-py3-none-any.whl:

Publisher: release-please.yml on uermel/padbound

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page