MIDI & Controllers
Lightbridge talks to physical MIDI controllers through a layered architecture where every controller — built-in or custom — is represented in the app as a profile controller profile midi A complete description of a controller panel as it should appear in the UI — panel aspect, element positions, MIDI mappings, LED feedback, keyboard bindings. Everything needed to recreate the same panel on any machine. : a free-placement layout of elements (knobs, pads, faders, group labels) with optional MIDI mappings, LED feedback, and keyboard bindings. Once a profile exists, the panel renders, parameters get drag-bound drag-bind midi Creating a MIDI binding by dragging from a parameter slider onto a controller panel element. No menus, no CC lookup — just a drag and a drop. to elements, and incoming MIDI events flow through to those parameters automatically.
For the hands-on workflow, see the MIDI Controllers guide.
What a profile contains
A profile is a complete description of a controller panel as it should appear in the UI:
- A name and an optional chassis (the panel’s aspect ratio and faithful-device skin)
- A list of elements placed freely on the panel — each one a knob, pad, fader, or group label
- Optional device name so the app can auto-attach the profile to a known piece of hardware on connect
- Optional SysEx init message for hardware that needs an initialisation handshake (the APC40 needs one to enter Mode 2, for example)
Each element is one physical control. It has a normalized rectangle — x, y, w, h as fractions (0–1) of the panel — so a layout scales to any panel size while keeping its proportions (faders are typically tall, group labels are typically wide). It also has an optional label and colour, an optional MIDI assignment (a CC for knobs and faders, a note for buttons and pads), an optional keyboard shortcut, and an optional LED feedback mode that tells the hardware how to light up when the parameter changes.
Advanced — ControllerProfile and ControllerElement types
interface ControllerProfile { id: string name: string elements: ControllerElement[] chassis?: { aspect?: number; skinKitId?: string } // Panel aspect + faithful skin midiDeviceName?: string // For auto-attaching to known devices builtIn?: boolean // True for the built-in faithful profiles sysExInit?: number[] // Hardware init message (e.g. APC40 Mode 2)}
interface ControllerElement { id: string // Stable across edits type: 'knob' | 'pad' | 'fader' | 'group' label: string // Normalized free-placement rectangle — fractions (0–1) of the panel. x: number y: number // Top-left position w: number h: number // Size (faders are tall, groups are wide) midi?: MidiMapping // CC, note, PC, or 14-bit CC this element listens to keyBinding?: string // Optional keyboard shortcut color?: string // Hex tint ledFeedback?: LedFeedback // How the hardware should reflect state}Four MIDI mapping kinds are supported:
MidiMappingCC—{ type: 'cc', channel: 0–15, cc: 0–127 }for continuous controls (absolute 7-bit). An optionalrelative: 'twosComplement' | 'signedBit' | 'binaryOffset'marks an endless encoder: incoming values are decoded as signed deltas and accumulated onto the bound parameter’s current value, rather than read as an absolute position.MidiMappingNote—{ type: 'note', channel: 0–15, note: 0–127 }for buttons and pads.MidiMappingPC—{ type: 'pc', channel: 0–15 }— a Program Change, routed as a single momentary trigger (for pads/foot-switches that emit PC instead of notes).MidiMappingCC14—{ type: 'cc14', channel: 0–15, msbCC: 0–31 }— a high-resolution 14-bit CC. The fine LSB is paired automatically onmsbCC + 32(the MIDI standard), giving 16384 steps for smooth slow sweeps. A small input assembler coalesces the MSB/LSB pair into one logical value before routing.
The discriminated union is still open-ended — NRPN is the next planned kind.
Two LED feedback kinds are supported:
LedNoteColor— pads send a Note-On with velocity mapped from a colour palette (apc40palette currently).LedCCRing— knobs send two CCs, one for the ring display mode and one for the position.
Built-in vs custom profiles
Lightbridge ships three built-in profiles, all for the Akai APC40 MkII:
- APC40 Knobs — 8 device knobs + 8 track knobs (2×8 grid), with ring LED feedback.
- APC40 Pads — the 5×8 pad grid, with note-color LED feedback.
- APC40 Faders — 8 channel faders + master + crossfade (10×3 grid).
These three profiles are created automatically when the app starts, and their CC numbers and note assignments match the APC40 MkII’s hardware protocol exactly. Plug the controller in and every knob and pad lines up with an element on the panel, no setup required.
Every other controller is custom. To use one, you build a profile via the Controller Manager (open from Controller → Controller Manager in the menu bar, or ⌘K). It’s a three-pane editor: a rail of the controllers in your show, a canvas where you place elements freely to match your hardware’s layout, and a property inspector (label, color, MIDI mapping, key binding). Add controls from the toolbar or drop in building blocks; an Add mode browses the device catalog and your saved presets.
The editor includes MIDI Learn (select an element, click MIDI Learn, wiggle a physical control), manual entry (CC, note, Program Change, or 14-bit CC), LED feedback configuration, and a device name binding so the profile auto-activates when that hardware is detected. (A 14-bit control is learned as its plain MSB CC, then switched to 14-bit in manual entry.) Selecting a pad also reveals a Binding section — create or attach an action button (opening the Button Editor for full target setup), or assign the pad as the global Beat Sync or Resync trigger — so a pad is fully configurable in the editor, not only by dragging onto the live panel.
How a MIDI event reaches a parameter
The journey from a physical knob turn to a parameter change goes like this:
- The MIDI hardware sends a message over USB (a CC change, a note on/off, a Program Change, or a 14-bit CC’s MSB/LSB pair).
- Lightbridge parses the message into a structured event.
- The raw event callback fires first — used by MIDI Learn in the editor to capture any single message from any controller.
- A small input assembler coalesces a 14-bit CC’s MSB/LSB pair into one logical value (everything else passes straight through). The APC40-specific mapper then tries first (backward compat); if no match, GenericProtocol iterates custom profiles. Profiles with a
midiDeviceNameonly match events from that device; universal profiles (no device name) match everything. - The matched element is looked up in the bindings for the current performance set.
- The value (normalised to
[0, 1]) is pushed into that parameter via the parameter priority chain where MIDI sits at the top tier. - The rendered output reflects the new value on the next frame.
A few things worth noticing:
- Bindings are per-set. The same physical knob can drive Scale on Set A and Hue on Set B without rewiring.
- Profiles are show-level. They live on the show file, not on a set. Switching sets doesn’t change which controllers are connected; it just changes what the controls do.
- Multiple profiles can be active simultaneously. A Launchpad pad panel can sit next to an APC40 knob panel next to a custom MIDImix fader panel. Each routes independently.
Advanced — GenericProtocol and routing internals
The runtime that turns incoming MIDI bytes into something useful is GenericProtocol:
mapEventToProfileControl(event, profile) → { controlId, value } | undefinedThe function iterates the profile’s elements, looks for one whose midi field matches the incoming event (same type, same channel, same CC/note/MSB number; a pc mapping matches any Program Change on its channel), and returns its control ID along with the normalized value (event.value / 127, / 16383 for an assembled 14-bit CC, 1 for a Program Change trigger, or 0 for note-off).
For built-in profiles, the returned ID is the element’s raw ID (device_knob/1) — preserving compatibility with the hardcoded APC40 path. For user profiles, it’s profileId/elementId (cp_abc123/knob_xyz) — a fully scoped identifier that can’t collide with anything else.
This is the only place that knows about the MIDI wire format. Everything downstream — bindings, parameter overlays, LED feedback — works in terms of control IDs and normalized [0, 1] values.
The complete data flow:
USB MIDI input │ │ parseMidiMessage(raw bytes) → MidiEvent { type, channel, number, value } ▼For each loaded profile: mapEventToProfileControl(event, profile) → ControlId, value │ │ (first match wins) ▼Look up controllerBindings[controlId] in the active set │ │ → { target: "nodeId/paramKey", label, rangeMin?, rangeMax? } ▼Apply the value to the target parameter │ ▼RendercontrollerBindings lives on PerformanceSet (per-set), and controllerProfiles lives on ShowFile (show-level). That separation is what gives you the per-set bindings + show-level profiles split.
The drag-bind UX
Creating a binding is a drag operation, not a menu interaction:
- Each parameter slider has a small
≡icon. Drag it. - Every controller panel element accepts the drop.
- On drop, the binding is created — the parameter is now wired to that physical control.
- The element re-renders with a binding label showing what it’s bound to (e.g.
Voronoi → Scale).
For action buttons created from the parameter slider’s right-click menu, the same drag-bind workflow applies. Pad elements that support LED feedback pick up the action button’s colour, so the physical pad lights up to match what’s in the UI.
LED feedback
When a knob’s value changes — because of modulation, incoming MIDI, or a programmatic update — Lightbridge sends a feedback message LED feedback midi Messages sent back to the hardware so physical controls light up to match what's on screen. Knobs glow their ring to show the current value; pads take the colour of the bound action button. back to the hardware so the physical control matches the app’s state. For knobs, this lights up the ring around the knob to show the current value. For pads, this sets the pad’s colour to match the bound action button.
Feedback is opt-in per element. Profiles for hardware that doesn’t support LED feedback simply omit it; the routing still works, the hardware just doesn’t reflect state visually.
Advanced — drag-bind and LED feedback internals
Drag-bind uses HTML5 drag-and-drop with custom MIME types. The slider’s ≡ icon starts a drag with MIME_PARAM data containing the node ID and the param key. Controller panel elements register as drop targets for this MIME type. On drop, the element’s control ID and the drag’s parameter target get written into controllerBindings for the active set.
For action buttons, the same flow uses MIME_CUSTOM_BUTTON instead of MIME_PARAM. Pad elements with LedNoteColor feedback read the action button’s colour and forward it to the hardware.
LED feedback uses a color translator registry with 9 device-specific translators:
apc40— 16-color indexed palette (APC40 MkII, APC Mini MK2)novation— 128-color indexed palette (Launchpad X, Mini MK3, Pro MK3, MK2)push— Ableton Push palette (shares Novation’s structure)mft— MIDI Fighter Twister hue spectrum (0–127 = full rainbow)apc-mini-mk1— 3-color only (green, red, amber)onoff— simple brightness threshold (nanoKONTROL2, MIDImix)xone-k2— tri-color velocity rangeslaunch-control-xl— bi-color packed velocity (red + green = amber)generic— perceptual luminance fallback for unknown hardware
Each translator converts a CSS hex color to the device-native LED value. The runtime calls getTranslator(colorMap) to find the right one, falling back to generic for unknown devices.
Sharing controller profiles
Lightbridge ships with ~35 device-specific presets across 21 controllers. These are full-fidelity templates: grid layout, MIDI assignments, LED feedback, and device name — all pre-configured. Applying a preset to a new profile gives you a working panel in one click.
You can also save your own layouts as presets (the + button in the editor). Presets preserve everything, including MIDI mappings and LED feedback. Share them by exporting the profile as JSON via the editor’s Export button.
Presets are organized by manufacturer in the editor’s left drawer, with a search field for quick access.
Where to go from here
- MIDI Controllers guide — the hands-on walkthrough: connecting, building a panel, drag-binding, action buttons.
- Parameter Priority — MIDI sits at the top of the priority chain. A bound knob always wins.
- Performing Live — how MIDI fits into a gig workflow alongside hold mode and action buttons.