Choosing a way in
A Twist Commando control is set to control something: a "target". Your app can be reached through these:
| Way in | Your app needs | Turns | Shows your state | Names |
|---|---|---|---|---|
| Applug | The Applug package (or its JSON lines) | Levels and steps | Value, its text and on/off, live | Yes: yours, by name |
| MIDI port | MIDI input (MIDI Learn) | Absolute or relative | Value and on/off, live | No: the user names the control |
| Mackie Control | Mackie Control host support | Faders, pots, buttons | Values, lights, meters, live | Yes: from your display |
| Shortcuts | App Intents | 0–100, coalesced | Value and on/off, polled | No |
| Command line | A CLI or URL scheme | 0–100, coalesced | Value and on/off, polled | No |
| Keyboard shortcuts | Menu shortcuts | One key per step | No | No |
Building something new? Use Applug: a few dozen lines, and your controls appear in Twist Commando by name, with your value text on the screens and in the on-screen indicator. If your app already has MIDI Learn, it already works: point your users at the MIDI port, and send your values back on the same numbers so the controller's lights follow.
Applug: your own controls
New in Twist Commando 0.13 (as the App Link); Applug from 0.15.
Your app tells Twist Commando which controls it offers (a volume, a mute, scrolling), and the user picks them by name for any knob, fader or pad: Other Apps in its Controls menu, then your app, then the control. Turns and presses come to you; you send back each control's value, how to show it ("−6 dB") and whether it's on, and the controller follows.
The package
Applug is a small open protocol for sharing a Mac app's controls with any app that asks: Twist Commando, a remote on your phone, whatever comes next. Your app provides its controls: it listens on a port of its own and announces itself, and Twist Commando finds it and connects. The Swift package does all of that.
- Add the package:
.package(url: "https://github.com/idallas/applug", from: "0.2.0")(macOS 14 or later, MIT). - Make an
ApplugProviderwith your controls, handle its turns and presses, start it, and send every change withset. - A sandboxed app needs
com.apple.security.network.server.
import Applug
let provider = ApplugProvider(name: "Mixer", controls: [
Control(id: "volume", name: "Volume", kind: .level, value: mixer.volume, text: mixer.volumeText),
Control(id: "mute", name: "Mute", kind: .toggle, on: mixer.isMuted),
Control(id: "output", name: "Output", kind: .meter, channels: 2),
])
provider.onTurn = { turn in
if turn.id == "volume", let value = turn.value { mixer.volume = value }
}
provider.onPress = { press in
if press.id == "mute", press.down { mixer.isMuted.toggle() }
}
provider.onWatchedMetersChange = { meters in mixer.metering = meters.contains("output") }
provider.start()
// Whenever the state changes (and after every turn or press):
provider.set("volume", value: mixer.volume, text: mixer.volumeText)
provider.set("mute", on: mixer.isMuted)
// About 20 times a second, while someone's watching:
provider.sendMeter("output", levels: [left, right])
The package's README has the rest. To see it work, swift run applug demo in a checkout offers a few made-up controls; "Applug Demo" shows up in Twist Commando.
Other languages
Applug is small enough to speak without the package: announce yourself with a distributed notification, listen on a port, and exchange the JSON lines in SPEC.md (hello, controls, update, meter from you; turn, press, meterSubscribe back). Anything that can post a distributed notification will do: Objective-C, Python with PyObjC, a tiny helper next to anything else.
Up to 0.14, apps connected in to Twist Commando on port 9034 instead (the original App Link). 0.15 and later don't listen there, and the package doesn't dial it: an app that spoke the port should move to Applug, and its users to Twist Commando 0.15.
Settings › Controls › Other Apps lists the apps that have offered controls, and which are running. Apps are remembered with their controls, so users can set up knobs while yours is closed; those knobs say it isn't running until it connects.
The Twist Commando MIDI port
Twist Commando makes a virtual MIDI device named Twist Commando, with a source (what Twist Commando sends) and a destination (what your app sends back). It appears in Core MIDI like any device and keeps the same unique IDs across launches, so your saved mappings stay valid. A control set to MIDI Out sends on it.
What a control sends
- Turns, absolute: Control Change, 0–127, as the value moves. The default.
- Turns, relative: Control Change, one message per step:
1for up,127for down (two's complement). For your app's own endless values, such as a playhead. - Presses: a note (note on velocity 127, then note off), or a Control Change (127, then 0). With Toggle on, each press alternates on and off instead of sending press and release.
The user can choose any channel and number. Unless they do, a control's numbers come from where it is, so a whole controller can be learned without typing anything:
| Channel | Bank 1–4 → channels 1–4; on the Shift layer, 5–8 |
|---|---|
| Turn CC | 20 + the control's position in the bank (20–35 for the Twister's 16 knobs) |
| Press note | 36 + its position (36–51), or as a CC, 52 + its position (52–67) |
What your app sends back
Send to the Twist Commando destination, on the same channel and number:
- A value: the turn's CC with your current value (0–127). The knob's ring or fader light moves to it, and the window and web pages show it. Send it whenever the value changes in your app (from the mouse, automation, undo), and once when your app starts, so the controller agrees with you.
- On or off: the press's note (note on with velocity above 0 for on; note off or velocity 0 for off), or its CC (above 0 for on, 0 for off). The control's switch light follows: a lit mute, a playing deck.
Absolute turns pick up from your value: after you send 64, a turn continues from 64, not from wherever the knob was. That's what makes two-way feel right.
Example
// Your app's "Volume" was learned as CC 20, channel 1.
// Twist Commando sends: B0 14 5A (CC 20 = 90)
// The user moves your slider; send back:
// B0 14 40 (CC 20 = 64: the ring moves)
// "Mute" was learned as note 36; mute in your app, send:
// 90 24 7F (note on: the switch lights)
Mackie Control
For the richest link (track names on the Komplete Kontrol's and SL MkIII's screens, meters on the rings, every button lit), Twist Commando is a Mackie Control Universal surface with up to three extenders, on virtual ports named Twist Commando MCU and Twist Commando MCU XT 1–3. If your app can drive a Mackie Control (as DAWs do), users add it as a surface on those ports and assign controls to its strips.
- Display: SysEx
F0 00 00 66 14 12 <offset> <ASCII…> F7, two lines of 56 characters, 7 per strip: names on top, values below. - Faders: pitch bend on channels 1–8 (9 is the master), 14 bits, both ways. Moves come with touch, note
0x68+ strip. - V-Pots: turns arrive as CC
0x10+ strip (bit 6 set is anticlockwise, the low bits the speed); set the ring with CC0x30+ strip. - Buttons and lights: notes Rec
0x00, Solo0x08, Mute0x10, Select0x18(+ strip), V-Pot press0x20; banks0x2E/0x2F, channels0x30/0x31. Light with note on0x7F(0x01blinks). - Meters: channel pressure
strip << 4 | level(0–12). Send them about every 100 ms while playing; they fall on their own. - Handshake: send the device query (SysEx
00); Twist Commando answers with its serial and challenge (01); reply with02; it confirms with03.
Its controls are under Logic Pro in Twist Commando's menus, where it was built and tested. Any app that drives a Mackie Control the way Logic does should work too, but test yours.
Shortcuts and App Intents
An Apple Shortcut control runs shortcuts: one when pressed, one as it turns, and one now and then to read your state back. Expose what your app does as App Intents and users build those shortcuts in a minute.
Each run gets JSON as its input:
{"event": "turn", "value": 62, "previous": 59, "delta": 3, "on": true, "bank": 1, "knob": 5}
eventispush,turnorstatus;valueandpreviousare 0–100.- Turns are coalesced: one run at a time, then the latest value. Keep turn intents quick.
- Reporting back: return a number 0–100 (moves the ring),
on/offor a boolean (sets the switch light), or a dictionary like{"value": 45, "on": true}for both. A status shortcut does the same every few seconds, so changes made in your app show.
Good intents for this: set a value (with a 0–100 parameter), get a value, toggle, and get on/off.
Command-line tools
A Run Command control runs a shell command (in the user's shell) when pressed or turned, and can run one for status. If your app has a command-line tool or a URL scheme (open "yourapp://set?volume=$TWIST_VALUE"), that's enough. The same details arrive as environment variables:
TWIST_EVENT=turn TWIST_VALUE=62 TWIST_PREVIOUS=59 TWIST_DELTA=3
TWIST_ON=1 TWIST_BANK=1 TWIST_KNOB=5
Print what you'd return from a shortcut (45, on, or {"value": 45, "on": true}) to move the ring and light.
Keyboard shortcuts
A Keyboard Shortcut control sends one shortcut per step each way as it turns (say ⌘[ and ⌘]), and another when pressed. It needs nothing from your app but menu shortcuts, and shows nothing back, so it's the last resort.
The other way: the web API
To read or drive Twist Commando itself (show its controls in your app, or press one from it), use the local HTTP server behind its web pages. The user turns it on in Settings › General › Web Pages. It listens on http://127.0.0.1:9033; from another device it needs the code shown there, as ?code=123456 once (a cookie remembers it). Requests from other websites' pages are refused.
Watching: GET /events?device=<name>
Server-sent events. Without device, the controller shown in its window. Each data: line is JSON:
"type": "bank", on every change and a few times a second:device,deviceName,bank,bankName,banks,bankNames,shifted,meterView,connected,setupsandsetup,scenesandscene(ids and names), andcontrols: each withslot,control("Knob 3"),kind,assigned,name,value(0–1),text("−6.2 dB"),on,hue(0–1),meter,pressableandturnable."type": "activity"when a control is used: what the on-screen indicator shows (title,subtitle,value,hue).- Add
levels=1for meters on every control, about 20 times a second.
Acting: GET /act?device=<name>&cmd=…&t=<token>
Every action carries the token from GET /token as t. It changes each time Twist Commando starts: on a 409 reply, fetch it again.
press | slot, on=1 then on=0: presses and releases a control |
|---|---|
turn | slot, value 0–1: moves a control to a value |
bank | bank 0–3 |
shift, meter | Toggle Shift, or the meter view |
setup, scene | id: switch to a setup, restore a scene |
rename, color | slot with name, or color (a value from /control, or empty for none) |
Use each control's slot from /events as it is. GET /control?device=…&slot=…&t=… describes one control, with the colors to choose from.
Making it feel right
- Always send your state back. A ring that shows your real value, and a light that shows your real mute, is what makes a controller trustworthy. Send on change, not on a timer.
- Absolute for things with ends (volumes, mixes), relative for things without (scrolling, nudging, a playhead). Treat each relative step as one fine adjustment: a fast turn sends more of them.
- Keep MIDI mappings stable. Save them by the port's name or unique ID, and never assume a channel layout beyond what the user learned.
- Don't echo. When a value arrives from Twist Commando, don't send the same value straight back (it's harmless, but wasteful).
Building something with Twist Commando? We'd like to hear about it, and to list it.