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.”
🎮 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.”
🎬 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.”
⚡ 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.”
🕹️ 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
- GitHub: https://github.com/amiable-dev/conductor
- Discussions: https://github.com/amiable-dev/conductor/discussions
- Issues: https://github.com/amiable-dev/conductor/issues
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
| Feature | Conductor | Stream Deck | Keyboard Maestro | Karabiner |
|---|---|---|---|---|
| Price | Free (MIT) | $150-300 | $36 | Free |
| 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 |
| Platform | macOS (Linux/Win planned) | macOS/Windows | macOS only | macOS only |
| Customization | Unlimited (TOML + GUI) | GUI only | GUI + AppleScript | Complex JSON |
| Hardware Cost | $0 (reuse existing) | $150+ (proprietary) | Any keyboard | Any 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
- Export your Stream Deck button layouts
- Map buttons to gamepad/MIDI IDs in Conductor
- Import provided template configs
- Save $150+ by reusing existing hardware
See Stream Deck Migration Guide →
From Keyboard Maestro
- Export keyboard shortcuts list
- Map shortcuts to MIDI/gamepad triggers
- Add velocity sensitivity for multi-function buttons
- 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.
Option 1: Install Pre-Built Binaries (Recommended)
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) orconductor-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:
- Visit Native Instruments Downloads
- Download Native Access (the NI installation manager)
- Install Native Access and sign in (free account)
- 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
Using the GUI (Recommended)
v2.0.0 includes a visual configuration editor:
-
Open Conductor GUI:
open /Applications/"Conductor GUI.app" -
Connect your MIDI device in the device panel
-
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.)
-
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 Settings → Game 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:
- Forget the device in Bluetooth settings
- Put controller in pairing mode
- Re-pair the controller
- Test in Game Controllers settings
USB Connection Issues:
- Try a different USB port
- Try a different USB cable
- 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:
-
Run Conductor once:
cargo run --release 2 -
macOS will show a permission dialog: “conductor would like to receive keystrokes from any application”
-
Click Open System Settings or manually navigate:
- Open System Settings → Privacy & Security → Input Monitoring
- Find
conductor(orTerminalif running viacargo run) - Toggle the switch to ON
-
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:
- Go to System Settings → Privacy & Security → Accessibility
- Click the + button
- Navigate to
target/release/conductor(or addTerminal) - Click Open
This is optional and only needed for specific advanced features.
Running Conductor
Using the GUI (Recommended)
The simplest way to run Conductor v2.0.0:
-
Launch the GUI:
open /Applications/"Conductor GUI.app" -
The daemon starts automatically in the background
-
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
-
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
Option 1: GUI Auto-Start (Recommended)
The Conductor GUI includes built-in auto-start functionality:
- Open Conductor GUI → Settings
- Enable “Start Conductor on login”
- 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:
- Open Native Instruments Controller Editor
- Select Maschine Mikro MK3
- Edit pad pages (A-H)
- Save as
.ncmm3file - Use with
--profileflag
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:
- Check USB connection
- Open Audio MIDI Setup and verify device appears
- Try different USB port
- Restart device
Error: Failed to open HID device
Solution:
- Grant Input Monitoring permission (see above)
- Install Native Instruments drivers
- Check USB cable and connection
- Try running with sudo (not recommended long-term):
sudo ./target/release/conductor 2
Error: Permission denied (os error 13)
Solution:
- Check Input Monitoring permission
- Verify binary has correct permissions:
ls -l target/release/conductor chmod +x target/release/conductor
Runtime Errors - Game Controllers
Error: Gamepad not detected
Solution:
- Check connection (USB or Bluetooth)
- Verify controller appears in System Settings → Game Controllers
- Grant Input Monitoring permission
- Try reconnecting the controller
- Check debug output:
DEBUG=1 conductor --foreground
Error: Gamepad buttons not responding
Solution:
- Use MIDI Learn to discover correct button IDs
- Verify button IDs are in range 128-255 (not 0-127)
- Check that Input Monitoring permission is granted
- Test in System Settings → Game Controllers
- Try a different USB cable or re-pair Bluetooth
Error: Analog stick not working
Solution:
- Check axis IDs (128-131 for sticks, 132-133 for triggers)
- Verify direction is correct (Clockwise/CounterClockwise)
- Adjust dead zone if too sensitive
- Use button triggers instead of analog for precise control
LED Issues (MIDI Controllers Only)
LEDs not lighting up:
- Verify Native Instruments drivers installed
- Check Input Monitoring permission
- Test with different LED scheme:
cargo run --release 2 --led rainbow - Check DEBUG output:
DEBUG=1 cargo run --release 2
LEDs lighting wrong pads:
- Verify you’re using a device profile
- Check profile has correct note mappings
- See Device Profiles
- Use pad mapper to verify notes:
cargo run --bin pad_mapper
Gamepad-Specific Issues
Controller works in games but not Conductor:
- Ensure Conductor has Input Monitoring permission
- Check that controller is SDL2-compatible
- Try USB connection instead of Bluetooth
- Restart Conductor after connecting controller
Bluetooth pairing issues:
- Forget device in Bluetooth settings
- 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
- Re-pair and test in System Settings
- Use USB cable as fallback
Battery/Power issues (wireless):
- Charge or replace batteries
- Use USB cable for wired mode
- 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
- Learn the GUI: Read GUI Quick Start Guide
- MIDI Learn Tutorial: See MIDI Learn Mode
- Device Templates: Check Using Device Templates
- Per-App Profiles: Set up Application-Specific Profiles
- Gamepad Setup: Read Gamepad Support Guide (v3.0+)
For CLI Users
- Daemon Control: Read Daemon & Hot-Reload Guide
- CLI Reference: See conductorctl Commands
- Manual Configuration: Check Configuration Overview
- Advanced Actions: Explore Actions Reference
For All Users
- Gamepad Support: Gamepad Support Guide (v3.0+)
- Troubleshooting: Common Issues
- LED Customization: LED System Documentation
- Diagnostic Tools: Debugging Guide
Getting Help
If you encounter issues:
- Check Common Issues
- Use Diagnostic Tools
- Enable debug logging:
DEBUG=1 cargo run --release 2 - File an issue on GitHub with:
- macOS version
- Hardware (Intel/Apple Silicon)
- Device model
- Error messages
- Output of
cargo --versionandrustc --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
Option 1: Install Pre-Built Binaries (Recommended)
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
xpadneodriver for best compatibility - Install:
sudo apt install -y xpadneo(Ubuntu/Debian)
PlayStation Controllers:
- Native support via
hid-playstationkernel 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
Using the GUI (Recommended)
v3.0.0 includes a visual configuration editor:
-
Open Conductor GUI:
conductor-gui -
Connect your device in the device panel (MIDI or gamepad)
-
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.)
-
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:
- Check USB connection:
lsusb - Verify ALSA sees device:
aconnect -l - Check udev rules are loaded
- Verify user is in plugdev group
Error: Permission denied opening MIDI device
Solution:
- Check udev rules:
ls -l /dev/snd/* - Add user to audio group:
sudo usermod -a -G audio $USER - Log out and back in
- Verify:
groups | grep audio
Runtime Errors - Game Controllers
Error: Gamepad not detected
Solution:
- Check /dev/input:
ls -l /dev/input/js* - Test with jstest:
jstest /dev/input/js0 - Check udev rules are loaded:
sudo udevadm control --reload-rules - Verify groups:
groups | grep -E "plugdev|input" - Check debug output:
DEBUG=1 conductor --foreground
Error: Permission denied: /dev/input/js0
Solution:
- Check file permissions:
ls -l /dev/input/js0 - Verify udev rules:
cat /etc/udev/rules.d/50-conductor.rules - Add user to plugdev:
sudo usermod -a -G plugdev,input $USER - Reload udev:
sudo udevadm control --reload-rules && sudo udevadm trigger - Log out and back in
Error: Gamepad buttons not responding
Solution:
- Use MIDI Learn to discover correct button IDs
- Verify button IDs are in range 128-255 (not 0-127)
- Test in jstest to verify hardware works
- Check that gamepad appears in
conductorctl status - Try USB connection instead of Bluetooth
Error: Analog stick not working
Solution:
- Check axis IDs in jstest:
jstest /dev/input/js0 - Verify axis IDs match config (128-133)
- Check dead zone settings
- 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:
- Check /dev/input:
ls /dev/input/js* - Verify udev rules for Bluetooth devices
- Check dmesg for errors:
dmesg | tail -20 - 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
- Learn the GUI: Read GUI Quick Start Guide
- MIDI Learn Tutorial: See MIDI Learn Mode
- Device Templates: Check Using Device Templates
- Per-App Profiles: Set up Application-Specific Profiles
- Gamepad Setup: Read Gamepad Support Guide (v3.0+)
For CLI Users
- Daemon Control: Read Daemon & Hot-Reload Guide
- CLI Reference: See conductorctl Commands
- Manual Configuration: Check Configuration Overview
- Advanced Actions: Explore Actions Reference
For All Users
- Gamepad Support: Gamepad Support Guide (v3.0+)
- Troubleshooting: Common Issues
- Diagnostic Tools: Debugging Guide
Getting Help
If you encounter issues:
- Check Common Issues
- Use Diagnostic Tools
- Enable debug logging:
DEBUG=1 conductor --foreground - Check system logs:
journalctl --user -u conductor -f - 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 --versionandrustc --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+
Option 1: Install Pre-Built Binaries (Recommended)
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:
- Download rustup-init.exe from https://rustup.rs/
- Run the installer
- Follow the prompts and select the default installation
- 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):
- Download from https://visualstudio.microsoft.com/downloads/
- Install “Desktop development with C++” workload
- Restart your terminal
Option B - Full Visual Studio:
- Download Visual Studio Community (free)
- 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:
- Visit Native Instruments Downloads
- Download Native Access (the NI installation manager)
- Install Native Access and sign in (free account)
- 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:
- Open Settings → Devices → Bluetooth & other devices
- Click “Add Bluetooth or other device”
- Select “Bluetooth”
- Put controller in pairing mode:
- Xbox: Hold pair button until LED flashes
- PlayStation: Hold Share + PS button
- Switch Pro: Hold sync button on top
- Select controller from list
- Wait for pairing to complete
DS4Windows for PlayStation Controllers (Optional):
- Download from https://ds4-windows.com/
- Install and run DS4Windows
- Connect DualShock 4 or DualSense
- 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
Using the GUI (Recommended)
v3.0.0 includes a visual configuration editor:
-
Open Conductor GUI:
conductor-gui -
Connect your device in the device panel (MIDI or gamepad)
-
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.)
-
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:
- Download from https://visualstudio.microsoft.com/downloads/
- Install “Desktop development with C++” workload
- 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:
- Check USB connection
- Open Device Manager and verify device appears
- Install device drivers
- Restart Windows
Error: Failed to open MIDI device
Solution:
- Close other MIDI applications (DAWs, etc.)
- Disconnect and reconnect device
- Restart Windows
- Try different USB port
Runtime Errors - Game Controllers
Error: Gamepad not detected
Solution:
- Open Game Controllers (joy.cpl) and verify controller appears
- Test controller in Properties dialog
- Check battery level (wireless)
- Try USB connection instead of Bluetooth
- Run Conductor as Administrator (temporary test)
- Check debug output:
$env:DEBUG=1; conductor --foreground
Error: Gamepad buttons not responding
Solution:
- Use MIDI Learn to discover correct button IDs
- Verify button IDs are in range 128-255 (not 0-127)
- Test controller in joy.cpl
- For PlayStation controllers, try DS4Windows
- Check that gamepad appears in
conductorctl status
Error: Analog stick not working
Solution:
- Check axis IDs in joy.cpl Properties
- Verify axis IDs match config (128-133)
- Check dead zone settings
- Calibrate controller in joy.cpl
- Use button triggers instead of analog for precise control
Bluetooth Gamepad Issues
Controller not pairing:
- Open Settings → Bluetooth & other devices
- Remove old pairings
- Put controller in pairing mode
- Pair as new device
- Test in joy.cpl
Controller disconnects randomly:
- Check battery level
- Update Bluetooth drivers
- Move USB Bluetooth adapter away from other USB 3.0 devices
- Use USB cable instead
Controller lag or latency:
- Use USB cable for lowest latency
- Use Xbox Wireless Adapter instead of Bluetooth
- Update controller firmware
- 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
- Learn the GUI: Read GUI Quick Start Guide
- MIDI Learn Tutorial: See MIDI Learn Mode
- Device Templates: Check Using Device Templates
- Per-App Profiles: Set up Application-Specific Profiles
- Gamepad Setup: Read Gamepad Support Guide (v3.0+)
For CLI Users
- Daemon Control: Read Daemon & Hot-Reload Guide
- CLI Reference: See conductorctl Commands
- Manual Configuration: Check Configuration Overview
- Advanced Actions: Explore Actions Reference
For All Users
- Gamepad Support: Gamepad Support Guide (v3.0+)
- Troubleshooting: Common Issues
- Diagnostic Tools: Debugging Guide
Getting Help
If you encounter issues:
- Check Common Issues
- Use Diagnostic Tools
- Enable debug logging:
$env:DEBUG=1; conductor --foreground - Check Event Viewer for errors
- File an issue on GitHub with:
- Windows version
- Device model (MIDI or gamepad)
- Error messages
- Output of
cargo --versionandrustc --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:
- Install
rustc(Rust compiler) - Install
cargo(Rust package manager and build tool) - 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
-
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
-
conductor-daemon - CLI daemon + diagnostic tools
- Main
conductorbinary (daemon service) conductorctl- CLI control tool (v1.0.0+)- 6 diagnostic binaries:
midi_diagnostic- MIDI event viewerled_diagnostic- LED testing toolled_tester- Interactive LED controlpad_mapper- Note number mappertest_midi- Port connectivity testmidi_simulator- MIDI event simulator (testing)
- Main
-
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
- Download from Native Instruments
- Install via Native Access
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
- Download: Visual Studio Build Tools
- Install “Desktop development with C++” workload
OR
- Full Visual Studio (Community edition is free)
Optional:
- Native Instruments Drivers: For Maschine Mikro MK3 support
- Download from Native Instruments
- Install via Native Access
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:
- Run the binary:
./target/release/conductor - Configure mappings: Edit
config.toml - Read documentation:
See Also
- Cargo Book - Complete cargo documentation
- Rust Platform Support - All supported targets
- CLI Commands - Running the binary with options
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
-
Plug in your MIDI controller via USB
-
In the Conductor GUI, go to the Device Connection panel
-
Your device should appear in the list. Click Connect
-
The status bar at the bottom should show: “Connected to [Your Device]”
If your device doesn’t appear:
- Check USB cable
- Try a different USB port
- See Troubleshooting Device Connection
Step 4: Create Your First Mapping with MIDI Learn
The fastest way to create a mapping is using MIDI Learn mode:
-
Click “Add Mapping” in the Mappings panel
-
Click “Learn” next to the Trigger field
-
Press any pad/button on your MIDI controller
-
The trigger configuration auto-fills with the detected input
-
Choose an action:
- Keystroke - Press a key (e.g., Cmd+C for copy)
- Launch - Open an application
- Text - Type text
- Shell - Run a command
-
Click “Save”
-
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:
- Select trigger type: Velocity Range
- Use MIDI Learn to detect the note
- 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:
- Select trigger type: Long Press
- Use MIDI Learn
- Set Hold Duration: 2000ms
- Action: Open Calculator
Chord Detection
Press multiple pads simultaneously:
- Select trigger type: Chord
- Use MIDI Learn and press all pads within 100ms
- Action: Launch your favorite app
Step 6: Use Device Templates
Skip manual configuration with built-in device templates:
-
Go to Settings → Device Templates
-
Select your controller:
- Maschine Mikro MK3
- Launchpad Mini
- APC Mini
- Korg nanoKONTROL2
- Novation Launchkey Mini
- AKAI MPK Mini
-
Click Load Template
-
The template loads pre-configured mappings for common workflows
-
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:
-
Connect your gamepad via USB or Bluetooth
-
Verify it’s recognized by your system:
- macOS: System Settings → Game Controllers
- Linux:
ls /dev/input/js* - Windows: Devices and Printers
-
Open Conductor GUI → Device Templates
-
Filter by “🎮 Game Controllers”
-
Select your controller:
- Xbox 360/One/Series X|S
- PlayStation DualShock 4/DualSense (PS5)
- Nintendo Switch Pro Controller
-
Click “Create Config” → Reload daemon
-
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):
-
Connect your HID device via USB
-
Verify system recognition (same commands as above)
-
Open Conductor GUI → MIDI Learn
-
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”
-
Repeat for all buttons/axes you want to map
-
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
jstestrecognizes your controller:jstest /dev/input/js0 - May need udev rules for device permissions
- Install
xdotoolfor 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:
- Check USB/Bluetooth connection
- Verify system recognition (see commands above)
- Try USB instead of Bluetooth (or vice versa)
- Restart Conductor daemon:
conductorctl stop && conductorctl reload
Buttons not working:
- Use MIDI Learn to verify button IDs (should be 128-255)
- Check Event Console for incoming events
- 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:
-
Go to Settings → General
-
Enable “Start Conductor on login”
-
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:
-
Go to Per-App Profiles in the GUI
-
Add a new profile:
- Application: Select an app (e.g., “Visual Studio Code”)
- Profile: Select a config profile
-
When you switch to that application, Conductor automatically loads the configured profile
-
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:
-
Go to Event Console in the GUI
-
Watch MIDI events as they happen:
- Note On/Off
- Velocity values
- Control Change
- Pitch Bend
- Aftertouch
-
Filter events by type or note number
-
Export logs for debugging
This is invaluable for troubleshooting “why isn’t my mapping working?”
Troubleshooting
Device Not Found
-
Check USB connection:
system_profiler SPUSBDataType | grep -i midi -
Restart the daemon:
conductorctl stop open /Applications/"Conductor GUI.app" -
Check Audio MIDI Setup:
open -a "Audio MIDI Setup"
LEDs Not Working
-
Ensure Native Instruments drivers are installed (for Maschine controllers)
-
Grant Input Monitoring permission:
- System Settings → Privacy & Security → Input Monitoring
- Enable for “Conductor GUI”
-
Check LED scheme in GUI Settings
-
View debug logs in Event Console
Mappings Not Triggering
-
Use Event Console to verify MIDI events are being received
-
Use MIDI Learn to verify the correct note numbers
-
Check mode - is the mapping in the current mode or global?
-
Reload config:
conductorctl reload
Permission Denied (macOS)
If you see “Permission denied” errors:
- System Settings → Privacy & Security → Input Monitoring
- Add “Conductor GUI” to the list
- 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
- Explore Device Templates - Load pre-built configs for popular controllers
- Try Velocity Sensitivity - One pad, three actions based on press strength
- Set Up Hybrid Mode - Combine MIDI + gamepad simultaneously
💡 Get Inspired
- Configuration Examples - Copy-paste ready workflows for DAWs, development, streaming
- Gamepad Support Guide - Turn your Xbox controller into a macro pad
- LED System Guide - Add visual feedback to your controller
📖 Go Deeper
- Triggers Reference - All 15+ trigger types explained
- Actions Reference - Complete action type catalog
- Context-Aware Mappings - App-based, time-based, conditional actions
🤝 Join the Community
- GitHub Discussions - Ask questions, share configs
- Report Issues - Found a bug? Request a feature?
- Contribute - Help make Conductor better
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
- Save the file (Cmd+S or Ctrl+O in nano)
- Stop Conductor if it’s running (Ctrl+C)
- 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
- Understanding Modes - Create mode-based workflows
- Configuration Overview - Full config reference
- All Trigger Types - Complete trigger reference
- All Action Types - Complete action reference
- Example Configurations - Pre-built configs
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):
- Click “Learn” next to a trigger field
- Press a control on your device (pad, button, stick, encoder, etc.)
- Conductor auto-fills the trigger configuration
- 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
-
Open Conductor GUI and ensure your device is connected
-
Navigate to Mappings panel
-
Click “Add Mapping” or edit an existing one
-
Click the “Learn” button next to the Trigger field
-
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] -
Press any control on your MIDI or gamepad device:
- MIDI: Pad, button, encoder/knob, fader, touch strip
- Gamepad: Button, analog stick, trigger, D-pad
-
Trigger auto-fills (examples):
MIDI Pad:
Trigger Type: Note Note: 36 Channel: 0Gamepad Button:
Trigger Type: GamepadButton Button: 128Analog Stick:
Trigger Type: GamepadAnalogStick Axis: 130 Direction: Clockwise -
Assign an action (Keystroke, Launch, Text, etc.)
-
Click “Save”
-
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)
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Press A button on Xbox controller
- Auto-detects:
GamepadButton: 128 - Select Action: Keystroke
- Use Keystroke Picker: Press
Cmd+C - Save
Result: Pressing A button executes Cmd+C
Flight Stick Example
Goal: Map trigger to enter key
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Pull trigger on flight stick
- Auto-detects:
GamepadButton: 128(or GamepadTrigger if analog) - Action: Keystroke →
Return - Save
Result: Pulling trigger presses Enter
Racing Wheel Example
Goal: Map wheel rotation to browser navigation
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Turn wheel right
- Auto-detects:
GamepadAnalogStick: Axis 128, Direction: Clockwise - Action: Keystroke →
Cmd+RightArrow(forward in browser) - Save
Result: Turning wheel right navigates forward in browser
HOTAS Example
Goal: Map throttle up to volume increase
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Move throttle up
- Auto-detects:
GamepadAxis: 129, Direction: Clockwise - Action: Volume Control → Up
- 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:
- Learn → Press MIDI pad 36 → Auto-detects
Note: 36 - Action: Keystroke
Cmd+C
Gamepad A Button for Paste:
- Learn → Press A button → Auto-detects
GamepadButton: 128 - 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:
- Check device connection:
- MIDI: Ensure device shows in Device Panel
- Gamepad: Verify connection in system settings
- Check Event Console: Open Event Console and verify events are being received
- Try different control: Some controls may not send events (e.g., mode buttons)
- Restart device: Disconnect and reconnect the device
Wrong Button/Note Detected
Symptoms: Input Learn detects incorrect button ID or note number
Solutions:
- Verify in Event Console: Check what events are actually being sent
- Check button ID range:
- MIDI: 0-127
- Gamepad: 128-255
- Load device template: Use a pre-configured template for your controller
- Manual override: Click “Advanced” and manually enter the correct ID
Gamepad Not Recognized
Symptoms: Gamepad buttons don’t trigger Input Learn
Solutions:
- Ensure SDL2 compatibility: Check if gamepad is SDL2-compatible
- Check system recognition:
- macOS: System Settings → Game Controllers
- Linux:
ls /dev/input/js* - Windows: Devices and Printers
- Try USB instead of Bluetooth (or vice versa)
- Restart Conductor daemon:
conductorctl stop && conductor --foreground
Multiple Events Detected
Symptoms: Input Learn shows “Multiple events detected, please try again”
Solutions:
- Press only one control at a time
- Wait for button/pad to release before pressing again
- Disable auto-repeat: Some controllers send rapid-fire messages
Analog Stick Not Detected
Symptoms: Moving stick doesn’t trigger Input Learn
Solutions:
- Move stick beyond dead zone: Move at least 15% from center
- Check axis ID: Ensure using correct stick (left vs right)
- Verify in Event Console: See if axis events are being received
- Try different direction: Some sticks may have faulty axes
Trigger Pull Not Detected
Symptoms: Pulling analog trigger doesn’t work
Solutions:
- Pull trigger fully: Some triggers need >50% pull
- Check threshold: Try different pull depths
- Use digital trigger instead: Try the digital trigger button (LT/RT button)
- Verify in Event Console: Check if trigger axis events appear
Velocity Not Detected
Symptoms: Input Learn only creates Note trigger, not Velocity Range
Solutions:
- Vary velocity: Try pressing soft, medium, and hard during different Learn attempts
- Manual configuration: Create Velocity Range trigger manually after Learn
- 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)
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Press pad → Auto-fills
Note: 36 - Select Action: Keystroke
- Use Keystroke Picker: Press
Cmd+C - Save
Result: Pressing pad executes Cmd+C
Example 2: Gamepad Multi-Button Combo
Goal: LB + RB switches to Media mode
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Press LB + RB together → Auto-fills
GamepadButtonChord: [136, 137] - Action: Mode Change → Select “Media”
- Save
Result: Pressing LB + RB together switches to Media mode
Example 3: Velocity-Sensitive Volume
Goal: Soft press = volume down, hard press = volume up
-
Click “Add Mapping”
-
Click “Learn” next to Trigger
-
Press pad softly → Auto-fills
VelocityRange: Note 36, Min: 0, Max: 40 -
Action: Volume Control → Down
-
Save
-
Click “Add Mapping” again
-
Click “Learn”
-
Press same pad hard → Auto-fills
VelocityRange: Note 36, Min: 81, Max: 127 -
Action: Volume Control → Up
-
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
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Hold pad for 2+ seconds → Auto-fills
LongPress: Note 36, Duration: 2000ms - Action: Launch → Select
/Applications/Spotify.app - Save
Result: Holding pad for 2 seconds opens Spotify
Example 5: Encoder for Volume
Goal: Turn encoder to adjust volume
-
Click “Add Mapping”
-
Click “Learn” next to Trigger
-
Turn encoder right → Auto-fills
EncoderTurn: CC 1, Direction: Clockwise -
Action: Volume Control → Up
-
Save
-
Click “Add Mapping” again
-
Click “Learn”
-
Turn encoder left → Auto-fills
EncoderTurn: CC 1, Direction: Counterclockwise -
Action: Volume Control → Down
-
Save
Result: Turning encoder controls system volume
Example 6: Analog Stick Navigation
Goal: Right stick controls browser navigation
-
Click “Add Mapping”
-
Click “Learn” next to Trigger
-
Move right stick right → Auto-fills
GamepadAnalogStick: Axis 130, Direction: Clockwise -
Action: Keystroke →
Cmd+RightArrow -
Save
-
Click “Add Mapping”
-
Click “Learn”
-
Move right stick left → Auto-fills
GamepadAnalogStick: Axis 130, Direction: CounterClockwise -
Action: Keystroke →
Cmd+LeftArrow -
Save
Result: Moving right stick navigates forward/back in browser
Example 7: Racing Wheel Throttle
Goal: Wheel triggers control volume
- Click “Add Mapping”
- Click “Learn” next to Trigger
- Pull right trigger → Auto-fills
GamepadTrigger: 133, Threshold: 128 - Action: Volume Control → Up
- Save
Result: Pulling right trigger increases volume
Tips & Best Practices
Tip 1: Use Event Console
Before starting Input Learn:
- Open Event Console
- Press your control
- 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:
- Switch to target app (e.g., Logic Pro)
- Switch back to Conductor GUI
- Use Input Learn
- Assign action relevant to that app
Tip 3: Batch Learn
Create multiple mappings quickly:
- Click “Learn”
- Press control
- Assign action
- Save
- Immediately click “Add Mapping” and repeat
Tip 4: Device Templates First
Before manual Learn:
- Check if a device template exists for your controller
- Load template to get 90% of mappings
- Use Learn to customize the remaining 10%
Tip 5: Test Immediately
After creating a mapping:
- Click “Save”
- Immediately test by pressing the control
- Verify action executes correctly
- 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
- Gamepad Support Guide - Complete gamepad documentation
- Device Templates - Pre-configured controller mappings
- Trigger Reference - All trigger types explained
- Action Reference - All action types explained
- Per-App Profiles - Automatic profile switching
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 modescolor: 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:
- Mode-specific mappings are checked first
- If no match, global mappings are checked
- 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 modemode = "previous"- Cycle to previous modemode = "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
- First Mapping Tutorial - Learn basic mapping syntax
- Configuration Overview - Complete config.toml structure
- Actions Reference - All available actions including ModeChange
- LED System - How mode colors work with LED feedback
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
Via GUI (Recommended)
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
- File Watcher: Monitors
~/.config/conductor/config.tomlfor changes - Debouncing: 500ms debounce window to avoid redundant reloads
- Parsing: Validates TOML syntax and config structure
- Atomic Swap: Replaces active config with new one in a single operation
- 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:
- Write to temporary file:
state.json.tmp - Verify write succeeded
- Rename to
state.json(atomic operation) - 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 statusreload- Reload configurationvalidate- Validate config without reloadingstop- Graceful shutdownping- 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 bytesreload: ~100 bytesvalidate: ~150 bytesping: ~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
Menu Bar Integration (macOS)
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:
- Go to Settings → General
- Enable “Show menu bar icon”
- 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
- Per-App Profiles - Automatic profile switching
- CLI Reference - Complete CLI documentation
- Configuration Overview - Config file reference
- Performance Tuning - Optimization guide
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
-
Connect a Device
- Navigate to Devices tab
- Select your MIDI controller from the list
- Click Connect
-
Choose a Template (optional)
- Click Device Templates
- Select a pre-configured template for your device
- Click Apply Template
-
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”).
- Go to Modes tab
- Click + Add Mode
- Fill in:
- Name: Descriptive name (e.g., “Video Editing”)
- Color: Visual identifier (blue, green, purple, etc.)
- Click Save
Editing Modes
- Select the mode from the list
- Click Edit Mode
- Modify settings:
- Name
- Color
- Mode-specific mappings
- Click Save Changes
Deleting Modes
- Select the mode
- Click Delete Mode
- 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.
- Go to Mappings tab
- Choose:
- Mode-specific mappings (active only in selected mode)
- Global mappings (active across all modes)
- Click + Add Mapping
Using MIDI Learn
The fastest way to create mappings:
- Click 🎹 MIDI Learn button
- Press/turn the button/knob on your device
- Conductor detects the pattern (note, velocity, long press, etc.)
- The trigger is auto-filled
- Configure the action (what to do)
- 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
- In the Mappings tab
- Click the mapping to edit
- Modify trigger or action
- Click Save Changes
Deleting Mappings
- Select the mapping
- Click Delete
- Confirm deletion
Device Management
Connecting Devices
- Go to Devices tab
- View available MIDI devices
- See connection status
- Monitor daemon status (running, uptime, events processed)
Using Device Templates
Templates provide pre-configured mappings for popular controllers:
- Click 📋 Device Templates
- Browse available templates:
- Maschine Mikro MK3
- Launchpad Mini
- Korg nanoKONTROL
- Custom templates
- Select a template
- Click Apply
- Reload daemon configuration
Profile Management
Profiles let you switch entire configurations:
- Click 🔄 Profiles
- View available profiles
- Switch manually or enable per-app automatic switching
- 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:
- Go to Settings tab
- Click 📊 Show Event Console
- View live MIDI events:
- Note on/off
- Velocity values
- Control changes
- Timing information
- 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:
- See the file path
- Click 📋 to copy path to clipboard
- Click 📝 to open in your default editor
Menu Bar (System Tray)
The menu bar icon provides quick access:
Menu Options
- 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
-
Use MIDI Learn for faster setup - it’s more accurate than manual entry
-
Test mappings immediately after creation - press the button to verify
-
Start with global mappings for frequently used actions (volume, mode switch)
-
Use descriptive names for modes and mappings - future you will thank you
-
Export your config regularly - back up your work
-
Use the event console when troubleshooting - see exactly what MIDI data is coming in
-
Organize with modes - keep related mappings together
-
Device templates save time - start with a template and customize
Troubleshooting
GUI Won’t Connect to Daemon
- Check daemon is running:
conductorctl status - Start daemon if needed:
conductor - Check IPC socket exists:
ls /tmp/conductor.sock - Restart daemon:
conductorctl stop && conductor
Mappings Not Saving
- Check file permissions on config file
- Verify config path in Settings tab
- Check daemon logs for errors
- Try manual edit to verify TOML syntax
MIDI Events Not Detected
- Check device connection in Devices tab
- Use event console to verify MIDI data
- Ensure correct MIDI port selected
- Check device permissions (Input Monitoring on macOS)
Menu Bar Icon Missing
- macOS: Check System Settings → Privacy & Security → Accessibility
- Linux: Ensure system tray extension installed
- Try restarting the GUI application
Next Steps
- Learn about MIDI Learn mode
- Set up per-app profiles
- Explore device templates
- Configure LED feedback
- Use the event console for debugging
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
Via GUI (Recommended)
-
Open Conductor GUI
-
Go to Settings → Device Templates
-
Select your controller from the dropdown
-
Click “Load Template”
-
Confirm the load (replaces current config)
-
Test - Press pads to verify mappings
-
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:
- Click Edit on any mapping
- Click Learn next to the trigger
- Press the pad/button you want
- Update the action
- Save
2. Add New Modes
Templates include 2-4 modes. Add more:
- Go to Modes panel
- Click Add Mode
- Set name and color
- Add mappings using MIDI Learn
3. Adjust LED Schemes
Change LED behavior:
- Go to Settings → LED Feedback
- Select scheme: Reactive, Rainbow, Pulse, etc.
- Customize colors
- Save
4. Export Customized Template
Save your modifications:
- Go to Settings → Export Config
- Save as new template file
- Share with others or use on other machines
Creating Custom Templates
You can create templates for controllers not included:
Step 1: Map Your Device
- Connect device and use MIDI Learn for all controls
- Organize into modes (Default, Media, Development, etc.)
- Configure LED feedback (if supported)
- Test thoroughly with real workflows
Step 2: Export Template
- Settings → Export Config
- 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
- Reset config to default
- Load your template
- Verify all mappings work
- Check LED behavior
Step 5: Share (Optional)
Submit to Conductor template library:
- Create PR to
config/device_templates/directory - Include template file + documentation
- 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:
- Check template file exists in
~/.config/conductor/templates/ - Validate TOML syntax:
conductorctl validate --config path/to/template.toml - Check permissions: Template file must be readable
- View error details in Event Console
Wrong Note Numbers
Symptoms: Template loads but pads don’t trigger actions
Solutions:
- Check device mode: Some controllers have multiple MIDI modes
- Verify MIDI channel: Template may use different channel than device
- Use MIDI Learn to detect actual note numbers
- Check Event Console to see incoming MIDI events
LEDs Not Working
Symptoms: Template loads but LEDs don’t respond
Solutions:
- Check device supports RGB: Some controllers only have single-color LEDs
- Verify HID access: Grant Input Monitoring permission
- Try different LED scheme: Some schemes may not work on all devices
- Check template LED config: May be disabled in template
Best Practices
Tip 1: Load Template First
When setting up a new controller:
- Load template first (if available)
- Test default mappings
- 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:
- Load template
- Customize for specific app (Logic Pro, VS Code, etc.)
- Export as new template
- 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
- MIDI Learn Mode - Customize templates
- Per-App Profiles - Create app-specific variants
- LED System - Customize LED behavior
- Configuration Reference - Template file format
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
- Connect your gamepad via USB or Bluetooth
- Ensure it’s recognized by your system
- 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):
- Open Conductor GUI
- Navigate to “Device Templates”
- Filter by “Gamepad Controllers”
- Select your controller (Xbox, PlayStation, or Switch Pro)
- 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-axis129: Left stick Y-axis130: Right stick X-axis131: 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)
| ID | Xbox | PlayStation | Switch |
|---|---|---|---|
| 128 | A (South) | Cross | B |
| 129 | B (East) | Circle | A |
| 130 | X (West) | Square | Y |
| 131 | Y (North) | Triangle | X |
D-Pad (132-135)
| ID | Button |
|---|---|
| 132 | Up |
| 133 | Down |
| 134 | Left |
| 135 | Right |
Shoulder Buttons (136-137)
| ID | Xbox | PlayStation | Switch |
|---|---|---|---|
| 136 | LB (L1) | L1 | L |
| 137 | RB (R1) | R1 | R |
Stick Clicks (138-139)
| ID | Button |
|---|---|
| 138 | Left stick click (L3) |
| 139 | Right stick click (R3) |
Menu Buttons (140-142)
| ID | Xbox | PlayStation | Switch |
|---|---|---|---|
| 140 | Menu (Start) | Options | + (Plus) |
| 141 | View (Select) | Share/Create | - (Minus) |
| 142 | Xbox button | PS button | Home |
Trigger Buttons Digital (143-144)
| ID | Xbox | PlayStation | Switch |
|---|---|---|---|
| 143 | LT (digital) | L2 (digital) | ZL |
| 144 | RT (digital) | R2 (digital) | ZR |
Analog Axes
Stick Axes (128-131)
| ID | Control |
|---|---|
| 128 | Left stick X-axis |
| 129 | Left stick Y-axis |
| 130 | Right stick X-axis |
| 131 | Right stick Y-axis |
Trigger Axes (132-133)
| ID | Control |
|---|---|
| 132 | Left trigger analog |
| 133 | Right 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:
- Open Conductor GUI
- Click “Learn” next to any mapping
- Press a button or move a stick on your gamepad
- 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:
- Reconnect the gamepad
- Try USB instead of Bluetooth (or vice versa)
- Ensure drivers are installed (Windows)
- Check SDL2 compatibility
Buttons Not Working
Verify button mapping:
- Use MIDI Learn to discover the actual button ID
- Check that button IDs are in the 128-255 range
- 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
- Start with templates: Use official Xbox/PS/Switch templates
- Use MIDI Learn: Let pattern detection configure triggers
- Test incrementally: Add mappings one at a time
- Document custom configs: Add descriptions to mappings
- Use global mappings: Mode switches work everywhere
- Backup configs: Save working configurations
Examples
Complete examples available in:
config/examples/gamepad-xbox-basic.toml- Device templates in Conductor GUI
- See Configuration Examples
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
- Open Conductor GUI
- Navigate to Devices tab
- Click 🔄 Profiles
- Click + New Profile
- Enter profile name (e.g., “vscode-profile”)
- Configure mappings
- 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
- In Profile Manager dialog
- Click Associate App
- Select profile
- Choose application from list
- 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:
- Focus changes to new application
- Conductor detects the frontmost app
- Looks up associated profile
- Loads and activates profile
- LED feedback shows profile change (optional)
Switching latency: ~50ms
Manual Override
Force a specific profile:
GUI Method
- Click 🔄 Profiles button
- Select profile from list
- Click Activate
CLI Method
conductorctl switch-profile vscode
Default Fallback
If no profile matches the current app, Conductor uses:
defaultprofile (if exists)- 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
- Profile Manager → Select profile
- Click Export
- Choose format (TOML/JSON)
- Save file
CLI Method
conductorctl export-profile vscode > vscode-profile.toml
Import Profiles
Load profiles from files:
GUI Method
- Profile Manager → Import
- Select file
- Choose name for imported profile
- Click Import
CLI Method
conductorctl import-profile vscode-profile.toml
Profile Validation
Test profiles before activating:
conductorctl validate-profile vscode
Troubleshooting
App Not Detected
-
Check permissions:
- macOS: Accessibility permissions granted
- Linux: Running with sufficient privileges
- Windows: No UAC blocking
-
Verify app name:
conductorctl frontmost-app -
Check association:
- Ensure app name in config matches actual name
- Use wildcards if app name varies
Profile Not Switching
-
Check app detection is enabled:
conductorctl status -
Verify profile exists:
ls ~/.config/conductor/profiles/ -
Test manual switch:
conductorctl switch-profile vscode -
Check logs:
conductorctl logs | grep profile
Wrong Profile Activates
- Check association priority
- Verify no conflicting wildcards
- Review app name matching in logs
Best Practices
-
Start with defaults: Create a base profile with common mappings
-
Use inheritance: Share global mappings across profiles
-
Test thoroughly: Verify each profile works in target app
-
Name clearly: Use descriptive profile names (e.g., “davinci-resolve” not “prof1”)
-
Document mappings: Add comments in TOML files
-
Version control: Keep profiles in git for history
-
Export regularly: Back up working profiles
-
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:
- Edit profile file
- Save changes
- Switch to different app and back
- Profile reloads automatically
Next Steps
- Learn about MIDI Learn to quickly create profile mappings
- Set up device templates for consistent layouts
- Use the event console to test profile mappings
- Explore LED feedback for visual profile indication
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
- Open Conductor GUI
- Navigate to Settings tab
- Scroll to LED Configuration
- Select scheme from dropdown
- Adjust brightness slider
- Configure fade time
- Enable/disable mode colors
- 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
-
Check device support:
# List HID devices ls /dev/hidraw* # Linux system_profiler SPUSBDataType # macOS -
Verify permissions (macOS):
- System Settings → Privacy & Security → Input Monitoring
- Grant access to Conductor
-
Test with diagnostic tool:
cargo run --bin led_diagnostic -
Check HID access:
# macOS: Ensure shared device access DEBUG=1 conductor 2 --led reactive
Wrong Colors
- Verify color mapping in config
- Check if device uses non-standard RGB order (some use GRB or BGR)
- Try different lighting scheme
- Calibrate brightness
Flickering LEDs
- Reduce update rate:
update_rate_hz = 30 - Enable frame skipping:
skip_intermediate_frames = true - Check USB power supply
- Disable other LED-controlling software
LEDs Stuck On/Off
- Restart Conductor
- Power cycle MIDI device
- Check for conflicting LED control (e.g., Native Instruments software)
- Reset LEDs:
conductor --led off 2then 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
-
Start with reactive: Most intuitive for new users
-
Match mode colors to usage: Visual cues help remember mode purpose
-
Test visibility: Ensure LEDs visible in your lighting conditions
-
Don’t overdo it: Complex animations can be distracting
-
Battery consideration: Disable LEDs for battery-powered setups
-
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
- Explore GUI configuration for visual LED setup
- Use MIDI Learn with LED feedback
- Set up per-app profiles with different LED schemes
- Try the event console to debug LED behavior
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
- Open Conductor GUI
- Navigate to Settings tab
- Click 📊 Show Event Console
- 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:
- Open Event Console
- Press the pad
- See
NOTE_ON | Note: 36in console - 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:
- Open Event Console
- Hold the pad
- Check if
LONG_PRESSevent appears - 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:
- Press pad softly: Check velocity value
- Press pad medium: Check velocity value
- Press pad hard: Check velocity value
- 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:
- Press chord notes
- 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:
- Trigger the mapping
- Watch action execution in console
- 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
- Select events
- Click Export
- 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
- Check daemon is running:
conductorctl status - Verify events are being generated (press pads)
- Restart daemon:
conductorctl restart - 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
-
Check buffer size isn’t full:
[event_console] buffer_size = 10000 # Increase if needed -
Verify event types are enabled:
[event_console] capture_midi = true capture_processed = true capture_actions = true -
Check log level:
DEBUG=1 conductorctl events
Best Practices
- Start with full view: See all event types initially
- Filter progressively: Add filters as you narrow down issues
- Use follow mode:
-fflag for live monitoring - Export for analysis: Save interesting sessions
- Watch timing: Event timing reveals latency issues
- Compare configs: Record events, change config, compare results
- 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
- Use console to build mappings with MIDI Learn
- Debug complex triggers in mapping configuration
- Monitor LED feedback in real-time
- Analyze per-app profile switching behavior
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:
- Open a mapping in the Mappings view
- Locate Velocity Mapping section
- Select mapping type from dropdown:
- Fixed
- Pass-Through
- Linear
- Curve
- Adjust parameters:
- Fixed: Set velocity (0-127)
- Linear: Set min/max range
- Curve: Choose curve type and intensity (0.0-1.0)
- 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
- Start with PassThrough and test your natural playing
- Identify issues:
- Soft hits not registering? → Try Exponential curve
- Too much variation? → Try Linear compression
- Need smoothing? → Try S-Curve
- Adjust intensity gradually (0.3 → 0.5 → 0.7)
- 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:
- Save your config (hot-reload applies changes)
- Test range: Hit the pad softly, medium, hard
- Check preview graph: Does curve match your intent?
- 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→ linearintensity = 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
Related Documentation
- Configuration: Velocity Mappings - Complete TOML reference
- Context-Aware Mappings - Combine with conditionals
- Configuration: Actions - Action types reference
- Guides: Per-App Profiles - Different curves per application
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:00to06: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:
- Condition Type Selector: Dropdown with descriptions
- Time Pickers: Visual time selection for TimeRange
- Day Toggles: Click days for DayOfWeek conditions
- Text Inputs: For app names (AppRunning, AppFrontmost)
- Mode Dropdown: Auto-populated from available modes (ModeIs)
- Logical Operators: Add/remove sub-conditions for And/Or
- Simple (non-nested) And/Or/Not supported
- Complex nested logic requires TOML editing
- Then/Else Actions: Full ActionSelector for both branches
- Optional Else: Toggle to add/remove else_action
To Configure:
- Select action type = “Conditional”
- Choose condition type
- Fill in condition parameters
- Configure then_action
- (Optional) Enable else_action and configure
- 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:
- Test TimeRange alone
- Test AppRunning alone
- 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:
- Check condition type matches intended logic
- Verify app name matches process name (
ps aux | grep <app>) - Test time format (must be
HH:MM, 24-hour) - Check day numbers (Monday=1, not 0)
- Add debug
else_actionto confirm condition is evaluating
Wrong Action Executes
Problem: else_action runs instead of then_action
Solutions:
- Verify condition logic (And vs Or)
- Check app name capitalization (case-sensitive on some platforms)
- Test each sub-condition individually
- Add
type = "Text"debug actions to see which path executes
AppFrontmost Not Working
Problem: Condition always false
Solutions:
- Verify platform support (macOS only currently)
- Check app name matches bundle name (use Activity Monitor)
- Try partial name (“Safari” instead of “com.apple.Safari”)
Related Documentation
- Configuration: Conditionals - Complete TOML reference
- Tutorial: Dynamic Workflows - Step-by-step guide
- Guides: Velocity Curves - Combine with velocity mappings
- Configuration: Actions - Action types reference
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:
- Input: Conductor receives MIDI from your controller (pads, encoders, buttons)
- Mapping: Conductor maps input events to SendMIDI actions
- Output: SendMIDI sends MIDI messages to a virtual MIDI port
- 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:
-
Open Audio MIDI Setup application
- Found in
/Applications/Utilities/Audio MIDI Setup.app - Or press
Cmd+Spaceand search “Audio MIDI Setup”
- Found in
-
Show MIDI Studio
- Go to
Window→Show MIDI Studio(or pressCmd+2)
- Go to
-
Double-click IAC Driver icon
- If you don’t see it, create it:
Window→Show MIDI Studio→ click the globe icon
- If you don’t see it, create it:
-
Enable the driver
- Check “Device is online” checkbox
- You should see at least one port named “IAC Driver Bus 1”
-
(Optional) Add more ports
- Click the + button under “Ports” to create additional buses
- Rename buses to something meaningful (e.g., “Conductor → Logic Pro”)
-
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:
- Visit Tobias Erichsen’s loopMIDI page
- Download the installer (free, no registration required)
- Run the installer (requires Administrator privileges)
- Launch loopMIDI from Start Menu
Create Virtual Port:
- In loopMIDI window, enter a port name (e.g., “Conductor Virtual Out”)
- Click + (Plus button) to create the port
- 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
audiogroup: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:
- Save the config above to your
config.toml - Restart Conductor daemon:
conductorctl reload - Open your DAW and create a software instrument track
- Set the track’s MIDI input to your virtual port (IAC Driver/loopMIDI)
- Enable MIDI input recording on the track
- Press pad 1 on your controller
- 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# | Name | Purpose | Range |
|---|---|---|---|
| 1 | Modulation Wheel | Vibrato, tremolo | 0-127 |
| 7 | Volume | Track/channel volume | 0-127 (100=max) |
| 10 | Pan | Left/right panning | 0=left, 64=center, 127=right |
| 11 | Expression | Volume changes within a note | 0-127 |
| 64 | Sustain Pedal | Hold notes | 0-63=off, 64-127=on |
| 71 | Filter Resonance | Synth filter resonance | 0-127 |
| 74 | Filter Cutoff | Synth filter cutoff freq | 0-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:
-
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 -
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
-
Test: Press the pad again - the parameter should move!
DAW-Specific MIDI Learn:
- Logic Pro:
Cmd+L→ Click parameter → Move controller →Cmd+Lto 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:
- Open DAW preferences/settings
- Go to MIDI Input settings
- Ensure your virtual port is enabled:
- Logic Pro:
Preferences→MIDI→Inputs→ Enable “IAC Driver Bus 1” - Ableton Live:
Preferences→Link/Tempo/MIDI→MIDI Ports→ Enable “Track” and “Remote” for IAC Driver - Reaper:
Preferences→MIDI Devices→ Enable input for virtual port
- Logic Pro:
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:
-
DAW Buffer Size: Larger audio buffers = more latency
- Solution: Reduce buffer size in DAW audio preferences (e.g., 128 or 256 samples)
-
MIDI Port Polling: Some virtual MIDI implementations poll slowly
- Solution: Restart DAW and Conductor daemon
-
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:
-
List available ports:
conductorctl list-midi-outputs -
Check port name matches exactly (case-sensitive):
# Wrong: port = "iac driver bus 1" # Correct: port = "IAC Driver Bus 1" -
macOS: Verify IAC Driver is online in Audio MIDI Setup
-
Windows: Ensure loopMIDI is running and port is created
-
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=1to verify MIDI messages are sent correctly
Next Steps
- Platform-Specific Guides: See detailed examples for Logic Pro and Ableton Live
- Advanced Patterns: Learn about context-aware mappings with conditionals
- Velocity Control: Explore velocity curves for expressive MIDI control
- Troubleshooting: Check MIDI Output Troubleshooting for common issues
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:
- Beginner: Time-based app launcher
- Intermediate: Velocity-sensitive DAW control
- 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
- Save
config.toml - Config will hot-reload automatically
- Press your configured pad during work hours (Mon-Fri 9am-5pm)
- Verify Slack launches
- Press the same pad outside work hours
- 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:
- Open Mappings view
- Click “Add Mapping”
- Set trigger to Note 8
- Select action type “Conditional”
- Select condition type “And”
- Add two sub-conditions:
- TimeRange: 09:00 to 17:00
- DayOfWeek: Select Mon-Fri
- Configure then_action: Launch → Slack
- Configure else_action: Launch → Discord
- 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
-
With Logic Pro closed:
- Press pad → Logic Pro launches
- Wait 2 seconds → See notification
-
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 Velocity | Without Curve | With 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:
- Single condition
- Add second condition with And
- Add nested conditional
- Add velocity mapping
2. Test Each Layer
After adding each condition:
- Save config
- Test the new behavior
- Verify existing behavior still works
- 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:
- Time format is
HH:MM(24-hour) - App name matches exactly (check Activity Monitor on macOS)
- Day numbers are correct (Monday=1, not 0)
- 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:
- Logical operator (And vs Or)
- Nesting structure (use proper TOML indentation)
- 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:
- Open GUI velocity curve preview
- Adjust intensity in 0.1 increments
- 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:
- Per-App Profiles: Different mappings for different apps
- Mode-Based Logic: Use ModeIs condition for mode-specific behavior
- Sequence Actions: Chain multiple actions with delays
- 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
- Guide: Context-Aware Mappings - Deep dive into conditionals
- Guide: Velocity Curves - Master velocity mappings
- Configuration: Actions - All available actions
- Configuration: Triggers - All available triggers
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 splitdaw-control.toml- Velocity-sensitive music productiongaming-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.”
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.”
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.”
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.”
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.”
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.”
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.”
Share Your Setup
Have an interesting Conductor configuration? Share it with the community!
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 - User testimonials and results
- Device Templates - Pre-built configs for popular controllers
- Configuration Examples - Copy-paste ready configs
- Community Discussions - Ask questions, share ideas
Success Stories
Real users sharing how Conductor transformed their workflows.
“From $300 Stream Deck to $30 Xbox Controller”
Sarah, Twitch Streamer • Portland, 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)
“Velocity Curves Changed My Music Production Workflow”
Marcus, Electronic Music Producer • Berlin, 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.”
“Racing Wheel Became My Video Editor”
Chris, YouTube Creator • Los 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.”
“One-Button Git Workflow Saves Me 30 Minutes Daily”
Dr. Elena Rodriguez, Research Scientist • Cambridge, 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.”
“HOTAS for Productivity? Best Decision Ever.”
Jake, Full-Stack Developer • Austin, 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.”
“Turned My Launchpad Into a DAW Command Center”
Ava, Film Composer • Nashville, 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?’”
Submit Your Success Story
Have you transformed your workflow with Conductor? Share your story and inspire others!
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
- Setup Gallery - Visual showcase of real Conductor configurations
- Configuration Examples - Copy-paste ready configs
- Device Templates - Pre-built configs for popular controllers
- Community Discussions - Join the conversation
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_diagnosticto 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
- Explore Velocity Curves - Fine-tune velocity response
- Set Up LED Feedback - Visual mode indicators
- Join Community - Share your setup and get help
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 statusinstead of justgit 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
- Explore Streaming Examples - OBS and streaming platform control
- See Automation Examples - Advanced productivity workflows
- Learn Shell Actions - Shell command configuration
- Join Developer Community - Share your dev setup
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):
| Action | Hotkey | Controller Button |
|---|---|---|
| Scene 1 | Ctrl+Shift+1 | A |
| Scene 2 | Ctrl+Shift+2 | B |
| Scene 3 | Ctrl+Shift+3 | X |
| Scene 4 | Ctrl+Shift+4 | Y |
| Start Recording | Ctrl+Shift+R | LB |
| Start Streaming | Ctrl+Shift+S | RB |
| Mute Microphone | Ctrl+Shift+M | B (double-tap) |
| Toggle Webcam | Ctrl+Shift+W | Start |
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
| Setup | Hardware | Cost | Savings |
|---|---|---|---|
| Conductor | Xbox Controller (owned) | $0 | $150-300 |
| Conductor | New Xbox Controller | $30-60 | $90-270 |
| Conductor | PlayStation DualSense | $60-70 | $80-240 |
| Stream Deck | Stream Deck Mini | $79.99 | - |
| Stream Deck | Stream Deck MK.2 | $149.99 | - |
| Stream Deck | Stream Deck XL | $249.99 | - |
Bottom Line: Reusing an existing gamepad = $150-300 saved, same functionality.
Next Steps
- See Success Stories - Real streamer testimonials
- Explore Automation Examples - Advanced workflows
- Learn Button Chords - Complex actions
- Join Streaming Community - Share your setup
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
- See Success Stories - Chris’s racing wheel workflow
- Explore Gallery - Visual setup examples
- Learn Analog Axes - Pedal control
- Join Video Editing Community - Share your creative setup
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
| Task | Manual Time | Automated Time | Daily Frequency | Time Saved |
|---|---|---|---|---|
| Form filling | 45s | 2s | 10× | 7m 10s |
| Email reply | 60s | 5s | 15× | 13m 45s |
| App switching | 5s | 1s | 50× | 3m 20s |
| Window management | 8s | 1s | 30× | 3m 30s |
| Total Daily | - | - | - | 27m 45s |
| Annual | - | - | - | 115 hours |
Next Steps
- See Developer Workflows - Git automation
- See Streaming Examples - OBS automation
- Learn Sequences - Multi-step actions
- Explore Conditionals - Context-aware mappings
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:
- Open Audio MIDI Setup (Applications → Utilities)
- Show MIDI Studio (Window → Show MIDI Studio)
- Double-click IAC Driver
- Check “Device is online”
- Ensure at least one bus exists (default: “IAC Driver Bus 1”)
- Click Apply
2. Configure Logic Pro MIDI Input
In Logic Pro:
- Open Logic Pro → Settings → MIDI → Inputs (or press ⌘,)
- Locate IAC Driver Bus 1 in the device list
- Check the box to enable it
- 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:
- Open Logic Pro → Control Surfaces → Setup
- Click New → Install… (or press ⌘N)
- Choose Mackie Control or Generic from the list
- In the device settings:
- Input: Select IAC Driver Bus 1
- Output: (Optional - for feedback)
- Click Apply or OK
5. Test the Configuration
- Start the Conductor daemon:
conductor - Open a Logic Pro project
- Press Pad 1 on your controller → Logic should Play/Pause
- Press Pad 2 → Logic should Stop
- 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:
| Function | CC Number | Value Range | Notes |
|---|---|---|---|
| Play/Pause | 115 | 127 | Toggle transport |
| Stop | 116 | 127 | Stop playback |
| Record | 117 | 127 | Toggle record |
| Rewind | 118 | 127 | Skip backward |
| Fast Forward | 119 | 127 | Skip forward |
| Cycle | 120 | 127 | Toggle cycle mode |
| Volume (Track 1) | 7 | 0-127 | Channel 0 |
| Pan (Track 1) | 10 | 0-127 | Channel 0 |
| Volume (Track 2) | 7 | 0-127 | Channel 1 |
| Pan (Track 2) | 10 | 0-127 | Channel 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
- Open a Logic Pro project with a software instrument
- Show Smart Controls (press B or View → Show Smart Controls)
- Click Learn button in Smart Controls header
- Press a pad on your MIDI controller
- Logic will assign that CC to the Smart Control knob
- 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
- In Logic Pro, go to Logic Pro → Control Surfaces → Learn Assignment
- Click the parameter you want to control (e.g., a plugin knob, fader, button)
- Press the pad on your MIDI controller
- Logic will map that CC to the parameter
- 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:
- Verify IAC Driver is enabled in Audio MIDI Setup
- Restart Logic Pro after enabling IAC Driver
- Check Logic Pro → Settings → MIDI → Reset MIDI Drivers
Messages Sent But No Response
Problem: Conductor sends MIDI but Logic doesn’t respond.
Solution:
- Verify the control surface is configured in Logic Pro → Control Surfaces → Setup
- Ensure the correct CC numbers for your Logic version (some changed in Logic Pro 10.5+)
- Try using MIDI Learn to discover the correct CC numbers
- Check MIDI channel (most Logic functions use Channel 0)
Latency Issues
Problem: Noticeable delay between pad press and Logic response.
Solution:
- Lower Logic Pro’s I/O buffer size: Logic Pro → Settings → Audio → I/O Buffer Size
- Use IAC Driver (lowest latency) instead of virtual MIDI apps
- Reduce CPU load (freeze tracks, disable unused plugins)
Control Surface Conflicts
Problem: Multiple control surfaces interfering with each other.
Solution:
- In Logic Pro → Control Surfaces → Setup, disable unused control surfaces
- Ensure IAC Driver is assigned to only one control surface
- 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
- Ableton Live Integration - Control Ableton Live with Conductor
- MIDI Output Troubleshooting - Advanced debugging
- DAW Control Guide - General DAW control concepts
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:
- macOS: IAC Driver (see DAW Control Guide)
- Windows: loopMIDI (see DAW Control Guide)
- Linux: ALSA virtual port (see DAW Control Guide)
Quick Start
1. Configure Virtual MIDI Port
macOS (IAC Driver)
- Open Audio MIDI Setup (Applications → Utilities)
- Show MIDI Studio (Window → Show MIDI Studio)
- Double-click IAC Driver
- Check “Device is online”
- Click Apply
Windows (loopMIDI)
- Download and install loopMIDI
- Launch loopMIDI
- Create a new port (e.g., “Conductor Virtual”)
- 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:
- Open Live → Preferences (macOS) or Options → Preferences (Windows/Linux)
- Go to the Link, Tempo & MIDI tab
- 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
- Enable Track and Remote for the input port
- 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
- Start the Conductor daemon:
conductor - Open Ableton Live with a project containing clips
- Press Pad 1 on your controller → Scene 1 should launch
- 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
- In Ableton Live, click MIDI in the top-right corner (or press ⌘M / Ctrl+M)
- The interface will highlight in purple
- Click the parameter you want to control (e.g., a volume fader, play button, device knob)
- Press the pad on your MIDI controller
- Ableton will map that MIDI message to the parameter
- 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
- Open a device (e.g., Filter, Reverb, Wavetable synth)
- Enter MIDI Map Mode (click MIDI or press ⌘M)
- Click a device knob or parameter
- Press a pad on your MIDI controller
- 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 -lto 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:
- Verify Track and Remote are enabled for the MIDI port in Preferences
- Ensure the correct MIDI channel is used (usually Channel 0)
- Use MIDI Map Mode in Ableton to verify what MIDI messages are being received
- Check Conductor logs:
conductor --verbose
Clip Launch Notes Don’t Match
Problem: Sending Note 0 doesn’t launch the expected clip.
Solution:
- Enter MIDI Map Mode in Ableton
- Click the clip you want to trigger
- Press the pad on your controller to learn the note
- Update your Conductor config with the correct note number
Windows: Port Name Issues
Problem: Port name “loopMIDI Port” not found.
Solution:
- Check the exact port name in loopMIDI application
- Update Conductor config to match the exact name (case-sensitive)
- Ensure loopMIDI is running before starting Conductor daemon
Latency Issues
Problem: Noticeable delay between pad press and Ableton response.
Solution:
- Lower Ableton’s audio buffer size: Preferences → Audio → Buffer Size
- Use native virtual MIDI drivers (IAC on macOS, ALSA on Linux) instead of third-party apps
- Close unnecessary applications to reduce CPU load
- 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
- Logic Pro Integration - Control Logic Pro with Conductor
- MIDI Output Troubleshooting - Advanced debugging
- DAW Control Guide - General DAW control concepts
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/displayauto_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_msfor slower chord playing (e.g., 200-300ms) - Decrease
double_tap_timeout_msfor faster double-tap detection (e.g., 200ms) - Adjust
hold_threshold_msfor 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 identifiercolor(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 mappingaction(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:
- Locates
config.toml - Parses TOML syntax
- Validates structure and values
- Compiles triggers and actions
- 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 checkor 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
triggerandaction - All trigger/action types are spelled correctly
- Note numbers match your device (use
pad_mapperto 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 ...
2. Group Related Mappings
# 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:
- Start with one mode and one mapping
- Test it works
- Add more mappings one at a time
- 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.tomlfor changes - Automatically reload without restarting
- Validate before applying (no downtime on errors)
See Also
- Modes Guide - Deep dive into modes
- Triggers Reference - All trigger types
- Actions Reference - All action types
- First Mapping Tutorial - Step-by-step guide
- TOML Specification - Official TOML documentation
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:
- Validate TOML syntax online: https://www.toml-lint.com/
- Check quotes, brackets, and indentation
- 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:
| Range | Protocol | Used For | Examples |
|---|---|---|---|
| 0-127 | MIDI | Notes, CC, Encoders | MIDI note C4=60, CC Mod Wheel=1 |
| 128-255 | Game Controllers | Buttons, Axes, Triggers | Gamepad 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 tomedium_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 Range | Button Type | Specific Buttons |
|---|---|---|
| 128-131 | Face Buttons | 128=South (A/Cross/B), 129=East (B/Circle/A), 130=West (X/Square/Y), 131=North (Y/Triangle/X) |
| 132-135 | D-Pad | 132=Up, 133=Down, 134=Left, 135=Right |
| 136-137 | Shoulders | 136=L1/LB/L, 137=R1/RB/R |
| 138-139 | Stick Clicks | 138=L3 (left stick click), 139=R3 (right stick click) |
| 140-142 | Menu Buttons | 140=Start, 141=Select/Back/View, 142=Guide/Home/PS |
| 143-144 | Digital Triggers | 143=L2/LT/ZL, 144=R2/RT/ZR (digital press, not analog) |
Cross-Platform Button Mapping:
| Button | Xbox | PlayStation | Nintendo Switch |
|---|---|---|---|
| South (128) | A | Cross (×) | B |
| East (129) | B | Circle (○) | A |
| West (130) | X | Square (□) | Y |
| North (131) | Y | Triangle (△) | X |
| L1 (136) | LB | L1 | L |
| R1 (137) | RB | R1 | R |
| L2 (143) | LT | L2 | ZL |
| R2 (144) | RT | R2 | ZR |
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:
| ID | Axis | Description |
|---|---|---|
| 128 | Left Stick X | Left stick horizontal (left = CounterClockwise, right = Clockwise) |
| 129 | Left Stick Y | Left stick vertical (down = CounterClockwise, up = Clockwise) |
| 130 | Right Stick X | Right stick horizontal (left = CounterClockwise, right = Clockwise) |
| 131 | Right Stick Y | Right 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:
| ID | Trigger | Description |
|---|---|---|
| 132 | Left Trigger | L2 (PlayStation), LT (Xbox), ZL (Switch) |
| 133 | Right Trigger | R2 (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:
| Threshold | Use Case | Sensitivity |
|---|---|---|
| 0-20 | Feather touch | Very sensitive |
| 40-60 | Light pull | Medium sensitivity |
| 64 | Half-pull | Balanced (recommended) |
| 80-100 | Firm pull | Low sensitivity |
| 110-127 | Full pull | Very 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
- ID Separation: Always use 0-127 for MIDI, 128-255 for gamepad
- Chord Detection: You can mix MIDI and gamepad IDs in
NoteChordandGamepadButtonChordtriggers - Mode Switching: Use either MIDI or gamepad inputs to switch modes
- Global Mappings: Define device-agnostic global mappings that work across both protocols
- 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)
| Octave | C | C# | D | D# | E | F | F# | G | G# | A | A# | B |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| -2 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| -1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 0 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
| 1 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
| 2 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
| 3 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
| 4 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
| 5 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
| 6 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
| 7 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
| 8 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | - | - | - | - |
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)
| ID | Button | Xbox | PlayStation | Switch | Device Type |
|---|---|---|---|---|---|
| 128 | South | A | Cross (×) | B | Gamepad |
| 129 | East | B | Circle (○) | A | Gamepad |
| 130 | West | X | Square (□) | Y | Gamepad |
| 131 | North | Y | Triangle (△) | X | Gamepad |
| 132 | D-Pad Up | D-Up | D-Up | D-Up | Gamepad |
| 133 | D-Pad Down | D-Down | D-Down | D-Down | Gamepad |
| 134 | D-Pad Left | D-Left | D-Left | D-Left | Gamepad |
| 135 | D-Pad Right | D-Right | D-Right | D-Right | Gamepad |
| 136 | Left Shoulder | LB | L1 | L | Gamepad |
| 137 | Right Shoulder | RB | R1 | R | Gamepad |
| 138 | Left Stick Click | L3 | L3 | L-Stick | Gamepad |
| 139 | Right Stick Click | R3 | R3 | R-Stick | Gamepad |
| 140 | Start | Menu | Options | + | Gamepad |
| 141 | Select | View | Share | - | Gamepad |
| 142 | Guide | Xbox | PS | Home | Gamepad |
| 143 | Left Trigger | LT | L2 | ZL | Gamepad (digital) |
| 144 | Right Trigger | RT | R2 | ZR | Gamepad (digital) |
Gamepad Axis IDs (128-133)
| ID | Axis | Description | Device Type |
|---|---|---|---|
| 128 | Left Stick X | Horizontal (left/right) | Gamepad, Racing Wheel |
| 129 | Left Stick Y | Vertical (up/down) | Gamepad, HOTAS Throttle |
| 130 | Right Stick X | Horizontal (left/right) | Gamepad, Flight Stick |
| 131 | Right Stick Y | Vertical (up/down) | Gamepad, Flight Stick |
| 132 | Left Trigger | Analog (0-127) | Gamepad, Racing Wheel (brake) |
| 133 | Right Trigger | Analog (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:
- Open Conductor GUI
- Navigate to a mapping
- Click “MIDI Learn”
- Press the desired button on your gamepad
- The button ID will be auto-filled
2. Testing Triggers
Use the live event console in Conductor GUI to see real-time trigger events:
- Open Conductor GUI
- Navigate to “Event Console”
- Press buttons/move sticks on your gamepad
- Observe the event type and ID
- 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:
- Open Conductor GUI
- Navigate to “Device Templates”
- Filter by device type
- 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
- Verify USB/Bluetooth connection
- Check if gamepad is recognized by OS
- Ensure Conductor has Input Monitoring permissions (macOS)
- Try reconnecting the gamepad
Button IDs Not Working
- Use MIDI Learn to verify correct button ID
- Check ID range (128-255 for gamepad)
- Ensure gamepad is SDL2-compatible
- Consult device-specific documentation
Analog Stick False Triggers
- Increase dead zone threshold (default: 10%)
- Clean analog stick (dust/debris can cause drift)
- Use directional filtering (
directionparameter)
Analog Trigger Sensitivity
- Adjust
thresholdparameter (64 = half-pull) - Lower threshold for more sensitivity
- Higher threshold for less sensitivity
- Test with event console to find optimal value
Related Documentation
- Actions Reference: Learn about available actions to execute
- Gamepad Support Guide: Comprehensive guide to using gamepads with Conductor
- Configuration Guide: Complete configuration file structure
- MIDI Learn: Auto-detect button and note IDs
- Device Templates: Pre-configured templates for popular devices
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 Type | Description | Complexity |
|---|---|---|
| Keystroke | Send keyboard shortcuts | Simple |
| Text | Type text strings | Simple |
| Launch | Open applications | Simple |
| Shell | Execute shell commands | Simple |
| Sequence | Chain multiple actions | Moderate |
| Delay | Add timing control | Simple |
| MouseClick | Simulate mouse clicks | Simple |
| Repeat | Repeat actions N times | Moderate |
| VolumeControl | System volume control | Simple |
| ModeChange | Switch mapping modes | Simple |
| SendMidi | Send MIDI messages | Moderate |
| Conditional | Context-aware execution | Advanced |
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 keyctrl/control- Control keyalt/option- Alt/Option keyshift- Shift key
Special Keys:
- Navigation:
up,down,left,right,home,end,pageup,pagedown - Editing:
backspace,delete,tab,return,enter,escape,esc - Function:
f1throughf12 - 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 -acommand- 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 startcommand- Accepts app names or paths
- Uses file associations
Troubleshooting:
- Test manually:
open -a "App Name"(macOS) orwhich 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 repeatdelay_ms(optional): Delay in milliseconds between iterations (not applied after last iteration)
Edge Cases:
count = 0is valid (no-op)count = 1executes 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:
- Pagination: Scroll through long lists
- Batch Processing: Repeat workflow on multiple items
- Velocity Mapping: Different repeat counts for soft/medium/hard presses
- 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 outputUnmute- Unmute audio outputSet- Set volume to specific level (0-100), requiresvalueparameter
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 trueNever- Always falseTimeRange- 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 matchingAnd- Logical AND of multiple conditionsOr- Logical OR of multiple conditionsNot- Logical negation
See Also:
- Configuration: Conditionals - Complete TOML reference
- Guide: Context-Aware Mappings - User guide with examples
- Tutorial: Dynamic Workflows - Step-by-step tutorial
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
- Minimize Delays: Only add delays where needed (UI loading, animations)
- Batch Operations: Use Sequence instead of multiple mappings
- Avoid High-Frequency Repeats: Add
delay_between_msfor rapid repeats - Test Incrementally: Start with count=1, then increase
- Consider User Experience: Long-running actions should have feedback
Common Pitfalls
- Blocking Operations: Repeats and sequences block - cannot interrupt
- Timing Fragility: UI timing varies by system load
- Nested Explosions: 10 × 10 nested repeat = 100 executions
- Error Swallowing:
stop_on_error = falsecontinues 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
- Action Types Reference - Complete technical specifications
- Configuration Examples - Real-world configuration patterns
- Trigger Types - Trigger configuration reference
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:
minmust 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 notesintensity = 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 compressionintensity = 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 linearintensity = 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:
| Intensity | Effect | Typical Use Case |
|---|---|---|
| 0.0 - 0.2 | Very subtle | Fine-tuning, slight adjustment |
| 0.3 - 0.5 | Moderate | Noticeable but natural feel |
| 0.6 - 0.8 | Strong | Significant transformation |
| 0.9 - 1.0 | Extreme | Dramatic 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:
typemust be one of:"Fixed","PassThrough","Linear","Curve"- Invalid type will cause config load error
Fixed Validation:
velocitymust be 0-127- Missing
velocitywill cause error
Linear Validation:
minandmaxmust be 0-127minshould be ≤max(not enforced, but illogical if reversed)- Missing
minormaxwill cause error
Curve Validation:
curve_typemust be one of:"Exponential","Logarithmic","SCurve"intensitymust be 0.0-1.0- Missing
curve_typeorintensitywill 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
Related Documentation
- Guide: Customizing Velocity Response - User guide with tutorials
- Configuration: Actions - Action types that use velocity
- Guide: Context-Aware Mappings - Combine with conditionals
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:
- condition: The condition to evaluate
- then_action: Action to execute if condition is true
- 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:00to06: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:
daysarray 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
pgrepsubprocess 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:
conditionsarray 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:
conditionsarray 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:
conditionmust 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:
typefield is required for all conditions (except Always/Never which can be just strings)- Invalid condition type will cause config load error
TimeRange:
startandendmust be inHH:MMformat- Hour must be 00-23, minute must be 00-59
DayOfWeek:
daysarray must contain at least one day- Each day must be 1-7
AppRunning/AppFrontmost:
app_namemust be a non-empty string
ModeIs:
modemust be a non-empty string- Mode name is not validated against actual modes (runtime check)
And/Or:
conditionsarray must contain at least one condition- Each element must be a valid condition
Not:
conditionmust 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"
Related Documentation
- Guide: Context-Aware Mappings - User guide with tutorials
- Configuration: Actions - Action types reference
- Guide: Velocity Curves - Combine with velocity mappings
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:
- Open System Settings → Privacy & Security
- Select “Input Monitoring”
- 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
-
Check device connection:
DEBUG=1 cargo run --release 2 --led reactive # Look for: "✓ Connected to Mikro MK3 LED interface" -
Verify permissions (macOS HID):
- System Settings → Privacy → Input Monitoring
-
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 (
reactiveorstatic)
Mode Colors Not Showing
- Issue: All pads same color regardless of mode
- Cause: Using non-mode-aware scheme
- Solution: Use
staticorreactivescheme
Performance Impact
- Issue: High CPU usage
- Cause: Complex animation scheme
- Solution: Switch to
reactiveorstatic(<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
| Scheme | CPU (Idle) | CPU (Active) |
|---|---|---|
| off | 0% | 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
- Reference → LED System - Complete LED feature reference
- Reference → Action Types - ModeChange action
- Device Support → Maschine Mikro MK3 - HID device details
- Configuration → Modes - Mode system configuration
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
- Basic Workflows
- Developer Productivity
- Content Creation
- Repetition & Automation
- Advanced Patterns
- 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
-
Check connection:
# List devices cargo run --bin test_midi cargo run --bin gamepad_diagnostic -
Verify auto_connect:
[device] auto_connect = true # Must be enabled -
Check daemon logs:
DEBUG=1 conductor --foreground # Look for "Connected to MIDI device" and "Connected to gamepad"
Only MIDI or Only Gamepad Working
- Verify ID ranges: MIDI must use 0-127, gamepad must use 128-255
- Check for ID conflicts: No overlapping IDs between devices
- 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
-
Increase chord timeout:
[advanced_settings] chord_timeout_ms = 100 # Increase from 50ms -
Check button IDs: Verify you’re using correct MIDI note + gamepad button ID
-
Use
NoteChordfor MIDI+gamepad: NotGamepadButtonChord
Latency Issues
- Reduce chord_timeout_ms for faster single-button response
- Check system load: Hybrid mode uses minimal CPU but check for other processes
- 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:
- Use MIDI for velocity-sensitive, musical tasks
- Use gamepad for navigation, shortcuts, and ergonomic controls
- Avoid excessive chord mappings (keep under 20 total)
- Test thoroughly before live use
See Also
- Gamepad Support Guide - Complete gamepad documentation
- Gamepad API Reference - Technical API details
- Action Types Reference - All available actions
- Trigger Types - All trigger configurations
Performance Tips
Timing Best Practices
- Application Launch: Wait 1000-2000ms after launching apps
- UI Navigation: Use 100-200ms between UI interactions
- Form Fills: 100ms delay between field navigation
- File Operations: 500-1000ms for save/load operations
- Network Operations: 2000ms+ for remote operations
Repeat Guidelines
- Start Small: Begin with count=1, increase gradually
- Add Delays: Use
delay_between_msfor counts >5 - Test Incrementally: Verify each step before combining
- Consider UX: Counts >100 may appear as hang
- 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_errorsetting - Slow performance: Reduce repeat counts or add delays
See Also
- Actions Reference - Complete action type documentation
- Action Types Reference - Technical specifications
- Trigger Types - Trigger configuration
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 numbermin_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 numbermin_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 numbermax_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 numbersmax_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 numberdirection(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:
| Device | Support | Type | Notes |
|---|---|---|---|
| Maschine Mikro MK3 | ✅ | Polyphonic | Excellent sensitivity |
| Akai MPD Series | ✅ | Polyphonic | Requires firmware 1.5+ |
| Novation Launchpad Pro | ✅ | Polyphonic | Excellent sensitivity |
| Launchpad Mini | ❌ | None | No aftertouch |
| Generic MIDI Keyboard | ⚠️ | Channel | Global 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_diagnostictool 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_diagnostictool - Adjust pressure thresholds
PitchBend Jittery
- Increase
center_deadzonevalue - Enable throttling in
advanced_settings - Use
delta_thresholdfor 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_diagnosticfor 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:
f1throughf12 - 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 -acommand - 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 startcommand - 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-nameor 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 performamount(integer, optional):- For
Up/Down: Increment amount 0-100 (default 5) - For
Set: Absolute level 0-100
- For
Operations:
Up: Increase volume by amountDown: Decrease volume by amountMute: Mute audio outputUnmute: Unmute audio outputToggle: Toggle mute stateSet: Set to specific level (requiresamountparameter)
Platform Support:
| Platform | Method | Latency | Dependencies |
|---|---|---|---|
| macOS | AppleScript | 50-100ms | None (built-in) |
| Linux (PulseAudio) | pactl | 10-30ms | pulseaudio-utils |
| Linux (Pipewire) | wpctl | 5-15ms | wireplumber |
| Linux (ALSA) | amixer | 15-40ms | alsa-utils |
| Windows | nircmd | 20-50ms | nircmd.exe |
| Windows | COM API | 5-10ms | None (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
audiogroup
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 (ifrelative = true)relative(boolean, optional): If true,modeis 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:
| Effect | Duration | Description |
|---|---|---|
| Flash | 150ms | Quick white flash |
| Sweep | 120ms | Left-to-right wave |
| FadeOut | 200ms | Fade out old, fade in new |
| Spiral | 240ms | Center-outward spiral |
| None | 0ms | Instant 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_mappingsto work from all modes
LEDs Not Updating:
- Verify
coloris 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
Delayaction 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 repeatcount(integer): Number of repetitionsdelay_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
xandyare 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 objectsoperator(string, optional): “And” or “Or” (default: “And”)then_action(object): Action when conditions are trueelse_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 trueOr: 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:
- F23: LED Lighting Schemes - 10 pre-defined animation patterns
- F24: LED Velocity Feedback - Color-coded response based on pad velocity
- F25: LED Mode Indicators - Visual feedback for active mode
- 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
| Scheme | Description | Use Case | CPU Impact |
|---|---|---|---|
off | All LEDs disabled | Power saving, minimal distraction | 0% |
static | Solid color based on current mode | Clear mode indication | <1% |
breathing | Slow 2-second breathing effect | Ambient, relaxed workflow | ~1% |
pulse | Fast 500ms pulse | High-energy, performance | ~1% |
rainbow | Rainbow cycle across pads | Creative sessions, visual appeal | ~2% |
wave | Wave animation pattern | Dynamic feedback | ~2% |
sparkle | Random sparkles | Playful, attention-grabbing | ~3% |
reactive | Velocity-based colors (most common) | Precise feedback, performance | ~1% |
vumeter | VU meter style (bottom-up) | Audio visualization | ~2% |
spiral | Spiral pattern animation | Artistic, 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 Range | Color | Visual | Meaning |
|---|---|---|---|
| 0-40 | Green | 🟢 | Soft press |
| 41-80 | Yellow | 🟡 | Medium press |
| 81-127 | Red | 🔴 | 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:
| Mode | Default Color | RGB Values | Visual |
|---|---|---|---|
| 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:
- Pad pressed → Immediate color change (velocity-based)
- Pad released → Wait 1 second
- 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)
-
Check permissions (macOS):
- System Settings → Privacy & Security → Input Monitoring
- Enable permission for Terminal or your IDE
-
Verify HID connection:
DEBUG=1 cargo run --release 2 --led reactive # Look for "✓ Connected to Mikro MK3 LED interface" -
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
staticorreactivescheme, notrainboworsparkle
Performance Considerations
CPU Usage by Scheme
| Scheme | CPU (Idle) | CPU (Active) | Memory |
|---|---|---|---|
| off | 0% | 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
reactiveorstaticfor lowest CPU usage - Visual Appeal: Use
rainboworspiralfor presentations/demos - Battery (USB-powered hubs): Use
offorstaticto 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
- Configuration → LED Feedback
- Device Support → Maschine Mikro MK3
- Reference → Action Types (ModeChange action)
- Troubleshooting → Common Issues
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
conductorctlutility - 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 releaserainbow- Static rainbow gradientbreathing- Breathing effect (all pads)pulse- Pulsing effectwave- Wave pattern with brightness gradientsparkle- Random twinkling LEDsvumeter- VU meter style gradient (green → yellow → red)spiral- Spiral/diagonal patternstatic- Static single coloroff- 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:
- Open Native Instruments Controller Editor
- Select “Maschine Mikro MK3”
- Edit pad pages (A-H)
- Assign MIDI notes to each pad
- Save as
.ncmm3file - Use with
--profileflag
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 errorswarn- Warnings and errorsinfo- General information (default)debug- Debug informationtrace- 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
Stopcommand 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:
- First: Attempts graceful IPC shutdown (same as
shutdowncommand) - Waits: 500ms for daemon to exit
- 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
| Scenario | Use Command | Why |
|---|---|---|
| Daemon running in foreground | shutdown | Faster, direct IPC |
| Daemon started manually (not service) | shutdown | No service to unload |
| Service installed and running | stop | Ensures service unloaded |
| Daemon not responding | stop --force | Bypasses IPC, forces unload |
| Development workflow | shutdown | Quick restarts |
| Production/installed service | stop | Proper 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
| Command | launchctl operation | Starts daemon? | Auto-start on login? |
|---|---|---|---|
install | load | ✓ Yes | ✗ No |
start | load | ✓ Yes | ✗ No |
enable | load -w | ✓ Yes | ✓ Yes |
stop | unload | ✗ Stops | ✗ No |
disable | unload -w | ✗ Stops | ✗ No |
Common patterns:
- Quick setup:
install→ daemon runs but won’t auto-start on reboot - Production setup:
installthenenable→ 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:
- Generates LaunchAgent plist from template
- Copies plist to
~/Library/LaunchAgents/com.amiable.conductor.plist - Optionally installs binary to
/usr/local/bin(requires sudo) - 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:
- Stops service if running
- Removes LaunchAgent plist
- 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:
- Loads service with
launchctl load - Waits for daemon to respond to IPC
- 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:
- Gracefully stops service
- Waits 500ms
- Starts service
- 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:
- Loads service with
launchctl load -wflag - Starts daemon immediately (because plist has
RunAtLoad=true) - 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
disableto 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:
- Unloads service with
launchctl unload -wflag - Stops daemon immediately
- 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 pressedNoteOff- Pad/key releasedCC- Control Change (knobs, sliders)PitchBend- Touch strip, pitch wheelAftertouch- Pressure sensitivityProgramChange- 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:
- Attempts to open HID device
- Displays connection status
- Tests individual LED control
- 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:
- Run the tool
- Press each pad on your controller
- Write down the note number displayed
- 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
| Command | Purpose | Example |
|---|---|---|
| Daemon Service (v1.0.0+) | ||
conductor [PORT] | Start daemon service | cargo run --release --bin conductor 2 |
--led <SCHEME> | Set LED scheme | conductor 2 --led rainbow |
--profile <PATH> | Load profile | conductor 2 --profile my.ncmm3 |
--pad-page <PAGE> | Force pad page | conductor 2 --pad-page H |
--config <PATH> | Custom config | conductor 2 --config dev.toml |
| Daemon Control (v1.0.0+) | ||
conductorctl status | Show daemon status | conductorctl status |
conductorctl reload | Hot-reload config | conductorctl reload |
conductorctl validate | Validate config | conductorctl validate |
conductorctl ping | Health check | conductorctl ping |
conductorctl shutdown | Stop daemon (IPC) | conductorctl shutdown |
conductorctl stop | Stop service (LaunchAgent) | conductorctl stop --force |
| Device Management | ||
conductorctl list-devices | List MIDI devices | conductorctl list-devices |
conductorctl set-device | Switch MIDI device | conductorctl set-device 2 |
conductorctl get-device | Show current device | conductorctl get-device |
| Service Management (macOS) | ||
conductorctl install | Install LaunchAgent | conductorctl install --install-binary |
conductorctl uninstall | Remove service | conductorctl uninstall --remove-logs |
conductorctl start | Start service | conductorctl start --wait 10 |
conductorctl restart | Restart service | conductorctl restart |
conductorctl enable | Enable auto-start | conductorctl enable |
conductorctl disable | Disable auto-start | conductorctl disable |
conductorctl service-status | Service status | conductorctl service-status |
| Global Options | ||
--json | JSON output | conductorctl status --json |
| Diagnostic Tools | ||
DEBUG=1 | Enable debug log | DEBUG=1 conductor 2 |
midi_diagnostic | View MIDI events | cargo run --bin midi_diagnostic 2 |
led_diagnostic | Test LEDs | cargo run --bin led_diagnostic |
led_tester | Interactive LED test | cargo run --bin led_tester |
pad_mapper | Find note numbers | cargo run --bin pad_mapper 2 |
test_midi | Test MIDI ports | cargo 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
- macOS Installation - Platform-specific setup
- Building Guide - Build from source
- Diagnostics Guide - Detailed troubleshooting
- Configuration Overview - Config file structure
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
- Device Configuration
- Input Mode Selection
- Modes
- Mappings
- Trigger Types
- Action Types
- Advanced Settings
- ID Range Allocation
- Complete Examples
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | String | Yes | - | Human-readable device name |
auto_connect | Boolean | No | true | Automatically 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
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Unique mode identifier |
color | String | No | LED 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
| Field | Type | Required | Description |
|---|---|---|---|
description | String | No | Human-readable description of the mapping |
trigger | Table | Yes | Trigger definition (see Trigger Types) |
action | Table | Yes | Action 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:
| ID | Xbox | PlayStation | Switch | Description |
|---|---|---|---|---|
| 128 | A | Cross (✕) | B | South button |
| 129 | B | Circle (○) | A | East button |
| 130 | X | Square (□) | Y | West button |
| 131 | Y | Triangle (△) | X | North button |
| 132 | - | - | - | D-Pad Up |
| 133 | - | - | - | D-Pad Down |
| 134 | - | - | - | D-Pad Left |
| 135 | - | - | - | D-Pad Right |
| 136 | LB | L1 | L | Left shoulder |
| 137 | RB | R1 | R | Right shoulder |
| 138 | L3 | L3 | L-Click | Left stick click |
| 139 | R3 | R3 | R-Click | Right stick click |
| 140 | Menu | Options | + | Start button |
| 141 | View | Share | - | Select/Back button |
| 142 | Xbox | PS | Home | Guide/Home button |
| 143 | LT | L2 | ZL | Left trigger (digital) |
| 144 | RT | R2 | ZR | Right 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:
| ID | Axis | Description |
|---|---|---|
| 128 | Left Stick X | Horizontal movement (left/right) |
| 129 | Left Stick Y | Vertical movement (up/down) |
| 130 | Right Stick X | Horizontal movement (left/right) |
| 131 | Right Stick Y | Vertical movement (up/down) |
| 132 | Left Trigger | Analog trigger pressure (L2/LT/ZL) |
| 133 | Right Trigger | Analog 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
| Field | Type | Default | Description |
|---|---|---|---|
chord_timeout_ms | Integer | 100 | Max time between first and last note/button in chord (ms) |
double_tap_timeout_ms | Integer | 300 | Max time between taps for double-tap detection (ms) |
hold_threshold_ms | Integer | 2000 | Minimum 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
| Range | Protocol | Type | Examples |
|---|---|---|---|
| 0-127 | MIDI | Notes/Pads | C0=36, C4=60, G9=127 |
| 0-127 | MIDI | CC/Encoders | Mod Wheel=1, Volume=7 |
| 128-144 | Game Controllers | Buttons | Face buttons, D-Pad, shoulders |
| 128-133 | Game Controllers | Analog Axes | Sticks, triggers |
| 145-255 | Reserved | Future | Extended 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
- MIDI configs: Use IDs 0-127 only
- Gamepad configs: Use IDs 128-255 only
- 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
- Trigger Types Reference - Detailed trigger documentation
- Action Types Reference - Detailed action documentation
- Gamepad Support Guide - Getting started with gamepads
- Configuration Examples - Real-world configuration examples
- CLI Commands - Command-line tools and debugging
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
InputEventfor 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
| Variant | Description | Use Case |
|---|---|---|
MidiOnly | Connect to MIDI devices only | Traditional MIDI controller workflows |
GamepadOnly | Connect to gamepad devices only | Pure gamepad macro pad setup |
Both | Connect to both MIDI and gamepad | Hybrid 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
GamepadDeviceManagerinstance (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 forInputEventmessagescommand_tx- Channel sender forDaemonCommandmessages (reconnection, etc.)
Returns:
Ok((GamepadId, Name))- Tuple of gamepad ID and device nameErr(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 gamepadsErr(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:
- Polls for gamepad events at 1ms intervals
- Converts gilrs events to
InputEvent - Sends events through the provided channel
- Detects disconnection and triggers reconnection if enabled
Reconnection Logic
When a gamepad disconnects and auto_reconnect is enabled:
- Spawns a reconnection thread
- Uses exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s
- Checks for available gamepads at each interval
- Sends
DaemonCommand::ReconnectGamepadwhen a device is found - 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 gamepadmode- Input mode selection (MidiOnly,GamepadOnly, orBoth)
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 unifiedInputEventstreamcommand_tx- Channel sender for daemon commands
Returns:
Ok(String)- Status message describing connected devicesErr(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 Event | InputEvent Variant | ID Range | Notes |
|---|---|---|---|
| Button Press | PadPressed | 128-255 | Velocity = 100 (default) |
| Button Release | PadReleased | 128-255 | N/A |
| Analog Stick | EncoderTurned | 128-131 | -1.0..1.0 → 0..127 (64 = center) |
| Analog Trigger | EncoderTurned | 132-133 | 0.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
| ID | Standard Name | Xbox | PlayStation | Nintendo Switch |
|---|---|---|---|---|
| 128 | SOUTH | A | Cross (×) | B |
| 129 | EAST | B | Circle (○) | A |
| 130 | WEST | X | Square (□) | Y |
| 131 | NORTH | Y | Triangle (△) | X |
| 132 | DPAD_UP | D-Pad Up | D-Pad Up | D-Pad Up |
| 133 | DPAD_DOWN | D-Pad Down | D-Pad Down | D-Pad Down |
| 134 | DPAD_LEFT | D-Pad Left | D-Pad Left | D-Pad Left |
| 135 | DPAD_RIGHT | D-Pad Right | D-Pad Right | D-Pad Right |
| 136 | LEFT_SHOULDER | LB | L1 | L |
| 137 | RIGHT_SHOULDER | RB | R1 | R |
| 138 | LEFT_THUMB | Left Stick | L3 | Left Stick |
| 139 | RIGHT_THUMB | Right Stick | R3 | Right Stick |
| 140 | START | Start | Options | + (Plus) |
| 141 | SELECT | Back | Share | - (Minus) |
| 142 | GUIDE | Xbox Button | PS Button | Home Button |
| 143 | LEFT_TRIGGER | LT (digital) | L2 (digital) | ZL (digital) |
| 144 | RIGHT_TRIGGER | RT (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
| ID | Axis Name | Range | Normalized | Notes |
|---|---|---|---|---|
| 128 | LEFT_STICK_X | -1.0 to 1.0 | 0 to 127 | 64 = center, 0 = left, 127 = right |
| 129 | LEFT_STICK_Y | -1.0 to 1.0 | 0 to 127 | 64 = center, 0 = up, 127 = down |
| 130 | RIGHT_STICK_X | -1.0 to 1.0 | 0 to 127 | 64 = center, 0 = left, 127 = right |
| 131 | RIGHT_STICK_Y | -1.0 to 1.0 | 0 to 127 | 64 = center, 0 = up, 127 = down |
| 132 | LEFT_TRIGGER | 0.0 to 1.0 | 0 to 127 | Analog pressure (L2/LT) |
| 133 | RIGHT_TRIGGER | 0.0 to 1.0 | 0 to 127 | Analog 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
| Error | Cause | Solution |
|---|---|---|
| “Failed to initialize gilrs” | SDL2 not available or system error | Install SDL2, check system permissions |
| “No gamepads connected” | No physical gamepad detected | Connect a gamepad, check USB connection |
| “Already connected to a gamepad” | Attempted to connect twice | Call disconnect() before reconnecting |
| “No input devices could be connected” | Both MIDI and gamepad failed | Check 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
| Metric | Value | Notes |
|---|---|---|
| Polling Interval | 1ms | Balances latency and CPU usage |
| Reconnect Attempts | 6 | Exponential backoff schedule |
| Max Reconnect Time | ~60s | Sum of backoff delays |
| Event Channel Capacity | 1024 | Default mpsc buffer size |
| Event Latency | <5ms | gilrs → InputEvent → channel |
| CPU Usage (Idle) | <1% | Efficient polling loop |
| Memory Overhead | ~100KB | Per GamepadDeviceManager |
Device Compatibility
Tested Controllers
| Controller | Status | Notes |
|---|---|---|
| Xbox One/Series Controllers | ✅ Fully Supported | SDL2 GameController mapping |
| PlayStation 4/5 DualShock/DualSense | ✅ Fully Supported | Standard button layout |
| Nintendo Switch Pro Controller | ✅ Fully Supported | Button labels differ (A/B swapped) |
| Generic USB Gamepads | ✅ Supported | May require custom SDL2 mapping |
| Logitech F310/F710 | ✅ Supported | Switch to XInput mode |
| 8BitDo Controllers | ✅ Supported | Use 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
| Platform | Status | Requirements |
|---|---|---|
| macOS | ✅ Supported | Native HID support |
| Linux | ✅ Supported | SDL2 + udev rules |
| Windows | ✅ Supported | SDL2 + 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)
- Pressure-Sensitive Buttons: Variable velocity based on analog button pressure
- Gyroscope/Accelerometer Support: Motion controls for advanced controllers
- Haptic Feedback: Rumble/vibration control via actions
- Custom Button Mappings: Override default button-to-ID mappings
- Multi-Controller Support: Connect multiple gamepads simultaneously
- Per-Controller Profiles: Different mappings for different gamepad models
- Axis Inversion/Scaling: Fine-tune analog stick sensitivity
- 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
- Gamepad Support Guide - User-facing documentation
- Configuration Schema - TOML configuration reference
- Trigger Types - Available trigger configurations
- Action Types - Available action types
- Architecture Overview - System design
Glossary
| Term | Definition |
|---|---|
| HID | Human Interface Device - USB standard for input devices |
| gilrs | Rust library for game controller input (built on SDL2) |
| SDL2 | Simple DirectMedia Layer - cross-platform game controller API |
| InputEvent | Protocol-agnostic event abstraction |
| GamepadId | gilrs identifier for a specific connected gamepad |
| Arc/Mutex | Rust concurrency primitives for shared state |
| mpsc | Multi-Producer, Single-Consumer channel for thread communication |
Last Updated: 2025-11-21 API Version: v3.0 Crate Versions:
conductor-core: 3.0.0conductor-daemon: 3.0.0gilrs: 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
- conductor-core: Pure Rust engine library (zero UI dependencies)
- conductor-daemon: CLI daemon + 6 diagnostic tools
- 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
midirinput callbacks - Event Loop: Coordinates event processing thread
- Mode Management: Tracks current mode via
AtomicU8 - Device Profiles: Loads
.ncmm3profiles 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
ProcessedEvent→Action - Mode System: Global mappings + per-mode mappings
- Trigger Matching: Supports Note, CC, Chord, Velocity, LongPress, DoubleTap
- Compilation: Converts
Triggerconfig →CompiledTriggerfor fast matching
actions.rs
- Action Executor: Executes compiled actions via
enigo - Keystroke Simulation: Key sequences with modifiers
- Application Launch: Platform-specific (macOS
open, Linuxexec, Windowsstart) - Shell Execution: Runs arbitrary shell commands
- Sequences: Chains multiple actions with delays
feedback.rs
- Trait Abstraction:
PadFeedbacktrait 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
.ncmm3XML 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:
AtomicU8for current mode (lock-free reads/writes)AtomicBoolfor 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/Oenigo: Keyboard/mouse simulationhidapi: HID device access (withmacos-shared-device)serde/toml: Config parsingquick-xml: XML profile parsingcrossbeam-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,chronofor 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
notifycrate - 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
| Section | v0.1.0 | v0.2.0 | v1.0.0 | Stability |
|---|---|---|---|---|
[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:
- Deprecation notice (version N)
- Deprecation period with warnings (version N+1)
- 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:
- API Design Document - Complete API specification (AMI-123)
- Configuration Compatibility - Backward compatibility strategy (AMI-125)
- Implementation Viewpoint 1 - Monorepo with Tauri
- Implementation Viewpoint 2 - Alternative approach
Related Documentation
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
InputEventchannel 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
| Mode | MIDI Manager | Gamepad Manager | Use Case |
|---|---|---|---|
MidiOnly | ✅ Active | ❌ Disabled | Traditional MIDI controller workflows |
GamepadOnly | ❌ Disabled | ✅ Active | Game controller macro setups |
Both | ✅ Active | ✅ Active | Hybrid 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?
- Conflict Prevention: MIDI note 60 and gamepad button never collide
- Unified Processing: EventProcessor handles both identically
- Simple Disambiguation: Check ID range to determine source protocol
- 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
MidiEventinstances (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
| Metric | MIDI | Gamepad | Hybrid |
|---|---|---|---|
| Event Latency | <1ms | <2ms | <2ms |
| Polling Rate | Callback-driven | 1ms (1000Hz) | Both |
| CPU Usage (idle) | <0.1% | <0.5% | <0.6% |
| Memory Overhead | ~200KB | ~1MB | ~1.2MB |
| Thread Count | 1 (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:
- Multiple Gamepad Support: Connect multiple gamepads simultaneously
- Device Prioritization: Configurable priority when events collide
- Custom ID Ranges: Allow users to remap ID ranges via config
- Hot-Swapping: Dynamic device addition/removal without restart
- Input Filtering: Filter specific buttons/axes before EventProcessor
- Virtual Devices: Create virtual MIDI/gamepad devices for testing
Related Documentation
- Gamepad Support Guide - User-facing gamepad setup
- Architecture Overview - Overall system architecture
- Event Processing Pipeline - Event flow details
- Device Templates Guide - Pre-configured templates
- Configuration Reference - Config file syntax
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:
- Unifies MIDI and HID inputs into a single event stream
- Separates ID ranges to prevent conflicts (0-127 vs 128-255)
- Abstracts protocol differences behind
InputEvent - Enables hybrid workflows with both MIDI and gamepad devices
- 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
| Capability | Risk Level | Description |
|---|---|---|
Network | Low | HTTP requests, websockets |
Audio | Low | Audio device access |
Midi | Low | MIDI device access |
Filesystem | Medium | File read/write |
Subprocess | High | Execute shell commands |
SystemControl | High | System-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
- Discovery: Conductor scans
~/.conductor/plugins/forplugin.tomlfiles - Load: Binary is loaded via
libloading, plugin instance created - Initialize:
initialize()method called (if implemented) - Execute:
execute()called for each MIDI event - Shutdown:
shutdown()method called before unload (if implemented) - 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:
- Discover: Scan for new plugins
- Load/Unload: Control plugin lifecycle
- Enable/Disable: Toggle plugin availability
- Grant/Revoke: Manage capabilities
- 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
- Error Handling: Always return proper errors, never panic
- Performance: Keep
execute()fast (<10ms ideal) - Resource Cleanup: Implement
shutdown()for cleanup - Documentation: Document all parameters in README
- Testing: Write tests for all functionality
- Security: Request minimum necessary capabilities
- 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.tomlis 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_pluginsymbol is exported
Capability Denied
- Check risk level in GUI Plugin Manager
- Grant capability manually if needed
- Consider using lower-risk alternatives
Further Reading
- PLUGIN_DEVELOPMENT_GUIDE.md - Comprehensive guide
- HTTP Plugin Example - Reference implementation
- Plugin API Reference - Complete API documentation
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
| Feature | Native Plugins (v2.3) | WASM Plugins (v2.5+) |
|---|---|---|
| Platform | Platform-specific (.dylib/.so/.dll) | Universal (.wasm) |
| Security | Full system access | Sandboxed |
| Memory Safety | Depends on language | Guaranteed |
| Resource Limits | None | CPU, memory, I/O |
| Installation | Manual copy | Single file |
| Verification | SHA256 checksum | Cryptographic signatures |
| Languages | Rust, C, C++ | Rust, C, C++, Go, Swift, Zig |
| Startup Time | Fast (~1ms) | Fast (~10ms) |
| Runtime Overhead | None | Minimal (~5%) |
Plugin Lifecycle
- Load - WASM module loaded and validated
- Verify (v2.7) - Cryptographic signature checked
- Initialize - Plugin setup, capabilities granted
- Execute - Plugin called for MIDI events
- Shutdown - Cleanup and resource release
- 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:
- Unsigned - Development only (optional signatures)
- Self-Signed - Valid signature from any key
- Trusted Keys - Signature must match trusted key list
Capability System
Plugins request capabilities to access system resources:
| Capability | Risk | Description |
|---|---|---|
Network | 🟢 Low | HTTP requests, WebSocket |
Filesystem | 🟡 Medium | Read/write files (sandboxed) |
Subprocess | 🔴 High | Execute shell commands |
SystemControl | 🔴 High | System-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
Recommended Structure
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-optfor 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
- WASM Plugin Development Guide - Complete development tutorial
- Plugin Security - Signing and verification
- Plugin Examples - Real-world examples
- Plugin API Reference - API documentation
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
-
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); } -
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"]; -
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
- Plugin Security - Signing and verification
- Plugin Examples - Real-world examples
- WASM Plugins Overview - Architecture and concepts
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
-
Protect Private Keys
# Set secure permissions chmod 600 ~/.conductor/my-plugin-key.private # Never commit to version control echo "*.private" >> .gitignore -
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" -
Publish Public Key
- Include in README
- Post on official website
- Add to plugin registry
-
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
-
Verify Before Trust
# Always verify signature first conductor-sign verify downloaded_plugin.wasm # Check developer information # Only trust if matches expected developer -
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" -
Audit Trusted Keys
# Regularly review trusted keys conductor-sign trust list # Remove unused keys conductor-sign trust remove <key> -
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
- WASI Preopening: Conductor pre-opens the plugin data directory
- Path Mapping: All plugin paths mapped to sandbox root
- Validation: WASI runtime blocks access outside preopened directories
- 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
| Capability | Risk | Auto-Grant | Requires Approval |
|---|---|---|---|
| Network | 🟢 Low | Yes | No |
| Audio | 🟢 Low | Yes | No |
| Midi | 🟢 Low | Yes | No |
| Filesystem | 🟡 Medium | No | Yes |
| Subprocess | 🔴 High | No | Yes + Warning |
| SystemControl | 🔴 High | No | Yes + 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
- Ed25519 Specification (RFC 8032)
- WASI Security Model
- WebAssembly Security
- Cryptographic Signing Best Practices
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
-
Get Spotify API Credentials
- Visit Spotify Developer Dashboard
- Create an app
- Note Client ID and Client Secret
- Add redirect URI:
http://localhost:8888/callback
-
Authenticate
# First-time setup (opens browser for OAuth) spotify-auth --client-id YOUR_ID --client-secret YOUR_SECRET -
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
| Action | Parameters | Description |
|---|---|---|
play | None | Resume playback |
pause | None | Pause playback |
play_pause | None | Toggle play/pause |
next | None | Skip to next track |
previous | None | Previous track |
volume | level: 0-127 | Set volume (maps to 0-100%) |
shuffle | None | Toggle shuffle |
repeat | mode: "off"|"track"|"context" | Set repeat mode |
get_state | None | Get 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
-
Enable OBS WebSocket
- OBS Studio → Tools → WebSocket Server Settings
- Enable WebSocket server
- Set password (optional but recommended)
- Note port (default: 4455)
-
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
| Action | Parameters | Description |
|---|---|---|
set_scene | scene_name | Switch to scene |
get_current_scene | None | Get active scene |
start_streaming | None | Start streaming |
stop_streaming | None | Stop streaming |
toggle_streaming | None | Toggle streaming |
start_recording | None | Start recording |
stop_recording | None | Stop recording |
toggle_recording | None | Toggle recording |
toggle_mute | source_name | Mute/unmute source |
set_volume | source_name, volume: 0-1 | Set source volume |
toggle_filter | source_name, filter_name | Toggle filter |
set_transition | transition_name, duration_ms | Set 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
| Action | Parameters | Description |
|---|---|---|
lock_screen | None | Lock screen (macOS/Linux) |
sleep | None | Put system to sleep |
shutdown | force: bool | Shutdown system |
notify | title, message, sound: bool | Show notification |
brightness | level: 0-127 | Set screen brightness |
launch | app: string | Launch application |
volume | level: 0-127 | Set system volume |
volume_up | None | Increase volume 10% |
volume_down | None | Decrease volume 10% |
mute | None | Toggle system mute |
Platform-Specific Notes
macOS:
lock_screen: Usespmsetcommandbrightness: Requires screen brightness permissionlaunch: Usesopen -a
Linux:
lock_screen: Usesloginctlorxdg-screensaverbrightness: Requires/sys/class/backlightaccesslaunch: Usesxdg-open
Windows:
lock_screen: Usesrundll32.exebrightness: Uses WMIlaunch: Usesstart
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(¶ms) {
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
-
Check logs:
DEBUG=1 conductor --config config.toml 0 2>&1 | grep WASM -
Verify WASM format:
file my_plugin.wasm # Should show: WebAssembly (wasm) binary module -
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
- Running Tests
- End-to-End Test Suite
- Code Coverage
- Test Reporting
- Writing Tests
- Interactive CLI Tool
- Test Scenarios
- Game Controllers (HID) Testing (v3.0+)
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
Using Nextest (Recommended)
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:
- GitHub Actions runs coverage on all PRs
- Codecov receives coverage reports and provides:
- Coverage percentage badge
- Line-by-line coverage visualization
- Coverage diffs on PRs
- Historical coverage trends
- PR Comments show coverage delta (increase/decrease)
- Status Checks fail if coverage drops below threshold
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:
- Analyzes coverage for changed files
- Posts a comment with coverage diff
- Updates status checks
- 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
- Clear events between tests: Always clear the event queue between tests to avoid interference
- Use gestures for complex interactions: Prefer high-level gestures over manual event sequences
- Verify timing: Use
Instant::now()to verify timing-sensitive operations - Test edge cases: Test velocity boundaries (0, 40, 41, 80, 81, 127)
- Test multiple channels: Verify channel masking works correctly
- 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) ← NEWconfig_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:
- Use the MIDI simulator for all MIDI event generation
- Test edge cases (boundary values, timing variations)
- Include negative tests (error conditions, invalid inputs)
- Verify timing with tolerance (±10-35ms for CI stability)
- Document test purpose with clear comments
- 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:
- Unit Tests: Component-level testing of InputManager, GamepadDeviceManager, and event conversion
- Integration Tests: Multi-component testing of hybrid mode, event streams, and device lifecycle
- 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:
- Connect physical gamepad via USB or Bluetooth
- Launch Conductor daemon with gamepad support
- Verify gamepad detected and connected
- 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:
- Press each button on the gamepad
- Verify correct button ID assigned (128-255)
- Check Event Console for button events
- 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:
- Leave analog sticks at center (neutral) position
- Verify no events generated (dead zone active)
- Move stick slightly (within dead zone)
- Verify no events still (dead zone threshold)
- Move stick beyond dead zone
- Verify
EncoderTurnedevents generated - Return stick to center
- 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:
- Leave triggers released (0.0 position)
- Verify no events generated
- Pull trigger slightly (below threshold)
- Verify no events (threshold not met)
- Pull trigger beyond threshold
- Verify
EncoderTurnedevents generated - Release trigger
- 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:
- Create gamepad template file (TOML)
- Place in
~/.conductor/templates/directory - Select template in GUI
- Verify mappings loaded correctly
- Test button mappings from template
- 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:
- Open GUI configuration
- Create new mapping
- Click “MIDI Learn” button
- Press gamepad button
- Verify button ID captured (128-255)
- Assign action to mapping
- 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:
- Connect gamepad and verify active
- Physically disconnect gamepad (unplug USB or disable Bluetooth)
- Verify daemon detects disconnection
- Wait for reconnection attempts (check logs)
- Reconnect gamepad
- Verify daemon reconnects automatically
- 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
inputgroup - 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:
- Open Conductor GUI
- Navigate to “Event Console” tab
- Press gamepad buttons/move sticks
- 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
Related Documentation
- Event Processing Architecture
- MIDI Event Types
- Action System
- Game Controller Support
- Contributing Guide
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 bendtests/action_tests.rs: Application launch and volume controltests/action_orchestration_tests.rs: Action sequences and conditionalsconductor-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:
- Download Native Access
- Sign in (free account)
- Install drivers for your device
- Restart computer after installation
- 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?
- Try a different computer (to rule out hardware failure)
- Test with manufacturer’s software (e.g., NI Controller Editor)
- Check for firmware updates
- 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:
-
Run Conductor:
cargo run --release 2 -
macOS will show a permission dialog
-
Click Open System Settings
-
In Privacy & Security → Input Monitoring:
- Find
conductororTerminal(if running via cargo) - Toggle switch to ON
- If switch is already ON, toggle OFF then ON again
- Find
-
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:
- Open Native Access
- Verify “Maschine” or controller-specific software is installed
- Check for updates
- Reinstall if necessary
- 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
<?xmldeclaration - 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:
- Open Native Instruments Controller Editor
- Select Maschine Mikro MK3
- Create a simple profile:
- Page A: Notes 12-27 (chromatic)
- Page B: Notes 36-51 (drums)
- Save as
test-profile.ncmm3 - 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 statusdoesn’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:
-
Run Conductor:
conductor --foreground -
macOS shows permission dialog for Input Monitoring
-
Click Open System Settings
-
In Privacy & Security > Input Monitoring:
- Find
conductororTerminal(if running via cargo) - Toggle switch to ON
- If already ON, toggle OFF then ON to reset
- Find
-
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):
- Open Conductor GUI
- Navigate to mappings
- Click “Learn” button next to trigger field
- Press button on gamepad
- 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:
- Controller disconnects (USB unplugged, Bluetooth drops, battery dies)
- Daemon logs:
[WARN] Gamepad disconnected (ID: 0) - Daemon continues running, monitoring for reconnection
- Controller reconnects
- Daemon logs:
[INFO] Gamepad reconnected (ID: 0) - 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:
-
Check Event Console:
conductorctl events --follow --type gamepadVerify button presses appear
-
Enable Debug Logging:
DEBUG=1 conductor --foregroundLook for SDL2 detection messages
-
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
-
File GitHub Issue:
- Include all collected information above
- Attach relevant portions of debug log
- Describe expected vs actual behavior
- See Support Resources
Related Documentation:
- Gamepad Support Guide - Configuration reference
- Event Console Guide - Real-time debugging
- MIDI Learn Guide - Auto-detect buttons
- Device Templates - Pre-configured gamepad setups
Platform-Specific Issues
macOS: Permission Dialogs Keep Appearing
Cause: Binary changes (recompiling) invalidates permissions
Solution: Grant permission to Terminal.app instead:
- System Settings → Privacy & Security → Input Monitoring
- Add
Terminal(or your terminal emulator) - 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:
- 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
- Add user to plugdev:
sudo usermod -a -G plugdev $USER
-
Log out and back in
-
Test:
cargo run --release 2
Windows: MIDI Device Not Recognized
Cause: Driver not installed or generic USB driver used
Solution:
- Open Device Manager
- Look for “MIDI Device” or your controller under “Sound, video and game controllers”
- Right-click → Update Driver
- Choose manufacturer driver (not generic USB)
- 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:
-
Disable debug logging:
# Don't use DEBUG=1 in production cargo run --release 2 -
Use simpler LED scheme:
# Instead of animated schemes cargo run --release 2 --led reactive # or off -
Optimize shell actions:
# Avoid long-running commands in mappings # Bad: command = "sleep 10 && echo done" # Good: command = "echo done &" # Background process -
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:
- Use release build (not debug)
- Check system load (Activity Monitor/Task Manager)
- Close unnecessary applications
- 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:
- Check Diagnostics Guide for detailed troubleshooting
- Enable debug logging:
DEBUG=1 cargo run --release 2 - Run diagnostic tools:
cargo run --bin midi_diagnostic 2 cargo run --bin led_diagnostic cargo run --bin test_midi - Collect information:
- macOS/Linux/Windows version
- Conductor version:
cargo --version - Device model
- Error messages (full output)
- Debug log output
- 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 --verboselogs) - 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:
- Virtual MIDI port not created or offline
- Port name mismatch (case-sensitive)
- Conductor started before port was created
- Permissions issue (Linux/Windows)
Solutions:
macOS (IAC Driver)
-
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
-
List available MIDI ports to verify the exact name:
# Use conductor diagnostic tool cargo run --bin test_midi -
Restart Conductor daemon:
conductorctl stop conductor
Windows (loopMIDI)
-
Verify loopMIDI is running:
- Check system tray for loopMIDI icon
- If not running, launch loopMIDI from Start Menu
-
Verify port exists in loopMIDI:
- Open loopMIDI application
- Ensure at least one port is listed (e.g., “Conductor Virtual”)
-
Check exact port name (case-sensitive):
- Note the exact name shown in loopMIDI
- Update
config.tomlto match exactly:[modes.mappings.action.then_action] type = "SendMIDI" port = "Conductor Virtual" # Must match exactly
-
Restart Conductor:
conductorctl stop conductor
Linux (ALSA)
-
Verify virtual MIDI port exists:
aconnect -lLook for “Virtual Raw MIDI” or similar.
-
Load ALSA virtual MIDI module if missing:
sudo modprobe snd-virmidi -
Verify port name and update config:
# List MIDI ports aconnect -lUpdate
config.tomlwith the exact port name. -
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:
-
Enable verbose logging in Conductor:
conductor --verboseLook for lines like:
[DEBUG] Sending MIDI: NoteOn(note=60, velocity=100, channel=0) to port "IAC Driver Bus 1" -
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:
- Open Logic Pro → Settings → MIDI → Inputs (⌘,)
- Locate your virtual MIDI port
- Check the box to enable it
- Close Settings
Ableton Live:
- Open Preferences → Link, Tempo & MIDI
- In MIDI Ports section, find your virtual port
- Enable Track and Remote for the input port
- Close Preferences
Reaper:
- Open Preferences → MIDI Devices
- Find your virtual MIDI port in the input list
- Enable Enable input from this device
- 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:
- Logic Pro → Control Surfaces → Learn Assignment
- Click the parameter to control
- Note the CC number shown (update your config)
Ableton Live:
- Click MIDI button (top-right, or press ⌘M)
- Click the parameter to control
- Press your MIDI controller (Ableton learns the mapping)
- 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:
- Logic Pro → Settings → Audio
- Reduce I/O Buffer Size to 128 or 64 samples
- Note: Lower buffer = lower latency, but higher CPU usage
Ableton Live:
- Preferences → Audio
- Reduce Buffer Size to 128 or 64 samples
Reaper:
- Preferences → Audio → Device
- 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:
- Close unnecessary applications
- Freeze/bounce tracks in DAW to reduce CPU usage
- 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:
- Complex velocity curves (use simpler curves)
- Conditional actions with many conditions (simplify)
- 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:
- Enable IAC Driver in Audio MIDI Setup
- Restart the DAW
- Alternatively: Logic Pro → Settings → MIDI → Reset MIDI Drivers
Windows (loopMIDI)
Cause: DAW doesn’t detect dynamically created ports.
Solution:
- Create loopMIDI port before launching DAW
- If DAW is running, restart it after creating the port
Linux (ALSA)
Cause: ALSA virtual port created after DAW startup.
Solution:
- Load
snd-virmidimodule before launching DAW:sudo modprobe snd-virmidi - Restart DAW
- Alternatively: Use
aconnectto manually connect ports
Platform-Specific Issues
macOS Specific
IAC Driver “Device is offline” After Reboot
Problem: IAC Driver unchecks itself after macOS reboot.
Solution:
- Open Audio MIDI Setup
- Double-click IAC Driver
- Check “Device is online”
- 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:
- Grant Input Monitoring permissions:
- System Settings → Privacy & Security → Input Monitoring
- Enable for Terminal (if running
cargo run)
- Restart Conductor
Windows Specific
loopMIDI Port Disappears
Problem: loopMIDI port intermittently disappears.
Solution:
- Ensure loopMIDI is set to start with Windows:
- Right-click loopMIDI tray icon → Settings → Start with Windows
- If port disappears, restart loopMIDI application
- Restart Conductor daemon
Multiple MIDI Drivers Conflict
Problem: Multiple virtual MIDI drivers (loopMIDI, rtpMIDI, etc.) cause conflicts.
Solution:
- Use only one virtual MIDI driver at a time
- Disable/uninstall unused drivers
- 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:
- Bridge ALSA to JACK using
a2jmidid:sudo apt-get install a2jmidid a2jmidid -e & - Use
qjackctlto route ALSA virtual port to JACK - 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:
- View → Show MIDI Environment
- Click Monitor object
- Watch for incoming MIDI messages
Ableton Live:
- Preferences → Link, Tempo & MIDI
- Enable Track and Remote for the port
- Create a MIDI track
- Arm the track for recording
- Watch the MIDI input meter
Reaper:
- View → MIDI Device Diagnostics
- Select your virtual MIDI port
- Watch for incoming messages
Test MIDI Loopback
Create a loopback test to verify MIDI output is working:
-
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
-
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:
-
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 -
Check configuration:
cat ~/.config/conductor/config.toml -
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
- DAW Control Guide - General MIDI output concepts
- Logic Pro Integration - Logic Pro-specific setup
- Ableton Live Integration - Ableton Live-specific setup
- Configuration Reference - SendMIDI action syntax
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:
- Lists all MIDI input ports
- Lists all MIDI output ports
- Attempts to open port 2 (default)
- Waits for a MIDI event (5-second timeout)
- 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
audioorplugdevgroup - Check udev rules
- Add user to
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
velis 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:
-
Verify MIDI events are being sent:
cargo run --bin midi_diagnostic 2 # Press pads - you should see NoteOn/NoteOff events -
Find note numbers for config:
cargo run --bin midi_diagnostic 2 # Press pad → note:12 → use 12 in config.toml -
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 -
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) -
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
--profileflag 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:
- Searches for HID device (Maschine Mikro MK3)
- Attempts to open HID device
- Tests LED control by cycling through all pads
- 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:
-
Test individual pad LEDs:
> on 0 white 3 # Bottom-left pad should light up bright white -
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) -
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 -
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:
-
Run pad_mapper:
cargo run --bin pad_mapper 2 -
Draw a grid on paper:
[ ] [ ] [ ] [ ] <- Top row [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] <- Bottom row -
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
-
Result:
[12][13][14][15] <- Top row [ 8][ 9][10][11] [ 4][ 5][ 6][ 7] [ 0][ 1][ 2][ 3] <- Bottom rowWait, 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.
-
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:
- Open Device Manager (Win+X → Device Manager)
- Expand “Sound, video and game controllers”
- Look for your MIDI device
- 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:
system_profiler SPUSBDataType | grep -i usbcargo run --bin test_midi- Check USB connection
- Verify drivers installed
“Failed to open HID device”
Meaning: Device found but can’t access HID interface
Debug steps:
- Check permissions (Input Monitoring on macOS)
cargo run --bin led_diagnostic- Close other applications using device
- Reinstall NI drivers
“TOML parse error at line X”
Meaning: Syntax error in config.toml
Debug steps:
- Open config.toml in editor
- Go to line X
- Check for:
- Missing quotes
- Wrong brackets
- Typos in field names
- Validate at https://www.toml-lint.com/
“Missing field ‘type’ in trigger”
Meaning: Invalid config structure
Debug steps:
- Find the mapping missing
type - Add
typefield:[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
- Common Issues - Quick solutions to frequent problems
- CLI Commands - Complete command reference
- Configuration Overview - Config syntax validation
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:
- GitHub Discussions: https://github.com/amiable-dev/conductor/discussions
- Issues: https://github.com/amiable-dev/conductor/issues
- Discord: Coming soon!
See GOVERNANCE.md for community structure.
Support
See the main SUPPORT.md for getting help.