# External clock sync & controller actions

FaustWave can follow an **external MIDI clock** — a hardware sequencer, groovebox, or another DAW acts as the tempo master, and FaustWave's whole transport (BPM, play/stop, every sequencer, every transport-linked Builder clock) rides along. This is how FaustWave joins a live set.

## Wiring it up — routing IS the switch

The routing table has a permanent MIDI Sink called **Transport Clock In** (`transport-clock:midi-in`). There is no settings toggle and no mode switch: **connecting a MIDI input to this Sink enables clock follow; disconnecting it stops it.** Same doctrine as everything else in the Patchbay.

1. Connect your clock-sending device (USB-MIDI, or a virtual port like loopMIDI on Windows / an IAC bus on macOS when the master is software).

> ⚠️ **Windows: create the VIRTUAL port before you launch FaustWave.** A loopMIDI-style port created while the IDE is already running is reported like any other — it appears in the MIDI Devices panel and in `routing.list`, reads `connection: "open"`, and can be wired in the Patchbay — but **carries no data in either direction**. Nothing distinguishes it from a working port except that nothing ever arrives; rescanning, reopening the port and reloading the renderer all fail to revive it, only a restart of the app does (a WinMM/Chromium limitation; CoreMIDI does not have it). Start loopMIDI, add the port, *then* start FaustWave.
>
> This is about virtual ports only. **Real USB hardware plugged in after launch works at once**, and so does unplug/replug of a port that was there at startup. Since Web MIDI cannot tell the two cases apart, FaustWave does not warn — it only records the neutral fact on the Devices row and in `midi.ports.list`: *this port was not there at startup*. If a port that carries the late-arrival marker stays silent, this is the first thing to rule out.
2. In the Patchbay matrix, connect that device's input Source to **Transport Clock In**.

> 🔘 **MCP**: `routing.connect { kind: "midi", from_source: "midi-input-<deviceId>", from_port: "out", to_sink: "transport-clock:midi-in", to_port: "in" }`.

The persisted routing survives restarts and unplug/replug like any other hardware routing.

## What "following" means

MIDI clock is a stream of tick messages (24 per quarter note) plus Start/Stop commands. FaustWave does **not** replace its own clock with those ticks — the sample-accurate Faust master clock (see **FaustWave — Sequencer** § *The shared Faust master clock*) stays the only timebase that triggers notes. Instead, the tick stream is treated as a **measurement**: a windowed tempo estimate (about two beats of ticks, first lock after half a beat) continuously nudges the transport BPM, the same way a cruise control holds speed. Tick jitter — real hardware clocks wobble by several milliseconds — is absorbed by the estimator; your groove stays sample-locked.

While ticks are arriving, an **EXT** tag appears next to the BPM readout in the topbar transport strip (hover it for the source device and current external tempo).

- **Start (0xFA)** — the transport (re)starts from the top, even if it was already playing. This mirrors what the master does: pressing Start on the master realigns everyone to bar 1. The **start** is phase-locked: the grid is anchored on the master's own step raster rather than on FaustWave's reaction time, so the first beat lands with the master instead of ~110 ms behind it (measured ±2–5 ms against a deterministic software master). This is the START only — see below for what happens over the following minutes.
- **Stop (0xFC)** — the transport stops.
- **Continue (0xFB)** — followed as play, but FaustWave restarts from the top rather than resuming mid-song, and deliberately does **not** snap to the master's raster (there is no song position to snap to). Song-position resume is on the roadmap.
- **Ticks alone never start playback.** Many masters send clock continuously even while stopped — FaustWave uses that to keep its tempo estimate warm, so pressing Start on the master drops you in at the right BPM immediately.

## Rules and gotchas

- **One clock source at a time.** If two devices are routed to Transport Clock In, the one that ticks first wins; the other is ignored until the active one goes silent for over a second (then the survivor takes over). Two merged clock streams would read as double tempo — don't route two masters.
- **Manual BPM edits are overridden while EXT is active.** The external master owns the tempo; edit BPM on the master, not in FaustWave.
- **Out-of-range masters clamp.** FaustWave's transport range is 30–300 BPM; a master outside that range pins FaustWave to the nearer limit.
- **Clock lost** (device unplugged, master powered off): after ~1 second the EXT tag disappears and FaustWave keeps the last followed BPM and play state — the set doesn't stop just because the cable did. The Logs panel records the hand-off.
- **No hardware handy?** Any DAW can be the master over a virtual MIDI port (loopMIDI on Windows, IAC on macOS) — e.g. Reaper's "Send clock/SPP to output" per MIDI output device. On Windows, create the virtual port before launching FaustWave (see the warning above).

## Controller actions (pads & buttons run FaustWave)

The second controller sink is **Controller Actions** (`controller-actions:midi-in`) — route a controller's MIDI input onto it and its pads/buttons can fire any action in the IDE: launch a scene, stop the transport, toggle recording. Same doctrine: routing is the enable switch, and the mapping persists per project.

Bindings are authored via commands (ask the assistant, or palette.run):

> 🔘 **MCP**: `controller.bind.add { type: "note", number: 36, channel: 16, action_id: "sequencer.scene.launch", input: { scene_id: "..." } }` — pad note 36 on channel 16 launches that scene. `type: "cc"` fires button-style on the rising edge across value 64. `channel` is required: 1..16, or 0 = any channel — only when nothing else on that port uses the number (a hardware sequencer sharing the port fires an omni binding on every step). `controller.bind.list` shows the map; `controller.bind.remove { binding_id }` drops one.

Or **Learn** it in the Controller panel: choose Action or Param, pick **Note** or **CC** (one kind at a time — a hardware sequencer sharing the port sends notes that look like pad presses), arm Learn, press the pad or turn the knob; a param widget's right-click menu has a "MIDI Learn…" row that lands there pre-selected.

Every fired binding lands in the Activity log like a UI click — a live set stays auditable. A binding to a mistyped action id is rejected at add time; a binding whose action breaks later warns in the Logs panel instead of silently doing nothing.

## Encoders → sound parameters (midi_cc node)

Turning a hardware encoder into a *sound* control is not a binding — it's a Builder node. Add a **MIDI CC** node (Sources category) to the instrument's graph, set its CC number / channel / min–max range, and wire its `value` output into any control input (`cutoff_in`, `res_in`, a gain). The CC-to-value mapping runs inside the Faust worklet (`[midi:ctrl]` metadata — no JS hop, smoothed against zipper noise). Route the controller's MIDI input onto the **instrument's** `:midi-in` sink for this — the module, not the Controller Actions sink.

Rule of thumb: **buttons/pads → Controller Actions sink + bindings; knobs/encoders → midi_cc node in the instrument.**

## What FaustWave does not do (yet)

- **Running phase lock**: the START is snapped onto the master's raster (see above), but from then on the ticks correct the **rate only, never the position** — there is no control loop nudging the downbeats back together. Against a deterministic master the residual drift is ~0.04 ms/s; against real hardware it is whatever that device's tempo quantisation costs — a BeatStep Pro running "120" actually sends 119.9, which works out to ~0.6 ms/s, i.e. roughly 36 ms per minute. For a song-length set that is inaudible; over a long DJ-style set it is not, and re-pressing Start on the master re-aligns everyone.
- **Clock master**: FaustWave sending clock OUT to hardware is planned but not wired yet.
- **Ableton Link**: on the roadmap after the MIDI path is proven.
