Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bitwig OSC Bridge

A Bitwig Studio controller extension that streams the project's remote controls to OSC over UDP — built for Chromatik / LX (default target 127.0.0.1:3030), but any OSC receiver works.

The core idea: anything in Bitwig that can reach a device's remote-controls page — automation lanes, modulators, envelope/audio followers, Grid values/signals added to remotes — can be bound to an OSC address and streamed. Bound knobs send their modulatedValue() (modulation included, pushed by the host at control rate), so a Grid LFO wobbling a remote knob reaches Chromatik, not just the knob's set position.

How it works

  • The extension observes a fixed bank tree: 32 tracks × 8 devices × 8 remote knobs per device (first/selected remote page), plus each track's own 8-knob remote-controls page (the track's root remotes, not scoped to a device).

  • Inventory: the "Print remotes" button (controller preferences) dumps every existing remote to the controller console with its coordinate: t3.d1.k2 <track> <device> <knob name>.

  • Binding: 16 per-project mapping slots in the Studio I/O panel (saved in the Bitwig project). Slot syntax:

    t3.k1    /lx/output/brightness
    t3.d1.k2 /lx/modulation/Globals/macro2
    

    (1-based track[.device].knob coordinate, then a full OSC address — t.k binds a track-level remote, t.d.k binds a device-level remote. Values are sent as normalized 0–1 floats — what LX's OSC handler expects for ranged parameters.)

    Mappings can alternatively be written to ~/.bitwig-osc-bridge/mappings.txt (one slot line per line, # comments allowed) — the file fully replaces all 16 slots, applied within ~1s, and is persisted into the project via the same Studio I/O slots, so external tooling (scripts, AI sessions) can create bindings without opening the Studio I/O panel.

  • Stats: the "Print OSC stats" button logs per-binding update counts and rate (updates/sec) since the last dump — use it to measure the host's observer push rate against a fast modulator before relying on it for tight sync.

Build

Requires JDK 21+ and Maven (the Bitwig extension API is fetched from maven.bitwig.com).

mvn package             # produces target/OscBridge.bwextension
mvn package -Pinstall   # also copies it to ~/Documents/Bitwig Studio/Extensions/ (macOS)

mvn package also builds the web UI (see below) via exec-maven-plugin, so no manual npm step is needed — just node/npm on the PATH Maven runs with.

Web UI development

The embedded HTTP server's web page (remotes ↔ Chromatik params binding UI) lives in web-ui/ as a Vite + React + TypeScript app, not hand-written HTML.

The page is two columns: a single Bitwig panel on the left, and Mapping Slots + Chromatik Parameters stacked on the right. Clicking a knob on the left arms it, then clicking a param on the right binds the two — the Chromatik panel's header always shows what's currently armed ("Binding → p3.k2 · <device> / <knob>", with a ✕ to clear it) so the click target is never a guess.

The Bitwig panel folds what used to be two side-by-side panels into one, with chips to switch mode:

  • Probes (the default) — 8 pinnable slots that follow whatever's selected in Bitwig until captured; the intended way to reach devices nested too deep for the static track/device banks (see below). Each probe card is labeled with its coord prefix (p1p8) rather than a bare number, since that prefix is literally the p1 in a p1.k1 mapping; copy under the capture button explains that probes fill the lowest free slot and that the number is stable and referenced by your mappings. If the running extension build predates probes support, the panel opens on Browse automatically instead of dead-ending on an unsupported-feature message.
  • Browse all remotes — the full static track/device/knob bank tree (the old "Bitwig Remotes" panel), for binding without capturing a probe first.

The Chromatik params panel filters ~35,000 raw OSC params down to what's worth binding to a remote knob. A mode toggle at the top of the panel switches between two views:

  • Macro controls (the default) — only the macro slots of Chromatik's macro modulators (the "Knobs", "Triggers", and "Switches" modulators added in Chromatik's modulation section), organized as a channel tree instead of a flat list: "Global" (engine-level modulators with no owning channel) is pinned first, then channels alphabetically, each an expandable node (expanded by default) that shows the channel name once alongside its total macro count. A channel's modulator groups nest inside it, with any intermediate pattern/effect names on the way to the modulator dimmed in the group heading (e.g. "Rack › Colorization" dimmed under the "Thunderstorm" channel node, with the modulator's own label at full weight after it) — previously every group repeated the channel name in its own heading. The raw /lx/mixer/channel/4/... path is still there, just moved to a hover tooltip. The header shows "{n} macros · {m} modulators · {c} channels"; on a real rig this narrows ~35,541 total params down to ~304 macro params across 38 modulators. If a macro knob has been given a custom name in Chromatik — a sibling StringParameter's value field, e.g. .../macros/label3 next to .../macros/macro3 — its slot renders as "K1 (YourName)" with the default label and OSC address moved to a hover tooltip; this needs a chromatik-mcp build that supplies StringParameter.value (not yet released) plus a Chromatik restart to pick it up, otherwise slots just show plain defaults (K1..K8/T1..T8) as before.
  • All parameters — the full component tree. A category chip row above the search box — Knobs, Triggers, Enums & selectors, Other — toggles which LX parameter types are included (Knobs and Triggers on by default; a "{shown} of {total} shown" count sits alongside), and the search box then narrows further within whatever categories are active. Nothing is permanently hidden — toggling a chip back on brings its params back into the tree.

