From 71498b1e7679a22a57a42a0ad46cc875eb336f62 Mon Sep 17 00:00:00 2001 From: uermel Date: Sat, 14 Mar 2026 23:08:32 -0700 Subject: [PATCH] update readme --- README.md | 268 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 217 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 32cfdcf..3f30060 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ +

@@ -8,31 +9,48 @@

+

+ PyPI + Python + License +

-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** — 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+**. + ```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 @@ -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 }) }) @@ -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 +
+AKAI LPD8 MK2 + **Control Surface**: 8 RGB pads + 8 knobs\ **Banks**: 4 banks with hardware-based switching\ **Capabilities**: @@ -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) +
+ +
+AKAI APC mini MK2 -#### 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 @@ -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 +
-#### PreSonus ATOM -**Control Surface**: 16 RGB pads (4×4) + 4 encoders + 20 buttons\ +
+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 @@ -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 +
+ +
+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) -#### Xjam (ESI/Artesia Pro) **Control Surface**: 16 pads + 6 knobs per bank\ **Banks**: 3 banks (Green, Yellow, Red) with synchronized pad/knob switching\ **Capabilities**: @@ -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) +
+ +
+Behringer X-Touch Mini -#### Behringer X-Touch Mini **Control Surface**: 8 encoders with buttons + 16 pads + 1 fader\ **Banks**: 2 layers (A, B) with hardware switching\ **Capabilities**: @@ -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 +
+ +## 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` — 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