GitHub - JamesRyanATX/fcbnerd: Do things with a Behringer FCB1010 MIDI pedalboard in MacOS
Pangram verdict · v3.3
We believe this text is mainly AI, with some human-written content.
AI likelihood · overall
AIArticle text · 840 words · 2 segments analyzed
Use a MIDI foot controller as an extra keyboard for your Mac. fcbnerd connects to your MIDI sources and either runs a shell command when a footswitch or pedal sends a message you've bound, or prints one JSON object per line for every message so another program can decide what a stomp means. $ fcbnerd -q --bind '1:20:127=open ~/Downloads' --bind 'pc:1:0=say hello' Or stream everything for another program to handle: $ fcbnerd {"type":"connected","source":"UM-ONE","time":"2026-09-14T20:01:00.120Z"} {"type":"pc","channel":1,"program":0,"source":"UM-ONE","time":"2026-09-14T20:01:02.345Z"} {"type":"cc","channel":1,"controller":27,"value":84,"source":"UM-ONE","time":"2026-09-14T20:01:03.910Z"} Built for the Behringer FCB1010, but nothing in it is FCB1010-specific: any CoreMIDI source works. Why a command-line tool instead of an app Anything that acts on your Mac, like pressing keys or running scripts, needs permissions that sandboxed apps can't get, and every user wants a different set of actions anyway. fcbnerd only reads MIDI, which needs no permissions. The actions belong to your shell, or to a tool that already has the access, such as Hammerspoon or Keyboard Maestro. Install brew trust --tap jamesryanatx/tap # Homebrew 7+ won't load third-party taps until you trust them brew install JamesRyanATX/tap/fcbnerd Or from source without Homebrew (Xcode or the Swift toolchain, macOS 13+): swift build -c release cp .build/release/fcbnerd /usr/local/bin/ Usage fcbnerd [listen] [--source NAME] [--format json|text] [--bind BINDING]... [--quiet] [--shell PATH] fcbnerd list [--format json|text] fcbnerd simulate listen (default) connects to every MIDI source, or only those whose name contains --source, and streams events until interrupted. It follows hotplug: unplug the interface mid-set and plug it back in, and the stream carries on with disconnected / connected lines. list prints the sources available right now. simulate publishes a virtual MIDI source named fcbnerd simulator that plays synthetic presses, a pedal sweep and a sysex message on a loop. Run it in one terminal and fcbnerd in another to build a consumer with no pedal attached. --format text prints aligned columns for eyeballing, including a bind= pattern for each message you can bind. Scripts should use the default JSON; the text layout may change. --bind runs a command when a message matches; see below. --quiet stops printing events, leaving only the bound commands. --shell PATH picks the shell that runs bound commands (default /bin/sh). Status messages go to stderr; stdout carries only events. Each line is flushed as soon as it's written, so pipes see events immediately. Binding commands First find out what your pedal sends. Run fcbnerd -f text and press the switch: $ fcbnerd -f text 16:30:41.115 pc channel=1 program=7 bind=pc:1:7 [USB MIDI Interface] 16:30:41.115 cc channel=1 controller=20 value=127 bind=1:20:127 [USB MIDI Interface] Then bind a command to that pattern: fcbnerd --bind '1:20:127=open ~/Downloads' A binding is PATTERN=COMMAND. Everything after the first = is the command, so it can contain = and : itself. Use --bind as many times as you like. Every binding that matches a message starts, in the order given, and they run at the same time. Pattern Matches CHANNEL:CONTROLLER:VALUE Control change, e.g. 1:20:127. cc:1:20:127 also works. pc:CHANNEL:PROGRAM Program change, e.g. pc:1:7. Any number can be *: 1:27:* is every value of controller 27 on channel 1, which is how you bind an expression pedal. Commands run in the background through /bin/sh -c, or the shell you give with --shell. Their stdin is /dev/null. Their stdout goes to fcbnerd's stderr, so it can't corrupt the event stream; with --quiet it goes to stdout. They see these environment variables: Variable MIDI_TYPE cc or pc MIDI_CHANNEL 1–16 MIDI_CONTROLLER, MIDI_VALUE For cc MIDI_PROGRAM For pc MIDI_SOURCE MIDI source name # Expression pedal sets output volume fcbnerd -q --bind '1:27:*=osascript -e "set volume output volume $((MIDI_VALUE * 100 / 127))"' Every stomp runs the command, so two quick presses run it twice even if the first run hasn't finished. That also means every matching message starts a shell. Keep broad patterns like *:*:127 or pc:*:* away from noisy devices. Pedal sweeps are the exception. A sweep sends dozens of values a second, so for a binding with a * value, only one copy of the command runs at a time for each control (channel and controller). While it runs, fcbnerd keeps only that control's newest value and runs it next, which keeps the shell count down and still ends on the pedal's final position. If a command is still running after 5 seconds, fcbnerd says so on stderr. A command that exits non-zero gets its binding and exit status printed to stderr. Stopping fcbnerd (Ctrl+C, kill, closing the terminal, or a closed stdout) sends SIGTERM to any command still running, including processes it started.
Shell functions Functions and aliases from your interactive shell aren't loaded in sh -c. In bash, export a function to make it visible (macOS's /bin/sh is bash, so the default shell sees it): greet() { say "preset $MIDI_PROGRAM"; } export -f greet fcbnerd -q --bind 'pc:1:*=greet' zsh can't export functions. Put them in a file and source it with zsh: --shell /bin/zsh --bind 'pc:1:*=source ~/.fcbnerd.zsh && greet'. On/off switches The FCB1010 sends nothing when you let go of a switch (see FCB1010 notes), so a binding fires on the press only.