Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Turn Any Controller Into a Supercharged Macro Pad

What if every button on your Xbox controller could be a keyboard shortcut? What if tapping a MIDI pad softly did one thing, and hitting it hard did something else?

Conductor is the missing link between your game controllers, MIDI devices, and your computer. It’s open-source, blazingly fast (<1ms latency), and lets you create workflows that expensive macro pads can’t touch.

v3.0 is here: MIDI + gamepads in one workflow. Use your $30 Xbox controller as a 15-button macro pad, or combine a MIDI controller with a racing wheel for creative hybrid setups.

The Killer Feature: Velocity sensitivity. Press soft = copy, press hard = paste. One pad, multiple actions. Mind. Blown.

What’s Possible with Conductor

Transform your creative workflow in ways traditional macro tools can’t:

🎵 Music Production

  • Velocity-sensitive recording: Soft press = loop record, hard press = punch record
  • One pad, three actions: Turn a $30 MIDI controller into a pressure-sensitive control surface
  • Gamepad as DAW navigator: Use your Xbox controller for timeline navigation while keeping hands on MIDI keyboard

💻 Software Development

  • Git workflow on a pad: Soft = status, medium = commit, hard = commit+push
  • Velocity-based scrolling: Soft tap = 1 line, hard hit = jump 10 lines
  • Build triggers: Press A button to build, hold B to run tests

🎮 Content Creation

  • Racing wheel for video editing: Use pedals for effects control, wheel for timeline scrubbing
  • Gamepad for streaming: 15+ shortcuts at your fingertips without touching keyboard
  • Form automation: Fill complex forms with one button press

⚡ Power Users

  • Repurpose existing hardware: That dusty Xbox controller? It’s now a 15-button macro pad
  • Hot-reload everything: Change configs on-the-fly, no restart needed (0-10ms reload)
  • Context-aware mappings: Different actions based on active app, time of day, or system state

Key Features

Multi-Protocol Input (v3.0+)

MIDI Controllers:

  • Full MIDI controller support with RGB LED feedback
  • Native Instruments Maschine, Launchpad, APC Mini, and more
  • SysEx support for advanced LED control

HID Gamepads (NEW!):

  • Xbox 360/One/Series controllers
  • PlayStation DualShock 4/DualSense controllers
  • Nintendo Switch Pro Controller
  • Button chords, analog sticks, triggers with full velocity sensitivity
  • See the Gamepad Support Guide for details

Coming Soon:

  • OSC (Open Sound Control) for networked devices
  • Custom USB HID devices
  • Keyboard/mouse intercept for hybrid workflows

Core Capabilities (v2.0.0)

Event Detection:

  • 4 Core Triggers: Note, Velocity Range, Encoder, Control Change
  • 5 Advanced Triggers: Long Press, Double-Tap, Chord, Aftertouch, Pitch Bend
  • 10 Action Types: Keystroke, Text, Launch, Shell, Volume Control, and more

Visual Interface (NEW!):

  • Tauri GUI: Modern desktop app for visual configuration
  • MIDI Learn: One-click auto-detection of MIDI inputs
  • Live Preview: Real-time event monitoring and testing
  • Device Templates: 6 built-in templates for popular controllers

Daemon Infrastructure (NEW!):

  • Background Service: Runs as system service with auto-start
  • Hot-Reload: Config changes applied in 0-10ms without restart
  • IPC Control: Control daemon via CLI or GUI
  • Per-App Profiles: Automatic profile switching

LED & Feedback:

  • 10 LED Schemes: Reactive, Rainbow, Pulse, Breathing, and custom patterns
  • Multi-Mode System: Switch between mapping sets on the fly
  • Device Profile Support: Load Native Instruments Controller Editor configurations

Performance

  • Response latency: <1ms typical
  • Memory footprint: 5-10MB
  • CPU usage: <1% idle, <5% active
  • Binary size: 3-5MB (optimized)

Who is Conductor For?

🎹 Music Producers & Live Performers

You have: A MIDI controller (Maschine, Launchpad, APC Mini) that’s not fully integrated into your workflow, or you’re manually switching modes constantly during recording sessions.

Conductor gives you: Velocity-sensitive DAW control, per-app profile switching, RGB LED feedback that shows your current state, and hot-reload that lets you tweak mappings mid-session.

Example workflow:

“Soft press on pad 1 = loop record, medium press = one-shot sample, hard press = toggle reverb. One pad, three actions. I don’t switch modes anymore—velocity does it for me.”

See Music Production Examples →


💻 Software Developers & DevOps Engineers

You have: Too many terminal windows open, countless keyboard shortcuts to remember, and repetitive git workflows that waste 20-30 minutes daily.

Conductor gives you: One-button git operations, build/test triggers mapped to gamepad buttons, and context-aware shortcuts that change based on your active IDE.

Example workflow:

“Press gamepad button A = git status. Hold for 2 seconds = commit and push with auto-generated message. My PlayStation controller saves me 30 minutes every day.”

See Developer Workflows →


🎮 Streamers & Content Creators

You have: A $150-300 Stream Deck on your wishlist, or you’re using keyboard shortcuts that break your flow during streams.

Conductor gives you: Professional stream controls for free using your existing Xbox/PlayStation controller, with velocity-sensitive audio fading and button chords for complex actions.

Example workflow:

“Xbox A button = switch scene, B = mute mic, triggers = analog audio fade in/out. I saved $300 by repurposing my gamepad instead of buying a Stream Deck.”

See Streaming Setup Guide →


🎬 Video Editors & Post-Production

You have: Keyboard-heavy editing workflows that strain your wrists, and you wish timeline scrubbing felt more natural.

Conductor gives you: Analog pedal control for timeline speed and zoom, ergonomic button layouts on MIDI controllers or racing wheels, and hands-free transport control.

Example workflow:

“My racing wheel’s gas pedal controls timeline playback speed (0-200%). Brake pedal = zoom level. It sounds crazy, but it’s incredibly intuitive and ergonomic.”

See Video Editing Examples →


⚡ Power Users & Automation Enthusiasts

You have: Repetitive tasks you’re sick of doing manually, forms that need filling out dozens of times daily, or app-specific shortcuts you can never remember.

Conductor gives you: Context-aware shortcuts that change based on active application, form automation that fills entire forms with one button press, and MIDI Learn that makes configuration visual and instant.

Example workflow:

“I mapped long-press to ‘fill entire web form with saved data.’ Double-tap = refresh page. Velocity determines scroll speed. Saved hours every week.”

See Automation Examples →


🕹️ Gamers Repurposing Controllers

You have: A dusty Xbox controller, old racing wheel, or HOTAS setup that you barely use anymore.

Conductor gives you: A second life for your gaming hardware as professional productivity tools. That $150 racing wheel becomes a $300 video editing controller.

Example workflow:

“My Thrustmaster HOTAS from Star Citizen now controls my entire dev environment. 20+ shortcuts without touching the keyboard. Plus, it’s just fun to use.”

See Gaming Hardware Repurposing →


Not Sure Where to Start?

Try the Quick Start Guide → - Set up your first mapping in 5 minutes

Browse Device Templates → - Pre-built configs for popular controllers

Explore Use Cases → - See how others use Conductor

Why Conductor?

Unlike existing MIDI mapping tools, Conductor provides:

  • Multi-Protocol Support: Use MIDI controllers AND gamepads in the same workflow (v3.0+)
  • Advanced Timing: Long press, double-tap, chord detection out of the box
  • Velocity Sensitivity: Different actions for soft/medium/hard pad hits
  • Full RGB Feedback: Not just on/off LEDs, but animated schemes and reactive color
  • Modern Architecture: Fast Rust core, hot-reload config, cross-platform
  • Open Source: Fully customizable, extensible, community-driven

Quick Examples

MIDI Controller

# Press pad lightly for copy, hard for paste
[[modes.mappings]]
trigger = { type = "VelocityRange", note = 36 }
soft = { action = { type = "Keystroke", key = "C", modifiers = ["Cmd"] } }
hard = { action = { type = "Keystroke", key = "V", modifiers = ["Cmd"] } }

# Hold for 2 seconds to open terminal
[[modes.mappings]]
trigger = { type = "LongPress", note = 37, duration_ms = 2000 }
action = { type = "Launch", path = "/Applications/Utilities/Terminal.app" }

Gamepad (v3.0+)

# Press A button to build your project
[[modes.mappings]]
trigger = { type = "GamepadButton", button = "South" }  # A on Xbox, X on PlayStation
action = { type = "Shell", command = "cargo build" }

# Hold B button for 1 second to run tests
[[modes.mappings]]
trigger = { type = "GamepadButton", button = "East", hold_ms = 1000 }
action = { type = "Shell", command = "cargo test" }

Platform Support

  • macOS: Full support (11+ Big Sur, Apple Silicon + Intel)
  • Linux: Planned for Phase 4 (Q4 2025)
  • Windows: Planned for Phase 4 (Q4 2025)

Device Compatibility

MIDI Controllers

  • Fully Supported: Native Instruments Maschine Mikro MK3 (RGB LEDs, HID access)
  • MIDI-Only Support: Any USB MIDI controller with basic LED feedback
  • Coming Soon: Launchpad, APC Mini, Korg nanoKontrol, and more

HID Gamepads (v3.0+)

  • Xbox: Xbox 360, Xbox One, Xbox Series X/S controllers
  • PlayStation: DualShock 4, DualSense (PS5)
  • Nintendo: Switch Pro Controller
  • Generic: Any gamepad with standard HID support

Get Started

Ready to dive in? Check out the Quick Start Guide or Installation Instructions.

Project Status

Conductor is currently at v3.0 with multi-protocol input support, production-ready daemon infrastructure, and visual GUI configuration.

What’s New in v3.0:

  • 🎮 HID Gamepad Support: Xbox, PlayStation, Switch Pro controllers
  • 🎯 Unified Input Manager: MIDI + gamepad in single workflow
  • 📦 Controller Templates: 3 official gamepad templates (Xbox, PS, Switch)
  • 🔍 MIDI Learn for Gamepads: Auto-detect gamepad buttons
  • Hot-Plug Detection: Automatic reconnection with exponential backoff

v2.0.0 Features:

  • 🎛️ Tauri GUI: Visual configuration editor with MIDI Learn mode
  • 🔄 Hot-Reload Daemon: 0-10ms config reloads without restart
  • 🎯 Per-App Profiles: Automatic profile switching based on active app
  • 📊 Live Event Console: Real-time event monitoring
  • 📦 Device Templates: 6 built-in MIDI templates

See the Roadmap for planned features and Changelog for full release notes.

Community

License

Conductor is open source software licensed under the MIT License. See LICENSE for details.


Next: Install Conductor | Quick Start

Conductor vs Alternatives

Choosing the right automation tool? Here’s how Conductor compares to popular alternatives.

Feature Comparison

FeatureConductorStream DeckKeyboard MaestroKarabiner
PriceFree (MIT)$150-300$36Free
MIDI Support✅ Full❌ None⚠️ Limited❌ None
Gamepad Support✅ Full (v3.0)❌ None❌ None❌ None
Velocity Sensitivity✅ Yes❌ No❌ No❌ No
Hot-Reload✅ <10ms❌ No⚠️ Slow✅ Fast
RGB LED Feedback✅ Yes✅ Yes❌ No❌ No
Per-App Profiles✅ Yes✅ Yes✅ Yes✅ Yes
Response Latency<1ms~5-10ms~2-5ms<1ms
PlatformmacOS (Linux/Win planned)macOS/WindowsmacOS onlymacOS only
CustomizationUnlimited (TOML + GUI)GUI onlyGUI + AppleScriptComplex JSON
Hardware Cost$0 (reuse existing)$150+ (proprietary)Any keyboardAny keyboard

Winner By Use Case

🎹 Music Production → Conductor

Why: Only tool with full MIDI + velocity sensitivity + gamepad support. Turn existing hardware into professional control surfaces.

Example: Use Maschine pads for velocity-sensitive recording while Xbox controller handles DAW navigation.


🎮 Gaming Hardware → Conductor

Why: Only automation tool supporting gamepads, racing wheels, flight sticks, and HOTAS controllers.

Example: Repurpose your $30 Xbox controller as a 15-button macro pad instead of buying a $150 Stream Deck.


💰 Budget-Conscious → Conductor

Why: Free + works with hardware you already own = $0-$300 savings.

Comparison:

  • Conductor: Free (use existing Xbox/MIDI controller)
  • Stream Deck: $150-300 + proprietary hardware
  • Keyboard Maestro: $36/license + keyboard only

🖱️ Plug-and-Play Simplicity → Stream Deck

Why: Zero configuration, visual GUI, works out of the box.

Trade-off: Limited to Stream Deck hardware, no MIDI/gamepad, no velocity sensitivity, expensive.


⚙️ Maximum Flexibility → Conductor

Why: Hybrid MIDI+gamepad, open-source, unlimited customization, hot-reload.

Example: Combine MIDI pads (recording) + Xbox controller (navigation) + racing wheel pedals (effects) in one workflow. No other tool can do this.


💻 Keyboard-Only Automation → Keyboard Maestro or Karabiner

Why: Mature, keyboard-focused automation with macOS integration.

Trade-off: No MIDI, no gamepads, no velocity sensitivity, slower reload.


Quick Decision Guide

Choose Conductor if you:

  • ✅ Own a MIDI controller or gamepad
  • ✅ Want velocity-sensitive actions (one button = multiple functions)
  • ✅ Need hybrid MIDI+gamepad workflows
  • ✅ Want to reuse existing hardware (save $150+)
  • ✅ Value open-source and customization

Choose Stream Deck if you:

  • ✅ Want zero-configuration plug-and-play
  • ✅ Don’t own MIDI/gamepad hardware
  • ✅ Prefer visual button displays
  • ✅ Budget isn’t a concern ($150-300)

Choose Keyboard Maestro if you:

  • ✅ Only need keyboard-based macros
  • ✅ Want mature macOS integration
  • ✅ Don’t need MIDI/gamepad support

Choose Karabiner if you:

  • ✅ Need deep keyboard remapping
  • ✅ Want free solution (keyboard only)
  • ✅ Don’t mind complex JSON configuration

Unique Conductor Advantages

1. Velocity Sensitivity (Unmatched)

No other tool offers this: Soft/medium/hard press = different actions on the same button.

Example: Press pad softly = copy, press hard = paste. One pad, two functions.

2. Hybrid Multi-Protocol (v3.0)

No other tool supports: MIDI + gamepads simultaneously in one workflow.

Example: MIDI pads for recording + Xbox controller for navigation + racing wheel for effects.

3. Repurpose Existing Hardware

Save $150-300: Use Xbox controller, PlayStation DualSense, MIDI keyboard you already own instead of buying Stream Deck.

4. Open Source & Free

MIT licensed: No subscriptions, no vendor lock-in, community-driven development.

5. Blazing Fast (<1ms latency)

Lowest latency: Hot-reload in <10ms, event processing <1ms, daemon architecture.


Migration Guides

From Stream Deck

  1. Export your Stream Deck button layouts
  2. Map buttons to gamepad/MIDI IDs in Conductor
  3. Import provided template configs
  4. Save $150+ by reusing existing hardware

See Stream Deck Migration Guide →

From Keyboard Maestro

  1. Export keyboard shortcuts list
  2. Map shortcuts to MIDI/gamepad triggers
  3. Add velocity sensitivity for multi-function buttons
  4. Gain MIDI/gamepad support KM doesn’t have

See Keyboard Maestro Migration Guide →


Frequently Compared

“Can I use Conductor alongside Stream Deck?”

Yes! Conductor and Stream Deck can coexist. Use Stream Deck for visual buttons, Conductor for MIDI/gamepad + velocity sensitivity.

“Is Conductor harder to configure than Stream Deck?”

Initial setup: Conductor requires TOML config or GUI (5-10 min), Stream Deck is plug-and-play (1 min).

Long-term: Conductor’s hot-reload (<10ms) is faster than Stream Deck’s UI reconfiguration. Visual MIDI Learn mode makes config easy.

“Why not just use Keyboard Maestro?”

Keyboard Maestro is excellent for keyboard-only macros, but lacks:

  • MIDI support
  • Gamepad support
  • Velocity sensitivity
  • Fast hot-reload
  • Hybrid multi-protocol workflows

Conductor is the only tool supporting MIDI+gamepad+velocity in one workflow.


Try Conductor Free

No commitment, no credit card, no proprietary hardware required.

Get Started → | Download Templates → | Join Community →

macOS Installation Guide

Overview

This guide walks through installing and configuring Conductor v3.0.0 on macOS. Conductor now includes multi-protocol input support (MIDI controllers + game controllers), a background daemon service, and a modern Tauri-based GUI for visual configuration.

Installation Options:

  • Option 1 (Recommended): Download pre-built GUI app + daemon binaries from GitHub Releases
  • Option 2: Build from source (developers/advanced users)

Installation takes approximately 10-15 minutes.

1. Download Conductor

Visit the Releases Page and download:

For GUI + Daemon (Recommended):

  • conductor-gui-macos-universal.tar.gz - GUI application with daemon
  • OR download daemon separately: conductor-aarch64-apple-darwin.tar.gz (Apple Silicon) or conductor-x86_64-apple-darwin.tar.gz (Intel)

2. Install the GUI Application

# Extract the GUI app
tar xzf conductor-gui-macos-universal.tar.gz

# Move to Applications folder
mv "Conductor GUI.app" /Applications/

# Open the app
open /Applications/"Conductor GUI.app"

3. Install the Daemon Binary (Optional - GUI includes daemon)

If you want to use the daemon independently:

# Extract daemon binary
tar xzf conductor-*.tar.gz

# Make it executable
chmod +x conductor

# Move to PATH
sudo mv conductor /usr/local/bin/

# Verify installation
conductor --version

Skip to Configuring macOS Permissions


Option 2: Build from Source

Prerequisites

1. Hardware Requirements

Conductor v3.0 supports two types of input devices:

MIDI Controllers:

  • Native Instruments Maschine Mikro MK3 (recommended, full RGB LED support)
  • Generic MIDI controllers (keyboard controllers, pad controllers, etc.)
  • USB-MIDI or MIDI over Bluetooth

Game Controllers (HID) (v3.0+):

  • Gamepads: Xbox (360, One, Series X|S), PlayStation (DualShock 4, DualSense), Switch Pro Controller
  • Joysticks: Flight sticks, arcade sticks
  • Racing Wheels: Logitech, Thrustmaster, or any SDL2-compatible wheel
  • HOTAS: Hands On Throttle And Stick systems
  • Custom Controllers: Any SDL2-compatible HID device

You need at least one MIDI controller OR one game controller to use Conductor. Both can be used simultaneously.

2. Software Requirements

Rust Toolchain (for building from source):

Conductor is written in Rust and requires the Rust compiler and Cargo build system.

Check if Rust is already installed:

rustc --version
cargo --version

If you see version numbers (e.g., rustc 1.75.0), skip to the next section.

Install Rust using rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Follow the prompts and select the default installation. Then reload your shell:

source $HOME/.cargo/env

Verify installation:

rustc --version  # Should show: rustc 1.75.0 (or later)
cargo --version  # Should show: cargo 1.75.0 (or later)

Node.js and npm (for GUI only):

Required if building the Tauri GUI:

# Install Node.js via Homebrew
brew install node@20

# Verify installation
node --version  # Should show: v20.x.x
npm --version   # Should show: 10.x.x

SDL2 Library (for game controllers):

SDL2 is included via the gilrs v0.10 Rust crate. No additional installation required - it’s built into Conductor automatically.

3. Platform-Specific Requirements

Xcode Command Line Tools (Required):

Required for compiling native dependencies:

xcode-select --install

If already installed, you’ll see: “command line tools are already installed”.

4. Device-Specific Drivers (Optional)

Native Instruments Drivers (for Maschine Mikro MK3 only):

If using a Maschine Mikro MK3, install Native Instruments drivers for full RGB LED support.

Download and install:

  1. Visit Native Instruments Downloads
  2. Download Native Access (the NI installation manager)
  3. Install Native Access and sign in (free account)
  4. In Native Access, install:
    • Maschine software (includes drivers)
    • Controller Editor (for creating custom profiles, optional)

Verify driver installation:

system_profiler SPUSBDataType | grep -i maschine

You should see output like:

Maschine Mikro MK3:
  Product ID: 0x1600
  Vendor ID: 0x17cc  (Native Instruments)

Game Controller Drivers:

Most modern game controllers work natively on macOS without additional drivers:

  • Xbox Controllers: Native support (360, One, Series X|S)
  • PlayStation Controllers: Native support via Bluetooth or USB
  • Switch Pro Controller: Native support via Bluetooth or USB
  • Generic SDL2 Controllers: Usually work without drivers

No additional drivers are required for gamepad support.

Building from Source

1. Clone the Repository

# Choose a location for the project
cd ~/projects  # or wherever you keep code

# Clone the repository
git clone https://github.com/amiable-dev/conductor.git
cd conductor

2. Build the Daemon

Release build (recommended for regular use):

# Build the entire workspace (daemon + core)
cargo build --release --workspace

# Or build just the daemon binary
cargo build --release --package conductor-daemon

The release build takes 2-5 minutes on modern hardware and produces an optimized binary (~3-5MB) in target/release/conductor.

Build output:

   Compiling conductor-core v2.0.0 (/Users/you/projects/conductor/conductor-core)
   Compiling conductor-daemon v2.0.0 (/Users/you/projects/conductor/conductor-daemon)
    Finished release [optimized] target(s) in 2m 14s

3. Build the GUI (Optional)

# Install frontend dependencies
cd conductor-gui/ui
npm ci

# Build the frontend
npm run build

# Build the Tauri backend
cd ../src-tauri
cargo build --release

# The GUI app bundle will be at:
# conductor-gui/src-tauri/target/release/bundle/macos/Conductor GUI.app

4. Verify the Build

# Test daemon binary
./target/release/conductor --version

# Or run it
./target/release/conductor

# Test GUI (if built)
open conductor-gui/src-tauri/target/release/bundle/macos/"Conductor GUI.app"

Setting Up Configuration

v2.0.0 includes a visual configuration editor:

  1. Open Conductor GUI:

    open /Applications/"Conductor GUI.app"
    
  2. Connect your MIDI device in the device panel

  3. Use MIDI Learn mode:

    • Click “Learn” next to any trigger
    • Press a pad/button on your controller
    • The trigger config auto-fills
    • Assign an action (keystroke, launch app, etc.)
  4. Save configuration - automatically writes to ~/.config/conductor/config.toml

See GUI Quick Start for detailed tutorial.

Manual Configuration (Advanced)

If you prefer to edit config.toml manually:

Config location: ~/.config/conductor/config.toml

Create a minimal config:

mkdir -p ~/.config/conductor

cat > ~/.config/conductor/config.toml << 'EOF'
[device]
name = "Mikro"
auto_connect = true

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Test mapping - Copy"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

[[global_mappings]]
description = "Emergency exit (hold pad 0 for 3 seconds)"
[global_mappings.trigger]
type = "LongPress"
note = 0
hold_duration_ms = 3000
[global_mappings.action]
type = "Shell"
command = "killall conductor"
EOF

This creates a basic configuration with:

  • One mode (Default)
  • One test mapping (pad 12 = Cmd+C)
  • One emergency exit (hold pad 0 to quit)

Hot-reload: The daemon automatically reloads config within 0-10ms when you save changes.

Verifying Device Connection

Verifying MIDI Controller Connection

Check USB Device Enumeration

system_profiler SPUSBDataType | grep -i mikro

Expected output:

Maschine Mikro MK3:
  Product ID: 0x1600
  Vendor ID: 0x17cc (Native Instruments)
  Serial Number: XXXXX
  Location ID: 0x14200000 / 5

Check MIDI Connectivity

# Open Audio MIDI Setup
open -a "Audio MIDI Setup"

In the MIDI Studio window (Window → Show MIDI Studio):

  • You should see “Maschine Mikro MK3” listed
  • It should be connected (not grayed out)
  • Double-click to view its properties

Test MIDI Events

# Run diagnostic tool
cargo run --bin midi_diagnostic 2

Press pads on your controller. You should see:

[NoteOn] ch:0 note:12 vel:87
[NoteOff] ch:0 note:12 vel:0

If nothing appears:

  • Check USB connection
  • Verify correct port number (try 0, 1, 2, etc.)
  • Restart the device
  • Check Audio MIDI Setup

Verifying Game Controller Connection

Check System Recognition

# Open System Settings
open /System/Library/PreferencePanes/GameController.prefPane

Or manually navigate:

  • Open System SettingsGame Controllers
  • Your gamepad should appear in the list
  • Click to test buttons and analog sticks

Supported indicators:

  • Green icon: Controller is connected and working
  • Controller name displayed (e.g., “Xbox Wireless Controller”)
  • Button test interface available

Check via Conductor Status

# Start Conductor and check status
conductorctl status

# Look for gamepad in device list
# Example output:
# Connected Devices:
#   - Xbox Wireless Controller (Gamepad)

Test Gamepad Events

Use Conductor’s event console to verify gamepad inputs:

# Start Conductor with debug logging
DEBUG=1 conductor --foreground

Press buttons on your gamepad. You should see:

[GamepadButton] button:128 (A/Cross/B)
[GamepadButton] button:129 (B/Circle/A)
[GamepadAnalogStick] axis:128 value:255 (Left stick right)

If nothing appears:

  • Check USB or Bluetooth connection
  • Verify controller appears in System Settings → Game Controllers
  • Try reconnecting the controller
  • Restart Conductor
  • Check battery level (wireless controllers)

Platform-Specific Troubleshooting

Bluetooth Connection Issues:

  1. Forget the device in Bluetooth settings
  2. Put controller in pairing mode
  3. Re-pair the controller
  4. Test in Game Controllers settings

USB Connection Issues:

  1. Try a different USB port
  2. Try a different USB cable
  3. Restart the controller (unplug/replug or hold power button)

Configuring macOS Permissions

Input Monitoring Permission (Required for HID/LED Control)

macOS requires explicit permission for applications to access HID devices like the Maschine Mikro MK3 and game controllers.

Grant permission:

  1. Run Conductor once:

    cargo run --release 2
    
  2. macOS will show a permission dialog: “conductor would like to receive keystrokes from any application”

  3. Click Open System Settings or manually navigate:

    • Open System SettingsPrivacy & SecurityInput Monitoring
    • Find conductor (or Terminal if running via cargo run)
    • Toggle the switch to ON
  4. Restart Conductor:

    cargo run --release 2
    

Why this permission is required:

  • MIDI Controllers: HID-based RGB LED control (Maschine Mikro MK3)
  • Game Controllers: Reading gamepad button and analog stick inputs
  • Input Simulation: Simulating keyboard/mouse actions

Verify HID access:

DEBUG=1 cargo run --release 2

Look for:

[DEBUG] HID device opened successfully
[DEBUG] LED controller initialized
[DEBUG] Gamepad connected: Xbox Wireless Controller

If you see “HID device open failed” or gamepad not detected:

  • Input Monitoring permission is enabled
  • USB cable is connected (or Bluetooth paired)
  • Native Instruments drivers are installed (for Mikro MK3)
  • Controller appears in System Settings → Game Controllers

Accessibility Permission (Optional, for Advanced Actions)

Some actions (e.g., controlling other apps programmatically) may require Accessibility permission:

  1. Go to System SettingsPrivacy & SecurityAccessibility
  2. Click the + button
  3. Navigate to target/release/conductor (or add Terminal)
  4. Click Open

This is optional and only needed for specific advanced features.

Running Conductor

The simplest way to run Conductor v2.0.0:

  1. Launch the GUI:

    open /Applications/"Conductor GUI.app"
    
  2. The daemon starts automatically in the background

  3. Control via GUI:

    • View real-time MIDI events in the Event Console
    • Edit configuration visually
    • Monitor daemon status in the status bar
    • Pause/resume/reload from the GUI
  4. Control via menu bar (when daemon is running):

    • Click the Conductor icon in menu bar
    • Quick actions: Pause, Reload Config, Open GUI, Quit

Using the Daemon CLI

For headless operation or scripting:

Start the daemon:

# Start daemon in foreground
conductor

# Start daemon in background
conductor &

# Or use launchd (see Auto-Start section below)

Control the daemon with conductorctl:

# Check status
conductorctl status

# Reload configuration
conductorctl reload

# Stop daemon
conductorctl stop

# Validate config without reloading
conductorctl validate

# Ping daemon (latency check)
conductorctl ping

Output formats:

# Human-readable output (default)
conductorctl status

# JSON output (for scripting)
conductorctl status --json

Legacy CLI Options (Daemon)

The daemon binary still supports v1.0.0 CLI arguments:

# With LED lighting scheme
conductor --led reactive

# With device profile
conductor --profile ~/Downloads/my-profile.ncmm3

# With debug logging
DEBUG=1 conductor

Auto-Start on Login

The Conductor GUI includes built-in auto-start functionality:

  1. Open Conductor GUISettings
  2. Enable “Start Conductor on login”
  3. Click Save

This creates a LaunchAgent automatically and handles daemon startup.

Option 2: Manual LaunchAgent Setup

For daemon-only auto-start (no GUI):

1. Create LaunchAgent plist

mkdir -p ~/Library/LaunchAgents

cat > ~/Library/LaunchAgents/com.conductor.daemon.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.conductor.daemon</string>

    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/conductor</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <dict>
        <key>Crashed</key>
        <true/>
    </dict>

    <key>StandardOutPath</key>
    <string>/tmp/conductor.log</string>

    <key>StandardErrorPath</key>
    <string>/tmp/conductor.err</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin</string>
    </dict>
</dict>
</plist>
EOF

2. Load the LaunchAgent

launchctl load ~/Library/LaunchAgents/com.conductor.daemon.plist

3. Verify It’s Running

# Check launchd
launchctl list | grep conductor

# Check daemon status
conductorctl status

You should see:

Conductor Daemon Status:
  State: Running
  Uptime: 2m 15s
  Config: /Users/you/.config/conductor/config.toml
  IPC Socket: /tmp/conductor.sock

4. Control the LaunchAgent

# Stop
launchctl unload ~/Library/LaunchAgents/com.conductor.daemon.plist

# Start
launchctl load ~/Library/LaunchAgents/com.conductor.daemon.plist

# Restart
launchctl unload ~/Library/LaunchAgents/com.conductor.daemon.plist
launchctl load ~/Library/LaunchAgents/com.conductor.daemon.plist

5. Check Logs

# Standard output
tail -f /tmp/conductor.log

# Errors
tail -f /tmp/conductor.err

# Or use daemon status
conductorctl status --json | jq

Post-Installation Steps

1. Test Your Mappings

Press pads on your controller and verify actions execute correctly.

Test checklist:

  • Pad presses trigger actions
  • LEDs respond to presses (if using Mikro MK3)
  • Mode switching works (if configured)
  • Encoder controls work (if mapped)
  • Long press detection works
  • Double-tap detection works

2. Customize config.toml

Edit config.toml to add your own mappings. See:

3. Create Device Profile (Optional)

If you want custom pad layouts:

  1. Open Native Instruments Controller Editor
  2. Select Maschine Mikro MK3
  3. Edit pad pages (A-H)
  4. Save as .ncmm3 file
  5. Use with --profile flag

See Device Profiles Documentation for details.

Platform-Specific Notes

macOS Versions

Tested on:

  • macOS Sonoma (14.x) - Full support
  • macOS Ventura (13.x) - Full support
  • macOS Monterey (12.x) - Full support

Known issues:

  • macOS Big Sur (11.x) and earlier: HID shared device access may not work

Apple Silicon (M1/M2/M3)

Conductor works natively on Apple Silicon:

# Build for current architecture
cargo build --release

# Binary will be ARM64 (aarch64) on M1/M2/M3
file target/release/conductor
# Output: target/release/conductor: Mach-O 64-bit executable arm64

No special configuration needed - all dependencies support ARM64.

Intel Macs

Works identically to Apple Silicon. If you need a universal binary:

# Build for both architectures
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin

cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin

# Combine into universal binary
lipo -create \
    target/x86_64-apple-darwin/release/conductor \
    target/aarch64-apple-darwin/release/conductor \
    -output conductor-universal

Shared Device Access

Conductor uses macos-shared-device feature in hidapi to allow concurrent access with NI Controller Editor. This means:

  • ✅ You can run Conductor and Controller Editor simultaneously
  • ✅ Both can control LEDs without conflicts
  • ✅ Both receive MIDI input

This is enabled by default in Cargo.toml:

[dependencies]
hidapi = { version = "2.4", features = ["macos-shared-device"] }

Troubleshooting

Build Errors

Error: error: linker 'cc' not found

Solution: Install Xcode Command Line Tools:

xcode-select --install

Error: error: could not compile 'hidapi'

Solution: Update Rust and dependencies:

rustup update
cargo clean
cargo build --release

Runtime Errors - MIDI

Error: No MIDI input ports available

Solution:

  1. Check USB connection
  2. Open Audio MIDI Setup and verify device appears
  3. Try different USB port
  4. Restart device

Error: Failed to open HID device

Solution:

  1. Grant Input Monitoring permission (see above)
  2. Install Native Instruments drivers
  3. Check USB cable and connection
  4. Try running with sudo (not recommended long-term):
    sudo ./target/release/conductor 2
    

Error: Permission denied (os error 13)

Solution:

  1. Check Input Monitoring permission
  2. Verify binary has correct permissions:
    ls -l target/release/conductor
    chmod +x target/release/conductor
    

Runtime Errors - Game Controllers

Error: Gamepad not detected

Solution:

  1. Check connection (USB or Bluetooth)
  2. Verify controller appears in System Settings → Game Controllers
  3. Grant Input Monitoring permission
  4. Try reconnecting the controller
  5. Check debug output: DEBUG=1 conductor --foreground

Error: Gamepad buttons not responding

Solution:

  1. Use MIDI Learn to discover correct button IDs
  2. Verify button IDs are in range 128-255 (not 0-127)
  3. Check that Input Monitoring permission is granted
  4. Test in System Settings → Game Controllers
  5. Try a different USB cable or re-pair Bluetooth

Error: Analog stick not working

Solution:

  1. Check axis IDs (128-131 for sticks, 132-133 for triggers)
  2. Verify direction is correct (Clockwise/CounterClockwise)
  3. Adjust dead zone if too sensitive
  4. Use button triggers instead of analog for precise control

LED Issues (MIDI Controllers Only)

LEDs not lighting up:

  1. Verify Native Instruments drivers installed
  2. Check Input Monitoring permission
  3. Test with different LED scheme:
    cargo run --release 2 --led rainbow
    
  4. Check DEBUG output:
    DEBUG=1 cargo run --release 2
    

LEDs lighting wrong pads:

  1. Verify you’re using a device profile
  2. Check profile has correct note mappings
  3. See Device Profiles
  4. Use pad mapper to verify notes:
    cargo run --bin pad_mapper
    

Gamepad-Specific Issues

Controller works in games but not Conductor:

  1. Ensure Conductor has Input Monitoring permission
  2. Check that controller is SDL2-compatible
  3. Try USB connection instead of Bluetooth
  4. Restart Conductor after connecting controller

Bluetooth pairing issues:

  1. Forget device in Bluetooth settings
  2. Put controller in pairing mode (varies by controller):
    • Xbox: Hold pair button until LED flashes
    • PlayStation: Hold Share + PS button
    • Switch Pro: Hold sync button on top
  3. Re-pair and test in System Settings
  4. Use USB cable as fallback

Battery/Power issues (wireless):

  1. Charge or replace batteries
  2. Use USB cable for wired mode
  3. Check battery indicator in System Settings

For more troubleshooting help, see Gamepad Support Guide and Common Issues.

Next Steps

Now that Conductor v3.0.0 is installed and running:

For GUI Users

  1. Learn the GUI: Read GUI Quick Start Guide
  2. MIDI Learn Tutorial: See MIDI Learn Mode
  3. Device Templates: Check Using Device Templates
  4. Per-App Profiles: Set up Application-Specific Profiles
  5. Gamepad Setup: Read Gamepad Support Guide (v3.0+)

For CLI Users

  1. Daemon Control: Read Daemon & Hot-Reload Guide
  2. CLI Reference: See conductorctl Commands
  3. Manual Configuration: Check Configuration Overview
  4. Advanced Actions: Explore Actions Reference

For All Users

Getting Help

If you encounter issues:

  1. Check Common Issues
  2. Use Diagnostic Tools
  3. Enable debug logging: DEBUG=1 cargo run --release 2
  4. File an issue on GitHub with:
    • macOS version
    • Hardware (Intel/Apple Silicon)
    • Device model
    • Error messages
    • Output of cargo --version and rustc --version

Last Updated: November 21, 2025 (v3.0.0) macOS Support: 11.0+ (Big Sur and later) Architecture: Universal Binary (Intel + Apple Silicon) Input Support: MIDI Controllers + Game Controllers (HID)

Linux Installation Guide

Overview

This guide walks through installing and configuring Conductor v3.0.0 on Linux. Conductor now includes multi-protocol input support (MIDI controllers + game controllers), a background daemon service with systemd integration, and a modern Tauri-based GUI for visual configuration.

Installation Options:

  • Option 1 (Recommended): Download pre-built binaries from GitHub Releases
  • Option 2: Build from source (developers/advanced users)

Installation takes approximately 15-20 minutes.

Supported Distributions:

  • Ubuntu 20.04+ (LTS recommended)
  • Debian 11+ (Bullseye or later)
  • Fedora 35+
  • Arch Linux (rolling release)
  • Other systemd-based distributions

1. Download Conductor

Visit the Releases Page and download:

For GUI + Daemon (Recommended):

  • conductor-gui-linux-x86_64.tar.gz - GUI application with daemon
  • OR download daemon separately: conductor-x86_64-unknown-linux-gnu.tar.gz

2. Install the Binaries

# Extract the archive
tar xzf conductor-x86_64-unknown-linux-gnu.tar.gz

# Make binaries executable
chmod +x conductor conductorctl

# Move to PATH
sudo install -m 755 conductor /usr/local/bin/
sudo install -m 755 conductorctl /usr/local/bin/

# Verify installation
conductor --version
conductorctl --version

3. Install systemd Service (Optional)

# Create systemd user service directory
mkdir -p ~/.config/systemd/user

# Create service file
cat > ~/.config/systemd/user/conductor.service << 'EOF'
[Unit]
Description=Conductor Daemon - MIDI and Gamepad Macro Controller
After=network.target sound.target

[Service]
Type=simple
ExecStart=/usr/local/bin/conductor --foreground
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=default.target
EOF

# Reload systemd and enable service
systemctl --user daemon-reload
systemctl --user enable conductor
systemctl --user start conductor

# Check status
systemctl --user status conductor

Skip to Hardware Requirements


Option 2: Build from Source

Prerequisites

1. Hardware Requirements

Conductor v3.0 supports two types of input devices:

MIDI Controllers:

  • Native Instruments Maschine Mikro MK3 (recommended, full RGB LED support)
  • Generic MIDI controllers (keyboard controllers, pad controllers, etc.)
  • USB-MIDI or MIDI over Bluetooth

Game Controllers (HID) (v3.0+):

  • Gamepads: Xbox (360, One, Series X|S), PlayStation (DualShock 4, DualSense), Switch Pro Controller
  • Joysticks: Flight sticks, arcade sticks
  • Racing Wheels: Logitech, Thrustmaster, or any SDL2-compatible wheel
  • HOTAS: Hands On Throttle And Stick systems
  • Custom Controllers: Any SDL2-compatible HID device

You need at least one MIDI controller OR one game controller to use Conductor. Both can be used simultaneously.

2. Software Requirements

Rust Toolchain (for building from source):

Conductor is written in Rust and requires the Rust compiler and Cargo build system.

Check if Rust is already installed:

rustc --version
cargo --version

If you see version numbers (e.g., rustc 1.75.0), skip to the next section.

Install Rust using rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Follow the prompts and select the default installation. Then reload your shell:

source $HOME/.cargo/env

Verify installation:

rustc --version  # Should show: rustc 1.75.0 (or later)
cargo --version  # Should show: cargo 1.75.0 (or later)

SDL2 Library (for game controllers):

SDL2 is included via the gilrs v0.10 Rust crate. No additional installation required - it’s built into Conductor automatically.

3. Platform-Specific Requirements

Ubuntu/Debian:

sudo apt update
sudo apt install -y \
    build-essential \
    pkg-config \
    libasound2-dev \
    libudev-dev \
    libusb-1.0-0-dev \
    libjack-jackd2-dev

Fedora/RHEL:

sudo dnf install -y \
    gcc \
    gcc-c++ \
    pkg-config \
    alsa-lib-devel \
    systemd-devel \
    libusbx-devel \
    jack-audio-connection-kit-devel

Arch Linux:

sudo pacman -S base-devel alsa-lib systemd-libs libusb jack2

4. Game Controller Support (evdev)

Install evdev and jstest:

# Ubuntu/Debian
sudo apt install -y evdev joystick jstest-gtk

# Fedora/RHEL
sudo dnf install -y evdev joystick

# Arch Linux
sudo pacman -S linuxconsole

Verify game controller detection:

# List connected joysticks
ls /dev/input/js*

# Test a gamepad (if connected)
jstest /dev/input/js0

5. udev Rules (Required for HID Access)

Create udev rules to allow non-root access to HID devices:

sudo tee /etc/udev/rules.d/50-conductor.rules << 'EOF'
# Native Instruments Maschine Mikro MK3
SUBSYSTEM=="usb", ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1600", MODE="0666", GROUP="plugdev"

# Generic MIDI devices
SUBSYSTEM=="usb", ATTRS{bInterfaceClass}=="01", ATTRS{bInterfaceSubClass}=="03", MODE="0666", GROUP="plugdev"

# Game Controllers (SDL2-compatible)
SUBSYSTEM=="input", ATTRS{name}=="*Xbox*", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="input", ATTRS{name}=="*PlayStation*", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="input", ATTRS{name}=="*PLAYSTATION*", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="input", ATTRS{name}=="*DualShock*", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="input", ATTRS{name}=="*DualSense*", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="input", ATTRS{name}=="*Switch*", MODE="0666", GROUP="plugdev"

# Generic joystick/gamepad access
SUBSYSTEM=="input", KERNEL=="js[0-9]*", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="input", KERNEL=="event[0-9]*", MODE="0666", GROUP="plugdev"
EOF

# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger

Add your user to plugdev group:

sudo usermod -a -G plugdev $USER
sudo usermod -a -G input $USER

# Log out and back in for changes to take effect

Verify group membership:

groups | grep -E "plugdev|input"

Building from Source

1. Clone the Repository

# Choose a location for the project
cd ~/projects  # or wherever you keep code

# Clone the repository
git clone https://github.com/amiable-dev/conductor.git
cd conductor

2. Build the Daemon

Release build (recommended for regular use):

# Build the entire workspace (daemon + core)
cargo build --release --workspace

# Or build just the daemon binary
cargo build --release --package conductor-daemon

The release build takes 2-5 minutes on modern hardware and produces an optimized binary (~3-5MB) in target/release/conductor.

Build output:

   Compiling conductor-core v3.0.0 (/home/you/projects/conductor/conductor-core)
   Compiling conductor-daemon v3.0.0 (/home/you/projects/conductor/conductor-daemon)
    Finished release [optimized] target(s) in 2m 14s

3. Build the GUI (Optional)

# Install Node.js (if not already installed)
# Ubuntu/Debian
sudo apt install -y nodejs npm

# Fedora/RHEL
sudo dnf install -y nodejs npm

# Install frontend dependencies
cd conductor-gui/ui
npm ci

# Build the frontend
npm run build

# Build the Tauri backend
cd ../src-tauri
cargo build --release

# The GUI app bundle will be at:
# conductor-gui/src-tauri/target/release/conductor-gui

4. Install Binaries

# Return to project root
cd ~/projects/conductor

# Install binaries
sudo install -m 755 target/release/conductor /usr/local/bin/
sudo install -m 755 target/release/conductorctl /usr/local/bin/

# Verify installation
conductor --version
conductorctl --version

Verifying Device Connection

Verifying MIDI Controller Connection

Check USB Device Enumeration

lsusb | grep -i "Native Instruments"

Expected output:

Bus 001 Device 010: ID 17cc:1600 Native Instruments Maschine Mikro MK3

Check ALSA MIDI Ports

aconnect -l

Expected output should list your MIDI controller:

client 24: 'Maschine Mikro MK3' [type=kernel]
    0 'Maschine Mikro MK3 MIDI 1'

Test MIDI Events

# Run diagnostic tool
cargo run --bin midi_diagnostic 0

# Or if installed:
midi_diagnostic 0

Press pads on your controller. You should see:

[NoteOn] ch:0 note:12 vel:87
[NoteOff] ch:0 note:12 vel:0

If nothing appears:

  • Check USB connection
  • Verify correct port number (try 0, 1, 2, etc.)
  • Check udev rules are loaded
  • Verify user is in plugdev group

Verifying Game Controller Connection

Check evdev Detection

# List input devices
ls -l /dev/input/js*
ls -l /dev/input/event*

# Example output:
# crw-rw---- 1 root plugdev 13, 0 Nov 21 10:00 /dev/input/js0

Check Permissions

# Check that your user has access
ls -l /dev/input/js0

# Should show group as plugdev with rw permissions
# crw-rw---- 1 root plugdev 13, 0 Nov 21 10:00 /dev/input/js0

Test Gamepad with jstest

# Install jstest if not already installed
sudo apt install -y joystick  # Ubuntu/Debian
sudo dnf install -y joystick  # Fedora
sudo pacman -S linuxconsole   # Arch

# Test the gamepad
jstest /dev/input/js0

You should see button and axis values update when you press buttons or move sticks.

Press Ctrl+C to exit jstest

Check via Conductor Status

# Start Conductor
conductor --foreground &

# Check status
conductorctl status

# Look for gamepad in device list
# Example output:
# Connected Devices:
#   - Xbox Wireless Controller (Gamepad)

Test Gamepad Events

Use Conductor’s debug logging to verify gamepad inputs:

# Start Conductor with debug logging
DEBUG=1 conductor --foreground

Press buttons on your gamepad. You should see:

[GamepadButton] button:128 (A/Cross/B)
[GamepadButton] button:129 (B/Circle/A)
[GamepadAnalogStick] axis:128 value:255 (Left stick right)

If nothing appears:

  • Check USB or Bluetooth connection
  • Verify /dev/input/js* devices exist
  • Check udev rules are loaded
  • Verify user is in plugdev and input groups
  • Try reconnecting the controller

Platform-Specific Troubleshooting

Bluetooth Connection (BlueZ):

# Install Bluetooth tools
sudo apt install -y bluez bluez-tools  # Ubuntu/Debian
sudo dnf install -y bluez bluez-tools  # Fedora

# Enable Bluetooth service
sudo systemctl enable bluetooth
sudo systemctl start bluetooth

# Pair a controller using bluetoothctl
bluetoothctl
> scan on
> pair XX:XX:XX:XX:XX:XX
> connect XX:XX:XX:XX:XX:XX
> trust XX:XX:XX:XX:XX:XX
> exit

Xbox Controllers:

  • Wireless controllers require xpadneo driver for best compatibility
  • Install: sudo apt install -y xpadneo (Ubuntu/Debian)

PlayStation Controllers:

  • Native support via hid-playstation kernel module
  • For older systems, use ds4drv: sudo pip3 install ds4drv

Permissions Issues:

# If gamepad not accessible, check groups
groups

# Add user to required groups if missing
sudo usermod -a -G plugdev,input $USER

# Log out and log back in

Configuration

v3.0.0 includes a visual configuration editor:

  1. Open Conductor GUI:

    conductor-gui
    
  2. Connect your device in the device panel (MIDI or gamepad)

  3. Use MIDI Learn mode:

    • Click “Learn” next to any trigger
    • Press a button on your controller or gamepad
    • The trigger config auto-fills
    • Assign an action (keystroke, launch app, etc.)
  4. Save configuration - automatically writes to ~/.config/conductor/config.toml

See GUI Quick Start for detailed tutorial.

Manual Configuration (Advanced)

If you prefer to edit config.toml manually:

Config location: ~/.config/conductor/config.toml

Create a minimal config:

mkdir -p ~/.config/conductor

cat > ~/.config/conductor/config.toml << 'EOF'
[device]
name = "Mikro"
auto_connect = true

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Test mapping - Copy"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["ctrl"]

[[modes.mappings]]
description = "Gamepad A button - Paste"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # A/Cross/B button
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["ctrl"]

[[global_mappings]]
description = "Emergency exit (hold pad 0 for 3 seconds)"
[global_mappings.trigger]
type = "LongPress"
note = 0
hold_duration_ms = 3000
[global_mappings.action]
type = "Shell"
command = "killall conductor"
EOF

This creates a basic configuration with:

  • One mode (Default)
  • One MIDI test mapping (pad 12 = Ctrl+C)
  • One gamepad test mapping (A button = Ctrl+V)
  • One emergency exit (hold pad 0 to quit)

Hot-reload: The daemon automatically reloads config within 0-10ms when you save changes.

Running Conductor

Using the Daemon with systemd

The recommended way to run Conductor on Linux:

Start the service:

systemctl --user start conductor

Check status:

systemctl --user status conductor
conductorctl status

Enable auto-start on boot:

systemctl --user enable conductor

Control the daemon:

# Reload configuration
conductorctl reload

# Stop daemon
systemctl --user stop conductor

# Restart daemon
systemctl --user restart conductor

# View logs
journalctl --user -u conductor -f

Using the GUI

# Launch the GUI (starts daemon automatically)
conductor-gui

The GUI provides:

  • Real-time event console
  • Visual configuration editor
  • Device status monitoring
  • Daemon control (pause/resume/reload)

Manual Mode (Development/Testing)

For testing or development:

# Run in foreground
conductor --foreground

# Run with debug logging
DEBUG=1 conductor --foreground

# Run with specific config
conductor --config ~/my-config.toml --foreground

Troubleshooting

Build Errors

Error: error: linker 'cc' not found

Solution: Install build tools:

# Ubuntu/Debian
sudo apt install build-essential

# Fedora/RHEL
sudo dnf install gcc gcc-c++

# Arch Linux
sudo pacman -S base-devel

Error: could not compile 'alsa-sys'

Solution: Install ALSA development libraries:

# Ubuntu/Debian
sudo apt install libasound2-dev

# Fedora/RHEL
sudo dnf install alsa-lib-devel

# Arch Linux
sudo pacman -S alsa-lib

Error: could not compile 'libudev-sys'

Solution: Install udev development libraries:

# Ubuntu/Debian
sudo apt install libudev-dev

# Fedora/RHEL
sudo dnf install systemd-devel

# Arch Linux
sudo pacman -S systemd-libs

Runtime Errors - MIDI

Error: No MIDI input ports available

Solution:

  1. Check USB connection: lsusb
  2. Verify ALSA sees device: aconnect -l
  3. Check udev rules are loaded
  4. Verify user is in plugdev group

Error: Permission denied opening MIDI device

Solution:

  1. Check udev rules: ls -l /dev/snd/*
  2. Add user to audio group: sudo usermod -a -G audio $USER
  3. Log out and back in
  4. Verify: groups | grep audio

Runtime Errors - Game Controllers

Error: Gamepad not detected

Solution:

  1. Check /dev/input: ls -l /dev/input/js*
  2. Test with jstest: jstest /dev/input/js0
  3. Check udev rules are loaded: sudo udevadm control --reload-rules
  4. Verify groups: groups | grep -E "plugdev|input"
  5. Check debug output: DEBUG=1 conductor --foreground

Error: Permission denied: /dev/input/js0

Solution:

  1. Check file permissions: ls -l /dev/input/js0
  2. Verify udev rules: cat /etc/udev/rules.d/50-conductor.rules
  3. Add user to plugdev: sudo usermod -a -G plugdev,input $USER
  4. Reload udev: sudo udevadm control --reload-rules && sudo udevadm trigger
  5. Log out and back in

Error: Gamepad buttons not responding

Solution:

  1. Use MIDI Learn to discover correct button IDs
  2. Verify button IDs are in range 128-255 (not 0-127)
  3. Test in jstest to verify hardware works
  4. Check that gamepad appears in conductorctl status
  5. Try USB connection instead of Bluetooth

Error: Analog stick not working

Solution:

  1. Check axis IDs in jstest: jstest /dev/input/js0
  2. Verify axis IDs match config (128-133)
  3. Check dead zone settings
  4. Use button triggers instead of analog for precise control

Bluetooth Gamepad Issues

Controller not pairing:

# Install BlueZ tools
sudo apt install -y bluez bluez-tools

# Enable Bluetooth
sudo systemctl enable bluetooth
sudo systemctl start bluetooth

# Pair controller
bluetoothctl
> power on
> agent on
> default-agent
> scan on
# Wait for controller to appear
> pair XX:XX:XX:XX:XX:XX
> connect XX:XX:XX:XX:XX:XX
> trust XX:XX:XX:XX:XX:XX
> exit

Controller connects but not detected:

  1. Check /dev/input: ls /dev/input/js*
  2. Verify udev rules for Bluetooth devices
  3. Check dmesg for errors: dmesg | tail -20
  4. Try USB connection as fallback

systemd Service Issues

Service won’t start:

# Check service status
systemctl --user status conductor

# View logs
journalctl --user -u conductor -n 50

# Common issues:
# - Binary not in PATH
# - Config file missing or invalid
# - Permissions issues

Service stops after logout:

# Enable lingering to keep user services running
loginctl enable-linger $USER

Next Steps

Now that Conductor v3.0.0 is installed and running:

For GUI Users

  1. Learn the GUI: Read GUI Quick Start Guide
  2. MIDI Learn Tutorial: See MIDI Learn Mode
  3. Device Templates: Check Using Device Templates
  4. Per-App Profiles: Set up Application-Specific Profiles
  5. Gamepad Setup: Read Gamepad Support Guide (v3.0+)

For CLI Users

  1. Daemon Control: Read Daemon & Hot-Reload Guide
  2. CLI Reference: See conductorctl Commands
  3. Manual Configuration: Check Configuration Overview
  4. Advanced Actions: Explore Actions Reference

For All Users

Getting Help

If you encounter issues:

  1. Check Common Issues
  2. Use Diagnostic Tools
  3. Enable debug logging: DEBUG=1 conductor --foreground
  4. Check system logs: journalctl --user -u conductor -f
  5. File an issue on GitHub with:
    • Linux distribution and version
    • Kernel version (uname -r)
    • Device model (MIDI or gamepad)
    • Error messages from logs
    • Output of cargo --version and rustc --version

Last Updated: November 21, 2025 (v3.0.0) Linux Support: systemd-based distributions Architecture: x86_64, ARM64 Input Support: MIDI Controllers + Game Controllers (HID)

Windows Installation

Overview

This guide walks through installing and configuring Conductor v3.0.0 on Windows. Conductor now includes multi-protocol input support (MIDI controllers + game controllers), a background daemon service, and a modern Tauri-based GUI for visual configuration.

Installation Options:

  • Option 1 (Recommended): Download pre-built binaries from GitHub Releases
  • Option 2: Build from source (developers/advanced users)

Installation takes approximately 15-20 minutes.

Supported Windows Versions:

  • Windows 11 (recommended)
  • Windows 10 (1903 or later)
  • Windows Server 2019+

1. Download Conductor

Visit the Releases Page and download:

For GUI + Daemon (Recommended):

  • conductor-gui-windows-x86_64.zip - GUI application with daemon
  • OR download daemon separately: conductor-x86_64-pc-windows-msvc.zip

2. Install the Binaries

# Extract the archive
Expand-Archive -Path conductor-x86_64-pc-windows-msvc.zip -DestinationPath C:\Program Files\Conductor

# Add to PATH (PowerShell as Administrator)
$env:Path += ";C:\Program Files\Conductor"
[Environment]::SetEnvironmentVariable("Path", $env:Path, [EnvironmentVariableTarget]::Machine)

# Verify installation
conductor --version
conductorctl --version

3. Install as Windows Service (Optional)

# Run as Administrator
# Create scheduled task to auto-start Conductor
schtasks /create /tn "Conductor Daemon" /tr "C:\Program Files\Conductor\conductor.exe" /sc onlogon /rl highest

Skip to Hardware Requirements


Option 2: Build from Source

Prerequisites

1. Hardware Requirements

Conductor v3.0 supports two types of input devices:

MIDI Controllers:

  • Native Instruments Maschine Mikro MK3 (recommended, full RGB LED support)
  • Generic MIDI controllers (keyboard controllers, pad controllers, etc.)
  • USB-MIDI or MIDI over Bluetooth

Game Controllers (HID) (v3.0+):

  • Gamepads: Xbox (360, One, Series X|S - native support), PlayStation (DualShock 4, DualSense), Switch Pro Controller
  • Joysticks: Flight sticks, arcade sticks
  • Racing Wheels: Logitech, Thrustmaster, or any DirectInput/XInput compatible wheel
  • HOTAS: Hands On Throttle And Stick systems
  • Custom Controllers: Any SDL2-compatible HID device

You need at least one MIDI controller OR one game controller to use Conductor. Both can be used simultaneously.

2. Software Requirements

Rust Toolchain (for building from source):

Conductor is written in Rust and requires the Rust compiler and Cargo build system.

Check if Rust is already installed:

rustc --version
cargo --version

If you see version numbers (e.g., rustc 1.75.0), skip to the next section.

Install Rust using rustup:

  1. Download rustup-init.exe from https://rustup.rs/
  2. Run the installer
  3. Follow the prompts and select the default installation
  4. Restart your terminal/PowerShell

Verify installation:

rustc --version  # Should show: rustc 1.75.0 (or later)
cargo --version  # Should show: cargo 1.75.0 (or later)

SDL2 Library (for game controllers):

SDL2 is included via the gilrs v0.10 Rust crate. No additional installation required - it’s built into Conductor automatically.

3. Platform-Specific Requirements

Microsoft C++ Build Tools (Required):

Required for compiling native Rust dependencies.

Option A - Visual Studio Build Tools (Recommended):

  1. Download from https://visualstudio.microsoft.com/downloads/
  2. Install “Desktop development with C++” workload
  3. Restart your terminal

Option B - Full Visual Studio:

  1. Download Visual Studio Community (free)
  2. Install “Desktop development with C++” workload

Verify installation:

where cl
# Should show: C:\Program Files\Microsoft Visual Studio\...\cl.exe

4. Game Controller Support

Windows Game Controllers:

Most game controllers work natively on Windows without additional drivers:

Xbox Controllers:

  • Xbox 360: Native XInput support
  • Xbox One: Native XInput support via USB or Xbox Wireless Adapter
  • Xbox Series X|S: Native XInput support via USB, Bluetooth, or Xbox Wireless Adapter
  • No additional drivers required

PlayStation Controllers:

  • DualShock 4: Native DirectInput support via USB or Bluetooth
  • DualSense (PS5): Native DirectInput support via USB or Bluetooth
  • For XInput emulation (optional): Install DS4Windows from https://ds4-windows.com/

Switch Pro Controller:

  • Native DirectInput support via USB or Bluetooth
  • For XInput emulation (optional): Install BetterJoy or reWASD

Generic Controllers:

  • Most USB and Bluetooth gamepads work via DirectInput
  • SDL2 provides automatic mapping for 100+ controller types

Verify game controller detection:

# Open Windows Settings
start ms-settings:devices-controllersandgamedevices

# Or open Game Controllers directly
control joy.cpl

Your controller should appear in the list. Click “Properties” to test buttons and axes.

5. Device-Specific Drivers (Optional)

Native Instruments Drivers (for Maschine Mikro MK3 only):

If using a Maschine Mikro MK3, install Native Instruments drivers for full RGB LED support.

Download and install:

  1. Visit Native Instruments Downloads
  2. Download Native Access (the NI installation manager)
  3. Install Native Access and sign in (free account)
  4. In Native Access, install:
    • Maschine software (includes drivers)
    • Controller Editor (for creating custom profiles, optional)

Verify driver installation:

# Check Device Manager
devmgmt.msc

# Look for "Maschine Mikro MK3" under:
# - Sound, video and game controllers
# - Universal Serial Bus devices

Building from Source

1. Clone the Repository

# Choose a location for the project
cd ~\Projects  # or wherever you keep code

# Clone the repository
git clone https://github.com/amiable-dev/conductor.git
cd conductor

2. Build the Daemon

Release build (recommended for regular use):

# Build the entire workspace (daemon + core)
cargo build --release --workspace

# Or build just the daemon binary
cargo build --release --package conductor-daemon

The release build takes 3-7 minutes on modern hardware and produces an optimized binary (~3-5MB) in target\release\conductor.exe.

Build output:

   Compiling conductor-core v3.0.0 (C:\Users\you\Projects\conductor\conductor-core)
   Compiling conductor-daemon v3.0.0 (C:\Users\you\Projects\conductor\conductor-daemon)
    Finished release [optimized] target(s) in 3m 42s

3. Build the GUI (Optional)

# Install Node.js (if not already installed)
# Download from https://nodejs.org/ (LTS version)

# Install frontend dependencies
cd conductor-gui\ui
npm ci

# Build the frontend
npm run build

# Build the Tauri backend
cd ..\src-tauri
cargo build --release

# The GUI exe will be at:
# conductor-gui\src-tauri\target\release\conductor-gui.exe

4. Install Binaries

# Return to project root
cd ~\Projects\conductor

# Create installation directory
New-Item -ItemType Directory -Force -Path "C:\Program Files\Conductor"

# Copy binaries (requires Administrator)
Copy-Item target\release\conductor.exe "C:\Program Files\Conductor\"
Copy-Item target\release\conductorctl.exe "C:\Program Files\Conductor\"

# Add to PATH (PowerShell as Administrator)
$env:Path += ";C:\Program Files\Conductor"
[Environment]::SetEnvironmentVariable("Path", $env:Path, [EnvironmentVariableTarget]::Machine)

# Verify installation (restart terminal first)
conductor --version
conductorctl --version

Verifying Device Connection

Verifying MIDI Controller Connection

Check USB Device in Device Manager

# Open Device Manager
devmgmt.msc

Look for your MIDI controller under:

  • Sound, video and game controllers
  • Universal Serial Bus devices

Example: “Maschine Mikro MK3” should appear

Check MIDI Ports

# List available MIDI ports
conductor --list-ports

# Or use test_midi diagnostic
cargo run --bin test_midi

Test MIDI Events

# Run diagnostic tool (replace 0 with your port number)
cargo run --bin midi_diagnostic 0

# Or if installed:
midi_diagnostic 0

Press pads on your controller. You should see:

[NoteOn] ch:0 note:12 vel:87
[NoteOff] ch:0 note:12 vel:0

If nothing appears:

  • Check USB connection
  • Verify correct port number (try 0, 1, 2, etc.)
  • Install device drivers
  • Check Device Manager for errors

Verifying Game Controller Connection

Check Windows Game Controllers

# Open Game Controllers panel
control joy.cpl

# Or via Settings
start ms-settings:devices-controllersandgamedevices

Your controller should appear in the list. Select it and click “Properties” to test:

  • Button presses
  • Analog stick movements
  • Trigger pulls
  • D-pad inputs

Check via Device Manager

# Open Device Manager
devmgmt.msc

Look for your controller under:

  • Xbox Peripherals (Xbox controllers)
  • Human Interface Devices (Generic gamepads)
  • Bluetooth (Wireless controllers)

Check via Conductor Status

# Start Conductor
Start-Process conductor

# Check status
conductorctl status

# Look for gamepad in device list
# Example output:
# Connected Devices:
#   - Xbox Wireless Controller (Gamepad)

Test Gamepad Events

Use Conductor’s debug logging to verify gamepad inputs:

# Start Conductor with debug logging
$env:DEBUG=1
conductor --foreground

Press buttons on your gamepad. You should see:

[GamepadButton] button:128 (A/Cross/B)
[GamepadButton] button:129 (B/Circle/A)
[GamepadAnalogStick] axis:128 value:255 (Left stick right)

If nothing appears:

  • Check USB or Bluetooth connection
  • Verify controller appears in Game Controllers (joy.cpl)
  • Check battery level (wireless controllers)
  • Try reconnecting the controller
  • Restart Conductor

Platform-Specific Troubleshooting

Xbox Wireless Adapter:

  • Requires Xbox Wireless Adapter for Windows
  • USB adapter available from Microsoft or third-party
  • Automatic driver installation on Windows 10+

Bluetooth Connection:

  1. Open Settings → Devices → Bluetooth & other devices
  2. Click “Add Bluetooth or other device”
  3. Select “Bluetooth”
  4. Put controller in pairing mode:
    • Xbox: Hold pair button until LED flashes
    • PlayStation: Hold Share + PS button
    • Switch Pro: Hold sync button on top
  5. Select controller from list
  6. Wait for pairing to complete

DS4Windows for PlayStation Controllers (Optional):

  1. Download from https://ds4-windows.com/
  2. Install and run DS4Windows
  3. Connect DualShock 4 or DualSense
  4. DS4Windows provides XInput emulation and additional features

Permissions:

  • Windows does not require special permissions for gamepad access
  • If issues persist, run Conductor as Administrator (not recommended long-term)

Configuration

v3.0.0 includes a visual configuration editor:

  1. Open Conductor GUI:

    conductor-gui
    
  2. Connect your device in the device panel (MIDI or gamepad)

  3. Use MIDI Learn mode:

    • Click “Learn” next to any trigger
    • Press a button on your controller or gamepad
    • The trigger config auto-fills
    • Assign an action (keystroke, launch app, etc.)
  4. Save configuration - automatically writes to %USERPROFILE%\.config\conductor\config.toml

See GUI Quick Start for detailed tutorial.

Manual Configuration (Advanced)

If you prefer to edit config.toml manually:

Config location: %USERPROFILE%\.config\conductor\config.toml

Create a minimal config:

# Create config directory
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\conductor"

# Create config file
@"
[device]
name = "Mikro"
auto_connect = true

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Test mapping - Copy"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["ctrl"]

[[modes.mappings]]
description = "Gamepad A button - Paste"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # A/Cross/B button
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["ctrl"]

[[global_mappings]]
description = "Emergency exit (hold pad 0 for 3 seconds)"
[global_mappings.trigger]
type = "LongPress"
note = 0
hold_duration_ms = 3000
[global_mappings.action]
type = "Shell"
command = "taskkill /IM conductor.exe /F"
"@ | Out-File -FilePath "$env:USERPROFILE\.config\conductor\config.toml" -Encoding utf8

This creates a basic configuration with:

  • One mode (Default)
  • One MIDI test mapping (pad 12 = Ctrl+C)
  • One gamepad test mapping (A button = Ctrl+V)
  • One emergency exit (hold pad 0 to quit)

Hot-reload: The daemon automatically reloads config within 0-10ms when you save changes.

Running Conductor

Using the GUI

# Launch the GUI (starts daemon automatically)
conductor-gui

The GUI provides:

  • Real-time event console
  • Visual configuration editor
  • Device status monitoring
  • Daemon control (pause/resume/reload)

Using the Daemon

# Run in foreground
conductor --foreground

# Run in background
Start-Process conductor

# Check status
conductorctl status

# Control daemon
conductorctl reload  # Reload configuration
conductorctl stop    # Stop daemon

Auto-Start on Login

Option 1 - Scheduled Task:

# Create scheduled task (run as Administrator)
$action = New-ScheduledTaskAction -Execute "C:\Program Files\Conductor\conductor.exe"
$trigger = New-ScheduledTaskTrigger -AtLogon
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest
Register-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -TaskName "Conductor Daemon" -Description "Auto-start Conductor daemon on login"

Option 2 - Startup Folder:

# Create shortcut in Startup folder
$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\Conductor.lnk")
$Shortcut.TargetPath = "C:\Program Files\Conductor\conductor.exe"
$Shortcut.Save()

Troubleshooting

Build Errors

Error: link.exe not found

Solution: Install Visual Studio Build Tools:

  1. Download from https://visualstudio.microsoft.com/downloads/
  2. Install “Desktop development with C++” workload
  3. Restart terminal

Error: could not compile 'windows-sys'

Solution: Update Rust toolchain:

rustup update
cargo clean
cargo build --release

Runtime Errors - MIDI

Error: No MIDI input ports available

Solution:

  1. Check USB connection
  2. Open Device Manager and verify device appears
  3. Install device drivers
  4. Restart Windows

Error: Failed to open MIDI device

Solution:

  1. Close other MIDI applications (DAWs, etc.)
  2. Disconnect and reconnect device
  3. Restart Windows
  4. Try different USB port

Runtime Errors - Game Controllers

Error: Gamepad not detected

Solution:

  1. Open Game Controllers (joy.cpl) and verify controller appears
  2. Test controller in Properties dialog
  3. Check battery level (wireless)
  4. Try USB connection instead of Bluetooth
  5. Run Conductor as Administrator (temporary test)
  6. Check debug output: $env:DEBUG=1; conductor --foreground

Error: Gamepad buttons not responding

Solution:

  1. Use MIDI Learn to discover correct button IDs
  2. Verify button IDs are in range 128-255 (not 0-127)
  3. Test controller in joy.cpl
  4. For PlayStation controllers, try DS4Windows
  5. Check that gamepad appears in conductorctl status

Error: Analog stick not working

Solution:

  1. Check axis IDs in joy.cpl Properties
  2. Verify axis IDs match config (128-133)
  3. Check dead zone settings
  4. Calibrate controller in joy.cpl
  5. Use button triggers instead of analog for precise control

Bluetooth Gamepad Issues

Controller not pairing:

  1. Open Settings → Bluetooth & other devices
  2. Remove old pairings
  3. Put controller in pairing mode
  4. Pair as new device
  5. Test in joy.cpl

Controller disconnects randomly:

  1. Check battery level
  2. Update Bluetooth drivers
  3. Move USB Bluetooth adapter away from other USB 3.0 devices
  4. Use USB cable instead

Controller lag or latency:

  1. Use USB cable for lowest latency
  2. Use Xbox Wireless Adapter instead of Bluetooth
  3. Update controller firmware
  4. Close background applications

Windows Firewall

If running Conductor across network (advanced):

# Allow Conductor through firewall
New-NetFirewallRule -DisplayName "Conductor" -Direction Inbound -Program "C:\Program Files\Conductor\conductor.exe" -Action Allow

Next Steps

Now that Conductor v3.0.0 is installed and running:

For GUI Users

  1. Learn the GUI: Read GUI Quick Start Guide
  2. MIDI Learn Tutorial: See MIDI Learn Mode
  3. Device Templates: Check Using Device Templates
  4. Per-App Profiles: Set up Application-Specific Profiles
  5. Gamepad Setup: Read Gamepad Support Guide (v3.0+)

For CLI Users

  1. Daemon Control: Read Daemon & Hot-Reload Guide
  2. CLI Reference: See conductorctl Commands
  3. Manual Configuration: Check Configuration Overview
  4. Advanced Actions: Explore Actions Reference

For All Users

Getting Help

If you encounter issues:

  1. Check Common Issues
  2. Use Diagnostic Tools
  3. Enable debug logging: $env:DEBUG=1; conductor --foreground
  4. Check Event Viewer for errors
  5. File an issue on GitHub with:
    • Windows version
    • Device model (MIDI or gamepad)
    • Error messages
    • Output of cargo --version and rustc --version

Last Updated: November 21, 2025 (v3.0.0) Windows Support: Windows 10 (1903+), Windows 11, Server 2019+ Architecture: x86_64 Input Support: MIDI Controllers + Game Controllers (HID)

Building Conductor from Source

Overview

This guide covers building Conductor from source code on any supported platform. Whether you’re developing new features, customizing behavior, or just want to run the latest code, this guide walks through the complete build process.

Prerequisites

Rust Toolchain

Conductor requires Rust 1.70.0 or later.

Installing Rust

The recommended way to install Rust is via rustup, the official Rust toolchain installer:

# macOS/Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows (PowerShell)
# Download and run: https://win.rustup.rs/

Follow the installation prompts. The installer will:

  1. Install rustc (Rust compiler)
  2. Install cargo (Rust package manager and build tool)
  3. Configure your shell environment

After installation, reload your shell:

# macOS/Linux
source $HOME/.cargo/env

# Windows: Close and reopen terminal

Verify installation:

rustc --version  # Should show: rustc 1.75.0 (or later)
cargo --version  # Should show: cargo 1.75.0 (or later)

Updating Rust

Keep your toolchain up to date:

rustup update

Workspace Architecture (v0.2.0+)

Conductor uses a Cargo workspace with three packages since v0.2.0:

Package Structure

  1. conductor-core - Pure Rust engine library

    • Zero UI dependencies (no colored output, pure logic)
    • Public API for embedding in other applications
    • 30+ public types exported
    • Event processing, mapping engine, action execution
  2. conductor-daemon - CLI daemon + diagnostic tools

    • Main conductor binary (daemon service)
    • conductorctl - CLI control tool (v1.0.0+)
    • 6 diagnostic binaries:
      • midi_diagnostic - MIDI event viewer
      • led_diagnostic - LED testing tool
      • led_tester - Interactive LED control
      • pad_mapper - Note number mapper
      • test_midi - Port connectivity test
      • midi_simulator - MIDI event simulator (testing)
  3. conductor (root) - Backward compatibility layer

    • Re-exports conductor-core types
    • For existing v0.1.0 tests only
    • New code should use conductor-core directly

When to Use Each Package

  • Use conductor-core: Embed Conductor in your application
  • Use conductor-daemon: Run as standalone CLI/daemon
  • Use conductor (root): Only for backward compatibility

Public API Example

use conductor_core::{Config, MappingEngine, EventProcessor, ActionExecutor};

let config = Config::load("config.toml")?;
let mut engine = MappingEngine::new();
// Process events, execute actions...

Platform-Specific Dependencies

macOS

Required:

  • Xcode Command Line Tools: Provides compilers and linkers
    xcode-select --install
    

Optional (for Maschine Mikro MK3):

  • Native Instruments Drivers: Required for HID/LED support

Verify:

# Check compiler
clang --version

# Check USB device (if connected)
system_profiler SPUSBDataType | grep -i mikro

Linux (Ubuntu/Debian)

Required:

sudo apt update
sudo apt install -y \
    build-essential \
    pkg-config \
    libasound2-dev \
    libudev-dev \
    libusb-1.0-0-dev

For Fedora/RHEL:

sudo dnf install -y \
    gcc \
    pkg-config \
    alsa-lib-devel \
    systemd-devel \
    libusbx-devel

For Arch Linux:

sudo pacman -S base-devel alsa-lib systemd-libs libusb

udev rules (for HID access without sudo):

Create /etc/udev/rules.d/50-conductor.rules:

sudo tee /etc/udev/rules.d/50-conductor.rules << 'EOF'
# Native Instruments Maschine Mikro MK3
SUBSYSTEM=="usb", ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1600", MODE="0666", GROUP="plugdev"

# Generic MIDI devices
SUBSYSTEM=="usb", ATTRS{bInterfaceClass}=="01", ATTRS{bInterfaceSubClass}=="03", MODE="0666", GROUP="plugdev"
EOF

sudo udevadm control --reload-rules
sudo udevadm trigger

Add your user to the plugdev group:

sudo usermod -a -G plugdev $USER
# Log out and back in for changes to take effect

Windows

Required:

  • Microsoft C++ Build Tools: For compiling native dependencies

    OR

    • Full Visual Studio (Community edition is free)

Optional:

  • Native Instruments Drivers: For Maschine Mikro MK3 support

Verify:

# Check MSVC compiler
where cl

# Check Rust
rustc --version
cargo --version

Cloning the Repository

Via Git

# HTTPS (recommended for most users)
git clone https://github.com/yourusername/conductor.git
cd conductor

# SSH (if you have SSH keys configured)
git clone git@github.com:yourusername/conductor.git
cd conductor

Verify Repository Contents

ls -la

# Expected files/directories:
# - Cargo.toml     (Rust project manifest)
# - Cargo.lock     (Dependency lock file)
# - src/           (Source code)
# - config.toml    (Example configuration)
# - README.md      (Project readme)
# - docs/          (Documentation)

Build Commands

Workspace Builds (v0.2.0+)

Build the entire workspace (all 3 packages in parallel):

# Development build
cargo build --workspace

# Release build (optimized)
cargo build --release --workspace

Build times (workspace):

  • Clean build: ~12s (was 15-20s in v0.1.0)
  • Incremental: <2s
  • Parallel compilation across all packages

Package-Specific Builds

Build individual packages for faster iteration:

# Core engine only
cargo build --package conductor-core
cargo build -p conductor-core  # Short form

# Daemon + tools
cargo build -p conductor-daemon

# Compatibility layer
cargo build -p conductor

# Specific binary
cargo build --release --bin conductor
cargo build --release --bin conductorctl
cargo build --release --bin midi_diagnostic

Running Binaries

# Main daemon
cargo run --release --bin conductor 2

# Daemon control
cargo run --release --bin conductorctl status

# Diagnostic tool
cargo run --release --bin midi_diagnostic 2

Debug Build

Fastest compilation, includes debug symbols, no optimization:

cargo build

Output: target/debug/conductor (~10-20MB binary)

When to use:

  • Development and testing
  • Debugging with lldb/gdb
  • Frequent recompilation

Performance: ~20-30% slower than release builds

Release Build

Optimized compilation, stripped debug symbols, smaller binary:

cargo build --release

Output: target/release/conductor (~3-5MB binary)

When to use:

  • Production use
  • Performance-critical applications
  • Distribution

Build time: 12s clean, <2s incremental (workspace)

Performance: Full optimization, <1ms MIDI latency

Clean Build

Remove all build artifacts and start fresh:

# Clean build artifacts
cargo clean

# Then rebuild
cargo build --release

When to use:

  • After updating dependencies
  • Troubleshooting build issues
  • Freeing disk space (build artifacts can be 1-2GB)

Check Only (No Binary)

Verify code compiles without producing a binary:

cargo check

Fastest way to verify code correctness during development. Use this for quick iteration.

Run Directly

Build and run in one command:

# Debug mode
cargo run

# Release mode (recommended)
cargo run --release

# With arguments
cargo run --release -- 2 --led reactive

Note the -- separator between cargo arguments and program arguments.

Build Optimization

Release Profile Configuration

Conductor’s Cargo.toml includes optimized release settings:

[profile.release]
opt-level = 3        # Maximum optimization
lto = true           # Link-Time Optimization (smaller binary, longer build)
codegen-units = 1    # Single codegen unit (better optimization)
strip = true         # Strip debug symbols (smaller binary)
panic = 'abort'      # Abort on panic (smaller binary, no unwinding)

Results:

  • Binary size: ~3-5MB (vs ~15-20MB without optimization)
  • Startup time: <100ms
  • MIDI latency: <1ms
  • Memory usage: 5-10MB

Custom Optimization Levels

Override optimization for specific dependencies:

# In Cargo.toml
[profile.dev.package.midir]
opt-level = 3  # Optimize MIDI library even in debug builds

Faster Debug Builds

Trade optimization for compile speed during development:

# In Cargo.toml
[profile.dev]
opt-level = 1        # Basic optimization
debug = true         # Keep debug symbols
incremental = true   # Enable incremental compilation

Parallel Compilation

Cargo uses all CPU cores by default. To limit:

# Use 4 cores
cargo build -j 4

# Use 1 core (debugging build issues)
cargo build -j 1

Cross-Compilation

macOS: Universal Binary (Intel + Apple Silicon)

Build a single binary that runs on both architectures:

# Install targets
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin

# Build for both
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin

# Create universal binary
lipo -create \
    target/x86_64-apple-darwin/release/conductor \
    target/aarch64-apple-darwin/release/conductor \
    -output target/release/conductor-universal

# Verify
file target/release/conductor-universal
# Output: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]

Linux: Cross-Compile for Different Architectures

# Install target
rustup target add aarch64-unknown-linux-gnu

# Install cross-compiler (Ubuntu/Debian)
sudo apt install gcc-aarch64-linux-gnu

# Build
cargo build --release --target aarch64-unknown-linux-gnu

Windows: Cross-Compile from Linux

Using the cross tool:

# Install cross
cargo install cross

# Build for Windows
cross build --release --target x86_64-pc-windows-gnu

Dependency Management

Updating Dependencies

# Update all dependencies to latest compatible versions
cargo update

# Check for outdated dependencies
cargo outdated

# Update specific dependency
cargo update -p midir

Dependency Audit

Check for security vulnerabilities:

# Install cargo-audit
cargo install cargo-audit

# Run audit
cargo audit

Vendoring Dependencies (Offline Builds)

For building without internet access:

# Download all dependencies
cargo vendor

# Configure to use vendored deps
mkdir -p .cargo
cat > .cargo/config.toml << 'EOF'
[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "vendor"
EOF

# Now cargo build works offline
cargo build --release

Binary Size Optimization

Size Comparison

# Default release build
cargo build --release
ls -lh target/release/conductor
# ~3-5MB

# Further optimization
cargo build --release --features "optimize-size"
ls -lh target/release/conductor
# ~2-3MB

Extreme Size Reduction

Add to Cargo.toml:

[profile.release-min]
inherits = "release"
opt-level = "z"      # Optimize for size
lto = true
codegen-units = 1
strip = true
panic = 'abort'

Build with:

cargo build --profile release-min

Result: ~1-2MB binary (may be slightly slower)

Analyze Binary Size

# Install cargo-bloat
cargo install cargo-bloat

# Analyze
cargo bloat --release --crates
cargo bloat --release -n 20  # Show top 20 functions by size

Development Builds

Watch Mode (Auto-Rebuild on Changes)

# Install cargo-watch
cargo install cargo-watch

# Auto-rebuild on save
cargo watch -x check
cargo watch -x 'run -- 2'
cargo watch -x 'test'

Build with Specific Features

# Build with all features
cargo build --all-features

# Build with specific feature
cargo build --features "midi-learn"

# Build without default features
cargo build --no-default-features

Build Examples and Binaries

Conductor includes diagnostic tools:

# Build all binaries
cargo build --release --bins

# Build specific binary
cargo build --release --bin midi_diagnostic

# List available binaries
ls target/release/midi_*
# midi_diagnostic
# led_diagnostic
# led_tester
# pad_mapper
# test_midi

Testing Builds

Run Tests

# Run all tests
cargo test

# Run tests with output
cargo test -- --nocapture

# Run specific test
cargo test test_event_processor

# Run tests with release optimizations
cargo test --release

Run Benchmarks

# Install cargo-criterion
cargo install cargo-criterion

# Run benchmarks
cargo bench

Integration Tests

# Run only integration tests
cargo test --test '*'

# Run with real MIDI device (requires hardware)
cargo test --test integration_midi -- --ignored

Troubleshooting Build Issues

Common Errors

Error: “linker ‘cc’ not found”

Cause: Missing C compiler

Solution:

  • macOS: xcode-select --install
  • Linux: sudo apt install build-essential
  • Windows: Install Visual Studio Build Tools

Error: “could not compile ‘hidapi’”

Cause: Missing USB/HID development libraries

Solution:

  • macOS: Install Xcode Command Line Tools
  • Linux: sudo apt install libudev-dev libusb-1.0-0-dev
  • Windows: Install Windows SDK

Error: “failed to fetch dependencies”

Cause: Network issues or outdated index

Solution:

# Update cargo index
cargo update

# Or manually remove and re-fetch
rm -rf ~/.cargo/registry/index
cargo build

Error: “out of disk space”

Cause: Build artifacts consuming too much space

Solution:

# Clean all build artifacts
cargo clean

# Clean entire cargo cache (frees 1-5GB)
rm -rf ~/.cargo/registry/cache
rm -rf ~/.cargo/registry/src

Incremental Build Issues

If incremental builds produce errors:

# Disable incremental compilation
CARGO_INCREMENTAL=0 cargo build --release

# Or add to ~/.cargo/config.toml:
[build]
incremental = false

Dependency Conflicts

If cargo reports conflicting dependencies:

# Show dependency tree
cargo tree

# Show why a dependency is included
cargo tree -i <dependency-name>

# Update all dependencies
cargo update

Build Performance Tips

Parallel Compilation

Use all CPU cores:

# Set in ~/.cargo/config.toml
[build]
jobs = 8  # Or number of cores

Use Faster Linker

macOS

# Install zld (faster linker)
brew install michaeleisel/zld/zld

# Configure in .cargo/config.toml
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld"]

[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/opt/homebrew/bin/zld"]

Linux

# Install mold (very fast linker)
sudo apt install mold  # Ubuntu 22.04+
# or download from: https://github.com/rui314/mold

# Configure in .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

Result: 2-3x faster linking (especially for incremental builds)

Use sccache (Shared Compilation Cache)

# Install sccache
cargo install sccache

# Configure
export RUSTC_WRAPPER=sccache

# Check stats
sccache --show-stats

Speeds up rebuilds across multiple projects.

Distribution

Creating Release Artifacts

# Build optimized binary
cargo build --release

# Copy to distribution directory
mkdir -p dist
cp target/release/conductor dist/
cp config.toml dist/config.example.toml
cp README.md dist/

# Create tarball
cd dist
tar czf conductor-v0.1.0-macos-aarch64.tar.gz *

Code Signing (macOS)

For distribution outside the App Store:

# Sign the binary
codesign --force --deep --sign "Developer ID Application: Your Name" \
    target/release/conductor

# Verify signature
codesign -dv --verbose=4 target/release/conductor

# Notarize (requires paid Apple Developer account)
xcrun notarytool submit conductor.zip \
    --keychain-profile "AC_PASSWORD" \
    --wait

Next Steps

After building successfully:

  1. Run the binary: ./target/release/conductor
  2. Configure mappings: Edit config.toml
  3. Read documentation:

See Also


Last Updated: November 11, 2025 Rust Version: 1.70.0+ required, 1.75.0+ recommended

Quick Start Guide

Get Conductor v3.0+ up and running in under 5 minutes using the visual GUI.

Prerequisites

  • macOS 11.0+ (Big Sur or later) - Intel or Apple Silicon
  • Input device:
    • MIDI controller (any USB MIDI device - Maschine Mikro MK3 recommended for full RGB LED support), OR
    • Game controller (Xbox, PlayStation, Switch Pro, joystick, racing wheel, HOTAS, or any SDL2-compatible HID device)
  • 10 minutes for installation (3-5 minutes for gamepad setup with templates)

Step 1: Download Conductor

Visit the GitHub Releases page and download:

For most users (recommended):

  • conductor-gui-macos-universal.tar.gz - GUI application (includes daemon)

For CLI-only users:

  • conductor-aarch64-apple-darwin.tar.gz (Apple Silicon)
  • conductor-x86_64-apple-darwin.tar.gz (Intel)

Step 2: Install the GUI

# Extract the downloaded file
tar xzf conductor-gui-macos-universal.tar.gz

# Move to Applications folder
mv "Conductor GUI.app" /Applications/

# Open the app
open /Applications/"Conductor GUI.app"

First launch: macOS will ask for permissions. Grant Input Monitoring permission when prompted (required for LED control and device access).

Step 3: Connect Your Device

  1. Plug in your MIDI controller via USB

  2. In the Conductor GUI, go to the Device Connection panel

  3. Your device should appear in the list. Click Connect

  4. The status bar at the bottom should show: “Connected to [Your Device]”

If your device doesn’t appear:

Step 4: Create Your First Mapping with MIDI Learn

The fastest way to create a mapping is using MIDI Learn mode:

  1. Click “Add Mapping” in the Mappings panel

  2. Click “Learn” next to the Trigger field

  3. Press any pad/button on your MIDI controller

  4. The trigger configuration auto-fills with the detected input

  5. Choose an action:

    • Keystroke - Press a key (e.g., Cmd+C for copy)
    • Launch - Open an application
    • Text - Type text
    • Shell - Run a command
  6. Click “Save”

  7. Test it! Press the same pad/button - the action should execute

Example: Map pad to open Spotify:

  • Trigger: Note 36 (auto-detected via MIDI Learn)
  • Action: Launch → /Applications/Spotify.app

Step 5: Explore Advanced Features

Velocity Sensitivity

Map different actions based on how hard you hit a pad:

  1. Select trigger type: Velocity Range
  2. Use MIDI Learn to detect the note
  3. Set velocity ranges:
    • Soft (0-40): Pause playback
    • Hard (81-127): Skip track

Long Press Detection

Hold a pad for 2+ seconds to trigger a different action:

  1. Select trigger type: Long Press
  2. Use MIDI Learn
  3. Set Hold Duration: 2000ms
  4. Action: Open Calculator

Chord Detection

Press multiple pads simultaneously:

  1. Select trigger type: Chord
  2. Use MIDI Learn and press all pads within 100ms
  3. Action: Launch your favorite app

Step 6: Use Device Templates

Skip manual configuration with built-in device templates:

  1. Go to SettingsDevice Templates

  2. Select your controller:

    • Maschine Mikro MK3
    • Launchpad Mini
    • APC Mini
    • Korg nanoKONTROL2
    • Novation Launchkey Mini
    • AKAI MPK Mini
  3. Click Load Template

  4. The template loads pre-configured mappings for common workflows

  5. Customize as needed using MIDI Learn

Quick Start with Game Controllers (v3.0+)

Conductor v3.0+ supports game controllers (gamepads, joysticks, racing wheels, HOTAS, etc.) as macro input devices alongside MIDI controllers. You can use them individually or together!

Supported Device Types

  • Gamepads: Xbox, PlayStation, Nintendo Switch Pro (templates available)
  • Joysticks: Flight sticks, arcade sticks (manual config)
  • Racing Wheels: Logitech, Thrustmaster (manual config)
  • HOTAS: Throttle and stick controllers (manual config)
  • Custom Controllers: Any SDL2-compatible HID device

Option 1: Gamepad with Template (Fastest - ~3 minutes)

The quickest way to set up an Xbox, PlayStation, or Switch Pro controller:

  1. Connect your gamepad via USB or Bluetooth

  2. Verify it’s recognized by your system:

    • macOS: System Settings → Game Controllers
    • Linux: ls /dev/input/js*
    • Windows: Devices and Printers
  3. Open Conductor GUIDevice Templates

  4. Filter by “🎮 Game Controllers”

  5. Select your controller:

    • Xbox 360/One/Series X|S
    • PlayStation DualShock 4/DualSense (PS5)
    • Nintendo Switch Pro Controller
  6. Click “Create Config”Reload daemon

  7. Test buttons! Press any button - the mapped action should execute

Time to completion: ~3 minutes from connection to working mappings

Option 2: Manual Setup (Joysticks, Wheels, Other - ~10 minutes)

For non-gamepad controllers (flight sticks, racing wheels, HOTAS):

  1. Connect your HID device via USB

  2. Verify system recognition (same commands as above)

  3. Open Conductor GUIMIDI Learn

  4. Create mappings using MIDI Learn:

    • Click “Add Mapping”
    • Click “Learn” next to Trigger
    • Press a button or move an axis on your controller
    • Conductor auto-detects the input
    • Choose an action (Keystroke, Launch, Shell, etc.)
    • Click “Save”
  5. Repeat for all buttons/axes you want to map

  6. Test your mappings!

Time to completion: ~10 minutes for 10-15 button mappings

Example: Flight Stick Setup

Trigger button → Launch flight simulator:

[[modes.mappings]]
description = "Flight stick trigger: Launch Flight Sim"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Auto-detected via MIDI Learn
[modes.mappings.action]
type = "Launch"
path = "/Applications/Flight Simulator.app"

Hat switch → Arrow keys:

[[modes.mappings]]
description = "Hat up: View up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132  # D-Pad/Hat up
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

Example: Racing Wheel Setup

Wheel rotation → Steering:

[[modes.mappings]]
description = "Wheel right: Steer right"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 128  # Wheel axis (auto-detected)
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"

Pedals → Gas/Brake:

[[modes.mappings]]
description = "Gas pedal: Accelerate"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Gas pedal axis
threshold = 64  # 25% pedal press
[modes.mappings.action]
type = "Keystroke"
keys = "w"

Platform-Specific Notes

macOS:

  • Most gamepads work via USB or Bluetooth without drivers
  • Xbox Wireless Adapter may require driver installation
  • Grant Input Monitoring permission when prompted

Linux:

  • Ensure jstest recognizes your controller: jstest /dev/input/js0
  • May need udev rules for device permissions
  • Install xdotool for keystroke simulation

Windows:

  • Xbox controllers work natively
  • PlayStation controllers may need DS4Windows or similar
  • Check Device Manager for driver status

Hybrid MIDI + Gamepad Setup

You can use MIDI controllers and gamepads simultaneously:

  • MIDI devices: Button IDs 0-127
  • Gamepad devices: Button IDs 128-255
  • No conflicts - they work together seamlessly!

Example: Use Maschine pads for music production, gamepad for application shortcuts.

Troubleshooting Game Controllers

Controller not detected:

  1. Check USB/Bluetooth connection
  2. Verify system recognition (see commands above)
  3. Try USB instead of Bluetooth (or vice versa)
  4. Restart Conductor daemon: conductorctl stop && conductorctl reload

Buttons not working:

  1. Use MIDI Learn to verify button IDs (should be 128-255)
  2. Check Event Console for incoming events
  3. Ensure no conflicting mappings exist

Analog stick too sensitive:

  • Conductor uses a 10% automatic dead zone
  • For more precision, use discrete buttons (D-Pad) instead of analog sticks

For more details, see the Gamepad Support Guide.

Step 7: Enable Auto-Start (Optional)

Run Conductor automatically when you log in:

  1. Go to SettingsGeneral

  2. Enable “Start Conductor on login”

  3. Click Save

The daemon will now start automatically in the background every time you log in.

Using the Daemon CLI (Optional)

For advanced users who prefer terminal control:

# Check daemon status
conductorctl status

# Reload configuration (hot-reload in 0-10ms)
conductorctl reload

# Stop daemon
conductorctl stop

# Validate config without reloading
conductorctl validate

# Ping daemon (check latency)
conductorctl ping

See Daemon & CLI Guide for full details.

Per-App Profiles (Automatic Profile Switching)

Conductor v2.0.0 can automatically switch configurations based on which application is active:

  1. Go to Per-App Profiles in the GUI

  2. Add a new profile:

    • Application: Select an app (e.g., “Visual Studio Code”)
    • Profile: Select a config profile
  3. When you switch to that application, Conductor automatically loads the configured profile

  4. Example use cases:

    • Logic Pro: Pads control DAW functions (play, record, etc.)
    • VS Code: Pads trigger common shortcuts (run, debug, search)
    • Chrome: Pads control tabs and navigation

Live Event Console

Debug your mappings in real-time:

  1. Go to Event Console in the GUI

  2. Watch MIDI events as they happen:

    • Note On/Off
    • Velocity values
    • Control Change
    • Pitch Bend
    • Aftertouch
  3. Filter events by type or note number

  4. Export logs for debugging

This is invaluable for troubleshooting “why isn’t my mapping working?”

Troubleshooting

Device Not Found

  1. Check USB connection:

    system_profiler SPUSBDataType | grep -i midi
    
  2. Restart the daemon:

    conductorctl stop
    open /Applications/"Conductor GUI.app"
    
  3. Check Audio MIDI Setup:

    open -a "Audio MIDI Setup"
    

LEDs Not Working

  1. Ensure Native Instruments drivers are installed (for Maschine controllers)

  2. Grant Input Monitoring permission:

    • System Settings → Privacy & Security → Input Monitoring
    • Enable for “Conductor GUI”
  3. Check LED scheme in GUI Settings

  4. View debug logs in Event Console

Mappings Not Triggering

  1. Use Event Console to verify MIDI events are being received

  2. Use MIDI Learn to verify the correct note numbers

  3. Check mode - is the mapping in the current mode or global?

  4. Reload config:

    conductorctl reload
    

Permission Denied (macOS)

If you see “Permission denied” errors:

  1. System SettingsPrivacy & SecurityInput Monitoring
  2. Add “Conductor GUI” to the list
  3. Restart the GUI app

🎉 Congratulations! You’re Now a Conductor Power User

You’ve just unlocked:

  • ✅ Visual configuration with Input Learn
  • ✅ Hot-reload (0-10ms config changes)
  • ✅ Per-app profiles
  • ✅ v3.0 gamepad + MIDI support

What’s Next?

🚀 Level Up Your Setup

💡 Get Inspired

📖 Go Deeper

🤝 Join the Community

Need help? Check the FAQ or Troubleshooting Guide.

Loving Conductor? ⭐ Star us on GitHub to support the project!

Your First Mapping

Learn how to create custom mappings by building a practical example step-by-step.

What You’ll Build

A simple pad mapping that opens Visual Studio Code when you press pad 1 (Note 60).

Understanding Mapping Structure

Every mapping has two parts:

[[modes.mappings]]
trigger = { ... }  # What activates the mapping
action = { ... }   # What happens when activated

Think of it as: “When [trigger], do [action]”

Step 1: Open Your Config File

The config file is config.toml in your project root:

cd /path/to/conductor
open config.toml  # macOS
# or
nano config.toml  # Terminal editor

Step 2: Find the Modes Section

Scroll down to find the [[modes]] section. It looks like this:

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
# Existing mappings here...

Step 3: Add Your Mapping

Add this at the end of the [[modes.mappings]] section:

[[modes.mappings]]
trigger = { Note = { note = 60, velocity_min = 0 } }
action = { Launch = { app_path = "/Applications/Visual Studio Code.app" } }

What this means:

  • trigger: When pad Note 60 is pressed (any velocity ≥0)
  • action: Launch Visual Studio Code

Step 4: Save and Reload

  1. Save the file (Cmd+S or Ctrl+O in nano)
  2. Stop Conductor if it’s running (Ctrl+C)
  3. Restart it:
cargo run --release 2

Step 5: Test It

Press the first pad (bottom-left on Mikro MK3). Visual Studio Code should launch!

Understanding Note Numbers

How do you know which pad is Note 60?

Quick Reference (Maschine Mikro MK3)

Pad Layout (4x4 grid):
┌────┬────┬────┬────┐
│ 60 │ 61 │ 62 │ 63 │  Top row
├────┼────┼────┼────┤
│ 64 │ 65 │ 66 │ 67 │
├────┼────┼────┼────┤
│ 68 │ 69 │ 70 │ 71 │
└────┴────┴────┴────┘
   72   73   74   75    Bottom row

Find Note Numbers for Any Device

Use the pad mapper tool:

cargo run --bin pad_mapper

Then press each pad to see its note number.

Common Mapping Patterns

Pattern 1: Keyboard Shortcut

[[modes.mappings]]
trigger = { Note = { note = 61, velocity_min = 0 } }
action = { Keystroke = { keys = ["Cmd", "T"] } }  # New tab

Pattern 2: Type Text

[[modes.mappings]]
trigger = { Note = { note = 62, velocity_min = 0 } }
action = { Text = { text = "user@example.com" } }

Pattern 3: Run Shell Command

[[modes.mappings]]
trigger = { Note = { note = 63, velocity_min = 0 } }
action = { Shell = { command = "open -a Calculator" } }

Pattern 4: Volume Control

[[modes.mappings]]
trigger = { Note = { note = 64, velocity_min = 0 } }
action = { VolumeControl = "VolumeUp" }

Next Steps

Input Learn Mode

Input Learn is the fastest way to create mappings in Conductor v3.0. Instead of manually entering note numbers, button IDs, or MIDI parameters, simply press a control on your device and let Conductor auto-detect everything.

What is Input Learn?

Input Learn (formerly “MIDI Learn”) is a one-click workflow that works with both MIDI controllers and Game Controllers (HID):

  1. Click “Learn” next to a trigger field
  2. Press a control on your device (pad, button, stick, encoder, etc.)
  3. Conductor auto-fills the trigger configuration
  4. Assign an action and save

That’s it! No need to know note numbers, button IDs, CC values, or MIDI channels.

Supported Device Types

Input Learn works with:

MIDI Controllers

  • Pad controllers: Maschine, Launchpad, etc.
  • Keyboards: MIDI keyboards with velocity sensitivity
  • Encoders/knobs: Rotary encoders, faders
  • DJ controllers: Mixers, CDJs

Game Controllers (HID)

  • Gamepads: Xbox, PlayStation, Nintendo Switch Pro
  • Joysticks: Flight sticks, arcade sticks
  • Racing wheels: Logitech, Thrustmaster, Fanatec
  • Flight controls: HOTAS systems
  • Custom controllers: Any SDL2-compatible HID device

How to Use Input Learn

Basic Workflow

  1. Open Conductor GUI and ensure your device is connected

  2. Navigate to Mappings panel

  3. Click “Add Mapping” or edit an existing one

  4. Click the “Learn” button next to the Trigger field

  5. Input Learn window opens with a 10-second countdown:

    Input Learn Mode
    
    Waiting for input...
    Press any control on your device
    
    Time remaining: 8 seconds
    [Cancel]
    
  6. Press any control on your MIDI or gamepad device:

    • MIDI: Pad, button, encoder/knob, fader, touch strip
    • Gamepad: Button, analog stick, trigger, D-pad
  7. Trigger auto-fills (examples):

    MIDI Pad:

    Trigger Type: Note
    Note: 36
    Channel: 0
    

    Gamepad Button:

    Trigger Type: GamepadButton
    Button: 128
    

    Analog Stick:

    Trigger Type: GamepadAnalogStick
    Axis: 130
    Direction: Clockwise
    
  8. Assign an action (Keystroke, Launch, Text, etc.)

  9. Click “Save”

  10. Test it! Press the same control - the action should execute

Supported Trigger Types

Input Learn auto-detects and configures these trigger types:

MIDI Triggers

1. Note (Basic Pad/Button Press)

What it detects:

  • Note number
  • MIDI channel
  • Velocity range (if applicable)

Example:

  • Press pad → Auto-fills: Note: 36, Channel: 0

Use cases:

  • Basic pad mappings
  • Button presses
  • Keyboard keys

2. Velocity Range (Pressure-Sensitive)

What it detects:

  • Note number
  • Soft/medium/hard press patterns
  • Velocity thresholds

Example:

  • Press pad softly → VelocityRange: Note 36, Min: 0, Max: 40
  • Press pad hard → VelocityRange: Note 36, Min: 81, Max: 127

Use cases:

  • Different actions for soft vs hard hits
  • Velocity-sensitive controls

3. Long Press

What it detects:

  • Note number
  • Hold duration (auto-calculated from your press)

Example:

  • Hold pad for 2 seconds → LongPress: Note 36, Duration: 2000ms

Use cases:

  • Hold pad to open app
  • Long press for alternate action

4. Double-Tap

What it detects:

  • Note number
  • Double-tap timing window

Example:

  • Tap pad twice quickly → DoubleTap: Note 36, Window: 300ms

Use cases:

  • Quick double-tap for special actions
  • Distinguishing single vs double taps

5. Chord (Multiple Notes)

What it detects:

  • All pressed notes
  • Chord window (how fast notes must be pressed together)

Example:

  • Press pads 36, 40, 43 together → Chord: [36, 40, 43], Window: 100ms

Use cases:

  • Shortcuts requiring multiple pads
  • Musical chord detection

6. Encoder/Knob Rotation

What it detects:

  • CC (Control Change) number
  • Direction (Clockwise/Counterclockwise)
  • Value range

Example:

  • Turn encoder right → EncoderTurn: CC 1, Direction: Clockwise
  • Turn encoder left → EncoderTurn: CC 1, Direction: Counterclockwise

Use cases:

  • Volume control
  • Scrolling
  • Parameter adjustment

7. Control Change (CC)

What it detects:

  • CC number
  • Value range
  • Continuous vs momentary

Example:

  • Move fader → CC: 7, Range: 0-127
  • Press button → CC: 64, Value: 127

Use cases:

  • Faders
  • Knobs with CC messages
  • Sustain pedals

8. Aftertouch (Pressure)

What it detects:

  • Note number (if channel aftertouch)
  • Pressure threshold

Example:

  • Press pad harder after initial hit → Aftertouch: Note 36, Threshold: 64

Use cases:

  • Pressure-sensitive effects
  • Dynamic parameter control

9. Pitch Bend

What it detects:

  • Pitch bend range
  • Direction (Up/Down/Center)

Example:

  • Move pitch wheel up → PitchBend: Direction: Up, Threshold: 8192

Use cases:

  • Pitch wheel mappings
  • Touch strip controls

Game Controller (HID) Triggers

10. GamepadButton

What it detects:

  • Button ID (128-255 range)
  • Device type (gamepad, joystick, wheel, etc.)

Example:

  • Press A button (Xbox) → GamepadButton: 128
  • Press Cross button (PlayStation) → GamepadButton: 128
  • Press B button (Switch) → GamepadButton: 128

Use cases:

  • Face button mappings (A, B, X, Y)
  • Shoulder buttons (LB, RB, L1, R1)
  • D-pad buttons
  • Menu buttons (Start, Select, Home)

Button ID Reference:

  • Face buttons: 128-131 (A/B/X/Y)
  • D-pad: 132-135 (Up/Down/Left/Right)
  • Shoulder buttons: 136-137 (LB/RB, L1/R1)
  • Stick clicks: 138-139 (L3/R3)
  • Menu buttons: 140-142 (Start/Select/Home)
  • Digital triggers: 143-144 (LT/RT, L2/R2, ZL/ZR)

11. GamepadButtonChord

What it detects:

  • Multiple button IDs pressed simultaneously
  • Chord timing window

Example:

  • Press A + B together → GamepadButtonChord: [128, 129], Window: 50ms
  • Press LB + RB together → GamepadButtonChord: [136, 137], Window: 50ms

Use cases:

  • Multi-button shortcuts
  • Mode switching combos
  • Emergency actions

12. GamepadAnalogStick

What it detects:

  • Axis ID (128-131 for stick axes)
  • Direction (Clockwise/CounterClockwise)
  • Dead zone (automatic 10%)

Example:

  • Move right stick right → GamepadAnalogStick: Axis 130, Direction: Clockwise
  • Move left stick up → GamepadAnalogStick: Axis 129, Direction: Clockwise
  • Move right stick left → GamepadAnalogStick: Axis 130, Direction: CounterClockwise

Use cases:

  • Navigation controls
  • Scrolling
  • Cursor movement

Stick Axes:

  • 128: Left stick X-axis (left/right)
  • 129: Left stick Y-axis (up/down)
  • 130: Right stick X-axis (left/right)
  • 131: Right stick Y-axis (up/down)

13. GamepadTrigger

What it detects:

  • Trigger ID (132-133 for analog triggers)
  • Threshold value (0-255 range)
  • Pull depth detection

Example:

  • Pull right trigger halfway → GamepadTrigger: 133, Threshold: 128
  • Pull left trigger fully → GamepadTrigger: 132, Threshold: 200

Use cases:

  • Pressure-sensitive actions
  • Volume control
  • Acceleration/braking (racing wheels)
  • Throttle control (flight sticks)

Trigger IDs:

  • 132: Left trigger (L2, LT, ZL)
  • 133: Right trigger (R2, RT, ZR)

Advanced Input Learn Features

Countdown Timer

The Input Learn window shows a 10-second countdown. This gives you time to:

  • Position your hand
  • Find the right control
  • Try different velocities/pressures
  • Test analog stick directions

Countdown reaches zero → Input Learn cancels automatically

Cancellation

Click “Cancel” at any time to abort Input Learn without creating a mapping.

Keyboard shortcut: Press Esc to cancel

Pattern Detection

Input Learn is smart - it detects patterns automatically:

MIDI Patterns

Long Press Detection:

  • If you hold a pad for >1 second during Learn mode, Conductor suggests a Long Press trigger

Double-Tap Detection:

  • If you tap a pad twice quickly, Conductor suggests a Double-Tap trigger

Chord Detection:

  • If you press multiple pads within 100ms, Conductor suggests a Chord trigger

Velocity Variation:

  • If you press the same pad with varying velocities, Conductor suggests a Velocity Range trigger

Game Controller Patterns

Button Hold Detection:

  • Hold a button for >1 second → Suggests LongPress trigger

Button Double-Tap Detection:

  • Tap a button twice quickly → Suggests DoubleTap trigger

Multi-Button Detection:

  • Press multiple buttons simultaneously → Suggests GamepadButtonChord trigger

Analog Stick Movement:

  • Move stick in any direction → Detects axis and direction automatically

Trigger Pull Detection:

  • Pull analog trigger → Detects threshold and creates GamepadTrigger

Velocity Range Suggestions

When Input Learn detects a note, it suggests velocity ranges:

Input Learn Complete!

Detected: Note 36

Suggested velocity ranges:
- Soft (0-40): Gentle tap
- Medium (41-80): Normal press
- Hard (81-127): Strong hit

Create separate mappings for each range?
[Yes] [No, use single Note trigger]

This makes it easy to create pressure-sensitive mappings.

Device-Specific Workflows

Gamepad Example (Xbox Controller)

Goal: Map A button to copy text (Cmd+C)

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Press A button on Xbox controller
  4. Auto-detects: GamepadButton: 128
  5. Select Action: Keystroke
  6. Use Keystroke Picker: Press Cmd+C
  7. Save

Result: Pressing A button executes Cmd+C

Flight Stick Example

Goal: Map trigger to enter key

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Pull trigger on flight stick
  4. Auto-detects: GamepadButton: 128 (or GamepadTrigger if analog)
  5. Action: KeystrokeReturn
  6. Save

Result: Pulling trigger presses Enter

Racing Wheel Example

Goal: Map wheel rotation to browser navigation

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Turn wheel right
  4. Auto-detects: GamepadAnalogStick: Axis 128, Direction: Clockwise
  5. Action: KeystrokeCmd+RightArrow (forward in browser)
  6. Save

Result: Turning wheel right navigates forward in browser

HOTAS Example

Goal: Map throttle up to volume increase

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Move throttle up
  4. Auto-detects: GamepadAxis: 129, Direction: Clockwise
  5. Action: Volume ControlUp
  6. Save

Result: Moving throttle up increases system volume

Hybrid MIDI + Gamepad

Goal: Use MIDI pad and gamepad button together

You can freely mix MIDI and gamepad inputs:

MIDI Pad for Copy:

  1. Learn → Press MIDI pad 36 → Auto-detects Note: 36
  2. Action: Keystroke Cmd+C

Gamepad A Button for Paste:

  1. Learn → Press A button → Auto-detects GamepadButton: 128
  2. Action: Keystroke Cmd+V

No conflicts: MIDI uses IDs 0-127, gamepads use IDs 128-255

Troubleshooting

No Input Detected

Symptoms: Input Learn countdown reaches zero without detecting anything

Solutions:

  1. Check device connection:
    • MIDI: Ensure device shows in Device Panel
    • Gamepad: Verify connection in system settings
  2. Check Event Console: Open Event Console and verify events are being received
  3. Try different control: Some controls may not send events (e.g., mode buttons)
  4. Restart device: Disconnect and reconnect the device

Wrong Button/Note Detected

Symptoms: Input Learn detects incorrect button ID or note number

Solutions:

  1. Verify in Event Console: Check what events are actually being sent
  2. Check button ID range:
    • MIDI: 0-127
    • Gamepad: 128-255
  3. Load device template: Use a pre-configured template for your controller
  4. Manual override: Click “Advanced” and manually enter the correct ID

Gamepad Not Recognized

Symptoms: Gamepad buttons don’t trigger Input Learn

Solutions:

  1. Ensure SDL2 compatibility: Check if gamepad is SDL2-compatible
  2. Check system recognition:
    • macOS: System Settings → Game Controllers
    • Linux: ls /dev/input/js*
    • Windows: Devices and Printers
  3. Try USB instead of Bluetooth (or vice versa)
  4. Restart Conductor daemon: conductorctl stop && conductor --foreground

Multiple Events Detected

Symptoms: Input Learn shows “Multiple events detected, please try again”

Solutions:

  1. Press only one control at a time
  2. Wait for button/pad to release before pressing again
  3. Disable auto-repeat: Some controllers send rapid-fire messages

Analog Stick Not Detected

Symptoms: Moving stick doesn’t trigger Input Learn

Solutions:

  1. Move stick beyond dead zone: Move at least 15% from center
  2. Check axis ID: Ensure using correct stick (left vs right)
  3. Verify in Event Console: See if axis events are being received
  4. Try different direction: Some sticks may have faulty axes

Trigger Pull Not Detected

Symptoms: Pulling analog trigger doesn’t work

Solutions:

  1. Pull trigger fully: Some triggers need >50% pull
  2. Check threshold: Try different pull depths
  3. Use digital trigger instead: Try the digital trigger button (LT/RT button)
  4. Verify in Event Console: Check if trigger axis events appear

Velocity Not Detected

Symptoms: Input Learn only creates Note trigger, not Velocity Range

Solutions:

  1. Vary velocity: Try pressing soft, medium, and hard during different Learn attempts
  2. Manual configuration: Create Velocity Range trigger manually after Learn
  3. Check controller: Some pads don’t send velocity (always velocity 127)

Examples

Example 1: Basic MIDI Pad Mapping

Goal: Map pad to copy text (Cmd+C)

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Press pad → Auto-fills Note: 36
  4. Select Action: Keystroke
  5. Use Keystroke Picker: Press Cmd+C
  6. Save

Result: Pressing pad executes Cmd+C

Example 2: Gamepad Multi-Button Combo

Goal: LB + RB switches to Media mode

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Press LB + RB together → Auto-fills GamepadButtonChord: [136, 137]
  4. Action: Mode Change → Select “Media”
  5. Save

Result: Pressing LB + RB together switches to Media mode

Example 3: Velocity-Sensitive Volume

Goal: Soft press = volume down, hard press = volume up

  1. Click “Add Mapping”

  2. Click “Learn” next to Trigger

  3. Press pad softly → Auto-fills VelocityRange: Note 36, Min: 0, Max: 40

  4. Action: Volume ControlDown

  5. Save

  6. Click “Add Mapping” again

  7. Click “Learn”

  8. Press same pad hard → Auto-fills VelocityRange: Note 36, Min: 81, Max: 127

  9. Action: Volume ControlUp

  10. Save

Result: Soft press = volume down, hard press = volume up

Example 4: Long Press to Launch App

Goal: Hold pad for 2 seconds to open Spotify

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Hold pad for 2+ seconds → Auto-fills LongPress: Note 36, Duration: 2000ms
  4. Action: Launch → Select /Applications/Spotify.app
  5. Save

Result: Holding pad for 2 seconds opens Spotify

Example 5: Encoder for Volume

Goal: Turn encoder to adjust volume

  1. Click “Add Mapping”

  2. Click “Learn” next to Trigger

  3. Turn encoder right → Auto-fills EncoderTurn: CC 1, Direction: Clockwise

  4. Action: Volume ControlUp

  5. Save

  6. Click “Add Mapping” again

  7. Click “Learn”

  8. Turn encoder left → Auto-fills EncoderTurn: CC 1, Direction: Counterclockwise

  9. Action: Volume ControlDown

  10. Save

Result: Turning encoder controls system volume

Example 6: Analog Stick Navigation

Goal: Right stick controls browser navigation

  1. Click “Add Mapping”

  2. Click “Learn” next to Trigger

  3. Move right stick right → Auto-fills GamepadAnalogStick: Axis 130, Direction: Clockwise

  4. Action: KeystrokeCmd+RightArrow

  5. Save

  6. Click “Add Mapping”

  7. Click “Learn”

  8. Move right stick left → Auto-fills GamepadAnalogStick: Axis 130, Direction: CounterClockwise

  9. Action: KeystrokeCmd+LeftArrow

  10. Save

Result: Moving right stick navigates forward/back in browser

Example 7: Racing Wheel Throttle

Goal: Wheel triggers control volume

  1. Click “Add Mapping”
  2. Click “Learn” next to Trigger
  3. Pull right trigger → Auto-fills GamepadTrigger: 133, Threshold: 128
  4. Action: Volume ControlUp
  5. Save

Result: Pulling right trigger increases volume

Tips & Best Practices

Tip 1: Use Event Console

Before starting Input Learn:

  1. Open Event Console
  2. Press your control
  3. Verify the event appears

This helps debug “Learn not detecting” issues.

Tip 2: Learn in Context

When creating per-app profiles, Learn with that app in focus:

  1. Switch to target app (e.g., Logic Pro)
  2. Switch back to Conductor GUI
  3. Use Input Learn
  4. Assign action relevant to that app

Tip 3: Batch Learn

Create multiple mappings quickly:

  1. Click “Learn”
  2. Press control
  3. Assign action
  4. Save
  5. Immediately click “Add Mapping” and repeat

Tip 4: Device Templates First

Before manual Learn:

  1. Check if a device template exists for your controller
  2. Load template to get 90% of mappings
  3. Use Learn to customize the remaining 10%

Tip 5: Test Immediately

After creating a mapping:

  1. Click “Save”
  2. Immediately test by pressing the control
  3. Verify action executes correctly
  4. Adjust if needed

Tip 6: Gamepad Button IDs

Remember the ID ranges:

  • MIDI: 0-127 (notes, CC, etc.)
  • Gamepad: 128-255 (buttons, axes, triggers)
  • No overlap: Both can coexist in same config

Tip 7: Analog Stick Dead Zones

Analog sticks have 10% dead zones:

  • Center position won’t trigger
  • Move at least 15% from center
  • Prevents false triggers

Tip 8: Hybrid Setups

Combine MIDI and gamepad strengths:

  • MIDI pads: Velocity-sensitive music actions
  • Gamepad buttons: Navigation and shortcuts
  • MIDI encoders: Fine parameter control
  • Gamepad triggers: Pressure-sensitive volume

Next Steps


Last Updated: November 21, 2025 (v3.0)

Modes: Context Switching for Different Workflows

Overview

Modes are Conductor’s system for context switching - they allow you to define completely different mapping sets for different workflows, all accessible from the same MIDI controller. Think of modes as “profiles” or “layers” that transform your controller’s behavior based on what you’re doing.

For example:

  • Default Mode: General productivity shortcuts (copy, paste, window switching)
  • Developer Mode: IDE shortcuts, terminal commands, debugging tools
  • Media Mode: Audio/video playback controls, screen capture, streaming tools

Modes enable a single 16-pad controller to provide hundreds of distinct functions without reconfiguring anything.

Mode Architecture

Mode Structure in config.toml

Modes are defined in your config.toml using the [[modes]] array:

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Copy text"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Paste text"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["cmd"]

[[modes]]
name = "Developer"
color = "green"

[[modes.mappings]]
description = "Run tests"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Shell"
command = "cargo test"

[[modes.mappings]]
description = "Git commit"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Shell"
command = "git commit"

Key Points:

  • Each [[modes]] section defines a complete mode
  • name: Displayed in console when switching modes
  • color: LED feedback color theme (optional but recommended)
  • [[modes.mappings]]: Array of triggers and actions for this mode

Mode Colors and LED Feedback

Each mode can have a distinct color theme for visual identification:

Available Colors:

  • blue - Default mode (calm, general use)
  • green - Development/productivity (focused work)
  • purple - Media/creative (entertainment)
  • red - Emergency/system (critical functions)
  • yellow - Testing/debug (temporary mappings)
  • white - Neutral (fallback)

How Colors Work:

[[modes]]
name = "Default"
color = "blue"  # All pads in this mode default to blue when idle

When using the reactive lighting scheme:

  • Idle pads show the mode’s base color at dim brightness
  • Pressed pads light up with velocity-based colors (green/yellow/red)
  • Released pads fade back to the mode’s base color

When using static patterns (rainbow, wave, etc.):

  • The mode color is used as the primary theme color
  • Pattern variations are tinted with the mode color

Global vs Mode-Specific Mappings

Conductor supports two types of mappings:

Mode-Specific Mappings

Defined inside [[modes.mappings]], these only work when that mode is active:

[[modes]]
name = "Media"
color = "purple"

[[modes.mappings]]
description = "Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "space"
modifiers = []

This Play/Pause mapping only works in Media mode.

Global Mappings

Defined in [[global_mappings]] at the top level, these work in ALL modes:

[[global_mappings]]
description = "Emergency exit"
[global_mappings.trigger]
type = "LongPress"
note = 0
hold_duration_ms = 3000
[global_mappings.action]
type = "Shell"
command = "killall conductor"

[[global_mappings]]
description = "Volume up (always available)"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

Use Cases for Global Mappings:

  • Emergency shutdown (long press escape)
  • Volume control (encoder always controls volume)
  • Mode switching (dedicated mode-change buttons)
  • System-wide shortcuts (screenshots, lock screen)

Priority Order:

  1. Mode-specific mappings are checked first
  2. If no match, global mappings are checked
  3. If still no match, the event is ignored

This means mode-specific mappings can “override” global ones by using the same trigger.

Switching Between Modes

There are three ways to switch modes:

1. Encoder Rotation

The most common method - use your encoder as a mode selector:

[[global_mappings]]
description = "Next mode (encoder clockwise)"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = "next"

[[global_mappings]]
description = "Previous mode (encoder counter-clockwise)"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "CounterClockwise"
[global_mappings.action]
type = "ModeChange"
mode = "previous"

This creates a circular mode selector:

  • Turn clockwise: Default → Developer → Media → Default…
  • Turn counter-clockwise: Default → Media → Developer → Default…

2. Dedicated Mode Buttons

Assign specific pads to jump directly to modes:

[[global_mappings]]
description = "Jump to Developer mode"
[global_mappings.trigger]
type = "Note"
note = 15  # Top-right pad
[global_mappings.action]
type = "ModeChange"
mode = "Developer"

[[global_mappings]]
description = "Jump to Media mode"
[global_mappings.trigger]
type = "Note"
note = 11  # Another pad
[global_mappings.action]
type = "ModeChange"
mode = "Media"

3. Chord-Based Mode Switching

Use pad combinations for advanced mode switching:

[[global_mappings]]
description = "Secret admin mode (pads 0+1+2 together)"
[global_mappings.trigger]
type = "NoteChord"
notes = [0, 1, 2]
chord_timeout_ms = 100
[global_mappings.action]
type = "ModeChange"
mode = "Admin"

ModeChange Action Parameters:

  • mode = "next" - Cycle to next mode
  • mode = "previous" - Cycle to previous mode
  • mode = "Default" - Jump to specific mode by name (case-sensitive)
  • mode = 0 - Jump to mode by index (0-based)

Practical Mode Examples

Example 1: Three-Mode General Setup

# Mode 0: Default (General Productivity)
[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Copy"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Paste"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Switch to next app"
[modes.mappings.trigger]
type = "Note"
note = 14
[modes.mappings.action]
type = "Keystroke"
keys = "tab"
modifiers = ["cmd"]

# Mode 1: Developer (Coding Tools)
[[modes]]
name = "Developer"
color = "green"

[[modes.mappings]]
description = "Run tests"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Shell"
command = "cargo test"

[[modes.mappings]]
description = "Build release"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Shell"
command = "cargo build --release"

[[modes.mappings]]
description = "Open terminal"
[modes.mappings.trigger]
type = "Note"
note = 14
[modes.mappings.action]
type = "Launch"
app = "Terminal"

# Mode 2: Media (Audio/Video Control)
[[modes]]
name = "Media"
color = "purple"

[[modes.mappings]]
description = "Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "space"
modifiers = []

[[modes.mappings]]
description = "Next track"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Keystroke"
keys = "right"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Screenshot"
[modes.mappings.trigger]
type = "Note"
note = 14
[modes.mappings.action]
type = "Keystroke"
keys = "4"
modifiers = ["cmd", "shift"]

# Global: Mode switching and volume
[[global_mappings]]
description = "Next mode"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = "next"

[[global_mappings]]
description = "Previous mode"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "CounterClockwise"
[global_mappings.action]
type = "ModeChange"
mode = "previous"

Example 2: Context-Aware Developer Modes

# General development
[[modes]]
name = "Dev-General"
color = "green"

[[modes.mappings]]
description = "Save all"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "s"
modifiers = ["cmd", "alt"]

# Rust-specific
[[modes]]
name = "Dev-Rust"
color = "green"

[[modes.mappings]]
description = "cargo check"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Shell"
command = "cargo check"

[[modes.mappings]]
description = "cargo fmt"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Shell"
command = "cargo fmt"

# Python-specific
[[modes]]
name = "Dev-Python"
color = "green"

[[modes.mappings]]
description = "pytest"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Shell"
command = "pytest"

[[modes.mappings]]
description = "black format"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Shell"
command = "black ."

Example 3: Velocity-Sensitive Multi-Mode

Combine modes with velocity detection for even more control:

[[modes]]
name = "Advanced"
color = "yellow"

# Soft press: Small increase
[[modes.mappings]]
description = "Small volume up"
[modes.mappings.trigger]
type = "VelocityRange"
note = 12
min_velocity = 0
max_velocity = 40
[modes.mappings.action]
type = "Shell"
command = "osascript -e 'set volume output volume (output volume of (get volume settings) + 5)'"

# Hard press: Large increase
[[modes.mappings]]
description = "Large volume up"
[modes.mappings.trigger]
type = "VelocityRange"
note = 12
min_velocity = 81
max_velocity = 127
[modes.mappings.action]
type = "Shell"
command = "osascript -e 'set volume output volume (output volume of (get volume settings) + 20)'"

Mode Design Best Practices

1. Use Descriptive Names

# Good: Clear what the mode does
[[modes]]
name = "Media-Playback"

# Bad: Ambiguous
[[modes]]
name = "Mode3"

2. Assign Consistent Color Themes

# Productivity modes: Blue family
[[modes]]
name = "Default"
color = "blue"

[[modes]]
name = "Email"
color = "blue"

# Development modes: Green family
[[modes]]
name = "Developer"
color = "green"

[[modes]]
name = "Testing"
color = "green"

# Creative modes: Purple/Magenta family
[[modes]]
name = "Media"
color = "purple"

[[modes]]
name = "Design"
color = "magenta"

3. Keep Mode Count Manageable

Recommended: 3-5 modes for most users

  • Too few modes: You’ll run out of pads for all your functions
  • Too many modes: You’ll forget which mode you’re in

Strategy: Group related functions into modes rather than creating a mode for every app.

4. Reserve Global Mappings for Critical Functions

Only use [[global_mappings]] for:

  • Mode switching itself
  • Emergency shutdowns
  • System-wide controls (volume, screen lock)
  • Functions that should work regardless of mode

5. Use Mode Colors for Visual Feedback

If your device supports LED feedback:

  • Test each mode and verify the color matches the function
  • Use calming colors (blue, green) for frequent modes
  • Use attention-grabbing colors (red, yellow) for special modes

6. Document Your Modes

Add descriptions to help remember your layout:

[[modes]]
name = "Developer"
color = "green"
# Description comments help future you remember the layout:
# Pad 0-3: Git commands (status, add, commit, push)
# Pad 4-7: Build commands (check, test, build, run)
# Pad 8-11: IDE shortcuts (format, refactor, debug, terminal)
# Pad 12-15: Project navigation (files, search, grep, docs)

[[modes.mappings]]
description = "Git status"
[modes.mappings.trigger]
type = "Note"
note = 0
[modes.mappings.action]
type = "Shell"
command = "git status"

Advanced Mode Techniques

Mode-Specific Timing Adjustments

Override advanced settings per mode:

[advanced_settings]
chord_timeout_ms = 100
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

[[modes]]
name = "Gaming"
color = "red"
# Gaming mode needs faster response
# (Note: Per-mode advanced settings not yet implemented,
#  but this is the planned syntax)

Conditional Mode Switching

Combine with Conditional actions for smart mode switching:

[[global_mappings]]
description = "Auto-switch to Dev mode if VS Code is active"
[global_mappings.trigger]
type = "Note"
note = 15
[global_mappings.action]
type = "Conditional"
condition = "ActiveApp"
value = "Visual Studio Code"
then_action = { type = "ModeChange", mode = "Developer" }
else_action = { type = "ModeChange", mode = "Default" }

Mode Sequences

Chain mode changes with other actions:

[[modes.mappings]]
description = "Open Spotify and switch to Media mode"
[modes.mappings.trigger]
type = "Note"
note = 15
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Launch", app = "Spotify" },
    { type = "Delay", duration_ms = 1000 },
    { type = "ModeChange", mode = "Media" }
]

Troubleshooting Modes

Mode Not Switching

Symptoms: Encoder turns but mode doesn’t change

Checks:

  • Verify [[global_mappings]] contains ModeChange actions
  • Check encoder is mapped correctly (use cargo run --bin midi_diagnostic 2)
  • Ensure mode names are spelled exactly as defined
  • Look for console output confirming mode changes

Debug:

DEBUG=1 cargo run --release 2
# Look for: "Mode changed: Default -> Developer"

Mappings Not Working in Mode

Symptoms: Pad press does nothing in a specific mode

Checks:

  • Verify you’re in the correct mode (check console output)
  • Confirm note numbers match (use cargo run --bin pad_mapper)
  • Check [[modes.mappings]] is properly nested under the mode
  • Ensure TOML syntax is valid

Test:

# Add a simple test mapping to verify mode is active
[[modes.mappings]]
description = "Test mode active"
[modes.mappings.trigger]
type = "Note"
note = 0
[modes.mappings.action]
type = "Shell"
command = "echo 'Mode working!' | tee /dev/tty"

Conflicting Mappings

Symptoms: Unexpected action triggers

Checks:

  • Global mappings override mode mappings if using same trigger
  • Multiple modes might have similar triggers
  • Check for copy-paste errors in note numbers

Solution: Use unique note numbers for each function, or intentionally use the priority system:

# Global: Emergency stop (works everywhere)
[[global_mappings]]
[global_mappings.trigger]
type = "LongPress"
note = 0

# Mode: Normal pad 0 function (only when not held)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 0

See Also


Last Updated: November 11, 2025 Implementation Status: Fully functional in current release

Daemon & Hot-Reload Guide

Conductor v2.0.0 introduces a production-ready background daemon service with lightning-fast configuration hot-reloading.

Architecture Overview

The Conductor daemon runs as a background service, providing:

  • Hot-Reload: Configuration changes applied in 0-10ms without restart
  • IPC Control: Control via CLI (conductorctl) or GUI
  • State Persistence: Automatic state saving with atomic writes
  • Crash Recovery: Auto-restart with KeepAlive (when using LaunchAgent)
  • Menu Bar Integration: macOS menu bar for quick actions

Starting the Daemon

Open the Conductor GUI app - the daemon starts automatically in the background.

Via CLI

# Start daemon in foreground
conductor

# Start daemon in background
conductor &

# Check if daemon is running
ps aux | grep conductor | grep -v grep

# Check daemon status via IPC
conductorctl status

Via LaunchAgent (Auto-Start)

See macOS Installation Guide for LaunchAgent setup.

Controlling the Daemon

conductorctl CLI

The conductorctl command provides full control over the daemon:

Status

# Human-readable status
conductorctl status

# Example output:
# Conductor Daemon Status:
#   State: Running
#   Uptime: 1h 23m 45s
#   Config: /Users/you/.config/conductor/config.toml
#   Last Reload: 2m 15s ago
#   IPC Socket: /tmp/conductor.sock
#   PID: 12345

JSON output (for scripting):

conductorctl status --json

# Example output:
# {
#   "state": "Running",
#   "uptime_seconds": 5025,
#   "config_path": "/Users/you/.config/conductor/config.toml",
#   "last_reload_seconds": 135,
#   "ipc_socket": "/tmp/conductor.sock",
#   "pid": 12345
# }

Reload Configuration

# Reload config (hot-reload in 0-10ms)
conductorctl reload

# Example output:
# Config reloaded successfully in 3ms
# Loaded 15 mappings across 3 modes

This applies configuration changes without restarting the daemon or interrupting active MIDI connections.

Performance: Config reload completes in 0-10ms on average (vs. ~500ms for full restart).

Validate Configuration

# Validate config without reloading
conductorctl validate

# Example output (success):
# ✓ Config is valid
# Found 3 modes with 15 total mappings
# Found 2 global mappings
# No errors detected

# Example output (error):
# ✗ Config validation failed:
# Error in mode 'Default', mapping 3:
#   Invalid note number: 128 (must be 0-127)

This is useful for:

  • Testing config changes before applying
  • CI/CD validation
  • Pre-commit hooks

Stop Daemon

# Graceful shutdown
conductorctl stop

# Example output:
# Daemon stopped gracefully

The daemon saves its state before shutting down.

Ping (Latency Check)

# Check IPC round-trip latency
conductorctl ping

# Example output:
# Pong! Round-trip: 0.43ms

This verifies the daemon is responsive and measures IPC performance.

Configuration Hot-Reload

How It Works

  1. File Watcher: Monitors ~/.config/conductor/config.toml for changes
  2. Debouncing: 500ms debounce window to avoid redundant reloads
  3. Parsing: Validates TOML syntax and config structure
  4. Atomic Swap: Replaces active config with new one in a single operation
  5. Reload Time: 0-10ms typical (measured)

Workflow

Edit your config file:

# Open in your editor
code ~/.config/conductor/config.toml

# Make changes and save

Automatic reload happens within 500-510ms of saving:

[2025-11-14 10:30:15] Config file changed
[2025-11-14 10:30:15] Waiting 500ms for debounce...
[2025-11-14 10:30:15] Reloading config...
[2025-11-14 10:30:15] Config reloaded successfully in 3ms

Or manually trigger reload:

conductorctl reload

What Gets Reloaded

  • ✅ All mappings (modes + global)
  • ✅ Device settings
  • ✅ LED schemes
  • ✅ Advanced settings (timeouts, thresholds)
  • ❌ IPC socket path (requires daemon restart)
  • ❌ Auto-start settings (requires LaunchAgent reload)

Error Handling

If config reload fails:

# Example: Invalid TOML syntax
conductorctl reload

# Output:
# ✗ Config reload failed: TOML parse error
# Error at line 42: expected '=' after key 'trigger'
# Previous config still active (no changes applied)

The daemon keeps the previous valid config and continues running.

State Persistence

The daemon automatically saves state to ~/.config/conductor/state.json:

What Gets Saved

  • Current mode
  • Active LED scheme
  • Per-app profile assignments
  • Last known device connection

Atomic Writes

State is saved using atomic writes to prevent corruption:

  1. Write to temporary file: state.json.tmp
  2. Verify write succeeded
  3. Rename to state.json (atomic operation)
  4. SHA256 checksum for integrity

Emergency Save

The daemon registers signal handlers to save state on:

  • SIGTERM (graceful shutdown)
  • SIGINT (Ctrl+C)
  • SIGHUP (hangup)

IPC Protocol

The daemon uses Unix domain sockets for inter-process communication.

Socket Location

Default: /tmp/conductor.sock

Protocol

Format: JSON messages over Unix socket

Request:

{
  "command": "reload",
  "args": {}
}

Response:

{
  "status": "success",
  "message": "Config reloaded successfully",
  "duration_ms": 3
}

Available Commands

  • status - Get daemon status
  • reload - Reload configuration
  • validate - Validate config without reloading
  • stop - Graceful shutdown
  • ping - Latency check

Security Limits

The IPC protocol enforces security limits to prevent abuse:

Request Size Limit: 1MB (1,048,576 bytes)

Requests exceeding this limit will be rejected with error code 1004 (InvalidRequest):

{
  "status": "error",
  "error": {
    "code": 1004,
    "message": "Request too large: 1500000 bytes exceeds maximum of 1048576 bytes (1MB)",
    "details": {
      "request_size": 1500000,
      "max_size": 1048576,
      "security": "Request rejected to prevent memory exhaustion"
    }
  }
}

Why this limit exists: Prevents memory exhaustion denial-of-service attacks where an attacker sends arbitrarily large requests to consume daemon memory.

Is 1MB enough?: Yes. Typical IPC requests are:

  • status: ~200 bytes
  • reload: ~100 bytes
  • validate: ~150 bytes
  • ping: ~50 bytes

Even large configuration payloads are well under 100KB. The 1MB limit provides 10x safety margin.

Performance Metrics

The daemon tracks and reports performance metrics:

Config Reload Latency

conductorctl status --json | jq '.metrics.reload_latency_ms'
# Output: 3.2

Targets:

  • ✅ <10ms: Excellent (target met in v2.0.0)
  • ⚠️ 10-50ms: Acceptable
  • ❌ >50ms: Investigate

IPC Round-Trip

conductorctl ping
# Output: Pong! Round-trip: 0.43ms

Targets:

  • ✅ <1ms: Excellent (target met in v2.0.0)
  • ⚠️ 1-5ms: Acceptable
  • ❌ >5ms: Investigate

Memory Usage

ps aux | grep conductor | awk '{print $6/1024 " MB"}'
# Output: 8.2 MB

Targets:

  • ✅ 5-10MB: Normal (daemon only)
  • ⚠️ 10-20MB: Acceptable
  • ❌ >20MB: Investigate memory leak

CPU Usage

top -pid $(pgrep conductor) -stats cpu -l 1 | tail -1
# Output: 0.3%

Targets:

  • ✅ <1%: Idle (no MIDI activity)
  • ✅ <5%: Active (processing MIDI events)
  • ⚠️ 5-10%: Heavy load
  • ❌ >10%: Investigate

The daemon includes an optional menu bar icon:

Features

  • Status indicator: Running (green), Stopped (gray), Error (red)
  • Quick actions:
    • Pause/Resume
    • Reload Config
    • Open GUI
    • Quit

Enable Menu Bar

In GUI Settings:

  1. Go to SettingsGeneral
  2. Enable “Show menu bar icon”
  3. Click Save

Or edit config:

[settings]
show_menu_bar = true

Troubleshooting

Daemon Won’t Start

Check if already running:

ps aux | grep conductor

If already running, stop it first:

conductorctl stop

Check logs:

# If using LaunchAgent
tail -f /tmp/conductor.err

# If running manually
conductor  # Run in foreground to see errors

Config Won’t Reload

Validate config syntax:

conductorctl validate

Check file watcher:

# Manually trigger reload
conductorctl reload

Check file permissions:

ls -l ~/.config/conductor/config.toml
# Should be readable by your user

High Latency

Check IPC performance:

conductorctl ping

Check system load:

top -l 1 | head -10

Restart daemon:

conductorctl stop
conductor &

State Not Persisting

Check state file:

ls -l ~/.config/conductor/state.json
cat ~/.config/conductor/state.json | jq

Check file permissions:

# State directory should be writable
ls -ld ~/.config/conductor

Advanced Configuration

Custom IPC Socket Path

Edit daemon config (future feature):

[daemon]
ipc_socket = "/tmp/my-custom-conductor.sock"

Then tell conductorctl to use it:

export MIDIMON_SOCKET=/tmp/my-custom-conductor.sock
conductorctl status

Custom Config Path

Run daemon with custom config:

conductor --config /path/to/config.toml

Or set environment variable:

export MIDIMON_CONFIG=/path/to/config.toml
conductor

Logging

Enable debug logging:

DEBUG=1 conductor

Output includes:

  • Config reload events
  • IPC requests/responses
  • State persistence operations
  • MIDI event processing (verbose)

Next Steps


Last Updated: November 14, 2025 (v2.0.0)

GUI Configuration

The Conductor GUI provides a visual interface for configuring your MIDI controller without manually editing TOML files.

Overview

The GUI application is built with Tauri v2 and provides:

  • Visual mode management - Create and edit mapping modes with different button layouts
  • Mapping editor - Configure triggers and actions with visual selectors
  • MIDI Learn - Auto-detect trigger patterns by pressing device buttons
  • Device management - Connect to MIDI devices and apply templates
  • Profile management - Per-app profiles that switch automatically
  • Live event console - Debug MIDI events in real-time
  • Settings panel - Configure application preferences

Getting Started

Launch the GUI

# Start the GUI application
./conductor-gui

# Or if installed system-wide
conductor-gui

The GUI will appear in your system tray with quick access to:

  • Status display
  • Reload configuration
  • Pause/resume processing
  • Mode switching
  • Open config file
  • View logs

Initial Setup

  1. Connect a Device

    • Navigate to Devices tab
    • Select your MIDI controller from the list
    • Click Connect
  2. Choose a Template (optional)

    • Click Device Templates
    • Select a pre-configured template for your device
    • Click Apply Template
  3. Create Your First Mode

    • Navigate to Modes tab
    • Click + Add Mode
    • Set a name and color
    • Click Save

Mode Configuration

Creating Modes

Modes allow different button mappings for different contexts (e.g., “Development”, “Media”, “Gaming”).

  1. Go to Modes tab
  2. Click + Add Mode
  3. Fill in:
    • Name: Descriptive name (e.g., “Video Editing”)
    • Color: Visual identifier (blue, green, purple, etc.)
  4. Click Save

Editing Modes

  1. Select the mode from the list
  2. Click Edit Mode
  3. Modify settings:
    • Name
    • Color
    • Mode-specific mappings
  4. Click Save Changes

Deleting Modes

  1. Select the mode
  2. Click Delete Mode
  3. Confirm deletion

Note: You cannot delete the last remaining mode.

Mapping Configuration

Creating Mappings

Mappings define what happens when you press a button or turn a knob.

  1. Go to Mappings tab
  2. Choose:
    • Mode-specific mappings (active only in selected mode)
    • Global mappings (active across all modes)
  3. Click + Add Mapping

Using MIDI Learn

The fastest way to create mappings:

  1. Click 🎹 MIDI Learn button
  2. Press/turn the button/knob on your device
  3. Conductor detects the pattern (note, velocity, long press, etc.)
  4. The trigger is auto-filled
  5. Configure the action (what to do)
  6. Click Save Mapping

See the MIDI Learn guide for details.

Manual Trigger Configuration

If you prefer manual setup:

Trigger Types

  • Note: Basic note on/off

    • Set note number (0-127)
    • Optional velocity range
  • Velocity Range: Different actions for soft/medium/hard presses

    • Soft: 0-40
    • Medium: 41-80
    • Hard: 81-127
  • Long Press: Hold detection

    • Set duration (default 2000ms)
  • Double Tap: Quick double-press

    • Set timeout window (default 300ms)
  • Note Chord: Multiple notes simultaneously

    • Add 2+ notes
    • Set detection window (default 100ms)
  • Encoder Turn: Knob rotation

    • Set CC number
    • Choose direction (Clockwise/CounterClockwise)
  • Control Change (CC): MIDI CC messages

    • Set CC number
    • Optional value range
  • Aftertouch: Pressure sensitivity

    • Optional minimum pressure
  • Pitch Bend: Touch strip control

    • Optional value range

Action Configuration

Action Types

  • Keystroke: Keyboard shortcuts

    • Select modifiers (Ctrl, Alt, Shift, Super/Cmd)
    • Set key(s)
  • Text: Type text strings

    • Enter text to type
  • Launch: Open applications

    • Enter application name or path
  • Shell: Execute shell commands

    • Enter command
    • Optional working directory
  • Volume Control: System volume

    • Up/Down/Mute/Set
  • Mode Change: Switch modes

    • Select target mode
  • Sequence: Chain multiple actions

    • Add multiple actions in order
  • Delay: Wait between actions

    • Set duration in milliseconds
  • Mouse Click: Simulate mouse input

    • Left/Right/Middle button
    • Single/Double/Triple click
  • Repeat: Repeat an action

    • Set count

Editing Mappings

  1. In the Mappings tab
  2. Click the mapping to edit
  3. Modify trigger or action
  4. Click Save Changes

Deleting Mappings

  1. Select the mapping
  2. Click Delete
  3. Confirm deletion

Device Management

Connecting Devices

  1. Go to Devices tab
  2. View available MIDI devices
  3. See connection status
  4. Monitor daemon status (running, uptime, events processed)

Using Device Templates

Templates provide pre-configured mappings for popular controllers:

  1. Click 📋 Device Templates
  2. Browse available templates:
    • Maschine Mikro MK3
    • Launchpad Mini
    • Korg nanoKONTROL
    • Custom templates
  3. Select a template
  4. Click Apply
  5. Reload daemon configuration

Profile Management

Profiles let you switch entire configurations:

  1. Click 🔄 Profiles
  2. View available profiles
  3. Switch manually or enable per-app automatic switching
  4. Export/import profiles for backup

Per-App Profiles

Automatically switch profiles based on the frontmost application.

See the Per-App Profiles guide for complete setup.

Live Event Console

Debug MIDI events in real-time:

  1. Go to Settings tab
  2. Click 📊 Show Event Console
  3. View live MIDI events:
    • Note on/off
    • Velocity values
    • Control changes
    • Timing information
  4. Use for troubleshooting and discovering note numbers

Settings

Application Settings

Configure Conductor preferences:

  • Auto-start: Launch on system startup
  • Log Level: Control logging verbosity (debug, info, warn, error)
  • Theme: Light/dark theme (future)

Configuration File

View and edit the raw config file:

  1. See the file path
  2. Click 📋 to copy path to clipboard
  3. Click 📝 to open in your default editor

The menu bar icon provides quick access:

  • Status: View daemon state
  • Reload Configuration: Hot-reload config changes
  • Pause Processing: Temporarily disable event processing
  • Resume Processing: Re-enable event processing
  • Switch Mode: Quick mode switching
    • Default
    • Development
    • Media
  • View Logs: Open system logs
  • Open Config File: Edit configuration
  • Quit Conductor: Stop daemon and quit

Icon States

  • 🟢 Running: Daemon active and processing events
  • 🟡 Paused: Processing paused
  • 🔴 Stopped: Daemon not running
  • ⚠️ Error: Error state

Keyboard Shortcuts

Global shortcuts (when GUI is focused):

  • Cmd/Ctrl + R: Reload configuration
  • Cmd/Ctrl + Q: Quit application
  • Cmd/Ctrl + ,: Open settings

Tips & Best Practices

  1. Use MIDI Learn for faster setup - it’s more accurate than manual entry

  2. Test mappings immediately after creation - press the button to verify

  3. Start with global mappings for frequently used actions (volume, mode switch)

  4. Use descriptive names for modes and mappings - future you will thank you

  5. Export your config regularly - back up your work

  6. Use the event console when troubleshooting - see exactly what MIDI data is coming in

  7. Organize with modes - keep related mappings together

  8. Device templates save time - start with a template and customize

Troubleshooting

GUI Won’t Connect to Daemon

  1. Check daemon is running: conductorctl status
  2. Start daemon if needed: conductor
  3. Check IPC socket exists: ls /tmp/conductor.sock
  4. Restart daemon: conductorctl stop && conductor

Mappings Not Saving

  1. Check file permissions on config file
  2. Verify config path in Settings tab
  3. Check daemon logs for errors
  4. Try manual edit to verify TOML syntax

MIDI Events Not Detected

  1. Check device connection in Devices tab
  2. Use event console to verify MIDI data
  3. Ensure correct MIDI port selected
  4. Check device permissions (Input Monitoring on macOS)
  • macOS: Check System Settings → Privacy & Security → Accessibility
  • Linux: Ensure system tray extension installed
  • Try restarting the GUI application

Next Steps

Device Templates Guide

Device templates provide pre-configured mappings for popular MIDI controllers, letting you get started in seconds instead of hours.

What are Device Templates?

Device templates are pre-built configuration profiles that include:

  • Note mappings for all pads/buttons
  • Common actions (copy/paste, play/pause, volume control)
  • LED configurations optimized for each device
  • Mode layouts designed for typical workflows

Instead of manually mapping 16+ pads, load a template and customize only what you need.

Built-In Templates (v2.0.0)

Conductor includes 6 built-in templates for popular controllers:

1. Maschine Mikro MK3

Full RGB LED support, 16 pads, encoder

Pre-configured:

  • 16 velocity-sensitive pads
  • Encoder for volume control
  • 4 modes (Default, Development, Media, Custom)
  • Reactive LED feedback

Best for: Music production, software development, general productivity

2. Launchpad Mini MK3

RGB LED grid, 64 pads

Pre-configured:

  • 8x8 pad grid
  • Scene launch buttons
  • Rainbow LED schemes
  • Mode selection via top row

Best for: Ableton Live control, clip launching, grid-based workflows

3. Novation Launchkey Mini MK3

25 keys, 16 pads, 8 knobs

Pre-configured:

  • 16 drum pads
  • 8 knobs for parameter control
  • Pitch/modulation wheels
  • Transport controls

Best for: Music production, MIDI sequencing, DAW control

4. AKAI MPK Mini MK3

25 keys, 8 pads, 8 knobs

Pre-configured:

  • 8 MPC-style pads
  • 8 assignable knobs
  • Arpeggiator controls
  • 4-way joystick

Best for: Beat making, music production, performance

5. Korg nanoKONTROL2

8 faders, 8 knobs, 24 buttons

Pre-configured:

  • 8-channel mixer layout
  • Transport controls
  • Scene/marker buttons
  • Fader automation

Best for: DAW mixing, transport control, automation

6. APC Mini

64 pads, 9 faders

Pre-configured:

  • 8x8 clip launch grid
  • Scene launch column
  • 9 channel faders
  • Shift button combinations

Best for: Ableton Live, clip launching, mixing

Loading a Template

  1. Open Conductor GUI

  2. Go to SettingsDevice Templates

  3. Select your controller from the dropdown

  4. Click “Load Template”

  5. Confirm the load (replaces current config)

  6. Test - Press pads to verify mappings

  7. Customize using MIDI Learn for any changes

Via CLI

Templates are stored as TOML files in:

~/.config/conductor/templates/

Load manually:

# Copy template to config location
cp ~/.config/conductor/templates/maschine-mikro-mk3.toml ~/.config/conductor/config.toml

# Reload daemon
conductorctl reload

Template Structure

Templates are standard config.toml files with device-specific optimizations:

[device]
name = "Maschine Mikro MK3"
template = "maschine-mikro-mk3"
auto_connect = true

[led_feedback]
scheme = "reactive"
default_color = [0, 120, 255]  # Blue

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Pad 1 - Copy"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

# ... more mappings

Customizing Templates

After loading a template:

1. Use MIDI Learn

The fastest way to modify mappings:

  1. Click Edit on any mapping
  2. Click Learn next to the trigger
  3. Press the pad/button you want
  4. Update the action
  5. Save

2. Add New Modes

Templates include 2-4 modes. Add more:

  1. Go to Modes panel
  2. Click Add Mode
  3. Set name and color
  4. Add mappings using MIDI Learn

3. Adjust LED Schemes

Change LED behavior:

  1. Go to SettingsLED Feedback
  2. Select scheme: Reactive, Rainbow, Pulse, etc.
  3. Customize colors
  4. Save

4. Export Customized Template

Save your modifications:

  1. Go to SettingsExport Config
  2. Save as new template file
  3. Share with others or use on other machines

Creating Custom Templates

You can create templates for controllers not included:

Step 1: Map Your Device

  1. Connect device and use MIDI Learn for all controls
  2. Organize into modes (Default, Media, Development, etc.)
  3. Configure LED feedback (if supported)
  4. Test thoroughly with real workflows

Step 2: Export Template

  1. SettingsExport Config
  2. Save as my-controller-name.toml

Step 3: Add Metadata

Edit the exported file to add template metadata:

[template]
name = "My Controller"
description = "Template for My MIDI Controller v2"
author = "Your Name"
version = "1.0.0"
created_at = "2025-11-14"
device_ids = ["USB MIDI Device", "My Controller MIDI 1"]

Step 4: Test Template

  1. Reset config to default
  2. Load your template
  3. Verify all mappings work
  4. Check LED behavior

Step 5: Share (Optional)

Submit to Conductor template library:

  1. Create PR to config/device_templates/ directory
  2. Include template file + documentation
  3. Add device to compatibility matrix

Template Compatibility

Templates are forward-compatible across Conductor versions:

  • ✅ v2.0.0 templates work in v2.1.0+
  • ⚠️ Newer features may not load in older versions
  • ✅ Missing features gracefully ignored

Troubleshooting

Template Not Loading

Symptoms: “Failed to load template” error

Solutions:

  1. Check template file exists in ~/.config/conductor/templates/
  2. Validate TOML syntax: conductorctl validate --config path/to/template.toml
  3. Check permissions: Template file must be readable
  4. View error details in Event Console

Wrong Note Numbers

Symptoms: Template loads but pads don’t trigger actions

Solutions:

  1. Check device mode: Some controllers have multiple MIDI modes
  2. Verify MIDI channel: Template may use different channel than device
  3. Use MIDI Learn to detect actual note numbers
  4. Check Event Console to see incoming MIDI events

LEDs Not Working

Symptoms: Template loads but LEDs don’t respond

Solutions:

  1. Check device supports RGB: Some controllers only have single-color LEDs
  2. Verify HID access: Grant Input Monitoring permission
  3. Try different LED scheme: Some schemes may not work on all devices
  4. Check template LED config: May be disabled in template

Best Practices

Tip 1: Load Template First

When setting up a new controller:

  1. Load template first (if available)
  2. Test default mappings
  3. Customize only what you need

This saves 90% of setup time.

Tip 2: Create Per-App Variants

Use templates as a base for per-app profiles:

  1. Load template
  2. Customize for specific app (Logic Pro, VS Code, etc.)
  3. Export as new template
  4. Assign to app in Per-App Profiles

Tip 3: Version Your Templates

When customizing templates:

[template]
version = "1.0.0"
base_template = "maschine-mikro-mk3"
customized_by = "Your Name"
customized_at = "2025-11-14"

This helps track changes over time.

Tip 4: Test Before Sharing

Before submitting templates:

  • ✅ Test all mappings
  • ✅ Verify LED feedback
  • ✅ Test in multiple apps
  • ✅ Document any device-specific quirks

Next Steps


Last Updated: November 14, 2025 (v2.0.0)

Gamepad Support Guide

Version: 3.0 Status: Stable Platforms: macOS, Linux, Windows

Overview

Conductor v3.0 introduces full support for gamepad controllers, allowing you to use Xbox, PlayStation, Nintendo Switch Pro, and other SDL-compatible gamepads as macro input devices alongside MIDI controllers.

Quick Start

1. Connect Your Gamepad

  1. Connect your gamepad via USB or Bluetooth
  2. Ensure it’s recognized by your system
  3. Conductor will automatically detect compatible gamepads

Supported Controllers:

  • Xbox 360, Xbox One, Xbox Series X|S
  • PlayStation DualShock 4, DualSense (PS5)
  • Nintendo Switch Pro Controller
  • Any SDL2-compatible gamepad

2. Choose a Template

The quickest way to get started is using a pre-configured template:

Via GUI (Recommended):

  1. Open Conductor GUI
  2. Navigate to “Device Templates”
  3. Filter by “Gamepad Controllers”
  4. Select your controller (Xbox, PlayStation, or Switch Pro)
  5. Click “Create Config”

Via Configuration File: Copy one of the example configs from config/examples/:

  • gamepad-xbox-basic.toml - Xbox controller template
  • Or use the included device templates

3. Start Using

# Start Conductor daemon
conductor --foreground

# Your gamepad is now ready to trigger actions!

Configuration Reference

Basic Setup

[device]
name = "Gamepad"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50          # Multi-button chord detection window
double_tap_timeout_ms = 300    # Double-tap detection window
hold_threshold_ms = 2000       # Long press threshold

Trigger Types

1. GamepadButton

Simple button press trigger.

[[modes.mappings]]
description = "A button: Confirm"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # A button (Xbox) / Cross (PS) / B (Switch)
[modes.mappings.action]
type = "Keystroke"
keys = "Return"

Optional fields:

  • velocity_min: Minimum pressure (0-255)

2. GamepadButtonChord

Multiple buttons pressed simultaneously.

[[modes.mappings]]
description = "A+B: Screenshot"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [128, 129]  # A + B buttons
timeout_ms = 50       # Buttons must be pressed within 50ms
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

3. GamepadAnalogStick

Analog stick movement detection.

[[modes.mappings]]
description = "Right stick right: Forward"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130              # Right stick X-axis
direction = "Clockwise" # Moving right
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]

Stick Axes:

  • 128: Left stick X-axis
  • 129: Left stick Y-axis
  • 130: Right stick X-axis
  • 131: Right stick Y-axis

Directions:

  • "Clockwise": Right (X-axis) or Up (Y-axis)
  • "CounterClockwise": Left (X-axis) or Down (Y-axis)

Dead Zone: Automatic 10% dead zone prevents false triggers

4. GamepadTrigger

Analog trigger threshold detection (L2/R2, LT/RT, ZL/ZR).

[[modes.mappings]]
description = "Right trigger: Volume up"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133   # Right trigger
threshold = 128 # Half-pull (0-255 range)
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Triggers:

  • 132: Left trigger (L2, LT, ZL)
  • 133: Right trigger (R2, RT, ZR)

Button ID Reference

Standard Gamepad Layout

Conductor uses a unified button ID scheme across all gamepads:

Face Buttons (128-131)

IDXboxPlayStationSwitch
128A (South)CrossB
129B (East)CircleA
130X (West)SquareY
131Y (North)TriangleX

D-Pad (132-135)

IDButton
132Up
133Down
134Left
135Right

Shoulder Buttons (136-137)

IDXboxPlayStationSwitch
136LB (L1)L1L
137RB (R1)R1R

Stick Clicks (138-139)

IDButton
138Left stick click (L3)
139Right stick click (R3)
IDXboxPlayStationSwitch
140Menu (Start)Options+ (Plus)
141View (Select)Share/Create- (Minus)
142Xbox buttonPS buttonHome

Trigger Buttons Digital (143-144)

IDXboxPlayStationSwitch
143LT (digital)L2 (digital)ZL
144RT (digital)R2 (digital)ZR

Analog Axes

Stick Axes (128-131)

IDControl
128Left stick X-axis
129Left stick Y-axis
130Right stick X-axis
131Right stick Y-axis

Trigger Axes (132-133)

IDControl
132Left trigger analog
133Right trigger analog

Value Range: 0-255 (128 = center for sticks)

Advanced Features

Pattern Detection

Conductor automatically detects advanced button patterns:

Double-Tap

Press the same button twice within 300ms.

# Automatically detected by MIDI Learn
# Manual configuration:
[modes.mappings.trigger]
type = "DoubleTap"
note = 128  # Button ID (reuses Note type)
timeout_ms = 300

Long Press

Hold a button for 2+ seconds.

# Automatically detected by MIDI Learn
# Manual configuration:
[modes.mappings.trigger]
type = "LongPress"
note = 128  # Button ID (reuses Note type)
duration_ms = 2000

MIDI Learn Mode

MIDI Learn now supports gamepad inputs:

Via GUI:

  1. Open Conductor GUI
  2. Click “Learn” next to any mapping
  3. Press a button or move a stick on your gamepad
  4. Conductor automatically generates the correct trigger config

Pattern Detection:

  • Press button once → GamepadButton
  • Press button twice quickly → DoubleTap
  • Hold button → LongPress
  • Press multiple buttons → GamepadButtonChord
  • Move analog stick → GamepadAnalogStick
  • Pull trigger → GamepadTrigger

Mode Switching

Use button chords for mode switching:

[[global_mappings]]
description = "LB+RB: Switch to Media mode"
[global_mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]  # LB + RB
timeout_ms = 50
[global_mappings.action]
type = "ModeChange"
mode = "Media"

Hybrid MIDI + Gamepad Setup

You can use MIDI controllers and gamepads simultaneously:

[device]
name = "Maschine Mikro MK3"  # MIDI device name
auto_connect = true

# Gamepad is automatically detected
# No additional configuration needed!

# MIDI mappings (button IDs 0-127)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 36  # MIDI note

# Gamepad mappings (button IDs 128-255)
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Gamepad button

Key Points:

  • MIDI uses IDs 0-127
  • Gamepad uses IDs 128-255
  • No conflicts, works seamlessly together

Common Use Cases

Desktop Navigation

[[modes]]
name = "Desktop"
color = "blue"

# Arrow keys on D-Pad
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 132  # D-Pad Up
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

# Copy/Paste on face buttons
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 130  # X button
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

Media Control

[[modes]]
name = "Media"
color = "purple"

# Play/Pause
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # A button
[modes.mappings.action]
type = "Keystroke"
keys = "PlayPause"

# Volume control with triggers
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Right trigger
threshold = 64
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Browser Control

# Navigate with right stick
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130  # Right stick X
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]  # Forward in browser

# Scroll with right stick Y
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 131  # Right stick Y
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "DownArrow"  # Scroll down

Troubleshooting

Gamepad Not Detected

Check connection:

# List connected gamepads
conductorctl status

# Check system recognition
# macOS: System Settings > Game Controllers
# Linux: ls /dev/input/js*
# Windows: Devices and Printers

Solutions:

  1. Reconnect the gamepad
  2. Try USB instead of Bluetooth (or vice versa)
  3. Ensure drivers are installed (Windows)
  4. Check SDL2 compatibility

Buttons Not Working

Verify button mapping:

  1. Use MIDI Learn to discover the actual button ID
  2. Check that button IDs are in the 128-255 range
  3. Ensure no conflicting mappings exist

Common mistakes:

  • Using MIDI note IDs (0-127) instead of gamepad IDs (128-255)
  • Incorrect axis ID for analog sticks
  • Dead zone preventing stick triggers

Analog Stick Too Sensitive

Adjust the dead zone threshold by using button triggers instead:

# Instead of analog stick trigger
# Use button-based threshold
[modes.mappings.trigger]
type = "GamepadButton"
button = 134  # D-Pad left instead of stick

Trigger Not Firing

Check threshold:

  • Threshold too high: Lower the value (try 32, 64, 128)
  • Threshold too low: Increase to prevent false triggers
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 64  # Start with 25% pull

Button Chords Not Detected

Adjust chord timeout:

[advanced_settings]
chord_timeout_ms = 100  # Increase from 50ms

Tips:

  • Press buttons as simultaneously as possible
  • Practice the timing
  • Use MIDI Learn to test detection

Performance Notes

Latency

  • Input latency: <1ms (1000Hz polling)
  • Event processing: <1ms
  • Total latency: ~2-5ms (comparable to MIDI)

Resource Usage

  • CPU: <1% (1ms polling intervals)
  • Memory: ~5-10MB additional
  • Battery: Minimal impact on wireless controllers

Compatibility

  • gilrs v0.10: Industry-standard gamepad library
  • SDL2 mappings: Supports 100+ controller types
  • Auto-detection: Works with most modern gamepads

Best Practices

  1. Start with templates: Use official Xbox/PS/Switch templates
  2. Use MIDI Learn: Let pattern detection configure triggers
  3. Test incrementally: Add mappings one at a time
  4. Document custom configs: Add descriptions to mappings
  5. Use global mappings: Mode switches work everywhere
  6. Backup configs: Save working configurations

Examples

Complete examples available in:

Further Reading


Need Help?

  • GitHub Issues: https://github.com/amiable/conductor/issues
  • Documentation: https://conductor.dev/docs
  • Examples: config/examples/

Per-App Profiles

Automatically switch configurations based on the frontmost application. Use different MIDI mappings for VS Code, Photoshop, Final Cut Pro, etc.

Overview

Per-app profiles enable context-aware MIDI control:

  • Automatic switching: Profiles activate when you switch apps
  • App-specific mappings: Different buttons for different workflows
  • Manual override: Force a specific profile when needed
  • Profile inheritance: Share common mappings across profiles

Getting Started

1. Enable App Detection

# macOS: Grant Accessibility permissions
# System Settings → Privacy & Security → Accessibility → Conductor

# Verify app detection is working
conductorctl frontmost-app

2. Create Profiles

Via GUI

  1. Open Conductor GUI
  2. Navigate to Devices tab
  3. Click 🔄 Profiles
  4. Click + New Profile
  5. Enter profile name (e.g., “vscode-profile”)
  6. Configure mappings
  7. Save profile

Via Config File

Create separate profile files in ~/.config/conductor/profiles/:

~/.config/conductor/profiles/
├── default.toml          # Fallback profile
├── vscode.toml           # VS Code mappings
├── photoshop.toml        # Photoshop mappings
└── fcpx.toml             # Final Cut Pro mappings

3. Register App Associations

Map applications to profiles:

GUI Method

  1. In Profile Manager dialog
  2. Click Associate App
  3. Select profile
  4. Choose application from list
  5. Click Save

Config Method

Edit ~/.config/conductor/app_profiles.toml:

[app_associations]
"Visual Studio Code" = "vscode"
"Code" = "vscode"  # Alternative app name
"Adobe Photoshop" = "photoshop"
"Final Cut Pro" = "fcpx"

Profile Configuration

Profile Structure

Each profile is a complete Conductor configuration:

# profiles/vscode.toml

[device]
name = "Mikro"
auto_connect = true

[[modes]]
name = "Coding"
color = "blue"

# Code navigation mappings
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 36

[modes.mappings.action]
type = "Keystroke"
modifiers = ["Ctrl"]
keys = "P"  # Quick file open

[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 37

[modes.mappings.action]
type = "Keystroke"
modifiers = ["Ctrl", "Shift"]
keys = "F"  # Find in files

# Debug controls
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 38

[modes.mappings.action]
type = "Keystroke"
keys = "F5"  # Start debugging

Shared Mappings

Use global mappings that work across all profiles:

# In each profile, include common mappings
[[global_mappings]]
[global_mappings.trigger]
type = "EncoderTurn"
cc = 7
direction = "Clockwise"

[global_mappings.action]
type = "VolumeControl"
action = "Up"

App Detection

Supported Platforms

  • macOS: Full support via Accessibility API
  • Linux: Full support via X11/Wayland
  • Windows: Full support via Win32 API

App Name Matching

Conductor matches application names flexibly:

# All of these will match Visual Studio Code
"Visual Studio Code" = "vscode"
"Code" = "vscode"
"code" = "vscode"
"VSCode" = "vscode"

Wildcards

Use wildcards for partial matching:

"*Adobe*" = "adobe-suite"  # Matches any Adobe app
"Chrome*" = "browser"       # Matches Chrome variants

Profile Switching

Automatic Switching

When app detection is enabled:

  1. Focus changes to new application
  2. Conductor detects the frontmost app
  3. Looks up associated profile
  4. Loads and activates profile
  5. LED feedback shows profile change (optional)

Switching latency: ~50ms

Manual Override

Force a specific profile:

GUI Method

  1. Click 🔄 Profiles button
  2. Select profile from list
  3. Click Activate

CLI Method

conductorctl switch-profile vscode

Default Fallback

If no profile matches the current app, Conductor uses:

  1. default profile (if exists)
  2. Main config.toml

Use Cases

Software Development

# profiles/dev.toml
# Buttons for common IDE actions

[[modes.mappings]]
# Run tests
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
modifiers = ["Ctrl"]
keys = "T"

[[modes.mappings]]
# Git commit
[modes.mappings.trigger]
type = "Note"
note = 37
[modes.mappings.action]
type = "Shell"
command = "git commit"

[[modes.mappings]]
# Format code
[modes.mappings.trigger]
type = "Note"
note = 38
[modes.mappings.action]
type = "Keystroke"
modifiers = ["Shift", "Alt"]
keys = "F"

Video Editing

# profiles/video.toml
# Timeline control and playback

[[modes.mappings]]
# Play/Pause
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

[[modes.mappings]]
# Cut clip
[modes.mappings.trigger]
type = "Note"
note = 37
[modes.mappings.action]
type = "Keystroke"
modifiers = ["Cmd"]
keys = "B"

[[modes.mappings]]
# Ripple delete
[modes.mappings.trigger]
type = "LongPress"
note = 37
duration_ms = 1000
[modes.mappings.action]
type = "Keystroke"
modifiers = ["Shift"]
keys = "Delete"

Photo Editing

# profiles/photo.toml
# Brush size, zoom, undo/redo

[[modes.mappings]]
# Increase brush size
[modes.mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "]"

[[modes.mappings]]
# Decrease brush size
[modes.mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "["

[[modes.mappings]]
# Undo
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
modifiers = ["Cmd"]
keys = "Z"

Profile Management

Export Profiles

Share or back up profiles:

GUI Method

  1. Profile Manager → Select profile
  2. Click Export
  3. Choose format (TOML/JSON)
  4. Save file

CLI Method

conductorctl export-profile vscode > vscode-profile.toml

Import Profiles

Load profiles from files:

GUI Method

  1. Profile Manager → Import
  2. Select file
  3. Choose name for imported profile
  4. Click Import

CLI Method

conductorctl import-profile vscode-profile.toml

Profile Validation

Test profiles before activating:

conductorctl validate-profile vscode

Troubleshooting

App Not Detected

  1. Check permissions:

    • macOS: Accessibility permissions granted
    • Linux: Running with sufficient privileges
    • Windows: No UAC blocking
  2. Verify app name:

    conductorctl frontmost-app
    
  3. Check association:

    • Ensure app name in config matches actual name
    • Use wildcards if app name varies

Profile Not Switching

  1. Check app detection is enabled:

    conductorctl status
    
  2. Verify profile exists:

    ls ~/.config/conductor/profiles/
    
  3. Test manual switch:

    conductorctl switch-profile vscode
    
  4. Check logs:

    conductorctl logs | grep profile
    

Wrong Profile Activates

  1. Check association priority
  2. Verify no conflicting wildcards
  3. Review app name matching in logs

Best Practices

  1. Start with defaults: Create a base profile with common mappings

  2. Use inheritance: Share global mappings across profiles

  3. Test thoroughly: Verify each profile works in target app

  4. Name clearly: Use descriptive profile names (e.g., “davinci-resolve” not “prof1”)

  5. Document mappings: Add comments in TOML files

  6. Version control: Keep profiles in git for history

  7. Export regularly: Back up working profiles

  8. Share templates: Contribute profiles for popular apps

Advanced Features

Conditional Switching

Switch based on multiple criteria:

[switching_rules]
# Only switch to fcpx profile if specific project is open
[[switching_rules.conditions]]
app = "Final Cut Pro"
window_title = "*MyProject*"
profile = "fcpx-myproject"

Profile Chains

Load multiple profiles in sequence:

[profile_chain]
base = "default"
overlay = "vscode"
# Loads default, then overlays vscode-specific mappings

Hot Reload

Profiles support hot reload:

  1. Edit profile file
  2. Save changes
  3. Switch to different app and back
  4. Profile reloads automatically

Next Steps

LED System

Conductor provides RGB LED feedback for controllers that support it, creating visual indicators for pad presses, modes, and system state.

Overview

The LED system provides:

  • Real-time feedback: LEDs respond instantly to pad presses
  • Mode visualization: Different colors for different modes
  • Multiple schemes: Reactive, rainbow, breathing, and more
  • Velocity sensitivity: LED brightness/color reflects press intensity
  • HID support: Direct RGB control for Maschine Mikro MK3 and similar devices
  • MIDI fallback: Basic on/off for standard MIDI controllers

Supported Devices

Full RGB Support (HID)

  • Native Instruments Maschine Mikro MK3: 16 RGB pads
  • Maschine MK3: 16 RGB pads
  • Other HID RGB devices: Configurable via device profiles

MIDI LED Support

  • Launchpad series: Note-based LED control
  • APC series: CC-based LED control
  • Generic MIDI: Via Note On/Off messages

Lighting Schemes

Reactive (Default)

LEDs respond to pad velocity and fade after release:

  • Soft (0-40): Green
  • Medium (41-80): Yellow
  • Hard (81-127): Red
  • Fade time: 1 second
# Enable reactive mode
conductor --led reactive 2

Rainbow

Rotating rainbow pattern across all pads:

conductor --led rainbow 2

Breathing

Pulsing effect synced across all pads:

conductor --led breathing 2

Wave

Cascading wave pattern:

conductor --led wave 2

Sparkle

Random twinkling effect:

conductor --led sparkle 2

VU Meter

Bottom-to-top audio level visualization:

conductor --led vumeter 2

Static

Solid color based on current mode:

conductor --led static 2

Off

Disable LED feedback:

conductor --led off 2

Configuration

Global LED Settings

In config.toml:

[led_settings]
scheme = "reactive"
brightness = 100  # 0-100
fade_time_ms = 1000
enable_mode_colors = true

Mode-Specific Colors

Each mode can have its own LED color theme:

[[modes]]
name = "Default"
color = "blue"  # LEDs tint blue when in this mode

[[modes]]
name = "Development"
color = "green"

[[modes]]
name = "Media"
color = "purple"

Available colors:

  • blue, green, purple, red, yellow, orange, pink, cyan, white

Per-Mapping LED Feedback

Individual mappings can override LED behavior:

[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 36

[modes.mappings.action]
type = "Keystroke"
keys = "Space"

[modes.mappings.led]
color = "red"
brightness = 80
duration_ms = 500  # Override fade time

HID LED Control (Maschine Mikro MK3)

Direct RGB Access

Conductor uses HID for precise RGB control:

  • Latency: <1ms response time
  • Color depth: 24-bit RGB (16.7M colors)
  • Refresh rate: 60Hz
  • Shared access: Works alongside Native Instruments software

LED Mapping

Physical pad layout to LED indices:

Pad Page A-H (16 pads):
┌────┬────┬────┬────┐
│ 36 │ 37 │ 38 │ 39 │  LED indices 0-3
├────┼────┼────┼────┤
│ 40 │ 41 │ 42 │ 43 │  LED indices 4-7
├────┼────┼────┼────┤
│ 44 │ 45 │ 46 │ 47 │  LED indices 8-11
├────┼────┼────┼────┤
│ 48 │ 49 │ 50 │ 51 │  LED indices 12-15
└────┴────┴────┴────┘

Custom HID Patterns

Advanced users can create custom LED patterns:

// Example: Custom chase pattern
pub fn led_chase_pattern(leds: &mut MikroMK3LEDs, frame: u32) {
    let pos = (frame / 10) % 16;
    for i in 0..16 {
        if i == pos {
            leds.set_pad_rgb(i, 255, 0, 0); // Red
        } else if i == (pos + 1) % 16 {
            leds.set_pad_rgb(i, 128, 0, 0); // Dim red
        } else {
            leds.set_pad_rgb(i, 0, 0, 0); // Off
        }
    }
    leds.update();
}

MIDI LED Control

Standard MIDI Devices

For devices without HID RGB support, Conductor uses MIDI:

[led_settings]
use_midi_leds = true
midi_channel = 1
note_on_velocity = 127  # LED on brightness
note_off_velocity = 0   # LED off

Launchpad-Style Control

Map LED colors to velocity values:

[led_settings.midi_colors]
red = 5
green = 21
yellow = 13
amber = 9
off = 12

Custom MIDI LED Mapping

Define custom LED control messages:

[[led_settings.custom_mappings]]
pad = 36  # MIDI note
led_on = { type = "NoteOn", channel = 1, note = 36, velocity = 127 }
led_off = { type = "NoteOff", channel = 1, note = 36, velocity = 0 }
color_map = { red = 5, green = 21, yellow = 13 }

GUI Configuration

Via Settings Panel

  1. Open Conductor GUI
  2. Navigate to Settings tab
  3. Scroll to LED Configuration
  4. Select scheme from dropdown
  5. Adjust brightness slider
  6. Configure fade time
  7. Enable/disable mode colors
  8. Click Save

Performance Optimization

Reduce LED Updates

For battery-powered devices or performance optimization:

[led_settings]
update_rate_hz = 30  # Default: 60Hz
skip_intermediate_frames = true  # Only update on significant changes

Disable LEDs Selectively

Turn off LEDs for specific modes:

[[modes]]
name = "Silent Mode"
color = "off"  # No LED feedback in this mode

Troubleshooting

LEDs Not Responding

  1. Check device support:

    # List HID devices
    ls /dev/hidraw*  # Linux
    system_profiler SPUSBDataType  # macOS
    
  2. Verify permissions (macOS):

    • System Settings → Privacy & Security → Input Monitoring
    • Grant access to Conductor
  3. Test with diagnostic tool:

    cargo run --bin led_diagnostic
    
  4. Check HID access:

    # macOS: Ensure shared device access
    DEBUG=1 conductor 2 --led reactive
    

Wrong Colors

  1. Verify color mapping in config
  2. Check if device uses non-standard RGB order (some use GRB or BGR)
  3. Try different lighting scheme
  4. Calibrate brightness

Flickering LEDs

  1. Reduce update rate: update_rate_hz = 30
  2. Enable frame skipping: skip_intermediate_frames = true
  3. Check USB power supply
  4. Disable other LED-controlling software

LEDs Stuck On/Off

  1. Restart Conductor
  2. Power cycle MIDI device
  3. Check for conflicting LED control (e.g., Native Instruments software)
  4. Reset LEDs: conductor --led off 2 then restart

Advanced Customization

Create Custom Schemes

Write custom lighting patterns in ~/.config/conductor/led_schemes/:

// custom_pulse.rs
pub struct CustomPulse {
    frame: u32,
}

impl LedScheme for CustomPulse {
    fn update(&mut self, leds: &mut dyn PadFeedback) {
        self.frame += 1;
        let brightness = ((self.frame as f32 / 30.0).sin() * 127.0 + 128.0) as u8;

        for i in 0..16 {
            leds.set_pad_color(i, brightness, brightness, 255);
        }
    }
}

Load custom scheme:

[led_settings]
scheme = "custom:custom_pulse"

Mode Transition Effects

Animate LED transitions when switching modes:

[led_settings.transitions]
enabled = true
duration_ms = 300
effect = "fade"  # fade, sweep, flash

Best Practices

  1. Start with reactive: Most intuitive for new users

  2. Match mode colors to usage: Visual cues help remember mode purpose

  3. Test visibility: Ensure LEDs visible in your lighting conditions

  4. Don’t overdo it: Complex animations can be distracting

  5. Battery consideration: Disable LEDs for battery-powered setups

  6. Accessibility: Use high-contrast colors for visibility

Integration Examples

LED Feedback for Success/Failure

Flash green on success, red on failure:

[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 36

[modes.mappings.action]
type = "Shell"
command = "./run_tests.sh"

[modes.mappings.led]
success_color = "green"
failure_color = "red"
flash_duration_ms = 500

VU Meter for Audio Input

Display audio levels:

conductor --led vumeter --audio-input "System Audio" 2

Custom Mode Indicators

Reserve specific pads for mode indication:

[led_settings.mode_indicators]
pad_36 = "mode_0"  # Blue when in mode 0
pad_37 = "mode_1"  # Green when in mode 1
pad_38 = "mode_2"  # Purple when in mode 2
always_on = true

Next Steps

Event Console

The Live Event Console provides real-time visualization of MIDI events, helping you debug mappings, discover note numbers, and understand your controller’s behavior.

Overview

The Event Console displays:

  • MIDI events: Note on/off, CC, aftertouch, pitch bend
  • Timing information: Timestamps and event duration
  • Velocity data: Press intensity values
  • Processed events: Long press, double-tap, chord detection
  • Action execution: What actions were triggered
  • Error messages: Failed actions or validation issues

Access the Console

GUI Method

  1. Open Conductor GUI
  2. Navigate to Settings tab
  3. Click 📊 Show Event Console
  4. View live events in real-time

CLI Method

# Monitor all events
conductorctl events

# Filter by event type
conductorctl events --type note

# Follow mode (live tail)
conductorctl events --follow

Event Types

MIDI Events

Note On/Off

[12:34:56.789] NOTE_ON  | Note: 36 | Vel: 87 | Ch: 1
[12:34:57.120] NOTE_OFF | Note: 36 | Vel: 0  | Ch: 1
Duration: 331ms

Control Change

[12:34:58.456] CC | CC: 7 | Value: 64 | Ch: 1

Aftertouch

[12:35:00.123] AFTERTOUCH | Pressure: 95 | Ch: 1

Pitch Bend

[12:35:01.789] PITCH_BEND | Value: 8192 | Ch: 1

Processed Events

Long Press Detected

[12:35:05.000] LONG_PRESS | Note: 36 | Duration: 2150ms
Trigger matched: "Emergency Stop" (mapping #5)

Double Tap Detected

[12:35:10.100] DOUBLE_TAP | Note: 37 | Interval: 245ms
Trigger matched: "Quick Launch" (mapping #8)

Chord Detected

[12:35:15.500] CHORD | Notes: [36, 40, 43] | Window: 87ms
Trigger matched: "Save All" (mapping #12)

Encoder Turn

[12:35:20.750] ENCODER_TURN | CC: 1 | Direction: Clockwise | Steps: 3
Trigger matched: "Volume Up" (mapping #2)

Action Execution

[12:35:25.100] ACTION_START | Type: Keystroke
  Modifiers: [Cmd, Shift]
  Keys: "S"
[12:35:25.105] ACTION_COMPLETE | Duration: 5ms | Status: Success

Errors

[12:35:30.000] ERROR | Failed to execute shell command
  Command: "/usr/bin/notexist"
  Error: "No such file or directory"
  Mapping: #15 ("Custom Script")

Filtering Events

By Event Type

# Only note events
conductorctl events --type note

# Only CC events
conductorctl events --type cc

# Only processed events
conductorctl events --type processed

# Only actions
conductorctl events --type action

# Only errors
conductorctl events --type error

By MIDI Channel

conductorctl events --channel 1

By Note Range

# Only pads (notes 36-51)
conductorctl events --note-min 36 --note-max 51

By Time Range

# Last 5 minutes
conductorctl events --since 5m

# Last hour
conductorctl events --since 1h

# Specific time
conductorctl events --since "2025-01-15 12:00"

Use Cases

Discover Note Numbers

Problem: Don’t know which MIDI note a pad sends

Solution:

  1. Open Event Console
  2. Press the pad
  3. See NOTE_ON | Note: 36 in console
  4. Use note 36 in your mapping
[12:40:00.000] NOTE_ON | Note: 36 | Vel: 92 | Ch: 1
                       ↑
                   This is the note number

Debug Long Press Not Triggering

Problem: Long press mapping not activating

Solution:

  1. Open Event Console
  2. Hold the pad
  3. Check if LONG_PRESS event appears
  4. Compare duration with your config:
[12:45:00.000] NOTE_ON    | Note: 36 | Vel: 87
[12:45:01.500] NOTE_OFF   | Note: 36 | Duration: 1500ms
                                      ↑
                        Too short! Config requires 2000ms

Fix by reducing duration_ms in config or holding longer.

Verify Velocity Ranges

Problem: Soft/medium/hard velocity ranges not working as expected

Solution:

  1. Press pad softly: Check velocity value
  2. Press pad medium: Check velocity value
  3. Press pad hard: Check velocity value
  4. Adjust ranges in config
Soft press:   [12:50:00.000] NOTE_ON | Vel: 25  ← Within 0-40
Medium press: [12:50:05.000] NOTE_ON | Vel: 65  ← Within 41-80
Hard press:   [12:50:10.000] NOTE_ON | Vel: 110 ← Within 81-127

Debug Chord Detection

Problem: Chord mapping not triggering

Solution:

  1. Press chord notes
  2. Check timing window in console:
[13:00:00.000] NOTE_ON | Note: 36
[13:00:00.150] NOTE_ON | Note: 40  ← 150ms gap, too slow!
[13:00:00.300] NOTE_ON | Note: 43

Config requires all notes within 100ms

Fix: Press notes faster OR increase chord_timeout_ms in config.

Test Action Execution

Problem: Action not executing as expected

Solution:

  1. Trigger the mapping
  2. Watch action execution in console
  3. Check for errors
[13:05:00.000] ACTION_START | Type: Shell
  Command: "open -a 'Visual Studio Code'"
[13:05:00.250] ACTION_COMPLETE | Duration: 250ms | Status: Success

vs.

[13:05:05.000] ACTION_START | Type: Shell
  Command: "open -a 'NotExist'"
[13:05:05.100] ERROR | Application not found
                ↑
            This tells you what went wrong

Monitor System Performance

Watch event processing latency:

[13:10:00.000] NOTE_ON      | Note: 36
[13:10:00.001] PROCESSED    | Duration: 1ms  ← Very fast!
[13:10:00.002] ACTION_START
[13:10:00.007] ACTION_COMPLETE | Duration: 5ms

Total: 7ms from button press to action complete

If latency >50ms, investigate performance issues.

GUI Console Features

Visual Event Timeline

The GUI console includes a visual timeline showing:

  • Event type (color-coded)
  • Timestamp
  • Event details
  • Expandable for full info

Event Highlighting

  • Green: Successful events
  • Yellow: Warnings (e.g., near timeout)
  • Red: Errors
  • Blue: Info (e.g., mode changes)

Auto-Scroll

Console auto-scrolls to latest events. Click Pause to freeze for inspection.

Event Export

  1. Select events
  2. Click Export
  3. Save as JSON/CSV for analysis

Event Statistics

View live statistics:

  • Events per second
  • Average velocity
  • Most-pressed pad
  • Error rate

Advanced Features

Pattern Recording

Record event sequences for analysis:

conductorctl record-events --duration 30s --output session.json

Playback recorded events:

conductorctl playback-events session.json

Event Filtering Rules

Create custom filters in config:

[event_console]
[[event_console.filters]]
name = "Only My Pads"
include_notes = [36, 37, 38, 39]
include_types = ["note", "processed"]

[[event_console.filters]]
name = "Errors Only"
include_types = ["error"]
min_severity = "warning"

Activate filter in GUI or CLI:

conductorctl events --filter "Only My Pads"

Event Triggers

Trigger actions based on events:

[event_console.triggers]
[[event_console.triggers.rules]]
condition = "error_rate > 5 per_minute"
action = { type = "Notification", message = "High error rate detected!" }

[[event_console.triggers.rules]]
condition = "note_36 pressed > 10 times in 5s"
action = { type = "Notification", message = "Stop mashing that button!" }

Performance Profiling

Enable detailed performance metrics:

[event_console]
enable_profiling = true
track_latency = true
track_memory = true

View profiling data:

conductorctl events --profiling

Output includes:

  • Event processing time
  • Memory allocation
  • CPU usage per event
  • Bottleneck identification

Troubleshooting

Console Not Updating

  1. Check daemon is running: conductorctl status
  2. Verify events are being generated (press pads)
  3. Restart daemon: conductorctl restart
  4. Check event monitoring is active: conductorctl is-event-monitoring-active

Too Many Events

Filter aggressively:

# Only errors and warnings
conductorctl events --type error --type warning

# Debounce rapid events
conductorctl events --debounce 100ms

Or reduce update rate in config:

[event_console]
max_events_per_second = 30

Missing Events

  1. Check buffer size isn’t full:

    [event_console]
    buffer_size = 10000  # Increase if needed
    
  2. Verify event types are enabled:

    [event_console]
    capture_midi = true
    capture_processed = true
    capture_actions = true
    
  3. Check log level:

    DEBUG=1 conductorctl events
    

Best Practices

  1. Start with full view: See all event types initially
  2. Filter progressively: Add filters as you narrow down issues
  3. Use follow mode: -f flag for live monitoring
  4. Export for analysis: Save interesting sessions
  5. Watch timing: Event timing reveals latency issues
  6. Compare configs: Record events, change config, compare results
  7. Document discoveries: Note note numbers and velocities for future reference

Integration with Other Tools

Export to CSV for Analysis

conductorctl events --output events.csv --duration 60s

Analyze in Excel/Google Sheets:

  • Event frequency
  • Velocity distribution
  • Timing patterns

Send to External Monitoring

# Stream events to external tool
conductorctl events --format json | jq '.' | your-monitoring-tool

Log to File

# Continuous logging
conductorctl events -f >> conductor-events.log

Next Steps

Customizing Velocity Response

Learn how to control the relationship between how hard you hit a pad and what action is triggered using velocity mappings.


Overview

Velocity mappings allow you to transform the input velocity (how hard you press a pad) into a different output velocity. This is useful for:

  • Normalizing velocity: Make soft and hard hits more consistent
  • Amplifying dynamics: Make subtle differences more pronounced
  • Creating custom response curves: Match your playing style

Velocity Mapping Types

Conductor supports four velocity mapping types:

1. Fixed

Output is always the same velocity, regardless of input

[velocity_mapping]
type = "Fixed"
velocity = 100  # Always outputs velocity 100 (0-127)

Use Cases:

  • Trigger actions that don’t need velocity variation
  • Ensure consistent behavior regardless of how hard you hit
  • Simplify testing and debugging

Example: Launch an application with a single tap (any velocity)


2. PassThrough

Output velocity = input velocity (1:1 mapping)

velocity_mapping = "PassThrough"

Use Cases:

  • Natural, unmodified velocity response
  • When you want direct MIDI pass-through
  • Default behavior when no mapping specified

Example: Control DAW volume with natural dynamics


3. Linear

Maps full input range (0-127) to custom output range

[velocity_mapping]
type = "Linear"
min = 40   # Minimum output velocity
max = 110  # Maximum output velocity

Behavior:

  • Input 0 → Output min
  • Input 127 → Output max
  • Values between are scaled proportionally

Use Cases:

  • Compress dynamic range (e.g., 0-127 → 60-100)
  • Expand subtle playing into wider range (e.g., 0-127 → 20-127)
  • Shift velocity range up or down

Example: Make soft hits louder while preventing excessively loud hits

min = 50   # Softest hit = 50 (instead of 0)
max = 110  # Hardest hit = 110 (instead of 127)

4. Curve

Applies non-linear transformation with adjustable intensity

[velocity_mapping]
type = "Curve"
curve_type = "Exponential"  # or "Logarithmic" or "SCurve"
intensity = 0.7             # 0.0 = linear, 1.0 = maximum effect

Curve Types

Exponential (output = input^(1-intensity)):

  • Makes soft hits louder while preserving hard hits
  • Higher intensity = more compression of soft notes
  • Great for making subtle playing more audible

Example:

curve_type = "Exponential"
intensity = 0.8  # Boost soft hits significantly

Logarithmic (output = log(1 + intensity × input)):

  • Compresses dynamic range
  • Makes loud hits quieter relative to soft hits
  • Useful for taming aggressive playing

Example:

curve_type = "Logarithmic"
intensity = 0.6  # Moderate compression

S-Curve (Sigmoid function):

  • Smooth acceleration in middle range
  • Soft and hard extremes less affected
  • Natural-feeling response with “sweet spot”

Example:

curve_type = "SCurve"
intensity = 0.5  # Balanced S-curve

GUI Configuration

The GUI provides a visual velocity curve editor with real-time preview:

  1. Open a mapping in the Mappings view
  2. Locate Velocity Mapping section
  3. Select mapping type from dropdown:
    • Fixed
    • Pass-Through
    • Linear
    • Curve
  4. Adjust parameters:
    • Fixed: Set velocity (0-127)
    • Linear: Set min/max range
    • Curve: Choose curve type and intensity (0.0-1.0)
  5. Preview curve in the graph:
    • X-axis: Input velocity (0-127)
    • Y-axis: Output velocity (0-127)
    • Diagonal line: 1:1 reference (Pass-Through)
    • Colored line: Your configured curve

The preview updates in real-time as you adjust parameters, allowing you to dial in the perfect response.


Practical Examples

Example 1: Consistent Application Launch

Make any tap launch an app, regardless of velocity:

[[modes.mappings]]
description = "Launch browser (any velocity)"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.velocity_mapping]
type = "Fixed"
velocity = 100  # Doesn't matter for Launch action

[modes.mappings.action]
type = "Launch"
app = "Safari"

Example 2: Gentle Volume Control

Compress volume adjustments for smoother control:

[[modes.mappings]]
description = "Gentle volume up"

[modes.mappings.trigger]
type = "Note"
note = 10

[modes.mappings.velocity_mapping]
type = "Linear"
min = 60   # Even softest tap has impact
max = 100  # Prevent ear-splitting jumps

[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Example 3: Expressive MIDI Control

Boost soft playing for more expressive DAW control:

[[modes.mappings]]
description = "Expressive note trigger"

[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7  # Make soft notes more audible

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "NoteOn"
channel = 0
note = 60
# Velocity is passed from the mapped input velocity

Example 4: S-Curve for Natural Feel

Create a responsive curve with smooth acceleration:

[[modes.mappings]]
description = "Natural dynamics"

[modes.mappings.trigger]
type = "Note"
note = 3

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "SCurve"
intensity = 0.5  # Balanced response

[modes.mappings.action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "CC"
channel = 0
controller = 7  # Volume CC
# CC value derived from mapped velocity

Tips & Best Practices

Finding Your Curve

  1. Start with PassThrough and test your natural playing
  2. Identify issues:
    • Soft hits not registering? → Try Exponential curve
    • Too much variation? → Try Linear compression
    • Need smoothing? → Try S-Curve
  3. Adjust intensity gradually (0.3 → 0.5 → 0.7)
  4. Use the preview graph to visualize the transformation

Per-Action Tuning

Different actions may need different curves:

  • Launch/Shell: Fixed (velocity doesn’t matter)
  • Volume Control: Linear compression (0-127 → 60-100)
  • MIDI Output: Curve (Exponential for expression, Logarithmic for drums)
  • Keystroke: Usually PassThrough or Fixed

Testing

After configuring a velocity mapping:

  1. Save your config (hot-reload applies changes)
  2. Test range: Hit the pad softly, medium, hard
  3. Check preview graph: Does curve match your intent?
  4. Iterate: Adjust intensity or type as needed

Advanced: Velocity-Sensitive Actions

Combine velocity mappings with velocity-sensitive triggers:

[[modes.mappings]]
description = "Soft hit = text, hard hit = keystroke"

[modes.mappings.trigger]
type = "VelocityRange"
note = 7
soft_max = 40
medium_max = 80

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.6  # Emphasize differences

[modes.mappings.action]
type = "VelocityRange"
soft_action = { type = "Text", text = "Hello" }
medium_action = { type = "Keystroke", keys = "space", modifiers = [] }
hard_action = { type = "Launch", app = "Terminal" }

The velocity mapping transforms the input before the VelocityRange thresholds are applied.


Mathematical Details

For those interested in the underlying transformations:

Exponential:

output = input^(1 - intensity)
  • intensity = 0 → linear (no change)
  • intensity = 1 → maximum compression (all inputs → 127)

Logarithmic:

normalized_input = input / 127
output = log(1 + intensity × normalized_input) / log(1 + intensity) × 127
  • intensity = 0 → linear
  • intensity = 1 → maximum compression

S-Curve (Sigmoid):

normalized_input = input / 127
k = intensity × 10 + 0.5  # Steepness factor
sigmoid = 1 / (1 + e^(-k × (normalized_input - 0.5)))
output = normalize(sigmoid) × 127
  • Lower intensity → gentler curve
  • Higher intensity → sharper transition in midrange


Next: Learn about Context-Aware Mappings to make your velocity curves change based on time, app, or mode.

Context-Aware Mappings

Make your MIDI controller adapt to your workflow by executing different actions based on time, active application, current mode, or day of week.


Overview

Context-aware mappings use conditional actions to change behavior dynamically. Instead of always doing the same thing, your controller can:

  • Switch profiles based on work hours vs evening
  • Route commands to the frontmost application
  • Execute different actions on weekdays vs weekends
  • Adapt to the current mode

This eliminates the need to manually switch profiles throughout the day.


Basic Conditional Structure

[[modes.mappings]]
description = "Context-aware action"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.action]
type = "Conditional"
condition = { /* condition goes here */ }
then_action = { /* action if condition is true */ }
else_action = { /* optional action if condition is false */ }

Condition Types

1. Always

Always executes the then_action

[modes.mappings.action]
type = "Conditional"
condition = "Always"
then_action = { type = "Keystroke", keys = "space", modifiers = [] }

Use Case: Testing, default behavior, or when you just want the then_action wrapper.


2. Never

Never executes the then_action (effectively disables the mapping)

[modes.mappings.action]
type = "Conditional"
condition = "Never"
then_action = { type = "Launch", app = "Disabled App" }

Use Case: Temporarily disable a mapping without deleting it.


3. TimeRange

Executes only during specific hours (24-hour format)

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "TimeRange"
start = "09:00"
end = "17:00"

[modes.mappings.action.then_action]
type = "Launch"
app = "Slack"

[modes.mappings.action.else_action]
type = "Text"
text = "Outside work hours"

Features:

  • Automatically handles ranges crossing midnight (e.g., 22:00 to 06:00)
  • Time is checked when action is triggered (not when config is loaded)

Use Cases:

  • Work mode (9am-5pm): Launch productivity apps
  • Evening mode (5pm-11pm): Launch entertainment apps
  • Night mode (11pm-9am): Disable noisy actions

4. DayOfWeek

Executes only on specific days

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "DayOfWeek"
days = [1, 2, 3, 4, 5]  # Monday=1 through Sunday=7

[modes.mappings.action.then_action]
type = "Shell"
command = "open ~/Work"

[modes.mappings.action.else_action]
type = "Shell"
command = "open ~/Personal"

Day Numbers:

  • 1 = Monday
  • 2 = Tuesday
  • 3 = Wednesday
  • 4 = Thursday
  • 5 = Friday
  • 6 = Saturday
  • 7 = Sunday

Use Cases:

  • Weekday-only shortcuts (work apps)
  • Weekend-only shortcuts (gaming, hobbies)
  • Different behaviors for different days

5. AppRunning

Checks if a specific application is currently running

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppRunning"
app_name = "Logic Pro"

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "space"
modifiers = []  # Play/pause in Logic

[modes.mappings.action.else_action]
type = "Launch"
app = "Logic Pro"

Platform Support:

  • ✅ macOS: Uses pgrep (case-insensitive)
  • ✅ Linux: Uses pgrep (case-insensitive)
  • ❌ Windows: Not yet supported

Use Cases:

  • Smart play/pause (launch DAW if not running, play/pause if running)
  • Toggle between apps (if browser running, open it; else launch IDE)
  • Conditional workflows based on running processes

Note: Uses partial string matching. "Chrome" matches "Google Chrome Helper".


6. AppFrontmost

Checks if a specific application has focus (active window)

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppFrontmost"
app_name = "Safari"

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]  # New tab in Safari

[modes.mappings.action.else_action]
type = "Text"
text = "Not in Safari"

Platform Support:

  • ✅ macOS: Uses NSWorkspace API
  • ❌ Linux: Not yet supported
  • ❌ Windows: Not yet supported

Use Cases:

  • App-specific shortcuts (browser shortcuts vs IDE shortcuts)
  • Context-switching workflows
  • Smart key remapping based on frontmost app

Note: Checks the actual frontmost application, not just if it’s running.


7. ModeIs

Checks if the current mode matches

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "ModeIs"
mode = "Development"

[modes.mappings.action.then_action]
type = "Shell"
command = "git status"

[modes.mappings.action.else_action]
type = "Text"
text = "Switch to Development mode first"

Use Cases:

  • Mode-specific behaviors within global mappings
  • Validate mode before executing sensitive commands
  • Different actions for different modes on same pad

Note: Mode name must match exactly (case-sensitive).


8. And (Logical AND)

All sub-conditions must be true

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] },
  { type = "AppRunning", app_name = "Slack" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "s"
modifiers = ["cmd", "shift"]  # Search in Slack during work hours on weekdays

Use Cases:

  • Work mode: weekdays AND business hours AND specific app
  • Complex conditions requiring multiple criteria

9. Or (Logical OR)

At least one sub-condition must be true

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Safari" },
  { type = "AppFrontmost", app_name = "Chrome" },
  { type = "AppFrontmost", app_name = "Firefox" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]  # New tab in any browser

Use Cases:

  • Action applies to multiple apps
  • Alternative conditions (weekend OR evening)

10. Not (Logical NOT)

Inverts the result of a condition

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Not"
condition = { type = "AppRunning", app_name = "Music" }

[modes.mappings.action.then_action]
type = "Launch"
app = "Music"

[modes.mappings.action.else_action]
type = "Text"
text = "Music already running"

Use Cases:

  • “If NOT running, launch”
  • “If NOT work hours, disable”
  • Invert any condition logic

GUI Configuration

The Conditional Action Editor in the GUI provides:

  1. Condition Type Selector: Dropdown with descriptions
  2. Time Pickers: Visual time selection for TimeRange
  3. Day Toggles: Click days for DayOfWeek conditions
  4. Text Inputs: For app names (AppRunning, AppFrontmost)
  5. Mode Dropdown: Auto-populated from available modes (ModeIs)
  6. Logical Operators: Add/remove sub-conditions for And/Or
    • Simple (non-nested) And/Or/Not supported
    • Complex nested logic requires TOML editing
  7. Then/Else Actions: Full ActionSelector for both branches
  8. Optional Else: Toggle to add/remove else_action

To Configure:

  1. Select action type = “Conditional”
  2. Choose condition type
  3. Fill in condition parameters
  4. Configure then_action
  5. (Optional) Enable else_action and configure
  6. Save

Practical Examples

Example 1: Work Hours Profile

Different behavior during work hours vs personal time:

[[modes.mappings]]
description = "Smart launcher - Slack at work, Discord after hours"

[modes.mappings.trigger]
type = "Note"
note = 8

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] }
]

[modes.mappings.action.then_action]
type = "Launch"
app = "Slack"

[modes.mappings.action.else_action]
type = "Launch"
app = "Discord"

Example 2: App-Aware Play/Pause

Different shortcuts for different media apps:

[[modes.mappings]]
description = "Universal play/pause"

[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Spotify" },
  { type = "AppFrontmost", app_name = "Music" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "space"
modifiers = []

[modes.mappings.action.else_action]
type = "Conditional"
condition = { type = "AppFrontmost", app_name = "Logic Pro" }
then_action = { type = "Keystroke", keys = "Return", modifiers = [] }
else_action = { type = "VolumeControl", operation = "Mute" }

Nested conditionals: Spotify/Music → space, Logic → Return, else → Mute


Example 3: Weekend Gaming Mode

Enable gaming shortcuts only on weekends:

[[modes.mappings]]
description = "Discord overlay (weekends only)"

[modes.mappings.trigger]
type = "Note"
note = 15

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "DayOfWeek"
days = [6, 7]  # Saturday, Sunday

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "`"
modifiers = ["shift", "cmd"]

# No else_action (does nothing on weekdays)

Example 4: Smart DAW Control

Launch DAW if not running, otherwise send MIDI:

[[modes.mappings]]
description = "Launch or control Logic Pro"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppRunning"
app_name = "Logic Pro"

[modes.mappings.action.then_action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "NoteOn"
channel = 0
note = 60
velocity = 100

[modes.mappings.action.else_action]
type = "Launch"
app = "Logic Pro"

Advanced: Nested Conditions

Combine multiple logical operators for complex logic:

[[modes.mappings]]
description = "Complex work mode: (weekday AND work hours) OR (weekend AND app running)"

[modes.mappings.trigger]
type = "Note"
note = 10

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  {
    type = "And"
    conditions = [
      { type = "DayOfWeek", days = [1, 2, 3, 4, 5] },
      { type = "TimeRange", start = "09:00", end = "17:00" }
    ]
  },
  {
    type = "And"
    conditions = [
      { type = "DayOfWeek", days = [6, 7] },
      { type = "AppRunning", app_name = "Xcode" }
    ]
  }
]

[modes.mappings.action.then_action]
type = "Shell"
command = "open ~/Code"

Translation: Open code folder if:

  • (Monday-Friday AND 9am-5pm) OR
  • (Saturday-Sunday AND Xcode is running)

Tips & Best Practices

Start Simple

Begin with single conditions:

  1. Test TimeRange alone
  2. Test AppRunning alone
  3. Combine with And/Or once individual conditions work

Use Descriptive Mappings

description = "Slack during work (Mon-Fri 9-5), Discord otherwise"

Clear descriptions help when debugging.

Test Edge Cases

  • Midnight crossing (TimeRange: 22:00 - 02:00)
  • Day transitions (Saturday → Sunday at midnight)
  • App name variations (“Chrome” vs “Google Chrome”)

Combine with Velocity Mappings

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7

[modes.mappings.action]
type = "Conditional"
# Velocity mapping applies before condition evaluation

Global vs Mode-Specific

  • Global mappings: Use ModeIs condition to adapt to current mode
  • Mode mappings: Already scoped to mode, use other conditions

Performance

Conditions are evaluated each time the trigger fires. Keep deeply nested conditions reasonable (<5 levels deep).


Troubleshooting

Condition Never True

Problem: Action never executes

Solutions:

  1. Check condition type matches intended logic
  2. Verify app name matches process name (ps aux | grep <app>)
  3. Test time format (must be HH:MM, 24-hour)
  4. Check day numbers (Monday=1, not 0)
  5. Add debug else_action to confirm condition is evaluating

Wrong Action Executes

Problem: else_action runs instead of then_action

Solutions:

  1. Verify condition logic (And vs Or)
  2. Check app name capitalization (case-sensitive on some platforms)
  3. Test each sub-condition individually
  4. Add type = "Text" debug actions to see which path executes

AppFrontmost Not Working

Problem: Condition always false

Solutions:

  1. Verify platform support (macOS only currently)
  2. Check app name matches bundle name (use Activity Monitor)
  3. Try partial name (“Safari” instead of “com.apple.Safari”)


Next: Try the Dynamic Workflows Tutorial for a hands-on example combining conditionals and velocity curves.

DAW Control with Conductor

Control your Digital Audio Workstation (DAW) directly from your MIDI controller using Conductor’s SendMIDI action. Transform your Maschine Mikro MK3 or other MIDI controller into a custom control surface for transport control, mixer automation, and parameter manipulation.


Overview

Conductor can send MIDI messages to your DAW, allowing you to:

  • Transport Control: Play, Stop, Record, Rewind, Fast Forward
  • Mixer Control: Volume faders (CC 7), Pan controls (CC 10), Mute/Solo
  • Parameter Automation: Plugin parameters, effect sends, EQ controls
  • MIDI Learn: Map any pad to any DAW parameter using MIDI Learn
  • Scene/Clip Triggering: Launch clips, scenes, or patterns in Live/Bitwig

This enables you to create custom control surfaces tailored to your workflow, without needing hardware-specific DAW integration.


How It Works

[Your MIDI Controller]
        ↓ (MIDI Input to Conductor)
  [Conductor Daemon]
        ↓ (SendMIDI Action)
  [Virtual MIDI Port] (IAC Driver, loopMIDI, ALSA)
        ↓ (MIDI from Conductor)
    [Your DAW] (Logic Pro, Ableton Live, etc.)

Key Components:

  1. Input: Conductor receives MIDI from your controller (pads, encoders, buttons)
  2. Mapping: Conductor maps input events to SendMIDI actions
  3. Output: SendMIDI sends MIDI messages to a virtual MIDI port
  4. DAW: Your DAW receives MIDI from the virtual port and responds

Platform Setup

Before using SendMIDI, you need a virtual MIDI port that Conductor can send to and your DAW can receive from.

macOS: IAC Driver

macOS includes a built-in virtual MIDI driver called IAC (Inter-Application Communication).

Setup Steps:

  1. Open Audio MIDI Setup application

    • Found in /Applications/Utilities/Audio MIDI Setup.app
    • Or press Cmd+Space and search “Audio MIDI Setup”
  2. Show MIDI Studio

    • Go to WindowShow MIDI Studio (or press Cmd+2)
  3. Double-click IAC Driver icon

    • If you don’t see it, create it: WindowShow MIDI Studio → click the globe icon
  4. Enable the driver

    • Check “Device is online” checkbox
    • You should see at least one port named “IAC Driver Bus 1”
  5. (Optional) Add more ports

    • Click the + button under “Ports” to create additional buses
    • Rename buses to something meaningful (e.g., “Conductor → Logic Pro”)
  6. Click Apply

Verify IAC Driver is Working:

# List MIDI output ports (Conductor should see IAC Driver)
./target/release/conductor-daemon --list-midi-outputs

# You should see:
# MIDI Output Ports:
# 0: IAC Driver Bus 1

Troubleshooting:

  • If IAC Driver doesn’t appear: Restart Audio MIDI Setup or reboot macOS
  • If port disappears after reboot: Make sure “Device is online” was checked and saved

Windows: loopMIDI

Windows doesn’t include built-in virtual MIDI ports, so we use loopMIDI (free, open-source).

Download & Install:

  1. Visit Tobias Erichsen’s loopMIDI page
  2. Download the installer (free, no registration required)
  3. Run the installer (requires Administrator privileges)
  4. Launch loopMIDI from Start Menu

Create Virtual Port:

  1. In loopMIDI window, enter a port name (e.g., “Conductor Virtual Out”)
  2. Click + (Plus button) to create the port
  3. The port should appear in the list with status “Opened by 0 applications”

Keep loopMIDI Running:

  • loopMIDI must be running for the virtual port to exist
  • To auto-start with Windows: Right-click loopMIDI in system tray → “Start minimized with Windows”

Verify loopMIDI Port:

# List MIDI output ports
.\target\release\conductor-daemon.exe --list-midi-outputs

# You should see:
# MIDI Output Ports:
# 0: Conductor Virtual Out

Troubleshooting:

  • Port not found: Make sure loopMIDI is running and port is created
  • Port opens but no MIDI received in DAW: Check DAW MIDI input preferences
  • loopMIDI crashes: Try creating port with simpler name (no special characters)

Linux: ALSA Virtual Port

Linux supports virtual MIDI ports through ALSA (Advanced Linux Sound Architecture).

Create Virtual Port with aconnect:

# Method 1: Using Conductor's built-in virtual port creation (recommended)
# Conductor can create virtual ports automatically on Linux via midir

# Method 2: Manual ALSA virtual port (if needed)
# Install ALSA utilities
sudo apt-get install alsa-utils

# Create a virtual port named "Conductor Output"
# This command creates a port that stays active
aseqdump -p "Conductor Output" &

# List ALSA MIDI ports
aconnect -l

# You should see something like:
# client 128: 'Conductor Output' [type=user]
#     0 'Conductor Output'

Using JACK for MIDI (Alternative):

If you use JACK for audio:

# Install JACK MIDI tools
sudo apt-get install qjackctl

# Start JACK
qjackctl &

# In QjackCtl:
# - Go to "Graph" or "Patchbay"
# - Create MIDI connections between Conductor and your DAW

Verify ALSA Port:

# List MIDI output ports
./target/release/conductor-daemon --list-midi-outputs

# You should see your created virtual port

Troubleshooting:

  • No ALSA ports: sudo modprobe snd-seq (load ALSA sequencer module)
  • Permission denied: Add user to audio group: sudo usermod -a -G audio $USER (logout/login after)
  • JACK vs ALSA confusion: Choose one system (ALSA is simpler for most users)

Conductor Configuration

Basic SendMIDI Example

Here’s a minimal configuration that sends a MIDI Note On message when you press pad 1:

# config.toml

[[modes]]
name = "Default"

# Send MIDI Note On (C4 / note 60) when pad 1 is pressed
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1  # Pad 1 on Maschine Mikro MK3

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"  # macOS (use your port name)
# port = "Conductor Virtual Out"  # Windows (loopMIDI)
# port = "Conductor Output"  # Linux (ALSA)

[modes.mappings.action.message]
type = "NoteOn"
note = 60  # MIDI note 60 = C4 (Middle C)
velocity = 100
channel = 0  # MIDI channel 1 (0-indexed)

Test This Configuration:

  1. Save the config above to your config.toml
  2. Restart Conductor daemon: conductorctl reload
  3. Open your DAW and create a software instrument track
  4. Set the track’s MIDI input to your virtual port (IAC Driver/loopMIDI)
  5. Enable MIDI input recording on the track
  6. Press pad 1 on your controller
  7. You should hear the instrument play note C4!

MIDI Message Types

Conductor supports all common MIDI message types. Here’s a reference for each:

1. Note On / Note Off

Trigger notes in software instruments.

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

# Note On: Start playing a note
[modes.mappings.action.message]
type = "NoteOn"
note = 60        # MIDI note number (0-127, 60 = C4)
velocity = 100   # How hard the note is played (0-127)
channel = 0      # MIDI channel (0-15, displays as 1-16 in DAWs)
# Note Off: Stop playing a note
[modes.mappings.action.message]
type = "NoteOff"
note = 60        # Same note number as the Note On
velocity = 64    # Release velocity (usually 0 or 64)
channel = 0

Common Note Numbers:

  • C3 (48), C4 (60), C5 (72) - Middle octaves
  • A0 (21) - Lowest note on 88-key piano
  • C8 (108) - Highest note on 88-key piano

Use Cases:

  • Trigger drum samples in drum racks
  • Play melodies on virtual instruments
  • Trigger clips in Ableton Live’s Session View

2. Control Change (CC)

Control parameters like volume, pan, filters, and effects.

[modes.mappings.action.message]
type = "CC"
controller = 7   # Controller number (0-127)
value = 100      # Controller value (0-127)
channel = 0

Common CC Numbers:

CC#NamePurposeRange
1Modulation WheelVibrato, tremolo0-127
7VolumeTrack/channel volume0-127 (100=max)
10PanLeft/right panning0=left, 64=center, 127=right
11ExpressionVolume changes within a note0-127
64Sustain PedalHold notes0-63=off, 64-127=on
71Filter ResonanceSynth filter resonance0-127
74Filter CutoffSynth filter cutoff freq0-127

Example: Volume Control

# Soft velocity = low volume (CC 7 = 40)
[[modes.mappings]]
[modes.mappings.trigger]
type = "VelocityRange"
note = 1
min_velocity = 0
max_velocity = 40

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.message]
type = "CC"
controller = 7
value = 40
channel = 0

3. Program Change

Switch between presets/patches in instruments or effects.

[modes.mappings.action.message]
type = "ProgramChange"
program = 42     # Program number (0-127)
channel = 0

Use Cases:

  • Switch guitar amp presets
  • Change synthesizer patches
  • Load different drum kits

Note: Program Change numbers are 0-indexed in MIDI (0-127) but often displayed as 1-128 in DAWs.


4. Pitch Bend

Bend notes up or down (like a pitch wheel).

[modes.mappings.action.message]
type = "PitchBend"
value = 0        # Pitch bend value (-8192 to +8191)
                 # 0 = center (no bend)
                 # +8191 = max up
                 # -8192 = max down
channel = 0

Examples:

# Pitch up (full bend up)
value = 8191

# Center position (no bend)
value = 0

# Pitch down (full bend down)
value = -8192

Use Cases:

  • Guitar bends in virtual guitars
  • Synth lead pitch slides
  • Trombone-style glissando effects

5. Aftertouch (Channel Pressure)

Apply pressure-based modulation to all notes on a channel.

[modes.mappings.action.message]
type = "Aftertouch"
pressure = 64    # Pressure amount (0-127)
channel = 0

Use Cases:

  • Vibrato after note starts
  • Filter modulation with sustained notes
  • Expressive synth control

Note: This is Channel Aftertouch (affects all notes). MIDI also supports Polyphonic Aftertouch (per-note), but it’s rarely used and not currently supported by SendMIDI.


Common Use Cases

Transport Control

Control DAW playback using MIDI CC messages. Most DAWs support MMC (MIDI Machine Control) or specific CC numbers for transport.

Logic Pro Transport (using CC messages):

# Play/Pause (CC 115)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.message]
type = "CC"
controller = 115  # Play/Continue
value = 127
channel = 0

# Stop (CC 116)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 2

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.message]
type = "CC"
controller = 116  # Stop
value = 127
channel = 0

# Record (CC 117)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 3

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.message]
type = "CC"
controller = 117  # Record
value = 127
channel = 0

Ableton Live Transport (using Note messages):

Ableton responds to specific MIDI notes for transport in some modes:

# Play (Note 91 on Channel 1)
[modes.mappings.action.message]
type = "NoteOn"
note = 91
velocity = 127
channel = 0

# Stop (Note 93)
[modes.mappings.action.message]
type = "NoteOn"
note = 93
velocity = 127
channel = 0

Note: Actual values depend on your DAW’s MIDI remote script. Check your DAW’s documentation or use MIDI Learn.


Mixer Control (Volume & Pan)

Control track volume and panning using CC 7 (Volume) and CC 10 (Pan).

Volume Fader with Velocity:

# Map pad velocity to track volume
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.message]
type = "CC"
controller = 7    # Volume
value = 100       # Will be overridden by velocity mapping
channel = 0       # Track 1

# Use velocity mapping to scale velocity to volume
[modes.mappings.action.velocity_mapping]
type = "Linear"
min = 0           # Soft hit = volume 0
max = 127         # Hard hit = volume 127

Pan Control:

# Encoder left/right = pan left/right
[[modes.mappings]]
[modes.mappings.trigger]
type = "EncoderTurn"
encoder = 1
direction = "Clockwise"

[modes.mappings.action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.message]
type = "CC"
controller = 10   # Pan
value = 127       # Full right
channel = 0

MIDI Learn in Your DAW

Most DAWs support MIDI Learn - a feature that lets you map any incoming MIDI message to any parameter by clicking the parameter and moving your controller.

General MIDI Learn Workflow:

  1. Configure Conductor to send a MIDI message when you press a pad:

    [[modes.mappings]]
    [modes.mappings.trigger]
    type = "Note"
    note = 1
    
    [modes.mappings.action]
    type = "SendMIDI"
    port = "IAC Driver Bus 1"
    
    [modes.mappings.action.message]
    type = "CC"
    controller = 20  # Arbitrary CC number
    value = 127
    channel = 0
    
  2. In your DAW:

    • Enable MIDI Learn mode (varies by DAW)
    • Click the parameter you want to control (e.g., filter cutoff)
    • Press your mapped pad
    • DAW learns the association (CC 20 → filter cutoff)
    • Exit MIDI Learn mode
  3. Test: Press the pad again - the parameter should move!

DAW-Specific MIDI Learn:

  • Logic Pro: Cmd+L → Click parameter → Move controller → Cmd+L to exit
  • Ableton Live: Click “MIDI” button in top right → Click parameter → Move controller → Click “MIDI” again
  • Reaper: Right-click parameter → “Learn” → Move controller
  • FL Studio: Right-click parameter → “Link to controller” → Move controller

Combining with Conditionals

Use Conditional actions to send different MIDI messages based on context (time, active app, mode).

Example: Different Transport Controls for Different DAWs

[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.action]
type = "Conditional"

# If Logic Pro is frontmost, send CC 115 (Play)
[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"
message = { type = "CC", controller = 115, value = 127, channel = 0 }

# Otherwise (Ableton), send Note 91
[modes.mappings.action.else_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"
message = { type = "NoteOn", note = 91, velocity = 127, channel = 0 }

Troubleshooting

MIDI Messages Not Received in DAW

Check Conductor is sending:

# Enable debug logging
DEBUG=1 conductorctl reload

# Press your mapped pad
# You should see: "Sending MIDI: [0x90, 0x3C, 0x64] to port 'IAC Driver Bus 1'"

Check DAW MIDI input settings:

  1. Open DAW preferences/settings
  2. Go to MIDI Input settings
  3. Ensure your virtual port is enabled:
    • Logic Pro: PreferencesMIDIInputs → Enable “IAC Driver Bus 1”
    • Ableton Live: PreferencesLink/Tempo/MIDIMIDI Ports → Enable “Track” and “Remote” for IAC Driver
    • Reaper: PreferencesMIDI Devices → Enable input for virtual port

Check track MIDI input:

  • Make sure the track is set to receive MIDI from your virtual port
  • Enable “Input Monitoring” or “Record Enable” on the track

High Latency / Delayed Response

Symptoms: MIDI messages arrive 50-500ms late

Causes:

  1. DAW Buffer Size: Larger audio buffers = more latency

    • Solution: Reduce buffer size in DAW audio preferences (e.g., 128 or 256 samples)
  2. MIDI Port Polling: Some virtual MIDI implementations poll slowly

    • Solution: Restart DAW and Conductor daemon
  3. System Load: High CPU usage delays MIDI processing

    • Solution: Close unnecessary applications

Test Latency:

# Send test Note On and measure response time in DAW
./target/release/conductorctl send-test-midi

Port Not Found

Error: Port 'IAC Driver Bus 1' is not connected

Solutions:

  1. List available ports:

    conductorctl list-midi-outputs
    
  2. Check port name matches exactly (case-sensitive):

    # Wrong:
    port = "iac driver bus 1"
    
    # Correct:
    port = "IAC Driver Bus 1"
    
  3. macOS: Verify IAC Driver is online in Audio MIDI Setup

  4. Windows: Ensure loopMIDI is running and port is created

  5. Linux: Check ALSA port exists with aconnect -l


Wrong MIDI Channel

Symptom: MIDI messages sent but DAW doesn’t respond, or wrong track responds

Solution: Check MIDI channel configuration

# MIDI channels are 0-indexed in Conductor config (0-15)
# But displayed as 1-16 in DAWs

# Channel 0 in Conductor = Channel 1 in DAW
[modes.mappings.action.message]
channel = 0  # This is MIDI channel 1

# Channel 15 in Conductor = Channel 16 in DAW
channel = 15  # This is MIDI channel 16

Check DAW track MIDI input channel:

  • Set track to receive from “All Channels” or specific channel that matches your config

Best Practices

1. Use Descriptive Port Names

# Instead of generic names:
port = "IAC Driver Bus 1"

# Use descriptive names (rename in Audio MIDI Setup):
port = "Conductor → Logic Pro"
port = "Conductor → Ableton"

2. Organize Mappings by Function

Group related controls together in your config:

# Transport Controls
[[modes.mappings]]
# ... Play mapping ...

[[modes.mappings]]
# ... Stop mapping ...

# Mixer Controls
[[modes.mappings]]
# ... Volume mapping ...

[[modes.mappings]]
# ... Pan mapping ...

3. Document Your CC Assignments

# CC 20-29: Filter controls
[[modes.mappings]]
[modes.mappings.action.message]
type = "CC"
controller = 20  # Filter Cutoff
value = 64
channel = 0

# CC 30-39: Effect sends
[[modes.mappings]]
[modes.mappings.action.message]
type = "CC"
controller = 30  # Reverb Send
value = 50
channel = 0

4. Use Velocity Mapping for Expressive Control

# Map pad velocity to filter cutoff for expressive control
[modes.mappings.action.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.5  # Gentle curve for nuanced control

5. Test Incrementally

  • Add one mapping at a time
  • Test each mapping before adding more
  • Use DEBUG=1 to verify MIDI messages are sent correctly

Next Steps


Ready to control your DAW? Start with the basic transport control example above, then customize to your workflow!

Tutorial: Building Dynamic Workflows

Step-by-step guide to combining velocity curves and conditional actions for powerful context-aware MIDI controller mappings.


Overview

This tutorial will teach you to create sophisticated, context-aware workflows that adapt to:

  • Time of day (work hours vs evening)
  • Active application (DAW vs browser vs IDE)
  • Day of week (weekdays vs weekends)
  • Current mode
  • How hard you hit the pad

By the end, you’ll build a complete workflow that transforms your MIDI controller into an intelligent assistant.


Prerequisites

  • Conductor installed and configured
  • Basic TOML editing skills (or use the GUI)
  • A MIDI controller connected
  • Understanding of basic triggers and actions

Estimated Time: 30-45 minutes


Tutorial Structure

We’ll build three progressively complex workflows:

  1. Beginner: Time-based app launcher
  2. Intermediate: Velocity-sensitive DAW control
  3. Advanced: Multi-condition workflow with nested logic

Workflow 1: Time-Based App Launcher (Beginner)

Goal: Launch Slack during work hours, Discord after hours.

Step 1: Create the Basic Mapping

Open your config.toml and add a new global mapping:

[[global_mappings]]
description = "Smart communication launcher"

[global_mappings.trigger]
type = "Note"
note = 8  # Choose your pad number

Step 2: Add the Conditional Action

Add the conditional logic:

[global_mappings.action]
type = "Conditional"

[global_mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] }
]

[global_mappings.action.then_action]
type = "Launch"
app = "Slack"

[global_mappings.action.else_action]
type = "Launch"
app = "Discord"

Step 3: Test the Workflow

  1. Save config.toml
  2. Config will hot-reload automatically
  3. Press your configured pad during work hours (Mon-Fri 9am-5pm)
  4. Verify Slack launches
  5. Press the same pad outside work hours
  6. Verify Discord launches

Understanding the Logic

IF (time is 9am-5pm AND day is Monday-Friday):
  Launch Slack
ELSE:
  Launch Discord

GUI Configuration

If using the GUI:

  1. Open Mappings view
  2. Click “Add Mapping”
  3. Set trigger to Note 8
  4. Select action type “Conditional”
  5. Select condition type “And”
  6. Add two sub-conditions:
    • TimeRange: 09:00 to 17:00
    • DayOfWeek: Select Mon-Fri
  7. Configure then_action: Launch → Slack
  8. Configure else_action: Launch → Discord
  9. Save

Workflow 2: Velocity-Sensitive DAW Control (Intermediate)

Goal: Control Logic Pro with velocity-sensitive MIDI notes, boosting soft hits for expressive playing.

Step 1: Create the Mapping

[[modes.mappings]]
description = "Expressive MIDI control"

[modes.mappings.trigger]
type = "Note"
note = 1

Step 2: Add Velocity Curve

Make soft hits more audible:

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7  # Strong boost for soft notes

Step 3: Add Conditional DAW Control

Only send MIDI if Logic Pro is running:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppRunning"
app_name = "Logic Pro"

[modes.mappings.action.then_action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "NoteOn"
channel = 0
note = 60  # Middle C
# Velocity is derived from mapped input velocity

[modes.mappings.action.else_action]
type = "Sequence"
actions = [
  { type = "Launch", app = "Logic Pro" },
  { type = "Delay", ms = 2000 },
  { type = "Text", text = "Logic Pro launched, try again" }
]

Step 4: Test the Workflow

  1. With Logic Pro closed:

    • Press pad → Logic Pro launches
    • Wait 2 seconds → See notification
  2. With Logic Pro running:

    • Press pad softly → Sends MIDI note with boosted velocity
    • Press pad hard → Sends MIDI note at full velocity
    • Notice soft notes are more audible due to exponential curve

Understanding the Flow

Input Velocity → Exponential Curve (boost soft hits) → Mapped Velocity

IF Logic Pro is running:
  Send MIDI with mapped velocity to IAC Driver
ELSE:
  Launch Logic Pro
  Wait 2 seconds
  Show notification

Velocity Curve Effect

Input VelocityWithout CurveWith Exponential (0.7)
20 (very soft)20~65 (much louder)
64 (medium)64~85 (slightly louder)
100 (hard)100~110 (barely affected)

Workflow 3: Multi-Condition Smart Assistant (Advanced)

Goal: Create a single pad that adapts to time, app, and velocity for different actions.

The Scenario

We want pad 10 to:

  • Work hours + Browser: Open new tab (velocity doesn’t matter)
  • Work hours + DAW: Send velocity-sensitive MIDI
  • Evening + Any app: Launch entertainment app based on velocity
    • Soft hit: Launch Spotify
    • Hard hit: Launch Steam

Step 1: Create the Foundation

[[global_mappings]]
description = "Adaptive smart pad"

[global_mappings.trigger]
type = "Note"
note = 10

Step 2: Configure Velocity Mapping

Use S-Curve for natural dynamics:

[global_mappings.velocity_mapping]
type = "Curve"
curve_type = "SCurve"
intensity = 0.5

Step 3: Build the Conditional Logic

First level: Check if work hours:

[global_mappings.action]
type = "Conditional"

[global_mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] }
]

Step 4: Work Hours Branch (Then Action)

During work hours, check which app is frontmost:

[global_mappings.action.then_action]
type = "Conditional"

[global_mappings.action.then_action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Safari" },
  { type = "AppFrontmost", app_name = "Chrome" },
  { type = "AppFrontmost", app_name = "Firefox" }
]

[global_mappings.action.then_action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]  # New tab in browser

[global_mappings.action.then_action.else_action]
type = "Conditional"
condition = { type = "AppRunning", app_name = "Logic Pro" }
then_action = {
  type = "SendMidi",
  port = "IAC Driver Bus 1",
  message_type = "NoteOn",
  channel = 0,
  note = 60
}
else_action = { type = "Text", text = "Work mode: Use browser or DAW" }

Step 5: Evening Branch (Else Action)

Outside work hours, use velocity to choose entertainment:

[global_mappings.action.else_action]
type = "VelocityRange"
soft_max = 64
soft_action = { type = "Launch", app = "Spotify" }
hard_action = { type = "Launch", app = "Steam" }

Complete Workflow

Here’s the full configuration:

[[global_mappings]]
description = "Adaptive smart pad: Browser/DAW at work, entertainment after hours"

[global_mappings.trigger]
type = "Note"
note = 10

[global_mappings.velocity_mapping]
type = "Curve"
curve_type = "SCurve"
intensity = 0.5

[global_mappings.action]
type = "Conditional"

[global_mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] }
]

# Work hours branch
[global_mappings.action.then_action]
type = "Conditional"

[global_mappings.action.then_action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Safari" },
  { type = "AppFrontmost", app_name = "Chrome" },
  { type = "AppFrontmost", app_name = "Firefox" }
]

[global_mappings.action.then_action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]

[global_mappings.action.then_action.else_action]
type = "Conditional"
condition = { type = "AppRunning", app_name = "Logic Pro" }
then_action = { type = "SendMidi", port = "IAC Driver Bus 1", message_type = "NoteOn", channel = 0, note = 60 }
else_action = { type = "Text", text = "Work mode: Use browser or DAW" }

# Evening branch
[global_mappings.action.else_action]
type = "VelocityRange"
soft_max = 64
soft_action = { type = "Launch", app = "Spotify" }
hard_action = { type = "Launch", app = "Steam" }

Testing the Advanced Workflow

Scenario 1: Monday 10am, Safari frontmost

  • Press pad → New tab in Safari

Scenario 2: Tuesday 2pm, Logic Pro running

  • Press pad softly → MIDI note with moderate velocity (S-curve mapped)
  • Press pad hard → MIDI note with high velocity

Scenario 3: Friday 8pm

  • Press pad softly → Spotify launches
  • Press pad hard → Steam launches

Scenario 4: Monday 10am, VS Code frontmost

  • Press pad → “Work mode: Use browser or DAW” notification

Understanding the Decision Tree

Input Velocity → S-Curve → Mapped Velocity

IF (weekday AND 9am-5pm):
  IF (browser is frontmost):
    New tab (Cmd+T)
  ELSE IF (Logic Pro running):
    Send MIDI note (mapped velocity)
  ELSE:
    Show notification
ELSE (evening/weekend):
  IF (soft hit, velocity ≤ 64):
    Launch Spotify
  ELSE (hard hit, velocity > 64):
    Launch Steam

Best Practices

1. Start Simple, Build Complexity

Don’t try to build the advanced workflow first. Start with:

  1. Single condition
  2. Add second condition with And
  3. Add nested conditional
  4. Add velocity mapping

2. Test Each Layer

After adding each condition:

  1. Save config
  2. Test the new behavior
  3. Verify existing behavior still works
  4. Add next layer

3. Use Descriptive Mappings

description = "Work hours: Browser tab OR DAW MIDI | Evening: Spotify/Steam"

Clear descriptions help when debugging.

4. Debug with Text Actions

If behavior is unexpected, temporarily replace actions with Text to see which path executes:

then_action = { type = "Text", text = "THEN path executed" }
else_action = { type = "Text", text = "ELSE path executed" }

5. GUI vs TOML

  • Simple conditionals: Use GUI for visual editing
  • Nested logic: Use TOML for precision and readability
  • Velocity curves: Use GUI for real-time preview graph

Common Patterns

Pattern 1: App-Specific Shortcuts

condition = { type = "AppFrontmost", app_name = "MyApp" }
then_action = { type = "Keystroke", keys = "s", modifiers = ["cmd"] }
else_action = { type = "Text", text = "Not in MyApp" }

Pattern 2: Time-Based Behavior

condition = { type = "TimeRange", start = "22:00", end = "08:00" }
then_action = { type = "Text", text = "Quiet hours - action disabled" }
else_action = { /* normal action */ }

Pattern 3: Launch-or-Control

condition = { type = "AppRunning", app_name = "MyDAW" }
then_action = { /* control command */ }
else_action = { type = "Launch", app = "MyDAW" }

Pattern 4: Velocity-Gated Actions

[velocity_mapping]
type = "Linear"
min = 50
max = 110

[action]
type = "VelocityRange"
soft_max = 70
soft_action = { /* gentle action */ }
hard_action = { /* aggressive action */ }

Troubleshooting

Problem: Condition Never True

Check:

  1. Time format is HH:MM (24-hour)
  2. App name matches exactly (check Activity Monitor on macOS)
  3. Day numbers are correct (Monday=1, not 0)
  4. Platform support (AppFrontmost is macOS-only)

Debug:

# Add debug text to both branches
then_action = { type = "Text", text = "Condition TRUE" }
else_action = { type = "Text", text = "Condition FALSE" }

Problem: Wrong Action Executes

Check:

  1. Logical operator (And vs Or)
  2. Nesting structure (use proper TOML indentation)
  3. Short-circuit evaluation (And stops at first false, Or at first true)

Debug:

  • Test each sub-condition individually
  • Simplify nested logic to isolate issue

Problem: Velocity Curve Doesn’t Feel Right

Solution:

  1. Open GUI velocity curve preview
  2. Adjust intensity in 0.1 increments
  3. Try different curve types:
    • Exponential: Boost soft hits
    • Logarithmic: Tame hard hits
    • S-Curve: Natural feel with sweet spot

Next Steps

Expand Your Workflows

Now that you’ve mastered dynamic workflows, try:

  1. Per-App Profiles: Different mappings for different apps
  2. Mode-Based Logic: Use ModeIs condition for mode-specific behavior
  3. Sequence Actions: Chain multiple actions with delays
  4. Advanced Velocity: Combine curves with VelocityRange triggers

Share Your Workflows

Export your config.toml and share with the community:

  • GitHub discussions
  • Reddit r/midicontrollers
  • Discord servers

Learn More


Summary

You’ve learned to:

  • ✅ Combine time and app conditions with And/Or logic
  • ✅ Use velocity curves for expressive control
  • ✅ Build nested conditional logic
  • ✅ Create context-aware workflows
  • ✅ Test and debug complex mappings
  • ✅ Follow best practices for maintainable configs

Your MIDI controller is now an intelligent, context-aware assistant that adapts to your workflow automatically.


Example Configs: See examples/ directory for complete workflow examples:

  • work-productivity.toml - Time-based work/personal split
  • daw-control.toml - Velocity-sensitive music production
  • gaming-streams.toml - Entertainment and streaming control

Setup Gallery

Visual showcase of real Conductor configurations. Get inspired and download ready-to-use configs!


Hybrid MIDI + Gamepad Setups

Music Producer’s Dream Desk

Hardware: Maschine Mikro MK3 (MIDI) + Xbox Series X Controller (Gamepad) Software: Ableton Live 11 + Splice + iZotope RX Cost: $0 additional (repurposed existing gear)

Setup Description:

  • MIDI pads (Maschine): Velocity-sensitive recording, sample triggering
  • Xbox controller: DAW navigation, transport controls, mixer view switching
  • Hybrid workflow: Hands on MIDI for music, thumbs on gamepad for navigation

Key Features:

  • Velocity Range triggers (soft/medium/hard on same pad)
  • Per-app profiles (Ableton vs Splice different mappings)
  • RGB LED feedback on Maschine (mode indicators)

Why This Works:

“I keep my hands on the MIDI pads for recording, but use my thumbs on the Xbox controller for transport and navigation. No more reaching for the keyboard mid-performance.”

Download Config →


Streaming Command Center

Hardware: Launchpad Mini (MIDI) + PlayStation DualSense (Gamepad) Software: OBS Studio + Discord + Spotify Replaces: $300 Stream Deck

Setup Description:

  • Launchpad grid: Scene switching, source visibility, filter toggles (with LED feedback)
  • DualSense: Audio mixing, chat mute, music controls, emergency stop
  • Hybrid power: Visual grid + ergonomic controller

Key Features:

  • LED feedback shows active scenes (green = live, red = muted, blue = preview)
  • Velocity-sensitive audio fading on DualSense triggers
  • Button chords (L1+R1 = mute all, L2+R2 = emergency offline)

Why This Works:

“I have visual confirmation from the Launchpad LEDs for scene switching, and the DualSense triggers let me fade audio smoothly. Best of both worlds.”

Download Config →


Gamepad-Only Setups

Budget Stream Deck Alternative (Xbox Controller)

Hardware: Xbox One Controller Software: OBS Studio + Discord + Voicemeeter Cost: $0 (reused controller) vs $150-300 Stream Deck Savings: $150-300

Button Mapping:

Face Buttons:
├─ A: Start/Stop Recording
├─ B: Mute Microphone
├─ X: Scene 1 (Gameplay)
└─ Y: Scene 2 (Webcam)

D-Pad:
├─ Up: Increase Volume
├─ Down: Decrease Volume
├─ Left: Previous Track (Spotify)
└─ Right: Next Track (Spotify)

Shoulders:
├─ LB: Switch to Discord
├─ RB: Switch to OBS
├─ LT (hold): Enable Push-to-Talk
└─ RT (hold): Instant Replay (save last 30s)

Menu Buttons:
├─ Start: Emergency "Be Right Back" scene
└─ Select: Screenshot

Why This Works:

“I was about to buy a Stream Deck. Then I realized my Xbox controller has 15+ buttons doing nothing. Configured Conductor in 20 minutes, saved $300.”

Download Config →


Developer Workflow (PlayStation DualSense)

Hardware: PlayStation DualSense Controller Software: VS Code + Terminal + Docker Desktop + GitHub Desktop Focus: Git workflows, build automation, window management

Button Mapping:

Face Buttons:
├─ Cross: Git Status
├─ Circle: Git Add + Commit
├─ Square: Build Project
└─ Triangle: Run Tests

D-Pad:
├─ Up: Next Workspace
├─ Down: Previous Workspace
├─ Left: Previous Tab
└─ Right: Next Tab

Triggers (Velocity-Sensitive):
├─ L2 (soft): Git Pull
├─ L2 (hard): Git Pull --rebase
├─ R2 (soft): Git Push
└─ R2 (hard): Git Push --force-with-lease

Shoulders:
├─ L1: Launch Terminal
├─ R1: Launch VS Code
├─ L1+R1 (chord): Docker Compose Up
└─ Touchpad Click: Launch GitHub Desktop

Why This Works:

“Velocity-sensitive git operations are incredible. Soft press = regular pull/push, hard press = force operations. One button, two functions.”

Download Config →


Specialized Controllers

Racing Wheel for Video Editing (Logitech G29)

Hardware: Logitech G29 Racing Wheel + Pedals + Shifter Software: DaVinci Resolve / Final Cut Pro Unique Factor: Analog pedal control for timeline/zoom

Control Mapping:

Pedals (Analog):
├─ Gas: Timeline Playback Speed (0-100%)
├─ Brake: Zoom Level (fine control)
└─ Clutch: Master Volume

Wheel Buttons:
├─ Button 1-4: Mark In/Out, Add Marker, Set Clip
├─ Button 5-8: Cut, Ripple Delete, Slip, Slide
├─ D-Pad: Nudge Frame Left/Right, Track Up/Down
└─ Paddle Shifters: Previous/Next Edit Point

Wheel Rotation:
└─ Scrub Timeline (analog precision)

Why This Works:

“Analog pedal control for timeline speed is game-changing. I can smoothly ramp from 10% to 200% playback, which is impossible with keyboard shortcuts. Plus, it’s ergonomic—my feet are doing work my hands don’t have to.”

Download Config →


HOTAS for Productivity (Thrustmaster T.16000M)

Hardware: Thrustmaster T.16000M FCS HOTAS Software: macOS productivity apps (Terminal, VS Code, Docker, Postman) Repurpose Value: $150 gaming hardware → $300 productivity tool

Control Mapping:

Joystick:
├─ Trigger (half-press): Build Project
├─ Trigger (full-press): Build + Run
├─ Thumb Button: Copy
├─ Top Buttons: Paste, Undo, Redo
├─ Hat Switch: Mission Control, Desktop 1-4
└─ Pinkie Switch: Toggle Fullscreen

Throttle:
├─ Base Button 1-6: Launch Apps (Terminal, VS Code, Browser, etc.)
├─ Throttle Hat: Window Snapping (Left/Right/Maximize)
├─ Slider: System Volume
└─ Rocker Switch: Previous/Next Tab

Why This Works:

“Dual-stage trigger for build operations is brilliant. Half-pull compiles, full-pull runs. Plus, having 20+ buttons within thumb reach is incredibly efficient.”

Download Config →


MIDI-Only Setups

Logic Pro Command Center (Launchpad Mini)

Hardware: Novation Launchpad Mini Software: Logic Pro X Focus: 4-mode workflow (Record, Mix, Edit, Perform)

Mode System:

Mode 1 (Blue): Recording
├─ Row 1: Transport (Play, Stop, Record, Loop)
├─ Row 2: Metronome, Click, Count-in, Pre-roll
├─ Row 3-8: Track Record Arm (8 tracks)

Mode 2 (Green): Mixing
├─ Columns: 8-channel fader control (velocity = level)
├─ Row 1: Mute 8 tracks
├─ Row 2: Solo 8 tracks

Mode 3 (Purple): Editing
├─ Row 1: Cut, Copy, Paste, Delete, Undo, Redo
├─ Row 2: Split, Join, Merge, Bounce
├─ Row 3-8: Marker creation, loop region

Mode 4 (Red): Live Performance
├─ Grid: Trigger scenes, loops, one-shots
├─ LED feedback: Green = playing, Red = stopped

Why This Works:

“RGB LED feedback is essential. I see what mode I’m in and what each pad does at a glance. Blue = recording mode, green = mixing, etc.”

Download Config →


Share Your Setup

Have an interesting Conductor configuration? Share it with the community!

Submit Your Setup →

Include:

  • Photos of your hardware setup
  • Description of your workflow
  • Config file (TOML)
  • Why it works for you

Featured setups receive:

  • Spot on this page
  • Social media shoutout
  • Entry in official config repository

More Resources

Success Stories

Real users sharing how Conductor transformed their workflows.


“From $300 Stream Deck to $30 Xbox Controller”

Sarah, Twitch StreamerPortland, OR

“I was about to buy a Stream Deck for $300 when I found Conductor. Now my old Xbox controller handles all my stream controls: scene switching, audio mutes, OBS recording. The velocity-sensitive triggers even let me fade audio in/out. Saved me $270 and it works better.”

Setup: Xbox One controller with OBS + Discord Time to configure: 20 minutes using templates Cost: $0 (reused existing controller) Result: Professional streaming setup without spending a dime

Favorite features:

  • Velocity-sensitive audio fading on triggers
  • Button chords for complex actions (LB+RB = emergency mute all)
  • Per-app profiles (different mappings for OBS vs Discord)

Download Sarah’s Config →


“Velocity Curves Changed My Music Production Workflow”

Marcus, Electronic Music ProducerBerlin, Germany

“The velocity-sensitive mappings are genius. I mapped my Maschine pads so soft hits trigger loop recording, medium hits trigger one-shots, and hard hits toggle effects. My workflow is 3x faster now because I don’t have to switch modes constantly.”

Setup: Maschine Mikro MK3 + Ableton Live 11 Key feature: Velocity Range triggers Time saved: 50% reduction in mode switching Result: Faster creative flow, more time making music

Workflow:

  • Soft press (0-40): Loop record
  • Medium press (41-80): One-shot sample
  • Hard press (81-127): Toggle effect (reverb/delay)

“No other tool can do this. I tried Stream Deck, Keyboard Maestro, even custom MIDI scripts. Conductor’s velocity sensitivity is unmatched.”

Download Marcus’s Config →


“Racing Wheel Became My Video Editor”

Chris, YouTube CreatorLos Angeles, CA

“This sounds crazy, but I use my racing wheel pedals for video editing. Gas pedal controls timeline speed, brake controls zoom. It’s way more intuitive than keyboard shortcuts, and my hands stay on the mouse for precision cuts.”

Setup: Logitech G29 + DaVinci Resolve Key feature: Analog axis pressure detection Unexpected benefit: Ergonomic, reduces wrist strain Result: Unique, comfortable editing workflow

Controls:

  • Gas pedal: Timeline playback speed (analog 0-100%)
  • Brake pedal: Zoom level (analog)
  • Clutch: Volume control
  • Wheel buttons: Mark in/out, cut, ripple delete

“My friends think I’m insane, but when they try it, they get it. It’s like driving through your timeline. Plus, my old wheel was collecting dust—now it’s essential to my workflow.”

Download Chris’s Config →


“One-Button Git Workflow Saves Me 30 Minutes Daily”

Dr. Elena Rodriguez, Research ScientistCambridge, MA

“I run hundreds of simulations daily. Before Conductor, I was typing ‘git status’, ‘git add .’, ‘git commit -m…’, ‘git push’ dozens of times. Now? One gamepad button. Soft press = status, hold 2 seconds = commit+push with auto-generated message.”

Setup: PlayStation DualSense + VS Code Time saved: 30 minutes per day Over 250 work days: 125 hours per year Result: More time for actual research

Git shortcuts:

  • Soft tap: git status
  • Medium press: git add . && git commit
  • Hard press / long hold: commit + push
  • Double-tap: git pull –rebase

“The velocity sensitivity is a game-changer. One button, three git operations based on how hard I press. It’s like magic.”

Download Elena’s Config →


“HOTAS for Productivity? Best Decision Ever.”

Jake, Full-Stack DeveloperAustin, TX

“I bought a HOTAS controller for Star Citizen, played it twice, and it sat unused for a year. Found Conductor, mapped all my dev tools to it, and now I can’t work without it. Hat switch = workspace navigation, throttle buttons = app launching, stick trigger = build/test.”

Setup: Thrustmaster T.16000M HOTAS + macOS productivity apps Hardware cost: Already owned (~$150 originally) Repurpose value: $150 hardware doing $300 Stream Deck’s job Result: 20+ shortcuts without touching keyboard

Mappings:

  • Stick trigger: Dual-stage (half-pull = build, full-pull = build+run)
  • Hat switch: Mission Control, desktops, window management
  • Throttle buttons: Launch Terminal, VS Code, Browser, Docker Desktop
  • Slider: System volume

“People think I’m crazy using a flight stick for coding, but it’s incredibly ergonomic. My hands rest naturally, and I have 20+ buttons within reach. Plus, it’s just fun.”

Download Jake’s Config →


“Turned My Launchpad Into a DAW Command Center”

Ava, Film ComposerNashville, TN

“I already had a Novation Launchpad for live performances. Conductor unlocked its full potential for studio work. Now it controls Logic Pro, my sample libraries, and mix automation—all with color-coded LED feedback showing what mode I’m in.”

Setup: Novation Launchpad Mini + Logic Pro X Key feature: RGB LED feedback schemes Modes: 4 modes (Recording, Mixing, Editing, Live Performance) Result: One controller, four complete workflows

LED feedback:

  • Blue mode: Recording (record, punch, loop, metronome)
  • Green mode: Mixing (faders, sends, automation)
  • Purple mode: Editing (cut, copy, paste, markers)
  • Red mode: Live performance (trigger scenes, effects)

“The LED feedback is crucial. I know exactly what mode I’m in and what each pad does at a glance. No more ‘wait, what does this button do again?’”

Download Ava’s Config →


Submit Your Success Story

Have you transformed your workflow with Conductor? Share your story and inspire others!

Submit Your Story →

What to include:

  • Your setup (hardware + software)
  • What problem you solved
  • Time/money saved
  • Favorite features
  • Optional: Share your config file

Your story might be featured here and help someone else discover Conductor!


More Inspiration

Music Production Workflows

Complete Conductor configurations for music producers using DAWs, sample libraries, and production software.

Overview

Conductor transforms MIDI controllers into intelligent, context-aware control surfaces for music production. Use velocity sensitivity, per-app profiles, and LED feedback to create efficient recording and mixing workflows.

What You’ll Learn:

  • Velocity-sensitive recording workflows
  • Per-app profile switching for different DAWs
  • LED feedback for visual mode indicators
  • Hybrid MIDI+gamepad setups for hands-free control

Quick Start: Velocity-Sensitive Recording

The killer feature for producers: map different recording modes to velocity ranges on a single pad.

Configuration

[device]
name = "Music Production Setup"
auto_connect = true

[[modes]]
name = "Recording"
color = "red"

# Soft press = Loop Record
[[modes.mappings]]
description = "Pad 1 Soft: Loop Record"
[modes.mappings.trigger]
type = "VelocityRange"
note = 36
min_velocity = 0
max_velocity = 40
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["cmd"]

# Medium press = Punch Record
[[modes.mappings]]
description = "Pad 1 Medium: Punch Record"
[modes.mappings.trigger]
type = "VelocityRange"
note = 36
min_velocity = 41
max_velocity = 80
[modes.mappings.action]
type = "Keystroke"
keys = "p"
modifiers = ["cmd", "shift"]

# Hard press = Toggle Record Enable
[[modes.mappings]]
description = "Pad 1 Hard: Record Enable"
[modes.mappings.trigger]
type = "VelocityRange"
note = 36
min_velocity = 81
max_velocity = 127
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["opt", "cmd"]

Result: One pad, three recording modes based on velocity. No more mode switching!


Ableton Live Integration

Full control surface setup for Ableton Live with transport, clip launching, and mixer control.

Hardware Recommendations

  • MIDI Controller: Maschine Mikro MK3, Launchpad Mini, APC Mini
  • Optional Gamepad: Xbox/PlayStation controller for navigation

Complete Configuration

[device]
name = "Ableton Live Control"
auto_connect = true

[advanced_settings]
hold_threshold_ms = 1000
double_tap_timeout_ms = 300

# ========== Mode 1: Recording ==========
[[modes]]
name = "Recording"
color = "red"

# Transport Controls (Pads 1-4)
[[modes.mappings]]
description = "Pad 1: Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

[[modes.mappings]]
description = "Pad 2: Stop"
[modes.mappings.trigger]
type = "Note"
note = 37
[modes.mappings.action]
type = "Keystroke"
keys = "Space"
modifiers = ["shift"]

[[modes.mappings]]
description = "Pad 3: Record (Velocity-Sensitive)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 38
min_velocity = 0
max_velocity = 80
[modes.mappings.action]
type = "Keystroke"
keys = "F9"

[[modes.mappings]]
description = "Pad 3 Hard: Overdub Record"
[modes.mappings.trigger]
type = "VelocityRange"
note = 38
min_velocity = 81
max_velocity = 127
[modes.mappings.action]
type = "Keystroke"
keys = "F9"
modifiers = ["shift"]

# Clip Launch (Pads 5-12)
[[modes.mappings]]
description = "Pad 5-12: Launch Clips in Scene"
[modes.mappings.trigger]
type = "Note"
note = 40
[modes.mappings.action]
type = "Keystroke"
keys = "Return"

# ========== Mode 2: Mixing ==========
[[modes]]
name = "Mixing"
color = "green"

# Volume (Encoders or Velocity-Mapped Pads)
[[modes.mappings]]
description = "Encoder 1: Master Volume"
[modes.mappings.trigger]
type = "EncoderTurn"
encoder = 1
direction = "Clockwise"
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

# Send Effects
[[modes.mappings]]
description = "Pad 1-4: Toggle Send A/B/C/D"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "1"
modifiers = ["opt", "cmd"]

# ========== Global: Mode Switching ==========
[[global_mappings]]
description = "Encoder Turn: Cycle Modes"
[global_mappings.trigger]
type = "EncoderTurn"
encoder = 0
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = "Mixing"

Workflow Tips

  • Use velocity for recording modes: Soft = loop, hard = punch
  • LED feedback: Configure LED schemes to show current mode (red = recording, green = mixing)
  • Per-track control: Map pads to track-specific record enable buttons

Logic Pro X Integration

Optimized for Logic Pro’s workflow with Smart Controls, Drummer, and arrangement features.

[See detailed Logic Pro examples in the Logic Pro Integration guide]


FL Studio Integration

Quick Setup

[[modes]]
name = "FL Studio"
color = "orange"

# Piano Roll
[[modes.mappings]]
description = "Pad 1: Open Piano Roll"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "F7"

# Mixer
[[modes.mappings]]
description = "Pad 2: Open Mixer"
[modes.mappings.trigger]
type = "Note"
note = 37
[modes.mappings.action]
type = "Keystroke"
keys = "F9"

# Pattern/Song Mode Toggle
[[modes.mappings]]
description = "Pad 3: Toggle Pattern/Song"
[modes.mappings.trigger]
type = "Note"
note = 38
[modes.mappings.action]
type = "Keystroke"
keys = "F8"

Hybrid MIDI + Gamepad Workflow

Combine MIDI controller for recording with gamepad for DAW navigation.

Setup

  • MIDI: Maschine, Launchpad, or APC for pads and encoders
  • Gamepad: Xbox/PlayStation controller for transport and navigation
[device]
name = "Hybrid Production Setup"

# MIDI Mappings (Note range 0-127)
[[modes.mappings]]
description = "MIDI Pad 1: Record"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["cmd"]

# Gamepad Mappings (Button range 128-255)
[[modes.mappings]]
description = "Xbox A: Play/Pause"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

[[modes.mappings]]
description = "Xbox D-Pad: Track Navigation"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132  # D-Pad Up
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

Why This Works: Keep hands on MIDI pads for recording, use thumbs on gamepad for transport.


Sample Libraries & Virtual Instruments

Kontakt / Native Instruments

[[modes.mappings]]
description = "Pad Hold: Articulation Switching"
[modes.mappings.trigger]
type = "LongPress"
note = 36
duration_ms = 1000
[modes.mappings.action]
type = "Keystroke"
keys = "1"
modifiers = ["ctrl"]

Splice Integration

Quick sample browser navigation:

[[modes.mappings]]
description = "Encoder: Browse Samples"
[modes.mappings.trigger]
type = "EncoderTurn"
encoder = 1
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "DownArrow"

Hardware Recommendations

Budget Setup ($0-50)

  • Reuse existing: Maschine, Launchpad, Xbox controller
  • Software: Any DAW (Ableton, Logic, FL Studio)
  • Cost: $0 (repurpose hardware)

Mid-Range Setup ($50-200)

  • MIDI Controller: Arturia BeatStep ($99)
  • Gamepad: Xbox Elite Controller ($180) or Standard ($60)
  • Hybrid: Best of both worlds

Pro Setup ($200+)

  • MIDI: Maschine MK3 ($599)
  • Optional: Stream Deck for visual feedback ($150)
  • Result: Professional control surface

Troubleshooting

MIDI Latency Issues

  • Problem: Delayed response from MIDI pads
  • Solution: Reduce buffer size in DAW preferences (128 samples or lower)
  • Conductor Impact: <1ms latency on Conductor side, latency is DAW-side

Velocity Not Working

  • Problem: All presses trigger same action regardless of velocity
  • Solution: Check MIDI controller supports velocity sensitivity
  • Test: Use cargo run --bin midi_diagnostic to verify velocity values

Per-App Profiles Not Switching

  • Problem: Same mappings work in all apps
  • Solution: Enable per-app profiles in Conductor config
  • Check: Ensure app names match exactly (case-sensitive)

Next Steps

Developer Workflows

Automate your development environment with one-button git operations, build triggers, and environment switching.

Overview

Conductor turns gamepads and MIDI controllers into powerful developer tools. Map git workflows, build commands, test runners, and environment management to physical buttons.

What You’ll Learn:

  • One-button git operations (velocity-sensitive)
  • Build/test/deploy automation
  • Docker and container management
  • IDE and terminal shortcuts
  • Window management and workspace switching

Quick Start: One-Button Git Workflow

Map all common git operations to a single gamepad button using velocity sensitivity.

[device]
name = "Developer Setup"
auto_connect = true

[[modes]]
name = "Development"
color = "blue"

# Xbox A Button: Git Operations (Velocity-Sensitive)
[[modes.mappings]]
description = "A Button Soft: Git Status"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
# Soft tap
[modes.mappings.action]
type = "Shell"
command = "cd $(pwd) && git status"

[[modes.mappings]]
description = "A Button (Hold 1s): Git Add + Commit"
[modes.mappings.trigger]
type = "GamepadButtonHold"
button = 128
duration_ms = 1000
[modes.mappings.action]
type = "Sequence"
actions = [
    { Shell = { command = "git add -A" } },
    { Shell = { command = "git commit -m 'Quick commit'" } }
]

[[modes.mappings]]
description = "A Button (Hold 2s): Commit + Push"
[modes.mappings.trigger]
type = "GamepadButtonHold"
button = 128
duration_ms = 2000
[modes.mappings.action]
type = "Sequence"
actions = [
    { Shell = { command = "git add -A" } },
    { Shell = { command = "git commit -m 'Auto commit'" } },
    { Shell = { command = "git push" } }
]

Result: One button for git status (tap), commit (hold 1s), commit+push (hold 2s).


Complete VS Code + Git Setup

Full gamepad integration for Visual Studio Code development.

Hardware

  • Recommended: PlayStation DualSense or Xbox Series X controller
  • Alternative: Any gamepad or MIDI controller

Configuration

[device]
name = "VS Code Development"
auto_connect = true

[advanced_settings]
hold_threshold_ms = 800
double_tap_timeout_ms = 300

[[modes]]
name = "VS Code"
color = "blue"

# ========== Git Operations ==========
# Face Buttons: Git Workflow
[[modes.mappings]]
description = "Cross/A: Git Status"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Shell"
command = "git status"

[[modes.mappings]]
description = "Circle/B: Git Add + Commit"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Sequence"
actions = [
    { Shell = { command = "git add -A" } },
    { Shell = { command = "git commit -m 'Update: $(date)'" } }
]

[[modes.mappings]]
description = "Square/X: Git Pull"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Shell"
command = "git pull --rebase"

[[modes.mappings]]
description = "Triangle/Y: Git Push"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Shell"
command = "git push"

# ========== Build & Test ==========
# Shoulders: Build and Test
[[modes.mappings]]
description = "L1/LB: Build Project"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136
[modes.mappings.action]
type = "Shell"
command = "npm run build"  # or cargo build, etc.

[[modes.mappings]]
description = "R1/RB: Run Tests"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137
[modes.mappings.action]
type = "Shell"
command = "npm test"

# Triggers: Advanced Build
[[modes.mappings]]
description = "L2/LT: Clean Build"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132
threshold = 64
[modes.mappings.action]
type = "Sequence"
actions = [
    { Shell = { command = "rm -rf dist node_modules" } },
    { Shell = { command = "npm install" } },
    { Shell = { command = "npm run build" } }
]

[[modes.mappings]]
description = "R2/RT: Deploy"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 64
[modes.mappings.action]
type = "Shell"
command = "npm run deploy"

# ========== Navigation ==========
# D-Pad: Workspace & Window Management
[[modes.mappings]]
description = "D-Pad Up: Mission Control"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "F3"

[[modes.mappings]]
description = "D-Pad Down: Show Desktop"
[modes.mappings.trigger]
type = "GamepadButton"
button = 133
[modes.mappings.action]
type = "Keystroke"
keys = "F11"

[[modes.mappings]]
description = "D-Pad Left: Previous Desktop"
[modes.mappings.trigger]
type = "GamepadButton"
button = 134
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"
modifiers = ["ctrl"]

[[modes.mappings]]
description = "D-Pad Right: Next Desktop"
[modes.mappings.trigger]
type = "GamepadButton"
button = 135
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["ctrl"]

# ========== App Launching ==========
# Button Chords: Launch Apps
[[modes.mappings]]
description = "L1+R1: Launch Terminal"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]
timeout_ms = 50
[modes.mappings.action]
type = "Launch"
application = "Terminal"

[[modes.mappings]]
description = "L2+R2: Launch VS Code"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [138, 139]
timeout_ms = 50
[modes.mappings.action]
type = "Launch"
application = "Visual Studio Code"

Docker & Container Management

[[modes]]
name = "Docker"
color = "cyan"

# Start/Stop Containers
[[modes.mappings]]
description = "Pad 1: Docker Compose Up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Shell"
command = "docker-compose up -d"

[[modes.mappings]]
description = "Pad 2: Docker Compose Down"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Shell"
command = "docker-compose down"

# Container Logs
[[modes.mappings]]
description = "Pad 3: View Logs"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Shell"
command = "docker-compose logs -f"

# Cleanup
[[modes.mappings]]
description = "Pad 4 (Hold): Prune System"
[modes.mappings.trigger]
type = "GamepadButtonHold"
button = 131
duration_ms = 2000
[modes.mappings.action]
type = "Shell"
command = "docker system prune -af"

Rust Development (Cargo)

[[modes]]
name = "Rust Dev"
color = "orange"

# Build
[[modes.mappings]]
description = "LB: Cargo Build"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136
[modes.mappings.action]
type = "Shell"
command = "cargo build"

# Test
[[modes.mappings]]
description = "RB: Cargo Test"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137
[modes.mappings.action]
type = "Shell"
command = "cargo test"

# Run
[[modes.mappings]]
description = "A Button: Cargo Run"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Shell"
command = "cargo run"

# Clippy
[[modes.mappings]]
description = "B Button: Cargo Clippy"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Shell"
command = "cargo clippy"

# Format
[[modes.mappings]]
description = "X Button: Cargo Format"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Shell"
command = "cargo fmt"

Python Development

[[modes]]
name = "Python Dev"
color = "yellow"

# Virtual Environment
[[modes.mappings]]
description = "Pad 1: Activate venv"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Shell"
command = "source venv/bin/activate"

# Run Tests
[[modes.mappings]]
description = "Pad 2: Run Pytest"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Shell"
command = "pytest"

# Linting
[[modes.mappings]]
description = "Pad 3: Run Flake8"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Shell"
command = "flake8 ."

# Format
[[modes.mappings]]
description = "Pad 4: Black Format"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Shell"
command = "black ."

Troubleshooting

Shell Commands Not Executing

  • Problem: Shell actions don’t run or show errors
  • Solution: Use absolute paths or ensure working directory is correct
  • Example: cd /path/to/project && git status instead of just git status

Environment Variables Not Available

  • Problem: Shell commands can’t find tools (npm, cargo, etc.)
  • Solution: Source your shell profile in command: source ~/.zshrc && npm run build

Permissions Issues

  • Problem: “Permission denied” errors
  • Solution: Ensure scripts are executable: chmod +x script.sh

Next Steps

Streaming Workflows

Complete OBS Studio and streaming platform integration using gamepads as free Stream Deck alternatives.

Overview

Turn your Xbox or PlayStation controller into a professional streaming control surface. Save $150-300 by repurposing existing gamepad hardware instead of buying a Stream Deck.

What You’ll Learn:

  • Scene switching and source control
  • Audio mixing with velocity-sensitive fading
  • Multi-platform setup (OBS + Discord + Spotify)
  • Emergency controls (instant mute, BRB scene)
  • Advanced: Button chords for complex actions

Quick Start: Basic OBS Control

Essential streaming controls on a gamepad.

[device]
name = "Basic Streaming Setup"
auto_connect = true

[[modes]]
name = "Streaming"
color = "purple"

# Face Buttons: Core Controls
[[modes.mappings]]
description = "A Button: Start/Stop Recording"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "B Button: Mute/Unmute Microphone"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "m"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "X Button: Scene 1 (Gameplay)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Keystroke"
keys = "1"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "Y Button: Scene 2 (Webcam)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Keystroke"
keys = "2"
modifiers = ["ctrl", "shift"]

Setup Required: Configure OBS hotkeys to match (File → Settings → Hotkeys)


Professional Streaming Setup

Complete Xbox/PlayStation controller configuration for OBS + Discord + Spotify.

Hardware

  • Xbox Controller: Xbox One, Series X/S ($30-60)
  • PlayStation Controller: DualShock 4, DualSense ($30-70)
  • Cost vs Stream Deck: $30-70 vs $150-300 (save $120-230!)

Full Configuration

[device]
name = "Pro Streaming Controller"
auto_connect = true

[advanced_settings]
hold_threshold_ms = 1000
double_tap_timeout_ms = 300

[[modes]]
name = "Live Streaming"
color = "red"

# ========== Scene Switching ==========
# Face Buttons: Primary Scenes
[[modes.mappings]]
description = "A: Gameplay Scene"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "1"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "B: Webcam Scene"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "2"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "X: Desktop Scene"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "Y: BRB Scene"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Keystroke"
keys = "4"
modifiers = ["ctrl", "shift"]

# ========== Audio Control ==========
# D-Pad: Volume & Music
[[modes.mappings]]
description = "D-Pad Up: Desktop Volume Up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

[[modes.mappings]]
description = "D-Pad Down: Desktop Volume Down"
[modes.mappings.trigger]
type = "GamepadButton"
button = 133
[modes.mappings.action]
type = "VolumeControl"
operation = "Down"

[[modes.mappings]]
description = "D-Pad Left: Previous Track (Spotify)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 134
[modes.mappings.action]
type = "Keystroke"
keys = "Previous"

[[modes.mappings]]
description = "D-Pad Right: Next Track (Spotify)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 135
[modes.mappings.action]
type = "Keystroke"
keys = "Next"

# ========== Recording & Streaming ==========
# Shoulders: Record & Stream
[[modes.mappings]]
description = "LB: Toggle Recording"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "RB: Toggle Streaming"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137
[modes.mappings.action]
type = "Keystroke"
keys = "s"
modifiers = ["ctrl", "shift"]

# ========== Velocity-Sensitive Audio Fading ==========
# Triggers: Audio Mixing
[[modes.mappings]]
description = "LT: Fade Out Desktop Audio"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132
threshold = 64
[modes.mappings.action]
type = "Sequence"
actions = [
    { VolumeControl = { operation = "Down" } },
    { Delay = { duration_ms = 50 } },
    { VolumeControl = { operation = "Down" } },
    { Delay = { duration_ms = 50 } },
    { VolumeControl = { operation = "Down" } }
]

[[modes.mappings]]
description = "RT: Fade In Desktop Audio"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 64
[modes.mappings.action]
type = "Sequence"
actions = [
    { VolumeControl = { operation = "Up" } },
    { Delay = { duration_ms = 50 } },
    { VolumeControl = { operation = "Up" } },
    { Delay = { duration_ms = 50 } },
    { VolumeControl = { operation = "Up" } }
]

# ========== Emergency Controls ==========
# Button Chords: Emergency Actions
[[modes.mappings]]
description = "LB+RB: Mute All (Mic + Desktop)"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]
timeout_ms = 50
[modes.mappings.action]
type = "Sequence"
actions = [
    { Keystroke = { keys = "m", modifiers = ["ctrl", "shift"] } },  # Mute mic
    { VolumeControl = { operation = "Mute" } }  # Mute desktop
]

[[modes.mappings]]
description = "LT+RT: Emergency BRB Scene"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [138, 139]
timeout_ms = 50
[modes.mappings.action]
type = "Keystroke"
keys = "9"
modifiers = ["ctrl", "shift"]  # Scene 9 = Emergency BRB

# ========== Source Control ==========
# Menu Buttons: Source Visibility
[[modes.mappings]]
description = "Start: Toggle Webcam Visibility"
[modes.mappings.trigger]
type = "GamepadButton"
button = 140
[modes.mappings.action]
type = "Keystroke"
keys = "w"
modifiers = ["ctrl", "shift"]

[[modes.mappings]]
description = "Select: Screenshot"
[modes.mappings.trigger]
type = "GamepadButton"
button = 141
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

OBS Hotkey Setup

Configure these hotkeys in OBS (File → Settings → Hotkeys):

ActionHotkeyController Button
Scene 1Ctrl+Shift+1A
Scene 2Ctrl+Shift+2B
Scene 3Ctrl+Shift+3X
Scene 4Ctrl+Shift+4Y
Start RecordingCtrl+Shift+RLB
Start StreamingCtrl+Shift+SRB
Mute MicrophoneCtrl+Shift+MB (double-tap)
Toggle WebcamCtrl+Shift+WStart

Multi-Platform Integration

Discord Integration

# Push-to-Talk
[[modes.mappings]]
description = "LT (Hold): Push-to-Talk Discord"
[modes.mappings.trigger]
type = "GamepadTriggerHold"
trigger = 132
threshold = 64
duration_ms = 100
[modes.mappings.action]
type = "Keystroke"
keys = "grave"  # Backtick key
modifiers = ["ctrl"]

# Mute/Deafen
[[modes.mappings]]
description = "Select (Double-Tap): Toggle Deafen"
[modes.mappings.trigger]
type = "GamepadButtonDoubleTap"
button = 141
timeout_ms = 300
[modes.mappings.action]
type = "Keystroke"
keys = "d"
modifiers = ["ctrl", "shift"]

Spotify Control

# Music Playback
[[modes.mappings]]
description = "D-Pad Center: Play/Pause Spotify"
[modes.mappings.trigger]
type = "GamepadButton"
button = 142  # Guide/Home button
[modes.mappings.action]
type = "Keystroke"
keys = "PlayPause"

Advanced: Instant Replay & Highlights

# Save Last 30 Seconds (Instant Replay)
[[modes.mappings]]
description = "RT (Hold 2s): Save Instant Replay"
[modes.mappings.trigger]
type = "GamepadTriggerHold"
trigger = 133
threshold = 64
duration_ms = 2000
[modes.mappings.action]
type = "Keystroke"
keys = "i"
modifiers = ["ctrl", "shift"]

# Add Stream Marker
[[modes.mappings]]
description = "Y (Hold): Add Stream Marker"
[modes.mappings.trigger]
type = "GamepadButtonHold"
button = 131
duration_ms = 1000
[modes.mappings.action]
type = "Keystroke"
keys = "k"
modifiers = ["ctrl", "shift"]

Troubleshooting

OBS Hotkeys Not Working

  • Problem: Button presses don’t trigger OBS actions
  • Solution: Ensure OBS hotkeys match exactly (case-sensitive)
  • Check: Run OBS as administrator (Windows) or grant permissions (macOS)

Audio Fading Too Fast/Slow

  • Problem: Volume changes too abruptly
  • Solution: Adjust delay duration in Sequence actions (increase from 50ms to 100ms+)

Button Chords Not Detecting

  • Problem: Pressing LB+RB doesn’t trigger chord action
  • Solution: Reduce chord timeout_ms (try 30ms instead of 50ms)

Cost Comparison

SetupHardwareCostSavings
ConductorXbox Controller (owned)$0$150-300
ConductorNew Xbox Controller$30-60$90-270
ConductorPlayStation DualSense$60-70$80-240
Stream DeckStream Deck Mini$79.99-
Stream DeckStream Deck MK.2$149.99-
Stream DeckStream Deck XL$249.99-

Bottom Line: Reusing an existing gamepad = $150-300 saved, same functionality.


Next Steps

Video Editing Workflows

Unique controller configurations for DaVinci Resolve, Final Cut Pro, and Adobe Premiere using racing wheels, MIDI controllers, and gamepads.

Overview

Conductor enables creative, ergonomic video editing workflows that traditional keyboard shortcuts can’t match. Use racing wheel pedals for analog timeline control, MIDI pads for quick cuts, and gamepads for timeline navigation.

What You’ll Learn:

  • Racing wheel pedals for timeline speed & zoom (analog control!)
  • MIDI pads for markers, cuts, and effects
  • Gamepad navigation for hands-free editing
  • Hybrid setups combining multiple controllers

Racing Wheel for Video Editing

The most unique and ergonomic editing workflow: use racing wheel pedals for analog timeline control.

Hardware

  • Racing Wheel: Logitech G29, G920, Thrustmaster T150 ($150-300)
  • Pedals: Gas, Brake, Clutch (included with wheel)
  • Software: DaVinci Resolve, Final Cut Pro, Premiere Pro

Why This Works

“Analog pedal control for playback speed is game-changing. I can smoothly ramp from 10% to 200% playback speed, which is impossible with keyboard shortcuts. Plus, it’s ergonomic—my feet are doing work my hands don’t have to.”

Configuration

[device]
name = "Racing Wheel Editor"
auto_connect = true

[[modes]]
name = "Editing"
color = "orange"

# ========== Pedals (Analog Control) ==========
# Gas Pedal: Timeline Playback Speed (0-200%)
[[modes.mappings]]
description = "Gas Pedal: Timeline Speed Control"
[modes.mappings.trigger]
type = "GamepadAxis"
axis = 133  # Gas pedal (R2/RT axis)
threshold = 10
[modes.mappings.action]
type = "Keystroke"
keys = "l"  # Increase playback speed
# Note: Pressure determines speed (0-200%)

# Brake Pedal: Zoom Level (Analog)
[[modes.mappings]]
description = "Brake Pedal: Timeline Zoom"
[modes.mappings.trigger]
type = "GamepadAxis"
axis = 132  # Brake pedal (L2/LT axis)
threshold = 10
[modes.mappings.action]
type = "Keystroke"
keys = "="
modifiers = ["cmd"]  # Zoom in

# Clutch Pedal: Master Volume
[[modes.mappings]]
description = "Clutch: Volume Control"
[modes.mappings.trigger]
type = "GamepadAxis"
axis = 131  # Clutch axis
threshold = 10
[modes.mappings.action]
type = "VolumeControl"
operation = "Set"

# ========== Wheel Rotation ==========
# Wheel Turn: Scrub Timeline
[[modes.mappings]]
description = "Wheel Left: Frame Backward"
[modes.mappings.trigger]
type = "GamepadAxis"
axis = 128  # Steering wheel X-axis
threshold = -20
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"

[[modes.mappings]]
description = "Wheel Right: Frame Forward"
[modes.mappings.trigger]
type = "GamepadAxis"
axis = 128
threshold = 20
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"

# ========== Wheel Buttons ==========
# Face Buttons: Common Edits
[[modes.mappings]]
description = "Button 1: Mark In"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "i"

[[modes.mappings]]
description = "Button 2: Mark Out"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "o"

[[modes.mappings]]
description = "Button 3: Cut"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Keystroke"
keys = "b"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Button 4: Ripple Delete"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Keystroke"
keys = "Delete"
modifiers = ["cmd", "shift"]

# Paddle Shifters: Previous/Next Edit Point
[[modes.mappings]]
description = "Left Paddle: Previous Edit"
[modes.mappings.trigger]
type = "GamepadButton"
button = 144  # Left paddle
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

[[modes.mappings]]
description = "Right Paddle: Next Edit"
[modes.mappings.trigger]
type = "GamepadButton"
button = 145  # Right paddle
[modes.mappings.action]
type = "Keystroke"
keys = "DownArrow"

# D-Pad: Track Navigation
[[modes.mappings]]
description = "D-Pad Up: Track Up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"
modifiers = ["shift"]

[[modes.mappings]]
description = "D-Pad Down: Track Down"
[modes.mappings.trigger]
type = "GamepadButton"
button = 133
[modes.mappings.action]
type = "Keystroke"
keys = "DownArrow"
modifiers = ["shift"]

DaVinci Resolve Configuration

Optimized for DaVinci Resolve’s edit, color, and Fairlight pages.

Edit Page

[[modes]]
name = "Resolve Edit"
color = "red"

# Quick Tools
[[modes.mappings]]
description = "A: Select Tool"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "a"

[[modes.mappings]]
description = "B: Blade Tool"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "b"

[[modes.mappings]]
description = "X: Trim Tool"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Keystroke"
keys = "t"

# Markers
[[modes.mappings]]
description = "Y: Add Marker"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Keystroke"
keys = "m"

Color Page

[[modes]]
name = "Resolve Color"
color = "purple"

# Color Wheels
[[modes.mappings]]
description = "Encoder 1: Lift Brightness"
[modes.mappings.trigger]
type = "EncoderTurn"
encoder = 1
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"
modifiers = ["ctrl"]

Final Cut Pro Configuration

[[modes]]
name = "Final Cut Pro"
color = "blue"

# Timeline
[[modes.mappings]]
description = "A: Play/Pause"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

# Effects
[[modes.mappings]]
description = "B: Effects Browser"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "5"
modifiers = ["cmd"]

# Export
[[modes.mappings]]
description = "LB+RB: Export"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]
timeout_ms = 50
[modes.mappings.action]
type = "Keystroke"
keys = "e"
modifiers = ["cmd"]

Adobe Premiere Pro Configuration

[[modes]]
name = "Premiere Pro"
color = "violet"

# Essential Graphics
[[modes.mappings]]
description = "X: Essential Graphics"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Keystroke"
keys = "9"
modifiers = ["shift"]

# Lumetri Color
[[modes.mappings]]
description = "Y: Lumetri Color"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Keystroke"
keys = "5"
modifiers = ["shift"]

MIDI Controller Setup (Launchpad/APC)

Use MIDI pads for quick access to effects, transitions, and markers.

# Effects Grid (8x8 Pads)
[[modes.mappings]]
description = "Pad 1: Gaussian Blur"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Sequence"
actions = [
    { Keystroke = { keys = "5", modifiers = ["cmd"] } },  # Open Effects
    { Delay = { duration_ms = 100 } },
    { Text = { text = "Gaussian Blur" } },
    { Keystroke = { keys = "Return" } }
]

Hybrid Setup: Wheel + MIDI + Gamepad

Combine racing wheel pedals (timeline), MIDI pads (effects), and gamepad (navigation).

[device]
name = "Ultimate Editing Rig"

# Wheel Pedals (Analog)
[[modes.mappings]]
trigger = { GamepadAxis = { axis = 133, threshold = 10 } }
action = { Keystroke = { keys = "l" } }  # Playback speed

# MIDI Pads (Effects)
[[modes.mappings]]
trigger = { Note = { note = 36 } }
action = { Keystroke = { keys = "b", modifiers = ["cmd"] } }  # Blade

# Gamepad (Navigation)
[[modes.mappings]]
trigger = { GamepadButton = { button = 132 } }
action = { Keystroke = { keys = "UpArrow" } }

Troubleshooting

Pedal Input Not Recognized

  • Problem: Pedals don’t trigger actions
  • Solution: Check axis IDs with cargo run --bin midi_diagnostic
  • Calibrate: Ensure pedals are calibrated in OS settings

Timeline Scrubbing Too Sensitive

  • Problem: Wheel rotation causes too much movement
  • Solution: Increase axis threshold (try 30-40 instead of 20)

Effects Not Applying

  • Problem: Effect shortcuts open panel but don’t apply
  • Solution: Add delays between keystrokes in Sequence (100-200ms)

Next Steps

Automation & Power User Workflows

Advanced productivity automation for form filling, app switching, window management, and repetitive task elimination.

Overview

Conductor enables power users to automate complex workflows that traditional macro tools can’t handle. Use velocity sensitivity for multi-function buttons, context-aware profiles for app-specific shortcuts, and sequences for multi-step automation.

What You’ll Learn:

  • Form automation (one-button form filling)
  • App-specific macro sets with per-app profiles
  • Window management and workspace automation
  • Context-aware mappings (time-based, app-based)
  • Advanced sequences and conditional actions

Quick Start: One-Button Form Filling

Automatically fill web forms with saved data using a single button press.

[device]
name = "Form Automation"
auto_connect = true

[[modes]]
name = "Form Fill"
color = "green"

# Complete Form Fill (Name, Email, Phone, Address)
[[modes.mappings]]
description = "Pad 1: Fill Contact Form"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Sequence"
actions = [
    { Text = { text = "John Doe" } },
    { Keystroke = { keys = "Tab" } },
    { Text = { text = "john.doe@example.com" } },
    { Keystroke = { keys = "Tab" } },
    { Text = { text = "(555) 123-4567" } },
    { Keystroke = { keys = "Tab" } },
    { Text = { text = "123 Main St, City, ST 12345" } }
]

# Velocity-Sensitive Form Options
[[modes.mappings]]
description = "Pad 2 Soft: Fill Work Email"
[modes.mappings.trigger]
type = "VelocityRange"
note = 37
min_velocity = 0
max_velocity = 40
[modes.mappings.action]
type = "Text"
text = "john.doe@company.com"

[[modes.mappings]]
description = "Pad 2 Hard: Fill Personal Email"
[modes.mappings.trigger]
type = "VelocityRange"
note = 37
min_velocity = 81
max_velocity = 127
[modes.mappings.action]
type = "Text"
text = "john.personal@gmail.com"

Time Saved: 30-60 seconds per form × 10 forms/day = 5-10 minutes daily


Per-App Profile Switching

Different mappings for different applications automatically.

Browser (Chrome/Firefox)

# Browser-Specific Profile
[[modes.mappings]]
description = "A Button: New Tab (Browser)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Conditional"
conditions = [
    {
        type = "AppActive",
        app_name = "Google Chrome",
        action = { Keystroke = { keys = "t", modifiers = ["cmd"] } }
    },
    {
        type = "AppActive",
        app_name = "Firefox",
        action = { Keystroke = { keys = "t", modifiers = ["cmd"] } }
    }
]

# Browser Navigation
[[modes.mappings]]
description = "D-Pad Left: Back"
[modes.mappings.trigger]
type = "GamepadButton"
button = 134
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"
modifiers = ["cmd"]

[[modes.mappings]]
description = "D-Pad Right: Forward"
[modes.mappings.trigger]
type = "GamepadButton"
button = 135
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]

Email Client (Mail/Outlook)

# Email-Specific Actions
[[modes.mappings]]
description = "A: New Email (Mail.app)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Conditional"
conditions = [
    {
        type = "AppActive",
        app_name = "Mail",
        action = { Keystroke = { keys = "n", modifiers = ["cmd"] } }
    }
]

# Quick Replies
[[modes.mappings]]
description = "Pad 1: Reply with Template"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Sequence"
actions = [
    { Keystroke = { keys = "r", modifiers = ["cmd"] } },  # Reply
    { Delay = { duration_ms = 200 } },
    { Text = { text = "Thanks for your email. I'll get back to you shortly.\n\nBest,\nJohn" } }
]

Window Management Automation

Advanced workspace and window control.

macOS Mission Control & Spaces

[[modes]]
name = "Window Management"
color = "cyan"

# Workspace Navigation
[[modes.mappings]]
description = "D-Pad Left: Previous Desktop"
[modes.mappings.trigger]
type = "GamepadButton"
button = 134
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"
modifiers = ["ctrl"]

[[modes.mappings]]
description = "D-Pad Right: Next Desktop"
[modes.mappings.trigger]
type = "GamepadButton"
button = 135
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["ctrl"]

# Mission Control
[[modes.mappings]]
description = "D-Pad Up: Mission Control"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "F3"

# Show Desktop
[[modes.mappings]]
description = "D-Pad Down: Show Desktop"
[modes.mappings.trigger]
type = "GamepadButton"
button = 133
[modes.mappings.action]
type = "Keystroke"
keys = "F11"

# Window Snapping (requires Rectangle.app or similar)
[[modes.mappings]]
description = "LB: Snap Left Half"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"
modifiers = ["ctrl", "opt"]

[[modes.mappings]]
description = "RB: Snap Right Half"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["ctrl", "opt"]

# Maximize
[[modes.mappings]]
description = "LB+RB: Maximize Window"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]
timeout_ms = 50
[modes.mappings.action]
type = "Keystroke"
keys = "Return"
modifiers = ["ctrl", "opt"]

App Launcher Matrix

Quick launch common apps with button grid.

# App Launching (Face Buttons)
[[modes.mappings]]
description = "A: Launch Browser"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Launch"
application = "Google Chrome"

[[modes.mappings]]
description = "B: Launch Terminal"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Launch"
application = "Terminal"

[[modes.mappings]]
description = "X: Launch VS Code"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Launch"
application = "Visual Studio Code"

[[modes.mappings]]
description = "Y: Launch Slack"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Launch"
application = "Slack"

# Chord Combinations for More Apps
[[modes.mappings]]
description = "LB+A: Launch Finder"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 128]
timeout_ms = 50
[modes.mappings.action]
type = "Launch"
application = "Finder"

[[modes.mappings]]
description = "LB+B: Launch Spotify"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 129]
timeout_ms = 50
[modes.mappings.action]
type = "Launch"
application = "Spotify"

Context-Aware Automation

Time-Based Mappings

# Morning Routine (6am-9am)
[[modes.mappings]]
description = "Morning: Launch Email + Calendar"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Conditional"
conditions = [
    {
        type = "TimeRange",
        start_time = "06:00",
        end_time = "09:00",
        action = {
            Sequence = {
                actions = [
                    { Launch = { application = "Mail" } },
                    { Delay = { duration_ms = 500 } },
                    { Launch = { application = "Calendar" } }
                ]
            }
        }
    }
]

# Evening Routine (6pm-11pm)
[[modes.mappings]]
description = "Evening: Launch Spotify + Dimming"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131
[modes.mappings.action]
type = "Conditional"
conditions = [
    {
        type = "TimeRange",
        start_time = "18:00",
        end_time = "23:00",
        action = {
            Sequence = {
                actions = [
                    { Launch = { application = "Spotify" } },
                    { Shell = { command = "osascript -e 'tell app \"System Events\" to key code 107'" } }  # F14 for dimming
                ]
            }
        }
    }
]

Clipboard Management

Advanced clipboard operations.

# Clipboard History (requires Alfred or Clipboard Manager)
[[modes.mappings]]
description = "LT: Paste from Clipboard History"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132
threshold = 64
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["cmd", "shift"]  # Alfred clipboard history

# Paste as Plain Text
[[modes.mappings]]
description = "RT: Paste Plain Text"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 64
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["cmd", "shift", "opt"]

System Automation

Screenshot & Screen Recording

# Screenshots
[[modes.mappings]]
description = "Select: Full Screenshot"
[modes.mappings.trigger]
type = "GamepadButton"
button = 141
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

[[modes.mappings]]
description = "Select (Hold): Selection Screenshot"
[modes.mappings.trigger]
type = "GamepadButtonHold"
button = 141
duration_ms = 1000
[modes.mappings.action]
type = "Keystroke"
keys = "4"
modifiers = ["cmd", "shift"]

# Screen Recording
[[modes.mappings]]
description = "Start (Hold 2s): Start Recording"
[modes.mappings.trigger]
type = "GamepadButtonHold"
button = 140
duration_ms = 2000
[modes.mappings.action]
type = "Keystroke"
keys = "5"
modifiers = ["cmd", "shift"]

Lock Screen & Sleep

[[modes.mappings]]
description = "LB+RB+Start: Lock Screen"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137, 140]
timeout_ms = 50
[modes.mappings.action]
type = "Keystroke"
keys = "q"
modifiers = ["ctrl", "cmd"]

Troubleshooting

Text Not Typing Correctly

  • Problem: Text actions type gibberish or incomplete
  • Solution: Add delays between Text and Keystroke actions (100-200ms)
  • Example: { Delay = { duration_ms = 150 } }

Per-App Profiles Not Switching

  • Problem: Same mappings work across all apps
  • Solution: Ensure app names match exactly (check Activity Monitor)
  • Case-Sensitive: “Google Chrome” ≠ “google chrome”

Sequences Executing Out of Order

  • Problem: Actions in Sequence trigger in wrong order
  • Solution: Increase delays between actions (try 200-300ms)

Time Savings Calculator

TaskManual TimeAutomated TimeDaily FrequencyTime Saved
Form filling45s2s10×7m 10s
Email reply60s5s15×13m 45s
App switching5s1s50×3m 20s
Window management8s1s30×3m 30s
Total Daily---27m 45s
Annual---115 hours

Next Steps

Logic Pro Integration

This guide shows you how to set up Conductor to control Logic Pro, Apple’s professional digital audio workstation. We’ll cover transport controls, mixer automation, and smart control integration.

Prerequisites

  • macOS (Logic Pro is macOS-only)
  • Conductor v2.1.0 or later
  • Logic Pro 10.5 or later
  • IAC Driver configured (see DAW Control Guide)

Quick Start

1. Enable IAC Driver

First, ensure your IAC Driver is enabled:

  1. Open Audio MIDI Setup (Applications → Utilities)
  2. Show MIDI Studio (Window → Show MIDI Studio)
  3. Double-click IAC Driver
  4. Check “Device is online”
  5. Ensure at least one bus exists (default: “IAC Driver Bus 1”)
  6. Click Apply

2. Configure Logic Pro MIDI Input

In Logic Pro:

  1. Open Logic Pro → Settings → MIDI → Inputs (or press ⌘,)
  2. Locate IAC Driver Bus 1 in the device list
  3. Check the box to enable it
  4. Close Settings

3. Create a Basic Transport Control Profile

Create or update your ~/.config/conductor/config.toml:

[[modes]]
name = "Logic Pro"
color = "purple"

# Play/Pause (Pad 1)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 115  # Logic Pro Play/Pause CC
value = 127
channel = 0

# Stop (Pad 2)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 2

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 116  # Logic Pro Stop CC
value = 127
channel = 0

# Record (Pad 3)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 3

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 117  # Logic Pro Record CC
value = 127
channel = 0

4. Set Up Logic Pro Control Surface

In Logic Pro:

  1. Open Logic Pro → Control Surfaces → Setup
  2. Click New → Install… (or press ⌘N)
  3. Choose Mackie Control or Generic from the list
  4. In the device settings:
    • Input: Select IAC Driver Bus 1
    • Output: (Optional - for feedback)
  5. Click Apply or OK

5. Test the Configuration

  1. Start the Conductor daemon: conductor
  2. Open a Logic Pro project
  3. Press Pad 1 on your controller → Logic should Play/Pause
  4. Press Pad 2 → Logic should Stop
  5. Press Pad 3 → Logic should start Recording

Logic Pro MIDI CC Mapping Reference

Logic Pro uses specific CC numbers for different transport and control functions:

FunctionCC NumberValue RangeNotes
Play/Pause115127Toggle transport
Stop116127Stop playback
Record117127Toggle record
Rewind118127Skip backward
Fast Forward119127Skip forward
Cycle120127Toggle cycle mode
Volume (Track 1)70-127Channel 0
Pan (Track 1)100-127Channel 0
Volume (Track 2)70-127Channel 1
Pan (Track 2)100-127Channel 1

Note: Track-specific controls use different MIDI channels (Track 1 = Channel 0, Track 2 = Channel 1, etc.)

Complete Transport Control Profile

Here’s a full-featured transport control profile for Logic Pro:

[[modes]]
name = "Logic Pro"
color = "purple"

# ===== Transport Controls =====

# Play/Pause (Pad 1)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 115
value = 127
channel = 0

# Stop (Pad 2)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 2

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 116
value = 127
channel = 0

# Record (Pad 3)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 3

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 117
value = 127
channel = 0

# Rewind (Pad 4)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 4

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 118
value = 127
channel = 0

# Fast Forward (Pad 5)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 5

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 119
value = 127
channel = 0

# Cycle On/Off (Pad 6)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 6

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 120
value = 127
channel = 0

Mixer Control with Velocity Curves

Use velocity-sensitive pads to control track volume with smooth curves:

# Track 1 Volume (Pad 9, velocity-sensitive)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 9

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 7  # Volume
channel = 0     # Track 1

[modes.mappings.action.then_action.message.velocity_curve]
type = "Curve"
input_min = 0
input_max = 127
output_min = 0
output_max = 127
curve = 1.5  # Slightly exponential for better fader control

# Track 2 Volume (Pad 10)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 10

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 7
channel = 1     # Track 2

[modes.mappings.action.then_action.message.velocity_curve]
type = "Curve"
input_min = 0
input_max = 127
output_min = 0
output_max = 127
curve = 1.5

Smart Controls Integration

Logic Pro’s Smart Controls can be MIDI-mapped for parameter automation:

Step 1: Map Smart Controls in Logic Pro

  1. Open a Logic Pro project with a software instrument
  2. Show Smart Controls (press B or View → Show Smart Controls)
  3. Click Learn button in Smart Controls header
  4. Press a pad on your MIDI controller
  5. Logic will assign that CC to the Smart Control knob
  6. Repeat for up to 8 Smart Control parameters

Step 2: Configure Conductor to Send Smart Control CCs

# Smart Control 1 (Pad 13, velocity controls CC value)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 13

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 71  # Smart Control 1 (default)
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"  # Direct velocity → CC value mapping

# Smart Control 2 (Pad 14)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 14

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 72  # Smart Control 2
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"

Default Smart Control CC Numbers:

  • Smart Control 1: CC 71
  • Smart Control 2: CC 72
  • Smart Control 3: CC 73
  • Smart Control 4: CC 74
  • Smart Control 5: CC 75
  • Smart Control 6: CC 76
  • Smart Control 7: CC 77
  • Smart Control 8: CC 78

Advanced: Multi-Function Pads with Long Press

Use long press to access secondary functions on the same pad:

# Pad 1: Play (short press) / Stop (long press)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 115  # Play
value = 127
channel = 0

# Long press variant (Stop)
[[modes.mappings]]
[modes.mappings.trigger]
type = "LongPress"
note = 1
duration_ms = 800

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 116  # Stop
value = 127
channel = 0

Using MIDI Learn in Logic Pro

Logic Pro has a built-in MIDI Learn feature for custom mappings:

Step 1: Enter MIDI Learn Mode

  1. In Logic Pro, go to Logic Pro → Control Surfaces → Learn Assignment
  2. Click the parameter you want to control (e.g., a plugin knob, fader, button)
  3. Press the pad on your MIDI controller
  4. Logic will map that CC to the parameter
  5. Press Done when finished

Step 2: Configure Conductor

Once Logic has learned the CC mapping, configure Conductor to send that CC:

# Example: Plugin parameter learned as CC 20
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 15

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.apple.logic10"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 20  # Whatever Logic learned
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"

Troubleshooting

IAC Driver Not Appearing in Logic Pro

Problem: IAC Driver doesn’t show up in Logic Pro’s MIDI Inputs.

Solution:

  1. Verify IAC Driver is enabled in Audio MIDI Setup
  2. Restart Logic Pro after enabling IAC Driver
  3. Check Logic Pro → Settings → MIDI → Reset MIDI Drivers

Messages Sent But No Response

Problem: Conductor sends MIDI but Logic doesn’t respond.

Solution:

  1. Verify the control surface is configured in Logic Pro → Control Surfaces → Setup
  2. Ensure the correct CC numbers for your Logic version (some changed in Logic Pro 10.5+)
  3. Try using MIDI Learn to discover the correct CC numbers
  4. Check MIDI channel (most Logic functions use Channel 0)

Latency Issues

Problem: Noticeable delay between pad press and Logic response.

Solution:

  1. Lower Logic Pro’s I/O buffer size: Logic Pro → Settings → Audio → I/O Buffer Size
  2. Use IAC Driver (lowest latency) instead of virtual MIDI apps
  3. Reduce CPU load (freeze tracks, disable unused plugins)

Control Surface Conflicts

Problem: Multiple control surfaces interfering with each other.

Solution:

  1. In Logic Pro → Control Surfaces → Setup, disable unused control surfaces
  2. Ensure IAC Driver is assigned to only one control surface
  3. Use unique MIDI channels for different control surfaces

Complete Example Profile

Here’s a production-ready Logic Pro control profile with all features:

Download: logic-pro-complete.toml (coming soon)

Features:

  • Transport controls (Play, Stop, Record, Rewind, FF, Cycle)
  • 4-track mixer (Volume + Pan per track)
  • 8 Smart Control parameters
  • Multi-function pads (short/long press)
  • Velocity-sensitive mixer control
  • AppFrontmost conditional (only works when Logic is active)

Next Steps

Further Reading

Ableton Live Integration

This guide shows you how to set up Conductor to control Ableton Live, one of the most popular DAWs for electronic music production and live performance. We’ll cover transport controls, clip launching, device parameter mapping, and session view integration.

Prerequisites

  • macOS, Windows, or Linux
  • Conductor v2.1.0 or later
  • Ableton Live 11 or later (Standard or Suite)
  • Virtual MIDI port configured:

Quick Start

1. Configure Virtual MIDI Port

macOS (IAC Driver)

  1. Open Audio MIDI Setup (Applications → Utilities)
  2. Show MIDI Studio (Window → Show MIDI Studio)
  3. Double-click IAC Driver
  4. Check “Device is online”
  5. Click Apply

Windows (loopMIDI)

  1. Download and install loopMIDI
  2. Launch loopMIDI
  3. Create a new port (e.g., “Conductor Virtual”)
  4. Leave loopMIDI running in the background

Linux (ALSA)

# Create virtual MIDI port
sudo modprobe snd-virmidi

# Verify port created
aconnect -l

2. Configure Ableton Live MIDI Input

In Ableton Live:

  1. Open Live → Preferences (macOS) or Options → Preferences (Windows/Linux)
  2. Go to the Link, Tempo & MIDI tab
  3. In the MIDI Ports section, locate your virtual MIDI port:
    • macOS: IAC Driver (IAC Bus 1)
    • Windows: loopMIDI Port
    • Linux: Virtual Raw MIDI 1-0
  4. Enable Track and Remote for the input port
  5. Close Preferences

3. Create a Basic Transport Control Profile

Create or update your ~/.config/conductor/config.toml:

[[modes]]
name = "Ableton Live"
color = "orange"

# Play/Stop (Pad 1)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"  # macOS
# windows_title_regex = ".*Ableton Live.*"  # Windows/Linux alternative

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"  # Or "loopMIDI Port" on Windows

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 0  # Ableton Scene Launch (Scene 1)
velocity = 127
channel = 0

# Stop All Clips (Pad 2)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 2

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 1  # Custom mapped to "Stop All Clips"
value = 127
channel = 0

4. Test the Configuration

  1. Start the Conductor daemon: conductor
  2. Open Ableton Live with a project containing clips
  3. Press Pad 1 on your controller → Scene 1 should launch
  4. Press Pad 2 → All clips should stop (if mapped)

Ableton Live MIDI Mapping

Ableton Live uses a flexible MIDI mapping system. Unlike Logic Pro’s fixed CC assignments, you map each control manually using MIDI Learn.

Using MIDI Map Mode

  1. In Ableton Live, click MIDI in the top-right corner (or press ⌘M / Ctrl+M)
  2. The interface will highlight in purple
  3. Click the parameter you want to control (e.g., a volume fader, play button, device knob)
  4. Press the pad on your MIDI controller
  5. Ableton will map that MIDI message to the parameter
  6. Click MIDI again to exit MIDI Map Mode

What Ableton Learns:

  • Note messages → Note number, channel
  • CC messages → CC number, channel
  • Velocity → Mapped to 0-127 parameter range

Clip Launching

Ableton’s Session View uses a grid of clips. Each clip can be triggered via MIDI notes.

Default Clip Launch Mapping

By default, Ableton maps:

  • Track 1: Notes 0-127 (clip slots 1-128)
  • Track 2: Same notes on MIDI Channel 1
  • Track 3: Same notes on MIDI Channel 2
  • etc.

OR you can use MIDI Map Mode to assign specific notes to specific clips.

Example: Launch Clips in Track 1

# Launch Clip 1 in Track 1 (Pad 9)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 9

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 0  # Clip slot 1
velocity = 127
channel = 0  # Track 1

# Launch Clip 2 in Track 1 (Pad 10)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 10

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 1  # Clip slot 2
velocity = 127
channel = 0

Scene Launching

Scenes launch all clips in a row simultaneously:

# Launch Scene 1 (Pad 1)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 0  # Scene 1 (use MIDI Learn to discover exact note)
velocity = 127
channel = 0

Note: Scene launch note numbers vary by Ableton version and configuration. Use MIDI Learn to discover the correct note numbers for your setup.

Device Parameter Control

Ableton’s devices (instruments, effects) have mappable parameters.

Step 1: Map Device Parameters in Ableton

  1. Open a device (e.g., Filter, Reverb, Wavetable synth)
  2. Enter MIDI Map Mode (click MIDI or press ⌘M)
  3. Click a device knob or parameter
  4. Press a pad on your MIDI controller
  5. Ableton maps that control

Pro Tip: Ableton learns CC messages from your controller. To use velocity instead, you’ll need to map in Ableton using MIDI notes, not CCs.

Step 2: Configure Conductor for Velocity-Controlled Parameters

Use velocity curves to control device parameters smoothly:

# Control Filter Cutoff with Velocity (Pad 13)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 13

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 20  # Learned in Ableton MIDI Map Mode
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "Curve"
input_min = 0
input_max = 127
output_min = 0
output_max = 127
curve = 1.2  # Slightly exponential for better control

# Control Reverb Decay (Pad 14)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 14

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 21  # Learned in Ableton
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "Linear"
input_min = 0
input_max = 127
output_min = 20
output_max = 100  # Limited range (20-100% decay)

Complete Transport & Mixer Control

Here’s a comprehensive Ableton Live control profile:

[[modes]]
name = "Ableton Live"
color = "orange"

# ===== Transport Controls =====

# Play/Stop (Pad 1)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 1

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 10  # Mapped via MIDI Learn
value = 127
channel = 0

# Record (Pad 2)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 2

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 11  # Mapped via MIDI Learn
value = 127
channel = 0

# ===== Mixer Controls =====

# Track 1 Volume (Pad 9, velocity-sensitive)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 9

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 20  # Mapped via MIDI Learn to Track 1 Volume
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "Curve"
input_min = 0
input_max = 127
output_min = 0
output_max = 127
curve = 1.5  # Exponential for fader feel

# Track 2 Volume (Pad 10)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 10

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "CC"
controller = 21  # Mapped to Track 2 Volume
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "Curve"
input_min = 0
input_max = 127
output_min = 0
output_max = 127
curve = 1.5

# ===== Clip Launching =====

# Launch Clip Slot 1 (Pad 13)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 13

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 0  # Clip slot 1 in Track 1
velocity = 127
channel = 0

# Launch Clip Slot 2 (Pad 14)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 14

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 1  # Clip slot 2
velocity = 127
channel = 0

Advanced: Drum Rack Control

Ableton’s Drum Rack is a powerful sampler for drum sounds. Each pad in a Drum Rack can be triggered via MIDI notes.

Default Drum Rack Mapping

Drum Racks use notes 36-99 (C1-D#7) for the 64 pads:

  • C1 (36): Kick drum (top-left pad)
  • C#1 (37): Next pad
  • D1 (38): Snare drum (often)
  • F#1 (42): Closed hi-hat (often)
  • A#1 (46): Open hi-hat (often)

Example: Trigger Drum Rack Sounds

# Kick Drum (Pad 9)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 9

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 36  # C1 - Kick
channel = 0  # Track with Drum Rack

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"  # Use controller velocity

# Snare Drum (Pad 10)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 10

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 38  # D1 - Snare
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"

# Closed Hi-Hat (Pad 11)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 11

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 42  # F#1 - Closed Hi-Hat
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"

Push-Style Session Navigation (Advanced)

Ableton Push controllers use a grid layout for clips. You can replicate this with Conductor:

# 4x4 Clip Grid (Pads 9-12, 13-16, 17-20, 21-24)
# Track 1, Clip Slots 1-4
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 9

[[modes.mappings.action.conditions]]
type = "AppFrontmost"
bundle_id = "com.ableton.live"

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 0  # Track 1, Clip 1
velocity = 127
channel = 0

# Continue for all 16 pads...
# (Pad 10 → Track 1 Clip 2, Pad 13 → Track 2 Clip 1, etc.)

Mapping Strategy:

  • Row 1 (Pads 9-12): Track 1, Clips 1-4
  • Row 2 (Pads 13-16): Track 2, Clips 1-4
  • Row 3 (Pads 17-20): Track 3, Clips 1-4
  • Row 4 (Pads 21-24): Track 4, Clips 1-4

Troubleshooting

MIDI Port Not Appearing in Ableton

Problem: Virtual MIDI port doesn’t show up in Ableton’s Preferences.

Solution:

  • macOS: Verify IAC Driver is enabled in Audio MIDI Setup
  • Windows: Ensure loopMIDI is running in the background
  • Linux: Check aconnect -l to verify virtual port exists
  • Restart Ableton Live after creating the virtual port

Messages Sent But No Response

Problem: Conductor sends MIDI but Ableton doesn’t respond.

Solution:

  1. Verify Track and Remote are enabled for the MIDI port in Preferences
  2. Ensure the correct MIDI channel is used (usually Channel 0)
  3. Use MIDI Map Mode in Ableton to verify what MIDI messages are being received
  4. Check Conductor logs: conductor --verbose

Clip Launch Notes Don’t Match

Problem: Sending Note 0 doesn’t launch the expected clip.

Solution:

  1. Enter MIDI Map Mode in Ableton
  2. Click the clip you want to trigger
  3. Press the pad on your controller to learn the note
  4. Update your Conductor config with the correct note number

Windows: Port Name Issues

Problem: Port name “loopMIDI Port” not found.

Solution:

  1. Check the exact port name in loopMIDI application
  2. Update Conductor config to match the exact name (case-sensitive)
  3. Ensure loopMIDI is running before starting Conductor daemon

Latency Issues

Problem: Noticeable delay between pad press and Ableton response.

Solution:

  1. Lower Ableton’s audio buffer size: Preferences → Audio → Buffer Size
  2. Use native virtual MIDI drivers (IAC on macOS, ALSA on Linux) instead of third-party apps
  3. Close unnecessary applications to reduce CPU load
  4. On Windows, ensure loopMIDI is up to date

Complete Example Profile

Here’s a production-ready Ableton Live control profile:

Download: ableton-live-complete.toml (coming soon)

Features:

  • Transport controls (Play, Stop, Record)
  • 8 clip launchers (2 tracks × 4 clips)
  • 4 device parameter controls (velocity-sensitive)
  • Drum Rack triggering (4 sounds with velocity)
  • AppFrontmost conditional (only works when Ableton is active)
  • Velocity curves for expressive control

Next Steps

Further Reading

Configuration Overview

Introduction

Conductor uses a single config.toml file to define all device settings, modes, mappings, and advanced behavior. This file uses TOML (Tom’s Obvious, Minimal Language) - a human-friendly configuration format that’s easy to read and edit.

This guide provides a comprehensive overview of the configuration structure, required sections, validation, and best practices.

Configuration File Location

By default, Conductor looks for config.toml in the current working directory:

# Current directory
./config.toml

# Or specify a custom location
conductor --config /path/to/my-config.toml

Tip: Keep your config.toml in the project root directory for simplicity.

TOML Syntax Basics

If you’re new to TOML, here are the essentials:

# Comments start with #

# Simple key-value pairs
name = "My Device"
port = 2

# Booleans
auto_connect = true
debug = false

# Numbers
velocity = 127
timeout_ms = 300

# Strings (use quotes)
description = "This is a description"

# Arrays
notes = [12, 13, 14, 15]
modifiers = ["cmd", "shift"]

# Tables (sections)
[section_name]
key = "value"

# Array of tables (repeated sections)
[[modes]]
name = "Mode 1"

[[modes]]
name = "Mode 2"

Key Points:

  • Use = for assignment
  • Use [] for sections (tables)
  • Use [[]] for repeating sections (arrays of tables)
  • Strings must be in quotes
  • Comments use #

Configuration Structure Diagram

config.toml
├── [device]                    # Device identification (optional)
│   ├── name
│   └── auto_connect
│
├── [advanced_settings]         # Timing thresholds (optional)
│   ├── chord_timeout_ms
│   ├── double_tap_timeout_ms
│   └── hold_threshold_ms
│
├── [[modes]]                   # Mode definitions (required, 1+)
│   ├── name
│   ├── color
│   └── [[modes.mappings]]      # Mode-specific mappings
│       ├── description
│       ├── [trigger]
│       └── [action]
│
└── [[global_mappings]]         # Global mappings (optional)
    ├── description
    ├── [trigger]
    └── [action]

Required vs Optional Sections

Required Sections

Minimum viable config:

# At least one mode is REQUIRED
[[modes]]
name = "Default"

# At least one mapping (either in modes or global) is REQUIRED
[[modes.mappings]]
description = "Example mapping"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "a"
modifiers = []

Without these, Conductor will start but won’t do anything useful.

Optional Sections

These enhance functionality but aren’t required:

# Optional: Device identification
[device]
name = "Mikro"
auto_connect = true

# Optional: Timing customization
[advanced_settings]
chord_timeout_ms = 100
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

# Optional: Global mappings (work in all modes)
[[global_mappings]]
description = "Volume control"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

Section Breakdown

[device] Section

Optional section for device identification:

[device]
name = "Mikro"           # Friendly name (used in logs)
auto_connect = true      # Automatically connect to device on startup

Parameters:

  • name (string): Device name for logging/display
  • auto_connect (boolean): If true, auto-connect to first available MIDI device

Default behavior (if omitted): No auto-connect, manual port selection required.

[advanced_settings] Section

Optional timing customization:

[advanced_settings]
chord_timeout_ms = 100          # Max time between notes in a chord
double_tap_timeout_ms = 300     # Max time between taps for double-tap
hold_threshold_ms = 2000        # Min hold time for long press

Parameters:

  • chord_timeout_ms (u64): Milliseconds to wait for additional notes in a chord (default: 100)
  • double_tap_timeout_ms (u64): Maximum time between two taps to count as double-tap (default: 300)
  • hold_threshold_ms (u64): Minimum hold duration for long press trigger (default: 2000)

Defaults (if omitted):

chord_timeout_ms = 100
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

Use cases:

  • Increase chord_timeout_ms for slower chord playing (e.g., 200-300ms)
  • Decrease double_tap_timeout_ms for faster double-tap detection (e.g., 200ms)
  • Adjust hold_threshold_ms for longer/shorter long-press (e.g., 1000-3000ms)

[[modes]] Section

Defines a mode with its mappings. At least one mode is required.

[[modes]]
name = "Default"              # Mode name (required, unique)
color = "blue"                # LED color theme (optional)

[[modes.mappings]]            # Mode-specific mappings (array)
description = "Copy"          # Human-readable description (optional)
[modes.mappings.trigger]      # Trigger definition (required)
type = "Note"
note = 12
[modes.mappings.action]       # Action definition (required)
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

Mode Parameters:

  • name (string, required): Unique mode identifier
  • color (string, optional): LED theme color (blue, green, purple, red, yellow, white)

Mapping Parameters:

  • description (string, optional): Human-readable description (useful for documentation)
  • trigger (table, required): Defines what event triggers this mapping
  • action (table, required): Defines what happens when triggered

Multiple modes:

[[modes]]
name = "Default"
# ... mappings ...

[[modes]]
name = "Developer"
# ... mappings ...

[[modes]]
name = "Media"
# ... mappings ...

[[global_mappings]] Section

Optional mappings that work in ALL modes:

[[global_mappings]]
description = "Emergency exit"
[global_mappings.trigger]
type = "LongPress"
note = 0
hold_duration_ms = 3000
[global_mappings.action]
type = "Shell"
command = "killall conductor"

[[global_mappings]]
description = "Volume up"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

Use cases:

  • Emergency shutdown
  • Mode switching
  • Volume control
  • System-wide shortcuts (screenshot, lock screen)

Priority: Global mappings are checked after mode-specific mappings. If a mode-specific mapping matches the same trigger, it takes precedence.

Triggers and Actions

Every mapping consists of a trigger (what event to detect) and an action (what to do when triggered).

Common Trigger Types

# Basic note on/off
[trigger]
type = "Note"
note = 12

# Velocity-sensitive
[trigger]
type = "VelocityRange"
note = 12
min_velocity = 0
max_velocity = 40

# Long press
[trigger]
type = "LongPress"
note = 12
hold_duration_ms = 2000

# Double-tap
[trigger]
type = "DoubleTap"
note = 12
double_tap_timeout_ms = 300

# Chord (multiple notes together)
[trigger]
type = "NoteChord"
notes = [12, 13, 14]
chord_timeout_ms = 100

# Encoder rotation
[trigger]
type = "EncoderTurn"
direction = "Clockwise"  # or "CounterClockwise"

# Control change
[trigger]
type = "CC"
cc_number = 1

See Triggers Reference for complete list.

Common Action Types

# Keyboard shortcut
[action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

# Type text
[action]
type = "Text"
text = "Hello, world!"

# Open application
[action]
type = "Launch"
app = "Terminal"

# Run shell command
[action]
type = "Shell"
command = "cargo test"

# Volume control
[action]
type = "VolumeControl"
operation = "Up"  # Up, Down, Mute, Set

# Change mode
[action]
type = "ModeChange"
mode = "next"  # next, previous, or mode name

# Sequence of actions
[action]
type = "Sequence"
actions = [
    { type = "Launch", app = "Spotify" },
    { type = "Delay", duration_ms = 1000 },
    { type = "Keystroke", keys = "space", modifiers = [] }
]

See Actions Reference for complete list.

Minimal Configuration Example

The simplest possible working config:

[[modes]]
name = "Default"

[[modes.mappings]]
description = "Test"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "a"
modifiers = []

What this does:

  • Creates one mode called “Default”
  • Pressing pad 12 types “A”

Full Configuration Example

A complete, production-ready config:

# Device configuration
[device]
name = "Mikro"
auto_connect = true

# Advanced timing settings
[advanced_settings]
chord_timeout_ms = 100
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

# Mode 1: General productivity
[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Copy"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Paste"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Switch app"
[modes.mappings.trigger]
type = "Note"
note = 14
[modes.mappings.action]
type = "Keystroke"
keys = "tab"
modifiers = ["cmd"]

# Mode 2: Development
[[modes]]
name = "Developer"
color = "green"

[[modes.mappings]]
description = "Run tests"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Shell"
command = "cargo test"

[[modes.mappings]]
description = "Build release"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Shell"
command = "cargo build --release"

# Mode 3: Media control
[[modes]]
name = "Media"
color = "purple"

[[modes.mappings]]
description = "Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 12
[modes.mappings.action]
type = "Keystroke"
keys = "space"
modifiers = []

[[modes.mappings]]
description = "Next track"
[modes.mappings.trigger]
type = "Note"
note = 13
[modes.mappings.action]
type = "Keystroke"
keys = "right"
modifiers = ["cmd"]

# Global mappings (work in all modes)
[[global_mappings]]
description = "Emergency exit (hold 3 seconds)"
[global_mappings.trigger]
type = "LongPress"
note = 0
hold_duration_ms = 3000
[global_mappings.action]
type = "Shell"
command = "killall conductor"

[[global_mappings]]
description = "Next mode"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = "next"

[[global_mappings]]
description = "Previous mode"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "CounterClockwise"
[global_mappings.action]
type = "ModeChange"
mode = "previous"

[[global_mappings]]
description = "Volume up"
[global_mappings.trigger]
type = "Note"
note = 15
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

[[global_mappings]]
description = "Volume down"
[global_mappings.trigger]
type = "Note"
note = 11
[global_mappings.action]
type = "VolumeControl"
operation = "Down"

Configuration Validation

Loading Process

When Conductor starts, it:

  1. Locates config.toml
  2. Parses TOML syntax
  3. Validates structure and values
  4. Compiles triggers and actions
  5. Initializes mapping engine

Common Validation Errors

Syntax Error

Error:

TOML parse error: expected `=`, but found `:`

Cause: Invalid TOML syntax (e.g., using : instead of =)

Fix: Check TOML syntax rules, ensure proper formatting


Missing Required Field

Error:

Missing field `type` in trigger definition

Cause: Trigger or action missing required type field

Fix: Add required fields:

[trigger]
type = "Note"  # Required
note = 12      # Required for Note trigger

Invalid Field Value

Error:

Invalid trigger type: 'NotePress' (expected: Note, VelocityRange, LongPress, ...)

Cause: Typo or unsupported value

Fix: Check spelling, refer to Triggers Reference


Mode Name Conflict

Error:

Duplicate mode name: 'Default'

Cause: Two modes with the same name

Fix: Ensure each mode has a unique name


No Mappings Defined

Warning (not an error, but Conductor won’t do anything):

Warning: No mappings defined. Controller won't trigger any actions.

Fix: Add at least one mapping in a mode or global_mappings

Validation Checklist

Before running Conductor, verify:

  • TOML syntax is valid (use cargo check or online TOML validator)
  • At least one [[modes]] section exists
  • Each mode has a unique name
  • At least one mapping exists (in modes or global)
  • Every mapping has both trigger and action
  • All trigger/action types are spelled correctly
  • Note numbers match your device (use pad_mapper to verify)
  • File is saved as UTF-8 (not UTF-16 or other encoding)

Configuration Tips and Best Practices

1. Use Comments Liberally

# ===================================
# MODE 1: General Productivity
# ===================================
[[modes]]
name = "Default"
color = "blue"

# Pad layout:
# 12=Copy  13=Paste  14=Undo  15=Redo
# 8=Save   9=Open    10=Find  11=App Switch
# 4=Cut    5=Select  6=Delete 7=Screenshot
# 0=Escape 1=Enter   2=Tab    3=Space

[[modes.mappings]]
description = "Copy (Pad 12)"
# ... trigger and action ...
# Clipboard operations
[[modes.mappings]]
description = "Copy"
# ... copy mapping ...

[[modes.mappings]]
description = "Cut"
# ... cut mapping ...

[[modes.mappings]]
description = "Paste"
# ... paste mapping ...

# Navigation
[[modes.mappings]]
description = "Next tab"
# ... next tab ...

3. Use Descriptive Names

# Good: Clear and specific
[[modes]]
name = "Development-Rust"

# Bad: Ambiguous
[[modes]]
name = "Mode2"

4. Test Incrementally

When building a complex config:

  1. Start with one mode and one mapping
  2. Test it works
  3. Add more mappings one at a time
  4. Test after each addition

5. Keep a Backup

# Before major changes
cp config.toml config.toml.backup

# Or use version control
git add config.toml
git commit -m "Add media mode mappings"

6. Use Variables (for consistency)

While TOML doesn’t have variables, you can use comments to document repeated values:

# Standard velocity ranges (document once, use everywhere):
# Soft: 0-40
# Medium: 41-80
# Hard: 81-127

[[modes.mappings]]
description = "Soft press"
[modes.mappings.trigger]
type = "VelocityRange"
note = 12
min_velocity = 0    # Soft range
max_velocity = 40
# ... action ...

[[modes.mappings]]
description = "Hard press"
[modes.mappings.trigger]
type = "VelocityRange"
note = 12
min_velocity = 81   # Hard range
max_velocity = 127
# ... action ...

Configuration Hot-Reloading (Future Feature)

Note: Config hot-reloading is not yet implemented in the current version. To reload config changes:

# Current workaround: Restart Conductor
killall conductor
./target/release/conductor 2

Planned feature (Phase 2):

  • Watch config.toml for changes
  • Automatically reload without restarting
  • Validate before applying (no downtime on errors)

See Also

Troubleshooting

Config Not Loading

Problem: Conductor says “Config file not found”

Solutions:

# Check file exists
ls -la config.toml

# Check current directory
pwd

# Specify full path
./conductor --config /full/path/to/config.toml

Syntax Errors

Problem: “TOML parse error”

Solutions:

  1. Validate TOML syntax online: https://www.toml-lint.com/
  2. Check quotes, brackets, and indentation
  3. Look for typos in section names

Mappings Not Working

Problem: Config loads but pads don’t trigger actions

Debug steps:

# 1. Verify note numbers
cargo run --bin pad_mapper

# 2. Enable debug logging
DEBUG=1 cargo run --release 2

# 3. Check MIDI events
cargo run --bin midi_diagnostic 2

See Common Issues for more troubleshooting.


Last Updated: November 11, 2025 Config Version: 0.1 (current implementation)

Triggers Reference

Triggers define the input events that activate mappings in Conductor. This page provides comprehensive documentation for all supported trigger types across MIDI controllers and game controllers.

Overview

Conductor supports two primary input protocols:

  • MIDI Controllers (v1.0+): MIDI keyboards, pad controllers, encoders, and touch strips
  • Game Controllers (HID) (v3.0+): Gamepads, joysticks, racing wheels, flight sticks, HOTAS, arcade controllers, and custom SDL2-compatible HID devices

Both protocols share the same unified configuration format and can be used simultaneously in hybrid setups.

ID Range Allocation

To prevent conflicts between MIDI and HID devices, Conductor uses distinct ID ranges:

RangeProtocolUsed ForExamples
0-127MIDINotes, CC, EncodersMIDI note C4=60, CC Mod Wheel=1
128-255Game ControllersButtons, Axes, TriggersGamepad A button=128, Left stick X=128

This non-overlapping allocation ensures seamless coexistence of MIDI and gamepad inputs without configuration conflicts.


MIDI Triggers

MIDI triggers respond to events from MIDI controllers such as keyboards, pad controllers, and control surfaces.

Note

Basic note trigger with optional velocity threshold.

Use Case: Trigger actions on specific note presses (e.g., pad hits, keyboard notes).

[[modes.mappings]]
description = "Pad 1: Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 36          # MIDI note number (0-127)
velocity_min = 1   # Optional: Minimum velocity to trigger

[modes.mappings.action]
type = "Keystroke"
keys = "Space"

Parameters:

  • note (required): MIDI note number (0-127)
  • velocity_min (optional): Minimum velocity to trigger (0-127). Omit to trigger on any velocity.

Examples:

# Trigger on any velocity
note = 60  # Middle C

# Trigger only on hard hits (velocity > 80)
note = 36
velocity_min = 80

VelocityRange

Velocity-sensitive trigger that classifies note presses into soft, medium, and hard levels.

Use Case: Execute different actions based on how hard a pad or key is pressed.

[[modes.mappings]]
description = "Velocity-sensitive pad"
[modes.mappings.trigger]
type = "VelocityRange"
note = 36
soft_max = 40      # Optional: Max velocity for soft (default: 40)
medium_max = 80    # Optional: Max velocity for medium (default: 80)

[modes.mappings.action]
type = "Conditional"
condition = { type = "VelocityLevel", level = "Hard" }
then_action = { type = "Keystroke", keys = "F1" }
else_action = { type = "Keystroke", keys = "Space" }

Velocity Classification:

  • Soft: 0 to soft_max (default: 0-40)
  • Medium: soft_max + 1 to medium_max (default: 41-80)
  • Hard: medium_max + 1 to 127 (default: 81-127)

Parameters:

  • note (required): MIDI note number (0-127)
  • soft_max (optional): Maximum velocity for soft (default: 40)
  • medium_max (optional): Maximum velocity for medium (default: 80)

LongPress

Triggers when a note is held for longer than a specified duration.

Use Case: Hold a pad/key for extended actions (e.g., hold for shutdown, tap for play).

[[modes.mappings]]
description = "Hold pad for 2 seconds to shutdown"
[modes.mappings.trigger]
type = "LongPress"
note = 40
duration_ms = 2000  # Optional: Hold duration in milliseconds (default: 2000)

[modes.mappings.action]
type = "Shell"
command = "shutdown -h now"

Parameters:

  • note (required): MIDI note number (0-127)
  • duration_ms (optional): Hold duration in milliseconds (default: 2000)

Timing:

  • Default threshold: 2000ms (2 seconds)
  • Configurable globally via advanced_settings.hold_threshold_ms

DoubleTap

Triggers when a note is pressed and released twice within a time window.

Use Case: Double-tap a pad for quick actions (e.g., skip track, open app).

[[modes.mappings]]
description = "Double-tap pad to skip track"
[modes.mappings.trigger]
type = "DoubleTap"
note = 48
timeout_ms = 300  # Optional: Time window for double-tap (default: 300)

[modes.mappings.action]
type = "Keystroke"
keys = "Next"

Parameters:

  • note (required): MIDI note number (0-127)
  • timeout_ms (optional): Time window in milliseconds for detecting double-tap (default: 300)

Timing:

  • Default window: 300ms
  • Configurable globally via advanced_settings.double_tap_timeout_ms

NoteChord

Triggers when multiple notes are pressed simultaneously (within a narrow time window).

Use Case: Combine multiple pads/keys for complex actions (e.g., emergency exit, mode switch).

[[modes.mappings]]
description = "Pads 1+2+3: Emergency exit"
[modes.mappings.trigger]
type = "NoteChord"
notes = [36, 37, 38]  # List of MIDI notes
timeout_ms = 50       # Optional: Simultaneous press window (default: 50)

[modes.mappings.action]
type = "Shell"
command = "killall conductor"

Parameters:

  • notes (required): Array of MIDI note numbers (0-127)
  • timeout_ms (optional): Time window in milliseconds for simultaneous presses (default: 50)

Timing:

  • Default window: 50ms
  • Configurable globally via advanced_settings.chord_timeout_ms

EncoderTurn

Triggers on encoder/knob rotation (MIDI CC messages).

Use Case: Respond to encoder turns for volume, scrolling, or mode switching.

[[modes.mappings]]
description = "Encoder clockwise: Volume up"
[modes.mappings.trigger]
type = "EncoderTurn"
cc = 1                    # MIDI CC number (0-127)
direction = "Clockwise"   # Optional: "Clockwise", "CounterClockwise", or omit for both

[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Parameters:

  • cc (required): Control Change number (0-127)
  • direction (optional): Filter by direction:
    • "Clockwise": Encoder turned right/up (CC value increasing)
    • "CounterClockwise": Encoder turned left/down (CC value decreasing)
    • Omit: Respond to both directions

Common CC Numbers:

  • CC 1: Modulation Wheel
  • CC 7: Volume
  • CC 10: Pan
  • CC 74: Brightness/Filter Cutoff

Aftertouch

Triggers based on channel pressure (aftertouch) messages.

Use Case: Respond to pressure sensitivity on pads or keys.

[[modes.mappings]]
description = "Aftertouch: Vibrato"
[modes.mappings.trigger]
type = "Aftertouch"
pressure_min = 64  # Optional: Minimum pressure (0-127)

[modes.mappings.action]
type = "SendMidi"
port = "Virtual Output"
message_type = "CC"
channel = 0
controller = 1
value = 127

Parameters:

  • pressure_min (optional): Minimum pressure value to trigger (0-127)

PitchBend

Triggers based on pitch bend messages from touch strips or pitch bend wheels.

Use Case: Respond to touch strip movements or pitch wheel changes.

[[modes.mappings]]
description = "Pitch bend up"
[modes.mappings.trigger]
type = "PitchBend"
value_min = 8192   # Optional: Minimum value (0-16383, 8192 = center)
value_max = 16383  # Optional: Maximum value

[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Parameters:

  • value_min (optional): Minimum pitch bend value (0-16383)
  • value_max (optional): Maximum pitch bend value (0-16383)

Value Range:

  • 0-8191: Bend down
  • 8192: Center (no bend)
  • 8193-16383: Bend up

CC (Control Change)

Generic trigger for any MIDI Control Change message.

Use Case: Respond to sliders, knobs, buttons, or pedals that send CC messages.

[[modes.mappings]]
description = "Sustain pedal"
[modes.mappings.trigger]
type = "CC"
cc = 64           # CC number (0-127)
value_min = 64    # Optional: Minimum value to trigger (0-127)

[modes.mappings.action]
type = "SendMidi"
port = "Virtual Output"
message_type = "CC"
channel = 0
controller = 64
value = 127

Parameters:

  • cc (required): Control Change number (0-127)
  • value_min (optional): Minimum value to trigger (0-127)

Common CC Numbers:

  • CC 1: Modulation Wheel
  • CC 7: Volume
  • CC 10: Pan
  • CC 64: Sustain Pedal
  • CC 74: Brightness/Filter Cutoff

Game Controllers (HID) Triggers (v3.0+)

Game Controllers (HID) triggers respond to events from gamepad controllers, joysticks, racing wheels, flight sticks, HOTAS systems, arcade controllers, and any SDL2-compatible HID device.

Supported Device Types:

  • Gamepads: Xbox 360/One/Series X|S, PlayStation DualShock 4/DualSense, Nintendo Switch Pro Controller
  • Joysticks: Flight sticks, arcade sticks with analog axes and buttons
  • Racing Wheels: Logitech, Thrustmaster, Fanatec wheels with pedals
  • Flight Sticks: Thrustmaster T.Flight, Logitech Extreme 3D Pro
  • HOTAS: Hands On Throttle And Stick systems for flight simulation
  • Arcade Controllers: Fight sticks, arcade pads
  • Custom Controllers: Any SDL2-compatible HID device

All gamepad triggers use ID range 128-255 to avoid conflicts with MIDI (0-127).


GamepadButton

Standard button press trigger for face buttons, D-pad, shoulders, triggers, and menu buttons.

Use Case: Trigger actions on gamepad button presses (e.g., A button to confirm, D-pad for navigation).

[[modes.mappings]]
description = "A button: Confirm"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128       # Gamepad button ID (128-255)
velocity_min = 1   # Optional: Minimum pressure (0-127)

[modes.mappings.action]
type = "Keystroke"
keys = "Return"

Parameters:

  • button (required): Gamepad button ID (128-255)
  • velocity_min (optional): Minimum pressure to trigger (0-127). Useful for analog buttons on some controllers.

Standard Button IDs:

ID RangeButton TypeSpecific Buttons
128-131Face Buttons128=South (A/Cross/B), 129=East (B/Circle/A), 130=West (X/Square/Y), 131=North (Y/Triangle/X)
132-135D-Pad132=Up, 133=Down, 134=Left, 135=Right
136-137Shoulders136=L1/LB/L, 137=R1/RB/R
138-139Stick Clicks138=L3 (left stick click), 139=R3 (right stick click)
140-142Menu Buttons140=Start, 141=Select/Back/View, 142=Guide/Home/PS
143-144Digital Triggers143=L2/LT/ZL, 144=R2/RT/ZR (digital press, not analog)

Cross-Platform Button Mapping:

ButtonXboxPlayStationNintendo Switch
South (128)ACross (×)B
East (129)BCircle (○)A
West (130)XSquare (□)Y
North (131)YTriangle (△)X
L1 (136)LBL1L
R1 (137)RBR1R
L2 (143)LTL2ZL
R2 (144)RTR2ZR

Examples:

# Xbox A button / PlayStation Cross / Switch B
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "Return"

# D-Pad Up for navigation
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

# Left shoulder (LB/L1/L) for tab switching
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 136
[modes.mappings.action]
type = "Keystroke"
keys = "Tab"
modifiers = ["cmd", "shift"]

# Xbox Guide button for Spotlight
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 142
[modes.mappings.action]
type = "Keystroke"
keys = "Space"
modifiers = ["cmd"]

Device-Specific Notes:

  • Racing Wheels: Buttons on the wheel hub (paddle shifters, D-pad, rotary encoders) map to standard button IDs
  • Flight Sticks: Hat switches map to D-pad IDs (132-135), trigger button to face button ID
  • HOTAS: Throttle buttons, base buttons, and hat switches use extended button IDs (145+)
  • Arcade Controllers: All buttons (typically 6-8 action buttons) map to face button and shoulder IDs

GamepadButtonChord

Multiple gamepad buttons pressed simultaneously (chord detection).

Use Case: Combine multiple buttons for complex actions (e.g., LB+RB for mode switch, A+B for screenshot).

[[modes.mappings]]
description = "LB+RB: Switch mode"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]  # Array of button IDs (128-255)
timeout_ms = 50       # Optional: Simultaneous press window (default: 50)

[modes.mappings.action]
type = "ModeChange"
mode = "Media"

Parameters:

  • buttons (required): Array of gamepad button IDs (128-255). All buttons must be pressed within the timeout window.
  • timeout_ms (optional): Time window in milliseconds for simultaneous presses (default: 50)

Timing:

  • Default window: 50ms
  • Configurable globally via advanced_settings.chord_timeout_ms

Examples:

# Two-button chord: A+B for screenshot
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [128, 129]
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

# Three-button chord: LB+RB+Start for emergency exit
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137, 140]
timeout_ms = 100
[modes.mappings.action]
type = "Shell"
command = "killall conductor"

# Racing wheel: Both paddle shifters for mode change
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]  # Left + Right paddles
[modes.mappings.action]
type = "ModeChange"
mode = "Racing"

GamepadAnalogStick

Analog stick movement detection with directional filtering.

Use Case: Respond to analog stick movements for scrolling, navigation, or camera control.

[[modes.mappings]]
description = "Right stick right: Forward"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130              # Stick axis ID (128-131)
direction = "Clockwise" # Optional: "Clockwise" (right/up) or "CounterClockwise" (left/down)

[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]

Parameters:

  • axis (required): Analog stick axis ID (128-131)
  • direction (optional): Filter by direction:
    • "Clockwise": Right (X-axis) or Up (Y-axis)
    • "CounterClockwise": Left (X-axis) or Down (Y-axis)
    • Omit: Respond to both directions

Analog Stick Axis IDs:

IDAxisDescription
128Left Stick XLeft stick horizontal (left = CounterClockwise, right = Clockwise)
129Left Stick YLeft stick vertical (down = CounterClockwise, up = Clockwise)
130Right Stick XRight stick horizontal (left = CounterClockwise, right = Clockwise)
131Right Stick YRight stick vertical (down = CounterClockwise, up = Clockwise)

Dead Zone:

  • Automatic 10% dead zone prevents false triggers from stick drift
  • Axis values below threshold are ignored

Value Normalization:

  • Input range: -1.0 to +1.0 (raw analog values)
  • Output range: 0-255 (normalized to MIDI-like range)
  • Formula: ((value + 1.0) * 127.5) as u8

Examples:

# Left stick right for next item
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 128
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"

# Right stick up for volume up
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 131
direction = "Clockwise"
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

# Left stick (any direction) for scrolling
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 129  # Left stick Y
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

Device-Specific Notes:

  • Flight Sticks: Primary stick axes (pitch/roll) typically map to right stick IDs (130-131)
  • HOTAS: Throttle axis may map to left stick Y (129), rudder to left stick X (128)
  • Racing Wheels: Steering wheel rotation maps to left stick X (128)
  • Arcade Sticks: Analog joystick (if present) maps to left stick (128-129)

GamepadTrigger

Analog trigger pull detection with threshold (L2/R2, LT/RT, ZL/ZR).

Use Case: Respond to analog trigger pulls for fine-grained control (e.g., volume, acceleration, braking).

[[modes.mappings]]
description = "Right trigger: Volume up"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133   # Trigger ID (132-133)
threshold = 64  # Optional: Minimum pull value (0-127)

[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Parameters:

  • trigger (required): Analog trigger ID (132-133)
  • threshold (optional): Minimum pull value to trigger (0-127). Omit to trigger on any pull.

Analog Trigger IDs:

IDTriggerDescription
132Left TriggerL2 (PlayStation), LT (Xbox), ZL (Switch)
133Right TriggerR2 (PlayStation), RT (Xbox), ZR (Switch)

Value Range:

  • 0: Trigger fully released
  • 127: Trigger fully pulled
  • Typical threshold: 64 (half-pull)

Examples:

# Right trigger half-pull for volume up
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 64
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

# Left trigger full-pull for screenshot
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132
threshold = 100
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

# Right trigger (any pull) for next track
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
[modes.mappings.action]
type = "Keystroke"
keys = "Next"

Device-Specific Notes:

  • Racing Wheels: Throttle pedal maps to right trigger (133), brake pedal to left trigger (132)
  • Flight Sticks: Some flight sticks have analog trigger buttons that map to these IDs
  • HOTAS: Throttle position may map to right trigger (133)
  • DualSense (PS5): Supports adaptive trigger resistance (future feature)

Threshold Guidelines:

ThresholdUse CaseSensitivity
0-20Feather touchVery sensitive
40-60Light pullMedium sensitivity
64Half-pullBalanced (recommended)
80-100Firm pullLow sensitivity
110-127Full pullVery low sensitivity

Hybrid MIDI + Game Controller Mappings

Conductor v3.0 supports using MIDI and gamepad controllers simultaneously. The non-overlapping ID ranges (MIDI: 0-127, Gamepad: 128-255) ensure seamless coexistence.

Use Cases

  • Live Performance: MIDI controller for music, gamepad for lighting/effects
  • Production: MIDI keyboard for notes, gamepad for transport controls
  • Gaming + Music: Gamepad for game macros, MIDI pads for audio triggers
  • Accessibility: Use whichever input device is most comfortable

Example: Hybrid Configuration

[device]
name = "Hybrid"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

[[modes]]
name = "Hybrid"
color = "cyan"

# MIDI pad for play/pause
[[modes.mappings]]
description = "MIDI Pad 1: Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 36  # MIDI note (0-127)
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

# Gamepad A button for confirm
[[modes.mappings]]
description = "Gamepad A: Confirm"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Gamepad button (128-255)
[modes.mappings.action]
type = "Keystroke"
keys = "Return"

# MIDI encoder for volume
[[modes.mappings]]
description = "MIDI Encoder: Volume"
[modes.mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

# Gamepad right stick for scrolling
[[modes.mappings]]
description = "Gamepad Right Stick: Scroll"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 131  # Right stick Y
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

# MIDI + Gamepad chord for emergency exit
[[global_mappings]]
description = "MIDI Pad 1 + Gamepad A: Emergency Exit"
[global_mappings.trigger]
type = "NoteChord"
notes = [36, 128]  # Mix MIDI and gamepad IDs in chord
timeout_ms = 100
[global_mappings.action]
type = "Shell"
command = "killall conductor"

Hybrid Configuration Tips

  1. ID Separation: Always use 0-127 for MIDI, 128-255 for gamepad
  2. Chord Detection: You can mix MIDI and gamepad IDs in NoteChord and GamepadButtonChord triggers
  3. Mode Switching: Use either MIDI or gamepad inputs to switch modes
  4. Global Mappings: Define device-agnostic global mappings that work across both protocols
  5. Input Mode: Set input_mode = "Both" in [device] to enable hybrid mode (automatic in v3.0)

Advanced Trigger Patterns

Long Press with Gamepad

[[modes.mappings]]
description = "Hold A button for 2 seconds"
[modes.mappings.trigger]
type = "LongPress"
note = 128  # Gamepad button ID
duration_ms = 2000
[modes.mappings.action]
type = "Shell"
command = "shutdown -h now"

Double-Tap with Gamepad

[[modes.mappings]]
description = "Double-tap A button"
[modes.mappings.trigger]
type = "DoubleTap"
note = 128  # Gamepad button ID
timeout_ms = 300
[modes.mappings.action]
type = "Keystroke"
keys = "Next"

Velocity-Sensitive Gamepad Button

Some gamepads (e.g., DualShock 4, DualSense) have pressure-sensitive buttons:

[[modes.mappings]]
description = "Pressure-sensitive X button"
[modes.mappings.trigger]
type = "VelocityRange"
note = 130  # Gamepad X button
soft_max = 40
medium_max = 80
[modes.mappings.action]
type = "Conditional"
condition = { type = "VelocityLevel", level = "Hard" }
then_action = { type = "Keystroke", keys = "F1" }
else_action = { type = "Keystroke", keys = "Space" }

ID Reference Tables

MIDI Note Numbers (0-127)

OctaveCC#DD#EFF#GG#AA#B
-201234567891011
-1121314151617181920212223
0242526272829303132333435
1363738394041424344454647
2484950515253545556575859
3606162636465666768697071
4727374757677787980818283
5848586878889909192939495
696979899100101102103104105106107
7108109110111112113114115116117118119
8120121122123124125126127----

Common MIDI Note Examples:

  • 36 (C1): Typical bass drum / first pad on controllers
  • 60 (C3): Middle C
  • 127 (G8): Highest MIDI note

Gamepad Button IDs (128-255)

IDButtonXboxPlayStationSwitchDevice Type
128SouthACross (×)BGamepad
129EastBCircle (○)AGamepad
130WestXSquare (□)YGamepad
131NorthYTriangle (△)XGamepad
132D-Pad UpD-UpD-UpD-UpGamepad
133D-Pad DownD-DownD-DownD-DownGamepad
134D-Pad LeftD-LeftD-LeftD-LeftGamepad
135D-Pad RightD-RightD-RightD-RightGamepad
136Left ShoulderLBL1LGamepad
137Right ShoulderRBR1RGamepad
138Left Stick ClickL3L3L-StickGamepad
139Right Stick ClickR3R3R-StickGamepad
140StartMenuOptions+Gamepad
141SelectViewShare-Gamepad
142GuideXboxPSHomeGamepad
143Left TriggerLTL2ZLGamepad (digital)
144Right TriggerRTR2ZRGamepad (digital)

Gamepad Axis IDs (128-133)

IDAxisDescriptionDevice Type
128Left Stick XHorizontal (left/right)Gamepad, Racing Wheel
129Left Stick YVertical (up/down)Gamepad, HOTAS Throttle
130Right Stick XHorizontal (left/right)Gamepad, Flight Stick
131Right Stick YVertical (up/down)Gamepad, Flight Stick
132Left TriggerAnalog (0-127)Gamepad, Racing Wheel (brake)
133Right TriggerAnalog (0-127)Gamepad, Racing Wheel (throttle)

Device-Specific Mappings

Racing Wheels:

  • 128 (Left Stick X): Steering wheel rotation (left = CounterClockwise, right = Clockwise)
  • 132 (Left Trigger): Brake pedal (0-127)
  • 133 (Right Trigger): Throttle/gas pedal (0-127)
  • 136-137: Paddle shifters (left/right)
  • 128-144: Wheel buttons (D-pad, face buttons on wheel hub)

Flight Sticks:

  • 130 (Right Stick X): Stick roll (left/right)
  • 131 (Right Stick Y): Stick pitch (forward/back)
  • 132-135: Hat switch (maps to D-pad IDs)
  • 128-131: Primary action buttons (trigger, thumb buttons)

HOTAS (Hands On Throttle And Stick):

  • 129 (Left Stick Y): Throttle axis
  • 128 (Left Stick X): Rudder/twist axis
  • 130-131: Stick axes (pitch/roll)
  • 132-135: Hat switches
  • 136-144: Base buttons, throttle buttons

Arcade Sticks:

  • 128-131: 4-way/8-way joystick (if analog)
  • 132-135: D-pad (if digital joystick)
  • 128-137: Action buttons (6-8 button layout)

Configuration Tips

1. Finding Button IDs

Use the MIDI Learn feature (v3.0) to automatically detect button IDs:

  1. Open Conductor GUI
  2. Navigate to a mapping
  3. Click “MIDI Learn”
  4. Press the desired button on your gamepad
  5. The button ID will be auto-filled

2. Testing Triggers

Use the live event console in Conductor GUI to see real-time trigger events:

  1. Open Conductor GUI
  2. Navigate to “Event Console”
  3. Press buttons/move sticks on your gamepad
  4. Observe the event type and ID
  5. Use this information to configure triggers

3. Device Templates

Conductor includes pre-configured templates for popular devices:

  • Xbox Controller: xbox-controller.toml
  • PlayStation Controller: playstation-controller.toml
  • Switch Pro Controller: switch-pro-controller.toml

Load a template via GUI:

  1. Open Conductor GUI
  2. Navigate to “Device Templates”
  3. Filter by device type
  4. Select template and click “Create Config”

4. Global vs Mode Mappings

  • Global Mappings: Work across all modes (e.g., emergency exit, mode switching)
  • Mode Mappings: Active only in specific modes (e.g., media controls in “Media” mode)

Best Practice: Use global mappings for critical actions (exit, mode switch) and mode mappings for context-specific actions.

5. Timing Configuration

Fine-tune timing thresholds in [advanced_settings]:

[advanced_settings]
chord_timeout_ms = 50       # Multi-button chord detection
double_tap_timeout_ms = 300 # Double-tap detection
hold_threshold_ms = 2000    # Long press threshold

Recommendations:

  • Chord: 50ms (default) works well for most controllers
  • Double-Tap: 300ms (default) balances speed and accuracy
  • Long Press: 2000ms (default) prevents accidental triggers

Troubleshooting

Gamepad Not Detected

  1. Verify USB/Bluetooth connection
  2. Check if gamepad is recognized by OS
  3. Ensure Conductor has Input Monitoring permissions (macOS)
  4. Try reconnecting the gamepad

Button IDs Not Working

  1. Use MIDI Learn to verify correct button ID
  2. Check ID range (128-255 for gamepad)
  3. Ensure gamepad is SDL2-compatible
  4. Consult device-specific documentation

Analog Stick False Triggers

  1. Increase dead zone threshold (default: 10%)
  2. Clean analog stick (dust/debris can cause drift)
  3. Use directional filtering (direction parameter)

Analog Trigger Sensitivity

  1. Adjust threshold parameter (64 = half-pull)
  2. Lower threshold for more sensitivity
  3. Higher threshold for less sensitivity
  4. Test with event console to find optimal value


Last Updated: 2025-01-21 Version: 3.0 Maintainer: Amiable Team

Actions Reference

Actions define what happens when a trigger condition is met. Conductor supports a rich set of action types that can be composed in powerful ways.

Quick Reference

Action TypeDescriptionComplexity
KeystrokeSend keyboard shortcutsSimple
TextType text stringsSimple
LaunchOpen applicationsSimple
ShellExecute shell commandsSimple
SequenceChain multiple actionsModerate
DelayAdd timing controlSimple
MouseClickSimulate mouse clicksSimple
RepeatRepeat actions N timesModerate
VolumeControlSystem volume controlSimple
ModeChangeSwitch mapping modesSimple
SendMidiSend MIDI messagesModerate
ConditionalContext-aware executionAdvanced

Simple Actions

Keystroke

Send keyboard shortcuts with modifiers. The most common action type for productivity workflows.

# Single key
[modes.mappings.action]
type = "Keystroke"
keys = "space"

# With modifiers
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]  # Cmd+C

# Multiple modifiers
[modes.mappings.action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd", "shift"]  # Cmd+Shift+T

Available Modifiers:

  • cmd / command / meta - Command key (macOS) / Windows key
  • ctrl / control - Control key
  • alt / option - Alt/Option key
  • shift - Shift key

Special Keys:

  • Navigation: up, down, left, right, home, end, pageup, pagedown
  • Editing: backspace, delete, tab, return, enter, escape, esc
  • Function: f1 through f12
  • Other: space

Text

Type arbitrary text strings with full Unicode support. Uses keyboard simulation to type character-by-character, making it reliable for Unicode and complex text.

[modes.mappings.action]
type = "Text"
text = "Hello, World!"

Use Cases:

  • Email signatures and contact information
  • Code snippets and boilerplate
  • Commonly used phrases and templates
  • Form auto-fill with saved data
  • Multi-language text input
  • Text expansion (abbreviation → full text)

Advanced Examples:

Multi-line Code Snippet:

[modes.mappings.action]
type = "Text"
text = """
fn main() {
    println!(\"Hello, World!\");
}
"""

Email Signature:

[modes.mappings.action]
type = "Text"
text = """
Best regards,
John Doe
Senior Developer
john@example.com
"""

Unicode and Emoji:

[modes.mappings.action]
type = "Text"
text = "✅ Task completed! 🎉"

Special Characters:

[modes.mappings.action]
type = "Text"
text = "!@#$%^&*()_+-=[]{}|;':\",./<>?"

Slow Typing for Laggy UIs:

[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Text", text = "user" },
    { type = "Delay", ms = 100 },
    { type = "Text", text = "name" },
    { type = "Delay", ms = 100 },
    { type = "Text", text = "123" }
]

TOML String Escaping Tips:

  • Escape quotes: "He said \"hello\""
  • Multiline strings: """Line 1\nLine 2"""
  • Literal strings (no escaping): 'C:\Users\file.txt'
  • Backslashes: Double them \\ or use literal strings

Platform Behavior:

  • Does NOT use clipboard (leaves clipboard unchanged)
  • Types character-by-character via keyboard simulation
  • Respects active keyboard layout
  • Requires text input focus in target app

Troubleshooting:

  • If text doesn’t type: Ensure app has input focus
  • If Unicode fails: Check app supports UTF-8
  • If too fast: Use Sequence with Delay actions
  • If wrong characters: Check keyboard layout or escape TOML strings

Launch

Open applications by name or path. Simple, cross-platform application launcher.

Basic Usage:

[modes.mappings.action]
type = "Launch"
app = "Terminal"  # macOS application name

Full Path:

[modes.mappings.action]
type = "Launch"
app = "/Applications/Utilities/Terminal.app"

Script Execution:

[modes.mappings.action]
type = "Launch"
app = "/Users/username/scripts/backup.sh"  # Must have execute permissions (chmod +x)

Multiple Apps (Streaming Setup):

[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Launch", app = "OBS" },
    { type = "Delay", ms = 2000 },  # Wait for OBS to load
    { type = "Launch", app = "Discord" },
    { type = "Delay", ms = 1000 },
    { type = "Launch", app = "Spotify" }
]

Development Environment:

[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Launch", app = "Visual Studio Code" },
    { type = "Delay", ms = 1500 },
    { type = "Launch", app = "Terminal" },
    { type = "Delay", ms = 500 },
    { type = "Launch", app = "Safari" }
]

Platform Behavior:

  • macOS: Uses open -a command
    • App names: “Safari”, “Visual Studio Code”, “Logic Pro”
    • Full paths: “/Applications/Safari.app”
    • If running, brings to front (doesn’t launch duplicate)
  • Linux: Direct executable launch
    • Requires full path or executable in $PATH
    • Typically launches new instance even if running
  • Windows: Uses cmd /C start command
    • Accepts app names or paths
    • Uses file associations

Troubleshooting:

  • Test manually: open -a "App Name" (macOS) or which app-name (Linux)
  • Enable debug logging: DEBUG=1 cargo run --release 2
  • Use full paths if app names don’t work
  • Ensure scripts have execute permissions: chmod +x script.sh

Shell

Execute system commands directly without shell interpreter (secure execution).

[modes.mappings.action]
type = "Shell"
command = "git status"

Security Design: Commands are executed directly via Command::new(program).args(args) without using shell interpreters (sh, bash, cmd). This prevents command injection attacks while supporting common use cases.

Supported Examples:

# Simple commands
command = "git status"
command = "ls -la /tmp"

# File operations
command = "open ~/Downloads"

# System info
command = "system_profiler SPUSBDataType"

# AppleScript (macOS)
command = "osascript -e 'set volume 50'"
command = "osascript -e 'display notification \"MIDI triggered!\"'"

Shell Features NOT Supported (for security):

  • Command chaining: &&, ||, ;
  • Piping: |
  • Redirection: >, <, >>
  • Command substitution: $(...), `...`
  • Variable expansion: $HOME, ${VAR}

Alternative: Use Sequence action to chain multiple commands:

# Instead of: "git add . && git commit -m 'save'"
type = "Sequence"
actions = [
    { type = "Shell", command = "git add ." },
    { type = "Shell", command = "git commit -m 'save'" }
]

Timing & Flow Control

Delay

Add pauses between actions in sequences.

[modes.mappings.action]
type = "Delay"
ms = 500  # 500 milliseconds

Typical Uses:

  • Wait for UI to load
  • Slow down rapid automation
  • Add deliberate pacing to sequences

Sequence

Execute multiple actions in order. Automatic 50ms delay between actions.

[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "space", modifiers = ["cmd"] },  # Spotlight
    { type = "Delay", ms = 200 },  # Wait for Spotlight
    { type = "Text", text = "Terminal" },
    { type = "Keystroke", keys = "return" }  # Launch
]

Design Patterns:

1. Application Launcher:

type = "Sequence"
actions = [
    { type = "Keystroke", keys = "space", modifiers = ["cmd"] },
    { type = "Delay", ms = 100 },
    { type = "Text", text = "VS Code" },
    { type = "Keystroke", keys = "return" }
]

2. File Workflow:

type = "Sequence"
actions = [
    { type = "Keystroke", keys = "s", modifiers = ["cmd"] },  # Save
    { type = "Delay", ms = 100 },
    { type = "Keystroke", keys = "w", modifiers = ["cmd"] },  # Close
    { type = "Delay", ms = 50 },
    { type = "Keystroke", keys = "down" },  # Next file
    { type = "Keystroke", keys = "return" }  # Open
]

3. Git Workflow:

type = "Sequence"
actions = [
    { type = "Shell", command = "git add -A" },
    { type = "Delay", ms = 100 },
    { type = "Shell", command = "git commit -m 'quick save'" },
    { type = "Delay", ms = 100 },
    { type = "Shell", command = "git push" }
]

Repeat

Repeat an action (or sequence) multiple times.

# Simple repeat: scroll down 10 times
[modes.mappings.action]
type = "Repeat"
count = 10
action = { type = "Keystroke", keys = "down" }

With Delay Between Iterations:

[modes.mappings.action]
type = "Repeat"
count = 5
delay_ms = 200  # 200ms between each press
action = { type = "Keystroke", keys = "down" }

Repeat a Sequence:

[modes.mappings.action]
type = "Repeat"
count = 3
delay_ms = 1000
action = {
    type = "Sequence",
    actions = [
        { type = "Keystroke", keys = "down" },  # Select next item
        { type = "Delay", ms = 100 },
        { type = "Keystroke", keys = "return" },  # Open
        { type = "Delay", ms = 500 },
        { type = "Keystroke", keys = "w", modifiers = ["cmd"] }  # Close
    ]
}

Error Handling:

[modes.mappings.action]
type = "Repeat"
count = 3
delay_ms = 2000
action = { type = "Launch", app = "Xcode" }

Parameters:

  • count (required): Number of repetitions (0 = no-op, 1 = run once)
  • action (required): Action to repeat
  • delay_ms (optional): Delay in milliseconds between iterations (not applied after last iteration)

Edge Cases:

  • count = 0 is valid (no-op)
  • count = 1 executes once with no delay
  • Nested repeats multiply: 10 outer × 5 inner = 50 total
  • Large counts (>1000) may appear as hang
  • Blocking operation - cannot interrupt

Use Cases:

  1. Pagination: Scroll through long lists
  2. Batch Processing: Repeat workflow on multiple items
  3. Velocity Mapping: Different repeat counts for soft/medium/hard presses
  4. Retry Logic: Try launching app multiple times

Mouse Actions

MouseClick

Simulate mouse clicks with optional positioning.

# Click at current cursor position
[modes.mappings.action]
type = "MouseClick"
button = "Left"

# Click at specific coordinates
[modes.mappings.action]
type = "MouseClick"
button = "Right"
x = 500
y = 300

Button Options: Left, Right, Middle

Use Cases:

  • Click UI elements at fixed positions
  • Context menu automation
  • Drag-and-drop workflows (with sequences)

System Actions

VolumeControl

Control system volume with platform-specific implementations.

Platform Support:

  • macOS: AppleScript via osascript (full support)
  • Linux/Windows: Not yet implemented
# Volume up (increment by 5)
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

# Volume down (decrement by 5)
[modes.mappings.action]
type = "VolumeControl"
operation = "Down"

# Mute
[modes.mappings.action]
type = "VolumeControl"
operation = "Mute"

# Unmute
[modes.mappings.action]
type = "VolumeControl"
operation = "Unmute"

# Set specific volume (0-100)
[modes.mappings.action]
type = "VolumeControl"
operation = "Set"
value = 50

Operations:

  • Up - Increase volume by 5%
  • Down - Decrease volume by 5%
  • Mute - Mute audio output
  • Unmute - Unmute audio output
  • Set - Set volume to specific level (0-100), requires value parameter

Encoder Volume Control Example:

[[global_mappings]]
description = "Encoder for volume control"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "Clockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

[[global_mappings]]
description = "Encoder for volume control"
[global_mappings.trigger]
type = "EncoderTurn"
direction = "CounterClockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Down"

Performance: <100ms response time on macOS

ModeChange

Switch between mapping modes programmatically.

# Switch to specific mode by name
[modes.mappings.action]
type = "ModeChange"
mode = "Media"

Use Cases:

  • Context Switching: Switch to different mapping sets
  • Workflow Modes: Development, Media, Gaming modes
  • Scene Control: Different modes for streaming scenes

Example - Mode Switcher Pad:

[[modes.mappings]]
description = "Switch to Media mode"
[modes.mappings.trigger]
type = "Note"
note = 15
[modes.mappings.action]
type = "ModeChange"
mode = "Media"

Note: Mode changes trigger LED color updates and mapping context switches. The mode name must match a defined mode in your configuration.

MIDI Output Actions

SendMidi

Send MIDI messages to physical or virtual MIDI output ports. Enables DAW control, external synth control, and MIDI routing.

Platform Support: macOS, Linux, Windows (all platforms with MIDI output)

# Send MIDI Note On
[modes.mappings.action]
type = "SendMidi"
port = "IAC Driver Bus 1"  # Virtual MIDI port name
message_type = "NoteOn"
channel = 0        # MIDI channel 0-15 (maps to channel 1-16)
note = 60          # Middle C (0-127)
velocity = 100     # Note velocity (0-127)

Message Types:

NoteOn - Note on with velocity:

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "NoteOn"
channel = 0    # MIDI channel 0-15
note = 60      # Note number (0-127)
velocity = 100 # Velocity (0-127)

NoteOff - Note off:

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "NoteOff"
channel = 0
note = 60
velocity = 0  # Release velocity (usually 0)

CC (Control Change) - MIDI control change:

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "CC"
channel = 0
controller = 7  # Controller number (0-127, e.g., 7=Volume, 1=Modulation)
value = 100     # Controller value (0-127)

ProgramChange - Change program/preset:

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "ProgramChange"
channel = 0
program = 9  # Program number (0-127)

PitchBend - Pitch bend wheel:

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "PitchBend"
channel = 0
value = 0  # Pitch bend value (-8192 to +8191, 0 = center)

Aftertouch - Channel pressure:

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "Aftertouch"
channel = 0
pressure = 64  # Pressure value (0-127)

Use Cases:

  • DAW Control: Send notes/CC to control Logic Pro, Ableton Live, FL Studio
  • Virtual Instruments: Trigger software synths and samplers
  • MIDI Routing: Route controller input to different destinations
  • Hardware Synths: Control external synthesizers and drum machines
  • Lighting Control: Send MIDI to DMX/lighting systems

Example - Velocity-Sensitive MIDI:

[[modes.mappings]]
description = "Expressive MIDI note with velocity curve"

[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7  # Boost soft hits

[modes.mappings.action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "NoteOn"
channel = 0
note = 60
# Velocity is derived from velocity_mapping transformation

Example - Control DAW Volume:

[[modes.mappings]]
description = "Control track volume with CC"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "CC"
channel = 0
controller = 7   # Volume CC
value = 100

Example - Note On/Off Sequence:

[modes.mappings.action]
type = "Sequence"
actions = [
  { type = "SendMidi", port = "IAC", message_type = "NoteOn", channel = 0, note = 60, velocity = 100 },
  { type = "Delay", ms = 500 },
  { type = "SendMidi", port = "IAC", message_type = "NoteOff", channel = 0, note = 60, velocity = 0 }
]

Virtual MIDI Ports:

  • macOS: Use IAC Driver (Audio MIDI Setup → Window → Show MIDI Studio → IAC Driver)
  • Windows: Use loopMIDI or similar virtual MIDI port software
  • Linux: Use ALSA virtual ports

Note: Virtual MIDI port creation is not yet automated. Use system tools to create virtual ports, then reference them by name in the port parameter.

Advanced Actions

Conditional

Execute different actions based on runtime conditions such as time, active app, current mode, or day of week. Enables context-aware, adaptive behavior.

Complete Documentation: See Configuration: Conditionals for comprehensive reference.

Basic Structure:

[modes.mappings.action]
type = "Conditional"
condition = { /* condition definition */ }
then_action = { /* action if true */ }
else_action = { /* optional action if false */ }

Quick Examples:

Time-Based Launcher:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "TimeRange"
start = "09:00"
end = "17:00"

[modes.mappings.action.then_action]
type = "Launch"
app = "Slack"  # Work hours

[modes.mappings.action.else_action]
type = "Launch"
app = "Discord"  # Off hours

App-Aware Control:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppRunning"
app_name = "Logic Pro"

[modes.mappings.action.then_action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "NoteOn"
channel = 0
note = 60
velocity = 100

[modes.mappings.action.else_action]
type = "Launch"
app = "Logic Pro"

Multiple Conditions (AND):

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] },
  { type = "AppRunning", app_name = "Slack" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "s"
modifiers = ["cmd", "shift"]

Multiple Conditions (OR):

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Safari" },
  { type = "AppFrontmost", app_name = "Chrome" },
  { type = "AppFrontmost", app_name = "Firefox" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]  # New tab in any browser

Available Condition Types:

  • Always - Always true
  • Never - Always false
  • TimeRange - Time window (HH:MM format)
  • DayOfWeek - Specific days (1=Monday through 7=Sunday)
  • AppRunning - Process detection (macOS, Linux)
  • AppFrontmost - Active window detection (macOS only)
  • ModeIs - Current mode matching
  • And - Logical AND of multiple conditions
  • Or - Logical OR of multiple conditions
  • Not - Logical negation

See Also:

Action Composition Patterns

1. Velocity-Based Repeat Count

Different repeat counts based on how hard you hit the pad:

[modes.mappings.trigger]
type = "VelocityRange"
note = 10
ranges = [
    # Soft: repeat 3 times
    { min = 0, max = 40, action = {
        type = "Repeat",
        count = 3,
        action = { type = "Keystroke", keys = "down" }
    }},
    # Medium: repeat 5 times
    { min = 41, max = 80, action = {
        type = "Repeat",
        count = 5,
        action = { type = "Keystroke", keys = "down" }
    }},
    # Hard: repeat 10 times
    { min = 81, max = 127, action = {
        type = "Repeat",
        count = 10,
        action = { type = "Keystroke", keys = "down" }
    }}
]

2. Nested Repeats

Multiply effect (use with caution!):

[modes.mappings.action]
type = "Repeat"
count = 3  # Outer loop
action = {
    type = "Repeat",
    count = 5,  # Inner loop → 3 × 5 = 15 total
    action = { type = "Keystroke", keys = "down" }
}

3. Complex Automation Workflow

Combine sequences, repeats, and delays:

[modes.mappings.action]
type = "Sequence"
actions = [
    # Open file browser
    { type = "Keystroke", keys = "o", modifiers = ["cmd"] },
    { type = "Delay", ms = 500 },

    # Navigate to folder
    { type = "Keystroke", keys = "g", modifiers = ["cmd", "shift"] },
    { type = "Delay", ms = 200 },
    { type = "Text", text = "~/Downloads" },
    { type = "Keystroke", keys = "return" },
    { type = "Delay", ms = 500 },

    # Process first 3 files
    { type = "Repeat", count = 3, delay_between_ms = 1000, action = {
        type = "Sequence",
        actions = [
            { type = "Keystroke", keys = "return" },  # Open file
            { type = "Delay", ms = 800 },
            { type = "Keystroke", keys = "e", modifiers = ["cmd"] },  # Export
            { type = "Delay", ms = 500 },
            { type = "Keystroke", keys = "return" },  # Confirm
            { type = "Delay", ms = 1000 },
            { type = "Keystroke", keys = "w", modifiers = ["cmd"] },  # Close
            { type = "Delay", ms = 200 },
            { type = "Keystroke", keys = "down" }  # Next file
        ]
    }}
]

Performance & Best Practices

Timing Guidelines

  • Keystroke: Instant (<1ms)
  • Text: ~10ms per character
  • Launch: 100-2000ms (app dependent)
  • Shell: Variable (command dependent)
  • Sequence: 50ms auto-delay between actions
  • Delay: As specified
  • MouseClick: <5ms
  • Repeat: count × (action time + delay_between_ms)

Optimization Tips

  1. Minimize Delays: Only add delays where needed (UI loading, animations)
  2. Batch Operations: Use Sequence instead of multiple mappings
  3. Avoid High-Frequency Repeats: Add delay_between_ms for rapid repeats
  4. Test Incrementally: Start with count=1, then increase
  5. Consider User Experience: Long-running actions should have feedback

Common Pitfalls

  1. Blocking Operations: Repeats and sequences block - cannot interrupt
  2. Timing Fragility: UI timing varies by system load
  3. Nested Explosions: 10 × 10 nested repeat = 100 executions
  4. Error Swallowing: stop_on_error = false continues silently

Debugging Actions

Enable debug mode to see action execution:

DEBUG=1 cargo run --release 2

Watch for:

  • Action start/completion logs
  • Timing between actions
  • Error messages
  • Repeat iteration counts

See Also

Velocity Mapping Configuration

Complete TOML reference for velocity mapping configuration.


Overview

Velocity mappings transform input velocity (0-127) into output velocity using configurable curves. This allows you to customize the relationship between how hard you hit a pad and what action is triggered.


Configuration Location

Velocity mappings are configured per-mapping in the velocity_mapping field:

[[modes.mappings]]
description = "My mapping"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.velocity_mapping]
# Velocity mapping configuration goes here

[modes.mappings.action]
type = "SendMidi"
# ...

Velocity Mapping Types

Fixed

Output is always the same velocity, regardless of input.

Fields:

  • type (string, required): Must be "Fixed"
  • velocity (integer, required): Output velocity (0-127)

Example:

[velocity_mapping]
type = "Fixed"
velocity = 100

Use Cases:

  • Actions that don’t need velocity variation
  • Ensure consistent behavior regardless of hit strength
  • Testing and debugging

PassThrough

Output velocity equals input velocity (1:1 mapping). This is the default if no velocity mapping is specified.

Fields:

  • Can be specified as just the string "PassThrough" (no fields required)

Example (short form):

velocity_mapping = "PassThrough"

Example (explicit form):

[velocity_mapping]
type = "PassThrough"

Use Cases:

  • Natural, unmodified velocity response
  • Direct MIDI pass-through
  • When you want no velocity transformation

Linear

Maps full input range (0-127) to custom output range with linear scaling.

Fields:

  • type (string, required): Must be "Linear"
  • min (integer, required): Minimum output velocity (0-127)
  • max (integer, required): Maximum output velocity (0-127)

Behavior:

  • Input 0 → Output min
  • Input 127 → Output max
  • Values between are scaled proportionally

Formula:

output = min + (input / 127) × (max - min)

Example:

[velocity_mapping]
type = "Linear"
min = 40   # Softest hit = 40 (instead of 0)
max = 110  # Hardest hit = 110 (instead of 127)

Use Cases:

  • Compress dynamic range (e.g., 0-127 → 60-100)
  • Expand subtle playing (e.g., 0-127 → 20-127)
  • Shift velocity range up or down
  • Make soft hits louder while preventing loud spikes

Parameter Constraints:

  • min must be ≤ max
  • Both must be in range 0-127
  • If min = max, acts like Fixed mapping

Curve

Applies non-linear transformation with adjustable intensity.

Fields:

  • type (string, required): Must be "Curve"
  • curve_type (string, required): One of "Exponential", "Logarithmic", or "SCurve"
  • intensity (float, required): Curve intensity (0.0 = linear, 1.0 = maximum effect)

Example:

[velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7

Exponential Curve

Makes soft hits louder while preserving hard hits.

Formula:

output = input^(1 - intensity)

Behavior:

  • intensity = 0.0: Linear (no change)
  • intensity = 0.5: Moderate compression of soft notes
  • intensity = 1.0: Maximum compression (all inputs → 127)

Effect:

  • Low input velocities are boosted significantly
  • High input velocities remain relatively unchanged
  • Creates “compressed” feel that makes subtle playing more audible

Example:

[velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.8  # Strong boost for soft hits

Use Cases:

  • Make soft playing more audible
  • Compensate for light touch
  • Reduce dynamic range while preserving expressiveness
  • MIDI output to DAWs where soft notes get lost

Logarithmic Curve

Compresses dynamic range, making loud hits quieter relative to soft hits.

Formula:

normalized_input = input / 127
output = log(1 + intensity × normalized_input) / log(1 + intensity) × 127

Behavior:

  • intensity = 0.0: Linear (no change)
  • intensity = 0.5: Moderate compression
  • intensity = 1.0: Maximum compression

Effect:

  • High input velocities are attenuated
  • Low input velocities are relatively preserved
  • Creates “tamed” feel that prevents aggressive playing from being too loud

Example:

[velocity_mapping]
type = "Curve"
curve_type = "Logarithmic"
intensity = 0.6  # Moderate taming of loud hits

Use Cases:

  • Tame aggressive playing
  • Prevent ear-splitting volume spikes
  • Smooth out dynamic range for consistent mix
  • Volume control actions that need gentle adjustment

S-Curve (Sigmoid)

Smooth acceleration in middle range with soft and hard extremes less affected.

Formula:

normalized_input = input / 127
k = intensity × 10 + 0.5
sigmoid = 1 / (1 + exp(-k × (normalized_input - 0.5)))
output = normalize(sigmoid) × 127

Behavior:

  • intensity = 0.0: Nearly linear
  • intensity = 0.5: Balanced S-curve with noticeable “sweet spot”
  • intensity = 1.0: Sharp transition in midrange

Effect:

  • Creates a “sweet spot” in the middle velocity range
  • Soft and hard extremes are less affected
  • Natural-feeling response with smooth acceleration
  • Provides more control in the middle range while preserving extremes

Example:

[velocity_mapping]
type = "Curve"
curve_type = "SCurve"
intensity = 0.5  # Balanced response with sweet spot

Use Cases:

  • Natural-feeling dynamics
  • Emphasis on mid-range control
  • Smooth transitions between soft and hard
  • Expressive MIDI control with gradual acceleration

Intensity Parameter Guide

For Curve mappings, the intensity parameter controls how pronounced the curve effect is:

IntensityEffectTypical Use Case
0.0 - 0.2Very subtleFine-tuning, slight adjustment
0.3 - 0.5ModerateNoticeable but natural feel
0.6 - 0.8StrongSignificant transformation
0.9 - 1.0ExtremeDramatic effect, testing

Recommendation: Start with 0.5 and adjust in increments of 0.1 until you find the desired feel.


Combining with Actions

Velocity mappings apply before the action is executed. The transformed velocity is passed to the action.

SendMidi Action

[velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7

[action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "NoteOn"
channel = 0
note = 60
# Velocity is derived from the mapped input velocity

VelocityRange Action

Velocity mapping transforms the input before VelocityRange thresholds are applied:

[velocity_mapping]
type = "Linear"
min = 30
max = 120

[action]
type = "VelocityRange"
soft_max = 50    # Applied to mapped velocity (30-120 range)
medium_max = 90
soft_action = { type = "Text", text = "Soft" }
medium_action = { type = "Text", text = "Medium" }
hard_action = { type = "Text", text = "Hard" }

Default Behavior

If no velocity_mapping is specified, PassThrough is used by default:

[[modes.mappings]]
# No velocity_mapping specified = PassThrough
[modes.mappings.action]
type = "SendMidi"
# Velocity is passed through unchanged

Validation Rules

Type Validation:

  • type must be one of: "Fixed", "PassThrough", "Linear", "Curve"
  • Invalid type will cause config load error

Fixed Validation:

  • velocity must be 0-127
  • Missing velocity will cause error

Linear Validation:

  • min and max must be 0-127
  • min should be ≤ max (not enforced, but illogical if reversed)
  • Missing min or max will cause error

Curve Validation:

  • curve_type must be one of: "Exponential", "Logarithmic", "SCurve"
  • intensity must be 0.0-1.0
  • Missing curve_type or intensity will cause error

Examples

Example 1: Consistent Launch Action

Ignore velocity variation for application launching:

[[modes.mappings]]
description = "Launch browser (any hit strength)"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.velocity_mapping]
type = "Fixed"
velocity = 100  # Doesn't actually matter for Launch

[modes.mappings.action]
type = "Launch"
app = "Safari"

Example 2: Gentle Volume Control

Compress volume adjustments for smoother control:

[[modes.mappings]]
description = "Volume up (gentle)"

[modes.mappings.trigger]
type = "Note"
note = 10

[modes.mappings.velocity_mapping]
type = "Linear"
min = 60   # Even soft tap has impact
max = 100  # Prevent ear-splitting jumps

[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

Example 3: Expressive MIDI Output

Boost soft playing for DAW control:

[[modes.mappings]]
description = "MIDI note with boosted soft hits"

[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "Exponential"
intensity = 0.7

[modes.mappings.action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "NoteOn"
channel = 0
note = 60
velocity = 100  # This is overridden by mapped velocity

Example 4: Natural Response Curve

S-Curve for smooth, natural feel:

[[modes.mappings]]
description = "Natural dynamics with sweet spot"

[modes.mappings.trigger]
type = "Note"
note = 3

[modes.mappings.velocity_mapping]
type = "Curve"
curve_type = "SCurve"
intensity = 0.5

[modes.mappings.action]
type = "SendMidi"
port = "Virtual MIDI Port"
message_type = "CC"
channel = 0
controller = 7  # Volume CC
value = 64      # Overridden by mapped velocity


See Also: The GUI provides a visual curve preview graph to help you dial in the perfect response curve. The graph updates in real-time as you adjust parameters.

Conditional Action Configuration

Complete TOML reference for conditional action configuration.


Overview

Conditional actions allow mappings to execute different actions based on runtime conditions such as time of day, active application, current mode, or day of week. This enables context-aware behavior without manual profile switching.


Configuration Structure

Conditional actions use the Conditional action type with three components:

  1. condition: The condition to evaluate
  2. then_action: Action to execute if condition is true
  3. else_action: Optional action to execute if condition is false
[[modes.mappings]]
description = "Context-aware mapping"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.action]
type = "Conditional"
condition = { /* condition definition */ }
then_action = { /* action if true */ }
else_action = { /* optional action if false */ }

Condition Types

Always

Always evaluates to true (always executes then_action).

Fields: None (just the string "Always")

Example:

[modes.mappings.action]
type = "Conditional"
condition = "Always"
then_action = { type = "Keystroke", keys = "space", modifiers = [] }

Use Cases:

  • Testing conditional logic
  • Default behavior wrapper
  • Placeholder for future condition

Never

Always evaluates to false (never executes then_action, always uses else_action if present).

Fields: None (just the string "Never")

Example:

[modes.mappings.action]
type = "Conditional"
condition = "Never"
then_action = { type = "Launch", app = "Disabled App" }
else_action = { type = "Text", text = "This action is disabled" }

Use Cases:

  • Temporarily disable a mapping without deleting it
  • Testing else_action logic
  • Documented disabled features

TimeRange

Evaluates to true if current time falls within specified range (24-hour format).

Fields:

  • type (string, required): Must be "TimeRange"
  • start (string, required): Start time in "HH:MM" format (24-hour)
  • end (string, required): End time in "HH:MM" format (24-hour)

Behavior:

  • Automatically handles ranges that cross midnight (e.g., 22:00 to 06:00)
  • Time is evaluated when the action is triggered (not when config is loaded)
  • Timezone is system local time

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "TimeRange"
start = "09:00"
end = "17:00"

[modes.mappings.action.then_action]
type = "Launch"
app = "Slack"

[modes.mappings.action.else_action]
type = "Text"
text = "Outside work hours"

Use Cases:

  • Work mode (9am-5pm): Launch productivity apps
  • Evening mode (5pm-11pm): Launch entertainment apps
  • Night mode (11pm-9am): Disable noisy actions

Validation:

  • Format must be HH:MM (e.g., "09:00", "23:30")
  • Hour must be 00-23
  • Minute must be 00-59
  • Invalid format will cause config load error

DayOfWeek

Evaluates to true if current day matches one of the specified days.

Fields:

  • type (string, required): Must be "DayOfWeek"
  • days (array of integers, required): Day numbers (1=Monday through 7=Sunday)

Day Numbers:

  • 1 = Monday
  • 2 = Tuesday
  • 3 = Wednesday
  • 4 = Thursday
  • 5 = Friday
  • 6 = Saturday
  • 7 = Sunday

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "DayOfWeek"
days = [1, 2, 3, 4, 5]  # Monday through Friday

[modes.mappings.action.then_action]
type = "Shell"
command = "open ~/Work"

[modes.mappings.action.else_action]
type = "Shell"
command = "open ~/Personal"

Use Cases:

  • Weekday-only shortcuts (work apps)
  • Weekend-only shortcuts (gaming, hobbies)
  • Different behaviors for different days

Validation:

  • days array must not be empty
  • Each day must be 1-7
  • Invalid day number will cause config load error

AppRunning

Evaluates to true if a specific application is currently running (process detection).

Fields:

  • type (string, required): Must be "AppRunning"
  • app_name (string, required): Application name to check

Platform Support:

  • ✅ macOS: Uses pgrep (case-insensitive partial match)
  • ✅ Linux: Uses pgrep (case-insensitive partial match)
  • ❌ Windows: Not yet supported

Matching Behavior:

  • Uses partial string matching
  • Case-insensitive
  • "Chrome" matches "Google Chrome Helper"
  • "Logic" matches "Logic Pro", "Logic Pro Helper", etc.

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppRunning"
app_name = "Logic Pro"

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "space"
modifiers = []  # Play/pause in Logic

[modes.mappings.action.else_action]
type = "Launch"
app = "Logic Pro"

Use Cases:

  • Smart play/pause (launch DAW if not running, else control it)
  • Toggle between apps
  • Conditional workflows based on running processes

Performance:

  • Executes pgrep subprocess on each trigger
  • Minimal overhead (<10ms typically)
  • Cached briefly by system

AppFrontmost

Evaluates to true if a specific application has focus (active window).

Fields:

  • type (string, required): Must be "AppFrontmost"
  • app_name (string, required): Application name to check

Platform Support:

  • ✅ macOS: Uses NSWorkspace API (exact match)
  • ❌ Linux: Not yet supported
  • ❌ Windows: Not yet supported

Matching Behavior:

  • Exact frontmost application name match
  • Case-sensitive on macOS
  • Checks the actual active window, not just if app is running

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppFrontmost"
app_name = "Safari"

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]  # New tab in Safari

[modes.mappings.action.else_action]
type = "Text"
text = "Not in Safari"

Use Cases:

  • App-specific shortcuts (browser shortcuts vs IDE shortcuts)
  • Context-switching workflows
  • Smart key remapping based on frontmost app

Performance:

  • Native API call (very fast, <1ms)
  • No subprocess overhead

ModeIs

Evaluates to true if current mode matches the specified mode name.

Fields:

  • type (string, required): Must be "ModeIs"
  • mode (string, required): Mode name to check (exact match, case-sensitive)

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "ModeIs"
mode = "Development"

[modes.mappings.action.then_action]
type = "Shell"
command = "git status"

[modes.mappings.action.else_action]
type = "Text"
text = "Switch to Development mode first"

Use Cases:

  • Mode-specific behaviors within global mappings
  • Validate mode before executing sensitive commands
  • Different actions for different modes on same pad

Note: Mode name must match exactly (case-sensitive). Use mode names as defined in your [[modes]] configuration.


And (Logical AND)

Evaluates to true if all sub-conditions are true.

Fields:

  • type (string, required): Must be "And"
  • conditions (array, required): Array of condition objects

Behavior:

  • Short-circuits (stops evaluating as soon as one condition is false)
  • Empty array always evaluates to true

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] },
  { type = "AppRunning", app_name = "Slack" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "s"
modifiers = ["cmd", "shift"]  # Search in Slack during work hours

Use Cases:

  • Work mode: weekdays AND business hours AND specific app
  • Complex conditions requiring multiple criteria

Validation:

  • conditions array must not be empty
  • Each element must be a valid condition

Or (Logical OR)

Evaluates to true if at least one sub-condition is true.

Fields:

  • type (string, required): Must be "Or"
  • conditions (array, required): Array of condition objects

Behavior:

  • Short-circuits (stops evaluating as soon as one condition is true)
  • Empty array always evaluates to false

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Safari" },
  { type = "AppFrontmost", app_name = "Chrome" },
  { type = "AppFrontmost", app_name = "Firefox" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]  # New tab in any browser

Use Cases:

  • Action applies to multiple apps
  • Alternative conditions (weekend OR evening)

Validation:

  • conditions array must not be empty
  • Each element must be a valid condition

Not (Logical NOT)

Inverts the result of a condition (true becomes false, false becomes true).

Fields:

  • type (string, required): Must be "Not"
  • condition (object, required): Condition to invert

Example:

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Not"
condition = { type = "AppRunning", app_name = "Music" }

[modes.mappings.action.then_action]
type = "Launch"
app = "Music"

[modes.mappings.action.else_action]
type = "Text"
text = "Music already running"

Use Cases:

  • “If NOT running, launch”
  • “If NOT work hours, disable”
  • Invert any condition logic

Validation:

  • condition must be a valid condition object

Nested Conditions

Conditions can be nested to arbitrary depth using And/Or/Not operators.

Example: Complex Work Mode

[[modes.mappings]]
description = "(Weekday AND work hours) OR (Weekend AND Xcode running)"

[modes.mappings.trigger]
type = "Note"
note = 10

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  {
    type = "And"
    conditions = [
      { type = "DayOfWeek", days = [1, 2, 3, 4, 5] },
      { type = "TimeRange", start = "09:00", end = "17:00" }
    ]
  },
  {
    type = "And"
    conditions = [
      { type = "DayOfWeek", days = [6, 7] },
      { type = "AppRunning", app_name = "Xcode" }
    ]
  }
]

[modes.mappings.action.then_action]
type = "Shell"
command = "open ~/Code"

Translation: Open code folder if:

  • (Monday-Friday AND 9am-5pm) OR
  • (Saturday-Sunday AND Xcode is running)

Then/Else Actions

Then Action (Required)

Executed when condition evaluates to true. Can be any action type.

Example:

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "space"
modifiers = []

Else Action (Optional)

Executed when condition evaluates to false. If omitted, no action is taken when condition is false.

Example:

[modes.mappings.action.else_action]
type = "Text"
text = "Condition was false"

Nested Conditionals

Both then_action and else_action can themselves be Conditional actions:

[modes.mappings.action]
type = "Conditional"
condition = { type = "AppFrontmost", app_name = "Spotify" }

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "space"
modifiers = []

[modes.mappings.action.else_action]
type = "Conditional"
condition = { type = "AppFrontmost", app_name = "Logic Pro" }
then_action = { type = "Keystroke", keys = "Return", modifiers = [] }
else_action = { type = "VolumeControl", operation = "Mute" }

Translation:

  • If Spotify is frontmost: Press space
  • Else if Logic Pro is frontmost: Press Return
  • Else: Mute volume

Validation Rules

General:

  • type field is required for all conditions (except Always/Never which can be just strings)
  • Invalid condition type will cause config load error

TimeRange:

  • start and end must be in HH:MM format
  • Hour must be 00-23, minute must be 00-59

DayOfWeek:

  • days array must contain at least one day
  • Each day must be 1-7

AppRunning/AppFrontmost:

  • app_name must be a non-empty string

ModeIs:

  • mode must be a non-empty string
  • Mode name is not validated against actual modes (runtime check)

And/Or:

  • conditions array must contain at least one condition
  • Each element must be a valid condition

Not:

  • condition must be a valid condition object

Performance Considerations

  • TimeRange/DayOfWeek: Very fast (system time lookup)
  • ModeIs: Very fast (string comparison)
  • AppRunning: Moderate (~10ms, subprocess call to pgrep)
  • AppFrontmost: Very fast (<1ms, native API)
  • And/Or: Short-circuit evaluation (stops as soon as result is known)
  • Nested conditions: Evaluated depth-first

Recommendation: Keep deeply nested conditions reasonable (<5 levels) to maintain readability and predictable performance.


Examples

Example 1: Work Hours Profile

Different behavior during work vs personal time:

[[modes.mappings]]
description = "Slack at work, Discord after hours"

[modes.mappings.trigger]
type = "Note"
note = 8

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "And"
conditions = [
  { type = "TimeRange", start = "09:00", end = "17:00" },
  { type = "DayOfWeek", days = [1, 2, 3, 4, 5] }
]

[modes.mappings.action.then_action]
type = "Launch"
app = "Slack"

[modes.mappings.action.else_action]
type = "Launch"
app = "Discord"

Example 2: Universal Play/Pause

Different shortcuts for different media apps:

[[modes.mappings]]
description = "Universal play/pause"

[modes.mappings.trigger]
type = "Note"
note = 1

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "Or"
conditions = [
  { type = "AppFrontmost", app_name = "Spotify" },
  { type = "AppFrontmost", app_name = "Music" }
]

[modes.mappings.action.then_action]
type = "Keystroke"
keys = "space"
modifiers = []

[modes.mappings.action.else_action]
type = "Conditional"
condition = { type = "AppFrontmost", app_name = "Logic Pro" }
then_action = { type = "Keystroke", keys = "Return", modifiers = [] }
else_action = { type = "VolumeControl", operation = "Mute" }

Example 3: Smart DAW Control

Launch DAW if not running, otherwise send MIDI:

[[modes.mappings]]
description = "Launch or control Logic Pro"

[modes.mappings.trigger]
type = "Note"
note = 5

[modes.mappings.action]
type = "Conditional"

[modes.mappings.action.condition]
type = "AppRunning"
app_name = "Logic Pro"

[modes.mappings.action.then_action]
type = "SendMidi"
port = "IAC Driver Bus 1"
message_type = "NoteOn"
channel = 0
note = 60
velocity = 100

[modes.mappings.action.else_action]
type = "Launch"
app = "Logic Pro"


See Also: The GUI provides a Conditional Action Editor with visual condition type selector, time pickers, day toggles, and support for simple And/Or/Not operators. Complex nested logic can be configured via TOML editing.

Modes System

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

LED Feedback Configuration

Conductor provides rich LED feedback for MIDI controllers with comprehensive support for RGB control (HID devices) and basic on/off control (MIDI devices).

Quick Start

Command-Line Usage

The fastest way to enable LED feedback is via command-line flags:

# Start with reactive velocity feedback (recommended)
cargo run --release 2 --led reactive

# Try different schemes
cargo run --release 2 --led rainbow
cargo run --release 2 --led static
cargo run --release 2 --led off

Available Schemes

Choose from 10 built-in lighting schemes:

  • off - All LEDs disabled
  • static - Solid color based on current mode
  • breathing - Slow 2-second breathing effect
  • pulse - Fast 500ms pulse
  • rainbow - Rainbow cycle across pads
  • wave - Wave pattern animation
  • sparkle - Random sparkles
  • reactive - Velocity-based colors (green/yellow/red)
  • vumeter - VU meter style (bottom-up)
  • spiral - Spiral pattern animation

Configuration File

Basic Setup

Add LED settings to your config.toml:

[device]
name = "Mikro"
led_feedback = true  # Enable LED control

[led_settings]
scheme = "reactive"  # Default scheme
brightness = 255     # Full brightness (0-255)

Reactive Scheme (Velocity Feedback)

The most commonly used scheme, providing color-coded velocity feedback:

[led_settings]
scheme = "reactive"

[led_settings.reactive]
# Velocity ranges and colors
soft_color = { r = 0, g = 255, b = 0 }      # Green (0-40)
medium_color = { r = 255, g = 255, b = 0 }  # Yellow (41-80)
hard_color = { r = 255, g = 0, b = 0 }      # Red (81-127)

# Fade behavior
fade_duration_ms = 1000  # Wait 1s before fading
fade_steps = 10          # Number of fade steps (smoother = more steps)

Visual Example:

  • Soft tap → 🟢 Green LED
  • Medium tap → 🟡 Yellow LED
  • Hard hit → 🔴 Red LED
  • After 1 second → Smooth fade to mode color

Mode-Based Colors

Each mode can have its own color scheme:

[[modes]]
name = "Default"
color = "blue"
led_idle_brightness = 20      # Dim when idle
led_active_brightness = 255   # Full when pressed

[[modes]]
name = "Development"
color = "green"
led_idle_brightness = 30
led_active_brightness = 255

[[modes]]
name = "Media"
color = "purple"
led_idle_brightness = 15
led_active_brightness = 200

Supported Colors:

  • "blue" - (0, 100, 255)
  • "green" - (0, 255, 0)
  • "red" - (255, 0, 0)
  • "purple" - (200, 0, 255)
  • "yellow" - (255, 255, 0)
  • "cyan" - (0, 255, 255)
  • "white" - (255, 255, 255)
  • Custom: { r = 128, g = 64, b = 200 }

Mode Transition Effects

Add visual effects when switching modes:

[[global_mappings]]
description = "Next Mode with Sweep Effect"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = 1
relative = true
transition_effect = "Sweep"

Transition Effects:

[led_settings.transitions]
enable_effects = true
flash_duration_ms = 150   # Flash effect (white → off → new color)
sweep_delay_ms = 30       # Sweep effect (left-to-right wave)
fadeout_steps = 10        # FadeOut effect (smooth color transition)
spiral_delay_ms = 15      # Spiral effect (center-outward)

Available Effects:

  • Flash - Quick white flash (150ms) - fast mode switching
  • Sweep - Left-to-right wave (120ms) - smooth sweep
  • FadeOut - Fade old to new color (200ms) - smooth transition
  • Spiral - Center-outward spiral (240ms) - artistic
  • None - Instant change (0ms) - no animation

Mode Indicator Pads

Dedicate specific pads to show the current mode:

[led_settings]
mode_indicator_pads = [13, 14, 15, 16]  # Bottom row
# Pad 13 = Mode 0 indicator
# Pad 14 = Mode 1 indicator
# Pad 15 = Mode 2 indicator
# Pad 16 = Mode 3 indicator

When in Mode 1, pad 14 will light up in green while others remain dim.

Advanced Configuration

Animation Settings

Fine-tune animation parameters for each scheme:

[led_settings.breathing]
cycle_duration_ms = 2000  # 2-second breathing cycle
min_brightness = 0        # Fully off at minimum
max_brightness = 255      # Full brightness at peak

[led_settings.rainbow]
speed = 60           # Degrees per second
saturation = 255     # Full saturation
brightness = 255     # Full brightness
hue_spacing = 22.5   # Degrees between pads

[led_settings.sparkle]
spawn_rate_ms = 100  # New sparkle every 100ms
fade_duration_ms = 200  # Sparkle fades in 200ms
max_active = 4       # Max 4 sparkles at once

HID-Specific Settings (Mikro MK3)

[device]
name = "Mikro"
led_feedback = true
use_hid = true  # Force HID mode

[led_settings.hid]
shared_device = true  # Allow NI Controller Editor access
vendor_id = 0x17cc    # Native Instruments
product_id = 0x1700   # Maschine Mikro MK3

MIDI Feedback Settings

For standard MIDI controllers:

[device]
name = "Launchpad Mini"
led_feedback = true

[led_settings.midi]
note_offset = 36  # C1 (MIDI note 36) = Pad 1
channel = 0       # MIDI channel 0-15
on_velocity = 127 # Velocity for "LED on"
off_velocity = 0  # Velocity for "LED off"

Platform-Specific Notes

macOS

HID devices require Input Monitoring permission:

  1. Open System Settings → Privacy & Security
  2. Select “Input Monitoring”
  3. Enable permission for Terminal or your IDE

Shared device mode:

[led_settings.hid]
shared_device = true  # Works alongside NI Controller Editor

Linux

HID devices may require udev rules:

# Create udev rule for Mikro MK3
sudo nano /etc/udev/rules.d/99-maschine-mikro-mk3.rules

# Add this line:
SUBSYSTEM=="usb", ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1700", MODE="0666"

# Reload rules
sudo udevadm control --reload-rules
sudo udevadm trigger

Windows

HID devices work out of the box, but may require:

  • Native Instruments drivers installed
  • USB device permissions (Windows 10+)

Use Cases

Performance/Live Use

[led_settings]
scheme = "reactive"  # Immediate velocity feedback
brightness = 255     # Full brightness for stage visibility

Studio/Production

[led_settings]
scheme = "static"    # Subtle, non-distracting
brightness = 50      # Dim for low-light studio

Creative/Visual

[led_settings]
scheme = "rainbow"   # Eye-catching animations
brightness = 200     # Bright but not overwhelming

Focused Work

[led_settings]
scheme = "off"       # No distractions

Troubleshooting

LEDs Not Responding

  1. Check device connection:

    DEBUG=1 cargo run --release 2 --led reactive
    # Look for: "✓ Connected to Mikro MK3 LED interface"
    
  2. Verify permissions (macOS HID):

    • System Settings → Privacy → Input Monitoring
  3. Try MIDI fallback:

    [device]
    led_feedback = true
    use_hid = false  # Force MIDI mode
    

Wrong Colors

  • Issue: Colors don’t match expected RGB values
  • Cause: LED manufacturing variance (±10%)
  • Solution: Normal behavior, slight color variations expected

Flickering

  • Issue: LEDs flicker or strobe
  • Cause: Update rate too high or USB bandwidth saturation
  • Solution: Use simpler scheme (reactive or static)

Mode Colors Not Showing

  • Issue: All pads same color regardless of mode
  • Cause: Using non-mode-aware scheme
  • Solution: Use static or reactive scheme

Performance Impact

  • Issue: High CPU usage
  • Cause: Complex animation scheme
  • Solution: Switch to reactive or static (<1% CPU)

Examples

Example 1: Simple Reactive Setup

[device]
name = "Mikro"
led_feedback = true

[led_settings]
scheme = "reactive"

[[modes]]
name = "Default"
color = "blue"

[[modes]]
name = "Development"
color = "green"

Example 2: Advanced Multi-Mode with Transitions

[device]
name = "Mikro"
led_feedback = true

[led_settings]
scheme = "reactive"
brightness = 255

[led_settings.transitions]
enable_effects = true
flash_duration_ms = 150

[[modes]]
name = "Default"
color = "blue"
led_idle_brightness = 20
led_active_brightness = 255

[[modes]]
name = "Development"
color = "green"
led_idle_brightness = 30
led_active_brightness = 255

[[modes]]
name = "Media"
color = "purple"
led_idle_brightness = 15
led_active_brightness = 200

[[global_mappings]]
description = "Next Mode with Flash"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = 1
relative = true
transition_effect = "Flash"

Example 3: Studio Mode with Indicators

[device]
name = "Mikro"
led_feedback = true

[led_settings]
scheme = "static"
brightness = 50
mode_indicator_pads = [13, 14, 15, 16]

[[modes]]
name = "Recording"
color = "red"
led_idle_brightness = 10

[[modes]]
name = "Editing"
color = "green"
led_idle_brightness = 10

[[modes]]
name = "Mixing"
color = "blue"
led_idle_brightness = 10

Performance

CPU Usage by Scheme

SchemeCPU (Idle)CPU (Active)
off0%0%
static<1%<1%
reactive<1%~1%
breathing~1%~1%
rainbow~2%~2%
sparkle~3%~3%

Memory

  • Static schemes: <1MB
  • Animated schemes: <1MB
  • Per-pad state: ~50 bytes per pad

Update Rate

  • HID devices: 10fps (100ms per frame)
  • MIDI devices: Event-driven (no polling)

See Also

Device Profiles

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Configuration Examples

Real-world configuration patterns for common use cases. Each example is production-ready and can be adapted to your workflow.

Table of Contents

  1. Basic Workflows
  2. Developer Productivity
  3. Content Creation
  4. Repetition & Automation
  5. Advanced Patterns
  6. Hybrid MIDI + Gamepad Configuration

Basic Workflows

Application Launcher

Quick launch frequently used applications:

[[modes]]
name = "Launcher"
color = "blue"

[[modes.mappings]]
description = "Open Terminal"
[modes.mappings.trigger]
type = "Note"
note = 60
[modes.mappings.action]
type = "Launch"
app = "Terminal"

[[modes.mappings]]
description = "Open VS Code"
[modes.mappings.trigger]
type = "Note"
note = 61
[modes.mappings.action]
type = "Launch"
app = "Visual Studio Code"

[[modes.mappings]]
description = "Open Browser"
[modes.mappings.trigger]
type = "Note"
note = 62
[modes.mappings.action]
type = "Launch"
app = "Google Chrome"

Text Snippets

Common text expansions for email and documentation:

[[modes.mappings]]
description = "Email signature"
[modes.mappings.trigger]
type = "Note"
note = 70
[modes.mappings.action]
type = "Text"
text = """
Best regards,
Chris Joseph
Software Engineer
chris@amiable.dev
"""

[[modes.mappings]]
description = "Code review template"
[modes.mappings.trigger]
type = "Note"
note = 71
[modes.mappings.action]
type = "Text"
text = """
## Review Checklist
- [ ] Code follows style guide
- [ ] Tests are passing
- [ ] Documentation updated
- [ ] No obvious security issues
"""

Developer Productivity

Git Workflow

Common git operations on a single pad:

[[modes]]
name = "Git"
color = "green"

# Soft press: git status
[[modes.mappings]]
description = "Git status (soft)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60
ranges = [{ min = 0, max = 40 }]
[modes.mappings.action]
type = "Shell"
command = "git status"

# Medium press: git add & commit
[[modes.mappings]]
description = "Quick commit (medium)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60
ranges = [{ min = 41, max = 80 }]
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Shell", command = "git add -A" },
    { type = "Delay", ms = 100 },
    { type = "Shell", command = "git commit -m 'quick save'" }
]

# Hard press: commit and push
[[modes.mappings]]
description = "Commit and push (hard)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60
ranges = [{ min = 81, max = 127 }]
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Shell", command = "git add -A" },
    { type = "Delay", ms = 100 },
    { type = "Shell", command = "git commit -m 'quick save'" },
    { type = "Delay", ms = 100 },
    { type = "Shell", command = "git push" }
]

Code Navigation

Navigate code with velocity-based jumps:

# Jump lines based on velocity
[[modes.mappings]]
description = "Jump down (velocity-based)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 65
ranges = [
    { min = 0, max = 40, action = { type = "Keystroke", keys = "down" } },
    { min = 41, max = 80, action = { type = "Repeat", count = 5, action = { type = "Keystroke", keys = "down" } } },
    { min = 81, max = 127, action = { type = "Repeat", count = 10, action = { type = "Keystroke", keys = "down" } } }
]

# Navigate between functions
[[modes.mappings]]
description = "Next function"
[modes.mappings.trigger]
type = "Note"
note = 66
[modes.mappings.action]
type = "Keystroke"
keys = "down"
modifiers = ["cmd", "shift"]

Content Creation

Video Editing

Timeline navigation and editing:

[[modes]]
name = "Video"
color = "purple"

# Play/Pause
[[modes.mappings]]
description = "Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 60
[modes.mappings.action]
type = "Keystroke"
keys = "space"

# Frame-by-frame navigation
[[modes.mappings]]
description = "Previous frame"
[modes.mappings.trigger]
type = "Note"
note = 61
[modes.mappings.action]
type = "Keystroke"
keys = "left"

[[modes.mappings]]
description = "Next frame"
[modes.mappings.trigger]
type = "Note"
note = 62
[modes.mappings.action]
type = "Keystroke"
keys = "right"

# Jump 10 frames
[[modes.mappings]]
description = "Jump forward 10 frames"
[modes.mappings.trigger]
type = "Note"
note = 63
[modes.mappings.action]
type = "Repeat"
count = 10
action = { type = "Keystroke", keys = "right" }

Document Formatting

Batch formatting operations:

# Apply heading and next line
[[modes.mappings]]
description = "Apply heading 2"
[modes.mappings.trigger]
type = "Note"
note = 70
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "home" },  # Start of line
    { type = "Text", text = "## " },
    { type = "Keystroke", keys = "end" },  # End of line
    { type = "Keystroke", keys = "return" },  # New line
]

Repetition & Automation

Simple Repeats

Scroll through long lists or documents:

# Scroll down 10 lines
[[modes.mappings]]
description = "Scroll down 10 lines"
[modes.mappings.trigger]
type = "Note"
note = 80
[modes.mappings.action]
type = "Repeat"
count = 10
action = { type = "Keystroke", keys = "down" }

# Slow scroll (with delay)
[[modes.mappings]]
description = "Slow scroll down"
[modes.mappings.trigger]
type = "Note"
note = 81
[modes.mappings.action]
type = "Repeat"
count = 5
delay_between_ms = 200
action = { type = "Keystroke", keys = "down" }

# Page down 5 times
[[modes.mappings]]
description = "Jump 5 pages down"
[modes.mappings.trigger]
type = "Note"
note = 82
[modes.mappings.action]
type = "Repeat"
count = 5
delay_between_ms = 300
action = { type = "Keystroke", keys = "pagedown" }

Batch File Processing

Process multiple files with a repeated sequence:

[[modes.mappings]]
description = "Batch process 3 files"
[modes.mappings.trigger]
type = "LongPress"
note = 85
hold_duration_ms = 1500
[modes.mappings.action]
type = "Repeat"
count = 3
delay_between_ms = 1000
action = {
    type = "Sequence",
    actions = [
        { type = "Keystroke", keys = "return" },  # Open file
        { type = "Delay", ms = 800 },  # Wait for file to load
        { type = "Keystroke", keys = "e", modifiers = ["cmd"] },  # Export
        { type = "Delay", ms = 500 },
        { type = "Keystroke", keys = "return" },  # Confirm export
        { type = "Delay", ms = 1000 },  # Wait for export
        { type = "Keystroke", keys = "w", modifiers = ["cmd"] },  # Close file
        { type = "Delay", ms = 200 },
        { type = "Keystroke", keys = "down" }  # Next file
    ]
}

Velocity-Based Repeats

Different repeat counts based on pad pressure:

[[modes.mappings]]
description = "Variable repeat based on velocity"
[modes.mappings.trigger]
type = "VelocityRange"
note = 90
ranges = [
    # Soft: repeat 3 times
    { min = 0, max = 40, action = {
        type = "Repeat",
        count = 3,
        action = { type = "Keystroke", keys = "down" }
    }},
    # Medium: repeat 5 times
    { min = 41, max = 80, action = {
        type = "Repeat",
        count = 5,
        delay_between_ms = 100,
        action = { type = "Keystroke", keys = "down" }
    }},
    # Hard: repeat 10 times
    { min = 81, max = 127, action = {
        type = "Repeat",
        count = 10,
        delay_between_ms = 50,
        action = { type = "Keystroke", keys = "down" }
    }}
]

Nested Repeats

Multiply effect for grid or matrix operations:

# Navigate a 3x5 grid (15 total moves)
[[modes.mappings]]
description = "Navigate 3x5 grid"
[modes.mappings.trigger]
type = "DoubleTap"
note = 95
max_interval_ms = 300
[modes.mappings.action]
type = "Repeat"
count = 3  # 3 rows
delay_between_ms = 500
action = {
    type = "Sequence",
    actions = [
        # Move right 5 times
        { type = "Repeat", count = 5, delay_between_ms = 100, action = { type = "Keystroke", keys = "right" } },
        # Move down 1
        { type = "Delay", ms = 200 },
        { type = "Keystroke", keys = "down" },
        # Return to start of row
        { type = "Repeat", count = 5, action = { type = "Keystroke", keys = "left" } }
    ]
}

Retry Logic

Attempt operations with error tolerance:

[[modes.mappings]]
description = "Launch Xcode (retry 3 times)"
[modes.mappings.trigger]
type = "Note"
note = 100
[modes.mappings.action]
type = "Repeat"
count = 3
delay_between_ms = 2000
stop_on_error = false  # Continue even if already running
action = { type = "Launch", app = "Xcode" }

[[modes.mappings]]
description = "Network test (stop on first failure)"
[modes.mappings.trigger]
type = "Note"
note = 101
[modes.mappings.action]
type = "Repeat"
count = 5
delay_between_ms = 1000
stop_on_error = true  # Stop if ping fails
action = { type = "Shell", command = "ping -c 1 8.8.8.8" }

Advanced Patterns

Spotlight Launcher

Use Spotlight for precise app launching:

[[modes.mappings]]
description = "Spotlight launcher for Terminal"
[modes.mappings.trigger]
type = "Note"
note = 110
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "space", modifiers = ["cmd"] },  # Open Spotlight
    { type = "Delay", ms = 200 },  # Wait for Spotlight
    { type = "Text", text = "Terminal" },
    { type = "Delay", ms = 100 },
    { type = "Keystroke", keys = "return" }
]

Form Automation

Fill complex forms with timing:

[[modes.mappings]]
description = "Fill registration form"
[modes.mappings.trigger]
type = "LongPress"
note = 115
hold_duration_ms = 2000
[modes.mappings.action]
type = "Sequence"
actions = [
    # First name
    { type = "Text", text = "Chris" },
    { type = "Delay", ms = 100 },
    { type = "Keystroke", keys = "tab" },
    { type = "Delay", ms = 100 },

    # Last name
    { type = "Text", text = "Joseph" },
    { type = "Delay", ms = 100 },
    { type = "Keystroke", keys = "tab" },
    { type = "Delay", ms = 100 },

    # Email
    { type = "Text", text = "chris@amiable.dev" },
    { type = "Delay", ms = 100 },
    { type = "Keystroke", keys = "tab" },
    { type = "Delay", ms = 100 },

    # Phone
    { type = "Text", text = "+1-555-123-4567" },
    { type = "Delay", ms = 200 },

    # Submit
    { type = "Keystroke", keys = "return" }
]

Multi-Click Automation

Repeat mouse clicks at intervals:

[[modes.mappings]]
description = "Click button 5 times"
[modes.mappings.trigger]
type = "DoubleTap"
note = 120
max_interval_ms = 300
[modes.mappings.action]
type = "Repeat"
count = 5
delay_between_ms = 500
action = { type = "MouseClick", button = "Left" }

# Click at specific location repeatedly
[[modes.mappings]]
description = "Click refresh button"
[modes.mappings.trigger]
type = "Note"
note = 121
[modes.mappings.action]
type = "Repeat"
count = 3
delay_between_ms = 2000
action = { type = "MouseClick", button = "Left", x = 1200, y = 100 }

Macro Pad Layout

Complete 16-pad layout for development:

[device]
name = "Mikro"
auto_connect = true

[[modes]]
name = "Development"
color = "green"

# Row 1: Git operations
[[modes.mappings]]
description = "Git status"
[modes.mappings.trigger]
type = "Note"
note = 60
[modes.mappings.action]
type = "Shell"
command = "git status"

[[modes.mappings]]
description = "Git diff"
[modes.mappings.trigger]
type = "Note"
note = 61
[modes.mappings.action]
type = "Shell"
command = "git diff"

[[modes.mappings]]
description = "Quick commit"
[modes.mappings.trigger]
type = "Note"
note = 62
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Shell", command = "git add -A" },
    { type = "Delay", ms = 100 },
    { type = "Shell", command = "git commit -m 'quick save'" }
]

[[modes.mappings]]
description = "Git push"
[modes.mappings.trigger]
type = "Note"
note = 63
[modes.mappings.action]
type = "Shell"
command = "git push"

# Row 2: Build & test
[[modes.mappings]]
description = "Build project"
[modes.mappings.trigger]
type = "Note"
note = 64
[modes.mappings.action]
type = "Shell"
command = "cargo build"

[[modes.mappings]]
description = "Run tests"
[modes.mappings.trigger]
type = "Note"
note = 65
[modes.mappings.action]
type = "Shell"
command = "cargo test"

[[modes.mappings]]
description = "Run app"
[modes.mappings.trigger]
type = "Note"
note = 66
[modes.mappings.action]
type = "Shell"
command = "cargo run"

[[modes.mappings]]
description = "Format code"
[modes.mappings.trigger]
type = "Note"
note = 67
[modes.mappings.action]
type = "Shell"
command = "cargo fmt"

# Row 3: Navigation
[[modes.mappings]]
description = "Jump down (velocity)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 68
ranges = [
    { min = 0, max = 40, action = { type = "Keystroke", keys = "down" } },
    { min = 41, max = 80, action = { type = "Repeat", count = 5, action = { type = "Keystroke", keys = "down" } } },
    { min = 81, max = 127, action = { type = "Repeat", count = 10, action = { type = "Keystroke", keys = "down" } } }
]

[[modes.mappings]]
description = "Jump up (velocity)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 69
ranges = [
    { min = 0, max = 40, action = { type = "Keystroke", keys = "up" } },
    { min = 41, max = 80, action = { type = "Repeat", count = 5, action = { type = "Keystroke", keys = "up" } } },
    { min = 81, max = 127, action = { type = "Repeat", count = 10, action = { type = "Keystroke", keys = "up" } } }
]

# Row 4: Utilities
[[modes.mappings]]
description = "Open Terminal"
[modes.mappings.trigger]
type = "Note"
note = 72
[modes.mappings.action]
type = "Launch"
app = "Terminal"

[[modes.mappings]]
description = "Open VS Code"
[modes.mappings.trigger]
type = "Note"
note = 73
[modes.mappings.action]
type = "Launch"
app = "Visual Studio Code"

[[modes.mappings]]
description = "Open Browser"
[modes.mappings.trigger]
type = "Note"
note = 74
[modes.mappings.action]
type = "Launch"
app = "Google Chrome"

Context-Aware Actions (Conditional)

Execute different actions based on runtime conditions like active app, time of day, or system state.

App-Based Conditional

Different behavior when specific apps are running:

[[modes.mappings]]
description = "Context-aware play/pause"
[modes.mappings.trigger]
type = "Note"
note = 130
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "AppRunning", bundle_id = "com.apple.Logic" }
]
then_action = { type = "Keystroke", keys = "space" }  # Logic: Space to play/pause
else_action = { type = "Keystroke", keys = "space", modifiers = ["cmd"] }  # System: Media play/pause

Time-Based Automation

Launch different apps based on work hours:

[[modes.mappings]]
description = "Time-aware launcher"
[modes.mappings.trigger]
type = "Note"
note = 131
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "TimeRange", start = "09:00", end = "17:00" },
    { type = "DayOfWeek", days = ["Mon", "Tue", "Wed", "Thu", "Fri"] }
]
operator = "And"
then_action = { type = "Launch", app = "Slack" }  # Work app during work hours
else_action = { type = "Launch", app = "Discord" }  # Personal app otherwise

Multiple Conditions (OR Logic)

Launch file if any IDE is running:

[[modes.mappings]]
description = "Open project in active IDE"
[modes.mappings.trigger]
type = "Note"
note = 132
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "AppRunning", bundle_id = "com.microsoft.VSCode" },
    { type = "AppRunning", bundle_id = "com.jetbrains.IntelliJ" },
    { type = "AppRunning", bundle_id = "com.sublimetext.4" }
]
operator = "Or"  # Any one IDE is enough
then_action = { type = "Keystroke", keys = "o", modifiers = ["cmd"] }  # Cmd+O to open file
else_action = { type = "Launch", app = "Visual Studio Code" }  # Launch IDE if none running

Modifier-Based Conditional

Hold Shift for alternative behavior:

[[modes.mappings]]
description = "Delete or force-delete"
[modes.mappings.trigger]
type = "Note"
note = 133
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "ModifierPressed", modifier = "Shift" }
]
then_action = { type = "Keystroke", keys = "delete", modifiers = ["cmd", "option"] }  # Force delete
else_action = { type = "Keystroke", keys = "delete", modifiers = ["cmd"] }  # Normal delete

Nested Conditionals

Complex decision trees with multiple levels:

[[modes.mappings]]
description = "Smart launcher with time and app detection"
[modes.mappings.trigger]
type = "LongPress"
note = 135
hold_duration_ms = 1000
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "TimeRange", start = "09:00", end = "17:00" }
]
then_action = {
    type = "Conditional",
    conditions = [
        { type = "AppRunning", bundle_id = "com.apple.Logic" }
    ],
    then_action = { type = "Keystroke", keys = "n", modifiers = ["cmd"] },  # New Logic project
    else_action = { type = "Launch", app = "Logic Pro" }  # Launch Logic
}
else_action = {
    type = "Conditional",
    conditions = [
        { type = "DayOfWeek", days = ["Sat", "Sun"] }
    ],
    then_action = { type = "Launch", app = "Spotify" },  # Weekend music
    else_action = { type = "Launch", app = "Safari" }  # Evening browsing
}

Launch Only if Not Running

Avoid duplicate app launches:

[[modes.mappings]]
description = "Launch Spotify or play/pause if running"
[modes.mappings.trigger]
type = "Note"
note = 136
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "AppNotRunning", bundle_id = "com.spotify.client" }
]
then_action = { type = "Launch", app = "Spotify" }
else_action = { type = "Keystroke", keys = "space" }  # Already running, just play/pause

Mode-Aware Shortcuts

Different actions per mode using global mappings:

[[global_mappings]]
description = "Mode-aware F5 key"
[global_mappings.trigger]
type = "Note"
note = 137
[global_mappings.action]
type = "Conditional"
conditions = [
    { type = "ModeActive", mode = 1 }  # Development mode
]
then_action = { type = "Shell", command = "npm test" }  # Run tests in dev mode
else_action = { type = "Keystroke", keys = "f5" }  # Refresh in other modes

Weekend vs Weekday Behavior

Different workflows based on day of week:

[[modes.mappings]]
description = "Morning routine launcher"
[modes.mappings.trigger]
type = "Note"
note = 138
[modes.mappings.action]
type = "Conditional"
conditions = [
    { type = "DayOfWeek", days = ["Sat", "Sun"] },
    { type = "TimeRange", start = "08:00", end = "12:00" }
]
operator = "And"
then_action = {
    type = "Sequence",
    actions = [
        { type = "Launch", app = "Apple Music" },
        { type = "Delay", ms = 1000 },
        { type = "Launch", app = "Mail" }
    ]
}
else_action = {
    type = "Sequence",
    actions = [
        { type = "Launch", app = "Slack" },
        { type = "Delay", ms = 1000 },
        { type = "Launch", app = "Calendar" },
        { type = "Delay", ms = 500 },
        { type = "Launch", app = "Visual Studio Code" }
    ]
}

Hybrid MIDI + Gamepad Configuration (v3.0+)

Conductor v3.0 introduces support for Game Controllers (HID) alongside MIDI devices, enabling powerful hybrid workflows that combine the velocity-sensitive expressiveness of MIDI controllers with the ergonomic button layouts and analog sticks of gamepads, joysticks, racing wheels, flight sticks, HOTAS setups, and other game controllers.

Why Hybrid Mode?

Advantages:

  • Best of Both Worlds: MIDI’s velocity sensitivity + gamepad’s ergonomic buttons and analog sticks
  • No ID Conflicts: MIDI uses IDs 0-127, gamepad uses 128-255
  • Seamless Integration: Both devices work simultaneously through unified event stream
  • Device-Specific Strengths: Use each device for what it does best

Common Use Cases:

  • Music production with MIDI pads for recording + gamepad for DAW navigation
  • Live performance with MIDI keyboard + racing wheel pedals for effects control
  • Video editing with MIDI controller for timeline + gamepad for playback controls
  • Development with MIDI macro pad + gamepad for shortcuts and window management

Example 1: Music Production Workflow

Setup: Maschine Mikro MK3 (MIDI) + Xbox Controller (Gamepad)

This configuration demonstrates a complete music production workflow using both devices:

  • MIDI Device: Velocity-sensitive pads for recording, playback control, and instrument triggering
  • Gamepad Device: Ergonomic navigation, shortcuts, and transport controls
# Hybrid MIDI + Gamepad Configuration for Music Production
# Device: Maschine Mikro MK3 (MIDI) + Xbox Controller (Gamepad)
# Author: Conductor Examples
# Version: 3.0

[device]
name = "Hybrid Production"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

###########################################
# Mode 0: Production (Default)
###########################################

[[modes]]
name = "Production"
color = "blue"

# ========================================
# MIDI CONTROLLER MAPPINGS (IDs 0-127)
# ========================================

# --- Recording Controls (MIDI Pads) ---

[[modes.mappings]]
description = "Record (soft press)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60  # MIDI Pad 1
ranges = [{ min = 0, max = 40 }]
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Record with count-in (medium press)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60
ranges = [{ min = 41, max = 80 }]
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "k", modifiers = ["cmd"] },  # Enable count-in
    { type = "Delay", ms = 100 },
    { type = "Keystroke", keys = "r", modifiers = ["cmd"] }   # Start recording
]

[[modes.mappings]]
description = "Punch record (hard press)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60
ranges = [{ min = 81, max = 127 }]
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["cmd", "shift"]

# --- Playback Controls (MIDI Pads) ---

[[modes.mappings]]
description = "Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 61  # MIDI Pad 2
[modes.mappings.action]
type = "Keystroke"
keys = "space"

[[modes.mappings]]
description = "Stop"
[modes.mappings.trigger]
type = "Note"
note = 62  # MIDI Pad 3
[modes.mappings.action]
type = "Keystroke"
keys = "return"

[[modes.mappings]]
description = "Loop toggle"
[modes.mappings.trigger]
type = "Note"
note = 63  # MIDI Pad 4
[modes.mappings.action]
type = "Keystroke"
keys = "l"
modifiers = ["cmd"]

# --- Track Operations (MIDI Pads) ---

[[modes.mappings]]
description = "New track"
[modes.mappings.trigger]
type = "Note"
note = 64  # MIDI Pad 5
[modes.mappings.action]
type = "Keystroke"
keys = "t"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Duplicate track"
[modes.mappings.trigger]
type = "Note"
note = 65  # MIDI Pad 6
[modes.mappings.action]
type = "Keystroke"
keys = "d"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Delete track"
[modes.mappings.trigger]
type = "LongPress"
note = 66  # MIDI Pad 7 (hold to delete)
hold_duration_ms = 1500
[modes.mappings.action]
type = "Keystroke"
keys = "delete"
modifiers = ["cmd"]

# --- Quick Save (MIDI Pad) ---

[[modes.mappings]]
description = "Quick save"
[modes.mappings.trigger]
type = "Note"
note = 67  # MIDI Pad 8
[modes.mappings.action]
type = "Keystroke"
keys = "s"
modifiers = ["cmd"]

# --- Volume Control (MIDI Encoder) ---

[[modes.mappings]]
description = "Volume up"
[modes.mappings.trigger]
type = "EncoderTurn"
encoder = 0
direction = "Clockwise"
[modes.mappings.action]
type = "VolumeControl"
action = "Up"

[[modes.mappings]]
description = "Volume down"
[modes.mappings.trigger]
type = "EncoderTurn"
encoder = 0
direction = "CounterClockwise"
[modes.mappings.action]
type = "VolumeControl"
action = "Down"

# ========================================
# GAMEPAD CONTROLLER MAPPINGS (IDs 128-255)
# ========================================

# --- DAW Shortcuts (Face Buttons) ---

[[modes.mappings]]
description = "Copy (A button)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # A (Xbox) / Cross (PS)
[modes.mappings.action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Paste (B button)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129  # B (Xbox) / Circle (PS)
[modes.mappings.action]
type = "Keystroke"
keys = "v"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Undo (X button)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130  # X (Xbox) / Square (PS)
[modes.mappings.action]
type = "Keystroke"
keys = "z"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Redo (Y button)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 131  # Y (Xbox) / Triangle (PS)
[modes.mappings.action]
type = "Keystroke"
keys = "z"
modifiers = ["cmd", "shift"]

# --- Track Navigation (D-Pad) ---

[[modes.mappings]]
description = "Previous track"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132  # D-Pad Up
[modes.mappings.action]
type = "Keystroke"
keys = "up"

[[modes.mappings]]
description = "Next track"
[modes.mappings.trigger]
type = "GamepadButton"
button = 133  # D-Pad Down
[modes.mappings.action]
type = "Keystroke"
keys = "down"

[[modes.mappings]]
description = "Jump to start"
[modes.mappings.trigger]
type = "GamepadButton"
button = 134  # D-Pad Left
[modes.mappings.action]
type = "Keystroke"
keys = "return"

[[modes.mappings]]
description = "Jump to end"
[modes.mappings.trigger]
type = "GamepadButton"
button = 135  # D-Pad Right
[modes.mappings.action]
type = "Keystroke"
keys = "end"

# --- Zoom Controls (Shoulder Buttons) ---

[[modes.mappings]]
description = "Zoom out (LB)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136  # LB (Xbox) / L1 (PS)
[modes.mappings.action]
type = "Keystroke"
keys = "-"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Zoom in (RB)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137  # RB (Xbox) / R1 (PS)
[modes.mappings.action]
type = "Keystroke"
keys = "="
modifiers = ["cmd"]

# --- Timeline Scroll (Left Analog Stick) ---

[[modes.mappings]]
description = "Scroll timeline left"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 128  # Left Stick X
direction = "Negative"
threshold = 0.3
[modes.mappings.action]
type = "Keystroke"
keys = "left"

[[modes.mappings]]
description = "Scroll timeline right"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 128  # Left Stick X
direction = "Positive"
threshold = 0.3
[modes.mappings.action]
type = "Keystroke"
keys = "right"

# --- Mixer Navigation (Right Analog Stick) ---

[[modes.mappings]]
description = "Scroll mixer up"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 131  # Right Stick Y
direction = "Negative"
threshold = 0.3
[modes.mappings.action]
type = "Keystroke"
keys = "pageup"

[[modes.mappings]]
description = "Scroll mixer down"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 131  # Right Stick Y
direction = "Positive"
threshold = 0.3
[modes.mappings.action]
type = "Keystroke"
keys = "pagedown"

# --- Quick Actions (Triggers) ---

[[modes.mappings]]
description = "Fade in (LT analog)"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 132  # Left Trigger
direction = "Positive"
threshold = 0.5
[modes.mappings.action]
type = "Keystroke"
keys = "f"
modifiers = ["cmd", "shift"]

[[modes.mappings]]
description = "Fade out (RT analog)"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 133  # Right Trigger
direction = "Positive"
threshold = 0.5
[modes.mappings.action]
type = "Keystroke"
keys = "g"
modifiers = ["cmd", "shift"]

# --- Quick Markers (Stick Clicks) ---

[[modes.mappings]]
description = "Add marker (L3)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 138  # Left Stick Click
[modes.mappings.action]
type = "Keystroke"
keys = "m"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Next marker (R3)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 139  # Right Stick Click
[modes.mappings.action]
type = "Keystroke"
keys = "'"
modifiers = ["cmd"]

# ========================================
# HYBRID CHORD MAPPINGS (MIDI + Gamepad)
# ========================================

[[modes.mappings]]
description = "Emergency save (MIDI Pad 1 + Gamepad Start)"
[modes.mappings.trigger]
type = "NoteChord"
notes = [60, 140]  # MIDI note 60 + Gamepad Start button
timeout_ms = 100
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "s", modifiers = ["cmd"] },
    { type = "Delay", ms = 100 },
    { type = "Text", text = "Project saved!" }
]

###########################################
# Mode 1: Media Control
###########################################

[[modes]]
name = "Media"
color = "purple"

# --- Media Playback (Gamepad Face Buttons) ---

[[modes.mappings]]
description = "Play/Pause (A)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "space"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Next track (B)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "right"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Previous track (X)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 130
[modes.mappings.action]
type = "Keystroke"
keys = "left"
modifiers = ["cmd"]

# --- Volume (MIDI Pads with Velocity) ---

[[modes.mappings]]
description = "Volume control (velocity-based)"
[modes.mappings.trigger]
type = "VelocityRange"
note = 60
ranges = [
    { min = 0, max = 40, action = { type = "VolumeControl", action = "Down" } },
    { min = 41, max = 80, action = { type = "VolumeControl", action = "Set", volume = 50 } },
    { min = 81, max = 127, action = { type = "VolumeControl", action = "Up" } }
]

###########################################
# Mode 2: Navigation
###########################################

[[modes]]
name = "Navigation"
color = "green"

# --- Window Management (Gamepad) ---

[[modes.mappings]]
description = "Switch apps (D-Pad Left/Right)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 135  # D-Pad Right
[modes.mappings.action]
type = "Keystroke"
keys = "tab"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Mission Control (D-Pad Up)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "up"
modifiers = ["ctrl"]

# --- Quick Launch (MIDI Pads) ---

[[modes.mappings]]
description = "Launch DAW"
[modes.mappings.trigger]
type = "Note"
note = 60
[modes.mappings.action]
type = "Launch"
app = "Logic Pro"

[[modes.mappings]]
description = "Launch Browser"
[modes.mappings.trigger]
type = "Note"
note = 61
[modes.mappings.action]
type = "Launch"
app = "Google Chrome"

###########################################
# GLOBAL MAPPINGS (All Modes)
###########################################

[[global_mappings]]
description = "Mode switch: Encoder right = next mode"
[global_mappings.trigger]
type = "EncoderTurn"
encoder = 0
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = "Next"

[[global_mappings]]
description = "Mode switch: Encoder left = previous mode"
[global_mappings.trigger]
type = "EncoderTurn"
encoder = 0
direction = "CounterClockwise"
[global_mappings.action]
type = "ModeChange"
mode = "Previous"

[[global_mappings]]
description = "Emergency exit (Gamepad Select + Start)"
[global_mappings.trigger]
type = "GamepadButtonChord"
buttons = [141, 140]  # Select + Start
timeout_ms = 50
[global_mappings.action]
type = "Shell"
command = "pkill conductor"

[[global_mappings]]
description = "Quick mute (MIDI Pad 16 + Gamepad B)"
[global_mappings.trigger]
type = "NoteChord"
notes = [75, 129]  # MIDI note 75 + Gamepad B button
timeout_ms = 100
[global_mappings.action]
type = "VolumeControl"
action = "Mute"

Example 2: Live Performance with Racing Wheel

Setup: MIDI Keyboard (61 keys) + Logitech G29 Racing Wheel

This creative configuration uses a racing wheel’s pedals for real-time effects control during live performance:

# Hybrid MIDI Keyboard + Racing Wheel Configuration
# Use Case: Live electronic music performance with tactile effects control
# Device: MIDI Keyboard + Racing Wheel (pedals for effects)
# Version: 3.0

[device]
name = "Performance Rig"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

[[modes]]
name = "Performance"
color = "red"

# ========================================
# MIDI KEYBOARD (Standard note playing)
# ========================================

# Notes 0-127 pass through to DAW for instrument playing
# (Configure pass-through in your DAW)

# --- Quick Octave Shift (MIDI CC or Program Change) ---

[[modes.mappings]]
description = "Octave up"
[modes.mappings.trigger]
type = "Note"
note = 127  # Highest note
[modes.mappings.action]
type = "Keystroke"
keys = "up"
modifiers = ["shift"]

[[modes.mappings]]
description = "Octave down"
[modes.mappings.trigger]
type = "Note"
note = 0  # Lowest note
[modes.mappings.action]
type = "Keystroke"
keys = "down"
modifiers = ["shift"]

# ========================================
# RACING WHEEL PEDALS (Effects Control)
# ========================================

# --- Gas Pedal: Reverb/Delay Mix (Analog Control) ---

[[modes.mappings]]
description = "Increase reverb (gas pedal pressed)"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 133  # Right Trigger (gas pedal)
direction = "Positive"
threshold = 0.2
[modes.mappings.action]
type = "Keystroke"
keys = "1"  # DAW macro: increase reverb send

# --- Brake Pedal: Filter Cutoff (Analog Control) ---

[[modes.mappings]]
description = "Lower filter cutoff (brake pedal pressed)"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 132  # Left Trigger (brake pedal)
direction = "Positive"
threshold = 0.2
[modes.mappings.action]
type = "Keystroke"
keys = "2"  # DAW macro: decrease filter cutoff

# --- Clutch Pedal: Distortion Amount (if 3-pedal wheel) ---

[[modes.mappings]]
description = "Add distortion (clutch pedal)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 143  # Clutch pedal (digital threshold)
[modes.mappings.action]
type = "Keystroke"
keys = "3"  # DAW macro: enable distortion

# --- Wheel Buttons: Scene Launcher ---

[[modes.mappings]]
description = "Launch scene 1 (wheel button 1)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Button on wheel
[modes.mappings.action]
type = "Keystroke"
keys = "1"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Launch scene 2 (wheel button 2)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "2"
modifiers = ["cmd"]

# --- Shifter Paddles: Loop Control ---

[[modes.mappings]]
description = "Loop start (left paddle)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136  # Left shoulder
[modes.mappings.action]
type = "Keystroke"
keys = "["
modifiers = ["cmd"]

[[modes.mappings]]
description = "Loop end (right paddle)"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137  # Right shoulder
[modes.mappings.action]
type = "Keystroke"
keys = "]"
modifiers = ["cmd"]

# --- Hybrid Chord: Panic Stop (Keyboard + Wheel) ---

[[modes.mappings]]
description = "All sound off (MIDI note + wheel center button)"
[modes.mappings.trigger]
type = "NoteChord"
notes = [60, 142]  # Middle C + wheel center/guide button
timeout_ms = 100
[modes.mappings.action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "space" },  # Stop playback
    { type = "VolumeControl", action = "Mute" },  # Mute audio
    { type = "Delay", ms = 100 },
    { type = "Text", text = "EMERGENCY STOP ACTIVATED" }
]

[[global_mappings]]
description = "Emergency exit"
[global_mappings.trigger]
type = "GamepadButtonChord"
buttons = [141, 140]  # Select + Start on wheel
timeout_ms = 50
[global_mappings.action]
type = "Shell"
command = "pkill conductor"

Setup Instructions

1. Verify Device Connections

Before starting, ensure both devices are recognized:

# Check MIDI devices
cargo run --bin test_midi

# Check gamepad devices
cargo run --bin gamepad_diagnostic

# You should see both devices listed

2. Configure Hybrid Mode

In your config.toml, hybrid mode is enabled automatically when you use both MIDI (0-127) and gamepad (128-255) IDs in your mappings.

3. Test Individual Mappings

Start Conductor and test each device separately:

# Start in debug mode to see events
DEBUG=1 conductor --foreground

# Test MIDI pads/keys (you'll see note events 0-127)
# Test gamepad buttons (you'll see button events 128-255)

4. Mode Switching

The examples above use the MIDI encoder for mode switching. Alternatively, you can use gamepad buttons:

[[global_mappings]]
description = "Mode switch with gamepad Start button"
[global_mappings.trigger]
type = "GamepadButton"
button = 140  # Start button
[global_mappings.action]
type = "ModeChange"
mode = "Next"

Troubleshooting Hybrid Mode

Both Devices Not Responding

  1. Check connection:

    # List devices
    cargo run --bin test_midi
    cargo run --bin gamepad_diagnostic
    
  2. Verify auto_connect:

    [device]
    auto_connect = true  # Must be enabled
    
  3. Check daemon logs:

    DEBUG=1 conductor --foreground
    # Look for "Connected to MIDI device" and "Connected to gamepad"
    

Only MIDI or Only Gamepad Working

  1. Verify ID ranges: MIDI must use 0-127, gamepad must use 128-255
  2. Check for ID conflicts: No overlapping IDs between devices
  3. Test individually:
    # MIDI-only config
    cargo run --release 2  # Replace 2 with your MIDI port number
    
    # Gamepad-only config
    # (Remove MIDI mappings temporarily)
    

Hybrid Chords Not Triggering

  1. Increase chord timeout:

    [advanced_settings]
    chord_timeout_ms = 100  # Increase from 50ms
    
  2. Check button IDs: Verify you’re using correct MIDI note + gamepad button ID

  3. Use NoteChord for MIDI+gamepad: Not GamepadButtonChord

Latency Issues

  1. Reduce chord_timeout_ms for faster single-button response
  2. Check system load: Hybrid mode uses minimal CPU but check for other processes
  3. Update drivers: Ensure gamepad drivers are up to date

Advanced Hybrid Techniques

1. Device-Specific Modes

Create modes optimized for each device:

[[modes]]
name = "MIDI Focus"  # Heavy MIDI use
color = "blue"
# 80% MIDI mappings, 20% gamepad

[[modes]]
name = "Gamepad Focus"  # Heavy gamepad use
color = "green"
# 20% MIDI mappings, 80% gamepad

2. Layered Control

Use one device to modify the other’s behavior:

# Hold gamepad LB to make MIDI pads switch modes instead of triggering actions
[[modes.mappings]]
description = "Mode 1 (MIDI Pad 1 + LB held)"
[modes.mappings.trigger]
type = "NoteChord"
notes = [60, 136]  # Pad 1 + LB
timeout_ms = 50
[modes.mappings.action]
type = "ModeChange"
mode = 1

3. Analog Precision Control

Use gamepad analog triggers for precise parameter control:

# Fine volume control with trigger pressure
[[modes.mappings]]
description = "Precise volume (RT analog)"
[modes.mappings.trigger]
type = "GamepadAxisTurn"
axis = 133  # Right Trigger
direction = "Positive"
threshold = 0.1  # Very sensitive
[modes.mappings.action]
type = "VolumeControl"
action = "Set"
volume = 50  # Map to analog value in future versions

Performance Considerations

Hybrid Mode Overhead:

  • CPU: <1% additional overhead
  • Latency: <1ms additional latency
  • Memory: ~2-5MB for gamepad library

Best Practices:

  1. Use MIDI for velocity-sensitive, musical tasks
  2. Use gamepad for navigation, shortcuts, and ergonomic controls
  3. Avoid excessive chord mappings (keep under 20 total)
  4. Test thoroughly before live use

See Also

Performance Tips

Timing Best Practices

  1. Application Launch: Wait 1000-2000ms after launching apps
  2. UI Navigation: Use 100-200ms between UI interactions
  3. Form Fills: 100ms delay between field navigation
  4. File Operations: 500-1000ms for save/load operations
  5. Network Operations: 2000ms+ for remote operations

Repeat Guidelines

  1. Start Small: Begin with count=1, increase gradually
  2. Add Delays: Use delay_between_ms for counts >5
  3. Test Incrementally: Verify each step before combining
  4. Consider UX: Counts >100 may appear as hang
  5. Monitor Performance: Watch CPU/memory with large counts

Debugging

Enable debug mode to see action execution:

DEBUG=1 cargo run --release 2

Common issues:

  • Timing too tight: Increase delay_between_ms
  • Actions not executing: Check application focus
  • Repeats stopping early: Check stop_on_error setting
  • Slow performance: Reduce repeat counts or add delays

See Also

Trigger Types

Triggers define when an action should execute. Conductor supports a wide range of trigger types from simple note presses to complex patterns like chords and long presses.

Core Triggers

Note

Basic MIDI note on/off detection.

[trigger]
type = "Note"
note = 60  # Middle C
velocity_min = 1  # Optional: minimum velocity (default 1)

Parameters:

  • note (integer): MIDI note number (0-127)
  • velocity_min (integer, optional): Minimum velocity to trigger (default 1)

Use Cases:

  • Simple pad presses
  • Piano key detection
  • Basic button mapping

VelocityRange

Different actions based on how hard you press.

[trigger]
type = "VelocityRange"
note = 60
min_velocity = 80
max_velocity = 127

Parameters:

  • note (integer): MIDI note number
  • min_velocity (integer): Minimum velocity (0-127)
  • max_velocity (integer): Maximum velocity (0-127)

Velocity Levels:

  • Soft: 0-40
  • Medium: 41-80
  • Hard: 81-127

Use Cases:

  • Soft press for play, hard press for stop
  • Different shortcuts based on press intensity
  • Expressive control mappings

LongPress

Detect when a pad is held for a duration.

[trigger]
type = "LongPress"
note = 60
min_duration_ms = 1000  # Hold for 1 second

Parameters:

  • note (integer): MIDI note number
  • min_duration_ms (integer): Minimum hold duration in milliseconds (default 2000)

Use Cases:

  • Hold for 2s to quit app (prevent accidental quits)
  • Short tap for screenshot, long press for screen recording
  • Confirmation for destructive actions

DoubleTap

Detect quick double presses.

[trigger]
type = "DoubleTap"
note = 60
max_interval_ms = 300  # Optional: max time between taps

Parameters:

  • note (integer): MIDI note number
  • max_interval_ms (integer, optional): Maximum interval between taps (default 300ms)

Use Cases:

  • Double-tap to toggle fullscreen
  • Quick double-tap for emergency actions
  • Gesture-based shortcuts

NoteChord

Multiple notes pressed simultaneously.

[trigger]
type = "NoteChord"
notes = [60, 64, 67]  # C major chord
max_interval_ms = 100  # Optional

Parameters:

  • notes (array): List of MIDI note numbers
  • max_interval_ms (integer, optional): Maximum time between first and last note (default 100ms)

Use Cases:

  • Emergency exit (press 3 corners simultaneously)
  • Complex shortcuts requiring multiple pads
  • Safety mechanisms for important actions

CC (Control Change)

Continuous controller messages.

[trigger]
type = "CC"
cc = 1  # Modulation wheel
value_min = 64  # Optional: trigger only when value >= 64

Parameters:

  • cc (integer): Control change number (0-127)
  • value_min (integer, optional): Minimum value to trigger

Use Cases:

  • Button presses sending CC messages
  • Threshold-based triggers
  • Binary on/off switches

EncoderTurn

Encoder rotation with direction detection.

[trigger]
type = "EncoderTurn"
cc = 2
direction = "Clockwise"  # or "CounterClockwise"

Parameters:

  • cc (integer): Control change number
  • direction (string): “Clockwise” or “CounterClockwise”

Use Cases:

  • Volume control with encoder
  • Mode switching (clockwise = next, counter-clockwise = previous)
  • Parameter adjustment

Advanced Triggers

Aftertouch

Channel pressure sensitivity (pressure after initial press).

[trigger]
type = "Aftertouch"
note = 1  # Optional: specific pad (omit for channel aftertouch)
min_pressure = 64  # Optional: minimum pressure
max_pressure = 127  # Optional: maximum pressure

Parameters:

  • note (integer, optional): Specific pad for polyphonic aftertouch (omit for channel aftertouch)
  • min_pressure (integer, optional): Minimum pressure value (0-127)
  • max_pressure (integer, optional): Maximum pressure value (0-127)

Aftertouch Types:

  • Polyphonic (0xA0): Per-pad pressure (Maschine Mikro MK3, Launchpad Pro)
  • Channel (0xD0): Global pressure for entire device (most MIDI keyboards)

Use Cases:

  • Apply pressure to modulate effects
  • Pressure-sensitive volume control
  • Expression control without additional hardware

Device Compatibility:

DeviceSupportTypeNotes
Maschine Mikro MK3PolyphonicExcellent sensitivity
Akai MPD SeriesPolyphonicRequires firmware 1.5+
Novation Launchpad ProPolyphonicExcellent sensitivity
Launchpad MiniNoneNo aftertouch
Generic MIDI Keyboard⚠️ChannelGlobal only

Configuration Examples:

# Basic aftertouch trigger
[[modes.mappings]]
description = "Effect intensity via pressure"
[modes.mappings.trigger]
type = "Aftertouch"
note = 1
min_pressure = 64
[modes.mappings.action]
type = "Shell"
command = "osascript -e 'set volume 50'"

# Pressure ranges for different actions
[[modes.mappings]]
description = "Soft pressure"
[modes.mappings.trigger]
type = "Aftertouch"
note = 2
min_pressure = 1
max_pressure = 64
[modes.mappings.action]
type = "Keystroke"
keys = "1"

[[modes.mappings]]
description = "Hard pressure"
[modes.mappings.trigger]
type = "Aftertouch"
note = 2
min_pressure = 65
max_pressure = 127
[modes.mappings.action]
type = "Keystroke"
keys = "2"

Throttling Options:

Aftertouch generates continuous messages. Use throttling to control message rate:

[advanced_settings.aftertouch]
throttle_ms = 50        # Max 20 messages/sec
delta_threshold = 5     # Only trigger on ±5 pressure change
hysteresis_gap = 10     # Prevent zone oscillation

PitchBend

Touch strip or pitch wheel control (14-bit precision).

[trigger]
type = "PitchBend"
min_value = 8192   # Center
max_value = 16383  # Max up
center_deadzone = 100  # Optional: ignore small movements near center

Parameters:

  • min_value (integer): Minimum bend value (0-16383)
  • max_value (integer): Maximum bend value (0-16383)
  • center_deadzone (integer, optional): Deadzone size around center (8192)

Value Range:

  • Full Range: 0-16383 (14-bit resolution)
  • Center: 8192
  • Down: 0-8191
  • Up: 8193-16383
  • Normalized: -1.0 to +1.0

Use Cases:

  • Touch strip for volume control
  • Timeline scrubbing in DAWs
  • Smooth parameter sweeps
  • Multi-zone selection (divide strip into regions)

Configuration Examples:

# Volume control with touch strip
[[global_mappings]]
description = "Volume up via touch strip"
[global_mappings.trigger]
type = "PitchBend"
min_value = 8192  # Center
max_value = 16383  # Max up
center_deadzone = 100
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

# Multi-zone mapping (divide strip into 5 zones)
[[modes.mappings]]
description = "Zone 1: Far Left"
[modes.mappings.trigger]
type = "PitchBend"
min_value = 0
max_value = 3276  # 20% of range
[modes.mappings.action]
type = "Keystroke"
keys = "1"

[[modes.mappings]]
description = "Zone 2: Left"
[modes.mappings.trigger]
type = "PitchBend"
min_value = 3277
max_value = 6553  # 40% of range
[modes.mappings.action]
type = "Keystroke"
keys = "2"

# Center detection
[[modes.mappings]]
description = "Reset on center touch"
[modes.mappings.trigger]
type = "PitchBend"
min_value = 8092  # Center - 100
max_value = 8292  # Center + 100
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["cmd"]

Throttling Options:

Pitch bend generates 100-1000+ messages/sec. Use throttling:

[advanced_settings.pitch_bend]
throttle_ms = 50         # Max 20 messages/sec
delta_threshold = 100    # Trigger only on ±100 change
use_zones = true         # Zone-based triggering
num_zones = 16           # 16 discrete zones

Spring-Back Behavior:

Many controllers (Maschine Mikro MK3, MIDI keyboards) auto-return to center:

  • Returns to 8192 when released
  • Not suitable for persistent state/toggles
  • Use for momentary effects only
  • Spring-back generates message flood on release

Platform Notes:

  • macOS: Full 14-bit support, no special handling
  • Linux: Some ALSA drivers may reverse MSB/LSB byte order
  • Windows: Supported, but some USB MIDI drivers have 10-50ms latency

Trigger Composition

Triggers can work together for complex behaviors:

Example: Conditional Trigger

# Only trigger during specific time range
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 60
[modes.mappings.action]
type = "Conditional"
conditions = [{ type = "TimeRange", start = "09:00", end = "17:00" }]
then_action = { type = "Shell", command = "work-task" }
else_action = { type = "Shell", command = "personal-task" }

Performance Notes

  • Note/CC: <0.1ms detection latency
  • LongPress: Checked every 50ms
  • DoubleTap: 300ms window (configurable)
  • Chord: 100ms window (configurable)
  • Aftertouch: Continuous messages, use throttling
  • PitchBend: Continuous messages, 14-bit precision

Troubleshooting

Trigger Not Firing

  • Use midi_diagnostic tool to verify MIDI messages:
    cargo run --bin midi_diagnostic 2
    
  • Check note numbers match MIDI output
  • Verify velocity thresholds
  • Enable debug logging: DEBUG=1 cargo run

Aftertouch Not Working

  • Check device compatibility (not all controllers support aftertouch)
  • Verify polyphonic vs channel aftertouch
  • Test with midi_diagnostic tool
  • Adjust pressure thresholds

PitchBend Jittery

  • Increase center_deadzone value
  • Enable throttling in advanced_settings
  • Use delta_threshold for change-based triggering
  • Consider zone-based triggering instead of continuous

Chord Not Detected

  • Increase max_interval_ms (default 100ms)
  • Press pads more simultaneously
  • Check with midi_diagnostic for timing
  • Verify all note numbers correct

See Also

Action Types

Actions are what Conductor executes when a trigger is detected. This page documents all available action types and their configuration.

Core Actions

Keystroke

Simulates keyboard input with optional modifiers.

[action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

Parameters:

  • keys (string): Key(s) to press (e.g., “space”, “return”, “f1”)
  • modifiers (array): Optional modifiers: “cmd”, “ctrl”, “alt”, “shift”

Supported Keys:

  • Letters: a-z
  • Numbers: 0-9
  • Special: space, return, tab, escape, backspace, delete
  • Arrows: up, down, left, right
  • Function: f1 through f12
  • Navigation: home, end, pageup, pagedown

Text

Types arbitrary text strings with full Unicode support. Uses keyboard simulation to type character-by-character.

[action]
type = "Text"
text = "Hello, World!"

Parameters:

  • text (string): Text to type (supports Unicode, emoji, multi-line)

Use Cases:

  • Text snippets and templates
  • Email signatures and contact information
  • Code boilerplate and common patterns
  • Form auto-fill
  • Multi-language text input

Configuration Examples:

Simple Text:

[action]
type = "Text"
text = "user@example.com"

Multi-line Code Snippet:

[action]
type = "Text"
text = """
fn main() {
    println!(\"Hello, World!\");
}
"""

Unicode and Emoji:

[action]
type = "Text"
text = "✅ Task completed! 🎉"

Special Characters:

[action]
type = "Text"
text = "!@#$%^&*()_+-=[]{}|;':\",./<>?"

Text with Delay (using Sequence):

[action]
type = "Sequence"
actions = [
    { type = "Text", text = "username" },
    { type = "Delay", ms = 100 },
    { type = "Text", text = "@company.com" }
]

TOML String Escaping:

# Escape quotes in basic strings
text = "He said \"hello\""

# Use multiline strings for complex text
text = """
Line 1
Line 2 with "quotes"
"""

# Use literal strings to avoid escaping backslashes
text = 'C:\Users\Documents\file.txt'

Platform Support:

  • macOS: Accessibility APIs
  • Linux: X11/Wayland
  • Windows: Windows API

Behavior:

  • Types character-by-character (no clipboard)
  • Respects active keyboard layout
  • Typing speed controlled by OS
  • Requires text input focus

Troubleshooting:

  • Ensure target app has input focus
  • Check Input Monitoring permissions (macOS)
  • For slow UIs, use Sequence with Delay actions
  • For special keys (Enter, Tab, Ctrl), use Keystroke action instead

Launch

Opens an application by name or path using platform-specific system commands.

[action]
type = "Launch"
app = "Safari"

Parameters:

  • app (string): Application name or full path to executable/bundle

Platform Behavior:

macOS:

  • Uses open -a command
  • Accepts app names: “Safari”, “Visual Studio Code”, “Logic Pro”
  • Accepts full paths: “/Applications/Safari.app”
  • If app already running, brings to front
  • Searches standard paths: /Applications, ~/Applications, /System/Applications

Linux:

  • Direct executable launch via Command::spawn()
  • Requires full path or executable in $PATH
  • No automatic .desktop file resolution
  • Typically launches new instance even if already running

Windows:

  • Uses cmd /C start command
  • Accepts app names or paths
  • Uses Windows file associations
  • Behavior varies by app’s single-instance implementation

Examples:

# Launch by app name (macOS)
[action]
type = "Launch"
app = "Terminal"

# Launch by full path
[action]
type = "Launch"
app = "/Applications/Utilities/Terminal.app"

# Launch script with full path
[action]
type = "Launch"
app = "/Users/username/scripts/backup.sh"

# Launch multiple apps in sequence
[action]
type = "Sequence"
actions = [
    { type = "Launch", app = "VS Code" },
    { type = "Delay", ms = 1000 },
    { type = "Launch", app = "Terminal" }
]

Troubleshooting:

  • App not found: Use full path instead of app name
  • macOS test: Run open -a "App Name" in Terminal
  • Linux test: Run which app-name or use full path
  • Silent failures: Enable debug mode with DEBUG=1
  • Permissions: Ensure execute permissions on scripts (chmod +x)

Notes:

  • Launch is non-blocking (returns immediately)
  • No validation that app exists before launch attempt
  • Errors fail silently (check debug logs)
  • App names with spaces are automatically handled
  • Launch time varies: 100ms (small apps) to 10s (large IDEs/DAWs)

Shell

Executes a shell command.

[action]
type = "Shell"
command = "npm test"

Parameters:

  • command (string): Shell command to execute

Platform Notes:

  • Unix/macOS: Uses sh -c
  • Windows: Uses cmd /C

Security Warning: Be cautious with user input in shell commands.

System Actions

VolumeControl

Controls system volume with cross-platform support for increase, decrease, mute/unmute, and absolute level setting.

[action]
type = "VolumeControl"
operation = "Up"
amount = 5  # Optional: volume increment (default 5)

Parameters:

  • operation (string): Volume operation to perform
  • amount (integer, optional):
    • For Up/Down: Increment amount 0-100 (default 5)
    • For Set: Absolute level 0-100

Operations:

  • Up: Increase volume by amount
  • Down: Decrease volume by amount
  • Mute: Mute audio output
  • Unmute: Unmute audio output
  • Toggle: Toggle mute state
  • Set: Set to specific level (requires amount parameter)

Platform Support:

PlatformMethodLatencyDependencies
macOSAppleScript50-100msNone (built-in)
Linux (PulseAudio)pactl10-30mspulseaudio-utils
Linux (Pipewire)wpctl5-15mswireplumber
Linux (ALSA)amixer15-40msalsa-utils
Windowsnircmd20-50msnircmd.exe
WindowsCOM API5-10msNone (built-in)

Configuration Examples:

# Encoder volume control
[[global_mappings]]
description = "Volume Up"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 2
direction = "Clockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Up"
amount = 2  # Small increments for smooth control

[[global_mappings]]
description = "Volume Down"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 2
direction = "CounterClockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Down"
amount = 2

# Mute toggle
[[global_mappings]]
description = "Mute/Unmute"
[global_mappings.trigger]
type = "Note"
note = 16
[global_mappings.action]
type = "VolumeControl"
operation = "Toggle"

# Set to specific level
[[modes.mappings]]
description = "Set volume to 50%"
[modes.mappings.trigger]
type = "Note"
note = 8
[modes.mappings.action]
type = "VolumeControl"
operation = "Set"
amount = 50

# Velocity-based volume (multiple mappings)
[[modes.mappings]]
description = "Soft press = 25%"
[modes.mappings.trigger]
type = "VelocityRange"
note = 9
min_velocity = 0
max_velocity = 40
[modes.mappings.action]
type = "VolumeControl"
operation = "Set"
amount = 25

[[modes.mappings]]
description = "Medium press = 50%"
[modes.mappings.trigger]
type = "VelocityRange"
note = 9
min_velocity = 41
max_velocity = 80
[modes.mappings.action]
type = "VolumeControl"
operation = "Set"
amount = 50

[[modes.mappings]]
description = "Hard press = 75%"
[modes.mappings.trigger]
type = "VelocityRange"
note = 9
min_velocity = 81
max_velocity = 127
[modes.mappings.action]
type = "VolumeControl"
operation = "Set"
amount = 75

Use Cases:

  • Producer: Encoder for smooth volume adjustment while mixing
  • Developer: Quick mute during video calls, volume presets for different activities
  • Streamer: Real-time volume balancing during streams, emergency mute
  • Presenter: Volume control without touching laptop during presentations

Troubleshooting:

macOS:

  • No dependencies required
  • AppleScript latency ~50-100ms is normal
  • Requires no special permissions

Linux:

  • Install dependencies:
    # PulseAudio (most common)
    sudo apt install pulseaudio-utils
    
    # Pipewire (modern, fastest)
    sudo apt install pipewire wireplumber
    
    # ALSA (minimal systems)
    sudo apt install alsa-utils
    
  • Conductor auto-detects available backend
  • User must be in audio group

Windows:

  • Download nircmd.exe from https://www.nirsoft.net/utils/nircmd.html
  • Add to PATH or place in Conductor directory
  • COM API fallback requires no dependencies

Performance Notes:

  • Volume commands are non-blocking
  • State changes typically take 5-100ms depending on platform
  • Multiple rapid commands may queue on slower platforms (macOS AppleScript)

ModeChange

Switches between different mapping modes with optional LED transition effects and relative navigation.

[action]
type = "ModeChange"
mode = 1  # Switch to mode index 1

Parameters:

  • mode (integer): Zero-based mode index or offset (if relative = true)
  • relative (boolean, optional): If true, mode is offset from current mode (default false)
  • transition_effect (string, optional): Visual transition effect: “Flash”, “Sweep”, “FadeOut”, “Spiral”, “None”

Mode Indexing:

  • Modes are zero-based: Mode 0, Mode 1, Mode 2, etc.
  • Mode 0 is typically the default mode on startup
  • Invalid mode indices are clamped to valid range

Absolute vs Relative Mode Changes:

Absolute (direct jump):

[action]
type = "ModeChange"
mode = 2  # Jump directly to Mode 2
transition_effect = "Flash"

Relative (navigation):

# Next mode (with wrapping)
[action]
type = "ModeChange"
mode = 1  # +1 offset
relative = true
transition_effect = "Sweep"

# Previous mode (with wrapping)
[action]
type = "ModeChange"
mode = -1  # -1 offset
relative = true

Transition Effects:

EffectDurationDescription
Flash150msQuick white flash
Sweep120msLeft-to-right wave
FadeOut200msFade out old, fade in new
Spiral240msCenter-outward spiral
None0msInstant switch

Configuration Examples:

# Encoder for mode cycling
[[global_mappings]]
description = "Next Mode"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = 1  # +1 offset
relative = true
transition_effect = "Sweep"

[[global_mappings]]
description = "Previous Mode"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "CounterClockwise"
[global_mappings.action]
type = "ModeChange"
mode = -1  # -1 offset (wraps backward)
relative = true
transition_effect = "Sweep"

# Direct mode selection with chords
[[global_mappings]]
description = "Jump to Default Mode"
[global_mappings.trigger]
type = "NoteChord"
notes = [1, 9]  # Top-left corners
[global_mappings.action]
type = "ModeChange"
mode = 0
transition_effect = "Flash"

[[global_mappings]]
description = "Jump to Development Mode"
[global_mappings.trigger]
type = "NoteChord"
notes = [2, 10]
[global_mappings.action]
type = "ModeChange"
mode = 1
transition_effect = "Flash"

[[global_mappings]]
description = "Jump to Media Mode"
[global_mappings.trigger]
type = "NoteChord"
notes = [3, 11]
[global_mappings.action]
type = "ModeChange"
mode = 2
transition_effect = "FadeOut"

# Conditional mode change
[[modes.mappings]]
description = "Switch to Media mode if Spotify running"
[modes.mappings.trigger]
type = "Note"
note = 8
[modes.mappings.action]
type = "Conditional"
conditions = [{ type = "AppRunning", bundle_id = "com.spotify.client" }]
then_action = { type = "ModeChange", mode = 2, transition_effect = "FadeOut" }
else_action = { type = "Launch", app = "Spotify" }

Mode Configuration:

Modes are defined in your config.toml with names and LED colors:

[[modes]]
name = "Default"
color = "blue"             # LED color for this mode
led_idle_brightness = 20   # Brightness when pad not pressed
led_active_brightness = 255 # Brightness when pad pressed
mappings = [...]

[[modes]]
name = "Development"
color = "green"
led_idle_brightness = 30
led_active_brightness = 255
mappings = [...]

[[modes]]
name = "Media"
color = "purple"
led_idle_brightness = 15
led_active_brightness = 200
mappings = [...]

LED Feedback Integration:

  • Mode colors automatically update LEDs on mode change
  • Transition effects provide visual feedback
  • Optional mode indicator pads (e.g., bottom row shows active mode)
  • Idle/active brightness levels per mode

Use Cases:

Developer (Sam) - Context Switching:

  • Mode 0: General productivity shortcuts
  • Mode 1: IDE shortcuts (build, test, debug)
  • Mode 2: Media controls
  • Encoder rotation for quick mode cycling

Producer (Alex) - Production Workflow:

  • Mode 0: Recording (transport, record arm)
  • Mode 1: Mixing (volume, mute/solo, effects)
  • Mode 2: Mastering (compressor, EQ, limiter)
  • Chord combinations for instant mode jumps

Streamer (Jordan) - Live Streaming:

  • Mode 0: Pre-Stream (setup checks, app launching)
  • Mode 1: Live (scene switching, alerts)
  • Mode 2: BRB (limited controls, auto-mute)
  • Mode 3: Post-Stream (save, shutdown sequence)

Designer (Taylor) - Creative Workflows:

  • Mode 0: Sketch (drawing tools, layers)
  • Mode 1: Edit (selection, transform, filters)
  • Mode 2: Export (save presets, formats)
  • Visual LED feedback shows current mode

Circular Mode Navigation:

With relative = true, modes wrap around:

  • Mode 0 → +1 → Mode 1
  • Mode 1 → +1 → Mode 2
  • Mode 2 → +1 → Mode 0 (wraps to beginning)
  • Mode 0 → -1 → Mode 2 (wraps to end)

Troubleshooting:

Mode Not Switching:

  • Verify mode index is within range (0 to num_modes-1)
  • Check that modes are defined in config.toml
  • Enable debug logging: DEBUG=1 cargo run
  • Ensure mode change mapping is in global_mappings to work from all modes

LEDs Not Updating:

  • Verify color is set for each mode
  • Check device supports RGB LED feedback
  • Ensure LED feedback is enabled
  • Test with different transition effects

Transition Effects Not Working:

  • Only works with devices supporting RGB LEDs (Maschine Mikro MK3, etc.)
  • MIDI-only devices show instant switches
  • Check transition timing isn’t too fast to notice

Performance Notes:

  • Mode switches are near-instant (<1ms)
  • Transition effects add 0-240ms visual delay
  • LED updates are non-blocking
  • Rapid mode changes are queued and handled gracefully

Advanced Actions

Sequence

Executes multiple actions in order.

[action]
type = "Sequence"
actions = [
    { type = "Keystroke", keys = "n", modifiers = ["cmd"] },
    { type = "Delay", ms = 500 },
    { type = "Text", text = "New Document" }
]

Parameters:

  • actions (array): Array of action configurations

Notes:

  • Actions execute sequentially
  • Default 50ms delay between actions
  • Use Delay action for longer pauses

Delay

Pauses execution.

[action]
type = "Delay"
ms = 1000  # Wait 1 second

Parameters:

  • ms (integer): Milliseconds to wait

Use Cases:

  • Wait for app to launch
  • Timing between keystrokes
  • Synchronization in sequences

Repeat

Repeats an action multiple times.

[action]
type = "Repeat"
count = 10
delay_between_ms = 100
action = { type = "Keystroke", keys = "right" }

Parameters:

  • action (object): Action to repeat
  • count (integer): Number of repetitions
  • delay_between_ms (integer, optional): Delay between repetitions in milliseconds

Use Cases:

  • Navigate through items
  • Rapid-fire actions
  • Automation tasks

MouseClick

Simulates mouse clicks.

[action]
type = "MouseClick"
button = "left"
x = 100
y = 200

Parameters:

  • button (string): “left”, “right”, or “middle”
  • x (integer, optional): X coordinate (absolute)
  • y (integer, optional): Y coordinate (absolute)

Notes:

  • If x and y are omitted, clicks at current cursor position
  • Coordinates are screen-absolute

Conditional

Executes actions based on conditions.

[action]
type = "Conditional"
conditions = [
    { type = "AppRunning", bundle_id = "com.apple.Logic" }
]
operator = "And"
then_action = { type = "Keystroke", keys = "space" }
else_action = { type = "Launch", app = "Logic Pro" }

Parameters:

  • conditions (array): Array of condition objects
  • operator (string, optional): “And” or “Or” (default: “And”)
  • then_action (object): Action when conditions are true
  • else_action (object, optional): Action when conditions are false

Condition Types:

AppRunning

Check if an application is running.

{ type = "AppRunning", bundle_id = "com.spotify.client" }

AppNotRunning

Check if an application is not running.

{ type = "AppNotRunning", bundle_id = "com.spotify.client" }

TimeRange

Check if current time is within a range.

{ type = "TimeRange", start = "09:00", end = "17:00" }

Format: HH:MM in 24-hour format Note: Uses local system time

DayOfWeek

Check if today matches specified days.

{ type = "DayOfWeek", days = ["Mon", "Tue", "Wed", "Thu", "Fri"] }

Valid Days: “Mon”, “Tue”, “Wed”, “Thu”, “Fri”, “Sat”, “Sun”

ModifierPressed

Check if a modifier key is held.

{ type = "ModifierPressed", modifier = "Shift" }

Valid Modifiers: “Shift”, “Ctrl”, “Cmd”, “Alt”, “Option”

ModeActive

Check if a specific mode is active.

{ type = "ModeActive", mode = 1 }

Parameter: mode (integer): Zero-based mode index

Operators:

  • And: All conditions must be true
  • Or: At least one condition must be true

Nested Conditionals: You can nest conditionals for complex decision trees:

[action]
type = "Conditional"
conditions = [{ type = "TimeRange", start = "09:00", end = "17:00" }]
then_action = {
    type = "Conditional",
    conditions = [{ type = "AppRunning", bundle_id = "com.microsoft.VSCode" }],
    then_action = { type = "Keystroke", keys = "b", modifiers = ["cmd", "shift"] },
    else_action = { type = "Launch", app = "Visual Studio Code" }
}
else_action = { type = "Launch", app = "Spotify" }

Action Composition

Actions can be combined in powerful ways:

Example: Complex Workflow

[action]
type = "Sequence"
actions = [
    { type = "Launch", app = "Terminal" },
    { type = "Delay", ms = 1000 },
    { type = "Text", text = "cd ~/projects && npm test" },
    { type = "Keystroke", keys = "return" }
]

Example: Conditional with Repeat

[action]
type = "Conditional"
conditions = [{ type = "ModifierPressed", modifier = "Shift" }]
then_action = {
    type = "Repeat",
    count = 5,
    delay_between_ms = 200,
    action = { type = "Keystroke", keys = "right" }
}
else_action = { type = "Keystroke", keys = "right" }

Performance Notes

  • Keystroke: <1ms execution time
  • Shell: Asynchronous, non-blocking
  • Launch: Platform-dependent, typically 100-500ms
  • Conditional: 10-50ms per condition evaluation
  • Sequence: Sum of individual action times + delays

Troubleshooting

Keystroke Not Working

  • Verify key name spelling
  • Check modifier syntax
  • Ensure app has input focus

Shell Command Fails Silently

  • Test command in terminal first
  • Check environment variables
  • Enable debug logging: DEBUG=1 cargo run

Conditional Not Triggering

  • Verify bundle ID format (macOS): osascript -e 'id of app "AppName"'
  • Check time format (24-hour: “09:00”, not “9:00 AM”)
  • Verify day names match exactly

Launch App Not Found

  • Use exact app name as it appears in Applications folder
  • On Linux, use full executable path if not in PATH
  • Check permissions for app execution

See Also

LED System Reference

Conductor provides comprehensive LED feedback for supported MIDI controllers, with full RGB control for HID devices (Native Instruments Maschine Mikro MK3) and basic on/off control for standard MIDI devices.

Overview

The LED system consists of four main features:

  1. F23: LED Lighting Schemes - 10 pre-defined animation patterns
  2. F24: LED Velocity Feedback - Color-coded response based on pad velocity
  3. F25: LED Mode Indicators - Visual feedback for active mode
  4. F26: LED Fade Effects - Smooth transitions and fade-out animations

Feature F23: LED Lighting Schemes

Conductor supports 10 distinct lighting schemes that can be selected at startup or configured in your config file.

Available Schemes

SchemeDescriptionUse CaseCPU Impact
offAll LEDs disabledPower saving, minimal distraction0%
staticSolid color based on current modeClear mode indication<1%
breathingSlow 2-second breathing effectAmbient, relaxed workflow~1%
pulseFast 500ms pulseHigh-energy, performance~1%
rainbowRainbow cycle across padsCreative sessions, visual appeal~2%
waveWave animation patternDynamic feedback~2%
sparkleRandom sparklesPlayful, attention-grabbing~3%
reactiveVelocity-based colors (most common)Precise feedback, performance~1%
vumeterVU meter style (bottom-up)Audio visualization~2%
spiralSpiral pattern animationArtistic, mesmerizing~2%

Command-Line Usage

# Start with reactive scheme (recommended)
cargo run --release 2 --led reactive

# Rainbow animation
cargo run --release 2 --led rainbow

# Disable LEDs
cargo run --release 2 --led off

# Static mode colors
cargo run --release 2 --led static

Configuration

[led_settings]
scheme = "reactive"  # Default scheme on startup

Implementation Details

  • Update Rate: 10fps (100ms per frame) to maintain low CPU usage
  • HID Devices: Full RGB control with 16.7 million colors
  • MIDI Devices: Schemes degrade gracefully to on/off
  • Performance: All schemes designed for <3% CPU usage

Feature F24: LED Velocity Feedback

Reactive LED feedback provides immediate visual confirmation of pad velocity, using color-coded responses.

Velocity Color Mapping

Velocity RangeColorVisualMeaning
0-40Green🟢Soft press
41-80Yellow🟡Medium press
81-127Red🔴Hard press

Behavior

On Press:

  • Pad immediately lights up in velocity-appropriate color
  • Brightness scales with velocity (40% base + 60% velocity-based)
  • Color change happens within <1ms for responsive feedback

On Release:

  • Fade-out begins after 1 second
  • Fade duration: 200ms (configurable in future versions)
  • Pad returns to mode color or off state

Configuration Example

[led_settings]
scheme = "reactive"

# Future: Velocity threshold customization
[led_settings.reactive]
soft_color = { r = 0, g = 255, b = 0 }      # Green
medium_color = { r = 255, g = 255, b = 0 }  # Yellow
hard_color = { r = 255, g = 0, b = 0 }      # Red
fade_duration_ms = 1000

Use Cases

  • Performance: Immediate feedback on hit strength for drum pads
  • Practice: Visual metronome or timing reference
  • Recording: Confirm input without audio monitoring
  • Accessibility: Visual alternative to audio feedback

Feature F25: LED Mode Indicators

Visual feedback showing the currently active mode, helping users stay oriented when switching contexts.

Mode Color Scheme

Each mode has an associated color that LEDs display when using static or reactive schemes:

ModeDefault ColorRGB ValuesVisual
Mode 0 (Default)Blue(0, 100, 255)🔵
Mode 1 (Development)Green(0, 255, 0)🟢
Mode 2 (Media)Purple(200, 0, 255)🟣

Configuration

[[modes]]
name = "Default"
color = "blue"
led_idle_brightness = 20      # Brightness when idle (0-255)
led_active_brightness = 255   # Brightness when pressed

[[modes]]
name = "Development"
color = "green"
led_idle_brightness = 30
led_active_brightness = 255

[[modes]]
name = "Media"
color = "purple"
led_idle_brightness = 15
led_active_brightness = 200

Mode Transition Effects

When switching modes, you can apply visual transition effects:

[[global_mappings]]
description = "Next Mode with Flash"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = 1
relative = true
transition_effect = "Flash"  # Options: Flash, Sweep, FadeOut, Spiral, None

Transition Effects:

  • Flash: Quick white flash (150ms) - best for rapid mode switching
  • Sweep: Left-to-right wave (120ms) - smooth visual sweep
  • FadeOut: Fade old color to new (200ms) - smooth transition
  • Spiral: Center-outward spiral (240ms) - artistic transition

Optional Mode Indicator Pads

You can dedicate specific pads to always show the current mode:

[led_settings]
mode_indicator_pads = [13, 14, 15, 16]  # Bottom row shows mode
# Pad 13 lights up for Mode 0
# Pad 14 lights up for Mode 1
# Pad 15 lights up for Mode 2
# Pad 16 lights up for Mode 3

Feature F26: LED Fade Effects

Smooth transitions and fade animations provide polished visual feedback.

Fade-Out on Release

After pressing a pad, the LED fades out gracefully instead of turning off instantly:

Default Behavior:

  1. Pad pressed → Immediate color change (velocity-based)
  2. Pad released → Wait 1 second
  3. Fade begins → 200ms smooth fade to mode color or off

Timeline:

Press    Release            Fade Start         Complete
  |---------|-------------------|-----------------|
  0ms    Variable            +1000ms          +1200ms
  ↑                              ↑                 ↑
  Full brightness          Start fade         Dark/Mode color

Configurable Fade Parameters

[led_settings.fade]
delay_after_release_ms = 1000  # How long to wait before fading
fade_duration_ms = 200         # How long the fade takes
steps = 10                     # Number of discrete fade steps

Fade Applications

Reactive Scheme:

// Pseudo-code for reactive fade
on_pad_press(pad, velocity) {
    color = velocity_to_color(velocity)
    set_pad_color(pad, color)
    schedule_fade(pad, delay=1000ms, duration=200ms)
}

Mode Transition:

// Pseudo-code for mode transition fade
on_mode_change(old_mode, new_mode) {
    // FadeOut transition effect
    for brightness in (100% down_to 0%).step_by(10%) {
        set_all_pads(old_mode_color * brightness)
        wait(10ms)
    }
    for brightness in (0% up_to 100%).step_by(10%) {
        set_all_pads(new_mode_color * brightness)
        wait(10ms)
    }
}

Device Support

HID Devices (Full RGB Control)

Native Instruments Maschine Mikro MK3:

  • ✅ All 10 lighting schemes
  • ✅ Full RGB color spectrum (16.7M colors)
  • ✅ Velocity-based colors
  • ✅ Smooth fades and transitions
  • ✅ Shared device mode (works alongside NI Controller Editor)

Requirements:

  • macOS: Input Monitoring permission
  • USB connection: Direct HID communication
  • Drivers: Native Instruments drivers installed

MIDI Devices (On/Off Only)

Supported Controllers:

  • Novation Launchpad Mini/Pro
  • Akai APC Mini/40
  • Generic MIDI pad controllers

Limitations:

  • ❌ RGB colors (on/off only)
  • ❌ Smooth fades (instant on/off)
  • ✅ Reactive feedback (on when pressed, off after delay)
  • ✅ Mode indicators (basic)

Note Range: MIDI LEDs typically respond to notes C1-D#2 (36-51) for 16 pads


Configuration Examples

Minimal Configuration

[device]
name = "Mikro"
led_feedback = true

[led_settings]
scheme = "reactive"

Advanced Configuration

[device]
name = "Mikro"
led_feedback = true

[led_settings]
scheme = "reactive"
brightness = 255

[led_settings.reactive]
soft_color = { r = 0, g = 255, b = 0 }
medium_color = { r = 255, g = 255, b = 0 }
hard_color = { r = 255, g = 0, b = 0 }
fade_duration_ms = 1000

[led_settings.transitions]
enable_effects = true
flash_duration_ms = 150
sweep_delay_ms = 30
fadeout_steps = 10
spiral_delay_ms = 15

[[modes]]
name = "Default"
color = "blue"
led_idle_brightness = 20
led_active_brightness = 255

[[modes]]
name = "Development"
color = "green"
led_idle_brightness = 30
led_active_brightness = 255

Troubleshooting

LEDs Not Working (Mikro MK3)

  1. Check permissions (macOS):

    • System Settings → Privacy & Security → Input Monitoring
    • Enable permission for Terminal or your IDE
  2. Verify HID connection:

    DEBUG=1 cargo run --release 2 --led reactive
    # Look for "✓ Connected to Mikro MK3 LED interface"
    
  3. Check for conflicts:

    • Close Native Instruments Controller Editor
    • Unplug and replug USB cable
    • Try different USB port

Wrong Colors

  • Issue: Colors don’t match expected values
  • Cause: LED manufacturing variance (±10% color accuracy)
  • Solution: This is normal, colors may vary slightly between units

Flickering LEDs

  • Issue: LEDs flicker or strobe
  • Cause: Update rate too high or USB bandwidth saturation
  • Solution: Use simpler scheme (reactive or static) instead of complex animations

Mode Colors Not Showing

  • Issue: All pads same color regardless of mode
  • Cause: Using non-mode-aware scheme
  • Solution: Use static or reactive scheme, not rainbow or sparkle

Performance Considerations

CPU Usage by Scheme

SchemeCPU (Idle)CPU (Active)Memory
off0%0%Minimal
static<1%<1%<1MB
reactive<1%~1%<1MB
breathing~1%~1%<1MB
rainbow~2%~2%<1MB
sparkle~3%~3%~1MB

USB Bandwidth

  • Update Rate: 10fps (100ms per frame)
  • Data per Update: 51 bytes (1 byte report ID + 50 bytes RGB data)
  • Bandwidth: 510 bytes/sec (~0.004 Mbps)
  • Impact: Negligible on USB 2.0 (480 Mbps)

Recommendations

  • Performance Mode: Use reactive or static for lowest CPU usage
  • Visual Appeal: Use rainbow or spiral for presentations/demos
  • Battery (USB-powered hubs): Use off or static to reduce power draw

API Reference

Trait Interface

pub trait PadFeedback: Send {
    fn connect(&mut self) -> Result<(), Box<dyn Error>>;
    fn set_pad_color(&mut self, pad: u8, color: RGB) -> Result<(), Box<dyn Error>>;
    fn set_pad_velocity(&mut self, pad: u8, velocity: u8) -> Result<(), Box<dyn Error>>;
    fn set_mode_colors(&mut self, mode: u8) -> Result<(), Box<dyn Error>>;
    fn show_velocity_feedback(&mut self, pad: u8, velocity: u8) -> Result<(), Box<dyn Error>>;
    fn flash_pad(&mut self, pad: u8, color: RGB, duration_ms: u64) -> Result<(), Box<dyn Error>>;
    fn ripple_effect(&mut self, start_pad: u8, color: RGB) -> Result<(), Box<dyn Error>>;
    fn clear_all(&mut self) -> Result<(), Box<dyn Error>>;
    fn show_long_press_feedback(&mut self, pad: u8, elapsed_ms: u128) -> Result<(), Box<dyn Error>>;
    fn run_scheme(&mut self, scheme: &LightingScheme) -> Result<(), Box<dyn Error>>;
}

RGB Color Structure

pub struct RGB {
    pub r: u8,  // Red: 0-255
    pub g: u8,  // Green: 0-255
    pub b: u8,  // Blue: 0-255
}

impl RGB {
    pub const OFF: RGB = RGB { r: 0, g: 0, b: 0 };
    pub const RED: RGB = RGB { r: 255, g: 0, b: 0 };
    pub const GREEN: RGB = RGB { r: 0, g: 255, b: 0 };
    pub const BLUE: RGB = RGB { r: 0, g: 0, b: 255 };
    pub const YELLOW: RGB = RGB { r: 255, g: 255, b: 0 };
    pub const PURPLE: RGB = RGB { r: 255, g: 0, b: 255 };
}

See Also

CLI Commands Reference

Overview

Conductor provides a daemon service, control utility, and several diagnostic tools, all accessible via the command line. This reference covers all available commands, their options, and usage examples.

v1.0.0+ introduces daemon architecture with background service and hot-reload capabilities.

Daemon Service: conductor

The primary Conductor daemon service (v1.0.0+). Runs as a background process with config hot-reload.

Basic Syntax

# Start daemon (via cargo)
cargo run --release --bin conductor [PORT] [OPTIONS]

# Start daemon (release binary)
./target/release/conductor [PORT] [OPTIONS]

# Or use systemd/launchd (see Installation)
systemctl --user start conductor  # Linux
launchctl load ~/Library/LaunchAgents/com.amiable.conductor.plist  # macOS

Daemon Features (v1.0.0+)

  • Background Service: Runs continuously in the background
  • Config Hot-Reload: Reload configuration without restart (0-8ms latency)
  • State Persistence: Saves state on shutdown, restores on startup
  • IPC Control: Control via conductorctl utility
  • Auto-Recovery: Graceful error handling and device reconnection

Arguments

PORT (Required in some cases)

The MIDI input port number to connect to.

Finding available ports:

# List all MIDI ports
cargo run --release

# Or
./target/release/conductor

Output:

Available MIDI input ports:
0: USB MIDI Device
1: IAC Driver Bus 1
2: Maschine Mikro MK3 - Input
3: Digital Keyboard

Usage:

# Connect to port 2 (Maschine Mikro MK3)
cargo run --release 2

# Connect to port 0
cargo run --release 0

Note: If auto_connect = true in config.toml, the port argument is optional and Conductor will connect to the first available port.

Options

–led, –lighting

Select LED lighting scheme (for devices with LED support).

Syntax:

cargo run --release 2 --led <SCHEME>
# or
cargo run --release 2 --lighting <SCHEME>

Available schemes:

  • reactive (default) - Velocity-based colors, fade out after release
  • rainbow - Static rainbow gradient
  • breathing - Breathing effect (all pads)
  • pulse - Pulsing effect
  • wave - Wave pattern with brightness gradient
  • sparkle - Random twinkling LEDs
  • vumeter - VU meter style gradient (green → yellow → red)
  • spiral - Spiral/diagonal pattern
  • static - Static single color
  • off - LEDs disabled

Examples:

# Reactive mode (velocity-sensitive)
cargo run --release 2 --led reactive

# Rainbow gradient
cargo run --release 2 --led rainbow

# Breathing effect
cargo run --release 2 --lighting breathing

# Turn off LEDs
cargo run --release 2 --led off

LED Behavior by Scheme:

reactive:

  • Soft press (velocity < 50): Green Dim
  • Medium press (50 ≤ velocity < 100): Yellow Normal
  • Hard press (velocity ≥ 100): Red Bright
  • Fades out 1 second after release

rainbow:

  • Static rainbow gradient across all pads
  • No animation (constant colors)

sparkle:

  • Random white LEDs
  • 20% probability per pad per frame
  • Updates every 100ms

vumeter:

  • Green (bottom rows)
  • Yellow/Orange (middle)
  • Red (top)

wave:

  • Blue with varying brightness
  • Creates wave effect

–profile, -p

Load a Native Instruments Controller Editor profile (.ncmm3 file).

Syntax:

cargo run --release 2 --profile <PATH>
# or
cargo run --release 2 -p <PATH>

Examples:

# Relative path
cargo run --release 2 --profile my-profile.ncmm3

# Absolute path
cargo run --release 2 --profile ~/Downloads/base-template-ni-mikro-mk3.ncmm3

# macOS default location
cargo run --release 2 --profile "$HOME/Documents/Native Instruments/Controller Editor/Profiles/my-profile.ncmm3"

What profiles do:

  • Map physical pad positions to MIDI note numbers
  • Support 8 pad pages (A-H) per profile
  • Enable correct LED feedback for custom layouts
  • Allow seamless integration with NI Controller Editor

Creating profiles:

  1. Open Native Instruments Controller Editor
  2. Select “Maschine Mikro MK3”
  3. Edit pad pages (A-H)
  4. Assign MIDI notes to each pad
  5. Save as .ncmm3 file
  6. Use with --profile flag

See Device Profiles Documentation for complete guide.

–pad-page

Force a specific pad page when using a profile (instead of auto-detection).

Syntax:

cargo run --release 2 --profile <PATH> --pad-page <PAGE>

Valid pages: A, B, C, D, E, F, G, H (case-insensitive)

Examples:

# Force page A
cargo run --release 2 --profile my-profile.ncmm3 --pad-page A

# Force page H
cargo run --release 2 --profile my-profile.ncmm3 --pad-page h

When to use:

  • Auto-detection not working correctly
  • Want to lock to a specific page
  • Testing specific page mappings
  • Profile has identical notes across multiple pages

Default behavior (without --pad-page):

  • Auto-detect active page from incoming MIDI notes
  • Switch pages automatically when notes from different page detected

–config, -c

Specify custom configuration file location.

Syntax:

cargo run --release 2 --config <PATH>
# or
cargo run --release 2 -c <PATH>

Examples:

# Use alternative config
cargo run --release 2 --config config-dev.toml

# Full path
cargo run --release 2 --config /etc/conductor/config.toml

Default: ./config.toml (current directory)

–help, -h

Display help message with all available options.

Syntax:

cargo run --release -- --help
# or
./target/release/conductor --help

Note the -- separator when using cargo run.

–version, -v

Display Conductor version.

Syntax:

cargo run --release -- --version
# or
./target/release/conductor --version

Environment Variables

DEBUG=1

Enable verbose debug logging.

Syntax:

DEBUG=1 cargo run --release 2

Output includes:

  • MIDI event details (note on/off, velocity, channel)
  • HID connection status
  • LED updates (buffer contents)
  • Event processing (velocity detection, long press, chords)
  • Mapping matches and action execution
  • Mode changes
  • Error details

Example debug output:

[DEBUG] Connected to MIDI port 2: Maschine Mikro MK3 - Input
[DEBUG] HID device opened successfully
[DEBUG] LED controller initialized
[DEBUG] Loaded config with 3 modes, 24 mappings
[DEBUG] Starting in mode 0: Default

[MIDI] NoteOn ch:0 note:12 vel:87
[DEBUG] Processed: Note(12) with velocity Medium
[DEBUG] Matched mapping: "Copy text" (mode: Default)
[DEBUG] Executing action: Keystroke(keys: "c", modifiers: ["cmd"])
[DEBUG] LED update: pad 0 -> color 7 (Green) brightness 2
[MIDI] NoteOff ch:0 note:12 vel:0
[DEBUG] LED fade: pad 0 cleared after 1000ms

When to use:

  • Troubleshooting mapping issues
  • Debugging note number mismatches
  • Verifying LED control
  • Understanding event processing
  • Investigating performance issues

RUST_LOG

Control Rust logging levels (for development).

Syntax:

RUST_LOG=debug cargo run --release 2
RUST_LOG=trace cargo run --release 2
RUST_LOG=info cargo run --release 2

Levels:

  • error - Only errors
  • warn - Warnings and errors
  • info - General information (default)
  • debug - Debug information
  • trace - Very verbose

Filter by module:

# Only log MIDI events
RUST_LOG=conductor::midi=debug cargo run --release 2

# Multiple modules
RUST_LOG=conductor::event_processor=debug,conductor::mappings=trace cargo run --release 2

Complete Usage Examples

Example 1: Basic Usage

# List ports
cargo run --release

# Connect to port 2 with default settings
cargo run --release 2

Example 2: With LED Lighting

# Reactive mode (default)
cargo run --release 2 --led reactive

# Rainbow gradient
cargo run --release 2 --led rainbow

# Sparkle effect
cargo run --release 2 --led sparkle

Example 3: With Profile

# Auto-detect page
cargo run --release 2 --profile my-profile.ncmm3

# Force specific page
cargo run --release 2 --profile my-profile.ncmm3 --pad-page H

# With LED scheme
cargo run --release 2 --profile my-profile.ncmm3 --led reactive

Example 4: Debug Mode

# Enable debug output
DEBUG=1 cargo run --release 2

# With all options
DEBUG=1 cargo run --release 2 --profile my-profile.ncmm3 --led reactive

Example 5: Custom Config

# Use development config
cargo run --release 2 --config config-dev.toml

# Use config from different directory
cargo run --release 2 --config ~/conductor-configs/work.toml

Example 6: Production Binary

# Build release
cargo build --release

# Run with all options
./target/release/conductor 2 \
    --profile ~/Documents/NI/my-profile.ncmm3 \
    --led reactive \
    --config ~/conductor-configs/production.toml

Daemon Control: conductorctl

New in v1.0.0 - Control and monitor the Conductor daemon service.

Basic Syntax

# Via cargo
cargo run --release --bin conductorctl <COMMAND> [OPTIONS]

# Release binary
./target/release/conductorctl <COMMAND> [OPTIONS]

Commands

status

Display daemon status, device info, and performance metrics.

Syntax:

conductorctl status [--json]

Output (human-readable):

Conductor Daemon Status
=====================

Lifecycle State: Running
Uptime: 2h 34m 17s

Device
------
Connected: Yes
Name: Maschine Mikro MK3 - Input
Port: 2
Last Event: 3s ago

Configuration
------------
Modes: 3 (Default, Development, Media)
Global Mappings: 12
Mode Mappings: 24 (8 per mode)
Config File: /Users/you/.config/conductor/config.toml
Last Reload: 15m ago

Performance Metrics
------------------
Config Reloads: 7
Average Reload Time: 3ms
Last Reload Time: 2ms
Performance Grade: Excellent

IPC Latency: <1ms

JSON Output (--json):

conductorctl status --json
{
  "success": true,
  "data": {
    "lifecycle_state": "Running",
    "uptime_secs": 9257,
    "device": {
      "connected": true,
      "name": "Maschine Mikro MK3 - Input",
      "port": 2,
      "last_event_at": 1699900000
    },
    "config": {
      "modes": 3,
      "global_mappings": 12,
      "mode_mappings": 24
    },
    "performance": {
      "reload_count": 7,
      "avg_reload_ms": 3,
      "last_reload_ms": 2,
      "grade": "Excellent"
    }
  }
}

reload

Trigger configuration hot-reload without restarting the daemon.

Syntax:

conductorctl reload [--json]

Features:

  • Zero Downtime: No interruption to MIDI processing
  • Fast: 0-8ms reload latency (typically <3ms)
  • Atomic: All-or-nothing config swap
  • Validated: Config checked before reload

Output:

✓ Configuration reloaded successfully

Reload completed in 2ms
Modes: 3 (Default, Development, Media)
Global mappings: 12
Mode mappings: 24

When to use:

  • After editing config.toml
  • Testing new mappings
  • Switching between config profiles
  • Live development workflow

Example workflow:

# 1. Edit config
vim ~/.config/conductor/config.toml

# 2. Reload daemon
conductorctl reload

# 3. Test changes immediately (no restart needed!)

validate

Validate configuration file syntax without reloading.

Syntax:

conductorctl validate [--json]

Output (valid config):

✓ Configuration is valid

Modes: 3
Global mappings: 12
Total mappings: 36

Output (invalid config):

✗ Configuration validation failed

Error: Invalid trigger type 'NoteTap' at line 42
Expected one of: Note, VelocityRange, LongPress, DoubleTap,
                 NoteChord, EncoderTurn, Aftertouch, PitchBend, CC

Suggestion: Did you mean 'DoubleTap'?

When to use:

  • Before committing config changes
  • CI/CD validation
  • Debugging config syntax errors
  • Pre-flight checks

ping

Health check with latency measurement.

Syntax:

conductorctl ping [--json]

Output:

✓ Daemon is responsive
Latency: 0.4ms

When to use:

  • Verify daemon is running
  • Check IPC communication
  • Monitor system responsiveness
  • Health checks in scripts

shutdown

Gracefully shut down the running daemon process via IPC.

Syntax:

conductorctl shutdown [--json]

Output:

✓ Daemon stopped successfully

Uptime: 2h 34m 17s
State saved successfully

What it does:

  • Sends IPC Stop command to running daemon
  • Daemon saves state and exits gracefully
  • Closes MIDI/HID connections cleanly
  • Persists device state to disk

When to use:

  • Daemon is running and you want to stop it
  • Quick shutdown during development
  • When you know daemon is responsive

Requirements:

  • Daemon must be running
  • IPC socket must be accessible
  • No LaunchAgent/systemd interaction

stop

Stop the LaunchAgent/systemd service (tries graceful shutdown first).

Syntax:

conductorctl stop [--json] [--force]

Output:

Stopping Conductor service...
  Attempting graceful shutdown via IPC...
✓ Service stopped successfully

What it does:

  1. First: Attempts graceful IPC shutdown (same as shutdown command)
  2. Waits: 500ms for daemon to exit
  3. Then: Unloads LaunchAgent (macOS) or stops systemd service (Linux)

When to use:

  • Service is installed via conductorctl install
  • Need to stop the background service
  • Daemon may or may not be responsive
  • Want to ensure service is fully stopped

Requirements:

  • Service must be installed (via conductorctl install)
  • macOS: LaunchAgent plist exists
  • Linux: systemd unit exists

Options:

  • --force: Skip graceful shutdown, immediately unload service

Choosing Between shutdown and stop

ScenarioUse CommandWhy
Daemon running in foregroundshutdownFaster, direct IPC
Daemon started manually (not service)shutdownNo service to unload
Service installed and runningstopEnsures service unloaded
Daemon not respondingstop --forceBypasses IPC, forces unload
Development workflowshutdownQuick restarts
Production/installed servicestopProper service management

Decision tree:

Is daemon installed as a service?
├─ No → Use `shutdown`
└─ Yes
   ├─ Is daemon responsive?
   │  ├─ Yes → Use `stop` (graceful)
   │  └─ No → Use `stop --force`
   └─ Running in foreground for testing? → Use `shutdown`

Device Management Commands

list-devices

List all available MIDI input devices.

Syntax:

conductorctl list-devices [--json]

Output:

Available MIDI Devices
──────────────────────────────────────────────────
  [0] USB MIDI Device
  [1] IAC Driver Bus 1 (connected)
  [2] Maschine Mikro MK3 - Input

When to use:

  • Find available MIDI ports
  • Check which device is currently connected
  • Troubleshoot device connectivity

set-device

Switch the daemon to a different MIDI device without restart.

Syntax:

conductorctl set-device <PORT> [--json]

Example:

# Switch to port 2
conductorctl set-device 2

Output:

✓ Switched to device at port 2

When to use:

  • Switch between MIDI devices on the fly
  • Test different controllers
  • Recover from device disconnection

get-device

Show information about the currently connected MIDI device.

Syntax:

conductorctl get-device [--json]

Output:

Current MIDI Device
──────────────────────────────────────────────────
Status:     Connected
Name:       Maschine Mikro MK3 - Input
Port:       2
Last Event: 3s ago

When to use:

  • Verify which device is active
  • Check connection status
  • Debug event reception issues

Service Management Commands

Note: Service management commands are currently macOS-only (using LaunchAgent).

Understanding LaunchAgent Behavior

The Conductor LaunchAgent plist has RunAtLoad=true, which affects command behavior:

Key insight: launchctl load = load plist + start daemon immediately

Commandlaunchctl operationStarts daemon?Auto-start on login?
installload✓ Yes✗ No
startload✓ Yes✗ No
enableload -w✓ Yes✓ Yes
stopunload✗ Stops✗ No
disableunload -w✗ Stops✗ No

Common patterns:

  • Quick setup: install → daemon runs but won’t auto-start on reboot
  • Production setup: install then enable → daemon runs now AND on every login
  • One-step production: enable (if already installed) → starts + enables auto-start
  • Temporary disable: stop → stops now but will auto-start on next login (if enabled)
  • Complete disable: disable → stops now AND prevents auto-start

install

Install Conductor as a LaunchAgent service that starts automatically on login.

Syntax:

conductorctl install [--install-binary] [--force] [--json]

Options:

  • --install-binary: Copy daemon binary to /usr/local/bin/conductor
  • --force: Reinstall even if already installed

What it does:

  1. Generates LaunchAgent plist from template
  2. Copies plist to ~/Library/LaunchAgents/com.amiable.conductor.plist
  3. Optionally installs binary to /usr/local/bin (requires sudo)
  4. Loads service with launchctl

Output:

Installing Conductor service...
  ✓ Generated plist
  ✓ Installed to ~/Library/LaunchAgents/com.amiable.conductor.plist
  ✓ Loaded service

✓ Conductor service installed successfully

Next steps:
  • Start: conductorctl start
  • Enable auto-start: conductorctl enable
  • Check status: conductorctl service-status

When to use:

  • First-time setup for background service
  • Setting up production deployment
  • Enable auto-start on login

Example:

# Basic install (daemon must already be built)
conductorctl install

# Install and copy binary to system location
sudo conductorctl install --install-binary

# Force reinstall
conductorctl install --force

uninstall

Remove Conductor service from LaunchAgent.

Syntax:

conductorctl uninstall [--remove-binary] [--remove-logs] [--json]

Options:

  • --remove-binary: Also delete /usr/local/bin/conductor
  • --remove-logs: Delete log files

What it does:

  1. Stops service if running
  2. Removes LaunchAgent plist
  3. Optionally removes binary and logs

Output:

Uninstalling Conductor service...
  ✓ Stopped service
  ✓ Removed plist: ~/Library/LaunchAgents/com.amiable.conductor.plist

✓ Conductor service uninstalled successfully

When to use:

  • Removing Conductor completely
  • Clean uninstall before upgrade
  • Troubleshooting installation issues

start

Start the LaunchAgent service.

Syntax:

conductorctl start [--wait <SECONDS>] [--json]

Options:

  • --wait <SECONDS>: Wait up to N seconds for daemon to be ready (default: 5)

What it does:

  1. Loads service with launchctl load
  2. Waits for daemon to respond to IPC
  3. Verifies daemon is running

Output:

Starting Conductor service...
Waiting for daemon to be ready... ✓
✓ Service started successfully

When to use:

  • Start service after install
  • Start service after stop
  • Verify service starts correctly

Example:

# Start and wait up to 10 seconds
conductorctl start --wait 10

restart

Restart the LaunchAgent service (stop + start).

Syntax:

conductorctl restart [--wait <SECONDS>] [--json]

What it does:

  1. Gracefully stops service
  2. Waits 500ms
  3. Starts service
  4. Waits for daemon to be ready

Output:

Restarting Conductor service...
Stopping Conductor service...
✓ Service stopped successfully
Starting Conductor service...
✓ Service started successfully

When to use:

  • Apply config changes that need full restart
  • Recover from errors
  • Test service lifecycle

enable

Enable auto-start on login AND start the daemon immediately.

Syntax:

conductorctl enable [--json]

What it does:

  1. Loads service with launchctl load -w flag
  2. Starts daemon immediately (because plist has RunAtLoad=true)
  3. Enables auto-start on next login (persists across reboots)

Output:

✓ Service enabled (will start on login)

Note: This is equivalent to start + making it persistent across reboots.

When to use:

  • One-step setup: Enable and start in single command
  • Production deployment (start now + auto-start on reboot)
  • After disable to re-enable everything

Alternative: If you only want to enable auto-start WITHOUT starting now, use start to load the service first, then the -w flag will be set.

disable

Disable auto-start on login AND stop the daemon immediately.

Syntax:

conductorctl disable [--json]

What it does:

  1. Unloads service with launchctl unload -w flag
  2. Stops daemon immediately
  3. Disables auto-start on next login

Output:

✓ Service disabled (will not start on login)

Note: This is equivalent to stop + preventing auto-start on reboot.

When to use:

  • Complete shutdown: Stop now + prevent auto-start
  • Temporarily disable background service
  • Development workflow
  • Before uninstall

service-status

Show detailed service installation and runtime status.

Syntax:

conductorctl service-status [--json]

Output:

Conductor Service Status
──────────────────────────────────────────────────
Status:          Installed and Loaded
Service Label:   com.amiable.conductor
Plist:           ~/Library/LaunchAgents/com.amiable.conductor.plist ✓
Binary:          /usr/local/bin/conductor ✓

Service is loaded (enabled)

When to use:

  • Verify service is installed correctly
  • Check if auto-start is enabled
  • Troubleshoot service issues
  • Audit service configuration

Global Options

–json

Output in JSON format (for scripting/automation).

Available for: All commands

Example:

# Parse with jq
conductorctl status --json | jq '.data.device.connected'
# Output: true

# Check if reload succeeded
if conductorctl reload --json | jq -e '.success'; then
    echo "Reload successful"
fi

Usage Examples

Example 1: First-Time Service Setup (Production)

# Build daemon
cargo build --release --bin conductor

# Install as LaunchAgent service (starts immediately)
conductorctl install

# Verify running
conductorctl status

# Enable auto-start on login (also starts if not running)
# Since install already started it, this just enables persistence
conductorctl enable

# Check service details
conductorctl service-status

Alternative (simpler):

# Build daemon
cargo build --release --bin conductor

# One-step: Install service
conductorctl install

# One-step: Enable auto-start (if you want persistence across reboots)
conductorctl enable

Simplest production setup:

cargo build --release --bin conductor
conductorctl install --install-binary  # Copies to /usr/local/bin
conductorctl enable                     # Starts + enables auto-start

Example 2: Development Workflow

# Start daemon in foreground for testing
cargo run --release --bin conductor 2 --foreground

# In another terminal...

# Check status
conductorctl status

# Edit config
vim config.toml

# Hot-reload changes (zero downtime!)
conductorctl reload

# Test changes immediately

# Switch to different MIDI device
conductorctl list-devices
conductorctl set-device 1

# Stop when done
conductorctl shutdown

Example 3: Service Management Workflow

# Check if service is installed
conductorctl service-status

# Start service if not running
conductorctl start

# Check which MIDI device is active
conductorctl get-device

# Switch to different device without restart
conductorctl set-device 2

# Restart service (apply changes that need full restart)
conductorctl restart

# Disable auto-start temporarily
conductorctl disable

# Stop service
conductorctl stop

Example 4: Production Monitoring

#!/bin/bash
# monitor.sh - Health check script

# Check daemon health
if ! conductorctl ping --json | jq -e '.success'; then
    echo "Daemon not responding, restarting..."
    conductorctl restart
fi

# Get performance metrics
RELOAD_MS=$(conductorctl status --json | jq '.data.reload_stats.avg_reload_ms')
if [ "$RELOAD_MS" -gt 50 ]; then
    echo "Warning: Average reload time ${RELOAD_MS}ms (expected <50ms)"
fi

# Check MIDI device connectivity
CONNECTED=$(conductorctl get-device --json | jq '.data.device.connected')
if [ "$CONNECTED" != "true" ]; then
    echo "MIDI device disconnected!"
    # Try to reconnect
    conductorctl list-devices
fi

Example 5: Configuration Management

# Validate before deploy
if ! conductorctl validate --json | jq -e '.success'; then
    echo "Config validation failed"
    exit 1
fi

# Backup current config
cp ~/.config/conductor/config.toml ~/.config/conductor/config.toml.backup

# Deploy new config
cp config-v2.toml ~/.config/conductor/config.toml

# Apply changes (hot reload)
conductorctl reload

# Verify successful reload
conductorctl status

# If issues, rollback
# cp ~/.config/conductor/config.toml.backup ~/.config/conductor/config.toml
# conductorctl reload

Example 6: Automated Testing

#!/bin/bash
# test-config.sh

# Validate syntax
if ! conductorctl validate --json | jq -e '.data.valid'; then
    echo "Config validation failed"
    exit 1
fi

# Reload daemon
if ! conductorctl reload --json | jq -e '.success'; then
    echo "Reload failed"
    exit 1
fi

# Check reload performance
LATENCY=$(conductorctl status --json | jq '.data.reload_stats.last_reload_ms')
if [ "$LATENCY" -gt 10 ]; then
    echo "Warning: Reload took ${LATENCY}ms (expected <10ms)"
fi

# Verify device connection
CONNECTED=$(conductorctl get-device --json | jq '.data.device.connected')
if [ "$CONNECTED" != "true" ]; then
    echo "Error: MIDI device not connected"
    exit 1
fi

echo "✓ All checks passed"

Example 7: Complete Uninstall

# Stop service
conductorctl stop

# Disable auto-start
conductorctl disable

# Uninstall service (with cleanup)
conductorctl uninstall --remove-binary --remove-logs

# Verify removal
conductorctl service-status

Diagnostic Tools

Conductor includes several diagnostic utilities for debugging and configuration.

midi_diagnostic

Visualize all incoming MIDI events in real-time.

Purpose: Debug MIDI connectivity, view raw MIDI data, verify device is sending events.

Syntax:

cargo run --bin midi_diagnostic [PORT]

Example:

# Connect to port 2
cargo run --bin midi_diagnostic 2

Output:

Connected to MIDI port 2: Maschine Mikro MK3 - Input
Listening for MIDI events... (Ctrl+C to exit)

[NoteOn]  ch:0 note:12 vel:87
[NoteOff] ch:0 note:12 vel:0
[NoteOn]  ch:0 note:13 vel:45
[NoteOff] ch:0 note:13 vel:0
[CC]      ch:0 cc:1 value:64
[PitchBend] ch:0 value:8192

Event types shown:

  • NoteOn - Pad/key pressed
  • NoteOff - Pad/key released
  • CC - Control Change (knobs, sliders)
  • PitchBend - Touch strip, pitch wheel
  • Aftertouch - Pressure sensitivity
  • ProgramChange - Program/patch change

When to use:

  • Verify MIDI device is connected
  • Find note numbers for pads/keys
  • Debug why mappings aren’t triggering
  • Check velocity ranges
  • Verify CC numbers for encoders/knobs

Press Ctrl+C to exit.

led_diagnostic

Test LED functionality and HID connection.

Purpose: Verify HID access, test LED control, debug LED issues.

Syntax:

cargo run --bin led_diagnostic

What it does:

  1. Attempts to open HID device
  2. Displays connection status
  3. Tests individual LED control
  4. Cycles through all pads with different colors

Output:

LED Diagnostic Tool
==================

Searching for Maschine Mikro MK3...
✓ Device found: Maschine Mikro MK3 (17cc:1600)
✓ HID device opened successfully

Testing LED control...
- Lighting pad 0 (Red Bright)
- Lighting pad 1 (Green Bright)
- Lighting pad 2 (Blue Bright)
...
- Clearing all pads

✓ LED diagnostic complete

Error output (if HID not accessible):

✗ Failed to open HID device
  Possible causes:
  - Device not connected
  - Input Monitoring permission not granted
  - Native Instruments drivers not installed

  Solutions:
  1. Check USB connection
  2. Grant Input Monitoring permission (System Settings → Privacy & Security)
  3. Install NI drivers via Native Access

When to use:

  • LEDs not working in main application
  • Verify HID access before running conductor
  • Test after granting permissions
  • Debug LED coordinate mapping issues

led_tester

Interactive LED testing tool.

Purpose: Manual control of individual LEDs for testing and debugging.

Syntax:

cargo run --bin led_tester

Interactive mode:

LED Tester - Interactive Mode
==============================

Commands:
  on <pad> <color> <brightness>  - Turn on LED
  off <pad>                      - Turn off LED
  all <color> <brightness>       - Set all LEDs
  clear                          - Clear all LEDs
  rainbow                        - Show rainbow pattern
  test                           - Cycle through all pads
  quit                           - Exit

> on 0 red 3
✓ Pad 0: Red Bright

> all blue 2
✓ All pads: Blue Normal

> rainbow
✓ Rainbow pattern displayed

> quit

Colors: red, orange, yellow, green, blue, purple, magenta, white

Brightness: 0 (off), 1 (dim), 2 (normal), 3 (bright)

When to use:

  • Test specific pad LEDs
  • Verify coordinate mapping
  • Experiment with colors and brightness
  • Debug LED patterns

pad_mapper

Find MIDI note numbers for physical pads.

Purpose: Discover note numbers to use in config.toml.

Syntax:

cargo run --bin pad_mapper [PORT]

Example:

cargo run --bin pad_mapper 2

Usage:

  1. Run the tool
  2. Press each pad on your controller
  3. Write down the note number displayed
  4. Use those numbers in your config

Output:

Pad Mapper - Press pads to see note numbers
============================================
Connected to port 2: Maschine Mikro MK3 - Input

Press pads... (Ctrl+C to exit)

Pad pressed: Note 12 (velocity: 87)
Pad pressed: Note 13 (velocity: 64)
Pad pressed: Note 14 (velocity: 103)
Pad pressed: Note 15 (velocity: 52)

Tips:

  • Press pads in order (bottom-left to top-right)
  • Draw a grid on paper and write down note numbers
  • Use this data to update config.toml

When to use:

  • Setting up a new device
  • Creating initial config
  • Mapping custom devices
  • Verifying profile note numbers

test_midi

Test MIDI port connectivity and enumerate devices.

Purpose: Quick MIDI connectivity test, list all available ports.

Syntax:

cargo run --bin test_midi

Output:

MIDI Port Test
==============

Available input ports:
0: USB MIDI Device
1: IAC Driver Bus 1
2: Maschine Mikro MK3 - Input
3: Digital Keyboard

Available output ports:
0: USB MIDI Device
1: IAC Driver Bus 1
2: Maschine Mikro MK3 - Output

Testing port 2 (input)...
✓ Successfully opened port: Maschine Mikro MK3 - Input

Press a pad to test... (5 second timeout)
✓ Received MIDI event: NoteOn ch:0 note:12 vel:87

Connection test: PASSED

When to use:

  • Verify MIDI device detected
  • Check port numbers before running conductor
  • Debug connectivity issues
  • Confirm MIDI cable is working

Command Quick Reference

CommandPurposeExample
Daemon Service (v1.0.0+)
conductor [PORT]Start daemon servicecargo run --release --bin conductor 2
--led <SCHEME>Set LED schemeconductor 2 --led rainbow
--profile <PATH>Load profileconductor 2 --profile my.ncmm3
--pad-page <PAGE>Force pad pageconductor 2 --pad-page H
--config <PATH>Custom configconductor 2 --config dev.toml
Daemon Control (v1.0.0+)
conductorctl statusShow daemon statusconductorctl status
conductorctl reloadHot-reload configconductorctl reload
conductorctl validateValidate configconductorctl validate
conductorctl pingHealth checkconductorctl ping
conductorctl shutdownStop daemon (IPC)conductorctl shutdown
conductorctl stopStop service (LaunchAgent)conductorctl stop --force
Device Management
conductorctl list-devicesList MIDI devicesconductorctl list-devices
conductorctl set-deviceSwitch MIDI deviceconductorctl set-device 2
conductorctl get-deviceShow current deviceconductorctl get-device
Service Management (macOS)
conductorctl installInstall LaunchAgentconductorctl install --install-binary
conductorctl uninstallRemove serviceconductorctl uninstall --remove-logs
conductorctl startStart serviceconductorctl start --wait 10
conductorctl restartRestart serviceconductorctl restart
conductorctl enableEnable auto-startconductorctl enable
conductorctl disableDisable auto-startconductorctl disable
conductorctl service-statusService statusconductorctl service-status
Global Options
--jsonJSON outputconductorctl status --json
Diagnostic Tools
DEBUG=1Enable debug logDEBUG=1 conductor 2
midi_diagnosticView MIDI eventscargo run --bin midi_diagnostic 2
led_diagnosticTest LEDscargo run --bin led_diagnostic
led_testerInteractive LED testcargo run --bin led_tester
pad_mapperFind note numberscargo run --bin pad_mapper 2
test_midiTest MIDI portscargo run --bin test_midi

Common Workflows

First-Time Setup

# 1. List available ports
cargo run --release

# 2. Test connectivity
cargo run --bin test_midi

# 3. Map pads to note numbers
cargo run --bin pad_mapper 2

# 4. Run with basic settings
cargo run --release 2

# 5. Test LEDs
cargo run --release 2 --led rainbow

Troubleshooting

# 1. Check MIDI events are received
cargo run --bin midi_diagnostic 2

# 2. Enable debug logging
DEBUG=1 cargo run --release 2

# 3. Test LED control
cargo run --bin led_diagnostic

# 4. Verify port numbers
cargo run --bin test_midi

Production Use

# Build optimized binary
cargo build --release

# Run with all options
./target/release/conductor 2 \
    --profile ~/profiles/work.ncmm3 \
    --led reactive \
    --config ~/configs/production.toml

# Or use shell script
#!/bin/bash
DEBUG=0 ./target/release/conductor 2 \
    --profile "$HOME/Documents/NI/my-profile.ncmm3" \
    --led reactive \
    > /tmp/conductor.log 2>&1 &

See Also


Last Updated: November 14, 2025 Binary Version: 1.0.0

Configuration Schema Reference

Complete reference for Conductor’s TOML configuration file format. Covers both MIDI and Game Controllers (HID) with detailed field descriptions, examples, and validation rules.

Table of Contents

File Structure

A Conductor configuration file consists of these top-level sections:

[device]                    # Device configuration
[logging]                   # Logging settings (optional)
[advanced_settings]         # Timing thresholds and behavior
[[modes]]                   # Mode definitions (array)
[[global_mappings]]         # Global mappings (array, optional)

Device Configuration

The [device] section defines basic device settings.

Fields

[device]
name = "My Controller"           # String: Device name (for display)
auto_connect = true              # Boolean: Auto-connect on startup (default: true)

Field Reference

FieldTypeRequiredDefaultDescription
nameStringYes-Human-readable device name
auto_connectBooleanNotrueAutomatically connect to device on startup

Examples

MIDI Controller:

[device]
name = "Maschine Mikro MK3"
auto_connect = true

Gamepad:

[device]
name = "Xbox Controller"
auto_connect = true

Hybrid Setup (MIDI + Gamepad):

[device]
name = "Studio Setup"
auto_connect = true
# Both MIDI and gamepad can coexist - no conflicts

Input Mode Selection

Note: The input_mode field is not yet implemented in the configuration file format. The daemon currently determines input mode based on available devices at runtime:

  • MIDI Only: When only MIDI devices are connected
  • Gamepad Only: When only gamepads are connected
  • Both: When both MIDI and gamepad devices are available

Future Configuration (Planned)

In a future release, you’ll be able to explicitly set the input mode in the [device] section:

[device]
name = "My Setup"
input_mode = "Both"  # Options: "MidiOnly", "GamepadOnly", "Both"

Current Behavior

The system automatically handles:

  • MIDI-only configs: Mappings use ID range 0-127
  • Gamepad-only configs: Mappings use ID range 128-255
  • Hybrid configs: Mappings can use both ranges simultaneously

No configuration changes are needed to support multiple input types - simply define mappings using the appropriate ID ranges.

Modes

Modes allow you to define different sets of mappings for different contexts. Switch between modes using encoder rotation or button combinations.

Mode Definition

[[modes]]
name = "Default"                # String: Mode name (unique)
color = "blue"                  # String: Color for LED feedback (MIDI controllers)
# mappings array follows...

Field Reference

FieldTypeRequiredDescription
nameStringYesUnique mode identifier
colorStringNoLED color: “blue”, “green”, “purple”, “red”, “yellow”, “cyan”, “white”, “off”

Examples

[[modes]]
name = "Desktop"
color = "blue"

[[modes]]
name = "Media"
color = "purple"

[[modes]]
name = "Development"
color = "green"

Mappings

Mappings connect triggers (input events) to actions (system commands). Two types:

  • Mode mappings: Active only in their parent mode
  • Global mappings: Active across all modes

Mapping Structure

[[modes.mappings]]               # Mode-specific mapping
description = "Action description"
[modes.mappings.trigger]
type = "Note"                    # Trigger type
note = 60                        # Trigger parameters
[modes.mappings.action]
type = "Keystroke"               # Action type
keys = "Space"                   # Action parameters

[[global_mappings]]              # Global mapping
description = "Global action"
[global_mappings.trigger]
# ... trigger definition
[global_mappings.action]
# ... action definition

Field Reference

FieldTypeRequiredDescription
descriptionStringNoHuman-readable description of the mapping
triggerTableYesTrigger definition (see Trigger Types)
actionTableYesAction definition (see Action Types)

Trigger Types

Triggers define when an action should execute. Conductor supports MIDI triggers (ID range 0-127) and Gamepad triggers (ID range 128-255).

MIDI Triggers

MIDI triggers use the standard MIDI ID range (0-127) for notes, control changes, and other MIDI messages.

Note

Basic MIDI note on/off detection.

[trigger]
type = "Note"
note = 60                        # Integer: MIDI note number (0-127)
velocity_min = 1                 # Integer (optional): Minimum velocity (default 1)

Use Cases: Pad presses, piano keys, basic button mapping

VelocityRange

Different actions based on press intensity.

[trigger]
type = "VelocityRange"
note = 60                        # Integer: MIDI note number
min_velocity = 80                # Integer: Minimum velocity (0-127)
max_velocity = 127               # Integer: Maximum velocity (0-127)

Velocity Levels:

  • Soft: 0-40
  • Medium: 41-80
  • Hard: 81-127

Use Cases: Soft press for play, hard press for stop; velocity-sensitive shortcuts

LongPress

Detect when a pad is held for a duration.

[trigger]
type = "LongPress"
note = 60                        # Integer: MIDI note number
min_duration_ms = 1000           # Integer: Minimum hold duration (default 2000)

Use Cases: Hold 2s to quit app (prevent accidental quits); confirmation for destructive actions

DoubleTap

Detect quick double presses.

[trigger]
type = "DoubleTap"
note = 60                        # Integer: MIDI note number
max_interval_ms = 300            # Integer (optional): Max time between taps (default 300)

Use Cases: Double-tap to toggle fullscreen; gesture-based shortcuts

NoteChord

Multiple notes pressed simultaneously.

[trigger]
type = "NoteChord"
notes = [60, 64, 67]            # Array: List of MIDI note numbers
max_interval_ms = 100            # Integer (optional): Max time between notes (default 100)

Use Cases: Emergency exit (press 3 corners); complex shortcuts requiring multiple pads

CC (Control Change)

Continuous controller messages.

[trigger]
type = "CC"
cc = 1                          # Integer: Control change number (0-127)
value_min = 64                  # Integer (optional): Minimum value to trigger

Use Cases: Button presses sending CC messages; threshold-based triggers

EncoderTurn

Encoder rotation with direction detection.

[trigger]
type = "EncoderTurn"
cc = 2                          # Integer: Control change number
direction = "Clockwise"         # String: "Clockwise" or "CounterClockwise"

Use Cases: Volume control with encoder; mode switching; parameter adjustment

Aftertouch

Channel pressure sensitivity (pressure after initial press).

[trigger]
type = "Aftertouch"
note = 1                        # Integer (optional): Specific pad for polyphonic (omit for channel)
min_pressure = 64               # Integer (optional): Minimum pressure (0-127)
max_pressure = 127              # Integer (optional): Maximum pressure (0-127)

Aftertouch Types:

  • Polyphonic (0xA0): Per-pad pressure
  • Channel (0xD0): Global pressure for entire device

Use Cases: Apply pressure to modulate effects; pressure-sensitive volume control

PitchBend

Touch strip or pitch wheel control (14-bit precision).

[trigger]
type = "PitchBend"
min_value = 8192                # Integer: Minimum bend value (0-16383)
max_value = 16383               # Integer: Maximum bend value (0-16383)
center_deadzone = 100           # Integer (optional): Deadzone around center (8192)

Value Range:

  • Full Range: 0-16383 (14-bit resolution)
  • Center: 8192
  • Down: 0-8191
  • Up: 8193-16383

Use Cases: Touch strip for volume control; timeline scrubbing; multi-zone selection

Gamepad Triggers

Gamepad triggers use the extended ID range (128-255) for buttons, analog sticks, and triggers. All SDL2-compatible controllers are supported (Xbox, PlayStation, Nintendo Switch Pro, joysticks, racing wheels, flight sticks, HOTAS, custom controllers).

GamepadButton

Simple button press trigger.

[trigger]
type = "GamepadButton"
button = 128                    # Integer: Button ID (128-255)
velocity_min = 1                # Integer (optional): Minimum pressure (default 1)

Button ID Reference:

IDXboxPlayStationSwitchDescription
128ACross (✕)BSouth button
129BCircle (○)AEast button
130XSquare (□)YWest button
131YTriangle (△)XNorth button
132---D-Pad Up
133---D-Pad Down
134---D-Pad Left
135---D-Pad Right
136LBL1LLeft shoulder
137RBR1RRight shoulder
138L3L3L-ClickLeft stick click
139R3R3R-ClickRight stick click
140MenuOptions+Start button
141ViewShare-Select/Back button
142XboxPSHomeGuide/Home button
143LTL2ZLLeft trigger (digital)
144RTR2ZRRight trigger (digital)

Use Cases: Face button for confirm/cancel; D-Pad for arrow keys; shoulder buttons for tab switching

Examples:

# Xbox A button (PlayStation Cross, Switch B)
[[modes.mappings]]
description = "Confirm action"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "Return"

# D-Pad Up
[[modes.mappings]]
description = "Navigate up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

# Left shoulder button
[[modes.mappings]]
description = "Previous tab"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136
[modes.mappings.action]
type = "Keystroke"
keys = "Tab"
modifiers = ["cmd", "shift"]

GamepadButtonChord

Multiple buttons pressed simultaneously.

[trigger]
type = "GamepadButtonChord"
buttons = [128, 129]            # Array: List of button IDs (128-255)
timeout_ms = 50                 # Integer (optional): Max time between buttons (default 50)

Use Cases: Multi-button combos for screenshots; safety mechanisms; mode switching

Examples:

# A+B chord for screenshot
[[modes.mappings]]
description = "A+B: Screenshot"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [128, 129]  # A + B
timeout_ms = 50
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

# LB+RB chord for Mission Control
[[modes.mappings]]
description = "LB+RB: Mission Control"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]  # LB + RB
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"
modifiers = ["ctrl"]

# L3+R3 (both stick clicks) for mode change
[[global_mappings]]
description = "Stick clicks: Switch mode"
[global_mappings.trigger]
type = "GamepadButtonChord"
buttons = [138, 139]
[global_mappings.action]
type = "ModeChange"
mode = "Media"

GamepadAnalogStick

Analog stick movement detection with direction.

[trigger]
type = "GamepadAnalogStick"
axis = 130                      # Integer: Axis ID (128-133)
direction = "Clockwise"         # String: "Clockwise" or "CounterClockwise"

Axis ID Reference:

IDAxisDescription
128Left Stick XHorizontal movement (left/right)
129Left Stick YVertical movement (up/down)
130Right Stick XHorizontal movement (left/right)
131Right Stick YVertical movement (up/down)
132Left TriggerAnalog trigger pressure (L2/LT/ZL)
133Right TriggerAnalog trigger pressure (R2/RT/ZR)

Direction Mapping:

  • X-axis: “Clockwise” = moving right, “CounterClockwise” = moving left
  • Y-axis: “Clockwise” = moving up, “CounterClockwise” = moving down

Use Cases: Right stick for browser navigation; left stick for scrolling; camera control

Examples:

# Right stick horizontal: Browser navigation
[[modes.mappings]]
description = "Right stick right: Forward"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130  # Right stick X-axis
direction = "Clockwise"  # Moving right
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]

[[modes.mappings]]
description = "Right stick left: Back"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130  # Right stick X-axis
direction = "CounterClockwise"  # Moving left
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"
modifiers = ["cmd"]

# Right stick vertical: Scrolling
[[modes.mappings]]
description = "Right stick up: Scroll up"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 131  # Right stick Y-axis
direction = "Clockwise"  # Moving up
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

[[modes.mappings]]
description = "Right stick down: Scroll down"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 131  # Right stick Y-axis
direction = "CounterClockwise"  # Moving down
[modes.mappings.action]
type = "Keystroke"
keys = "DownArrow"

GamepadTrigger

Analog trigger pressure detection with threshold.

[trigger]
type = "GamepadTrigger"
trigger = 132                   # Integer: Trigger axis ID (132 or 133)
threshold = 64                  # Integer (optional): Pressure threshold (0-255, default 128)

Trigger IDs:

  • 132: Left trigger (L2/LT/ZL)
  • 133: Right trigger (R2/RT/ZR)

Threshold Values:

  • 0: Triggers immediately on any pressure
  • 64: Triggers at 25% pressure (light press)
  • 128: Triggers at 50% pressure (half-press, default)
  • 192: Triggers at 75% pressure (hard press)
  • 255: Triggers only at full press

Use Cases: Gradual volume control; variable speed actions; pressure-sensitive shortcuts

Examples:

# Light trigger press for volume control
[[modes.mappings]]
description = "Right trigger: Volume up"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Right trigger
threshold = 64  # 25% pressure
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

[[modes.mappings]]
description = "Left trigger: Volume down"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132  # Left trigger
threshold = 64
[modes.mappings.action]
type = "VolumeControl"
operation = "Down"

# Full trigger press for different action
[[modes.mappings]]
description = "Right trigger full press: Next track"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 240  # ~94% pressure
[modes.mappings.action]
type = "Keystroke"
keys = "Next"

Device-Specific Controller Types

While the trigger types above work with all SDL2-compatible controllers, here are specific device type examples:

Joysticks (Flight Sticks)

# Joystick trigger button
[[modes.mappings]]
description = "Trigger button: Fire"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Primary trigger
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

# Hat switch (often mapped to D-Pad buttons 132-135)
[[modes.mappings]]
description = "Hat up: Look up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

# Joystick pitch axis
[[modes.mappings]]
description = "Pitch forward"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 129  # Y-axis
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "w"

Racing Wheels

# Wheel rotation (left/right)
[[modes.mappings]]
description = "Steer left"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 128  # Wheel X-axis
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"

# Throttle pedal
[[modes.mappings]]
description = "Accelerate"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Throttle
threshold = 32  # Light pressure
[modes.mappings.action]
type = "Keystroke"
keys = "w"

# Brake pedal
[[modes.mappings]]
description = "Brake"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132  # Brake
threshold = 32
[modes.mappings.action]
type = "Keystroke"
keys = "s"

HOTAS (Hands On Throttle And Stick)

# Throttle axis
[[modes.mappings]]
description = "Throttle up"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 132  # Throttle axis
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "="

# Multiple hat switches (mapped to available buttons)
[[modes.mappings]]
description = "Hat 1 up: Target up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

# Pinky switch
[[modes.mappings]]
description = "Pinky switch: Modifier"
[modes.mappings.trigger]
type = "GamepadButton"
button = 143
[modes.mappings.action]
type = "ModeChange"
mode = "Combat"

Action Types

Actions define what happens when a trigger fires. For complete action type documentation, see Action Types Reference.

Common action types:

# Keystroke
[action]
type = "Keystroke"
keys = "c"
modifiers = ["cmd"]

# Launch application
[action]
type = "Launch"
app = "Terminal"

# Shell command
[action]
type = "Shell"
command = "echo Hello"

# Volume control
[action]
type = "VolumeControl"
operation = "Up"  # "Up", "Down", "Mute", "Set"

# Mode change
[action]
type = "ModeChange"
mode = "Media"

Advanced Settings

The [advanced_settings] section configures timing thresholds and detection behavior.

Fields

[advanced_settings]
chord_timeout_ms = 50           # Integer: Chord detection window (default 100)
double_tap_timeout_ms = 300     # Integer: Double-tap window (default 300)
hold_threshold_ms = 2000        # Integer: Long press threshold (default 2000)

Field Reference

FieldTypeDefaultDescription
chord_timeout_msInteger100Max time between first and last note/button in chord (ms)
double_tap_timeout_msInteger300Max time between taps for double-tap detection (ms)
hold_threshold_msInteger2000Minimum hold duration for long press detection (ms)

Recommendations

For gamepads (faster input):

[advanced_settings]
chord_timeout_ms = 50           # Shorter window for button chords
double_tap_timeout_ms = 200     # Faster double-tap
hold_threshold_ms = 1000        # Shorter long press (1s)

For MIDI controllers (larger physical pads):

[advanced_settings]
chord_timeout_ms = 100          # Longer window for pad chords
double_tap_timeout_ms = 300     # Standard double-tap
hold_threshold_ms = 2000        # Standard long press (2s)

ID Range Allocation

Conductor uses non-overlapping ID ranges to prevent conflicts between input protocols.

Current Allocation

RangeProtocolTypeExamples
0-127MIDINotes/PadsC0=36, C4=60, G9=127
0-127MIDICC/EncodersMod Wheel=1, Volume=7
128-144Game ControllersButtonsFace buttons, D-Pad, shoulders
128-133Game ControllersAnalog AxesSticks, triggers
145-255ReservedFutureExtended gamepad, keyboard, mouse

Detailed Gamepad ID Mapping

Face Buttons (128-131):

  • 128: South (A/Cross/B)
  • 129: East (B/Circle/A)
  • 130: West (X/Square/Y)
  • 131: North (Y/Triangle/X)

D-Pad (132-135):

  • 132: Up
  • 133: Down
  • 134: Left
  • 135: Right

Shoulders & Sticks (136-139):

  • 136: Left shoulder (LB/L1/L)
  • 137: Right shoulder (RB/R1/R)
  • 138: Left stick click (L3)
  • 139: Right stick click (R3)

Menu Buttons (140-142):

  • 140: Start (Menu/Options/+)
  • 141: Select (View/Share/-)
  • 142: Guide (Xbox/PS/Home)

Trigger Buttons (143-144):

  • 143: Left trigger digital (L2/LT/ZL)
  • 144: Right trigger digital (R2/RT/ZR)

Analog Axes (128-133):

  • 128: Left stick X-axis
  • 129: Left stick Y-axis
  • 130: Right stick X-axis
  • 131: Right stick Y-axis
  • 132: Left trigger analog
  • 133: Right trigger analog

Usage Guidelines

  1. MIDI configs: Use IDs 0-127 only
  2. Gamepad configs: Use IDs 128-255 only
  3. Hybrid configs: Mix both ranges freely - no conflicts!

Complete Examples

Example 1: MIDI-Only Configuration

[device]
name = "Maschine Mikro MK3"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 100
double_tap_timeout_ms = 300
hold_threshold_ms = 2000

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
description = "Pad 1: Play/Pause"
[modes.mappings.trigger]
type = "Note"
note = 36
[modes.mappings.action]
type = "Keystroke"
keys = "PlayPause"

[[modes.mappings]]
description = "Pads 1+2: Screenshot"
[modes.mappings.trigger]
type = "NoteChord"
notes = [36, 37]
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

[[global_mappings]]
description = "Encoder: Volume"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 2
direction = "Clockwise"
[global_mappings.action]
type = "VolumeControl"
operation = "Up"

Example 2: Gamepad-Only Configuration

[device]
name = "Xbox Controller"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50
double_tap_timeout_ms = 200
hold_threshold_ms = 1000

[[modes]]
name = "Desktop"
color = "blue"

[[modes.mappings]]
description = "A button: Confirm"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "Return"

[[modes.mappings]]
description = "B button: Cancel"
[modes.mappings.trigger]
type = "GamepadButton"
button = 129
[modes.mappings.action]
type = "Keystroke"
keys = "Escape"

[[modes.mappings]]
description = "Right stick right: Forward"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]

[[modes.mappings]]
description = "A+B chord: Screenshot"
[modes.mappings.trigger]
type = "GamepadButtonChord"
buttons = [128, 129]
timeout_ms = 50
[modes.mappings.action]
type = "Keystroke"
keys = "3"
modifiers = ["cmd", "shift"]

[[modes]]
name = "Media"
color = "purple"

[[modes.mappings]]
description = "A button: Play/Pause"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "PlayPause"

[[modes.mappings]]
description = "Right trigger: Volume up"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 64
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

[[global_mappings]]
description = "LB+RB: Switch mode"
[global_mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]
[global_mappings.action]
type = "ModeChange"
mode = "Media"

Example 3: Hybrid Configuration (MIDI + Gamepad)

Use both MIDI controller and gamepad simultaneously. No conflicts - they use different ID ranges.

[device]
name = "Studio Hybrid Setup"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 75
double_tap_timeout_ms = 300
hold_threshold_ms = 1500

[[modes]]
name = "Production"
color = "green"

# MIDI mappings (ID range 0-127)
[[modes.mappings]]
description = "MIDI Pad 1: Record"
[modes.mappings.trigger]
type = "Note"
note = 36  # MIDI note
[modes.mappings.action]
type = "Keystroke"
keys = "r"
modifiers = ["cmd"]

[[modes.mappings]]
description = "MIDI Encoder: Volume"
[modes.mappings.trigger]
type = "EncoderTurn"
cc = 2  # MIDI CC
direction = "Clockwise"
[modes.mappings.action]
type = "VolumeControl"
operation = "Up"

# Gamepad mappings (ID range 128-255)
[[modes.mappings]]
description = "Gamepad A: Play/Pause"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Gamepad button
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

[[modes.mappings]]
description = "Gamepad Right Stick: Navigate"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130  # Gamepad axis
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"
modifiers = ["cmd"]

# Hybrid chord: MIDI pad + Gamepad button
[[modes.mappings]]
description = "MIDI Pad 1 + Gamepad A: Save"
[modes.mappings.trigger]
type = "NoteChord"
notes = [36, 128]  # Mix MIDI and gamepad IDs
[modes.mappings.action]
type = "Keystroke"
keys = "s"
modifiers = ["cmd"]

[[global_mappings]]
description = "Gamepad Guide: Spotlight"
[global_mappings.trigger]
type = "GamepadButton"
button = 142
[global_mappings.action]
type = "Keystroke"
keys = "Space"
modifiers = ["cmd"]

Example 4: Joystick/Flight Stick Configuration

[device]
name = "Logitech Flight Stick"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50
double_tap_timeout_ms = 300
hold_threshold_ms = 1000

[[modes]]
name = "Flight"
color = "blue"

[[modes.mappings]]
description = "Trigger: Fire"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128
[modes.mappings.action]
type = "Keystroke"
keys = "Space"

[[modes.mappings]]
description = "Hat up: Look up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 132
[modes.mappings.action]
type = "Keystroke"
keys = "UpArrow"

[[modes.mappings]]
description = "Pitch forward"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 129
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "w"

[[modes.mappings]]
description = "Yaw left"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 128
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "a"

Example 5: Racing Wheel Configuration

[device]
name = "Logitech G29"
auto_connect = true

[advanced_settings]
chord_timeout_ms = 50
double_tap_timeout_ms = 300
hold_threshold_ms = 1000

[[modes]]
name = "Racing"
color = "red"

[[modes.mappings]]
description = "Steer left"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 128  # Wheel rotation
direction = "CounterClockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "LeftArrow"

[[modes.mappings]]
description = "Steer right"
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 128
direction = "Clockwise"
[modes.mappings.action]
type = "Keystroke"
keys = "RightArrow"

[[modes.mappings]]
description = "Throttle"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133
threshold = 32
[modes.mappings.action]
type = "Keystroke"
keys = "w"

[[modes.mappings]]
description = "Brake"
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132
threshold = 32
[modes.mappings.action]
type = "Keystroke"
keys = "s"

[[modes.mappings]]
description = "Shift up"
[modes.mappings.trigger]
type = "GamepadButton"
button = 137  # Right paddle
[modes.mappings.action]
type = "Keystroke"
keys = "e"

[[modes.mappings]]
description = "Shift down"
[modes.mappings.trigger]
type = "GamepadButton"
button = 136  # Left paddle
[modes.mappings.action]
type = "Keystroke"
keys = "q"

Validation Rules

Conductor validates configurations at load time. Common validation errors:

ID Range Violations

# ❌ INVALID: MIDI trigger using gamepad ID
[trigger]
type = "Note"
note = 150  # ERROR: MIDI notes must be 0-127

# ✅ VALID: Use GamepadButton for IDs 128+
[trigger]
type = "GamepadButton"
button = 150

Missing Required Fields

# ❌ INVALID: Missing 'note' field
[trigger]
type = "Note"
# ERROR: 'note' is required

# ✅ VALID: All required fields present
[trigger]
type = "Note"
note = 60

Invalid Values

# ❌ INVALID: Direction typo
[trigger]
type = "EncoderTurn"
cc = 2
direction = "CW"  # ERROR: Must be "Clockwise" or "CounterClockwise"

# ✅ VALID: Correct direction value
[trigger]
type = "EncoderTurn"
cc = 2
direction = "Clockwise"

See Also


Last Updated: 2025-01-21 Version: 3.0 Status: Complete

Environment Variables

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Game Controllers (HID) - Rust API Documentation

This document provides comprehensive API documentation for Conductor v3.0’s Game Controller (HID) support. These types enable integration of gamepads, joysticks, racing wheels, flight sticks, HOTAS setups, and other HID-compliant game controllers.

Overview

Conductor v3.0 introduces a unified input system that supports both MIDI controllers and Game Controllers (HID) simultaneously. The architecture uses protocol-agnostic abstractions to enable hybrid setups where MIDI and gamepad inputs coexist without ID conflicts.

Key Design Principles:

  • Non-overlapping ID ranges: Gamepad buttons use IDs 128-255, MIDI uses 0-127
  • Unified event stream: Both protocols convert to InputEvent for consistent processing
  • Flexible device selection: Support MIDI-only, gamepad-only, or hybrid (both) modes
  • Automatic reconnection: Background monitoring with exponential backoff
  • Thread-safe: Arc/Mutex patterns for concurrent access

Architecture Diagram

┌────────────────────────────────────────────────────────────────┐
│  InputManager (input_manager.rs)                               │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │  InputMode Selection                                     │ │
│  │  - MidiOnly / GamepadOnly / Both                         │ │
│  └──────────────────────────────────────────────────────────┘ │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │  MidiDeviceManager        GamepadDeviceManager           │ │
│  │  - MIDI events (0-127)    - Gamepad events (128-255)     │ │
│  │  - Convert to InputEvent  - Native InputEvent            │ │
│  └──────────────────────────────────────────────────────────┘ │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │  Unified InputEvent Stream                               │ │
│  │  - Single mpsc channel for all inputs                    │ │
│  │  - Processed by EventProcessor                           │ │
│  │  - Dispatched to MappingEngine                           │ │
│  └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

Core Types

InputMode

Location: conductor-daemon/src/input_manager.rs

Enum representing the device selection mode for the unified input system.

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputMode {
    /// Use MIDI device only
    MidiOnly,
    /// Use gamepad device only
    GamepadOnly,
    /// Use both MIDI and gamepad simultaneously
    Both,
}

Variants

VariantDescriptionUse Case
MidiOnlyConnect to MIDI devices onlyTraditional MIDI controller workflows
GamepadOnlyConnect to gamepad devices onlyPure gamepad macro pad setup
BothConnect to both MIDI and gamepadHybrid setups (e.g., MIDI pads + gamepad stick navigation)

Examples

use conductor_daemon::input_manager::{InputManager, InputMode};

// MIDI-only setup (traditional)
let midi_manager = InputManager::new(
    Some("Maschine Mikro MK3".to_string()),
    true,  // auto_reconnect
    InputMode::MidiOnly
);

// Gamepad-only setup
let gamepad_manager = InputManager::new(
    None,
    true,
    InputMode::GamepadOnly
);

// Hybrid setup (both MIDI and gamepad)
let hybrid_manager = InputManager::new(
    Some("Maschine Mikro MK3".to_string()),
    true,
    InputMode::Both
);

GamepadDeviceManager

Location: conductor-daemon/src/gamepad_device.rs

Manages the lifecycle of gamepad/HID device connections with automatic reconnection support and robust error handling.

Fields

pub struct GamepadDeviceManager {
    /// Whether to automatically reconnect on disconnect
    auto_reconnect: bool,

    /// Currently connected gamepad ID (Arc<Mutex<Option<gilrs::GamepadId>>>)
    gamepad_id: Arc<Mutex<Option<gilrs::GamepadId>>>,

    /// Currently connected gamepad name (Arc<Mutex<Option<String>>>)
    gamepad_name: Arc<Mutex<Option<String>>>,

    /// Whether currently connected (Arc<AtomicBool>)
    is_connected: Arc<AtomicBool>,

    /// Flag to signal polling thread to stop (Arc<AtomicBool>)
    stop_polling: Arc<AtomicBool>,

    /// Handle to polling thread (Arc<Mutex<Option<thread::JoinHandle<()>>>>)
    polling_thread: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
}

Constructor

pub fn new(auto_reconnect: bool) -> Self

Creates a new gamepad device manager.

Parameters:

  • auto_reconnect - Whether to automatically reconnect on disconnect

Returns:

  • A new GamepadDeviceManager instance (not yet connected)

Example:

use conductor_daemon::gamepad_device::GamepadDeviceManager;

let manager = GamepadDeviceManager::new(true);
assert!(!manager.is_connected());

Methods

connect()
pub fn connect(
    &mut self,
    event_tx: mpsc::Sender<InputEvent>,
    command_tx: mpsc::Sender<DaemonCommand>,
) -> Result<(gilrs::GamepadId, String), String>

Connects to the first available gamepad and starts the polling loop.

Parameters:

  • event_tx - Channel sender for InputEvent messages
  • command_tx - Channel sender for DaemonCommand messages (reconnection, etc.)

Returns:

  • Ok((GamepadId, Name)) - Tuple of gamepad ID and device name
  • Err(String) - Error message if connection fails

Errors:

  • gilrs initialization fails
  • No gamepads are connected
  • Already connected to a gamepad

Example:

use conductor_daemon::gamepad_device::GamepadDeviceManager;
use tokio::sync::mpsc;

async fn connect_gamepad() -> Result<(), String> {
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    let mut manager = GamepadDeviceManager::new(true);
    let (id, name) = manager.connect(event_tx, command_tx)?;

    println!("Connected to: {} (ID {:?})", name, id);

    // Process events
    while let Some(event) = event_rx.recv().await {
        println!("Received: {:?}", event);
    }

    Ok(())
}
disconnect()
pub fn disconnect(&mut self)

Disconnects from the current gamepad and stops the polling thread.

Example:

manager.disconnect();
assert!(!manager.is_connected());
is_connected()
pub fn is_connected(&self) -> bool

Returns whether the manager is currently connected to a gamepad.

Example:

if manager.is_connected() {
    println!("Gamepad is connected");
}
get_gamepad_name()
pub fn get_gamepad_name(&self) -> Option<String>

Returns the name of the currently connected gamepad, or None if not connected.

Example:

if let Some(name) = manager.get_gamepad_name() {
    println!("Connected to: {}", name);
}
list_gamepads() (static)
pub fn list_gamepads() -> Result<Vec<(gilrs::GamepadId, String, String)>, String>

Lists all connected gamepads. Returns a vector of (GamepadId, Name, UUID) tuples.

Returns:

  • Ok(Vec) - List of connected gamepads
  • Err(String) - Error if gilrs initialization fails

Example:

use conductor_daemon::gamepad_device::GamepadDeviceManager;

fn show_gamepads() -> Result<(), String> {
    let gamepads = GamepadDeviceManager::list_gamepads()?;
    for (id, name, uuid) in gamepads {
        println!("Gamepad: {} (ID: {:?}, UUID: {})", name, id, uuid);
    }
    Ok(())
}

Thread Safety

The GamepadDeviceManager uses:

  • Arc<Mutex<>> for shared state (gamepad ID, name, thread handle)
  • Arc<AtomicBool> for lock-free flags (is_connected, stop_polling)
  • Safe to share across threads

Polling Loop

The manager spawns a background thread that:

  1. Polls for gamepad events at 1ms intervals
  2. Converts gilrs events to InputEvent
  3. Sends events through the provided channel
  4. Detects disconnection and triggers reconnection if enabled

Reconnection Logic

When a gamepad disconnects and auto_reconnect is enabled:

  1. Spawns a reconnection thread
  2. Uses exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s
  3. Checks for available gamepads at each interval
  4. Sends DaemonCommand::ReconnectGamepad when a device is found
  5. Gives up after 6 attempts

InputManager

Location: conductor-daemon/src/input_manager.rs

Unified manager for both MIDI and gamepad input devices. Provides a single InputEvent stream for all inputs.

Fields

pub struct InputManager {
    /// MIDI device manager (optional)
    midi_manager: Option<MidiDeviceManager>,

    /// Gamepad device manager (optional)
    gamepad_manager: Option<GamepadDeviceManager>,

    /// Input mode selection
    mode: InputMode,
}

Constructor

pub fn new(
    midi_device_name: Option<String>,
    auto_reconnect: bool,
    mode: InputMode,
) -> Self

Creates a new unified input manager.

Parameters:

  • midi_device_name - Name of MIDI device to connect to (None = first available)
  • auto_reconnect - Enable automatic reconnection for both MIDI and gamepad
  • mode - Input mode selection (MidiOnly, GamepadOnly, or Both)

Example:

use conductor_daemon::input_manager::{InputManager, InputMode};

// MIDI + Gamepad hybrid setup
let manager = InputManager::new(
    Some("Maschine Mikro MK3".to_string()),
    true,
    InputMode::Both
);

// Gamepad-only setup
let gamepad_only = InputManager::new(
    None,
    true,
    InputMode::GamepadOnly
);

Methods

connect()
pub fn connect(
    &mut self,
    event_tx: mpsc::Sender<InputEvent>,
    command_tx: mpsc::Sender<DaemonCommand>,
) -> Result<String, String>

Connects to input devices based on the configured mode.

Parameters:

  • event_tx - Channel sender for unified InputEvent stream
  • command_tx - Channel sender for daemon commands

Returns:

  • Ok(String) - Status message describing connected devices
  • Err(String) - Error if no devices could be connected

Example:

use conductor_daemon::input_manager::{InputManager, InputMode};
use tokio::sync::mpsc;

async fn start_unified_input() -> Result<(), String> {
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    let mut manager = InputManager::new(None, true, InputMode::Both);
    let status = manager.connect(event_tx, command_tx)?;

    println!("Connected: {}", status);
    // Output: "MIDI: Maschine Mikro MK3 (port 0) | Gamepad: Xbox Controller (ID 0)"

    // Process unified event stream
    while let Some(event) = event_rx.recv().await {
        match event {
            InputEvent::PadPressed { pad, velocity, .. } => {
                if pad < 128 {
                    println!("MIDI pad {} pressed (velocity {})", pad, velocity);
                } else {
                    println!("Gamepad button {} pressed", pad);
                }
            }
            _ => {}
        }
    }

    Ok(())
}
is_connected()
pub fn is_connected(&self) -> bool

Returns true if any input device is connected.

get_status()
pub fn get_status(&self) -> (bool, bool)

Returns connection status for both devices as (midi_connected, gamepad_connected).

Example:

let (midi, gamepad) = manager.get_status();
println!("MIDI: {}, Gamepad: {}", midi, gamepad);
disconnect()
pub fn disconnect(&mut self)

Disconnects all input devices.

mode()
pub fn mode(&self) -> InputMode

Returns the current input mode.

get_connected_gamepads()
pub fn get_connected_gamepads(&self) -> Vec<(String, String)>

Returns a list of (ID, Name) tuples for connected gamepads.

list_gamepads() (static)
pub fn list_gamepads() -> Result<Vec<(gilrs::GamepadId, String, String)>, String>

Lists all available gamepads (delegates to GamepadDeviceManager::list_gamepads()).


Event Types

InputEvent

Location: conductor-core/src/events.rs

Protocol-agnostic input event abstraction. All gamepad events are converted to this type.

#[derive(Debug, Clone, PartialEq)]
pub enum InputEvent {
    /// Pad pressed (button on controller)
    PadPressed {
        pad: u8,
        velocity: u8,
        time: Instant,
    },

    /// Pad released (button released)
    PadReleased {
        pad: u8,
        time: Instant,
    },

    /// Encoder turned (analog stick or trigger)
    EncoderTurned {
        encoder: u8,
        value: u8,
        time: Instant,
    },

    /// Polyphonic aftertouch/pressure (specific pad)
    PolyPressure {
        pad: u8,
        pressure: u8,
        time: Instant,
    },

    /// Aftertouch/pressure (channel-wide)
    Aftertouch {
        pressure: u8,
        time: Instant,
    },

    /// Pitch bend/touch strip
    PitchBend {
        value: u16,
        time: Instant,
    },

    /// Program change
    ProgramChange {
        program: u8,
        time: Instant,
    },

    /// Generic control change
    ControlChange {
        control: u8,
        value: u8,
        time: Instant,
    },
}

Gamepad Event Mapping

Gamepad events are converted to InputEvent as follows:

Gamepad EventInputEvent VariantID RangeNotes
Button PressPadPressed128-255Velocity = 100 (default)
Button ReleasePadReleased128-255N/A
Analog StickEncoderTurned128-131-1.0..1.0 → 0..127 (64 = center)
Analog TriggerEncoderTurned132-1330.0..1.0 → 0..127

Button and Axis IDs

Button ID Constants

Location: conductor-core/src/gamepad_events.rs

Gamepad buttons use IDs 128-255 to avoid conflicts with MIDI note numbers (0-127).

pub mod button_ids {
    // Face buttons (128-131)
    pub const SOUTH: u8 = 128;         // A (Xbox), Cross (PS), B (Nintendo)
    pub const EAST: u8 = 129;          // B (Xbox), Circle (PS), A (Nintendo)
    pub const WEST: u8 = 130;          // X (Xbox), Square (PS), Y (Nintendo)
    pub const NORTH: u8 = 131;         // Y (Xbox), Triangle (PS), X (Nintendo)

    // D-Pad (132-135)
    pub const DPAD_UP: u8 = 132;
    pub const DPAD_DOWN: u8 = 133;
    pub const DPAD_LEFT: u8 = 134;
    pub const DPAD_RIGHT: u8 = 135;

    // Shoulder buttons (136-137)
    pub const LEFT_SHOULDER: u8 = 136;  // L1, LB
    pub const RIGHT_SHOULDER: u8 = 137; // R1, RB

    // Stick clicks (138-139)
    pub const LEFT_THUMB: u8 = 138;    // L3
    pub const RIGHT_THUMB: u8 = 139;   // R3

    // Menu buttons (140-142)
    pub const START: u8 = 140;         // Start, Options, +
    pub const SELECT: u8 = 141;        // Back, Share, -
    pub const GUIDE: u8 = 142;         // Xbox, PS, Home

    // Trigger digital fallback (143-144)
    pub const LEFT_TRIGGER: u8 = 143;  // L2, LT (digital threshold)
    pub const RIGHT_TRIGGER: u8 = 144; // R2, RT (digital threshold)
}

Cross-Platform Button Mapping

IDStandard NameXboxPlayStationNintendo Switch
128SOUTHACross (×)B
129EASTBCircle (○)A
130WESTXSquare (□)Y
131NORTHYTriangle (△)X
132DPAD_UPD-Pad UpD-Pad UpD-Pad Up
133DPAD_DOWND-Pad DownD-Pad DownD-Pad Down
134DPAD_LEFTD-Pad LeftD-Pad LeftD-Pad Left
135DPAD_RIGHTD-Pad RightD-Pad RightD-Pad Right
136LEFT_SHOULDERLBL1L
137RIGHT_SHOULDERRBR1R
138LEFT_THUMBLeft StickL3Left Stick
139RIGHT_THUMBRight StickR3Right Stick
140STARTStartOptions+ (Plus)
141SELECTBackShare- (Minus)
142GUIDEXbox ButtonPS ButtonHome Button
143LEFT_TRIGGERLT (digital)L2 (digital)ZL (digital)
144RIGHT_TRIGGERRT (digital)R2 (digital)ZR (digital)

Encoder/Axis ID Constants

pub mod encoder_ids {
    // Analog stick axes (128-131)
    pub const LEFT_STICK_X: u8 = 128;
    pub const LEFT_STICK_Y: u8 = 129;
    pub const RIGHT_STICK_X: u8 = 130;
    pub const RIGHT_STICK_Y: u8 = 131;

    // Trigger axes (132-133)
    pub const LEFT_TRIGGER: u8 = 132;  // L2, LT analog value
    pub const RIGHT_TRIGGER: u8 = 133; // R2, RT analog value
}

Axis Mapping Table

IDAxis NameRangeNormalizedNotes
128LEFT_STICK_X-1.0 to 1.00 to 12764 = center, 0 = left, 127 = right
129LEFT_STICK_Y-1.0 to 1.00 to 12764 = center, 0 = up, 127 = down
130RIGHT_STICK_X-1.0 to 1.00 to 12764 = center, 0 = left, 127 = right
131RIGHT_STICK_Y-1.0 to 1.00 to 12764 = center, 0 = up, 127 = down
132LEFT_TRIGGER0.0 to 1.00 to 127Analog pressure (L2/LT)
133RIGHT_TRIGGER0.0 to 1.00 to 127Analog pressure (R2/RT)

Note: A 0.1 deadzone is applied to analog sticks to reduce drift. Values within ±0.1 of center return 64.


Helper Functions

Button Conversion

Location: conductor-core/src/gamepad_events.rs

button_to_id()

pub fn button_to_id(button: gilrs::Button) -> u8

Converts gilrs Button enum to Conductor button ID (128-255 range).

Example:

use gilrs::Button;
use conductor_core::gamepad_events::button_to_id;

let id = button_to_id(Button::South);
assert_eq!(id, 128); // SOUTH (A/Cross/B)

button_pressed_to_input()

pub fn button_pressed_to_input(button: gilrs::Button) -> InputEvent

Converts gilrs ButtonPressed event to InputEvent::PadPressed with default velocity 100.

Example:

use gilrs::Button;
use conductor_core::gamepad_events::button_pressed_to_input;

let event = button_pressed_to_input(Button::South);
// Returns: InputEvent::PadPressed { pad: 128, velocity: 100, time: now() }

button_released_to_input()

pub fn button_released_to_input(button: gilrs::Button) -> InputEvent

Converts gilrs ButtonReleased event to InputEvent::PadReleased.

Axis Conversion

axis_to_encoder_id()

pub fn axis_to_encoder_id(axis: gilrs::Axis) -> u8

Converts gilrs Axis enum to Conductor encoder ID (128-133 range).

Example:

use gilrs::Axis;
use conductor_core::gamepad_events::axis_to_encoder_id;

let id = axis_to_encoder_id(Axis::LeftStickX);
assert_eq!(id, 128); // LEFT_STICK_X

normalize_axis()

pub fn normalize_axis(value: f32) -> u8

Normalizes gilrs axis values (-1.0 to 1.0) to MIDI-compatible range (0-127).

Normalization Rules:

  • Input range: -1.0 to 1.0
  • Output range: 0 to 127
  • Center point: 64
  • Deadzone: ±0.1 (returns 64 if within deadzone)

Example:

use conductor_core::gamepad_events::normalize_axis;

assert_eq!(normalize_axis(0.0), 64);   // Center
assert_eq!(normalize_axis(1.0), 127);  // Max right/up
assert_eq!(normalize_axis(-1.0), 0);   // Max left/down
assert_eq!(normalize_axis(0.05), 64);  // Deadzone (< 0.1)

axis_changed_to_input()

pub fn axis_changed_to_input(axis: gilrs::Axis, value: f32) -> InputEvent

Converts gilrs AxisChanged event to InputEvent::EncoderTurned.

Example:

use gilrs::Axis;
use conductor_core::gamepad_events::axis_changed_to_input;

let event = axis_changed_to_input(Axis::LeftStickX, 0.5);
// Returns: InputEvent::EncoderTurned { encoder: 128, value: 95, time: now() }

Integration Examples

Basic Gamepad Connection

use conductor_daemon::gamepad_device::GamepadDeviceManager;
use tokio::sync::mpsc;
use conductor_core::events::InputEvent;
use conductor_daemon::DaemonCommand;

async fn basic_gamepad_example() -> Result<(), String> {
    // Create channels
    let (event_tx, mut event_rx) = mpsc::channel::<InputEvent>(1024);
    let (command_tx, _) = mpsc::channel::<DaemonCommand>(32);

    // Create manager with auto-reconnect
    let mut manager = GamepadDeviceManager::new(true);

    // Connect to first available gamepad
    let (gamepad_id, gamepad_name) = manager.connect(
        event_tx.clone(),
        command_tx.clone()
    )?;

    println!("Connected to gamepad: {} (ID {:?})", gamepad_name, gamepad_id);

    // Process events
    while let Some(event) = event_rx.recv().await {
        match event {
            InputEvent::PadPressed { pad, velocity, .. } => {
                println!("Button {} pressed (velocity {})", pad, velocity);
            }
            InputEvent::PadReleased { pad, .. } => {
                println!("Button {} released", pad);
            }
            InputEvent::EncoderTurned { encoder, value, .. } => {
                println!("Encoder {} value: {}", encoder, value);
            }
            _ => {}
        }
    }

    Ok(())
}

Hybrid MIDI + Gamepad Setup

use conductor_daemon::input_manager::{InputManager, InputMode};
use conductor_core::events::InputEvent;
use conductor_core::gamepad_events::button_ids;
use tokio::sync::mpsc;

async fn hybrid_example() -> Result<(), String> {
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    // Create hybrid manager (both MIDI and gamepad)
    let mut manager = InputManager::new(
        Some("Maschine Mikro MK3".to_string()),
        true,
        InputMode::Both
    );

    // Connect to both devices
    let status = manager.connect(event_tx, command_tx)?;
    println!("Connected: {}", status);

    // Process unified event stream
    while let Some(event) = event_rx.recv().await {
        match event {
            InputEvent::PadPressed { pad, velocity, .. } => {
                if pad < 128 {
                    // MIDI pad (0-127)
                    println!("MIDI pad {} pressed (velocity {})", pad, velocity);
                } else {
                    // Gamepad button (128-255)
                    let button_name = match pad {
                        button_ids::SOUTH => "A/Cross",
                        button_ids::EAST => "B/Circle",
                        button_ids::WEST => "X/Square",
                        button_ids::NORTH => "Y/Triangle",
                        button_ids::START => "Start",
                        _ => "Unknown",
                    };
                    println!("Gamepad button {} ({}) pressed", pad, button_name);
                }
            }
            InputEvent::EncoderTurned { encoder, value, .. } => {
                if encoder < 128 {
                    // MIDI encoder/knob
                    println!("MIDI encoder {} value: {}", encoder, value);
                } else {
                    // Gamepad analog stick/trigger
                    println!("Gamepad axis {} value: {}", encoder, value);
                }
            }
            _ => {}
        }
    }

    Ok(())
}

Integrating with MappingEngine

use conductor_core::event_processor::EventProcessor;
use conductor_core::mapping::MappingEngine;
use conductor_core::config::Config;
use conductor_daemon::input_manager::{InputManager, InputMode};
use tokio::sync::mpsc;

async fn full_integration_example() -> Result<(), String> {
    // Load configuration
    let config = Config::load_from_path("config.toml")?;

    // Create event processor and mapping engine
    let mut event_processor = EventProcessor::new();
    let mut mapping_engine = MappingEngine::new(config);

    // Set up unified input
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    let mut input_manager = InputManager::new(
        None,
        true,
        InputMode::Both
    );

    input_manager.connect(event_tx, command_tx)?;

    // Process events through the full pipeline
    while let Some(input_event) = event_rx.recv().await {
        // InputEvent → ProcessedEvent
        if let Some(processed) = event_processor.process(input_event) {
            // ProcessedEvent → Action execution
            mapping_engine.handle_event(&processed);
        }
    }

    Ok(())
}

Listing Available Gamepads

use conductor_daemon::gamepad_device::GamepadDeviceManager;

fn list_gamepads_example() -> Result<(), String> {
    let gamepads = GamepadDeviceManager::list_gamepads()?;

    if gamepads.is_empty() {
        println!("No gamepads connected");
    } else {
        println!("Connected gamepads:");
        for (id, name, uuid) in gamepads {
            println!("  - {} (ID: {:?}, UUID: {})", name, id, uuid);
        }
    }

    Ok(())
}

Error Handling

Common Errors

ErrorCauseSolution
“Failed to initialize gilrs”SDL2 not available or system errorInstall SDL2, check system permissions
“No gamepads connected”No physical gamepad detectedConnect a gamepad, check USB connection
“Already connected to a gamepad”Attempted to connect twiceCall disconnect() before reconnecting
“No input devices could be connected”Both MIDI and gamepad failedCheck device connections, verify drivers

Handling Disconnections

The GamepadDeviceManager automatically handles disconnections when auto_reconnect is enabled:

let mut manager = GamepadDeviceManager::new(true); // auto_reconnect = true

// Disconnection is detected automatically
// Reconnection attempts occur with exponential backoff:
// 1s, 2s, 4s, 8s, 16s, 30s (max 6 attempts)

// Listen for reconnection commands
while let Some(command) = command_rx.recv().await {
    match command {
        DaemonCommand::ReconnectGamepad => {
            println!("Gamepad reconnected!");
            // Manager automatically reconnects
        }
        _ => {}
    }
}

Manual Error Handling

use conductor_daemon::gamepad_device::GamepadDeviceManager;

fn safe_connect() {
    let mut manager = GamepadDeviceManager::new(false); // no auto-reconnect

    loop {
        match manager.connect(event_tx.clone(), command_tx.clone()) {
            Ok((id, name)) => {
                println!("Connected to: {}", name);
                break;
            }
            Err(e) => {
                eprintln!("Connection failed: {}", e);
                std::thread::sleep(std::time::Duration::from_secs(5));
                // Retry after 5 seconds
            }
        }
    }
}

Configuration Examples

TOML Configuration for Gamepad Mappings

# config.toml

[device]
name = "Hybrid Controller"
auto_connect = true

[[global_mappings]]
[global_mappings.trigger]
type = "Note"
note = 128  # Gamepad SOUTH button (A/Cross)

[[global_mappings.actions]]
type = "Keystroke"
key = "Space"

[[global_mappings]]
[global_mappings.trigger]
type = "Note"
note = 140  # Gamepad START button

[[global_mappings.actions]]
type = "Launch"
app = "Terminal"

[[global_mappings]]
[global_mappings.trigger]
type = "EncoderTurn"
encoder = 128  # Left stick X-axis
direction = "Clockwise"

[[global_mappings.actions]]
type = "VolumeControl"
action = "Up"

Velocity-Sensitive Gamepad Triggers (Future Enhancement)

Currently, gamepad buttons use a fixed velocity of 100. Future versions may support pressure-sensitive triggers:

# Future feature (not yet implemented)
[[global_mappings]]
[global_mappings.trigger]
type = "VelocityRange"
note = 132  # Left trigger (analog)
min_velocity = 80
max_velocity = 127

[[global_mappings.actions]]
type = "Keystroke"
key = "F"
modifiers = ["Shift"]  # Hard press = Shift+F

Thread Safety and Concurrency

Arc/Mutex Patterns

The gamepad system uses Rust’s Arc<Mutex<>> and Arc<AtomicBool> for safe concurrent access:

// Internal state (Arc<Mutex<>>)
gamepad_id: Arc<Mutex<Option<gilrs::GamepadId>>>
gamepad_name: Arc<Mutex<Option<String>>>
polling_thread: Arc<Mutex<Option<thread::JoinHandle<()>>>>

// Atomic flags (Arc<AtomicBool>)
is_connected: Arc<AtomicBool>
stop_polling: Arc<AtomicBool>

Polling Thread Architecture

┌─────────────────────────────────────────────────────────┐
│  Main Thread                                            │
│  - Creates GamepadDeviceManager                         │
│  - Calls connect()                                      │
│  - Receives InputEvents via mpsc channel                │
└─────────────────────────────────────────────────────────┘
                        │
                        ▼ spawns
┌─────────────────────────────────────────────────────────┐
│  Polling Thread                                         │
│  - Polls gilrs at 1ms intervals                         │
│  - Converts gilrs events → InputEvent                   │
│  - Sends via mpsc::Sender<InputEvent>                   │
│  - Detects disconnection                                │
│  - Stops on stop_polling signal                         │
└─────────────────────────────────────────────────────────┘
                        │
                        ▼ spawns on disconnect
┌─────────────────────────────────────────────────────────┐
│  Reconnection Thread (if auto_reconnect = true)         │
│  - Exponential backoff (1s, 2s, 4s, 8s, 16s, 30s)      │
│  - Checks for available gamepads                        │
│  - Sends DaemonCommand::ReconnectGamepad when found     │
└─────────────────────────────────────────────────────────┘

Performance Characteristics

MetricValueNotes
Polling Interval1msBalances latency and CPU usage
Reconnect Attempts6Exponential backoff schedule
Max Reconnect Time~60sSum of backoff delays
Event Channel Capacity1024Default mpsc buffer size
Event Latency<5msgilrs → InputEvent → channel
CPU Usage (Idle)<1%Efficient polling loop
Memory Overhead~100KBPer GamepadDeviceManager

Device Compatibility

Tested Controllers

ControllerStatusNotes
Xbox One/Series Controllers✅ Fully SupportedSDL2 GameController mapping
PlayStation 4/5 DualShock/DualSense✅ Fully SupportedStandard button layout
Nintendo Switch Pro Controller✅ Fully SupportedButton labels differ (A/B swapped)
Generic USB Gamepads✅ SupportedMay require custom SDL2 mapping
Logitech F310/F710✅ SupportedSwitch to XInput mode
8BitDo Controllers✅ SupportedUse XInput/Switch mode

HID Device Types

The gamepad system supports any HID-compliant game controller:

  • Gamepads: Xbox, PlayStation, Nintendo, generic USB pads
  • Joysticks: Flight sticks, arcade sticks
  • Racing Wheels: Logitech G29, Thrustmaster T300
  • HOTAS: Hands-On Throttle-and-Stick setups
  • Custom Controllers: Any SDL2-compatible HID device

Platform Support

PlatformStatusRequirements
macOS✅ SupportedNative HID support
Linux✅ SupportedSDL2 + udev rules
Windows✅ SupportedSDL2 + XInput

Debugging and Diagnostics

Enabling Debug Logging

# Enable tracing logs
RUST_LOG=debug cargo run

# Filter for gamepad-specific logs
RUST_LOG=conductor_daemon::gamepad_device=trace cargo run

Diagnostic Commands

# List connected gamepads
cargo run --bin list_gamepads

# Test gamepad input
cargo run --bin test_gamepad_input

# Monitor unified event stream
cargo run --bin event_console

Example Debug Output

[DEBUG conductor_daemon::gamepad_device] Connecting to gamepad: Xbox Controller (ID: GamepadId(0))
[TRACE conductor_daemon::gamepad_device] Gamepad event: Event { id: GamepadId(0), event: ButtonPressed(South, 0) }
[DEBUG conductor_daemon::gamepad_device] Button 128 (SOUTH) pressed
[TRACE conductor_daemon::gamepad_device] Sent InputEvent::PadPressed { pad: 128, velocity: 100 }
[TRACE conductor_daemon::gamepad_device] Gamepad event: Event { id: GamepadId(0), event: AxisChanged(LeftStickX, 0.523, 0) }
[DEBUG conductor_daemon::gamepad_device] Encoder 128 (LEFT_STICK_X) value: 95

Future Enhancements

Planned Features (Not Yet Implemented)

  1. Pressure-Sensitive Buttons: Variable velocity based on analog button pressure
  2. Gyroscope/Accelerometer Support: Motion controls for advanced controllers
  3. Haptic Feedback: Rumble/vibration control via actions
  4. Custom Button Mappings: Override default button-to-ID mappings
  5. Multi-Controller Support: Connect multiple gamepads simultaneously
  6. Per-Controller Profiles: Different mappings for different gamepad models
  7. Axis Inversion/Scaling: Fine-tune analog stick sensitivity
  8. Macro Recording: Record gamepad input sequences

Experimental Features

// Future API (not yet available)
pub struct GamepadConfig {
    pub deadzone: f32,
    pub sensitivity: f32,
    pub invert_y_axis: bool,
    pub button_mappings: HashMap<gilrs::Button, u8>,
}

impl GamepadDeviceManager {
    pub fn new_with_config(
        auto_reconnect: bool,
        config: GamepadConfig
    ) -> Self { /* ... */ }
}

See Also


Glossary

TermDefinition
HIDHuman Interface Device - USB standard for input devices
gilrsRust library for game controller input (built on SDL2)
SDL2Simple DirectMedia Layer - cross-platform game controller API
InputEventProtocol-agnostic event abstraction
GamepadIdgilrs identifier for a specific connected gamepad
Arc/MutexRust concurrency primitives for shared state
mpscMulti-Producer, Single-Consumer channel for thread communication

Last Updated: 2025-11-21 API Version: v3.0 Crate Versions:

  • conductor-core: 3.0.0
  • conductor-daemon: 3.0.0
  • gilrs: 0.11.0

Compatibility Matrix

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Maschine Mikro MK3

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Generic MIDI Controllers

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Creating Device Profiles

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Development Setup

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Architecture Overview

Current Architecture (v0.2.0 - Phase 2)

Conductor uses a Cargo workspace architecture with three packages. Phase 2 migration completed successfully with zero breaking changes.

Workspace Packages

  1. conductor-core: Pure Rust engine library (zero UI dependencies)
  2. conductor-daemon: CLI daemon + 6 diagnostic tools
  3. conductor: Backward compatibility layer for existing tests

System Architecture

┌─────────────────────────────────────────────────────────────┐
│                    conductor-daemon                           │
│              (CLI Daemon + Diagnostic Tools)                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   main.rs       6 diagnostic binaries                      │
│   (daemon)      (tools for MIDI/LED testing)               │
│                                                             │
│                      ▼  imports                             │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────────┐
│                    conductor-core                             │
│              (Pure Rust Engine Library)                     │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │   Config    │───▶│    Mapping   │◀──▶│  Feedback    │  │
│  │   Loader    │    │    Engine    │    │   System     │  │
│  └─────────────┘    └──────────────┘    └──────────────┘  │
│         │                   │                    │         │
│         │                   │                    │         │
│         ▼                   ▼                    ▼         │
│  ┌─────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │   Events    │    │    Event     │    │  HID/MIDI    │  │
│  │   Types     │    │  Processor   │    │   LEDs       │  │
│  └─────────────┘    └──────────────┘    └──────────────┘  │
│         │                   │                    │         │
│         │                   │                    │         │
│         ▼                   ▼                    ▼         │
│  ┌─────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │   Actions   │    │    Device    │    │    Error     │  │
│  │  Executor   │    │   Profiles   │    │    Types     │  │
│  └─────────────┘    └──────────────┘    └──────────────┘  │
│                                                             │
│  Zero UI dependencies • Public API • Reusable library      │
└─────────────────────────────────────────────────────────────┘
                       ▲
┌──────────────────────┴──────────────────────────────────────┐
│                      conductor (root)                         │
│               (Backward Compatibility Layer)                │
├─────────────────────────────────────────────────────────────┤
│  Re-exports conductor_core types for existing tests          │
│  Maintains v0.1.0 import paths • Zero breaking changes     │
└─────────────────────────────────────────────────────────────┘

Event Processing Pipeline

The system uses a three-stage event processing architecture:

┌──────────────────┐
│   MIDI Device    │
└────────┬─────────┘
         │ Raw bytes
         ▼
┌──────────────────┐
│   MidiEvent      │  (Note On/Off, CC, Velocity)
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Event Processor  │  (State Machine)
│  - Velocity      │  - Soft/Med/Hard levels
│  - Long Press    │  - Hold detection (2000ms)
│  - Double-Tap    │  - Quick succession (300ms)
│  - Chord         │  - Simultaneous notes (50ms)
│  - Encoder       │  - Direction detection
└────────┬─────────┘
         │ ProcessedEvent
         ▼
┌──────────────────┐
│ Mapping Engine   │  (Mode-aware matching)
│  - Global maps   │
│  - Mode maps     │
│  - Trigger match │
└────────┬─────────┘
         │ Action
         ▼
┌──────────────────┐
│ Action Executor  │  (enigo for input simulation)
│  - Keystroke     │
│  - Shell         │
│  - Launch        │
│  - Volume        │
└──────────────────┘

Core Components

main.rs

  • Entry Point: CLI argument parsing, device enumeration
  • MIDI Connection: Sets up midir input callbacks
  • Event Loop: Coordinates event processing thread
  • Mode Management: Tracks current mode via AtomicU8
  • Device Profiles: Loads .ncmm3 profiles for pad mapping

config.rs

  • Configuration Types: Config, Mode, Mapping, Trigger, ActionConfig
  • TOML Parsing: Loads and validates config.toml
  • Serialization: Supports saving default configs

event_processor.rs

  • State Machine: Transforms raw MIDI → ProcessedEvent
  • Timing Detection: Long press, double-tap, hold threshold
  • Velocity Ranges: Soft (0-40), Medium (41-80), Hard (81-127)
  • Chord Detection: Buffers notes within 50ms window
  • Encoder Direction: Detects clockwise/counterclockwise rotation

mappings.rs

  • Mapping Engine: Matches ProcessedEventAction
  • Mode System: Global mappings + per-mode mappings
  • Trigger Matching: Supports Note, CC, Chord, Velocity, LongPress, DoubleTap
  • Compilation: Converts Trigger config → CompiledTrigger for fast matching

actions.rs

  • Action Executor: Executes compiled actions via enigo
  • Keystroke Simulation: Key sequences with modifiers
  • Application Launch: Platform-specific (macOS open, Linux exec, Windows start)
  • Shell Execution: Runs arbitrary shell commands
  • Sequences: Chains multiple actions with delays

feedback.rs

  • Trait Abstraction: PadFeedback trait for LED control
  • Device Factory: Creates HID (Mikro MK3) or MIDI feedback
  • Lighting Schemes: Off, Static, Breathing, Pulse, Rainbow, Wave, Sparkle, Reactive, VuMeter, Spiral

mikro_leds.rs

  • HID Control: Direct RGB LED control via hidapi
  • Maschine Mikro MK3: Full RGB control, 16 pads
  • Shared Device Mode: macOS shared access (runs alongside NI Controller Editor)
  • Effects: Velocity feedback, mode colors, reactive lighting

midi_feedback.rs

  • Standard MIDI: Fallback LED control via MIDI Note messages
  • Generic Devices: Works with any MIDI device with LED support
  • Limited Features: On/off only, no color control

device_profile.rs

  • NI Profile Parser: Parses .ncmm3 XML from Controller Editor
  • Pad Mapping: Maps physical pad positions → MIDI notes
  • Page Support: Handles pad pages (A-H) for multi-page controllers
  • Auto-Detection: Detects active pad page from incoming MIDI

Key Design Patterns

Mode System

Multiple modes (Default, Development, Media, etc.) allow different mapping sets. Each mode has:

  • Distinct name and color theme
  • Mode-specific mappings
  • LED color scheme

Mode changes triggered by:

  • Encoder rotation (CC messages)
  • Specific pad combinations
  • Programmatic mode switching

Global vs Mode Mappings

  • Global Mappings: Active in all modes (e.g., emergency exit, encoder volume)
  • Mode Mappings: Scoped to specific modes (e.g., dev tools in Development mode)

Profile-Based Note Mapping

Supports loading NI Controller Editor profiles (.ncmm3) to:

  • Map physical pad positions to MIDI notes
  • Handle different pad pages (A-H)
  • Auto-detect active pad page from events
  • Support custom controller configurations

LED Feedback System

Trait-based abstraction (PadFeedback) supports:

  • HID Devices: Full RGB control (Maschine Mikro MK3)
  • MIDI Devices: Basic on/off via MIDI Note messages

Reactive Mode: LEDs respond to velocity:

  • Soft (green), Medium (yellow), Hard (red)
  • Fade-out 1 second after release

Mode Colors: Distinct color themes per mode

  • Mode 0: Blue
  • Mode 1: Green
  • Mode 2: Purple

Threading and Concurrency

┌─────────────────────┐
│   Main Thread       │
│   - Config load     │
│   - Device connect  │
│   - Ctrl+C handler  │
└──────────┬──────────┘
           │
           ├─────────────────────────┐
           │                         │
           ▼                         ▼
┌─────────────────────┐   ┌─────────────────────┐
│   MIDI Callback     │   │   LED Effect        │
│   - Raw bytes       │   │   - Background      │
│   - crossbeam send  │   │   - Lighting loop   │
└──────────┬──────────┘   └─────────────────────┘
           │
           ▼
┌─────────────────────┐
│   Event Thread      │
│   - Process events  │
│   - Match mappings  │
│   - Execute actions │
└─────────────────────┘

Current Threading Model:

  • Main Thread: Setup, config loading, signal handling
  • MIDI Callbacks: Lock-free (via crossbeam-channel)
  • Event Thread: Dedicated thread for event processing
  • LED Effects: Optional background thread for lighting schemes

Atomic State:

  • AtomicU8 for current mode (lock-free reads/writes)
  • AtomicBool for running flag (graceful shutdown)

Performance Characteristics

  • Event Latency: <1ms typical
  • Memory Usage: 5-10MB
  • CPU Usage: <1% idle, <5% active
  • Binary Size: ~3-5MB (release with LTO)

Dependencies

Core Dependencies:

  • midir: Cross-platform MIDI I/O
  • enigo: Keyboard/mouse simulation
  • hidapi: HID device access (with macos-shared-device)
  • serde/toml: Config parsing
  • quick-xml: XML profile parsing
  • crossbeam-channel: Lock-free event channels

Platform-Specific:

  • macOS: AppleScript for volume control
  • Linux: xdotool (optional, for input simulation)
  • Windows: Native Windows APIs

Future Phases (Phase 4+: GUI)

The roadmap includes migrating to a workspace structure with separate crates. See Workspace Structure Design for complete details (AMI-124).

Target Structure

conductor/
├── Cargo.toml                      # Workspace root manifest
├── conductor-core/                   # Pure Rust engine (UI-free)
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs                  # Public API
│       ├── engine.rs               # MidiMonEngine
│       ├── config.rs               # Config types
│       ├── events.rs               # Event types
│       ├── mappings.rs             # Mapping engine
│       ├── actions.rs              # Action execution
│       ├── feedback.rs             # LED feedback
│       ├── device_profile.rs       # NI profile parser
│       └── error.rs                # Error types
├── conductor-daemon/                 # CLI binary (current main.rs)
│   ├── Cargo.toml
│   └── src/
│       ├── main.rs                 # CLI entry point
│       ├── cli.rs                  # Argument parsing
│       ├── debug.rs                # Debug output
│       └── bin/                    # Diagnostic tools
├── conductor-gui/                    # Tauri UI (Phase 4)
│   ├── Cargo.toml
│   ├── src-tauri/
│   └── ui/
└── config/                         # Config templates
    ├── default.toml
    ├── examples/
    └── device_templates/

Workspace Dependency Graph

graph TD
    A[conductor-gui<br/>Tauri UI] -->|depends on| C[conductor-core<br/>Engine Library]
    B[conductor-daemon<br/>CLI Binary] -->|depends on| C
    C -->|no dependencies on| A
    C -->|no dependencies on| B

    style C fill:#4a90e2,stroke:#2e5f8a,stroke-width:3px,color:#fff
    style A fill:#50c878,stroke:#2d7a4d,stroke-width:2px,color:#fff
    style B fill:#50c878,stroke:#2d7a4d,stroke-width:2px,color:#fff

Phase 2: Core Library Extraction ✅ COMPLETE (v0.2.0)

Goal: Extract engine logic into reusable conductor-core crate (AMI-123, AMI-124).

Public API (from API Design):

pub struct MidiMonEngine;
impl MidiMonEngine {
    pub fn new(config: Config) -> Result<Self, EngineError>;
    pub fn start(&mut self) -> Result<(), EngineError>;
    pub fn stop(&mut self) -> Result<(), EngineError>;
    pub fn reload_config(&mut self, config: Config) -> Result<(), EngineError>;
    pub fn current_mode(&self) -> u8;
    pub fn set_mode(&mut self, mode: u8) -> Result<(), EngineError>;
    pub fn config(&self) -> Config;
    pub fn stats(&self) -> EngineStats;

    // Callbacks for integration
    pub fn on_mode_change<F>(&mut self, callback: F)
    where F: Fn(u8) + Send + 'static;

    pub fn on_action<F>(&mut self, callback: F)
    where F: Fn(&ProcessedEvent, &Action) + Send + 'static;
}

Module Separation:

  • Public Modules: config, engine, events, actions, feedback, device, error
  • Private Modules: event_processor, timing, chord, velocity

Benefits:

  • Reusable engine for CLI, daemon, GUI
  • Zero UI dependencies in core (no colored, chrono for display)
  • Clean API boundaries with trait-based abstractions
  • Easier testing and integration
  • Thread-safe with Arc/RwLock for shared state

Build Commands:

# Build core library only
cargo build -p conductor-core

# Build CLI daemon
cargo build -p conductor-daemon --release

# Run with new structure
cargo run -p conductor-daemon --release -- 2 --led reactive

# Test workspace
cargo test --workspace

Phase 3: Daemon and Menu Bar

Goal: Add conductor-daemon with macOS menu bar integration.

Features:

  • System tray icon with status
  • Quick actions (Pause, Reload, Open Config)
  • Config hot-reloading via notify crate
  • Auto-start via LaunchAgent or Tauri autostart plugin
  • Frontmost app detection for per-app profiles

Integration Pattern:

use conductor_core::{Config, MidiMonEngine};

fn main() -> Result<()> {
    let config = Config::load("config.toml")?;
    let mut engine = MidiMonEngine::new(config)?;

    // Register callbacks for UI updates
    engine.on_mode_change(|mode| {
        update_menu_bar_icon(mode);
    });

    engine.start()?;
    Ok(())
}

Phase 4: GUI Configuration

Goal: Add conductor-gui with Tauri-based visual config editor.

Features:

  • Visual device mapping with SVG pad layouts
  • MIDI Learn mode (click → press → bind)
  • Profile management (import/export/share)
  • Live event console with filtering
  • Velocity curve editor
  • Test bindings without saving

UI Components:

  • Device visualizer (16-pad grid for Maschine Mikro MK3)
  • Mapping editor (trigger → action configuration)
  • Profile switcher (per-app profiles)
  • Event log (real-time MIDI/HID events)
  • Settings panel (advanced timing, thresholds)

Configuration Backward Compatibility

Core Commitment: Configuration files created for v0.1.0 will work in all v1.x.x versions without modification.

Stability Guarantees

Sectionv0.1.0v0.2.0v1.0.0Stability
[device]Stable
[[modes]]Stable
[[global_mappings]]Stable (optional, defaults to empty)
[advanced_settings]⚠️Optional (defaults provided)

Legend:

  • Fully Supported: Section works identically across versions
  • ⚠️ Optional: Section is optional with sensible defaults

Version Compatibility

v0.1.0 config → v0.2.0 engine ✅ (100% compatible)
v0.1.0 config → v1.0.0 engine ✅ (100% compatible)

v0.2.0 config → v0.1.0 engine ⚠️  (new features ignored with warnings)
v1.0.0 config → v0.1.0 engine ⚠️  (new features ignored with warnings)

Deprecation Policy

Conductor follows semantic versioning (SemVer) for configuration:

  • Major version (x.0.0): May introduce breaking changes with migration tools
  • Minor version (0.x.0): Adds features in backward-compatible manner
  • Patch version (0.0.x): Bug fixes only, no config changes

Breaking changes require:

  1. Deprecation notice (version N)
  2. Deprecation period with warnings (version N+1)
  3. Removal with migration tool (version N+2, major bump)

Test Coverage

All v0.1.0 configuration examples are validated in CI/CD via tests/config_compatibility_test.rs:

  • CFG-001: Basic config.toml loads without errors
  • CFG-003: Minimal device-only configs use defaults
  • CFG-004: All trigger types parse correctly
  • CFG-005: All action types parse correctly
  • CFG-006: Complex sequences work
  • CFG-007: Multiple modes supported
  • CFG-009: Legacy v0.1.0 syntax always works
  • CFG-010: Invalid syntax produces clear error messages

Coverage Requirements:

  • 100% coverage for config parsing (src/config.rs)
  • All documentation examples must parse successfully
  • Regression tests for every released version

For complete details, see: Configuration Backward Compatibility Strategy


API Design

For detailed public API design (Phase 2+), see:

InputManager Architecture

Version: 3.0 Status: Stable Module: conductor-daemon/src/input_manager.rs

Overview

The InputManager is Conductor’s unified input handling system introduced in v3.0. It provides a single, cohesive interface for managing both MIDI and HID (game controller) input devices, producing a unified stream of protocol-agnostic InputEvent instances for processing by the mapping engine.

Key Features

  • Multi-Protocol Support: Seamlessly integrates MIDI and HID game controller inputs
  • Unified Event Stream: Single InputEvent channel for all input types
  • Flexible Device Selection: Choose MIDI-only, gamepad-only, or hybrid (both) modes
  • ID Range Separation: Non-overlapping ID ranges prevent conflicts (MIDI: 0-127, HID: 128-255)
  • Automatic Reconnection: Inherits robust reconnection logic from device managers
  • Thread Safety: Arc/Mutex patterns for safe concurrent access

Architecture Diagram

┌────────────────────────────────────────────────────────────────────┐
│  InputManager (Unified Input Layer)                               │
│  ┌──────────────────────────────────────────────────────────────┐ │
│  │  InputMode Selection                                         │ │
│  │  - MidiOnly: MIDI device only                                │ │
│  │  - GamepadOnly: Game controller only                         │ │
│  │  - Both: MIDI + Gamepad simultaneously (hybrid mode)         │ │
│  └──────────────────────────────────────────────────────────────┘ │
│                                                                    │
│  ┌───────────────────────┐        ┌──────────────────────────┐   │
│  │  MidiDeviceManager    │        │  GamepadDeviceManager    │   │
│  │  (midir)              │        │  (gilrs v0.10)           │   │
│  │                       │        │                          │   │
│  │  - MIDI I/O           │        │  - HID event polling     │   │
│  │  - Port management    │        │  - SDL2 mappings         │   │
│  │  - Auto-reconnect     │        │  - Device enumeration    │   │
│  └──────────┬────────────┘        └────────────┬─────────────┘   │
│             │                                   │                 │
│             │ MidiEvent                         │ gilrs::Event    │
│             │                                   │                 │
│             ▼                                   ▼                 │
│  ┌──────────────────────┐          ┌──────────────────────────┐  │
│  │  MIDI → InputEvent   │          │  HID → InputEvent        │  │
│  │  Converter           │          │  Converter               │  │
│  │  (convert_midi)      │          │  (gamepad_events)        │  │
│  └──────────┬───────────┘          └────────────┬─────────────┘  │
│             │                                   │                 │
│             └───────────────┬───────────────────┘                 │
│                             ▼                                     │
│                  ┌───────────────────────┐                        │
│                  │  Unified InputEvent   │                        │
│                  │  Stream (mpsc)        │                        │
│                  └───────────┬───────────┘                        │
└──────────────────────────────┼────────────────────────────────────┘
                               │
                               ▼
                    ┌────────────────────────┐
                    │  EventProcessor        │
                    │  (conductor-core)        │
                    │                        │
                    │  - Velocity detection  │
                    │  - Long press          │
                    │  - Double-tap          │
                    │  - Chord detection     │
                    └────────┬───────────────┘
                             │
                             ▼
                    ┌────────────────────────┐
                    │  MappingEngine         │
                    │  (conductor-core)        │
                    │                        │
                    │  - Trigger matching    │
                    │  - Action execution    │
                    └────────────────────────┘

InputMode Enum

The InputMode enum controls which input devices are active:

pub enum InputMode {
    /// Use MIDI device only
    MidiOnly,

    /// Use gamepad device only
    GamepadOnly,

    /// Use both MIDI and gamepad simultaneously (hybrid mode)
    Both,
}

Mode Selection Strategy

ModeMIDI ManagerGamepad ManagerUse Case
MidiOnly✅ Active❌ DisabledTraditional MIDI controller workflows
GamepadOnly❌ Disabled✅ ActiveGame controller macro setups
Both✅ Active✅ ActiveHybrid workflows (MIDI + gamepad)

ID Range Separation

To prevent conflicts between MIDI and HID inputs, Conductor uses non-overlapping ID ranges:

MIDI ID Range (0-127)

MIDI protocol uses 7-bit addressing for notes and control changes:

  • Notes: 0-127 (C-2 to G8)
  • Control Changes: 0-127 (CC0 to CC127)
  • Velocity: 0-127 (off to maximum)

Example: MIDI note 60 (Middle C) → InputEvent::PadPressed { pad: 60, ... }

HID ID Range (128-255)

Game controller buttons and axes use IDs starting at 128:

Button IDs (128-144)

Face Buttons:
  128 = South (A/Cross/B)
  129 = East (B/Circle/A)
  130 = West (X/Square/Y)
  131 = North (Y/Triangle/X)

D-Pad:
  132 = Up
  133 = Down
  134 = Left
  135 = Right

Shoulder Buttons:
  136 = Left Shoulder (L1/LB)
  137 = Right Shoulder (R1/RB)

Stick Clicks:
  138 = Left Thumb (L3)
  139 = Right Thumb (R3)

Menu Buttons:
  140 = Start (Options/+)
  141 = Select (Share/-)
  142 = Guide (Xbox/PS/Home)

Trigger Digital:
  143 = Left Trigger (L2/LT)
  144 = Right Trigger (R2/RT)

Encoder IDs (128-133)

Analog stick axes and triggers use encoder IDs:

Analog Sticks:
  128 = Left Stick X
  129 = Left Stick Y
  130 = Right Stick X
  131 = Right Stick Y

Trigger Analog:
  132 = Left Trigger (L2/LT)
  133 = Right Trigger (R2/RT)

Why Non-Overlapping Ranges?

  1. Conflict Prevention: MIDI note 60 and gamepad button never collide
  2. Unified Processing: EventProcessor handles both identically
  3. Simple Disambiguation: Check ID range to determine source protocol
  4. Future Expansion: Room for additional input types (256-65535)

Device Management

MidiDeviceManager

Location: conductor-daemon/src/midi_device.rs

Responsibilities:

  • Connect to MIDI input ports via midir
  • Emit MidiEvent instances (NoteOn, NoteOff, ControlChange, etc.)
  • Handle MIDI device disconnections and reconnections
  • Enumerate available MIDI ports

Event Flow:

MIDI Device → midir callback → MidiEvent → mpsc channel

GamepadDeviceManager

Location: conductor-daemon/src/gamepad_device.rs

Responsibilities:

  • Poll HID game controllers via gilrs (v0.10)
  • Use SDL2-compatible controller mappings
  • Emit gilrs events (ButtonPressed, AxisChanged, etc.)
  • Handle gamepad disconnections and reconnections
  • Enumerate connected gamepads

Event Flow:

Gamepad → gilrs::Gilrs::next_event() → gilrs::Event → gamepad_events → InputEvent → mpsc channel

gilrs Integration

Conductor v3.0 uses gilrs v0.10 for HID game controller support:

  • SDL2 Compatibility: Supports SDL_GameController mapping database
  • Cross-Platform: Works on macOS, Linux, Windows
  • Controller Support: Xbox, PlayStation, Nintendo Switch Pro, generic gamepads
  • Polling Architecture: 1ms polling interval for low latency
  • Event Types: ButtonPressed, ButtonReleased, AxisChanged, Connected, Disconnected

Event Normalization

MIDI → InputEvent Conversion

The convert_midi_to_input() function maps MIDI protocol events to InputEvent:

fn convert_midi_to_input(midi_event: MidiEvent) -> InputEvent {
    match midi_event {
        MidiEvent::NoteOn { note, velocity, .. } =>
            InputEvent::PadPressed { pad: note, velocity, time: now },

        MidiEvent::NoteOff { note, .. } =>
            InputEvent::PadReleased { pad: note, time: now },

        MidiEvent::ControlChange { cc, value, .. } =>
            InputEvent::EncoderTurned { encoder: cc, value, time: now },

        MidiEvent::Aftertouch { pressure, .. } =>
            InputEvent::Aftertouch { pressure, time: now },

        MidiEvent::PitchBend { value, .. } =>
            InputEvent::PitchBend { value, time: now },

        // ... other mappings
    }
}

Key Insight: This conversion happens in a spawned tokio task, allowing the MIDI device manager to remain protocol-agnostic while the InputManager handles unification.

HID → InputEvent Conversion

The gamepad_events module provides three converter functions:

// Button press: gilrs::Event → InputEvent::PadPressed
pub fn button_pressed_to_input(
    button: gilrs::Button,
    gamepad_id: gilrs::GamepadId
) -> InputEvent {
    InputEvent::PadPressed {
        pad: button_to_id(button), // Maps to 128-144 range
        velocity: 100, // Default velocity for digital buttons
        time: Instant::now(),
    }
}

// Button release: gilrs::Event → InputEvent::PadReleased
pub fn button_released_to_input(
    button: gilrs::Button,
    gamepad_id: gilrs::GamepadId
) -> InputEvent {
    InputEvent::PadReleased {
        pad: button_to_id(button),
        time: Instant::now(),
    }
}

// Analog axis: gilrs::Event → InputEvent::EncoderTurned
pub fn axis_changed_to_input(
    axis: gilrs::Axis,
    value: f32, // -1.0 to 1.0
    gamepad_id: gilrs::GamepadId
) -> InputEvent {
    InputEvent::EncoderTurned {
        encoder: axis_to_encoder_id(axis), // Maps to 128-133 range
        value: normalize_axis_value(value), // Convert to 0-127
        time: Instant::now(),
    }
}

Key APIs

Creating an InputManager

use conductor_daemon::input_manager::{InputManager, InputMode};

// MIDI-only mode
let manager = InputManager::new(
    Some("Maschine Mikro MK3".to_string()),
    true, // auto_reconnect
    InputMode::MidiOnly
);

// Gamepad-only mode
let gamepad_manager = InputManager::new(
    None,
    true,
    InputMode::GamepadOnly
);

// Hybrid mode (both MIDI and gamepad)
let hybrid_manager = InputManager::new(
    Some("Maschine Mikro MK3".to_string()),
    true,
    InputMode::Both
);

Connecting to Devices

use tokio::sync::mpsc;
use conductor_core::events::InputEvent;
use conductor_daemon::daemon::DaemonCommand;

let (event_tx, mut event_rx) = mpsc::channel::<InputEvent>(1024);
let (command_tx, mut command_rx) = mpsc::channel::<DaemonCommand>(32);

// Connect returns status message or error
match manager.connect(event_tx, command_tx) {
    Ok(status) => println!("Connected: {}", status),
    Err(e) => eprintln!("Connection failed: {}", e),
}

// Example output:
// "MIDI: Maschine Mikro MK3 (port 2) | Gamepad: Xbox 360 Controller (ID GamepadId(0))"

Polling Events

// Process unified event stream
while let Some(event) = event_rx.recv().await {
    match event {
        InputEvent::PadPressed { pad, velocity, .. } => {
            if pad < 128 {
                println!("MIDI pad {} pressed (vel: {})", pad, velocity);
            } else {
                println!("Gamepad button {} pressed", pad);
            }
        }
        InputEvent::EncoderTurned { encoder, value, .. } => {
            if encoder < 128 {
                println!("MIDI CC {} = {}", encoder, value);
            } else {
                println!("Gamepad axis {} = {}", encoder, value);
            }
        }
        _ => {}
    }
}

Enumerating Gamepads

// List all connected gamepads
match InputManager::list_gamepads() {
    Ok(gamepads) => {
        for (id, name, uuid) in gamepads {
            println!("Gamepad: {:?} - {} (UUID: {})", id, name, uuid);
        }
    }
    Err(e) => eprintln!("Failed to list gamepads: {}", e),
}

// Get gamepads managed by this InputManager
let connected = manager.get_connected_gamepads();
for (id, name) in connected {
    println!("Active: {} ({})", name, id);
}

Checking Connection Status

// Check if any device is connected
if manager.is_connected() {
    println!("At least one device connected");
}

// Get detailed status
let (midi_connected, gamepad_connected) = manager.get_status();
println!("MIDI: {}, Gamepad: {}", midi_connected, gamepad_connected);

Disconnecting

// Graceful shutdown of all devices
manager.disconnect();

Hybrid Mode Architecture

When using InputMode::Both, the InputManager creates both MIDI and gamepad device managers and merges their event streams:

┌─────────────────────────────────────────────┐
│  InputManager::connect()                    │
│                                             │
│  1. Connect MIDI device                     │
│     - Create midi_event_rx channel          │
│     - Spawn converter task:                 │
│       while let Some(midi_evt) = rx.recv() {│
│         send(convert_midi_to_input(midi))   │
│       }                                     │
│                                             │
│  2. Connect Gamepad device                  │
│     - Directly sends InputEvent             │
│     - No conversion needed                  │
│                                             │
│  3. Both tasks send to same event_tx        │
│     - Unified mpsc::Sender<InputEvent>      │
│     - EventProcessor receives single stream │
└─────────────────────────────────────────────┘

Hybrid Mode Event Flow Example

// Time T0: MIDI note 60 pressed
InputEvent::PadPressed { pad: 60, velocity: 100, time: T0 }

// Time T1: Gamepad A button pressed (ID 128)
InputEvent::PadPressed { pad: 128, velocity: 100, time: T1 }

// Time T2: MIDI CC 7 changed
InputEvent::EncoderTurned { encoder: 7, value: 64, time: T2 }

// Time T3: Gamepad left stick X moved
InputEvent::EncoderTurned { encoder: 128, value: 90, time: T3 }

All events flow through the same channel, preserving temporal ordering and enabling hybrid workflows like:

  • MIDI pads for velocity-sensitive drumming
  • Gamepad sticks for smooth parameter sweeps
  • Gamepad buttons for mode switching
  • MIDI encoder for fine-grained control

Thread Safety

The InputManager and its device managers use Rust’s ownership system and Arc/Mutex patterns for safe concurrent access:

pub struct InputManager {
    midi_manager: Option<MidiDeviceManager>,
    gamepad_manager: Option<GamepadDeviceManager>,
    mode: InputMode,
}

pub struct GamepadDeviceManager {
    gamepad_id: Arc<Mutex<Option<gilrs::GamepadId>>>,
    gamepad_name: Arc<Mutex<Option<String>>>,
    is_connected: Arc<AtomicBool>,
    stop_polling: Arc<AtomicBool>,
    polling_thread: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
}

Synchronization Mechanisms

  • Arc<Mutex>: Shared mutable state (gamepad ID, name, thread handles)
  • Arc: Lock-free connection status and stop flags
  • mpsc channels: Lock-free event passing between threads
  • tokio::spawn: Async task for MIDI conversion
  • std::thread: Blocking thread for gamepad polling (gilrs is synchronous)

Error Handling

The InputManager provides graceful degradation in hybrid mode:

// In InputMode::Both:
match midi_mgr.connect(...) {
    Ok(_) => { /* MIDI connected */ },
    Err(e) => {
        warn!("Failed to connect MIDI (continuing with gamepad): {}", e);
        // Gamepad connection attempt continues
    }
}

match gamepad_mgr.connect(...) {
    Ok(_) => { /* Gamepad connected */ },
    Err(e) => {
        warn!("Failed to connect gamepad (continuing with MIDI): {}", e);
        // Return OK if MIDI connected
    }
}

// Only fail if BOTH connections failed
if status_messages.is_empty() {
    return Err("No input devices could be connected".to_string());
}

Performance Characteristics

MetricMIDIGamepadHybrid
Event Latency<1ms<2ms<2ms
Polling RateCallback-driven1ms (1000Hz)Both
CPU Usage (idle)<0.1%<0.5%<0.6%
Memory Overhead~200KB~1MB~1.2MB
Thread Count1 (callback)1 (polling)2

Code Examples

Example 1: MidiOnly Mode

use conductor_daemon::input_manager::{InputManager, InputMode};
use tokio::sync::mpsc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    let mut manager = InputManager::new(
        Some("Maschine Mikro MK3".to_string()),
        true,
        InputMode::MidiOnly
    );

    manager.connect(event_tx, command_tx)?;

    while let Some(event) = event_rx.recv().await {
        println!("MIDI Event: {:?}", event);
    }

    Ok(())
}

Example 2: GamepadOnly Mode

use conductor_daemon::input_manager::{InputManager, InputMode};
use tokio::sync::mpsc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    let mut manager = InputManager::new(
        None, // No MIDI device
        true,
        InputMode::GamepadOnly
    );

    manager.connect(event_tx, command_tx)?;

    while let Some(event) = event_rx.recv().await {
        println!("Gamepad Event: {:?}", event);
    }

    Ok(())
}

Example 3: Hybrid Mode (Both)

use conductor_daemon::input_manager::{InputManager, InputMode};
use conductor_core::events::InputEvent;
use tokio::sync::mpsc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _) = mpsc::channel(32);

    let mut manager = InputManager::new(
        Some("Maschine Mikro MK3".to_string()),
        true,
        InputMode::Both
    );

    let status = manager.connect(event_tx, command_tx)?;
    println!("Connected: {}", status);

    while let Some(event) = event_rx.recv().await {
        match event {
            InputEvent::PadPressed { pad, velocity, .. } => {
                if pad < 128 {
                    println!("MIDI pad {} pressed (velocity: {})", pad, velocity);
                } else {
                    println!("Gamepad button {} pressed", pad - 128);
                }
            }
            InputEvent::EncoderTurned { encoder, value, .. } => {
                if encoder < 128 {
                    println!("MIDI CC {} changed to {}", encoder, value);
                } else {
                    println!("Gamepad axis {} = {}", encoder - 128, value);
                }
            }
            _ => {}
        }
    }

    Ok(())
}

Example 4: Device Enumeration

use conductor_daemon::input_manager::InputManager;

fn main() -> Result<(), String> {
    // List all available gamepads
    println!("Available gamepads:");
    let gamepads = InputManager::list_gamepads()?;

    for (id, name, uuid) in gamepads {
        println!("  - {:?}: {} (UUID: {})", id, name, uuid);
    }

    Ok(())
}

Integration with EventProcessor

The InputManager produces InputEvent instances that flow directly into the EventProcessor:

// conductor-core/src/event_processor.rs
impl EventProcessor {
    pub fn process(&mut self, event: InputEvent) -> Vec<ProcessedEvent> {
        match event {
            InputEvent::PadPressed { pad, velocity, time } => {
                // Detect velocity levels, long press, double-tap, chords
                self.process_pad_press(pad, velocity, time)
            }
            InputEvent::EncoderTurned { encoder, value, time } => {
                // Detect encoder direction, acceleration
                self.process_encoder(encoder, value, time)
            }
            // ... other event types
        }
    }
}

The EventProcessor doesn’t care if the event came from MIDI or a gamepad—it processes all InputEvent instances identically using the ID range to determine device type when needed.

Testing

The InputManager includes comprehensive unit tests:

# Run InputManager tests
cargo test -p conductor-daemon input_manager

# Test specific mode creation
cargo test -p conductor-daemon test_input_manager_creation_midi_only
cargo test -p conductor-daemon test_input_manager_creation_gamepad_only
cargo test -p conductor-daemon test_input_manager_creation_both

# Test MIDI → InputEvent conversion
cargo test -p conductor-daemon test_convert_midi_note_on
cargo test -p conductor-daemon test_convert_midi_cc

Future Enhancements

Potential future improvements to the InputManager:

  1. Multiple Gamepad Support: Connect multiple gamepads simultaneously
  2. Device Prioritization: Configurable priority when events collide
  3. Custom ID Ranges: Allow users to remap ID ranges via config
  4. Hot-Swapping: Dynamic device addition/removal without restart
  5. Input Filtering: Filter specific buttons/axes before EventProcessor
  6. Virtual Devices: Create virtual MIDI/gamepad devices for testing

Terminology

Game Controllers (HID): The standard term used throughout Conductor documentation for HID input devices. This includes:

  • Gamepads: Xbox, PlayStation, Nintendo Switch Pro controllers (primary examples)
  • Joysticks: Flight sticks, arcade sticks
  • Racing Wheels: Steering wheel controllers
  • HOTAS: Hands-On Throttle-And-Stick systems
  • Custom Controllers: DIY Arduino-based controllers, specialized input devices

All SDL2-compatible HID game controllers are supported via the gilrs library.

Summary

The InputManager is a critical architectural component that:

  1. Unifies MIDI and HID inputs into a single event stream
  2. Separates ID ranges to prevent conflicts (0-127 vs 128-255)
  3. Abstracts protocol differences behind InputEvent
  4. Enables hybrid workflows with both MIDI and gamepad devices
  5. Provides flexible device selection via InputMode

This design allows Conductor to support a wide range of input devices while maintaining a clean, protocol-agnostic processing pipeline.

Plugin Development

Conductor v2.3 introduces a powerful plugin architecture that allows third-party developers to create custom actions through dynamically loaded shared libraries.

Overview

Plugins extend Conductor’s functionality by implementing the ActionPlugin trait. They can:

  • Execute custom logic when MIDI events occur
  • Access event metadata (velocity, mode, timestamp)
  • Request specific capabilities (network, filesystem, etc.)
  • Be loaded/unloaded dynamically without restart
  • Be managed through the GUI Plugin Manager

Quick Start

1. Create a New Plugin Project

cargo new --lib my_plugin
cd my_plugin

2. Configure Cargo.toml

[package]
name = "conductor-my-plugin"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]  # Required for dynamic loading

[dependencies]
conductor-core = { path = "../conductor-core", features = ["plugin"] }
serde_json = "1.0"

3. Implement the ActionPlugin Trait

use conductor_core::plugin::{ActionPlugin, Capability, TriggerContext};
use serde_json::Value;
use std::error::Error;

pub struct MyPlugin;

impl ActionPlugin for MyPlugin {
    fn name(&self) -> &str {
        "my_plugin"
    }

    fn version(&self) -> &str {
        "1.0.0"
    }

    fn description(&self) -> &str {
        "My custom Conductor plugin"
    }

    fn author(&self) -> &str {
        "Your Name"
    }

    fn license(&self) -> &str {
        "MIT"
    }

    fn capabilities(&self) -> Vec<Capability> {
        vec![Capability::Network]  // Request network access
    }

    fn execute(&mut self, params: Value, context: TriggerContext) -> Result<(), Box<dyn Error>> {
        // Your plugin logic here
        let velocity = context.velocity.unwrap_or(0);
        eprintln!("Plugin executed with velocity: {}", velocity);
        Ok(())
    }
}

#[no_mangle]
pub extern "C" fn _create_plugin() -> *mut dyn ActionPlugin {
    Box::into_raw(Box::new(MyPlugin))
}

4. Create Plugin Manifest

Create plugin.toml in your plugin directory:

[plugin]
name = "my_plugin"
version = "1.0.0"
description = "My custom Conductor plugin"
author = "Your Name"
homepage = "https://github.com/yourname/conductor-my-plugin"
license = "MIT"
type = "action"
binary = "libconductor_my_plugin.dylib"  # .so on Linux, .dll on Windows
checksum = ""  # Optional SHA256 checksum

[plugin.capabilities]
network = true

5. Build and Install

# Build the plugin
cargo build --release

# Install to Conductor plugins directory
mkdir -p ~/.conductor/plugins/my_plugin
cp target/release/libconductor_my_plugin.dylib ~/.conductor/plugins/my_plugin/
cp plugin.toml ~/.conductor/plugins/my_plugin/

6. Use in Configuration

[[modes.mappings]]
trigger = { Note = { note = 60 } }
action = { Plugin = {
    plugin = "my_plugin",
    params = {
        "custom_param": "value"
    }
}}

Capability System

Plugins request capabilities to access system resources. Conductor uses a risk-level based security model:

Capability Types

CapabilityRisk LevelDescription
NetworkLowHTTP requests, websockets
AudioLowAudio device access
MidiLowMIDI device access
FilesystemMediumFile read/write
SubprocessHighExecute shell commands
SystemControlHighSystem-level control

Risk Levels

  • Low (🟢): Auto-granted by default, considered safe
  • Medium (🟡): Requires user approval
  • High (🔴): Requires explicit user approval with warning

Requesting Capabilities

fn capabilities(&self) -> Vec<Capability> {
    vec![
        Capability::Network,      // Auto-granted
        Capability::Filesystem,   // Requires approval
    ]
}

Plugin Lifecycle

  1. Discovery: Conductor scans ~/.conductor/plugins/ for plugin.toml files
  2. Load: Binary is loaded via libloading, plugin instance created
  3. Initialize: initialize() method called (if implemented)
  4. Execute: execute() called for each MIDI event
  5. Shutdown: shutdown() method called before unload (if implemented)
  6. Unload: Plugin removed from memory

Advanced Features

Initialization and Shutdown

impl ActionPlugin for MyPlugin {
    fn initialize(&mut self) -> Result<(), Box<dyn Error>> {
        eprintln!("Plugin initializing...");
        // Setup code here
        Ok(())
    }

    fn shutdown(&mut self) -> Result<(), Box<dyn Error>> {
        eprintln!("Plugin shutting down...");
        // Cleanup code here
        Ok(())
    }
}

Accessing Context

fn execute(&mut self, params: Value, context: TriggerContext) -> Result<(), Box<dyn Error>> {
    let velocity = context.velocity.unwrap_or(0);
    let mode = context.current_mode.unwrap_or(0);
    let timestamp = context.timestamp;

    eprintln!("Velocity: {}, Mode: {}", velocity, mode);
    Ok(())
}

Parameter Parsing

fn execute(&mut self, params: Value, _context: TriggerContext) -> Result<(), Box<dyn Error>> {
    let url = params["url"]
        .as_str()
        .ok_or("Missing 'url' parameter")?;

    let method = params["method"]
        .as_str()
        .unwrap_or("GET");

    // Use parameters...
    Ok(())
}

Testing Plugins

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_plugin_metadata() {
        let plugin = MyPlugin;
        assert_eq!(plugin.name(), "my_plugin");
        assert_eq!(plugin.version(), "1.0.0");
    }

    #[test]
    fn test_plugin_execute() {
        let mut plugin = MyPlugin;
        let params = serde_json::json!({
            "param1": "value1"
        });
        let context = TriggerContext {
            velocity: Some(127),
            current_mode: Some(0),
            timestamp: std::time::Instant::now(),
        };

        assert!(plugin.execute(params, context).is_ok());
    }
}

GUI Plugin Manager

Plugins can be managed through the GUI:

  1. Discover: Scan for new plugins
  2. Load/Unload: Control plugin lifecycle
  3. Enable/Disable: Toggle plugin availability
  4. Grant/Revoke: Manage capabilities
  5. Statistics: View execution counts and latency

Example Plugins

HTTP Request Plugin

See examples/http-plugin/ for a complete example that demonstrates:

  • Making HTTP requests (GET, POST, PUT, DELETE)
  • Custom headers
  • JSON body
  • Velocity substitution
  • Error handling

Creating a Simple Logger Plugin

use std::fs::OpenOptions;
use std::io::Write;

pub struct LoggerPlugin {
    log_file: String,
}

impl LoggerPlugin {
    pub fn new() -> Self {
        Self {
            log_file: "/tmp/conductor.log".to_string(),
        }
    }
}

impl ActionPlugin for LoggerPlugin {
    // ... metadata methods ...

    fn capabilities(&self) -> Vec<Capability> {
        vec![Capability::Filesystem]
    }

    fn execute(&mut self, params: Value, context: TriggerContext) -> Result<(), Box<dyn Error>> {
        let message = params["message"].as_str().unwrap_or("Event triggered");
        let velocity = context.velocity.unwrap_or(0);

        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.log_file)?;

        writeln!(file, "[v={}] {}", velocity, message)?;
        Ok(())
    }
}

Best Practices

  1. Error Handling: Always return proper errors, never panic
  2. Performance: Keep execute() fast (<10ms ideal)
  3. Resource Cleanup: Implement shutdown() for cleanup
  4. Documentation: Document all parameters in README
  5. Testing: Write tests for all functionality
  6. Security: Request minimum necessary capabilities
  7. Logging: Use eprintln!() for debug output

Distribution

Binary Naming

  • macOS: libmyplugin.dylib
  • Linux: libmyplugin.so
  • Windows: myplugin.dll

Directory Structure

~/.conductor/plugins/
  └── my_plugin/
      ├── plugin.toml
      └── libconductor_my_plugin.dylib

Checksum Verification

Generate SHA256 for security:

shasum -a 256 target/release/libconductor_my_plugin.dylib

Add to plugin.toml:

[plugin]
checksum = "abc123..."

Troubleshooting

Plugin Not Discovered

  • Check plugin.toml is valid TOML
  • Verify ~/.conductor/plugins/ directory exists
  • Ensure binary name matches in manifest

Plugin Fails to Load

  • Check binary is compiled for correct platform
  • Verify crate-type = ["cdylib"] in Cargo.toml
  • Ensure _create_plugin symbol is exported

Capability Denied

  • Check risk level in GUI Plugin Manager
  • Grant capability manually if needed
  • Consider using lower-risk alternatives

Further Reading

Community Plugins

Share your plugins with the community! Submit a PR to add your plugin to the Plugin Registry.

WASM Plugins

Since: v2.5 Status: Production-ready

Conductor’s WASM (WebAssembly) plugin system provides a secure, sandboxed environment for running third-party plugins with enterprise-grade safety guarantees.

Overview

WASM plugins offer several advantages over native plugins:

  • Security: Sandboxed execution with no direct system access
  • Portability: Write once, run anywhere (same binary on macOS/Linux/Windows)
  • Safety: Memory-safe execution, no undefined behavior
  • Isolation: Resource limits prevent runaway plugins
  • Verification: Cryptographic signatures ensure plugin integrity

Architecture

┌─────────────────────────────────────────────────────┐
│  Conductor Core                                       │
│  ┌───────────────────────────────────────────────┐ │
│  │  WASM Runtime (wasmtime)                      │ │
│  │  ┌─────────────────────────────────────────┐ │ │
│  │  │  Plugin Instance                        │ │ │
│  │  │  - Fuel metering (CPU limits)           │ │ │
│  │  │  - Memory limits (128 MB default)       │ │ │
│  │  │  - WASI filesystem sandboxing           │ │ │
│  │  │  - Capability system (network, etc.)    │ │ │
│  │  └─────────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────┘ │
│                                                     │
│  ┌───────────────────────────────────────────────┐ │
│  │  Signature Verification (v2.7)                │ │
│  │  - Ed25519 digital signatures                 │ │
│  │  - SHA-256 integrity checking                 │ │
│  │  - Trust management                           │ │
│  └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

Quick Comparison

FeatureNative Plugins (v2.3)WASM Plugins (v2.5+)
PlatformPlatform-specific (.dylib/.so/.dll)Universal (.wasm)
SecurityFull system accessSandboxed
Memory SafetyDepends on languageGuaranteed
Resource LimitsNoneCPU, memory, I/O
InstallationManual copySingle file
VerificationSHA256 checksumCryptographic signatures
LanguagesRust, C, C++Rust, C, C++, Go, Swift, Zig
Startup TimeFast (~1ms)Fast (~10ms)
Runtime OverheadNoneMinimal (~5%)

Plugin Lifecycle

  1. Load - WASM module loaded and validated
  2. Verify (v2.7) - Cryptographic signature checked
  3. Initialize - Plugin setup, capabilities granted
  4. Execute - Plugin called for MIDI events
  5. Shutdown - Cleanup and resource release
  6. Unload - Module removed from memory

Security Features

Resource Limiting (v2.7)

Fuel Metering:

  • CPU execution limited to prevent infinite loops
  • Default: 100 million instructions (~100ms)
  • Configurable per-plugin

Memory Limits:

  • Default: 128 MB
  • Prevents memory exhaustion
  • Enforced by WASM runtime

Table Growth Limits:

  • Prevents unbounded table allocation
  • Maximum elements configurable

Filesystem Sandboxing (v2.7)

Directory Preopening:

  • WASI filesystem isolated to specific directories
  • Default: ~/.local/share/conductor/plugin-data/ (Linux)
  • Default: ~/Library/Application Support/conductor/plugin-data/ (macOS)
  • Plugins cannot access files outside sandbox

Cryptographic Signatures (v2.7)

Ed25519 Digital Signatures:

  • Industry-standard cryptography
  • 256-bit security level
  • Signature file: <plugin>.wasm.sig

Three-Tier Trust Model:

  1. Unsigned - Development only (optional signatures)
  2. Self-Signed - Valid signature from any key
  3. Trusted Keys - Signature must match trusted key list

Capability System

Plugins request capabilities to access system resources:

CapabilityRiskDescription
Network🟢 LowHTTP requests, WebSocket
Filesystem🟡 MediumRead/write files (sandboxed)
Subprocess🔴 HighExecute shell commands
SystemControl🔴 HighSystem-level operations

Risk Levels:

  • 🟢 Low: Auto-granted
  • 🟡 Medium: User approval required
  • 🔴 High: Explicit approval with warning

Example: Spotify Plugin

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct SpotifyParams {
    action: String,  // "play", "pause", "next", "previous"
}

#[no_mangle]
pub extern "C" fn init() {
    // Plugin initialization
}

#[no_mangle]
pub extern "C" fn execute(params_json: *const u8, params_len: usize) -> i32 {
    // Parse parameters
    let params_bytes = unsafe {
        std::slice::from_raw_parts(params_json, params_len)
    };
    let params: SpotifyParams = serde_json::from_slice(params_bytes)
        .expect("Invalid params");

    // Control Spotify via Web API
    match params.action.as_str() {
        "play" => spotify_play(),
        "pause" => spotify_pause(),
        "next" => spotify_next(),
        "previous" => spotify_previous(),
        _ => return 1, // Error
    }

    0 // Success
}

Building WASM Plugins

Prerequisites

# Add WASM target
rustup target add wasm32-wasip1

Project Setup

# Cargo.toml
[package]
name = "my-wasm-plugin"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Build

cargo build --target wasm32-wasip1 --release

Output: target/wasm32-wasip1/release/my_wasm_plugin.wasm

Plugin Distribution

my-plugin/
├── my_plugin.wasm           # WASM binary
├── my_plugin.wasm.sig       # Cryptographic signature (v2.7)
├── README.md                # Documentation
└── LICENSE                  # License file

Signing Your Plugin (v2.7)

# Generate keypair (one-time)
conductor-sign generate-key ~/.conductor/my-plugin-key

# Sign plugin
conductor-sign sign my_plugin.wasm ~/.conductor/my-plugin-key \
  --name "Your Name" \
  --email "you@example.com"

# Verify signature
conductor-sign verify my_plugin.wasm

Installation

User Installation

# Copy plugin to Conductor directory
mkdir -p ~/.conductor/wasm-plugins/
cp my_plugin.wasm ~/.conductor/wasm-plugins/
cp my_plugin.wasm.sig ~/.conductor/wasm-plugins/  # v2.7

Configuration

# config.toml
[[modes.mappings]]
trigger = { Note = { note = 60 } }
action = { WasmPlugin = {
    path = "~/.conductor/wasm-plugins/my_plugin.wasm",
    params = {
        "action": "play"
    }
}}

Official Plugins

Conductor provides several official WASM plugins:

Spotify Control

File: conductor_wasm_spotify.wasm Capabilities: Network Actions: play, pause, next, previous, volume, shuffle, repeat

OBS Studio Control

File: conductor_wasm_obs_control.wasm Capabilities: Network Actions: scene switching, recording, streaming, mute/unmute

System Utilities

File: conductor_wasm_system_utils.wasm Capabilities: SystemControl Actions: lock screen, sleep, notifications, brightness

Performance

Typical Execution Times:

  • Plugin load: ~10ms (one-time)
  • First execution: ~5ms (JIT compilation)
  • Subsequent executions: <1ms
  • Memory overhead: ~2-5 MB per plugin

Optimization Tips:

  • Keep plugins small (<1 MB ideal)
  • Minimize allocations in hot paths
  • Use wasm-opt for size/speed optimization
  • Profile with wasmtime::Store::fuel_consumed()

Troubleshooting

Plugin Fails to Load

Check WASM target:

file my_plugin.wasm
# Should show: WebAssembly (wasm) binary module version 0x1

Verify WASI compatibility:

wasm-objdump -x my_plugin.wasm | grep -A5 "Import"
# Should show WASI imports like wasi_snapshot_preview1

Out of Fuel Error

Increase fuel limit in configuration:

let mut config = WasmConfig::default();
config.max_fuel = 200_000_000;  // 200M instructions

Memory Limit Exceeded

Increase memory limit:

config.max_memory_bytes = 256 * 1024 * 1024;  // 256 MB

Signature Verification Failed

# Verify signature manually
conductor-sign verify my_plugin.wasm

# Check if key is trusted
conductor-sign trust list

# Add key to trusted list
conductor-sign trust add <public-key-hex> "Plugin Author"

Next Steps

Version History

  • v2.5 - Initial WASM plugin runtime
  • v2.6 - Example plugins (Spotify, OBS, System Utils)
  • v2.7 - Security hardening:
    • Resource limiting (fuel, memory, tables)
    • Directory preopening (filesystem sandboxing)
    • Plugin signing/verification (Ed25519)

Further Reading

WASM Plugin Development

This guide walks you through creating a WASM plugin for Conductor from scratch.

Prerequisites

Install Rust WASM Target

rustup target add wasm32-wasip1

Verify Installation

rustup target list | grep wasm32-wasip1
# Should show: wasm32-wasip1 (installed)

Creating Your First Plugin

1. Project Setup

# Create new library project
cargo new --lib my-midi-plugin
cd my-midi-plugin

2. Configure Cargo.toml

[package]
name = "my-midi-plugin"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]  # Required for WASM

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[profile.release]
opt-level = "z"     # Optimize for size
lto = true          # Link-time optimization
codegen-units = 1   # Better optimization
strip = true        # Remove debug symbols

3. Implement Plugin Logic

// src/lib.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct PluginParams {
    message: String,
}

#[derive(Serialize)]
struct PluginResult {
    success: bool,
    output: String,
}

/// Initialize plugin (called once on load)
#[no_mangle]
pub extern "C" fn init() {
    eprintln!("Plugin initialized");
}

/// Execute plugin action
#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    // Parse input parameters
    let params_bytes = unsafe {
        std::slice::from_raw_parts(params_ptr, params_len)
    };

    let params: PluginParams = match serde_json::from_slice(params_bytes) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("Failed to parse params: {}", e);
            return 1;  // Error code
        }
    };

    // Plugin logic
    eprintln!("Received message: {}", params.message);

    // Return success
    0
}

/// Cleanup (called before unload)
#[no_mangle]
pub extern "C" fn shutdown() {
    eprintln!("Plugin shutting down");
}

4. Build the Plugin

cargo build --target wasm32-wasip1 --release

Output file: target/wasm32-wasip1/release/my_midi_plugin.wasm

5. Test the Plugin

Create test_config.toml:

[[modes]]
name = "Default"

[[modes.mappings]]
trigger = { Note = { note = 60 } }  # Middle C
action = { WasmPlugin = {
    path = "target/wasm32-wasip1/release/my_midi_plugin.wasm",
    params = {
        "message": "Hello from MIDI!"
    }
}}

Advanced Examples

Example 1: HTTP Request Plugin

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct HttpParams {
    url: String,
    method: String,
}

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    let params_bytes = unsafe {
        std::slice::from_raw_parts(params_ptr, params_len)
    };

    let params: HttpParams = serde_json::from_slice(params_bytes)
        .expect("Invalid params");

    // Note: Requires Network capability
    // This is a simplified example - real implementation would use reqwest
    eprintln!("Making {} request to {}", params.method, params.url);

    0
}

Required Capability: Network

Example 2: File Logger Plugin

use std::fs::OpenOptions;
use std::io::Write;

#[derive(Deserialize)]
struct LogParams {
    message: String,
    level: String,  // "info", "warn", "error"
}

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    let params_bytes = unsafe {
        std::slice::from_raw_parts(params_ptr, params_len)
    };

    let params: LogParams = serde_json::from_slice(params_bytes)
        .expect("Invalid params");

    // Write to sandboxed directory
    // Path: ~/Library/Application Support/conductor/plugin-data/
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open("/plugin.log")  // Sandboxed path
        .expect("Failed to open log file");

    let timestamp = chrono::Utc::now().to_rfc3339();
    writeln!(file, "[{}] {}: {}",
        timestamp, params.level, params.message)
        .expect("Failed to write log");

    0
}

Required Capability: Filesystem

Example 3: Velocity-Responsive Plugin

#[derive(Deserialize)]
struct VelocityParams {
    action: String,
}

#[derive(Deserialize)]
struct TriggerContext {
    velocity: Option<u8>,
    mode: Option<u8>,
}

#[no_mangle]
pub extern "C" fn execute_with_context(
    params_ptr: *const u8,
    params_len: usize,
    context_ptr: *const u8,
    context_len: usize,
) -> i32 {
    let params_bytes = unsafe {
        std::slice::from_raw_parts(params_ptr, params_len)
    };
    let context_bytes = unsafe {
        std::slice::from_raw_parts(context_ptr, context_len)
    };

    let params: VelocityParams = serde_json::from_slice(params_bytes)
        .expect("Invalid params");
    let context: TriggerContext = serde_json::from_slice(context_bytes)
        .expect("Invalid context");

    let velocity = context.velocity.unwrap_or(0);

    // Adjust action based on velocity
    match velocity {
        0..=40 => eprintln!("Soft press: {}", params.action),
        41..=80 => eprintln!("Medium press: {}", params.action),
        81..=127 => eprintln!("Hard press: {}", params.action),
    }

    0
}

Capability Declaration

Declaring Capabilities

Capabilities are declared via a metadata function:

#[no_mangle]
pub extern "C" fn capabilities() -> *const u8 {
    let caps = vec!["Network", "Filesystem"];
    let json = serde_json::to_string(&caps).unwrap();
    let boxed = Box::new(json.into_bytes());
    Box::into_raw(boxed) as *const u8
}

Available Capabilities

// Low risk - auto-granted
"Network"      // HTTP requests, WebSocket
"Audio"        // Audio device access
"Midi"         // MIDI device access

// Medium risk - user approval
"Filesystem"   // File read/write (sandboxed)

// High risk - explicit approval
"Subprocess"   // Shell command execution
"SystemControl" // System-level operations

Resource Limits

Fuel (CPU) Limits

Plugins are limited by “fuel” (instruction count):

// Conductor automatically limits plugins to 100M instructions
// This prevents infinite loops and excessive CPU usage

// Check remaining fuel (from plugin side):
#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    // Your plugin code...

    // Conductor will automatically terminate if fuel runs out
    0
}

Default: 100,000,000 instructions (~100ms execution time)

Memory Limits

Default: 128 MB per plugin

// Conductor enforces memory limits automatically
// Allocations beyond the limit will fail

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    // Be mindful of memory allocations
    let large_buffer = vec![0u8; 1024 * 1024];  // 1 MB - OK
    // let huge_buffer = vec![0u8; 200 * 1024 * 1024];  // 200 MB - Would fail

    0
}

Filesystem Sandbox

Plugins with Filesystem capability can only access:

macOS: ~/Library/Application Support/conductor/plugin-data/ Linux: ~/.local/share/conductor/plugin-data/ Windows: %APPDATA%\conductor\plugin-data\

// These paths are relative to the sandbox root:
let ok_path = "/my-data.json";          // OK - sandboxed
let ok_path2 = "/subdir/file.txt";      // OK - sandboxed
// let bad_path = "/etc/passwd";         // BLOCKED - outside sandbox
// let bad_path2 = "../../../etc/passwd"; // BLOCKED - path traversal

Optimization

Size Optimization

# Cargo.toml
[profile.release]
opt-level = "z"     # Optimize for size
lto = true
codegen-units = 1
strip = true
panic = "abort"     # Smaller panic handling

Post-Build Optimization

# Install wasm-opt
cargo install wasm-opt

# Optimize WASM binary
wasm-opt -Oz \
  target/wasm32-wasip1/release/my_plugin.wasm \
  -o target/wasm32-wasip1/release/my_plugin_opt.wasm

# Check size reduction
ls -lh target/wasm32-wasip1/release/*.wasm

Performance Tips

  1. Minimize allocations in hot paths

    // Bad: allocates on every call
    fn process(data: &str) -> String {
        format!("Processed: {}", data)
    }
    
    // Good: reuse buffer
    fn process(data: &str, buffer: &mut String) {
        buffer.clear();
        buffer.push_str("Processed: ");
        buffer.push_str(data);
    }
  2. Use static data when possible

    // Bad: allocates vec on every call
    fn get_options() -> Vec<String> {
        vec!["option1".to_string(), "option2".to_string()]
    }
    
    // Good: static slice
    const OPTIONS: &[&str] = &["option1", "option2"];
  3. Lazy initialization

    use std::sync::OnceLock;
    
    static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
    
    fn get_client() -> &'static reqwest::Client {
        HTTP_CLIENT.get_or_init(|| reqwest::Client::new())
    }

Testing

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_params() {
        let params = r#"{"message": "test"}"#;
        let parsed: PluginParams = serde_json::from_str(params).unwrap();
        assert_eq!(parsed.message, "test");
    }
}

Integration Testing

# Build plugin
cargo build --target wasm32-wasip1 --release

# Test with wasmtime
wasmtime target/wasm32-wasip1/release/my_plugin.wasm

# Or use Conductor directly
conductor --config test_config.toml 0

Debugging

Enable Debug Logging

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    // Use eprintln! for debug output
    eprintln!("DEBUG: Received {} bytes", params_len);
    eprintln!("DEBUG: Params: {:?}", params);

    // This appears in Conductor's stderr
    0
}

Run with Debug Output

# See plugin debug output
DEBUG=1 conductor --config test_config.toml 0 2>&1 | grep "DEBUG:"

Common Issues

Plugin not loading:

# Verify WASM format
file target/wasm32-wasip1/release/my_plugin.wasm
# Should show: WebAssembly (wasm) binary module

# Check for WASI imports
wasm-objdump -x target/wasm32-wasip1/release/my_plugin.wasm | grep wasi

Out of fuel error:

  • Reduce computation in execute()
  • Move heavy work to init()
  • Use lazy initialization

Memory limit exceeded:

  • Reduce buffer sizes
  • Use streaming instead of loading entire data
  • Profile with cargo-bloat

Signing Your Plugin

See: Plugin Security Guide for complete signing instructions.

Quick reference:

# Generate keypair (one-time)
conductor-sign generate-key ~/.conductor/my-key

# Sign plugin
conductor-sign sign \
  target/wasm32-wasip1/release/my_plugin.wasm \
  ~/.conductor/my-key \
  --name "Your Name" \
  --email "you@example.com"

# Creates: my_plugin.wasm.sig

Distribution

Package Structure

my-midi-plugin/
├── my_plugin.wasm           # Binary
├── my_plugin.wasm.sig       # Signature
├── README.md                # Documentation
├── LICENSE                  # License
└── examples/
    └── config.toml          # Example configuration

README Template

# My MIDI Plugin

Brief description of what your plugin does.

## Installation

1. Download `my_plugin.wasm` and `my_plugin.wasm.sig`
2. Copy to `~/.conductor/wasm-plugins/`
3. Add to configuration (see example)

## Configuration

\```toml
[[modes.mappings]]
trigger = { Note = { note = 60 } }
action = { WasmPlugin = {
    path = "~/.conductor/wasm-plugins/my_plugin.wasm",
    params = {
        "param1": "value1"
    }
}}
\```

## Parameters

- `param1`: Description
- `param2`: Description

## Capabilities

- Network: For HTTP requests
- Filesystem: For data persistence

## License

MIT

Next Steps

Resources

Plugin Security

Since: v2.7 Status: Production-ready

Conductor provides enterprise-grade security for WASM plugins through cryptographic signatures, resource limiting, and filesystem sandboxing.

Security Architecture

┌─────────────────────────────────────────────────┐
│  Security Layers                                │
│                                                 │
│  ┌───────────────────────────────────────────┐ │
│  │  Layer 1: Cryptographic Verification     │ │
│  │  - Ed25519 digital signatures             │ │
│  │  - SHA-256 integrity checking             │ │
│  │  - Trust management                       │ │
│  └───────────────────────────────────────────┘ │
│                                                 │
│  ┌───────────────────────────────────────────┐ │
│  │  Layer 2: Resource Limiting               │ │
│  │  - CPU fuel metering (100M instructions)  │ │
│  │  - Memory limits (128 MB)                 │ │
│  │  - Table growth limits                    │ │
│  └───────────────────────────────────────────┘ │
│                                                 │
│  ┌───────────────────────────────────────────┐ │
│  │  Layer 3: Filesystem Sandboxing           │ │
│  │  - Directory preopening (WASI)            │ │
│  │  - Path validation                        │ │
│  │  - No escape from sandbox                 │ │
│  └───────────────────────────────────────────┘ │
│                                                 │
│  ┌───────────────────────────────────────────┐ │
│  │  Layer 4: Capability System               │ │
│  │  - Explicit permission model              │ │
│  │  - Risk-based approval                    │ │
│  │  - Revocable grants                       │ │
│  └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

Plugin Signing

Overview

Plugin signing uses Ed25519 digital signatures to ensure:

  • Authenticity: Plugin comes from claimed developer
  • Integrity: Plugin hasn’t been tampered with
  • Non-repudiation: Developer cannot deny signing

CLI Tool: conductor-sign

Installation

# Build with signing support
cargo build --package conductor-daemon \
  --bin conductor-sign \
  --features plugin-signing \
  --release

Binary location: target/release/conductor-sign

Signing Workflow

1. Generate Keypair

conductor-sign generate-key ~/.conductor/my-plugin-key

Output:

  • ~/.conductor/my-plugin-key.private (32 bytes, keep secure!)
  • ~/.conductor/my-plugin-key.public (hex-encoded)
  • Public key displayed in terminal

Example output:

Generating Ed25519 keypair...
✓ Keypair generated successfully!

Private key: /Users/you/.conductor/my-plugin-key.private
Public key:  /Users/you/.conductor/my-plugin-key.public

Public key (hex): 3dab8dbfaeb804085e879791d395d6afabe268535a2bc98ea70afa1edd291cca

⚠️  Keep your private key secure and never share it!

2. Sign Plugin

conductor-sign sign my_plugin.wasm ~/.conductor/my-plugin-key \
  --name "Your Name" \
  --email "you@example.com"

Creates: my_plugin.wasm.sig (JSON signature metadata)

Example signature file:

{
  "version": 1,
  "algorithm": "Ed25519",
  "plugin_hash": "a1b2c3d4...",
  "plugin_size": 123456,
  "public_key": "3dab8dbf...",
  "signature": "9f8e7d6c...",
  "signed_at": "2025-11-19T08:59:12Z",
  "developer": {
    "name": "Your Name",
    "email": "you@example.com"
  }
}

3. Verify Signature

conductor-sign verify my_plugin.wasm

Example output:

Verifying plugin: my_plugin.wasm
Signature file: "my_plugin.wasm.sig"
Trusted keys: 1

Signature Details:
  Version:     1
  Algorithm:   Ed25519
  Signed at:   2025-11-19T08:59:12Z
  Developer:   Your Name <you@example.com>
  Public key:  3dab8dbf...

✓ Signature verified successfully!
✓ Plugin signed by trusted key

4. Manage Trusted Keys

Add trusted key:

conductor-sign trust add 3dab8dbfaeb804085e879791d395d6afabe268535a2bc98ea70afa1edd291cca "Official Plugin"

List trusted keys:

conductor-sign trust list

Remove trusted key:

conductor-sign trust remove 3dab8dbf...

Trusted keys file: ~/.config/conductor/trusted_keys.toml

Trust Models

Conductor supports three trust levels:

Level 1: Unsigned (Development)

Use case: Development and testing

Configuration:

let mut config = WasmConfig::default();
config.require_signature = false;  // Default

Behavior:

  • No signature required
  • Plugin loads without verification
  • Suitable for development only

Level 2: Self-Signed

Use case: Personal plugins, one-off scripts

Configuration:

let mut config = WasmConfig::default();
config.require_signature = true;
config.allow_self_signed = true;

Behavior:

  • Signature must be valid (cryptographic check)
  • Any key accepted (no trust check)
  • Ensures binary integrity
  • Good for personal use

Level 3: Trusted Keys (Production)

Use case: Production deployments, marketplace plugins

Configuration:

let mut config = WasmConfig::default();
config.require_signature = true;
config.allow_self_signed = false;  // Default

Behavior:

  • Signature must be valid
  • Public key must be in trusted list
  • Full security model
  • Recommended for production

Security Best Practices

For Plugin Developers

  1. Protect Private Keys

    # Set secure permissions
    chmod 600 ~/.conductor/my-plugin-key.private
    
    # Never commit to version control
    echo "*.private" >> .gitignore
    
  2. Sign Every Release

    # Include signing in your release script
    cargo build --target wasm32-wasip1 --release
    conductor-sign sign \
      target/wasm32-wasip1/release/my_plugin.wasm \
      ~/.conductor/my-key \
      --name "Your Name" --email "you@example.com"
    
  3. Publish Public Key

    • Include in README
    • Post on official website
    • Add to plugin registry
  4. Use Separate Keys per Project

    # One key per plugin project
    conductor-sign generate-key ~/.conductor/plugin-a-key
    conductor-sign generate-key ~/.conductor/plugin-b-key
    

For End Users

  1. Verify Before Trust

    # Always verify signature first
    conductor-sign verify downloaded_plugin.wasm
    
    # Check developer information
    # Only trust if matches expected developer
    
  2. Add Keys Carefully

    # Only add keys from verified sources
    # Check plugin author's website for official key
    conductor-sign trust add <key> "Official Project Name"
    
  3. Audit Trusted Keys

    # Regularly review trusted keys
    conductor-sign trust list
    
    # Remove unused keys
    conductor-sign trust remove <key>
    
  4. Use Strict Mode in Production

    # ~/.config/conductor/config.toml
    [wasm]
    require_signature = true
    allow_self_signed = false  # Strict mode
    

Resource Limiting

CPU Limits (Fuel Metering)

Default: 100,000,000 instructions (~100ms)

What it prevents:

  • Infinite loops
  • Excessive CPU usage
  • Denial of service

How it works:

Every WASM instruction consumes 1 unit of fuel
Plugin execution stops when fuel exhausted

Configuration:

let mut config = WasmConfig::default();
config.max_fuel = 200_000_000;  // 200M instructions

Example:

// This would exceed fuel limit:
#[no_mangle]
pub extern "C" fn execute(...) -> i32 {
    loop {
        // Infinite loop - blocked by fuel limit
        std::thread::sleep(Duration::from_millis(1));
    }
}
// Conductor automatically terminates after ~100ms

Memory Limits

Default: 128 MB per plugin

What it prevents:

  • Memory exhaustion
  • Out-of-memory crashes
  • Resource hogging

Configuration:

let mut config = WasmConfig::default();
config.max_memory_bytes = 256 * 1024 * 1024;  // 256 MB

Example:

#[no_mangle]
pub extern "C" fn execute(...) -> i32 {
    // OK - 10 MB allocation
    let buffer = vec![0u8; 10 * 1024 * 1024];

    // BLOCKED - 200 MB exceeds limit
    // let huge = vec![0u8; 200 * 1024 * 1024];
    // ^ This allocation would fail

    0
}

Table Growth Limits

Default: 10,000 elements

What it prevents:

  • Unbounded table allocation
  • Memory exhaustion via tables
  • DoS attacks

Configuration:

let mut config = WasmConfig::default();
config.max_table_elements = 20_000;

Filesystem Sandboxing

Directory Preopening

Plugins with Filesystem capability can only access a specific directory:

macOS:

~/Library/Application Support/conductor/plugin-data/

Linux:

~/.local/share/conductor/plugin-data/

Windows:

%APPDATA%\conductor\plugin-data\

What’s Blocked

// ✅ ALLOWED - within sandbox
std::fs::write("/my-data.json", data)?;
std::fs::write("/subdir/file.txt", data)?;

// ❌ BLOCKED - path traversal
std::fs::write("/../../../etc/passwd", data)?;

// ❌ BLOCKED - absolute path outside sandbox
std::fs::write("/etc/passwd", data)?;

// ❌ BLOCKED - home directory escape
std::fs::write("~/other-file.txt", data)?;

How It Works

  1. WASI Preopening: Conductor pre-opens the plugin data directory
  2. Path Mapping: All plugin paths mapped to sandbox root
  3. Validation: WASI runtime blocks access outside preopened directories
  4. No Escape: Path traversal attempts automatically blocked

Capability System

Permission Model

Plugins must explicitly request capabilities:

// In plugin code
#[no_mangle]
pub extern "C" fn capabilities() -> *const u8 {
    let caps = vec!["Network", "Filesystem"];
    let json = serde_json::to_string(&caps).unwrap();
    // ... return JSON
}

Risk Levels

CapabilityRiskAuto-GrantRequires Approval
Network🟢 LowYesNo
Audio🟢 LowYesNo
Midi🟢 LowYesNo
Filesystem🟡 MediumNoYes
Subprocess🔴 HighNoYes + Warning
SystemControl🔴 HighNoYes + Warning

Granting Capabilities

Via GUI: Plugin Manager → Select Plugin → Grant Capability

Via Config:

[plugins.my_plugin]
granted_capabilities = ["Network", "Filesystem"]

Threat Model

Threats Mitigated

Binary Tampering

  • Mitigation: SHA-256 hash verification
  • Detection: Signature validation fails if binary modified

Malicious Plugin Injection

  • Mitigation: Ed25519 signature verification
  • Detection: Invalid signature rejected

Man-in-the-Middle

  • Mitigation: Cryptographic signatures
  • Detection: Signature doesn’t match binary

Supply Chain Attacks

  • Mitigation: Trusted key model
  • Detection: Untrusted keys rejected

Resource Exhaustion (DoS)

  • Mitigation: Fuel metering, memory limits
  • Detection: Automatic termination

Filesystem Access

  • Mitigation: Directory sandboxing
  • Detection: WASI blocks access

Privilege Escalation

  • Mitigation: Capability system
  • Detection: Capability checks

Threats NOT Mitigated

⚠️ Side-Channel Attacks

  • Timing attacks possible
  • Mitigation: Careful crypto implementation

⚠️ Social Engineering

  • User could trust malicious key
  • Mitigation: User education

⚠️ Key Compromise

  • Stolen private key can sign malicious plugins
  • Mitigation: Hardware security keys, key rotation

Security Checklist

For Plugin Developers

  • Generate unique keypair for project
  • Protect private key (chmod 600, never commit)
  • Sign every release
  • Publish public key on official channels
  • Request minimum necessary capabilities
  • Include security disclosure policy
  • Test with strict mode enabled
  • Document all security considerations

For End Users

  • Only download plugins from trusted sources
  • Always verify signatures before use
  • Check developer information matches expected
  • Only add keys from verified sources
  • Enable strict mode in production
  • Regularly audit trusted keys
  • Review capability requests
  • Keep Conductor updated

Troubleshooting

Signature Verification Failed

Check signature exists:

ls -la my_plugin.wasm.sig

Verify signature manually:

conductor-sign verify my_plugin.wasm

Common causes:

  • Binary modified after signing
  • Signature file missing
  • Wrong public key used
  • Corrupted signature file

Key Not Trusted

List trusted keys:

conductor-sign trust list

Add key:

conductor-sign trust add <public-key> "Plugin Name"

Verify key matches:

# Compare key in signature vs. expected
cat my_plugin.wasm.sig | jq '.public_key'

Out of Fuel

Symptoms:

  • Plugin terminates mid-execution
  • “fuel exhausted” error

Solutions:

// Increase fuel limit
let mut config = WasmConfig::default();
config.max_fuel = 200_000_000;

// Or optimize plugin code
// - Reduce loop iterations
// - Move heavy work to init()
// - Use lazy initialization

Advanced Topics

Hardware Security Keys

For maximum security, use hardware keys (YubiKey, etc.):

# Generate key on hardware device
# (Implementation depends on HSM/hardware)

# Sign using hardware key
conductor-sign sign my_plugin.wasm \
  --hardware-key /dev/yubikey \
  --name "Your Name" --email "you@example.com"

Key Rotation

# Generate new key
conductor-sign generate-key ~/.conductor/my-plugin-key-v2

# Sign with new key
conductor-sign sign my_plugin.wasm ~/.conductor/my-plugin-key-v2 \
  --name "Your Name" --email "you@example.com"

# Announce rotation to users
# Include both old and new public keys in transition period

Multi-Signature

For critical plugins, require multiple signatures:

# Sign with first key
conductor-sign sign my_plugin.wasm ~/.conductor/key1 \
  --name "Developer 1" --email "dev1@example.com"

# Co-sign with second key
conductor-sign cosign my_plugin.wasm ~/.conductor/key2 \
  --name "Developer 2" --email "dev2@example.com"

# Verify requires both signatures

Further Reading

Plugin Examples

Conductor includes three official WASM plugins that demonstrate real-world integration patterns. All plugins are signed with the official Conductor team key and include full source code.

Official Plugins

Spotify Web API Plugin

File: conductor_wasm_spotify.wasm Capabilities: Network Status: Production-ready

Control Spotify playback directly from your MIDI controller.

Features

  • Play/Pause control
  • Track navigation (next/previous)
  • Volume control
  • Shuffle toggle
  • Repeat mode toggle
  • Get current playback state

Setup

  1. Get Spotify API Credentials

  2. Authenticate

    # First-time setup (opens browser for OAuth)
    spotify-auth --client-id YOUR_ID --client-secret YOUR_SECRET
    
  3. Configuration

    # Play/Pause
    [[modes.mappings]]
    trigger = { Note = { note = 60 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = {
            "action": "play_pause"
        }
    }}
    
    # Next track
    [[modes.mappings]]
    trigger = { Note = { note = 61 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = {
            "action": "next"
        }
    }}
    
    # Previous track
    [[modes.mappings]]
    trigger = { Note = { note = 59 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = {
            "action": "previous"
        }
    }}
    
    # Volume control (velocity-sensitive)
    [[modes.mappings]]
    trigger = { Note = { note = 62 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = {
            "action": "volume",
            "level": "{velocity}"  # 0-127 mapped to 0-100%
        }
    }}
    

Available Actions

ActionParametersDescription
playNoneResume playback
pauseNonePause playback
play_pauseNoneToggle play/pause
nextNoneSkip to next track
previousNonePrevious track
volumelevel: 0-127Set volume (maps to 0-100%)
shuffleNoneToggle shuffle
repeatmode: "off"|"track"|"context"Set repeat mode
get_stateNoneGet current playback state

Advanced: Velocity-Sensitive Volume

[[modes.mappings]]
trigger = { VelocityRange = {
    note = 62,
    ranges = [
        { min = 0, max = 40, action_index = 0 },    # Soft: -10%
        { min = 41, max = 80, action_index = 1 },   # Medium: no change
        { min = 81, max = 127, action_index = 2 }   # Hard: +10%
    ]
}}
actions = [
    { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = { "action": "volume", "delta": "-10" }
    }},
    { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = { "action": "get_state" }
    }},
    { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_spotify.wasm",
        params = { "action": "volume", "delta": "+10" }
    }}
]

OBS Studio Control Plugin

File: conductor_wasm_obs_control.wasm Capabilities: Network Status: Production-ready

Control OBS Studio streaming/recording via WebSocket.

Features

  • Scene switching
  • Start/Stop streaming
  • Start/Stop recording
  • Source mute/unmute
  • Filter toggle
  • Transition control

Setup

  1. Enable OBS WebSocket

    • OBS Studio → Tools → WebSocket Server Settings
    • Enable WebSocket server
    • Set password (optional but recommended)
    • Note port (default: 4455)
  2. Configuration

    # Scene switching
    [[modes.mappings]]
    trigger = { Note = { note = 48 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_obs_control.wasm",
        params = {
            "action": "set_scene",
            "scene_name": "Gaming",
            "host": "localhost:4455",
            "password": "your_password"  # Optional
        }
    }}
    
    # Start streaming
    [[modes.mappings]]
    trigger = { Note = { note = 49 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_obs_control.wasm",
        params = {
            "action": "start_streaming",
            "host": "localhost:4455"
        }
    }}
    
    # Stop streaming
    [[modes.mappings]]
    trigger = { Note = { note = 50 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_obs_control.wasm",
        params = {
            "action": "stop_streaming",
            "host": "localhost:4455"
        }
    }}
    
    # Toggle mic mute
    [[modes.mappings]]
    trigger = { Note = { note = 51 } }
    action = { WasmPlugin = {
        path = "~/.conductor/wasm-plugins/conductor_wasm_obs_control.wasm",
        params = {
            "action": "toggle_mute",
            "source_name": "Microphone",
            "host": "localhost:4455"
        }
    }}
    

Available Actions

ActionParametersDescription
set_scenescene_nameSwitch to scene
get_current_sceneNoneGet active scene
start_streamingNoneStart streaming
stop_streamingNoneStop streaming
toggle_streamingNoneToggle streaming
start_recordingNoneStart recording
stop_recordingNoneStop recording
toggle_recordingNoneToggle recording
toggle_mutesource_nameMute/unmute source
set_volumesource_name, volume: 0-1Set source volume
toggle_filtersource_name, filter_nameToggle filter
set_transitiontransition_name, duration_msSet scene transition

Advanced: Scene Hotkeys

# Map pads to scenes
[[modes]]
name = "OBS Control"

[[modes.mappings]]
trigger = { Note = { note = 36 } }  # Pad 1
action = { WasmPlugin = { path = "...", params = { "action": "set_scene", "scene_name": "Intro" }}}

[[modes.mappings]]
trigger = { Note = { note = 37 } }  # Pad 2
action = { WasmPlugin = { path = "...", params = { "action": "set_scene", "scene_name": "Gaming" }}}

[[modes.mappings]]
trigger = { Note = { note = 38 } }  # Pad 3
action = { WasmPlugin = { path = "...", params = { "action": "set_scene", "scene_name": "Chatting" }}}

[[modes.mappings]]
trigger = { Note = { note = 39 } }  # Pad 4
action = { WasmPlugin = { path = "...", params = { "action": "set_scene", "scene_name": "BRB" }}}

System Utilities Plugin

File: conductor_wasm_system_utils.wasm Capabilities: SystemControl Status: Production-ready

System-level operations like screen lock, sleep, notifications.

Features

  • Lock screen
  • Sleep/shutdown
  • Brightness control
  • System notifications
  • Application launcher
  • Volume control (system-wide)

Configuration

# Lock screen
[[modes.mappings]]
trigger = { LongPress = { note = 60, duration_ms = 2000 } }
action = { WasmPlugin = {
    path = "~/.conductor/wasm-plugins/conductor_wasm_system_utils.wasm",
    params = {
        "action": "lock_screen"
    }
}}

# Display notification
[[modes.mappings]]
trigger = { Note = { note = 61 } }
action = { WasmPlugin = {
    path = "~/.conductor/wasm-plugins/conductor_wasm_system_utils.wasm",
    params = {
        "action": "notify",
        "title": "Recording Started",
        "message": "Stream is now live!",
        "sound": true
    }
}}

# Brightness control (velocity-sensitive)
[[modes.mappings]]
trigger = { Note = { note = 62 } }
action = { WasmPlugin = {
    path = "~/.conductor/wasm-plugins/conductor_wasm_system_utils.wasm",
    params = {
        "action": "brightness",
        "level": "{velocity}"  # 0-127 mapped to 0-100%
    }
}}

# Launch application
[[modes.mappings]]
trigger = { Note = { note = 63 } }
action = { WasmPlugin = {
    path = "~/.conductor/wasm-plugins/conductor_wasm_system_utils.wasm",
    params = {
        "action": "launch",
        "app": "Spotify"
    }
}}

Available Actions

ActionParametersDescription
lock_screenNoneLock screen (macOS/Linux)
sleepNonePut system to sleep
shutdownforce: boolShutdown system
notifytitle, message, sound: boolShow notification
brightnesslevel: 0-127Set screen brightness
launchapp: stringLaunch application
volumelevel: 0-127Set system volume
volume_upNoneIncrease volume 10%
volume_downNoneDecrease volume 10%
muteNoneToggle system mute

Platform-Specific Notes

macOS:

  • lock_screen: Uses pmset command
  • brightness: Requires screen brightness permission
  • launch: Uses open -a

Linux:

  • lock_screen: Uses loginctl or xdg-screensaver
  • brightness: Requires /sys/class/backlight access
  • launch: Uses xdg-open

Windows:

  • lock_screen: Uses rundll32.exe
  • brightness: Uses WMI
  • launch: Uses start

Creating Your Own Plugin

Template Repository

Start with the official template:

git clone https://github.com/amiable-dev/conductor-wasm-plugin-template
cd conductor-wasm-plugin-template

Example: Simple Notification Plugin

// src/lib.rs
use serde::Deserialize;

#[derive(Deserialize)]
struct NotifyParams {
    message: String,
}

#[no_mangle]
pub extern "C" fn init() {
    eprintln!("Notification plugin initialized");
}

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    let params_bytes = unsafe {
        std::slice::from_raw_parts(params_ptr, params_len)
    };

    let params: NotifyParams = match serde_json::from_slice(params_bytes) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("Invalid params: {}", e);
            return 1;
        }
    };

    // Platform-specific notification (simplified)
    #[cfg(target_os = "macos")]
    {
        let cmd = format!(
            "osascript -e 'display notification \"{}\" with title \"Conductor\"'",
            params.message
        );
        std::process::Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()
            .expect("Failed to show notification");
    }

    eprintln!("Notification sent: {}", params.message);
    0
}

Build and Test

# Build
cargo build --target wasm32-wasip1 --release

# Sign
conductor-sign sign \
  target/wasm32-wasip1/release/my_notify_plugin.wasm \
  ~/.conductor/my-key \
  --name "Your Name" --email "you@example.com"

# Test
cat > test_config.toml <<EOF
[[modes.mappings]]
trigger = { Note = { note = 60 } }
action = { WasmPlugin = {
    path = "target/wasm32-wasip1/release/my_notify_plugin.wasm",
    params = { "message": "Test notification" }
}}
EOF

conductor --config test_config.toml 0

Best Practices

Error Handling

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    // Always validate inputs
    if params_len == 0 {
        eprintln!("ERROR: Empty parameters");
        return 1;
    }

    // Handle JSON parsing errors
    let params: MyParams = match serde_json::from_slice(params_bytes) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("ERROR: Invalid JSON: {}", e);
            return 1;
        }
    };

    // Handle operation errors
    match perform_action(&params) {
        Ok(_) => 0,   // Success
        Err(e) => {
            eprintln!("ERROR: Action failed: {}", e);
            1  // Error
        }
    }
}

Performance Optimization

use std::sync::OnceLock;

// Lazy initialization (runs once)
static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

fn get_client() -> &'static reqwest::Client {
    HTTP_CLIENT.get_or_init(|| {
        reqwest::Client::builder()
            .timeout(Duration::from_secs(5))
            .build()
            .expect("Failed to create HTTP client")
    })
}

#[no_mangle]
pub extern "C" fn execute(params_ptr: *const u8, params_len: usize) -> i32 {
    // Reuse client instead of creating new one
    let client = get_client();

    // Your code...
    0
}

Resource Management

// Use Drop for cleanup
struct PluginState {
    connection: Option<Connection>,
}

impl Drop for PluginState {
    fn drop(&mut self) {
        if let Some(conn) = &mut self.connection {
            let _ = conn.close();
        }
        eprintln!("Plugin state cleaned up");
    }
}

static mut STATE: Option<PluginState> = None;

#[no_mangle]
pub extern "C" fn init() {
    unsafe {
        STATE = Some(PluginState {
            connection: None,
        });
    }
}

#[no_mangle]
pub extern "C" fn shutdown() {
    unsafe {
        STATE = None;  // Triggers Drop
    }
}

Troubleshooting

Plugin Not Executing

  1. Check logs:

    DEBUG=1 conductor --config config.toml 0 2>&1 | grep WASM
    
  2. Verify WASM format:

    file my_plugin.wasm
    # Should show: WebAssembly (wasm) binary module
    
  3. Check signature:

    conductor-sign verify my_plugin.wasm
    

Out of Fuel

// Symptoms: Plugin terminates early

// Solution 1: Optimize code
// - Move heavy work to init()
// - Reduce loop iterations
// - Use lazy initialization

// Solution 2: Increase fuel limit (config.toml)
[wasm]
max_fuel = 200_000_000

Network Requests Failing

// Check capability is granted
fn capabilities() -> Vec<String> {
    vec!["Network".to_string()]
}

// Use appropriate timeout
let client = reqwest::Client::builder()
    .timeout(Duration::from_secs(5))
    .build()?;

// Handle errors gracefully
match client.get(url).send().await {
    Ok(response) => { /* ... */ },
    Err(e) => {
        eprintln!("Network error: {}", e);
        return 1;
    }
}

Further Reading

Contributing Guide

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Testing Guide

This guide covers testing strategies for Conductor, including hardware-independent testing using the MIDI device simulator.

Table of Contents

MIDI Device Simulator

The MIDI device simulator allows comprehensive testing of Conductor without requiring physical hardware. It simulates all MIDI events and complex user interactions with precise timing control.

Features

The simulator supports:

  • Basic MIDI Events: Note On/Off, Control Change, Aftertouch, Pitch Bend, Program Change
  • Velocity Levels: Soft (0-40), Medium (41-80), Hard (81-127)
  • Timing-Based Triggers: Long press, double-tap detection
  • Complex Gestures: Chords, encoder rotation, velocity ramps
  • Precise Timing: Configurable delays and durations
  • Event Capture: Inspect all generated MIDI messages

Quick Start

use midi_simulator::{MidiSimulator, Gesture, EncoderDirection};

// Create a simulator on MIDI channel 0
let sim = MidiSimulator::new(0);

// Simulate a simple note press
sim.note_on(60, 100);
sim.note_off(60);

// Get captured events
let events = sim.get_events();
assert_eq!(events.len(), 2); // Note on + Note off

Running Tests

Unit Tests

Run the simulator’s built-in unit tests:

cargo test --test midi_simulator

Expected output:

running 12 tests
test tests::test_note_on_off ... ok
test tests::test_velocity_levels ... ok
test tests::test_control_change ... ok
test tests::test_aftertouch ... ok
test tests::test_pitch_bend ... ok
test tests::test_program_change ... ok
test tests::test_simple_tap_gesture ... ok
test tests::test_chord_gesture ... ok
test tests::test_encoder_simulation ... ok
test tests::test_velocity_ramp_gesture ... ok
test tests::test_scenario_builder ... ok
test tests::test_channel_masking ... ok

test result: ok. 12 passed; 0 failed

Integration Tests

Run integration tests that verify the complete event processing pipeline:

cargo test --test integration_tests

These tests cover:

  • Basic note event handling
  • Velocity level detection
  • Long press simulation with timing validation
  • Double-tap detection
  • Chord detection with multiple notes
  • Encoder direction detection (CW/CCW)
  • Aftertouch and pitch bend
  • Complex multi-event scenarios

All Tests

Run all tests including unit and integration:

cargo test

For improved test output and parallel execution, use cargo-nextest:

# Install nextest
cargo install cargo-nextest

# Run tests with nextest
cargo nextest run --all-features

# Or use the convenience script
./scripts/test-nextest.sh

Nextest provides:

  • Faster test execution through parallelization
  • Better output formatting with progress indicators
  • More detailed failure reporting
  • Per-test timing information

End-to-End Test Suite

The E2E test suite (tests/e2e_tests.rs) provides comprehensive validation of the complete Conductor pipeline from MIDI input through event processing, mapping, and action execution. See Integration Test Suites section below for full documentation of all E2E workflows, test architecture, and writing E2E tests.

Quick Start

# Run all E2E tests (20+ workflow tests)
cargo test --test e2e_tests

# Expected: 37 tests passed covering all critical workflows

Code Coverage

Conductor uses cargo-llvm-cov for code coverage tracking. The project maintains a minimum coverage threshold of 0.35% (baseline) with a Phase 1 target of 85%.

Installing Coverage Tools

# Install cargo-llvm-cov
cargo install cargo-llvm-cov

Generating Coverage Reports

Terminal Summary (Default)

Generate a coverage summary in the terminal:

# Using cargo-llvm-cov directly
cargo llvm-cov --all-features --workspace

# Or use the convenience script
./scripts/coverage.sh

# Or use just command
just coverage

Output example:

Filename                      Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
actions.rs                        212               212     0.00%          12                12     0.00%         134               134     0.00%
config.rs                          52                42    19.23%           3                 2    33.33%          52                47     9.62%
main.rs                           278               278     0.00%          13                13     0.00%         151               151     0.00%
mappings.rs                        96                96     0.00%           8                 8     0.00%          71                71     0.00%
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                            2263              2253     0.44%          66                65     1.52%        1413              1408     0.35%

HTML Report

Generate an interactive HTML coverage report:

# Generate HTML report
./scripts/coverage.sh --html

# Or use just command
just coverage-html

# Report saved to: target/llvm-cov/html/index.html

HTML Report with Auto-Open

Generate and automatically open the coverage report in your browser:

# Generate and open HTML report
./scripts/coverage.sh --open

# Or use just command
just coverage-open

LCOV Format (for CI)

Generate coverage in LCOV format for Codecov or other CI tools:

# Generate lcov.info
./scripts/coverage.sh --lcov

# Or use just command
just coverage-lcov

# Output: lcov.info

Coverage Configuration

Coverage settings are configured in .llvm-cov.toml:

[report]
# Fail if coverage is below this percentage
fail-under-lines = 0.35

[filter]
# Exclude test files and binaries from coverage
exclude-filename-regex = [
    ".*/tests/.*",
    ".*/bin/.*"
]

Coverage Targets

  • Phase 0 Baseline: 0.35% (established)
  • Phase 1 Target: 85% line coverage
  • Minimum Threshold: 80% (enforced in CI)

Coverage in CI/CD

Coverage is automatically tracked on every pull request:

  1. GitHub Actions runs coverage on all PRs
  2. Codecov receives coverage reports and provides:
    • Coverage percentage badge
    • Line-by-line coverage visualization
    • Coverage diffs on PRs
    • Historical coverage trends
  3. PR Comments show coverage delta (increase/decrease)
  4. Status Checks fail if coverage drops below threshold

View current coverage: codecov

Local Coverage Workflow

Recommended workflow for maintaining coverage:

# 1. Write tests for new code
vim tests/my_feature_test.rs

# 2. Run tests to verify they pass
cargo nextest run

# 3. Generate coverage report
just coverage-open

# 4. Identify uncovered lines (shown in red in HTML report)

# 5. Add tests for uncovered code paths

# 6. Verify coverage improved
just coverage

Test Reporting

GitHub Actions Integration

All tests run automatically on:

  • Push to main/develop branches
  • Pull requests
  • Manual workflow dispatch

Test results are displayed in the GitHub Actions UI with:

  • Test count (passed/failed/skipped)
  • Execution time
  • Detailed failure logs
  • Coverage percentage

Nextest Reports

Nextest provides enhanced test reporting:

# Run with detailed output
cargo nextest run --verbose

# Generate JUnit XML report
cargo nextest run --junit junit.xml

# Show only failed tests
cargo nextest run --failure-output immediate

Coverage Reports in PRs

When you create a pull request, Codecov automatically:

  1. Analyzes coverage for changed files
  2. Posts a comment with coverage diff
  3. Updates status checks
  4. Shows coverage on changed lines

Example PR comment:

Coverage: 85.2% (+2.1%) vs main

Files changed coverage:
  - src/new_feature.rs: 92.3% ✓
  - src/existing.rs: 78.1% ⚠️ (below target)

Local Test Scripts

Use convenience scripts for common testing tasks:

# Run all tests
./scripts/test.sh

# Run with nextest
./scripts/test-nextest.sh

# Generate coverage
./scripts/coverage.sh [--html|--open|--lcov]

Just Commands

The justfile provides convenient shortcuts:

# View all available commands
just

# Run tests
just test              # Standard cargo test
just test-nextest      # With nextest
just test-watch        # Watch mode (requires cargo-watch)

# Coverage
just coverage          # Terminal summary
just coverage-html     # HTML report
just coverage-open     # HTML report + open in browser
just coverage-lcov     # LCOV format for CI

# Linting and formatting
just lint              # Run clippy
just fmt               # Format code
just fmt-check         # Check formatting

# Complete CI check locally
just ci                # Run all CI checks (fmt, lint, test, coverage)

Writing Tests

Basic Event Tests

Test simple MIDI event generation:

#[test]
fn test_my_feature() {
    let sim = MidiSimulator::new(0);

    // Simulate user action
    sim.note_on(60, 100);
    sim.note_off(60);

    // Verify events
    let events = sim.get_events();
    assert_eq!(events.len(), 2);
    assert_eq!(events[0], vec![0x90, 60, 100]); // Note On
    assert_eq!(events[1], vec![0x80, 60, 0x40]); // Note Off
}

Velocity Level Tests

Test velocity-sensitive actions:

#[test]
fn test_velocity_levels() {
    let sim = MidiSimulator::new(0);

    // Test soft, medium, hard presses
    sim.note_on(60, 30);   // Soft (0-40)
    sim.note_on(60, 70);   // Medium (41-80)
    sim.note_on(60, 110);  // Hard (81-127)

    let events = sim.get_events();
    assert_eq!(events.len(), 3);
}

Timing-Based Tests

Test long press and timing detection:

#[test]
fn test_long_press() {
    let sim = MidiSimulator::new(0);
    let start = Instant::now();

    sim.perform_gesture(Gesture::LongPress {
        note: 60,
        velocity: 80,
        hold_ms: 2500,
    });

    let duration = start.elapsed();
    assert!(duration >= Duration::from_millis(2500));
}

Double-Tap Tests

Test double-tap detection with gap timing:

#[test]
fn test_double_tap() {
    let sim = MidiSimulator::new(0);

    sim.perform_gesture(Gesture::DoubleTap {
        note: 60,
        velocity: 80,
        tap_duration_ms: 50,
        gap_ms: 200,
    });

    let events = sim.get_events();
    assert_eq!(events.len(), 4); // 2 note ons + 2 note offs
}

Chord Tests

Test chord detection with multiple simultaneous notes:

#[test]
fn test_chord() {
    let sim = MidiSimulator::new(0);

    sim.perform_gesture(Gesture::Chord {
        notes: vec![60, 64, 67], // C major chord
        velocity: 80,
        stagger_ms: 10,
        hold_ms: 500,
    });

    let events = sim.get_events();
    assert_eq!(events.len(), 6); // 3 note ons + 3 note offs
}

Encoder Tests

Test encoder rotation with direction detection:

#[test]
fn test_encoder() {
    let sim = MidiSimulator::new(0);

    sim.perform_gesture(Gesture::EncoderTurn {
        cc: 1,
        direction: EncoderDirection::Clockwise,
        steps: 5,
        step_delay_ms: 0,
    });

    let events = sim.get_events();
    assert_eq!(events.len(), 5);

    // Verify values are increasing
    for i in 1..events.len() {
        assert!(events[i][2] > events[i-1][2]);
    }
}

Scenario Builder

Create complex test scenarios with the builder pattern:

use midi_simulator::ScenarioBuilder;

#[test]
fn test_complex_scenario() {
    let sim = MidiSimulator::new(0);

    let scenario = ScenarioBuilder::new()
        .note_on(60, 100)
        .wait(100)
        .control_change(1, 64)
        .wait(100)
        .aftertouch(80)
        .wait(100)
        .note_off(60)
        .build();

    sim.execute_sequence(scenario);

    let events = sim.get_events();
    assert_eq!(events.len(), 4);
}

Interactive CLI Tool

The simulator includes an interactive command-line interface for manual testing and experimentation.

Starting the CLI

cargo run --bin midi_simulator

Available Commands

╭─────────────────────────────────────────────────────────────╮
│ COMMANDS                                                    │
├─────────────────────────────────────────────────────────────┤
│ Basic:                                                      │
│   help, h, ?              Show help message                 │
│   quit, exit, q           Exit the simulator                │
│   clear, c                Clear event queue                 │
│   events, e               Show captured events              │
├─────────────────────────────────────────────────────────────┤
│ MIDI Events:                                                │
│   note <num> <vel>        Send note on/off                  │
│   velocity <note>         Test velocity levels              │
│   long <note> [ms]        Simulate long press               │
│   double <note> [gap_ms]  Simulate double-tap               │
│   chord <n1> <n2> ...     Simulate chord                    │
│   encoder <cc> <cw|ccw>   Simulate encoder rotation         │
│   aftertouch <pressure>   Send aftertouch                   │
│   pitch <value>           Send pitch bend (0-16383)         │
│   cc <num> <val>          Send control change               │
├─────────────────────────────────────────────────────────────┤
│ Scenarios:                                                  │
│   demo                    Run full demonstration            │
│   scenario [name]         Run specific test scenario        │
╰─────────────────────────────────────────────────────────────╯

Example Session

# Start the CLI
cargo run --bin midi_simulator

# Test velocity levels
> velocity 60
Simulating velocity levels (soft, medium, hard)...
✓ Velocity test complete

# Test long press
> long 60 2500
Simulating long press for 2500ms...
✓ Long press complete

# Test double-tap
> double 60 200
Simulating double-tap with 200ms gap...
✓ Double-tap complete

# Test chord (C major)
> chord 60 64 67
Simulating chord: [60, 64, 67]
✓ Chord complete

# Test encoder rotation
> encoder 1 cw 5
Simulating encoder CC1 Clockwise 5 steps...
✓ Encoder simulation complete

# Show captured events
> events
Captured events:
  1: [90, 60, 64, ...]
  2: [80, 60, 40, ...]
  ...

# Run full demo
> demo
Running demonstration scenarios...
1. Testing velocity levels...
2. Testing long press...
3. Testing double-tap...
4. Testing chord...
5. Testing encoder...
✓ Demo complete

# Exit
> quit
Goodbye!

Test Scenarios

The simulator includes pre-built test scenarios for common testing needs.

Velocity Scenario

Tests all three velocity levels:

> scenario velocity
Testing velocity levels: Soft (30), Medium (70), Hard (110)
✓ Velocity scenario complete

Timing Scenario

Tests short, medium, and long press durations:

> scenario timing
Testing press durations: Short (100ms), Medium (500ms), Long (2500ms)
✓ Timing scenario complete

Double-Tap Scenario

Tests double-tap detection:

> scenario doubletap
Testing double-tap with 200ms gap
✓ Double-tap scenario complete

Chord Scenario

Tests chord detection with C major:

> scenario chord
Testing chord detection: C major (60, 64, 67)
✓ Chord scenario complete

Encoder Scenario

Tests encoder rotation in both directions:

> scenario encoder
Testing encoder: 5 steps CW, then 5 steps CCW
✓ Encoder scenario complete

Complex Scenario

Tests mixed events and complex interactions:

> scenario complex
Running complex scenario: mixed events...
✓ Complex scenario complete

Advanced Gestures

Velocity Ramp

Simulate a velocity ramp from soft to hard:

sim.perform_gesture(Gesture::VelocityRamp {
    note: 60,
    min_velocity: 20,
    max_velocity: 120,
    steps: 5,
});

Simple Tap

Simulate a quick tap with precise duration:

sim.perform_gesture(Gesture::SimpleTap {
    note: 60,
    velocity: 80,
    duration_ms: 100,
});

Event Inspection

Getting Events

// Get all events and clear the queue
let events = sim.get_events();

// Peek at last event without clearing
let last = sim.peek_last_event();

// Clear the queue
sim.clear_events();

Parsing Events

for event in events {
    let status = event[0];
    let message_type = status & 0xF0;
    let channel = status & 0x0F;

    match message_type {
        0x90 => println!("Note On: {} vel {}", event[1], event[2]),
        0x80 => println!("Note Off: {}", event[1]),
        0xB0 => println!("CC{}: {}", event[1], event[2]),
        0xD0 => println!("Aftertouch: {}", event[1]),
        0xE0 => println!("Pitch Bend: {} {}", event[1], event[2]),
        _ => println!("Unknown message type"),
    }
}

Debug Output

Enable debug output to see all MIDI messages:

let mut sim = MidiSimulator::new(0);
sim.set_debug(true);

sim.note_on(60, 100);
// Output: [SIM] Sending: [90, 3C, 64]

Best Practices

  1. Clear events between tests: Always clear the event queue between tests to avoid interference
  2. Use gestures for complex interactions: Prefer high-level gestures over manual event sequences
  3. Verify timing: Use Instant::now() to verify timing-sensitive operations
  4. Test edge cases: Test velocity boundaries (0, 40, 41, 80, 81, 127)
  5. Test multiple channels: Verify channel masking works correctly
  6. Use scenario builder: Build complex scenarios declaratively with the builder pattern

Continuous Integration

The simulator is designed to work in CI environments without hardware:

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1
      - name: Run tests
        run: cargo test --all-features

Troubleshooting

Tests Timeout

If timing-based tests timeout, increase tolerance:

assert!(duration >= Duration::from_millis(2500));
assert!(duration < Duration::from_millis(2700)); // 200ms tolerance

Event Count Mismatch

Remember that get_events() clears the queue:

let events1 = sim.get_events(); // Gets and clears
let events2 = sim.get_events(); // Empty, events were already consumed

Velocity Detection

Ensure velocities match expected ranges:

// Soft: 0-40
// Medium: 41-80
// Hard: 81-127

Integration Test Suites

Conductor includes comprehensive integration test suites that verify complete feature sets without requiring physical hardware.

Event Processing Tests (AMI-117)

Location: tests/event_processing_tests.rs

Tests for aftertouch and pitch bend event processing:

Aftertouch Tests (26 tests):

  • Full pressure range validation (0-127)
  • Continuous pressure variation
  • Boundary value testing (min/max)
  • Aftertouch with note press scenarios
  • Multi-channel aftertouch support

Pitch Bend Tests (26 tests):

  • Center position verification (8192)
  • Full 14-bit range testing (0-16383)
  • Positive and negative bend ranges
  • Smooth sweep simulations
  • Pitch bend with note combinations
  • Multi-channel pitch bend support
# Run event processing tests
cargo test --test event_processing_tests

Action Tests (AMI-118)

Location: tests/action_tests.rs

Tests for application launch and volume control actions:

Launch Application Tests (14 tests):

  • Valid application path handling
  • Invalid path error handling
  • Paths with spaces
  • Process spawning verification
  • Permission denied scenarios
  • Concurrent process spawning
  • Platform-specific behavior detection

Volume Control Tests (14 tests):

  • Command detection (macOS, Linux, Windows)
  • Volume up/down command structure
  • Mute toggle command structure
  • Volume set command structure
  • Mock volume control execution
  • Shell command escaping
# Run action tests
cargo test --test action_tests

Action Orchestration Tests (AMI-119)

Location: tests/action_orchestration_tests.rs

Tests for complex action orchestration (38 tests):

Sequence Actions (F16):

  • Action ordering verification
  • Empty sequence handling
  • Single action sequences
  • Sequences with delays
  • Error propagation in sequences

Delay Actions (F17):

  • Timing accuracy (50ms, 100ms, 500ms)
  • Zero delay handling
  • Multiple sequential delays
  • Timing precision validation (±10ms tolerance)

MouseClick Actions (F18):

  • Click simulation structure
  • Coordinate validation
  • Button type validation (left, right, middle)
  • Click sequences with delays

Repeat Actions (F19):

  • Repeat count verification
  • Repeat with delays
  • Zero and single repetitions
  • High-volume repeat handling (100+ iterations)

Conditional Actions (F20):

  • Application-based conditions
  • Time-based conditions (hour ranges)
  • Modifier key conditions
  • Mode-based conditions
  • Multiple condition combinations (AND/OR logic)
  • Complex conditional expressions
# Run action orchestration tests
cargo test --test action_orchestration_tests

End-to-End Tests (AMI-120)

Location: tests/e2e_tests.rs

Comprehensive E2E testing of the complete Conductor pipeline (MIDI Input → Event Processing → Mapping → Action Execution):

Critical Workflows (20 tests):

  • Simple pad press → keystroke
  • Velocity-sensitive mapping (soft/medium/hard)
  • Long press detection (≥1000ms threshold)
  • Double-tap recognition (<300ms window)
  • Chord detection (<50ms window)
  • Mode switching via encoder
  • Mode-specific vs global mappings
  • Action sequences with delays
  • Conditional actions (app/time/mode)
  • Volume control via encoder

Performance & Edge Cases (5 tests):

  • Timing latency verification (<1ms)
  • Rapid note events (20+ events)
  • Invalid note range handling (0, 1, 126, 127)
  • Throughput testing (200 events <10ms)
  • Memory stability (1000 events)
# Run all E2E tests
cargo test --test e2e_tests

# Expected: 37 tests passed

Test Coverage Summary

Total test count: 183 tests

Breakdown by suite:

  • integration_tests.rs: 29 tests (basic event processing)
  • event_processing_tests.rs: 26 tests (aftertouch & pitch bend)
  • action_tests.rs: 14 tests (launch & volume control)
  • action_orchestration_tests.rs: 38 tests (sequences & conditionals)
  • e2e_tests.rs: 37 tests (end-to-end critical workflows) ← NEW
  • config_compatibility_test.rs: 15 tests (config validation)
  • midi_simulator.rs: 12 tests (simulator validation)
  • Additional unit tests: 12 tests (various modules)

Running All Integration Tests

# Run all integration tests
cargo test --test integration_tests \
           --test event_processing_tests \
           --test action_tests \
           --test action_orchestration_tests

# Run all tests with coverage
cargo test --all-features

Writing New Integration Tests

When adding new integration tests:

  1. Use the MIDI simulator for all MIDI event generation
  2. Test edge cases (boundary values, timing variations)
  3. Include negative tests (error conditions, invalid inputs)
  4. Verify timing with tolerance (±10-35ms for CI stability)
  5. Document test purpose with clear comments
  6. Group related tests into logical test modules

Example template:

#[test]
fn test_feature_name() {
    let sim = MidiSimulator::new(0);

    // Setup: Generate test events
    sim.note_on(60, 80);

    // Execute: Perform action
    let events = sim.get_events();

    // Verify: Check results
    assert_eq!(events.len(), 1);
    assert_eq!(events[0][0] & 0xF0, 0x90); // Note On
}

CI/CD Integration

All integration tests run automatically in GitHub Actions:

  • No hardware required: Uses MIDI simulator
  • Fast execution: <5 seconds total for all tests
  • Timing tolerance: Increased for CI environments (±35ms)
  • Platform coverage: Tests run on macOS, Linux, Windows

Game Controllers (HID) Testing (v3.0+)

Conductor v3.0 added support for all SDL2-compatible HID devices (gamepads, joysticks, racing wheels, flight sticks, HOTAS, and custom controllers). This section covers comprehensive testing strategies for gamepad functionality.

Overview

Game controller testing in Conductor covers three main areas:

  1. Unit Tests: Component-level testing of InputManager, GamepadDeviceManager, and event conversion
  2. Integration Tests: Multi-component testing of hybrid mode, event streams, and device lifecycle
  3. Manual Testing: Physical hardware testing with real game controllers

Unit Tests

InputManager Creation Tests

Test InputManager initialization with different input modes:

# Run InputManager tests
cargo test input_manager

Test Coverage:

  • InputMode::MidiOnly - MIDI device only (gamepad_manager = None)
  • InputMode::GamepadOnly - Gamepad device only (midi_manager = None)
  • InputMode::Both - Hybrid mode (both managers initialized)
  • Auto-reconnection configuration propagation
  • Device name configuration

Example test:

#[test]
fn test_input_manager_gamepad_only_mode() {
    use conductor_daemon::input_manager::{InputManager, InputMode};

    let manager = InputManager::new(
        None,  // No MIDI device
        true,  // auto_reconnect
        InputMode::GamepadOnly
    );

    // Verify only gamepad manager is initialized
    assert!(manager.has_gamepad_manager());
    assert!(!manager.has_midi_manager());
}

GamepadDeviceManager Lifecycle Tests

Test gamepad connection, disconnection, and state management:

# Run gamepad-specific unit tests
cargo test gamepad

Test Coverage:

  • Device detection and enumeration
  • Connection lifecycle (connect → active → disconnect)
  • Connection state tracking (is_connected flag)
  • Device ID assignment (0-based indexing)
  • Device name retrieval
  • Thread safety (Arc/Mutex patterns)

Example test:

#[test]
fn test_gamepad_connection_lifecycle() {
    use conductor_daemon::gamepad_device::GamepadDeviceManager;
    use tokio::sync::mpsc;

    let (event_tx, _event_rx) = mpsc::channel(1024);
    let (command_tx, _command_rx) = mpsc::channel(32);

    let mut manager = GamepadDeviceManager::new(true);

    // Test connection (requires physical gamepad)
    match manager.connect(event_tx, command_tx) {
        Ok((id, name)) => {
            println!("Connected: {} (ID {})", name, id);
            assert!(manager.is_connected());
        }
        Err(_) => {
            // Expected when no gamepad is connected
            assert!(!manager.is_connected());
        }
    }
}

Event Conversion Tests (HID → InputEvent)

Test conversion of gamepad events to InputEvent format:

Test Coverage:

  • Button press → InputEvent::PadPressed (IDs 128-255)
  • Button release → InputEvent::PadReleased (IDs 128-255)
  • Analog stick movement → InputEvent::EncoderTurned (X/Y axes)
  • Trigger pull → InputEvent::EncoderTurned (analog triggers)
  • D-pad press → InputEvent::PadPressed (direction buttons)

Example test (from conductor-core/tests/gamepad_input_test.rs):

#[test]
fn test_gamepad_button_press_detection() {
    let mut processor = EventProcessor::new();

    // Gamepad button press (button ID 128 = South/A/Cross/B)
    let event = InputEvent::PadPressed {
        pad: 128,
        velocity: 100,
        time: Instant::now(),
    };

    let processed = processor.process_input(event);

    // Should detect PadPressed with velocity level
    assert_eq!(processed.len(), 1);

    match &processed[0] {
        ProcessedEvent::PadPressed {
            note,
            velocity,
            velocity_level,
        } => {
            assert_eq!(*note, 128);
            assert_eq!(*velocity, 100);
            assert_eq!(*velocity_level, VelocityLevel::Hard); // 100 is in Hard range (81-127)
        }
        _ => panic!("Expected PadPressed event"),
    }
}

ID Range Validation Tests

Verify gamepad IDs are correctly mapped to 128-255 range:

Test Coverage:

  • Button IDs: 128-143 (Face buttons: 128-131, D-pad: 132-135, Shoulder buttons: 136-139, etc.)
  • Analog stick IDs: 128-131 (Left X: 128, Left Y: 129, Right X: 130, Right Y: 131)
  • Trigger IDs: 132-133 (Left trigger: 132, Right trigger: 133)
  • No collision with MIDI note range (0-127)

Example test:

#[test]
fn test_gamepad_id_range_no_midi_collision() {
    use conductor_core::events::InputEvent;
    use std::time::Instant;

    let time = Instant::now();

    // Test button IDs are >= 128
    let button_event = InputEvent::PadPressed {
        pad: 128,  // South button
        velocity: 100,
        time,
    };

    match button_event {
        InputEvent::PadPressed { pad, .. } => {
            assert!(pad >= 128, "Gamepad button ID must be >= 128");
            assert!(pad <= 255, "Gamepad button ID must be <= 255");
        }
        _ => panic!("Expected PadPressed"),
    }

    // Test analog stick IDs are >= 128
    let stick_event = InputEvent::EncoderTurned {
        encoder: 128,  // Left stick X
        value: 64,
        time,
    };

    match stick_event {
        InputEvent::EncoderTurned { encoder, .. } => {
            assert!(encoder >= 128, "Gamepad analog ID must be >= 128");
            assert!(encoder <= 255, "Gamepad analog ID must be <= 255");
        }
        _ => panic!("Expected EncoderTurned"),
    }
}

Button/Axis Mapping Correctness Tests

Verify correct mapping of standard gamepad layout:

Test Coverage:

  • Face buttons (South/East/West/North)
  • D-pad (Up/Down/Left/Right)
  • Shoulder buttons (L1/R1/L2/R2)
  • Stick buttons (L3/R3)
  • Start/Select/Guide buttons
  • Analog sticks (Left/Right X/Y)
  • Analog triggers (L2/R2)

Example test:

#[test]
fn test_standard_gamepad_button_mapping() {
    // Standard SDL2 gamepad button mappings
    const BUTTON_SOUTH: u8 = 128;    // A/Cross/B
    const BUTTON_EAST: u8 = 129;     // B/Circle/A
    const BUTTON_WEST: u8 = 130;     // X/Square/Y
    const BUTTON_NORTH: u8 = 131;    // Y/Triangle/X
    const DPAD_UP: u8 = 132;
    const DPAD_DOWN: u8 = 133;
    const DPAD_LEFT: u8 = 134;
    const DPAD_RIGHT: u8 = 135;

    // Verify no ID collisions
    let button_ids = vec![
        BUTTON_SOUTH, BUTTON_EAST, BUTTON_WEST, BUTTON_NORTH,
        DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT
    ];

    let mut seen = std::collections::HashSet::new();
    for id in button_ids {
        assert!(seen.insert(id), "Duplicate button ID: {}", id);
        assert!(id >= 128, "Button ID must be >= 128");
    }
}

Integration Tests

Integration tests verify multi-component interactions and complex workflows.

Hybrid Mode Event Stream Tests

Test simultaneous MIDI + gamepad event processing:

# Run integration tests
cargo test --test integration

Test Coverage:

  • Simultaneous MIDI and gamepad events
  • Event stream merging (single InputEvent channel)
  • No event loss or corruption
  • Correct event ordering
  • Thread synchronization

Example integration test:

#[test]
async fn test_hybrid_mode_event_stream() {
    use conductor_daemon::input_manager::{InputManager, InputMode};
    use tokio::sync::mpsc;

    let (event_tx, mut event_rx) = mpsc::channel(1024);
    let (command_tx, _command_rx) = mpsc::channel(32);

    let mut manager = InputManager::new(
        Some("Maschine Mikro MK3".to_string()),
        true,
        InputMode::Both  // Hybrid mode
    );

    // Connect both devices
    manager.connect(event_tx, command_tx).unwrap();

    // Simulate MIDI event
    // ... (MIDI event simulation)

    // Simulate gamepad event
    // ... (gamepad event simulation)

    // Verify both events arrive in order
    let event1 = event_rx.recv().await.unwrap();
    let event2 = event_rx.recv().await.unwrap();

    // Verify event types and ordering
    // ...
}

MIDI + Gamepad Event Ordering Tests

Verify events maintain temporal ordering:

Test Coverage:

  • Timestamp-based ordering
  • No race conditions
  • Event interleaving
  • Microsecond-level timing precision

Example test:

#[test]
fn test_event_ordering_with_timestamps() {
    use conductor_core::events::InputEvent;
    use std::time::{Duration, Instant};

    let base_time = Instant::now();

    // Create events with precise timestamps
    let midi_event = InputEvent::PadPressed {
        pad: 60,  // MIDI note
        velocity: 100,
        time: base_time,
    };

    let gamepad_event = InputEvent::PadPressed {
        pad: 128,  // Gamepad button
        velocity: 100,
        time: base_time + Duration::from_millis(10),
    };

    // Verify timestamps for ordering
    match (midi_event, gamepad_event) {
        (InputEvent::PadPressed { time: t1, .. },
         InputEvent::PadPressed { time: t2, .. }) => {
            assert!(t2 > t1, "Gamepad event should have later timestamp");
        }
        _ => panic!("Expected PadPressed events"),
    }
}

Device Disconnection/Reconnection Tests

Test automatic reconnection behavior:

Test Coverage:

  • Detect device disconnection
  • Automatic reconnection attempts
  • Exponential backoff (1s, 2s, 4s, 8s, 16s, 30s)
  • Maximum retry limit (6 attempts)
  • State restoration after reconnection
  • Event stream recovery

Example test:

#[test]
async fn test_gamepad_reconnection_logic() {
    use conductor_daemon::gamepad_device::GamepadDeviceManager;
    use tokio::sync::mpsc;
    use std::time::Duration;

    let (event_tx, _event_rx) = mpsc::channel(1024);
    let (command_tx, mut command_rx) = mpsc::channel(32);

    let mut manager = GamepadDeviceManager::new(true);  // auto_reconnect = true

    // Simulate disconnection by connecting then disconnecting
    if let Ok(_) = manager.connect(event_tx.clone(), command_tx.clone()) {
        manager.disconnect();

        // Verify disconnection detected
        assert!(!manager.is_connected());

        // Wait for reconnection attempt (background thread)
        tokio::time::sleep(Duration::from_secs(2)).await;

        // Check for reconnection command
        if let Ok(cmd) = tokio::time::timeout(
            Duration::from_secs(1),
            command_rx.recv()
        ).await {
            // Verify reconnection command received
            // ...
        }
    }
}

Auto-Reconnection Logic Tests

Verify reconnection backoff and retry behavior:

Test Coverage:

  • Backoff schedule: 1s, 2s, 4s, 8s, 16s, 30s
  • Maximum 6 attempts
  • DaemonCommand::DeviceReconnectionResult sent on completion
  • No resource leaks during retries
  • Thread cleanup on failure

Mode Switching with Gamepad Tests

Test switching between input modes during runtime:

Test Coverage:

  • Switch from MidiOnly to Both
  • Switch from GamepadOnly to Both
  • Switch from Both to MidiOnly
  • Switch from Both to GamepadOnly
  • Clean device disconnection during mode change
  • No event loss during transition

Example test:

#[test]
fn test_mode_switching_midi_to_hybrid() {
    use conductor_daemon::input_manager::{InputManager, InputMode};

    // Start with MIDI only
    let mut manager = InputManager::new(
        Some("Maschine Mikro MK3".to_string()),
        true,
        InputMode::MidiOnly
    );

    assert!(manager.has_midi_manager());
    assert!(!manager.has_gamepad_manager());

    // Switch to hybrid mode (would require runtime mode switching API)
    // Note: Current implementation requires manager recreation
    // Future enhancement: dynamic mode switching

    // Create new manager with Both mode
    let manager_hybrid = InputManager::new(
        Some("Maschine Mikro MK3".to_string()),
        true,
        InputMode::Both
    );

    assert!(manager_hybrid.has_midi_manager());
    assert!(manager_hybrid.has_gamepad_manager());
}

Manual Testing

Manual testing with physical game controllers is essential for validating real-world behavior.

Physical Gamepad Connection

Test Procedure:

  1. Connect physical gamepad via USB or Bluetooth
  2. Launch Conductor daemon with gamepad support
  3. Verify gamepad detected and connected
  4. Check logs for connection confirmation
# Start daemon with gamepad-only mode
cargo run --release -- --input-mode gamepad

# Or hybrid mode (MIDI + gamepad)
cargo run --release -- --input-mode both

# Check logs for connection status
tail -f ~/.conductor/daemon.log

Expected Output:

[INFO] Gamepad connected: Xbox Series Controller (ID 0)
[INFO] Gamepad events: 15 buttons, 6 axes
[INFO] Polling thread started

Button Mapping Verification

Test Procedure:

  1. Press each button on the gamepad
  2. Verify correct button ID assigned (128-255)
  3. Check Event Console for button events
  4. Verify no duplicate IDs

Manual Test Checklist:

  • South button (A/Cross/B) → ID 128
  • East button (B/Circle/A) → ID 129
  • West button (X/Square/Y) → ID 130
  • North button (Y/Triangle/X) → ID 131
  • D-Pad Up → ID 132
  • D-Pad Down → ID 133
  • D-Pad Left → ID 134
  • D-Pad Right → ID 135
  • Left Shoulder (L1/LB) → ID 136
  • Right Shoulder (R1/RB) → ID 137
  • Left Trigger Button (L2/LT) → ID 138 (if digital)
  • Right Trigger Button (R2/RT) → ID 139 (if digital)
  • Left Stick Button (L3) → ID 140
  • Right Stick Button (R3) → ID 141
  • Start/Options → ID 142
  • Select/Share → ID 143

Verification Command:

# Open Event Console in GUI
# Press each button and verify ID appears correctly

Analog Stick Dead Zone Testing

Test dead zone behavior for analog sticks:

Test Procedure:

  1. Leave analog sticks at center (neutral) position
  2. Verify no events generated (dead zone active)
  3. Move stick slightly (within dead zone)
  4. Verify no events still (dead zone threshold)
  5. Move stick beyond dead zone
  6. Verify EncoderTurned events generated
  7. Return stick to center
  8. Verify events stop (dead zone reactivated)

Dead Zone Configuration (default: 0.1 or 10%):

// Dead zone prevents drift from neutral position
const ANALOG_DEAD_ZONE: f32 = 0.1;  // 10% of full range

Manual Test Checklist:

  • Left stick neutral → no events
  • Left stick small movement → no events (within dead zone)
  • Left stick large movement → events generated
  • Left stick return to center → events stop
  • Right stick neutral → no events
  • Right stick small movement → no events
  • Right stick large movement → events generated
  • Right stick return to center → events stop

Trigger Threshold Testing

Test analog trigger activation thresholds:

Test Procedure:

  1. Leave triggers released (0.0 position)
  2. Verify no events generated
  3. Pull trigger slightly (below threshold)
  4. Verify no events (threshold not met)
  5. Pull trigger beyond threshold
  6. Verify EncoderTurned events generated
  7. Release trigger
  8. Verify events stop

Trigger Configuration (default threshold: 0.1 or 10%):

// Threshold for analog trigger activation
const TRIGGER_THRESHOLD: f32 = 0.1;  // 10% of full pull

Manual Test Checklist:

  • Left trigger released → no events
  • Left trigger slight pull → no events (below threshold)
  • Left trigger half pull → events generated
  • Left trigger full pull → events generated (max value)
  • Left trigger release → events stop
  • Right trigger released → no events
  • Right trigger slight pull → no events
  • Right trigger half pull → events generated
  • Right trigger full pull → events generated
  • Right trigger release → events stop

Template Loading Verification

Test gamepad template loading:

Test Procedure:

  1. Create gamepad template file (TOML)
  2. Place in ~/.conductor/templates/ directory
  3. Select template in GUI
  4. Verify mappings loaded correctly
  5. Test button mappings from template
  6. Verify actions execute correctly

Example Template (Xbox controller):

# ~/.conductor/templates/xbox-series-controller.toml

[device]
name = "Xbox Series Controller"
type = "gamepad"

[[modes]]
name = "Default"
color = "blue"

[[modes.mappings]]
trigger = { PadPressed = { pad = 128, velocity_range = [0, 127] } }  # A button
action = { Keystroke = { key = "Space", modifiers = [] } }

[[modes.mappings]]
trigger = { PadPressed = { pad = 129 } }  # B button
action = { Keystroke = { key = "Escape", modifiers = [] } }

Verification Commands:

# List available templates
conductorctl templates list

# Load template
conductorctl templates load xbox-series-controller

# Verify template active
conductorctl status

Manual Test Checklist:

  • Template file exists and is valid TOML
  • Template appears in GUI template selector
  • Template loads without errors
  • Mappings appear in mapping list
  • Button presses trigger correct actions
  • LED feedback works (if supported)

MIDI Learn with Gamepad

Test MIDI Learn mode with gamepad buttons:

Test Procedure:

  1. Open GUI configuration
  2. Create new mapping
  3. Click “MIDI Learn” button
  4. Press gamepad button
  5. Verify button ID captured (128-255)
  6. Assign action to mapping
  7. Test mapping works

Manual Test Checklist:

  • MIDI Learn mode activates
  • Gamepad button press detected
  • Correct button ID captured
  • Button ID displayed in UI
  • Mapping saved successfully
  • Mapping triggers action correctly
  • Multiple gamepad buttons can be learned
  • Chord detection works (multiple buttons)
  • Long press detection works
  • Double-tap detection works

Device Disconnection/Reconnection

Test device hot-plugging:

Test Procedure:

  1. Connect gamepad and verify active
  2. Physically disconnect gamepad (unplug USB or disable Bluetooth)
  3. Verify daemon detects disconnection
  4. Wait for reconnection attempts (check logs)
  5. Reconnect gamepad
  6. Verify daemon reconnects automatically
  7. Test button presses work after reconnection

Manual Test Checklist:

  • Daemon detects disconnection immediately
  • Logs show “Gamepad disconnected” message
  • Reconnection attempts start (1s, 2s, 4s, 8s, 16s, 30s backoff)
  • Logs show reconnection attempts
  • Gamepad reconnects when plugged back in
  • Logs show “Gamepad reconnected” message
  • Button presses work immediately after reconnection
  • No event loss after reconnection
  • State restored (mode, mappings, etc.)

Cross-Platform Verification

Test gamepad support across different operating systems:

Platform-Specific Testing:

macOS:

  • USB gamepad detection
  • Bluetooth gamepad detection
  • Xbox controller support
  • PlayStation controller support
  • Nintendo Switch Pro controller support
  • Generic HID gamepad support
  • Input Monitoring permissions granted

Linux:

  • USB gamepad detection via evdev
  • Bluetooth gamepad detection
  • Xbox controller support (xpad kernel module)
  • PlayStation controller support (hid-sony kernel module)
  • udev rules configured correctly
  • Permissions for /dev/input/event*

Windows:

  • USB gamepad detection
  • Bluetooth gamepad detection
  • Xbox controller support (native)
  • PlayStation controller support (DS4Windows)
  • DirectInput gamepad support
  • XInput gamepad support

Platform-Specific Testing Notes

macOS

Hardware Requirements:

  • Real hardware required (no emulation available)
  • Native SDL2 support via macOS HID APIs

Permissions:

  • Input Monitoring permissions required (System Settings → Privacy & Security)
  • Grant permissions to Terminal or Conductor daemon

Testing Approach:

  • Use physical controllers only
  • Test native Apple controllers (PS5, Xbox Series)
  • Test third-party controllers (8BitDo, Logitech)
# Check Input Monitoring permissions
tccutil reset SystemPolicyInputMonitoring

# Grant permissions to Terminal
sudo sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "INSERT INTO access VALUES('kTCCServiceAccessibility','com.apple.Terminal',0,1,1,NULL,NULL,NULL,NULL,NULL,NULL,NULL);"

Linux

Hardware Requirements:

  • Real hardware preferred
  • evdev emulation possible with evemu-device

Permissions:

  • User must be in input group
  • udev rules required for device access

Testing Approach:

  • Test with real controllers via USB/Bluetooth
  • Use evdev emulation for CI/CD testing
  • Test with various kernel modules (xpad, hid-sony)
# Add user to input group
sudo usermod -a -G input $USER

# Create udev rule for gamepad access
echo 'KERNEL=="event*", SUBSYSTEM=="input", MODE="0666"' | \
  sudo tee /etc/udev/rules.d/99-input.rules

# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger

# List connected gamepads
ls -l /dev/input/event*

# Test with evtest
sudo evtest /dev/input/event0

evdev Emulation for Testing:

# Install evemu tools
sudo apt-get install evemu-tools

# Record gamepad events to file
sudo evemu-record /dev/input/event0 > gamepad.events

# Replay events for testing
sudo evemu-play /dev/input/event0 < gamepad.events

Windows

Hardware Requirements:

  • Real hardware preferred
  • Virtual gamepad possible with vJoy

Testing Approach:

  • Test with real Xbox/PlayStation controllers
  • Use vJoy for virtual gamepad testing
  • Test both DirectInput and XInput modes

vJoy for Virtual Gamepads:

# Install vJoy
# Download from: https://sourceforge.net/projects/vjoystick/

# Configure virtual gamepad
vJoyConf.exe

# Test with gamepad tester
# Download from: https://gamepad-tester.com/

Test Coverage Requirements

Conductor maintains high test coverage standards for gamepad functionality:

Core Functionality Coverage (Target: 90%+)

  • InputManager: 95% line coverage

    • Mode selection (MidiOnly, GamepadOnly, Both)
    • Device initialization
    • Connection lifecycle
    • Event stream merging
  • GamepadDeviceManager: 90% line coverage

    • Device detection
    • Connection/disconnection
    • Event polling loop
    • Reconnection logic
    • State management
  • Event Conversion: 95% line coverage

    • Button press/release conversion
    • Analog stick conversion
    • Trigger conversion
    • ID range validation

Edge Cases Coverage (Target: 85%+)

  • Device Not Found:

    • No gamepad connected
    • Invalid device ID
    • Device disconnected during operation
    • Rapid connect/disconnect cycles
  • SDL2 Unavailable:

    • SDL2 library not installed
    • SDL2 initialization failure
    • gilrs library unavailable
  • Error Handling Paths:

    • Connection timeout
    • Thread spawn failure
    • Channel send/receive errors
    • Reconnection limit exceeded

Example edge case test:

#[test]
fn test_no_gamepad_connected_error() {
    use conductor_daemon::gamepad_device::GamepadDeviceManager;
    use tokio::sync::mpsc;

    let (event_tx, _event_rx) = mpsc::channel(1024);
    let (command_tx, _command_rx) = mpsc::channel(32);

    let mut manager = GamepadDeviceManager::new(false);  // auto_reconnect = false

    // Attempt connection with no gamepad present
    let result = manager.connect(event_tx, command_tx);

    // Should return error
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.contains("No gamepad detected"));

    // Manager should remain disconnected
    assert!(!manager.is_connected());
}

Test Commands Reference

# Run all gamepad-specific unit tests
cargo test gamepad

# Run InputManager tests
cargo test input_manager

# Run integration tests (requires physical gamepad)
cargo test --test integration

# Run all tests with verbose output
cargo test gamepad -- --nocapture

# Run tests and show timing
cargo nextest run gamepad

# Run tests with coverage
cargo llvm-cov --test gamepad --html

# Run specific test
cargo test test_gamepad_button_press_detection

Test Fixtures and Mocking

Mock Gamepad Devices for CI/CD

For CI/CD environments without physical hardware:

Linux (evdev emulation):

#[cfg(test)]
mod mock_gamepad {
    use std::process::Command;

    pub fn create_virtual_gamepad() -> Result<String, String> {
        // Create virtual gamepad using evemu
        let output = Command::new("evemu-device")
            .arg("/path/to/gamepad.desc")
            .output()
            .map_err(|e| format!("Failed to create virtual gamepad: {}", e))?;

        if output.status.success() {
            let device = String::from_utf8_lossy(&output.stdout);
            Ok(device.trim().to_string())
        } else {
            Err("Failed to create virtual gamepad".to_string())
        }
    }
}

Windows (vJoy):

#[cfg(target_os = "windows")]
#[cfg(test)]
mod mock_gamepad {
    use winapi::um::winuser::*;

    pub fn create_virtual_gamepad() -> Result<(), String> {
        // Initialize vJoy device
        // ...
        Ok(())
    }
}

Simulated HID Events

For unit tests without physical devices:

#[cfg(test)]
mod simulated_events {
    use conductor_core::events::InputEvent;
    use std::time::Instant;

    pub fn simulate_button_press(button: u8) -> InputEvent {
        InputEvent::PadPressed {
            pad: button,
            velocity: 100,
            time: Instant::now(),
        }
    }

    pub fn simulate_button_release(button: u8) -> InputEvent {
        InputEvent::PadReleased {
            pad: button,
            time: Instant::now(),
        }
    }

    pub fn simulate_analog_stick_movement(axis: u8, value: u8) -> InputEvent {
        InputEvent::EncoderTurned {
            encoder: axis,
            value,
            time: Instant::now(),
        }
    }
}

Test Data for Button/Axis Mapping

Standard test data for gamepad button/axis mapping:

#[cfg(test)]
mod test_data {
    // Standard gamepad button IDs (Xbox layout)
    pub const BUTTON_SOUTH: u8 = 128;     // A/Cross/B
    pub const BUTTON_EAST: u8 = 129;      // B/Circle/A
    pub const BUTTON_WEST: u8 = 130;      // X/Square/Y
    pub const BUTTON_NORTH: u8 = 131;     // Y/Triangle/X
    pub const DPAD_UP: u8 = 132;
    pub const DPAD_DOWN: u8 = 133;
    pub const DPAD_LEFT: u8 = 134;
    pub const DPAD_RIGHT: u8 = 135;
    pub const LEFT_SHOULDER: u8 = 136;    // L1/LB
    pub const RIGHT_SHOULDER: u8 = 137;   // R1/RB
    pub const LEFT_TRIGGER_BTN: u8 = 138; // L2/LT (digital)
    pub const RIGHT_TRIGGER_BTN: u8 = 139;// R2/RT (digital)
    pub const LEFT_STICK: u8 = 140;       // L3
    pub const RIGHT_STICK: u8 = 141;      // R3
    pub const START: u8 = 142;
    pub const SELECT: u8 = 143;

    // Analog axis IDs
    pub const LEFT_STICK_X: u8 = 128;
    pub const LEFT_STICK_Y: u8 = 129;
    pub const RIGHT_STICK_X: u8 = 130;
    pub const RIGHT_STICK_Y: u8 = 131;
    pub const LEFT_TRIGGER: u8 = 132;     // L2/LT (analog)
    pub const RIGHT_TRIGGER: u8 = 133;    // R2/RT (analog)

    // Test velocity values
    pub const VELOCITY_SOFT: u8 = 30;     // 0-40
    pub const VELOCITY_MEDIUM: u8 = 60;   // 41-80
    pub const VELOCITY_HARD: u8 = 100;    // 81-127

    // Test analog values (0-127 normalized)
    pub const ANALOG_CENTER: u8 = 64;
    pub const ANALOG_MIN: u8 = 0;
    pub const ANALOG_MAX: u8 = 127;
}

Debugging Tips

Event Console Usage

The GUI Event Console is invaluable for debugging gamepad events:

  1. Open Conductor GUI
  2. Navigate to “Event Console” tab
  3. Press gamepad buttons/move sticks
  4. Observe live event stream with IDs and values

Event Console Output Example:

[14:23:45.123] PadPressed { pad: 128, velocity: 100 }  // South button
[14:23:45.234] PadReleased { pad: 128 }
[14:23:46.001] EncoderTurned { encoder: 128, value: 95, direction: CW, delta: 31 }  // Left stick X
[14:23:46.112] PadPressed { pad: 132, velocity: 100 }  // D-Pad Up

Log Inspection

Enable debug logging for gamepad module:

# Set RUST_LOG environment variable
export RUST_LOG=conductor_daemon::gamepad_device=debug

# Or for all Conductor modules
export RUST_LOG=conductor=debug

# Run daemon
cargo run --release

Log Output Example:

[DEBUG conductor_daemon::gamepad_device] Gamepad 0 connected: Xbox Series Controller
[DEBUG conductor_daemon::gamepad_device] Polling thread started for gamepad 0
[DEBUG conductor_daemon::gamepad_device] Button pressed: South (128) velocity 100
[DEBUG conductor_daemon::gamepad_device] Axis movement: LeftX (128) value 95 delta 31
[DEBUG conductor_daemon::gamepad_device] Button released: South (128)

gilrs Event Debugging

For low-level HID event debugging:

#[cfg(test)]
fn debug_gilrs_events() {
    use gilrs::{Gilrs, Event};

    let mut gilrs = Gilrs::new().unwrap();

    println!("Detected gamepads:");
    for (_id, gamepad) in gilrs.gamepads() {
        println!("  {} (ID: {})", gamepad.name(), gamepad.id());
        println!("    Buttons: {}", gamepad.buttons().count());
        println!("    Axes: {}", gamepad.axes().count());
    }

    println!("\nPress buttons to see raw gilrs events (Ctrl+C to exit):");

    loop {
        while let Some(Event { id, event, time }) = gilrs.next_event() {
            println!("[{:?}] Gamepad {}: {:?}", time, id, event);
        }
    }
}

Run Debug Tool:

cargo test debug_gilrs_events -- --nocapture --ignored

Continuous Integration (CI/CD)

Gamepad tests in CI environments require special considerations:

GitHub Actions Configuration

name: Gamepad Tests

on: [push, pull_request]

jobs:
  test-gamepad-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1

      # Unit tests don't require physical hardware
      - name: Run gamepad unit tests
        run: cargo test gamepad --lib

      - name: Run InputManager tests
        run: cargo test input_manager

  test-gamepad-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rust-lang/setup-rust-toolchain@v1

      # Install evemu for virtual gamepad
      - name: Install evemu
        run: sudo apt-get install -y evemu-tools

      # Create virtual gamepad
      - name: Setup virtual gamepad
        run: |
          sudo evemu-device ./tests/fixtures/virtual_gamepad.desc &
          sleep 2

      # Run integration tests with virtual gamepad
      - name: Run gamepad integration tests
        run: cargo test --test integration -- gamepad

Coverage in CI

- name: Generate gamepad test coverage
  run: |
    cargo install cargo-llvm-cov
    cargo llvm-cov --test gamepad --lcov --output-path lcov-gamepad.info

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: lcov-gamepad.info
    flags: gamepad

Manual Test Checklist

Complete manual testing checklist for game controller support:

Device Detection

  • Gamepad detected via USB
  • Gamepad detected via Bluetooth
  • Multiple gamepads detected simultaneously
  • Gamepad ID assigned correctly (0-based)
  • Gamepad name retrieved correctly

Button ID Mapping (128-255)

  • Face buttons (South/East/West/North): 128-131
  • D-Pad (Up/Down/Left/Right): 132-135
  • Shoulder buttons (L1/R1): 136-137
  • Trigger buttons (L2/R2 digital): 138-139
  • Stick buttons (L3/R3): 140-141
  • Start/Select buttons: 142-143
  • No ID collision with MIDI notes (0-127)

Analog Stick Movement

  • Left stick X axis detected (ID 128)
  • Left stick Y axis detected (ID 129)
  • Right stick X axis detected (ID 130)
  • Right stick Y axis detected (ID 131)
  • Dead zone prevents drift (<10% movement)
  • Full range movement (0-127 values)
  • Direction detection (Clockwise/CounterClockwise)

Trigger Pull Detection

  • Left trigger analog detected (ID 132)
  • Right trigger analog detected (ID 133)
  • Trigger threshold prevents noise (<10% pull)
  • Full pull range (0-127 values)
  • Smooth value transitions

Hybrid MIDI + Gamepad

  • Both MIDI and gamepad devices connected
  • Events from both devices processed
  • No event loss or corruption
  • Correct event ordering maintained
  • Mode switching works with both inputs

Template Loading

  • Gamepad template loads successfully
  • Mappings appear in mapping list
  • Button presses trigger correct actions
  • Template selector shows gamepad templates
  • Template validation passes

MIDI Learn with Gamepad

  • MIDI Learn mode captures gamepad buttons
  • Button IDs 128-255 displayed correctly
  • Long press detection works in MIDI Learn
  • Double-tap detection works in MIDI Learn
  • Chord detection works (multiple buttons)

Device Disconnection/Reconnection

  • Disconnection detected immediately
  • Reconnection attempts start (exponential backoff)
  • Gamepad reconnects when available
  • Event stream resumes after reconnection
  • State restored (mappings, mode, etc.)
  • Maximum retry limit enforced (6 attempts)

Cross-Platform Verification

  • macOS: USB gamepad detection
  • macOS: Bluetooth gamepad detection
  • macOS: Input Monitoring permissions granted
  • Linux: USB gamepad detection (evdev)
  • Linux: Bluetooth gamepad detection
  • Linux: udev rules configured
  • Windows: USB gamepad detection
  • Windows: Bluetooth gamepad detection
  • Windows: XInput/DirectInput support

Event Console

  • Gamepad events appear in Event Console
  • Button IDs displayed correctly (128-255)
  • Velocity values displayed correctly
  • Analog values displayed correctly
  • Timestamps accurate
  • Event filtering works

Performance

  • Event latency <5ms
  • No event drops at 1000Hz polling
  • CPU usage <5% during active use
  • Memory usage stable (<10MB increase)
  • Thread cleanup on disconnection

Examples

See the integration tests in tests/integration_tests.rs for complete examples of:

  • Velocity detection tests
  • Long press simulation
  • Double-tap detection
  • Chord detection
  • Encoder simulation
  • Complex multi-event scenarios

Additional examples in specialized test suites:

  • tests/event_processing_tests.rs: Aftertouch and pitch bend
  • tests/action_tests.rs: Application launch and volume control
  • tests/action_orchestration_tests.rs: Action sequences and conditionals
  • conductor-core/tests/gamepad_input_test.rs: Game controller event processing

Support

For questions or issues with testing:

  • Check existing integration tests for examples
  • Use the interactive CLI tool to experiment
  • Review the simulator source code in tests/midi_simulator.rs
  • Check gamepad tests in conductor-core/tests/gamepad_input_test.rs
  • Open an issue on GitHub with test failure details

Release Process

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Common Issues and Solutions

Overview

This guide covers the most frequently encountered issues when using Conductor, along with step-by-step solutions. For detailed diagnostic procedures, see Diagnostics Guide.

MIDI Device Not Found

Symptoms

Error: No MIDI input ports available

Or:

Available MIDI input ports:
0: IAC Driver Bus 1
# Your device is missing from the list

Causes

  • USB cable not connected
  • Device not powered on
  • MIDI driver not installed (macOS/Windows)
  • Device in wrong mode (some controllers have multiple modes)
  • MIDI port disabled in system settings

Solutions

1. Check Physical Connection

# macOS: Check USB device enumeration
system_profiler SPUSBDataType | grep -i mikro
# or
system_profiler SPUSBDataType | grep -i midi

# Expected output:
# Maschine Mikro MK3:
#   Product ID: 0x1600
#   Vendor ID: 0x17cc (Native Instruments)

If device not found:

  • Try a different USB port
  • Try a different USB cable
  • Power cycle the device (unplug, wait 10 seconds, replug)
  • Check device is powered on (some controllers have power switches)

2. Verify MIDI Setup (macOS)

# Open Audio MIDI Setup
open -a "Audio MIDI Setup"

In the MIDI Studio window (Window → Show MIDI Studio):

  • Verify your device appears
  • Check it’s not grayed out (indicates active connection)
  • If grayed out, right-click and select “Enable”

Reset MIDI configuration (if corrupted):

# Quit Audio MIDI Setup first
rm ~/Library/Preferences/com.apple.audio.AudioMIDISetup.plist
rm ~/Library/Preferences/ByHost/com.apple.audio.AudioMIDISetup.*
# Reopen Audio MIDI Setup

3. Install/Reinstall Drivers

macOS/Windows:

  1. Download Native Access
  2. Sign in (free account)
  3. Install drivers for your device
  4. Restart computer after installation
  5. Reconnect device

Linux:

  • Most MIDI controllers work with built-in ALSA drivers
  • Install alsa-utils: sudo apt install alsa-utils
  • List devices: aconnect -l

4. Test with System Tools

macOS:

# List MIDI devices using system_profiler
system_profiler SPUSBDataType | grep -B 10 -A 10 MIDI

# Test with conductor diagnostic
cargo run --bin test_midi

Linux:

# List ALSA MIDI devices
aconnect -l

# Test with amidi
amidi -l

5. Check Port Numbers

# List all available ports
cargo run --release

# Try each port number
cargo run --bin midi_diagnostic 0
cargo run --bin midi_diagnostic 1
cargo run --bin midi_diagnostic 2
# ... press a pad/key on your controller to verify connection

Still Not Working?

  1. Try a different computer (to rule out hardware failure)
  2. Test with manufacturer’s software (e.g., NI Controller Editor)
  3. Check for firmware updates
  4. Contact manufacturer support

LEDs Not Working

Symptoms

  • Pads press correctly but LEDs don’t light up
  • LEDs flash once then go dark
  • Wrong pads lighting up
  • LEDs stuck on or flickering

Causes

  • Input Monitoring permission not granted (macOS)
  • HID driver not installed
  • Profile/coordinate mapping issues
  • HID device already in use by another application
  • Incorrect LED scheme selected

Solutions

1. Grant Input Monitoring Permission (macOS)

Step-by-step:

  1. Run Conductor:

    cargo run --release 2
    
  2. macOS will show a permission dialog

  3. Click Open System Settings

  4. In Privacy & SecurityInput Monitoring:

    • Find conductor or Terminal (if running via cargo)
    • Toggle switch to ON
    • If switch is already ON, toggle OFF then ON again
  5. Restart Conductor:

    cargo run --release 2
    

Verify permission granted:

DEBUG=1 cargo run --release 2

Look for:

[DEBUG] HID device opened successfully
[DEBUG] LED controller initialized

If you see “Failed to open HID device”, permission is not granted.

2. Test LED Hardware

# Run LED diagnostic tool
cargo run --bin led_diagnostic

Expected output:

✓ Device found: Maschine Mikro MK3
✓ HID device opened successfully
✓ Testing LED control...
✓ LED diagnostic complete

Error output:

✗ Failed to open HID device

If this fails, see Diagnostics Guide for HID troubleshooting.

3. Install/Verify Native Instruments Drivers

macOS/Windows:

  1. Open Native Access
  2. Verify “Maschine” or controller-specific software is installed
  3. Check for updates
  4. Reinstall if necessary
  5. Restart computer

Test after driver installation:

cargo run --bin led_diagnostic

4. Try Different LED Schemes

Some schemes might not work due to profile issues:

# Try rainbow (doesn't require note mapping)
cargo run --release 2 --led rainbow

# Try static
cargo run --release 2 --led static

# Try reactive (default)
cargo run --release 2 --led reactive

If rainbow/static work but reactive doesn’t, it’s a profile/mapping issue.

5. Fix Coordinate Mapping Issues

Wrong pads lighting up indicates coordinate mapping problems.

Solution: Use a device profile:

# Auto-detect page
cargo run --release 2 --profile path/to/profile.ncmm3

# Force specific page
cargo run --release 2 --profile profile.ncmm3 --pad-page A

Create a profile using Native Instruments Controller Editor if you don’t have one.

See Device Profiles Documentation for complete guide.

6. Check Shared Device Access

If Controller Editor is running simultaneously:

macOS: Conductor uses shared device access (should work)

Verify:

# Check Cargo.toml includes:
hidapi = { version = "2.4", features = ["macos-shared-device"] }

If LEDs work when Controller Editor is closed but not when it’s running:

  • Update to latest Conductor version
  • Rebuild from source: cargo build --release

Advanced Debugging

Enable debug logging and watch for LED updates:

DEBUG=1 cargo run --release 2 --led reactive

Press pads and look for:

[DEBUG] LED update: pad 0 -> color 7 (Green) brightness 2
[DEBUG] HID write: 81 bytes

If you see LED updates logged but LEDs don’t light:

  • Hardware issue (try different USB port)
  • Firmware issue (update device firmware)
  • Driver issue (reinstall NI drivers)

Events Not Triggering

Symptoms

  • Pads press but no actions execute
  • Some pads work, others don’t
  • LEDs work but actions don’t fire
  • Actions trigger randomly or inconsistently

Causes

  • Note numbers don’t match config.toml
  • Wrong mode active
  • Trigger conditions not met (velocity, timing)
  • Config syntax errors
  • Event processing disabled

Solutions

1. Verify Note Numbers

# Find actual note numbers
cargo run --bin pad_mapper 2

Press each pad and write down the note numbers.

Compare with config.toml:

[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 12  # Must match the actual note number from pad_mapper

If they don’t match, update config.toml with correct note numbers.

2. Check Active Mode

Conductor prints the current mode to console:

Mode changed: Default -> Developer
Currently in mode: Developer

Verify you’re in the expected mode:

  • Check encoder hasn’t switched modes accidentally
  • Verify mode-specific mappings are in the correct mode section
  • Try using global mappings for testing

Force to Default mode:

[[global_mappings]]
description = "Reset to default mode"
[global_mappings.trigger]
type = "Note"
note = 0
[global_mappings.action]
type = "ModeChange"
mode = "Default"

3. Test with Simple Mapping

Add a simple test mapping to verify basic functionality:

[[global_mappings]]
description = "Test mapping - Should print to console"
[global_mappings.trigger]
type = "Note"
note = 0  # Bottom-left pad
[global_mappings.action]
type = "Shell"
command = "echo 'Mapping works!' && say 'Mapping works'"

If this works, the issue is with specific trigger/action configuration.

4. Enable Debug Logging

DEBUG=1 cargo run --release 2

Press pads and watch for:

Good output (mapping working):

[MIDI] NoteOn ch:0 note:12 vel:87
[DEBUG] Processed: Note(12) with velocity Medium
[DEBUG] Matched mapping: "Copy text" (mode: Default)
[DEBUG] Executing action: Keystroke(keys: "c", modifiers: ["cmd"])
✓ Action executed successfully

Bad output (no match):

[MIDI] NoteOn ch:0 note:12 vel:87
[DEBUG] Processed: Note(12) with velocity Medium
[DEBUG] No mapping matched for event

If no mapping matched:

  • Note number mismatch
  • Wrong mode
  • Trigger conditions not met

5. Check Velocity/Timing Requirements

VelocityRange triggers require specific velocity:

[trigger]
type = "VelocityRange"
note = 12
min_velocity = 81  # Only triggers on HARD press (81-127)
max_velocity = 127

Solution: Press harder or adjust velocity range:

min_velocity = 0   # Accept any velocity
max_velocity = 127

LongPress triggers require holding:

[trigger]
type = "LongPress"
note = 12
hold_duration_ms = 2000  # Must hold for 2 seconds

Solution: Hold longer or reduce threshold:

hold_duration_ms = 500  # Only 0.5 seconds

DoubleTap requires two quick taps:

[trigger]
type = "DoubleTap"
note = 12
double_tap_timeout_ms = 300  # Must tap twice within 300ms

Solution: Tap faster or increase timeout:

double_tap_timeout_ms = 500  # 0.5 seconds between taps

6. Validate Config Syntax

# Check for TOML syntax errors
cargo check

# Or use online validator
# https://www.toml-lint.com/

Common syntax errors:

  • Missing quotes around strings
  • Wrong bracket type ([] vs [[]])
  • Typos in field names
  • Missing required fields

7. Test MIDI Events

Verify your device is sending events:

cargo run --bin midi_diagnostic 2

Press pads - you should see:

[NoteOn]  ch:0 note:12 vel:87
[NoteOff] ch:0 note:12 vel:0

If no events appear:

  • MIDI connection issue (see “MIDI Device Not Found” above)
  • Device in wrong mode
  • Device needs reset (power cycle)

Still Not Working?

Create a minimal test config:

cat > test-config.toml << 'EOF'
[[modes]]
name = "Test"

[[modes.mappings]]
description = "Test"
[modes.mappings.trigger]
type = "Note"
note = 0
[modes.mappings.action]
type = "Shell"
command = "say 'test works'"
EOF

cargo run --release 2 --config test-config.toml

Press pad 0. If you hear “test works”, the system is functioning.

Profile Detection Issues

Symptoms

  • “Failed to load profile” error
  • Wrong pads lighting up
  • LEDs work without profile but not with profile
  • Auto-page detection not working

Causes

  • Profile file not found
  • Invalid XML format
  • Wrong pad page active
  • Note numbers not in profile
  • Profile path has spaces or special characters

Solutions

1. Verify Profile File Exists

# Check file exists
ls -la path/to/profile.ncmm3

# Try absolute path
cargo run --release 2 --profile "$HOME/Downloads/profile.ncmm3"

# Escape spaces in path
cargo run --release 2 --profile "My\ Profile.ncmm3"

2. Validate Profile XML

Open the .ncmm3 file in a text editor and verify it’s valid XML:

<?xml version="1.0" encoding="UTF-8"?>
<DeviceProfile>
  <DeviceProperties>
    <Name>My Profile</Name>
    <Type>MASCHINE_MIKRO_MK3</Type>
  </DeviceProperties>

  <Mapping>
    <PageList>
      <Page name="Pad Page A">
        <!-- ... -->
      </Page>
    </PageList>
  </Mapping>
</DeviceProfile>

Common issues:

  • Missing <?xml declaration
  • Unclosed tags
  • Invalid characters
  • Corrupted file

Fix: Re-export from Controller Editor or use a backup.

3. Force Specific Pad Page

Auto-detection might fail if notes overlap between pages:

# Instead of auto-detect
cargo run --release 2 --profile profile.ncmm3

# Force page A
cargo run --release 2 --profile profile.ncmm3 --pad-page A

# Try each page
for page in A B C D E F G H; do
    echo "Testing page $page"
    cargo run --release 2 --profile profile.ncmm3 --pad-page $page
    sleep 5
    killall conductor
done

4. Create New Profile

If profile is corrupted:

  1. Open Native Instruments Controller Editor
  2. Select Maschine Mikro MK3
  3. Create a simple profile:
    • Page A: Notes 12-27 (chromatic)
    • Page B: Notes 36-51 (drums)
  4. Save as test-profile.ncmm3
  5. Test: cargo run --release 2 --profile test-profile.ncmm3

5. Debug Profile Loading

DEBUG=1 cargo run --release 2 --profile profile.ncmm3

Look for:

[DEBUG] Loading profile: profile.ncmm3
[DEBUG] Profile loaded: My Profile (MASCHINE_MIKRO_MK3)
[DEBUG] Found 8 pad pages
[DEBUG] Page A: 16 pads mapped (notes 12-27)

If you see errors:

[ERROR] Failed to parse profile: XML error at line 42
[ERROR] Profile validation failed: Missing required element

Fix the profile XML or create a new one.

6. Verify Note Numbers Match Profile

# Run pad mapper
cargo run --bin pad_mapper 2

Press pads and verify the notes are in your profile.

If note 50 is pressed but your profile only has notes 12-27, no LED will light.

Solution: Either:

  • Update profile to include note 50
  • Change hardware to send notes 12-27 (in Controller Editor)

Game Controllers (HID) Issues (v3.0+)

Overview

Conductor v3.0 introduced support for all SDL2-compatible HID devices including gamepads (Xbox, PlayStation, Nintendo Switch Pro), joysticks, racing wheels, flight sticks, HOTAS controllers, and custom controllers. This section covers common issues specific to game controller integration.

For gamepad configuration guidance, see the Gamepad Support Guide.

Gamepad Not Detected

Symptoms

  • No gamepad shown in Event Console
  • conductorctl status doesn’t list gamepad
  • Button presses have no effect
  • “No compatible gamepad detected” message

Causes

  • USB/Bluetooth connection not established
  • System not recognizing controller
  • SDL2 compatibility issues
  • Insufficient permissions (macOS Input Monitoring)
  • Missing drivers (Windows)
  • Controller in incompatible mode

Solutions

1. Verify Physical Connection

USB Connection:

# macOS: Check USB device enumeration
system_profiler SPUSBDataType | grep -i xbox
system_profiler SPUSBDataType | grep -i playstation
system_profiler SPUSBDataType | grep -i controller

# Linux: Check USB devices
lsusb | grep -i xbox
lsusb | grep -i sony
lsusb | grep -i nintendo

# Windows: Device Manager
# Devices and Printers > Game Controllers

Bluetooth Connection:

  • Verify controller is in pairing mode (usually hold PS/Xbox button + Share)
  • Check system Bluetooth settings show controller as connected
  • Try USB connection first to rule out Bluetooth issues
  • Some wireless adapters require specific drivers (Xbox Wireless Adapter on macOS)

If device not found:

  • Try a different USB port (prefer USB 3.0)
  • Try a different USB cable (some cables are charge-only)
  • Power cycle the controller (turn off, wait 10 seconds, turn on)
  • Check controller battery is charged (wireless controllers)
  • Remove and re-pair Bluetooth connection
2. Check System Recognition

macOS:

# Check via System Settings
# System Settings > General > Game Controllers

# Verify controller appears in system report
system_profiler SPUSBDataType | grep -B 5 -A 10 "Xbox\|PlayStation\|Nintendo"

Linux:

# Check joystick devices
ls -la /dev/input/js*
# Expected: /dev/input/js0, /dev/input/js1, etc.

# Test with jstest (install: sudo apt install joystick)
jstest /dev/input/js0

# Check evdev access
ls -la /dev/input/event*

# Verify permissions
groups | grep input

Windows:

1. Open "Set up USB game controllers" (search in Start menu)
2. Verify controller appears in list
3. Click "Properties" to test buttons
4. If shows "Unknown device", driver issue
3. Verify SDL2 Compatibility

Conductor uses SDL2 gamepad mappings. Most modern controllers are compatible, but some require specific configurations.

Test SDL2 detection:

# Enable debug logging to see SDL2 detection
DEBUG=1 conductor --foreground

# Look for:
[DEBUG] SDL2 initialized
[DEBUG] Found gamepad: Xbox 360 Controller (ID: 0)
[DEBUG] Gamepad mapping: 030000005e040000...(SDL2 GUID)

Known compatible controllers:

  • Xbox 360, Xbox One, Xbox Series X|S (all models)
  • PlayStation DualShock 4, DualSense (PS5)
  • Nintendo Switch Pro Controller
  • Steam Controller
  • Generic USB/Bluetooth gamepads with standard layout

If incompatible:

  • Check SDL_GameControllerDB for your controller
  • Some controllers need to be in specific mode (XInput vs DirectInput on Windows)
  • Custom controllers may need manual SDL2 mapping file
4. Grant Input Monitoring Permission (macOS)

Game controllers require Input Monitoring permission, just like MIDI HID devices.

Step-by-step:

  1. Run Conductor:

    conductor --foreground
    
  2. macOS shows permission dialog for Input Monitoring

  3. Click Open System Settings

  4. In Privacy & Security > Input Monitoring:

    • Find conductor or Terminal (if running via cargo)
    • Toggle switch to ON
    • If already ON, toggle OFF then ON to reset
  5. Restart Conductor:

    conductor --foreground
    

Verify permission:

DEBUG=1 conductor --foreground

Look for:

[DEBUG] Input Monitoring permission: Granted
[DEBUG] Gamepad access: Enabled

If you see “Input Monitoring permission denied”, permission not granted.

5. Install/Verify Drivers

macOS:

  • Xbox controllers: Generally work out-of-box
  • Xbox Wireless Adapter: Requires 360Controller driver
  • PlayStation controllers: Work via Bluetooth, some features need DS4Windows equivalent for macOS
  • Switch Pro: Works out-of-box via USB or Bluetooth

Linux:

# Install joystick/gamepad support
sudo apt install joystick xboxdrv

# Load xpad kernel module (Xbox controllers)
sudo modprobe xpad

# For Steam Controller
sudo apt install steam-devices

# Check kernel drivers loaded
lsmod | grep -E "xpad|joydev|evdev"

Windows:

  • Xbox controllers: Use official Xbox drivers (usually automatic via Windows Update)
  • PlayStation controllers: Require DS4Windows for full functionality
  • Switch Pro: Works but may need BetterJoy
  • Check Device Manager for “Unknown device” under Game Controllers
6. Test with Conductor Event Console
# Start daemon
conductor --foreground

# In another terminal, watch events
conductorctl events --follow

Press buttons on your gamepad - you should see:

[GAMEPAD] Button: 128 (A/Cross/B) | State: Pressed
[GAMEPAD] Button: 128 | State: Released

If no events appear:

  • Controller not detected by SDL2
  • Permission issue (macOS)
  • Driver issue (Windows/Linux)
  • Controller needs reset (see next section)
7. Reset Controller

Many connection issues resolve with a controller reset:

Xbox Controllers:

1. Hold Xbox button for 10 seconds (powers off)
2. Wait 10 seconds
3. Press Xbox button to power on
4. Reconnect to PC

PlayStation Controllers:

1. Find small reset button on back (near L2)
2. Use paperclip, press and hold 5 seconds
3. Reconnect via USB
4. Re-pair Bluetooth if needed

Switch Pro Controller:

1. Press and hold Sync button (top left) for 5 seconds
2. Release and press Home button
3. Reconnect via USB or re-pair Bluetooth

Buttons Not Triggering

Symptoms

  • Controller detected but button presses don’t trigger actions
  • Some buttons work, others don’t
  • Event Console shows button events but mappings don’t execute
  • Actions trigger randomly or on wrong buttons

Causes

  • Button IDs don’t match config (0-127 vs 128-255 range)
  • Wrong trigger type used
  • MIDI Learn didn’t detect button
  • Mode mismatch
  • Trigger conditions not met

Solutions

1. Verify Button ID Range

Critical: Gamepad buttons use IDs 128-255, not MIDI’s 0-127 range.

Common mistake:

# WRONG - This is a MIDI note, not gamepad button
[modes.mappings.trigger]
type = "Note"
note = 0  # MIDI range (0-127)

# CORRECT - Gamepad button ID
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Gamepad range (128-255)

ID Ranges:

  • MIDI devices: 0-127 (notes, CC)
  • Gamepad buttons: 128-255
  • No overlap, no conflicts
2. Use Event Console to Find Button IDs
# Start Event Console
conductorctl events --follow --type gamepad

Press each button and note the ID:

[GAMEPAD] Button: 128 | State: Pressed  # A (Xbox) / Cross (PS) / B (Switch)
[GAMEPAD] Button: 129 | State: Pressed  # B (Xbox) / Circle (PS) / A (Switch)
[GAMEPAD] Button: 132 | State: Pressed  # D-Pad Up
[GAMEPAD] Button: 136 | State: Pressed  # LB / L1 / L

Update your config with actual IDs:

[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Use the exact ID from Event Console
[modes.mappings.action]
type = "Keystroke"
keys = "Return"
3. Use MIDI Learn for Automatic Detection

GUI Method (Recommended):

  1. Open Conductor GUI
  2. Navigate to mappings
  3. Click “Learn” button next to trigger field
  4. Press button on gamepad
  5. Conductor auto-generates correct trigger config

Pattern Detection:

  • Press once → GamepadButton
  • Press twice quickly → DoubleTap
  • Hold button → LongPress
  • Press multiple buttons → GamepadButtonChord

See MIDI Learn Guide for details.

4. Check Trigger Type Matches Input

Button triggers require GamepadButton type:

# Face buttons, D-pad, shoulders, etc.
[modes.mappings.trigger]
type = "GamepadButton"
button = 128

Stick triggers require GamepadAnalogStick type:

# Left/right stick movement
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130  # Right stick X-axis
direction = "Clockwise"

Trigger triggers require GamepadTrigger type:

# L2/R2, LT/RT, ZL/ZR
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Right trigger
threshold = 128
5. Enable Debug Logging
DEBUG=1 conductor --foreground

Press buttons and watch for:

Good output (button detected, mapping matched):

[GAMEPAD] Button 128 pressed (A/Cross/B)
[DEBUG] Processed: GamepadButton(128)
[DEBUG] Matched mapping: "Confirm Action" (mode: Default)
[DEBUG] Executing action: Keystroke(keys: "Return")
✓ Action executed successfully

Bad output (button detected, no mapping):

[GAMEPAD] Button 128 pressed (A/Cross/B)
[DEBUG] Processed: GamepadButton(128)
[DEBUG] No mapping matched for event

If no mapping matched:

  • Button ID mismatch (check config vs Event Console)
  • Wrong mode active
  • Wrong trigger type
  • Trigger conditions not met (velocity, timing)
6. Test with Simple Mapping

Add a simple test mapping to verify basic functionality:

[[global_mappings]]
description = "Test gamepad - A button"
[global_mappings.trigger]
type = "GamepadButton"
button = 128  # A button (Xbox) / Cross (PS) / B (Switch)
[global_mappings.action]
type = "Shell"
command = "echo 'Gamepad works!' && say 'Gamepad works'"

If this works, issue is with specific trigger/action configuration.

Analog Stick Drift / False Triggers

Symptoms

  • Actions trigger without touching stick
  • Constant movement detected
  • Stick “stuck” in one direction
  • Unwanted repeated actions

Causes

  • Hardware stick drift (worn potentiometers)
  • Dead zone too small
  • Threshold too sensitive
  • Stick calibration issue

Solutions

1. Automatic Dead Zone (10%)

Conductor automatically applies a 10% dead zone to prevent false triggers from stick drift.

How it works:

  • Stick center: 128 (0-255 range)
  • Dead zone: 115-141 (10% in each direction)
  • Values in dead zone treated as 128 (neutral)

This prevents small drift values from triggering actions.

2. Check Hardware Drift

Test stick in system settings:

  • macOS: System Settings > Game Controllers > Properties
  • Linux: jstest /dev/input/js0
  • Windows: “Set up USB game controllers” > Properties > Test

Look for:

  • Stick position drifts without touching
  • Values don’t return to center (128)
  • Erratic movement when stationary

If hardware drift exceeds 10% (values outside 115-141 range), hardware issue.

3. Increase Trigger Threshold

Instead of analog stick trigger, use button-based threshold:

# Instead of this (too sensitive)
[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130  # Right stick X
direction = "Clockwise"

# Use this (requires more movement)
[modes.mappings.trigger]
type = "GamepadButton"
button = 135  # D-Pad right (more deliberate)

Or increase threshold for analog triggers:

[modes.mappings.trigger]
type = "GamepadAnalogStick"
axis = 130
direction = "Clockwise"
# Note: Dead zone is automatic, but ensure actions require significant movement
4. Calibrate Controller

Windows:

1. Open "Set up USB game controllers"
2. Select your controller
3. Click "Properties" > "Settings"
4. Click "Calibrate"
5. Follow calibration wizard

Linux:

# Install joystick calibration tool
sudo apt install joystick

# Run calibration
jscal /dev/input/js0

# Save calibration
sudo jscal-store /dev/input/js0

macOS:

  • No built-in calibration tool
  • Consider third-party tools or controller-specific software
  • Hardware drift may require controller replacement
5. Hardware Solutions

If drift persists after software fixes:

  • Clean the stick: Compressed air around stick base
  • Replace stick module: iFixit guides for most controllers
  • Replace controller: Modern controllers have drift issues (especially Joy-Cons, DualSense)
  • Use D-pad instead: More reliable for discrete directions

Analog Trigger Not Responding

Symptoms

  • Pulling trigger has no effect
  • Some trigger positions work, others don’t
  • Digital trigger press works but analog doesn’t
  • Trigger fires at wrong pressure level

Causes

  • Threshold too high (requires full pull)
  • Threshold too low (triggers immediately)
  • Wrong trigger type (digital vs analog)
  • Trigger axis ID incorrect
  • Hardware trigger issue

Solutions

1. Adjust Threshold Value

Threshold range: 0-255 (0 = not pressed, 255 = fully pressed)

Common thresholds:

# Very sensitive (25% pull)
threshold = 64

# Medium sensitivity (50% pull) - RECOMMENDED
threshold = 128

# Requires deep pull (75%)
threshold = 192

# Almost full pull (90%)
threshold = 230

Start with medium and adjust:

[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Right trigger
threshold = 128  # Start here

If no response: Lower threshold (64, 32) If too sensitive: Raise threshold (192, 230)

2. Verify Trigger vs Button

Analog triggers (L2/R2, LT/RT, ZL/ZR):

# Use GamepadTrigger for pressure sensitivity
[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 132  # Left trigger (L2, LT, ZL)
threshold = 128

[modes.mappings.trigger]
type = "GamepadTrigger"
trigger = 133  # Right trigger (R2, RT, ZR)
threshold = 128

Digital triggers (fully pressed):

# Use GamepadButton for on/off detection
[modes.mappings.trigger]
type = "GamepadButton"
button = 143  # Left trigger digital (L2, LT, ZL)

[modes.mappings.trigger]
type = "GamepadButton"
button = 144  # Right trigger digital (R2, RT, ZR)

When to use which:

  • GamepadTrigger: Variable pressure (volume control, throttle, gradual actions)
  • GamepadButton: On/off only (simpler, more reliable)
3. Test in Event Console
conductorctl events --follow --type gamepad

Pull trigger slowly and watch output:

[GAMEPAD] Trigger: 133 | Value: 0   # Not pressed
[GAMEPAD] Trigger: 133 | Value: 64  # 25% pressed
[GAMEPAD] Trigger: 133 | Value: 128 # 50% pressed
[GAMEPAD] Trigger: 133 | Value: 192 # 75% pressed
[GAMEPAD] Trigger: 133 | Value: 255 # Fully pressed

If no events appear:

  • Hardware trigger issue
  • Controller not sending analog data (check controller mode)
  • Driver issue (Windows: might be in DirectInput mode, need XInput)

If values don’t reach 255:

  • Trigger might have limited range
  • Lower threshold accordingly
  • Or use digital button trigger instead
4. Debug Trigger Detection
DEBUG=1 conductor --foreground

Pull trigger and look for:

[GAMEPAD] Trigger 133 value: 150
[DEBUG] Threshold: 128 (met)
[DEBUG] Matched mapping: "Volume Up"
[DEBUG] Executing action: VolumeControl(Up)
✓ Action executed

If threshold never met, value isn’t reaching threshold:

  • Lower threshold: threshold = 64
  • Or check hardware with Event Console

Hybrid MIDI + Gamepad Conflicts

Symptoms

  • MIDI or gamepad works alone, but not together
  • Wrong device responds to mapping
  • Actions trigger on wrong button/pad
  • Mode switching affects wrong device

Causes

  • ID range overlap (using 0-127 for gamepad)
  • Config doesn’t separate MIDI vs gamepad mappings
  • Trigger type mismatch
  • Device priority confusion

Solutions

1. Understand ID Separation

No conflicts by design:

  • MIDI devices: IDs 0-127 (notes, CC, pitch bend, aftertouch)
  • Gamepad devices: IDs 128-255 (buttons, sticks, triggers)

This works seamlessly:

# MIDI mapping - Pad 0 (note 36)
[[modes.mappings]]
[modes.mappings.trigger]
type = "Note"
note = 36  # MIDI range (0-127)
[modes.mappings.action]
type = "Keystroke"
keys = "1"

# Gamepad mapping - A button
[[modes.mappings]]
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # Gamepad range (128-255)
[modes.mappings.action]
type = "Keystroke"
keys = "2"
2. Fix ID Range Errors

Common mistake - using MIDI IDs for gamepad:

# WRONG - This triggers on MIDI note 10, not gamepad button
[modes.mappings.trigger]
type = "GamepadButton"
button = 10  # Wrong range!

# CORRECT - Gamepad button IDs start at 128
[modes.mappings.trigger]
type = "GamepadButton"
button = 128  # A button
3. Separate MIDI and Gamepad Modes

Organize by device type for clarity:

[[modes]]
name = "MIDI Controls"
color = "blue"

[[modes.mappings]]
description = "MIDI Pad 1"
[modes.mappings.trigger]
type = "Note"
note = 36

[[modes]]
name = "Gamepad Controls"
color = "green"

[[modes.mappings]]
description = "Gamepad A Button"
[modes.mappings.trigger]
type = "GamepadButton"
button = 128

Switch modes based on which device you’re using.

4. Mode Switching with Both Devices

Use different controls for mode switching:

# MIDI encoder for mode switching
[[global_mappings]]
description = "Encoder: Next mode"
[global_mappings.trigger]
type = "EncoderTurn"
cc = 1
direction = "Clockwise"
[global_mappings.action]
type = "ModeChange"
mode = "Next"

# Gamepad chord for mode switching
[[global_mappings]]
description = "LB+RB: Next mode"
[global_mappings.trigger]
type = "GamepadButtonChord"
buttons = [136, 137]  # LB + RB
timeout_ms = 50
[global_mappings.action]
type = "ModeChange"
mode = "Next"

This allows mode switching from either device without conflicts.

5. Verify with Event Console
conductorctl events --follow

Test both devices:

# Press MIDI pad
[MIDI] NoteOn ch:0 note:36 vel:87

# Press gamepad button
[GAMEPAD] Button: 128 | State: Pressed

Ensure correct event type appears for each device.

Device Disconnection / Reconnection

Symptoms

  • “Gamepad disconnected” message
  • Controller stops responding mid-session
  • Need to restart daemon after unplugging
  • Wireless controller loses connection

Causes

  • Wireless interference or battery low
  • USB cable disconnected
  • Bluetooth timeout
  • System power management
  • Driver issue

Solutions

1. Auto-Reconnection Behavior

Conductor automatically handles reconnection:

What happens:

  1. Controller disconnects (USB unplugged, Bluetooth drops, battery dies)
  2. Daemon logs: [WARN] Gamepad disconnected (ID: 0)
  3. Daemon continues running, monitoring for reconnection
  4. Controller reconnects
  5. Daemon logs: [INFO] Gamepad reconnected (ID: 0)
  6. Mappings resume automatically

No action needed in most cases - just reconnect the controller.

2. Verify Auto-Reconnection
# Watch daemon logs
DEBUG=1 conductor --foreground

# Disconnect controller (unplug USB or power off)
# You'll see:
[WARN] Gamepad disconnected (ID: 0)
[DEBUG] Polling for reconnection...

# Reconnect controller
# You'll see:
[INFO] Gamepad detected: Xbox 360 Controller (ID: 0)
[INFO] Gamepad reconnected successfully
3. Manual Reconnection Steps

If auto-reconnection fails:

USB Controllers:

1. Unplug USB cable
2. Wait 5 seconds
3. Plug back in
4. Check Event Console for events

Bluetooth Controllers:

1. Power off controller (hold PS/Xbox button)
2. Open system Bluetooth settings
3. Remove/forget the controller
4. Put controller in pairing mode
5. Re-pair to system
6. Test in Event Console
4. Daemon Restart (Last Resort)

If reconnection doesn’t work:

# Stop daemon
conductorctl stop

# Wait 2 seconds
sleep 2

# Start daemon
conductor --foreground

# Verify gamepad detected
conductorctl status
5. Prevent Wireless Disconnections

Check battery level:

  • Low battery causes disconnections
  • Keep controllers charged
  • Use wired connection for critical work

Reduce interference:

  • Keep controller within 10 feet of receiver
  • Avoid metal objects between controller and PC
  • Turn off other Bluetooth devices
  • Use USB connection if interference persists

Disable system power management:

macOS:

System Settings > Battery > Options
Uncheck "Put hard disks to sleep when possible"

Linux:

# Disable USB autosuspend for controller
echo -1 | sudo tee /sys/bus/usb/devices/.../power/autosuspend

Windows:

Device Manager > Universal Serial Bus controllers
Right-click USB Root Hub > Properties > Power Management
Uncheck "Allow the computer to turn off this device to save power"

Platform-Specific Gamepad Issues

macOS

Xbox Wireless Adapter Not Working

Problem: Xbox controller via Wireless Adapter not detected

Solution:

# Install 360Controller driver
# Download from: https://github.com/360Controller/360Controller/releases

# Or via Homebrew
brew install --cask 360controller

# Restart system
sudo reboot

# Verify detection
system_profiler SPUSBDataType | grep -i xbox
Permission Dialog Keeps Appearing

Problem: Input Monitoring permission prompt appears repeatedly

Solution:

# Grant permission to Terminal instead of conductor binary
# This persists across rebuilds when running via cargo

# Or code-sign the binary
codesign --force --deep --sign - target/release/conductor

Linux

Insufficient Permission to Access /dev/input

Problem: Permission denied when accessing gamepad

Solution:

# Add user to input group
sudo usermod -a -G input $USER

# Create udev rule for gamepads
sudo tee /etc/udev/rules.d/50-gamepad.rules << 'EOF'
# Xbox controllers
SUBSYSTEM=="usb", ATTRS{idVendor}=="045e", MODE="0666", GROUP="input"

# PlayStation controllers
SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", MODE="0666", GROUP="input"

# Nintendo controllers
SUBSYSTEM=="usb", ATTRS{idVendor}=="057e", MODE="0666", GROUP="input"

# Generic HID gamepads
SUBSYSTEM=="input", ATTRS{name}=="*Controller*", MODE="0666", GROUP="input"
SUBSYSTEM=="input", ATTRS{name}=="*Gamepad*", MODE="0666", GROUP="input"
EOF

# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger

# Log out and back in (or reboot)
Joystick Device Not Created

Problem: /dev/input/js0 doesn’t exist

Solution:

# Load joydev kernel module
sudo modprobe joydev

# Make permanent (add to /etc/modules)
echo "joydev" | sudo tee -a /etc/modules

# Verify joystick devices
ls -la /dev/input/js*
Xbox Controller Not Recognized

Problem: Xbox controller via USB not working

Solution:

# Install xboxdrv
sudo apt install xboxdrv

# Load xpad module
sudo modprobe xpad

# Test with jstest
jstest /dev/input/js0

Windows

Controller Shows as “Unknown Device”

Problem: Device Manager shows gamepad as “Unknown device”

Solution:

1. Open Device Manager
2. Right-click "Unknown device"
3. Select "Update driver"
4. Choose "Search automatically for drivers"
5. Or download from manufacturer:
   - Xbox: Windows Update installs automatically
   - PlayStation: Install DS4Windows
   - Switch Pro: Install BetterJoy
DS4Windows Conflict

Problem: PlayStation controller works in DS4Windows but not Conductor

Solution:

DS4Windows emulates Xbox controller, which Conductor can detect.

Option 1: Use DS4Windows (controller appears as Xbox)
- Keep DS4Windows running
- Conductor sees it as Xbox controller
- Use Xbox button IDs (128-255)

Option 2: Native PlayStation support
- Close DS4Windows
- Restart Conductor
- Use native PlayStation support
- Same button IDs (128-255) work with either
XInput vs DirectInput Mode

Problem: Gamepad not detected in one mode

Solution:

Some controllers have mode switches:
- XInput mode: Modern Windows support (preferred)
- DirectInput mode: Legacy support

Look for X/D switch on controller or hold button combo:
- Usually: Start + Back for 3 seconds switches mode
- LED indicator changes when mode switches

Conductor works best with XInput mode on Windows.

Getting Additional Help

If your gamepad issue isn’t covered here:

  1. Check Event Console:

    conductorctl events --follow --type gamepad
    

    Verify button presses appear

  2. Enable Debug Logging:

    DEBUG=1 conductor --foreground
    

    Look for SDL2 detection messages

  3. Collect Information:

    • OS version (macOS 14.2, Ubuntu 22.04, Windows 11)
    • Conductor version: conductor --version
    • Controller model (Xbox Series X, DualSense, etc.)
    • Connection type (USB, Bluetooth, Wireless Adapter)
    • Error messages from debug log
    • Output of:
      conductorctl status
      system_profiler SPUSBDataType | grep -i controller  # macOS
      lsusb | grep -i controller  # Linux
      
  4. File GitHub Issue:

    • Include all collected information above
    • Attach relevant portions of debug log
    • Describe expected vs actual behavior
    • See Support Resources

Related Documentation:

Platform-Specific Issues

macOS: Permission Dialogs Keep Appearing

Cause: Binary changes (recompiling) invalidates permissions

Solution: Grant permission to Terminal.app instead:

  1. System Settings → Privacy & Security → Input Monitoring
  2. Add Terminal (or your terminal emulator)
  3. Run via cargo run - permission persists across rebuilds

Alternative: Code-sign the binary:

codesign --force --deep --sign - target/release/conductor

macOS: “Cannot be opened because the developer cannot be verified”

Solution:

# Remove quarantine attribute
xattr -d com.apple.quarantine target/release/conductor

# Or allow in System Settings
# Right-click binary → Open → Allow

Linux: Permission Denied (USB/HID Access)

Cause: User not in plugdev group or missing udev rules

Solution:

  1. Add udev rules:
sudo tee /etc/udev/rules.d/50-conductor.rules << 'EOF'
# Native Instruments Maschine Mikro MK3
SUBSYSTEM=="usb", ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1600", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{bInterfaceClass}=="01", ATTRS{bInterfaceSubClass}=="03", MODE="0666", GROUP="plugdev"
EOF

sudo udevadm control --reload-rules
sudo udevadm trigger
  1. Add user to plugdev:
sudo usermod -a -G plugdev $USER
  1. Log out and back in

  2. Test:

cargo run --release 2

Windows: MIDI Device Not Recognized

Cause: Driver not installed or generic USB driver used

Solution:

  1. Open Device Manager
  2. Look for “MIDI Device” or your controller under “Sound, video and game controllers”
  3. Right-click → Update Driver
  4. Choose manufacturer driver (not generic USB)
  5. Or install via Native Access

Performance Issues

High CPU Usage

Symptom: Conductor using >10% CPU

Causes:

  • Debug logging enabled
  • Animated LED scheme with high update rate
  • Shell actions running slowly
  • Event processing loop not optimizing

Solutions:

  1. Disable debug logging:

    # Don't use DEBUG=1 in production
    cargo run --release 2
    
  2. Use simpler LED scheme:

    # Instead of animated schemes
    cargo run --release 2 --led reactive  # or off
    
  3. Optimize shell actions:

    # Avoid long-running commands in mappings
    # Bad:
    command = "sleep 10 && echo done"
    
    # Good:
    command = "echo done &"  # Background process
    
  4. Build release mode:

    # Debug builds are 20-30% slower
    cargo build --release
    ./target/release/conductor 2
    

High Latency (Delayed Response)

Symptom: Actions trigger 50-100ms+ after pad press

Solutions:

  1. Use release build (not debug)
  2. Check system load (Activity Monitor/Task Manager)
  3. Close unnecessary applications
  4. Check MIDI buffer settings in Audio MIDI Setup (macOS)

Verify latency:

DEBUG=1 cargo run --release 2

Watch timestamps:

[16:32:45.123] NoteOn received
[16:32:45.124] Action executed  # Should be <2ms

If >10ms, investigate system performance.

Getting Additional Help

If your issue isn’t covered here:

  1. Check Diagnostics Guide for detailed troubleshooting
  2. Enable debug logging: DEBUG=1 cargo run --release 2
  3. Run diagnostic tools:
    cargo run --bin midi_diagnostic 2
    cargo run --bin led_diagnostic
    cargo run --bin test_midi
    
  4. Collect information:
    • macOS/Linux/Windows version
    • Conductor version: cargo --version
    • Device model
    • Error messages (full output)
    • Debug log output
  5. File an issue on GitHub with collected information

Last Updated: November 21, 2025 Status: Actively maintained

MIDI Output Troubleshooting

This guide helps you diagnose and resolve common issues with Conductor’s MIDI output functionality, including SendMIDI actions, virtual MIDI ports, and DAW integration.

Quick Diagnostic Checklist

Before diving into specific issues, run through this quick checklist:

  • Virtual MIDI port is created and online (IAC Driver, loopMIDI, ALSA)
  • DAW has MIDI input enabled for the virtual port
  • Conductor config specifies the correct port name (case-sensitive)
  • MIDI messages are being sent (check conductor --verbose logs)
  • DAW is receiving MIDI (check DAW’s MIDI monitor)
  • Correct MIDI channel is used (usually Channel 0)
  • No other applications are blocking the MIDI port

Common Issues

Issue 1: “Port not found” Error

Symptoms:

ERROR: MIDI output port "IAC Driver Bus 1" not found
Available ports: []

Possible Causes:

  1. Virtual MIDI port not created or offline
  2. Port name mismatch (case-sensitive)
  3. Conductor started before port was created
  4. Permissions issue (Linux/Windows)

Solutions:

macOS (IAC Driver)

  1. Verify IAC Driver is online:

    # Open Audio MIDI Setup
    open -a "Audio MIDI Setup"
    
    • Window → Show MIDI Studio
    • Double-click IAC Driver
    • Check “Device is online”
    • Click Apply
  2. List available MIDI ports to verify the exact name:

    # Use conductor diagnostic tool
    cargo run --bin test_midi
    
  3. Restart Conductor daemon:

    conductorctl stop
    conductor
    

Windows (loopMIDI)

  1. Verify loopMIDI is running:

    • Check system tray for loopMIDI icon
    • If not running, launch loopMIDI from Start Menu
  2. Verify port exists in loopMIDI:

    • Open loopMIDI application
    • Ensure at least one port is listed (e.g., “Conductor Virtual”)
  3. Check exact port name (case-sensitive):

    • Note the exact name shown in loopMIDI
    • Update config.toml to match exactly:
      [modes.mappings.action.then_action]
      type = "SendMIDI"
      port = "Conductor Virtual"  # Must match exactly
      
  4. Restart Conductor:

    conductorctl stop
    conductor
    

Linux (ALSA)

  1. Verify virtual MIDI port exists:

    aconnect -l
    

    Look for “Virtual Raw MIDI” or similar.

  2. Load ALSA virtual MIDI module if missing:

    sudo modprobe snd-virmidi
    
  3. Verify port name and update config:

    # List MIDI ports
    aconnect -l
    

    Update config.toml with the exact port name.

  4. Permissions (if port access denied):

    # Add user to audio group
    sudo usermod -a -G audio $USER
    
    # Log out and log back in for changes to take effect
    

Issue 2: Messages Sent But Not Received by DAW

Symptoms:

  • Conductor logs show messages being sent
  • DAW doesn’t respond (no transport control, no parameter changes)
  • No error messages

Diagnosis:

  1. Enable verbose logging in Conductor:

    conductor --verbose
    

    Look for lines like:

    [DEBUG] Sending MIDI: NoteOn(note=60, velocity=100, channel=0) to port "IAC Driver Bus 1"
    
  2. Check DAW’s MIDI monitor:

    • Logic Pro: View → Show MIDI Environment → Monitor
    • Ableton Live: Preferences → Link, Tempo & MIDI → MIDI Ports (check Track/Remote enabled)
    • Reaper: View → MIDI Device Diagnostics

Possible Causes & Solutions:

Cause 1: DAW MIDI Input Not Enabled

Logic Pro:

  1. Open Logic Pro → Settings → MIDI → Inputs (⌘,)
  2. Locate your virtual MIDI port
  3. Check the box to enable it
  4. Close Settings

Ableton Live:

  1. Open Preferences → Link, Tempo & MIDI
  2. In MIDI Ports section, find your virtual port
  3. Enable Track and Remote for the input port
  4. Close Preferences

Reaper:

  1. Open Preferences → MIDI Devices
  2. Find your virtual MIDI port in the input list
  3. Enable Enable input from this device
  4. Click OK

Cause 2: Wrong MIDI Channel

Solution: Most DAWs use Channel 0 (sometimes labeled as “Channel 1” in DAW UI). Verify your config:

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 60
velocity = 100
channel = 0  # Change to match DAW expectations

Test different channels (0-15) to find the correct one.

Cause 3: Wrong CC Numbers or Note Numbers

Solution: Use DAW’s MIDI Learn feature to discover correct values:

Logic Pro:

  1. Logic Pro → Control Surfaces → Learn Assignment
  2. Click the parameter to control
  3. Note the CC number shown (update your config)

Ableton Live:

  1. Click MIDI button (top-right, or press ⌘M)
  2. Click the parameter to control
  3. Press your MIDI controller (Ableton learns the mapping)
  4. Note the CC/Note number (if you want to replicate in config)

Cause 4: Control Surface Conflicts

Solution: Disable conflicting control surfaces in DAW:

Logic Pro:

  • Logic Pro → Control Surfaces → Setup
  • Disable any control surfaces using the same MIDI port

Ableton Live:

  • Preferences → Link, Tempo & MIDI
  • In Control Surface section, ensure no conflicting surface is assigned to the same port

Issue 3: Latency / Delayed Response

Symptoms:

  • Noticeable delay (>50ms) between pad press and DAW response
  • Audio/MIDI feels “laggy”

Possible Causes & Solutions:

Cause 1: High Audio Buffer Size

Solution: Lower DAW’s audio buffer size:

Logic Pro:

  1. Logic Pro → Settings → Audio
  2. Reduce I/O Buffer Size to 128 or 64 samples
  3. Note: Lower buffer = lower latency, but higher CPU usage

Ableton Live:

  1. Preferences → Audio
  2. Reduce Buffer Size to 128 or 64 samples

Reaper:

  1. Preferences → Audio → Device
  2. Reduce Block size to 128 or 64 samples

Cause 2: Third-Party Virtual MIDI Drivers

Windows: loopMIDI adds ~5-10ms latency. This is usually acceptable, but if critical:

  • Consider rtpMIDI (network MIDI) for even lower latency
  • Ensure loopMIDI is up to date

macOS: Use IAC Driver (native, near-zero latency) instead of third-party apps.

Linux: Use ALSA virtual ports (native, low latency) instead of JACK MIDI (higher latency).

Cause 3: System CPU Load

Solution:

  1. Close unnecessary applications
  2. Freeze/bounce tracks in DAW to reduce CPU usage
  3. Increase audio buffer size if CPU is maxed out (trade latency for stability)

Cause 4: Conductor Processing Delay

Diagnosis: Check Conductor logs for processing time:

conductor --verbose

If you see warnings about slow processing, check:

  1. Complex velocity curves (use simpler curves)
  2. Conditional actions with many conditions (simplify)
  3. Sequence actions with many steps (reduce)

Solution: Simplify mappings to reduce processing overhead.


Issue 4: Messages Sent to Wrong Port

Symptoms:

  • DAW receives MIDI from a different source
  • Conductor logs show correct port, but DAW sees different port

Possible Causes & Solutions:

Cause 1: Multiple Virtual MIDI Ports

Solution: List all available MIDI ports and verify exact name:

# Use Conductor diagnostic tool
cargo run --bin test_midi

Update config to use the correct port name (case-sensitive):

[modes.mappings.action.then_action]
type = "SendMIDI"
port = "IAC Driver Bus 1"  # Not "IAC Driver Bus 2"

Cause 2: Port Name Changed

macOS (IAC Driver):

  • If you renamed the bus in Audio MIDI Setup, update your config

Windows (loopMIDI):

  • If you renamed the port in loopMIDI, update your config
  • Restart Conductor after renaming

Issue 5: Velocity Not Working

Symptoms:

  • All MIDI messages sent with same velocity (e.g., always 127)
  • Velocity curves not being applied

Possible Causes & Solutions:

Cause 1: Fixed Velocity in Config

Problem: Config specifies a fixed velocity value:

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 60
velocity = 127  # Fixed velocity
channel = 0

Solution: Use a velocity curve to map input velocity:

[modes.mappings.action.then_action.message]
type = "NoteOn"
note = 60
channel = 0

[modes.mappings.action.then_action.message.velocity_curve]
type = "PassThrough"  # Use controller velocity directly

Cause 2: Trigger Doesn’t Capture Velocity

Problem: Trigger type doesn’t include velocity information.

Solution: Use a trigger that captures velocity:

[modes.mappings.trigger]
type = "Note"  # Captures velocity
note = 1
# Velocity is automatically available to velocity_curve

Issue 6: Virtual Port Not Visible in DAW

Symptoms:

  • Virtual MIDI port exists (verified via OS tools)
  • DAW doesn’t list the port in MIDI preferences

Possible Causes & Solutions:

macOS (IAC Driver)

Cause: DAW started before IAC Driver was enabled.

Solution:

  1. Enable IAC Driver in Audio MIDI Setup
  2. Restart the DAW
  3. Alternatively: Logic Pro → Settings → MIDI → Reset MIDI Drivers

Windows (loopMIDI)

Cause: DAW doesn’t detect dynamically created ports.

Solution:

  1. Create loopMIDI port before launching DAW
  2. If DAW is running, restart it after creating the port

Linux (ALSA)

Cause: ALSA virtual port created after DAW startup.

Solution:

  1. Load snd-virmidi module before launching DAW:
    sudo modprobe snd-virmidi
    
  2. Restart DAW
  3. Alternatively: Use aconnect to manually connect ports

Platform-Specific Issues

macOS Specific

IAC Driver “Device is offline” After Reboot

Problem: IAC Driver unchecks itself after macOS reboot.

Solution:

  1. Open Audio MIDI Setup
  2. Double-click IAC Driver
  3. Check “Device is online”
  4. This should persist, but if not, consider using an AppleScript to auto-enable on login

Permissions Issues with HID Device

Problem: Conductor can’t access MIDI controller hardware.

Solution:

  1. Grant Input Monitoring permissions:
    • System Settings → Privacy & Security → Input Monitoring
    • Enable for Terminal (if running cargo run)
  2. Restart Conductor

Windows Specific

loopMIDI Port Disappears

Problem: loopMIDI port intermittently disappears.

Solution:

  1. Ensure loopMIDI is set to start with Windows:
    • Right-click loopMIDI tray icon → Settings → Start with Windows
  2. If port disappears, restart loopMIDI application
  3. Restart Conductor daemon

Multiple MIDI Drivers Conflict

Problem: Multiple virtual MIDI drivers (loopMIDI, rtpMIDI, etc.) cause conflicts.

Solution:

  1. Use only one virtual MIDI driver at a time
  2. Disable/uninstall unused drivers
  3. Assign unique port names if using multiple drivers

Linux Specific

ALSA Permissions Denied

Problem: Conductor can’t access ALSA virtual MIDI ports.

Solution:

# Add user to audio group
sudo usermod -a -G audio $USER

# Log out and log back in

# Verify group membership
groups | grep audio

JACK MIDI vs ALSA

Problem: DAW uses JACK MIDI, but Conductor uses ALSA.

Solution:

  1. Bridge ALSA to JACK using a2jmidid:
    sudo apt-get install a2jmidid
    a2jmidid -e &
    
  2. Use qjackctl to route ALSA virtual port to JACK
  3. Alternatively: Use JACK-native virtual MIDI ports instead of ALSA

Advanced Debugging

Enable Conductor Debug Logging

Run Conductor with verbose logging to see detailed MIDI output:

conductor --verbose

What to look for:

  • [DEBUG] Sending MIDI: ... - Confirms messages are being sent
  • [ERROR] Port not found: ... - Port name issues
  • [WARN] Slow processing time: ... - Performance issues

Monitor MIDI Traffic in DAW

Logic Pro:

  1. View → Show MIDI Environment
  2. Click Monitor object
  3. Watch for incoming MIDI messages

Ableton Live:

  1. Preferences → Link, Tempo & MIDI
  2. Enable Track and Remote for the port
  3. Create a MIDI track
  4. Arm the track for recording
  5. Watch the MIDI input meter

Reaper:

  1. View → MIDI Device Diagnostics
  2. Select your virtual MIDI port
  3. Watch for incoming messages

Test MIDI Loopback

Create a loopback test to verify MIDI output is working:

  1. Route virtual output back to Conductor input (macOS):

    • Audio MIDI Setup → IAC Driver
    • Create two buses: “Conductor Out” and “Conductor In”
    • Configure Conductor to send to “Conductor Out”
    • Configure Conductor to receive from “Conductor In”
    • Use a MIDI routing app to bridge them
  2. Send a test message:

    # Press a pad and verify it's received on the input
    conductor --verbose
    

Getting Help

If you’ve tried all troubleshooting steps and still have issues:

  1. Gather diagnostic information:

    # List MIDI ports
    cargo run --bin test_midi > midi_ports.txt
    
    # Run Conductor with verbose logging
    conductor --verbose 2>&1 | tee conductor_debug.log
    
    # (Press pads to trigger actions)
    
    # Stop after 30 seconds
    conductorctl stop
    
  2. Check configuration:

    cat ~/.config/conductor/config.toml
    
  3. Report the issue:

    • GitHub: https://github.com/amiable-dev/conductor/issues
    • Include:
      • Platform (macOS, Windows, Linux)
      • Conductor version (conductor --version)
      • DAW name and version
      • MIDI port name
      • Relevant config.toml sections
      • Debug logs (conductor_debug.log)
      • midi_ports.txt

See Also

FAQ

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Diagnostic Tools and Procedures

Overview

Conductor includes a comprehensive suite of diagnostic tools for debugging connectivity, event processing, LED control, and configuration issues. This guide covers each tool in detail and provides systematic troubleshooting procedures.

Quick Diagnostic Checklist

Before diving deep, run through this quick checklist:

# 1. Check USB/MIDI connectivity
cargo run --bin test_midi

# 2. Verify MIDI events are received
cargo run --bin midi_diagnostic 2

# 3. Test LED/HID access
cargo run --bin led_diagnostic

# 4. Find note numbers
cargo run --bin pad_mapper 2

# 5. Enable debug logging
DEBUG=1 cargo run --release 2

If all five succeed, the hardware and drivers are working correctly.

Diagnostic Tool Reference

1. test_midi - MIDI Port Testing

Purpose: Verify MIDI connectivity, enumerate all available ports, test basic MIDI communication.

Syntax:

cargo run --bin test_midi

What it does:

  1. Lists all MIDI input ports
  2. Lists all MIDI output ports
  3. Attempts to open port 2 (default)
  4. Waits for a MIDI event (5-second timeout)
  5. Reports success or failure

Expected output (working):

MIDI Port Test
==============

Available input ports:
0: USB MIDI Device
1: IAC Driver Bus 1
2: Maschine Mikro MK3 - Input
3: Digital Keyboard

Available output ports:
0: USB MIDI Device
1: IAC Driver Bus 1
2: Maschine Mikro MK3 - Output

Testing port 2 (input)...
✓ Successfully opened port: Maschine Mikro MK3 - Input

Waiting for MIDI event... (5 second timeout)
✓ Received MIDI event: NoteOn ch:0 note:12 vel:87

Connection test: PASSED
All MIDI functionality working correctly.

Error output (device not found):

Available input ports:
0: IAC Driver Bus 1
# Device missing!

Testing port 2 (input)...
✗ Error: Port index 2 out of range (only 1 ports available)

Connection test: FAILED

Interpreting results:

  • Device not listed: USB/driver issue (see Common Issues - MIDI Device Not Found)
  • Timeout on event: Device connected but not sending data
    • Check device is in MIDI mode (not HID-only mode)
    • Verify pads/keys are functional
    • Try different port number
  • “Permission denied”: User permission issue (Linux)
    • Add user to audio or plugdev group
    • Check udev rules

When to use:

  • First step in any MIDI troubleshooting
  • After connecting a new device
  • After driver installation
  • Verifying port numbers before running conductor

2. midi_diagnostic - MIDI Event Monitor

Purpose: Real-time visualization of all MIDI events for debugging event detection and mapping issues.

Syntax:

cargo run --bin midi_diagnostic <PORT>

# Example
cargo run --bin midi_diagnostic 2

What it does:

  • Opens specified MIDI port
  • Listens for all MIDI events
  • Displays events in human-readable format
  • Shows channel, note/cc number, velocity/value
  • Runs until Ctrl+C

Output format:

Connected to MIDI port 2: Maschine Mikro MK3 - Input
Listening for MIDI events... (Ctrl+C to exit)

[16:32:45] [NoteOn]  ch:0 note:12 vel:87
[16:32:45] [NoteOff] ch:0 note:12 vel:0
[16:32:46] [NoteOn]  ch:0 note:13 vel:64
[16:32:46] [NoteOff] ch:0 note:13 vel:0
[16:32:47] [CC]      ch:0 cc:1 value:64
[16:32:48] [PitchBend] ch:0 value:8192
[16:32:49] [Aftertouch] ch:0 note:12 pressure:48

Event types shown:

  • NoteOn: Note pressed

    • ch: MIDI channel (0-15)
    • note: Note number (0-127)
    • vel: Velocity (0-127)
  • NoteOff: Note released

    • vel is usually 0, some devices use release velocity
  • CC (Control Change): Knob, slider, encoder

    • cc: Controller number (0-127)
    • value: Value (0-127)
  • PitchBend: Touch strip, pitch wheel

    • value: Bend amount (0-16383, center=8192)
  • Aftertouch: Pressure sensitivity

    • note: Which note (if polyphonic)
    • pressure: Pressure value (0-127)
  • ProgramChange: Program/patch change

    • program: Program number (0-127)

Use cases:

  1. Verify MIDI events are being sent:

    cargo run --bin midi_diagnostic 2
    # Press pads - you should see NoteOn/NoteOff events
    
  2. Find note numbers for config:

    cargo run --bin midi_diagnostic 2
    # Press pad → note:12 → use 12 in config.toml
    
  3. Debug why mappings aren’t triggering:

    # Compare MIDI diagnostic output with config.toml
    # If you see note:25 but config has note:12, they don't match
    
  4. Verify velocity ranges:

    cargo run --bin midi_diagnostic 2
    # Soft tap:  vel:32  (0-40 range)
    # Medium:    vel:68  (41-80 range)
    # Hard hit:  vel:105 (81-127 range)
    
  5. Check encoder/knob CC numbers:

    cargo run --bin midi_diagnostic 2
    # Turn encoder → [CC] ch:0 cc:1 value:65
    # Use cc:1 in EncoderTurn trigger
    

Troubleshooting with midi_diagnostic:

Problem: No events appear when pressing pads

Solutions:

  • Wrong port number (try 0, 1, 2, 3, etc.)
  • Device not sending MIDI (check device settings)
  • USB cable issue (try different cable)
  • Driver issue (reinstall drivers)

Problem: Wrong note numbers appearing

Cause: Device is on different pad page or has custom profile

Solution: Either:

  • Change device to expected page
  • Update config.toml with actual note numbers
  • Use --profile flag with correct profile

Problem: Events appear but with unexpected values

Example: Expecting ch:0 but seeing ch:1

Solution: Update trigger channel in config:

[trigger]
type = "Note"
note = 12
channel = 1  # Add channel specification

3. led_diagnostic - LED/HID Testing

Purpose: Test LED control and HID device access for troubleshooting lighting issues.

Syntax:

cargo run --bin led_diagnostic

What it does:

  1. Searches for HID device (Maschine Mikro MK3)
  2. Attempts to open HID device
  3. Tests LED control by cycling through all pads
  4. Displays each step’s success/failure

Expected output (working):

LED Diagnostic Tool
==================

Searching for Maschine Mikro MK3...
✓ Device found: Maschine Mikro MK3 (VID:17cc PID:1600)
  Serial: XXXXXXXX
  Manufacturer: Native Instruments
  Product: Maschine Mikro MK3

Opening HID device...
✓ HID device opened successfully
✓ Device supports shared access

Testing LED control...
- Lighting pad 0 (Red Bright)... ✓
- Lighting pad 1 (Orange Bright)... ✓
- Lighting pad 2 (Yellow Bright)... ✓
- Lighting pad 3 (Green Bright)... ✓
- Lighting pad 4 (Blue Bright)... ✓
- Lighting pad 5 (Purple Bright)... ✓
[... continues for all 16 pads ...]

Testing patterns...
✓ Rainbow pattern
✓ All pads lit (White)
✓ All pads cleared

LED Diagnostic: PASSED
All LED functionality working correctly.

Error output (permission denied - macOS):

Searching for Maschine Mikro MK3...
✓ Device found: Maschine Mikro MK3 (VID:17cc PID:1600)

Opening HID device...
✗ Failed to open HID device
  Error: Permission denied (os error 13)

  Possible causes:
  1. Input Monitoring permission not granted
  2. Device already in exclusive use
  3. Native Instruments drivers not installed

  Solutions:
  1. Grant Input Monitoring permission:
     System Settings → Privacy & Security → Input Monitoring
     Enable Terminal or conductor

  2. Close other applications using the device:
     - Native Instruments Controller Editor
     - Maschine software
     - Other MIDI software

  3. Install NI drivers:
     Download Native Access and install Maschine drivers

LED Diagnostic: FAILED

Error output (device not found):

Searching for Maschine Mikro MK3...
✗ Device not found

Checked for:
- Vendor ID: 0x17cc (Native Instruments)
- Product ID: 0x1600 (Maschine Mikro MK3)

Possible causes:
1. Device not connected
2. Wrong USB port or cable
3. Device powered off
4. HID driver not installed

Solutions:
1. Check USB connection:
   system_profiler SPUSBDataType | grep -i mikro

2. Try different USB port/cable

3. Power cycle device (unplug, wait 10s, replug)

LED Diagnostic: FAILED

Interpreting results:

  • Device found, opened, LEDs working: All HID/LED functionality OK
  • Device found but won’t open: Permission or driver issue
  • Device not found: USB/hardware issue
  • Opens but LEDs don’t light: Coordinate mapping or hardware issue

When to use:

  • LEDs not working in main application
  • After granting Input Monitoring permission (verify it worked)
  • After installing NI drivers (verify driver installation)
  • Testing LED hardware functionality
  • Before reporting LED bugs

4. led_tester - Interactive LED Control

Purpose: Manual control of individual LEDs for testing coordinate mapping and hardware.

Syntax:

cargo run --bin led_tester

Interactive mode:

LED Tester - Interactive Mode
==============================
Device: Maschine Mikro MK3 (VID:17cc PID:1600)
Status: Connected

Commands:
  on <pad> <color> <brightness>   Turn on specific LED
  off <pad>                       Turn off specific LED
  all <color> <brightness>        Set all LEDs
  clear                           Clear all LEDs
  rainbow                         Show rainbow pattern
  test                            Cycle through all pads
  coords                          Show pad coordinate mapping
  help                            Show this help
  quit                            Exit

Pad numbers: 0-15
Colors: red, orange, yellow, green, blue, purple, magenta, white
Brightness: 0 (off), 1 (dim), 2 (normal), 3 (bright)

>

Example session:

> on 0 red 3
✓ Pad 0: Red Bright

> on 1 green 2
✓ Pad 1: Green Normal

> all blue 1
✓ All pads: Blue Dim

> rainbow
✓ Rainbow pattern displayed

> test
Testing all pads (press Ctrl+C to stop)...
Pad 0... Pad 1... Pad 2... [continues]

> coords
Pad Coordinate Mapping:
Pad Index -> LED Position

Physical Layout (bottom-up):
12  13  14  15  <- Top row
 8   9  10  11
 4   5   6   7
 0   1   2   3  <- Bottom row

LED Buffer (top-down):
 0   1   2   3  <- Top (buffer 0-3)
 4   5   6   7
 8   9  10  11
12  13  14  15  <- Bottom (buffer 12-15)

Mapping Table:
  Pad  0 -> LED 12    Pad  8 -> LED  4
  Pad  1 -> LED 13    Pad  9 -> LED  5
  Pad  2 -> LED 14    Pad 10 -> LED  6
  Pad  3 -> LED 15    Pad 11 -> LED  7
  Pad  4 -> LED  8    Pad 12 -> LED  0
  Pad  5 -> LED  9    Pad 13 -> LED  1
  Pad  6 -> LED 10    Pad 14 -> LED  2
  Pad  7 -> LED 11    Pad 15 -> LED  3

> clear
✓ All LEDs cleared

> quit
Goodbye!

Use cases:

  1. Test individual pad LEDs:

    > on 0 white 3
    # Bottom-left pad should light up bright white
    
  2. Verify coordinate mapping:

    > coords
    # Shows physical layout vs LED buffer
    > on 0 red 3
    # Verify bottom-left pad lights (should be LED buffer position 12)
    
  3. Test all colors:

    > on 0 red 3
    > on 1 orange 3
    > on 2 yellow 3
    > on 3 green 3
    > on 4 blue 3
    > on 5 purple 3
    
  4. Brightness testing:

    > on 0 white 0   # Off
    > on 0 white 1   # Dim
    > on 0 white 2   # Normal
    > on 0 white 3   # Bright
    

When to use:

  • Debugging coordinate mapping issues
  • Testing specific pad LEDs (identify dead LEDs)
  • Understanding LED coordinate system
  • Verifying color/brightness encoding

5. pad_mapper - Note Number Discovery

Purpose: Identify MIDI note numbers for physical pads to use in config.toml.

Syntax:

cargo run --bin pad_mapper <PORT>

# Example
cargo run --bin pad_mapper 2

What it does:

  • Opens MIDI port
  • Listens for NoteOn events
  • Displays note number and velocity for each press
  • Continues until Ctrl+C

Output:

Pad Mapper - Find Note Numbers for Your Controller
===================================================
Connected to port 2: Maschine Mikro MK3 - Input

Press pads in order (bottom-left to top-right) and write down note numbers.
Ctrl+C to exit when done.

Pad pressed: Note 12 (velocity: 87)
Pad pressed: Note 13 (velocity: 64)
Pad pressed: Note 14 (velocity: 92)
Pad pressed: Note 15 (velocity: 73)
Pad pressed: Note 8  (velocity: 55)
Pad pressed: Note 9  (velocity: 81)
[continues as you press pads...]

Recommended workflow:

  1. Run pad_mapper:

    cargo run --bin pad_mapper 2
    
  2. Draw a grid on paper:

    [ ] [ ] [ ] [ ]  <- Top row
    [ ] [ ] [ ] [ ]
    [ ] [ ] [ ] [ ]
    [ ] [ ] [ ] [ ]  <- Bottom row
    
  3. Press each pad starting bottom-left, moving right:

    • Press bottom-left → write down note number (e.g., 12)
    • Press next pad right → write down note number (e.g., 13)
    • Continue for all 16 pads
  4. Result:

    [12][13][14][15]  <- Top row
    [ 8][ 9][10][11]
    [ 4][ 5][ 6][ 7]
    [ 0][ 1][ 2][ 3]  <- Bottom row
    

    Wait, those look wrong! That’s because your device might be on a different page or have a custom profile. The numbers you write down are the ones to use in config.toml.

  5. Update config.toml with actual note numbers:

    [[modes.mappings]]
    description = "Bottom-left pad"
    [modes.mappings.trigger]
    type = "Note"
    note = 0  # Or whatever number you wrote down
    

Use cases:

  • Initial setup of a new device
  • Creating config.toml for the first time
  • Verifying note numbers after changing device profile
  • Debugging why specific pads don’t work

Tips:

  • Press pads gently and consistently (velocity affects detection)
  • Write down numbers immediately (easy to forget)
  • Take a photo of your paper grid for reference
  • Some controllers send different notes on different “pages” or “banks”

Advanced Debugging with DEBUG=1

Enable verbose debug logging to see internal event processing:

Syntax:

DEBUG=1 cargo run --release 2

Debug Output Format

MIDI Events:

[16:32:45.123] [MIDI] NoteOn ch:0 note:12 vel:87
[16:32:45.124] [MIDI] NoteOff ch:0 note:12 vel:0

Event Processing:

[DEBUG] Processing NoteOn: note=12 vel=87
[DEBUG] Detected velocity range: Medium (41-80)
[DEBUG] Stored note 12 in press tracker (time: 16:32:45.123)

Mapping Engine:

[DEBUG] Checking 24 mappings in mode 'Default'
[DEBUG] Matched mapping: "Copy text" (trigger: Note(12))
[DEBUG] Compiled action: Keystroke { keys: "c", modifiers: ["cmd"] }

Action Execution:

[DEBUG] Executing Keystroke action
[DEBUG] Pressing key: C
[DEBUG] Holding modifiers: [Cmd]
[DEBUG] Releasing key: C
✓ Action executed successfully (1.2ms)

LED Updates:

[DEBUG] LED update request: pad=0 color=Green brightness=2
[DEBUG] Mapped pad 0 -> LED position 12
[DEBUG] HID buffer: [80 00 00 ... 1D ... 00] (81 bytes)
[DEBUG] HID write successful (4 bytes written)

Mode Changes:

[DEBUG] Mode change requested: next
[DEBUG] Current mode: 0 (Default)
[DEBUG] Switching to mode: 1 (Developer)
[INFO] Mode changed: Default -> Developer

Timers and State:

[DEBUG] Long press timer started: note=12 threshold=2000ms
[DEBUG] Note released after 1523ms (threshold: 2000ms)
[DEBUG] Long press NOT triggered (held too short)
[DEBUG] Double-tap window started: note=12 timeout=300ms
[DEBUG] Second tap detected within 287ms
[DEBUG] Double-tap triggered: note=12
[DEBUG] Chord detection: notes=[12, 13, 14] timeout=100ms
[DEBUG] All notes pressed within 47ms
[DEBUG] Chord triggered: [12, 13, 14]

Interpreting Debug Output

Problem: Mapping not triggering

Look for:

[DEBUG] No mapping matched for event: Note(12)

Possible causes:

  • Note number mismatch (check pad_mapper output)
  • Wrong mode (check current mode in debug output)
  • Trigger conditions not met (velocity, timing)

Solution: Compare debug output with config.toml


Problem: Action not executing

Look for:

[DEBUG] Matched mapping: "Test"
[ERROR] Failed to execute action: Command not found

Cause: Action configuration error (e.g., invalid shell command)

Solution: Fix action in config.toml


Problem: LEDs not updating

Look for:

[DEBUG] LED update request: pad=0
[ERROR] HID write failed: Device not open

Cause: HID device not accessible

Solution: Check permissions, run led_diagnostic


USB Connection Verification

macOS

Check USB device enumeration:

# List all USB devices
system_profiler SPUSBDataType

# Filter for your device
system_profiler SPUSBDataType | grep -i mikro

# More detailed output
system_profiler SPUSBDataType | grep -B 10 -A 10 "Mikro"

Expected output:

Maschine Mikro MK3:
  Product ID: 0x1600
  Vendor ID: 0x17cc  (Native Instruments)
  Version: 1.00
  Serial Number: XXXXXXXX
  Speed: Up to 12 Mb/s
  Manufacturer: Native Instruments
  Location ID: 0x14200000 / 5
  Current Available (mA): 500
  Current Required (mA): 100
  Extra Operating Current (mA): 0

Check for device disappearance:

# Monitor USB devices
log stream --predicate 'eventMessage contains "USB"' --level info

Then plug/unplug device - you should see connection events.

Linux

Check USB devices:

# List USB devices
lsusb

# Filter for Native Instruments (VID: 17cc)
lsusb | grep 17cc

# Detailed info
lsusb -v -d 17cc:1600

Check dmesg for USB events:

# Show recent USB events
dmesg | grep -i usb | tail -20

# Monitor in real-time
dmesg -w | grep -i usb

Check ALSA MIDI:

# List ALSA MIDI devices
aconnect -l

# Or
amidi -l

Windows

Device Manager:

  1. Open Device Manager (Win+X → Device Manager)
  2. Expand “Sound, video and game controllers”
  3. Look for your MIDI device
  4. Right-click → Properties → Check status

Check MIDI devices via PowerShell:

Get-PnpDevice -Class MEDIA | Format-Table -AutoSize

Interpreting Error Messages

Common Error Patterns

“No MIDI input ports available”

Meaning: No MIDI devices detected at all

Debug steps:

  1. system_profiler SPUSBDataType | grep -i usb
  2. cargo run --bin test_midi
  3. Check USB connection
  4. Verify drivers installed

“Failed to open HID device”

Meaning: Device found but can’t access HID interface

Debug steps:

  1. Check permissions (Input Monitoring on macOS)
  2. cargo run --bin led_diagnostic
  3. Close other applications using device
  4. Reinstall NI drivers

“TOML parse error at line X”

Meaning: Syntax error in config.toml

Debug steps:

  1. Open config.toml in editor
  2. Go to line X
  3. Check for:
    • Missing quotes
    • Wrong brackets
    • Typos in field names
  4. Validate at https://www.toml-lint.com/

“Missing field ‘type’ in trigger”

Meaning: Invalid config structure

Debug steps:

  1. Find the mapping missing type
  2. Add type field:
    [trigger]
    type = "Note"  # Add this
    note = 12
    

“Permission denied (os error 13)”

Meaning: Insufficient permissions

macOS: Grant Input Monitoring permission

Linux: Add user to plugdev group and create udev rules

Windows: Run as Administrator (not recommended for regular use)


Systematic Troubleshooting Procedure

When something doesn’t work, follow this systematic approach:

Level 1: Hardware Connectivity

# 1. Check USB enumeration
system_profiler SPUSBDataType | grep -i mikro

# 2. Test MIDI ports
cargo run --bin test_midi

# 3. Monitor MIDI events
cargo run --bin midi_diagnostic 2

If Level 1 fails: Hardware or driver issue (see Common Issues)

Level 2: Software Connectivity

# 4. Test HID access
cargo run --bin led_diagnostic

# 5. Find note numbers
cargo run --bin pad_mapper 2

# 6. Run with debug logging
DEBUG=1 cargo run --release 2

If Level 2 fails: Permission or configuration issue

Level 3: Configuration Validation

# 7. Validate config syntax
cargo check

# 8. Test with minimal config
cat > test.toml << 'EOF'
[[modes]]
name = "Test"
[[modes.mappings]]
description = "Test"
[modes.mappings.trigger]
type = "Note"
note = 0
[modes.mappings.action]
type = "Shell"
command = "say test"
EOF

cargo run --release 2 --config test.toml

# 9. Compare note numbers
# pad_mapper output vs config.toml

If Level 3 fails: Config syntax or mapping issue

Level 4: Deep Debugging

# 10. Enable trace-level logging
RUST_LOG=trace cargo run --release 2

# 11. Check for resource exhaustion
# Activity Monitor (macOS) / Task Manager (Windows)

# 12. Test on different computer
# (isolates hardware vs software issues)

Performance Profiling

Response Time Analysis

DEBUG=1 cargo run --release 2 2>&1 | grep -E 'NoteOn|executed'

Measure latency:

[16:32:45.123456] NoteOn
[16:32:45.124789] Action executed (1.3ms)

Latency = 1.3ms (excellent, <5ms is good)

CPU Usage Monitoring

macOS:

# Monitor CPU in real-time
top | grep conductor

# Or use Activity Monitor GUI
open -a "Activity Monitor"

Expected CPU usage:

  • Idle: <1%
  • Active (pad presses): 2-5%
  • With animated LEDs: 3-7%

If >10%: Performance issue (check debug logging enabled)

Memory Usage

# macOS
ps aux | grep conductor

# Expected RSS: 5-10 MB

If >50MB: Memory leak (file a bug report)

Logs and Diagnostics Output

Redirecting Output

Save all output:

cargo run --release 2 > conductor.log 2>&1

Save only errors:

cargo run --release 2 2> conductor.err

Separate stdout and stderr:

cargo run --release 2 > conductor.out 2> conductor.err

Analyzing Logs

Count events:

grep -c "NoteOn" conductor.log

Find errors:

grep -i error conductor.log
grep -i failed conductor.log

Extract timings:

grep "Action executed" conductor.log | sed 's/.*(\(.*\)ms).*/\1/'

See Also


Last Updated: November 11, 2025 Diagnostic Tool Version: 0.1.0

Performance

Note: This section is under development and will be completed in Phase 1 (Q1 2025).

For now, please refer to:

Coming Soon

This page will cover:

  • TBD

Help Wanted: We’re looking for contributors to help write documentation. See Contributing Guide.

Changelog

See the main CHANGELOG.md in the repository.

Roadmap

See the main ROADMAP.md in the repository.

Community

Join the Conductor community:

See GOVERNANCE.md for community structure.

Support

See the main SUPPORT.md for getting help.