The search box works in both modes. Macro detection prefers a componentType field from chromatik-mcp when present, falling back to matching OSC addresses ending in /macroN otherwise — both signals identify the same params, but componentType requires a chromatik-mcp build that emits it plus a Chromatik restart to take effect.

Mapping Slots also got more readable: each filled slot shows a small resolved line — source device/knob → param address tail — above the raw, still directly editable coord /address text. After a bind, the slot row you just wrote to flashes so you can find it in the list of 16.

cd web-ui
npm install
npm run dev   # http://localhost:5173, proxies /api to the running extension on :3131

Run the extension in Bitwig first so /api/* has something to talk to — npm run dev gives hot reload against the live extension, no rebuild/reinstall loop needed while iterating on the UI.

mvn package runs npm ci && npm run build in web-ui/ during the generate-resources phase and writes the build output straight into src/main/resources/web/ (gitignored — it's generated, not source), which then gets packaged into the .bwextension jar as before. There's nothing to run by hand for a normal build.

Setup

Bitwig: Settings → Controllers → Add Controller → vendor lx-mcp, model OSC Bridge (no auto-detect — add it manually). Preferences hold the OSC target host/port, a Reconnect button, and the Print remotes / Print OSC stats buttons. The Web UI category holds the HTTP port, an "Enable Chromatik integration" toggle, and an Open button ("Open in browser") that opens the embedded web UI in the default browser — the reliable way to reach it now, since the URL is otherwise only shown in a transient popup notification at startup. Note the actual port can differ from the "HTTP Port" preference (the server falls back to an OS-assigned port if the preferred one is taken); the Open button always opens whichever port is actually in use, also recorded in ~/.bitwig-osc-bridge/status.json. Mapping slots live in the Studio I/O panel (window footer, opposite-arrows icon), under the OSC Bridge controller entry.

Chromatik: enable OSC input (RX) on port 3030. Find the target parameter's canonical path (e.g. a MacroKnobs modulator labeled "Globals" is /lx/modulation/Globals/macro1..8) and put it in a mapping slot.

Design notes / roadmap

  • Bank sizes are fixed at init; projects larger than 32×8 devices are silently outside the observed window (the inventory footer says so). Bump the constants if needed.
  • Coordinate-based slots are the deliberately-minimal binding UI. The planned next step is an embedded HTTP server in the extension (mirroring chromatik-mcp's in-process MCP pattern, with ~/.bitwig-mcp/status.json discovery): a local web page that lists remotes on one side and Chromatik's parameters on the other for click-to-bind, and MCP tools so an AI session spanning both servers can create bindings conversationally.
  • Prior art in this repo's history: a tempo/beat-sync stream to /lx/tempo (dropped as out of scope) and a selected-device knob page (superseded by whole-project enumeration).

Agent API (MCP)

mcp/ is a small stdio MCP server that wraps the extension's embedded REST API (/api/*, served alongside the web UI on 127.0.0.1:3131 by default) as tools, so an AI agent can inspect and edit OSC mappings directly instead of going through the Studio I/O panel or the web UI.

It discovers the running extension itself — no port to configure — by reading ~/.bitwig-osc-bridge/status.json (written by the extension while its controller is enabled) fresh on every call.

Build

cd mcp
npm install
npm run build   # emits mcp/dist/index.js

Register with an MCP client

A repo-root .mcp.json is already checked in:

{
  "mcpServers": {
    "bitwig-osc-bridge": {
      "command": "node",
      "args": ["mcp/dist/index.js"]
    }
  }
}

Point any stdio-capable MCP client (Claude Code, Claude Desktop, etc.) at this repo and it will pick that up automatically; for other clients, copy the bitwig-osc-bridge entry into their own MCP config, adjusting args to an absolute path to mcp/dist/index.js if needed.

Tools

  • get_state — full snapshot: remotes, probes, mapping slots, OSC target. Start here.
  • set_slot — write one of the 16 mapping slots ({ slot: 1-16, text }).
  • set_all_slots — replace all 16 slots at once ({ slots: string[16] }).
  • bind — bind a remote coord to an OSC address, reusing an existing slot for that coord or taking the first empty one ({ coord, oscAddress }).
  • capture_probe — pin a probe (1-8) to whatever's currently selected in Bitwig, to reach devices nested too deep for the static track/device banks.
  • release_probe — unpin a probe so it follows Bitwig's selection again.
  • set_probe_page — switch which remote-controls page a probe exposes ({ probe: 1-8, pageIndex: 0-based }).
  • set_osc_target — change the OSC send host/port ({ host, port }); the controller must be re-enabled in Bitwig afterward to apply it.
  • list_chromatik_params — list OSC-addressable parameters from a running Chromatik/LX instance, if the Chromatik integration is enabled.

Example flow

  1. get_state to see existing remotes and which coords are already bound.
  2. bind({ coord: "t3.d1.k2", oscAddress: "/lx/modulation/Globals/macro2" }) to wire a device remote knob to an OSC address (auto-picks a free slot).
  3. For a knob buried in a nested Grid chain not covered by the static banks, select the target device in Bitwig, then capture_probe({ probe: 1 }) followed by bind({ coord: "p1.k1", oscAddress: "..." }).

About

Bitwig controller extension that streams remote controls (incl. Grid values) to OSC — built for Chromatik/LX

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages