|
1 | | -# Prerequisits |
2 | | -- Python |
3 | | -- GCC |
4 | | -- ALSA |
5 | | -- ALSA dev libraries (`libasound2-dev`) |
6 | | -- ALSA tools (for using `aconnect` that is part of `alsa-utils` package) |
7 | | - |
8 | | -# Install |
9 | | -1. Copy the `c_lib.c` and `clock.py` |
10 | | -2. Compile `c_lib.c` to be used by `clock.py` with the following command:<br> |
11 | | -`gcc -O3 -fPIC -shared -o liblinkbridge.so midi_clock_lib.c -lasound` |
12 | | -3. Run the `clock.py`:<br> |
13 | | -`python3 clock.py` |
14 | | -4. Route the MIDI channel using `aconnect`:<br> |
15 | | -To list options `aconnect -l` then from the list type the `aconnect <source> <destination>` for example: `aconnect 128 130` |
| 1 | +# LinkBridge |
| 2 | + |
| 3 | +A tiny macOS menu bar app that forwards Ableton Link tempo to a hardware MIDI |
| 4 | +output. Drop it next to a Link-enabled DJ app or DAW and any MIDI gear that |
| 5 | +syncs to incoming MIDI clock will follow the tempo in real time. |
| 6 | + |
| 7 | +``` |
| 8 | + ┌──────────────┐ Link ┌──────────────┐ MIDI clock ┌──────────────┐ |
| 9 | + │ djay Pro / │ ─────────▶ │ LinkBridge │ ───────────────▶ │ Hardware │ |
| 10 | + │ Live / etc. │ │ (menu bar) │ (CoreMIDI) │ MIDI device │ |
| 11 | + └──────────────┘ └──────────────┘ └──────────────┘ |
| 12 | +``` |
| 13 | + |
| 14 | +## Requirements |
| 15 | + |
| 16 | +- macOS 15 (Sequoia) or later, Apple Silicon (tested on M2 Pro) |
| 17 | +- Python 3.11 or 3.12 (only needed for running from source / building the .app) |
| 18 | +- A USB-connected MIDI device with external clock support |
| 19 | +- An Ableton Link source on the same machine or LAN |
| 20 | + |
| 21 | +## Quick start (from source) |
| 22 | + |
| 23 | +```bash |
| 24 | +python3.11 -m venv venv |
| 25 | +source venv/bin/activate |
| 26 | +pip install -r requirements.txt |
| 27 | +python -m linkbridge |
| 28 | +``` |
| 29 | + |
| 30 | +A `♪ <bpm>` icon appears in the macOS menu bar within a few seconds. Click |
| 31 | +it, choose your MIDI output device from the **Output Device** submenu, and |
| 32 | +the clock starts streaming as soon as a Link peer reports a tempo. |
| 33 | + |
| 34 | +## Build a standalone `.app` |
| 35 | + |
| 36 | +```bash |
| 37 | +source venv/bin/activate |
| 38 | +pip install -r requirements-dev.txt |
| 39 | +./scripts/build_app.sh |
| 40 | +``` |
| 41 | + |
| 42 | +The bundle is produced at `dist/LinkBridge.app`. Drag it to `/Applications` |
| 43 | +or run it in place. |
| 44 | + |
| 45 | +**On first launch:** |
| 46 | + |
| 47 | +- macOS asks for **local network permission** ("Allow LinkBridge to find |
| 48 | + devices on local networks?"). Click **Allow**, otherwise Ableton Link |
| 49 | + cannot discover any peers and the tempo stays at the default 120 BPM |
| 50 | + forever. The bundle includes a description string explaining what the |
| 51 | + permission is used for. |
| 52 | +- The bundle is **unsigned** (no Apple Developer ID). If macOS Gatekeeper |
| 53 | + blocks it, right-click the `.app` in Finder, choose **Open**, confirm |
| 54 | + the warning, then launch normally afterwards. |
| 55 | + |
| 56 | +## Menu reference |
| 57 | + |
| 58 | +``` |
| 59 | +♪ 125.0 |
| 60 | +──────────── |
| 61 | +Output Device ▸ ● Circuit Tracks MIDI |
| 62 | + ○ DDJ-FLX4 |
| 63 | + ──── |
| 64 | + ↻ Refresh devices |
| 65 | +Enable Start/Stop events ☐ |
| 66 | +──────────── |
| 67 | +Quit |
| 68 | +``` |
| 69 | + |
| 70 | +| Item | Behavior | |
| 71 | +|---|---| |
| 72 | +| `♪ <bpm>` | Live tempo from the active Link peer. Shows `♪ --` when no MIDI device is selected, `♪ ERR` if the clock thread crashed. | |
| 73 | +| **Output Device** | All available CoreMIDI outputs. Pick one to route clock to it. The choice is remembered for next launch. `↻ Refresh devices` rebuilds the list — useful after plugging in a new USB device. | |
| 74 | +| **Enable Start/Stop events** | When ON, sends MIDI `START` on Link play and MIDI `STOP` on Link stop. Default OFF. The clock tick stream itself is **always** running once a device is selected, regardless of this toggle. | |
| 75 | +| **Quit** | Sends a final MIDI STOP if needed, closes the port, exits cleanly. | |
| 76 | + |
| 77 | +## Compatibility — which DJ software works? |
| 78 | + |
| 79 | +LinkBridge passes through whatever Ableton Link tempo it can see. The |
| 80 | +catch is that **not every DJ app broadcasts its deck tempo to Link** — most |
| 81 | +of them only listen. |
| 82 | + |
| 83 | +### "Just works" — fully automatic |
| 84 | + |
| 85 | +These apps act as Link tempo masters when a deck is playing, so LinkBridge |
| 86 | +picks up the tempo automatically with zero manual steps: |
| 87 | + |
| 88 | +- **djay Pro for Mac** (Algoriddim) — officially supports the DDJ-FLX4 and |
| 89 | + many other Pioneer / Numark / Reloop controllers; the playing deck |
| 90 | + becomes the Link master automatically. |
| 91 | +- **Mixxx 2.5+** — open source, free, ships with a community DDJ-FLX4 |
| 92 | + mapping and bidirectional Link support. |
| 93 | +- **Ableton Live**, **Logic Pro** with Link, **GarageBand**, and any other |
| 94 | + Link-aware DAW — playing the timeline broadcasts tempo to Link. |
| 95 | + |
| 96 | +### Works with a small manual step — Rekordbox |
| 97 | + |
| 98 | +**Rekordbox 7.x has a hard one-way Link integration:** the LINK button on |
| 99 | +each deck makes that deck *follow* Link, but Rekordbox does **not** |
| 100 | +broadcast the playing deck's tempo back to Link. This is a Rekordbox |
| 101 | +limitation, not a LinkBridge bug. See Pioneer's |
| 102 | +[Ableton Link FAQ](https://rekordbox.com/en/support/faq/ableton-link/) and |
| 103 | +this [community feature request](https://forums.pioneerdj.com/hc/en-us/community/posts/900002865663-Ableton-Link-in-Rekordbox-Suggestion-Follow-the-Master-Deck-BPM-option). |
| 104 | + |
| 105 | +To use LinkBridge with Rekordbox, drive the Link tempo manually from |
| 106 | +Rekordbox's Ableton Link sub-window: |
| 107 | + |
| 108 | +1. Open Rekordbox and open its **Ableton Link** sub-window (the small |
| 109 | + window with the BPM display, `TAP`, `+`, `-` buttons). |
| 110 | +2. Read the BPM of the track currently loaded on your master deck. |
| 111 | +3. Type or click that BPM into the Link sub-window using the `+`/`-` |
| 112 | + buttons (or use `TAP` to find it by ear). |
| 113 | +4. LinkBridge picks it up immediately and your hardware follows. |
| 114 | +5. When you change tracks, repeat — Rekordbox does not auto-update Link. |
| 115 | + |
| 116 | +> **Note about the tempo slider:** When a hardware controller like the |
| 117 | +> DDJ-FLX4 is connected, Rekordbox disables the on-screen TEMPO slider for |
| 118 | +> Link control entirely. The Link sub-window's `+`/`-` buttons (or MIDI- |
| 119 | +> mapped equivalents) are the only way to drive Link tempo from a |
| 120 | +> controller-attached Rekordbox session. |
| 121 | +
|
| 122 | +If the manual workflow is too tedious for your set, the |
| 123 | +[`rkbx_link`](https://github.com/grufkork/rkbx_link) project reads |
| 124 | +Rekordbox's process memory directly and pushes the master deck's tempo |
| 125 | +into Link automatically. It requires re-signing Rekordbox to add the |
| 126 | +`get-task-allow` entitlement and running with `sudo` — see its |
| 127 | +`MACOS_SETUP.md` for details. LinkBridge does not currently bundle this |
| 128 | +behaviour but composes cleanly with `rkbx_link` if you run them |
| 129 | +side-by-side. |
| 130 | + |
| 131 | +## Using with Novation Circuit Tracks |
| 132 | + |
| 133 | +The Circuit Tracks needs one Setup-view setting before it accepts external |
| 134 | +clock — this is a one-time change that persists across reboots. |
| 135 | + |
| 136 | +1. On the Circuit Tracks, hold **Shift** and press **Save** to enter |
| 137 | + Setup view. |
| 138 | +2. On the bottom row of pads find the four "MIDI data control" Rx/Tx |
| 139 | + pads. The rightmost pair is **MIDI Clock Rx/Tx**. Make sure **Clock |
| 140 | + Rx** is lit (factory default is OFF). |
| 141 | +3. Press **Play** on the Circuit Tracks to exit Setup view. |
| 142 | + |
| 143 | +After that: |
| 144 | + |
| 145 | +- Launch LinkBridge and pick **Circuit Tracks MIDI** from the Output |
| 146 | + Device submenu. |
| 147 | +- If the Circuit Tracks is **stopped** when LinkBridge starts streaming |
| 148 | + clock, it instantly enters external sync mode — the Tempo/Swing view |
| 149 | + shows `SYN` in red. Press Play and the pattern follows the incoming |
| 150 | + tempo. |
| 151 | +- If the Circuit Tracks was **already playing an internal pattern** when |
| 152 | + LinkBridge connected, it ignores the incoming clock and keeps playing |
| 153 | + at its internal tempo. **Recovery is one button press:** tap **Stop** |
| 154 | + on the Circuit Tracks; `SYN` appears immediately because the clock |
| 155 | + stream was already flowing. Press Play again and you're locked. |
| 156 | +- When LinkBridge quits, the clock stream stops, `SYN` disappears, and |
| 157 | + the Circuit Tracks reverts to internal clock — any pattern that was |
| 158 | + playing via SYN halts. |
| 159 | + |
| 160 | +This behaviour is the Circuit Tracks's, not LinkBridge's. Most other |
| 161 | +class-compliant USB MIDI gear follows incoming clock without any |
| 162 | +external-sync arming step. |
| 163 | + |
| 164 | +## Files and logs |
| 165 | + |
| 166 | +| Path | Purpose | |
| 167 | +|---|---| |
| 168 | +| `~/Library/Application Support/LinkBridge/settings.json` | Last selected device and Start/Stop toggle state | |
| 169 | +| `~/Library/Logs/LinkBridge/linkbridge.log` | Rotating log file (1 MB × 3) | |
| 170 | + |
| 171 | +Set `LINKBRIDGE_DEBUG=1` in the environment for verbose logging: |
| 172 | + |
| 173 | +```bash |
| 174 | +LINKBRIDGE_DEBUG=1 python -m linkbridge |
| 175 | +``` |
| 176 | + |
| 177 | +## Architecture |
| 178 | + |
| 179 | +A single Python process with three threads sharing a lock-guarded |
| 180 | +`ClockState` dataclass: |
| 181 | + |
| 182 | +| Thread | Module | Job | |
| 183 | +|---|---|---| |
| 184 | +| Main | `linkbridge/app.py` | rumps menu bar UI, 500 ms label refresh | |
| 185 | +| Clock | `linkbridge/clock_engine.py` | 24 ppqn MIDI clock generator with drift compensation | |
| 186 | +| Link | `linkbridge/link_monitor.py` | aalink callback loop pushing tempo + transport into ClockState | |
| 187 | + |
| 188 | +The Settings store and the MidiOutput helpers (`linkbridge/settings.py`, |
| 189 | +`linkbridge/midi_output.py`) are stateless utilities used by the threads |
| 190 | +above. |
| 191 | + |
| 192 | +## Tests |
| 193 | + |
| 194 | +```bash |
| 195 | +source venv/bin/activate |
| 196 | +pytest |
| 197 | +``` |
| 198 | + |
| 199 | +22 unit tests cover Settings, MidiOutput, and ClockEngine using fake |
| 200 | +clocks / fake MIDI sinks — no hardware or Link peer required. The Link |
| 201 | +monitor and the menu bar app are validated by manual smoke tests against |
| 202 | +real hardware (the smoke procedure is documented in the project plan). |
| 203 | + |
| 204 | +## Regenerating the app icon |
| 205 | + |
| 206 | +The icon source is `assets/icon.png` (1024×1024). To rebuild the |
| 207 | +`assets/LinkBridge.icns` file from a fresh source PNG: |
| 208 | + |
| 209 | +```bash |
| 210 | +.venv-probe/bin/pip install Pillow # or any venv with Pillow available |
| 211 | +python3 scripts/build_icon.py |
| 212 | +``` |
| 213 | + |
| 214 | +The script flood-fills the white background to transparent, crops to the |
| 215 | +non-transparent bbox, generates all 10 macOS iconset sizes, and runs |
| 216 | +`iconutil` to produce the final `.icns`. |
| 217 | + |
| 218 | +## Legacy Linux implementation |
| 219 | + |
| 220 | +The original ALSA / C Linux implementation lives in `legacy/` for |
| 221 | +reference and is not maintained. If you want to run that on Linux, the |
| 222 | +build commands are at the top of `legacy/midi_clock_lib.c` — but the |
| 223 | +macOS port in this branch is the only thing actively developed. |
| 224 | + |
| 225 | +## License |
| 226 | + |
| 227 | +See `LICENSE`. |
0 commit comments