Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 217 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@



<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/uermel/padbound/refs/heads/main/assets/logo_dark.svg">
Expand All @@ -8,31 +9,48 @@
</picture>
</p>

<p align="center">
<a href="https://pypi.org/project/padbound/"><img alt="PyPI" src="https://img.shields.io/pypi/v/padbound"></a>
<a href="https://pypi.org/project/padbound/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/padbound"></a>
<a href="https://github.com/uermel/padbound/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/pypi/l/padbound"></a>
</p>

A general, stateful python interface for MIDI controllers that abstracts hardware differences behind a simple API.
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 provides a high-level abstraction over MIDI controllers, allowing applications to work with three fundamental
control types (toggles, momentary triggers, continuous controls) and RGB colors without dealing with raw MIDI messages.
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, Momentary, and Continuous controls with unified API
- **Progressive State Discovery**: Honest representation of hardware limitations (knobs/faders start in "unknown" state)
- **Capability-Based API**: Validates hardware support before attempting operations
- **Thread-Safe**: Safe concurrent access from callbacks and main thread
- **Plugin Architecture**: Extensible system for supporting different controllers
- **Callback System**: Global, per-control, and type-based callbacks with error isolation
- **Bank Support**: Handles controllers with bank switching (when supported)
- **Strict/Permissive Modes**: Choose between errors or warnings for unsupported operations
- **Three Control Types** &mdash; Toggle (on/off switch), Momentary (press-and-release trigger), and Continuous (knobs/faders with 0.0&ndash;1.0 range)
- **Progressive State Discovery** &mdash; Continuous controls start in "unknown" state until first interaction, honestly representing hardware limitations
- **Capability-Based API** &mdash; Validates hardware support before attempting operations; strict mode raises errors, permissive mode logs warnings
- **Thread-Safe** &mdash; Immutable state snapshots (Pydantic frozen models) and lock-protected internals for safe concurrent access
- **Plugin Architecture** &mdash; 7 built-in plugins covering popular controllers, with a straightforward base class for adding more
- **Callback System** &mdash; Five callback levels (global, per-control, per-type, per-category, per-bank) with error isolation and signal-type filtering
- **Bank Support** &mdash; Handles hardware-managed bank switching with per-bank configuration
- **LED Feedback** &mdash; Set pad colors (RGB or named), LED animation modes (solid/pulse/blink), and on/off states from code
- **Configuration Hierarchy** &mdash; Per-bank, per-control settings for types, colors, and LED modes with wildcard pattern matching
- **Debug TUI** &mdash; Real-time terminal visualization of controller state via WebSocket

## Installation

Requires **Python 3.12+**.

```bash
pip install padbound
```

For development and debugging tools:

```bash
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

```python
Expand All @@ -54,64 +72,80 @@ with Controller(plugin='auto', auto_connect=True) as controller:
controller.process_events()
```

## Examples

See the `examples/` directory for controller-specific demos:
- `demo_akai_lpd8.py` - AKAI LPD8 MK2
- `demo_akai_apc_mini_mk2.py` - AKAI APC mini MK2
- `demo_presonus_atom.py` - PreSonus ATOM
- `demo_xjam.py` - Xjam
- `demo_x_touch_mini.py` - Behringer X-Touch Mini
## Usage

### Callback Registration

Padbound provides five levels of callback registration, from most specific to broadest:

```python
from padbound import Controller, ControlType

with Controller(plugin='auto', auto_connect=True) as controller:
# Per-control callback
# 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 (all toggles, all continuous, etc.)
# 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 (e.g., all transport buttons)
# 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 (all controls)
# 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()
```

### Setting Control State
Callbacks can also filter by MIDI signal type for controllers with multi-signal pads:

```python
# 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)

```python
from padbound import Controller, StateUpdate

with Controller(plugin='auto', auto_connect=True) as controller:
# Set pad LED color and state
# 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)

# Query control state
# 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'}")
print(f"Pad 1 is {'on' if state.is_on else 'off'}, color: {state.color}")
```

### Using Configuration
### Configuration

Configure control types, colors, and LED modes per bank and per control:

```python
from padbound import Controller, ControllerConfig, BankConfig, ControlConfig, ControlType

# Configure pad colors and types
config = ControllerConfig(banks={
'bank_1': BankConfig(controls={
'pad_1': ControlConfig(type=ControlType.TOGGLE, color='red', off_color='dim_red'),
'pad_2': ControlConfig(type=ControlType.MOMENTARY, color='green'),
'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
})
})

Expand All @@ -120,27 +154,74 @@ with Controller(plugin='auto', config=config, auto_connect=True) as controller:
controller.process_events()
```

## Supported Controllers
Configuration can also be updated at runtime with `controller.reconfigure(new_config)`.

### Progressive Discovery

### Capability Comparison
Continuous controls (knobs, faders, encoders) have no way to report their physical position until the user moves them. Padbound makes this explicit:

```python
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

```python
# 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:

```python
# In your application
controller = Controller(plugin='auto', auto_connect=True, debug_server=True)
print(f"Debug URL: {controller.debug_url}")
```

```bash
# 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 |
| **PreSonus ATOM** | 16 | 4 | — | 20 | ✓ Full | Solid/Pulse/Blink | 8 (HW) | — | Native Control mode, encoder acceleration |
| **Xjam** | 16 | 6 | — | — | — | — | 3 (HW) | ✓ SysEx | Multi-signal pads, multiple encoder modes |
| **X-Touch Mini** | 16 | 8 + buttons | 1 | — | Single | Solid | 2 (HW) | — | Deferred LED feedback, auto-reflecting encoder rings |
|---|---|---|---|---|---|---|---|---|---|
| **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
- **LED Modes**: Animation/behavior modes supported
- **Persistent Config**: Device stores configuration in non-volatile memory
- **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
<details>
<summary><b>AKAI LPD8 MK2</b></summary>

**Control Surface**: 8 RGB pads + 8 knobs\
**Banks**: 4 banks with hardware-based switching\
**Capabilities**:
Expand All @@ -149,9 +230,12 @@ with Controller(plugin='auto', config=config, auto_connect=True) as controller:
- **Pad Modes**: Toggle or momentary (global per bank)
- **Knob Feedback**: None (read-only)
- **Configuration**: Persistent (SysEx)
</details>

<details>
<summary><b>AKAI APC mini MK2</b></summary>

#### AKAI APC mini MK2
**Control Surface**: 8×8 RGB pad grid + 9 faders + 17 buttons\
**Control Surface**: 8x8 RGB pad grid + 9 faders + 17 buttons\
**Banks**: Single layer\
**Capabilities**:
- **Pad LED Feedback**: Full RGB via SysEx
Expand All @@ -160,9 +244,25 @@ with Controller(plugin='auto', config=config, auto_connect=True) as controller:
- **Fader Feedback**: None (read-only, initial position discovered)
- **Button LED Feedback**: Single-color (red for track, green for scene)
- **Configuration**: Volatile
</details>

#### PreSonus ATOM
**Control Surface**: 16 RGB pads (4×4) + 4 encoders + 20 buttons\
<details>
<summary><b>AKAI MPD218</b></summary>

**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)
</details>

<details>
<summary><b>PreSonus ATOM</b></summary>

**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
Expand All @@ -172,8 +272,25 @@ with Controller(plugin='auto', config=config, auto_connect=True) as controller:
- **Encoder Feedback**: None (read-only)
- **Button LED Feedback**: Single-color
- **Configuration**: Volatile
</details>

<details>
<summary><b>Synido TempoPad P16</b></summary>

**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)
</details>

<details>
<summary><b>Xjam (ESI/Artesia Pro)</b></summary>

#### Xjam (ESI/Artesia Pro)
**Control Surface**: 16 pads + 6 knobs per bank\
**Banks**: 3 banks (Green, Yellow, Red) with synchronized pad/knob switching\
**Capabilities**:
Expand All @@ -182,8 +299,11 @@ with Controller(plugin='auto', config=config, auto_connect=True) as controller:
- **Knob Type**: Configurable (absolute or 3 relative modes)
- **Knob Feedback**: None (read-only)
- **Configuration**: Persistent (SysEx)
</details>

<details>
<summary><b>Behringer X-Touch Mini</b></summary>

#### Behringer X-Touch Mini
**Control Surface**: 8 encoders with buttons + 16 pads + 1 fader\
**Banks**: 2 layers (A, B) with hardware switching\
**Capabilities**:
Expand All @@ -195,10 +315,56 @@ with Controller(plugin='auto', config=config, auto_connect=True) as controller:
- **Encoder Button Feedback**: Single-color
- **Fader Feedback**: None (read-only)
- **Configuration**: Volatile
</details>

## Writing a Plugin

To add support for a new controller, subclass `ControllerPlugin` and implement the required methods:

```python
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

## Documentation
The `examples/` directory contains runnable demos for each supported controller:

TBD
- `demo_akai_lpd8.py` &mdash; AKAI LPD8 MK2
- `demo_akai_apc_mini_mk2.py` &mdash; AKAI APC mini MK2
- `demo_akai_mpd218.py` &mdash; AKAI MPD218
- `demo_presonus_atom.py` &mdash; PreSonus ATOM
- `demo_synido_tempopad.py` &mdash; Synido TempoPad P16
- `demo_xjam.py` &mdash; Xjam
- `demo_x_touch_mini.py` &mdash; Behringer X-Touch Mini

## Acknowledgements

Expand Down
Loading