Virtual MIDI ports, hardware, RTP-MIDI and Bluetooth MIDI for macOS, Linux and Windows.
Find a file
James Coleman 57fe7ff53a fix(service): wait for launchd to drop the old job before loading the new
- Update now stopped the daemon and failed with "Bootstrap failed: 5: Input/output error" on macOS, leaving the agent's plist written and no job loaded. `launchctl bootout` returns once the daemon has been sent SIGTERM, and launchd refuses `bootstrap` until the daemon has exited and the job has left the domain.
- Installing or replacing the launchd agent now waits for the booted-out job to leave, for up to 25 seconds, which covers the 20 seconds launchd allows before SIGKILL.
- Starting the service now loads the agent's plist first when launchd does not hold the job, so the window's Start button and `service start` recover a registration left unloaded instead of failing in `kickstart`.
- The stand-in launchctl in the service lifecycle test returns from `bootout` while the daemon is still stopping and refuses `bootstrap` until it has gone, as launchd does, so `service install --start` over a running daemon pins the regression.
2026-10-04 08:43:28 -05:00
.cargo First commit 2026-09-28 13:59:10 -05:00
.claude/skills First commit 2026-09-28 13:59:10 -05:00
.specify docs(specs): drop the spec names used before the renumbering 2026-09-29 14:35:33 -05:00
crates fix(service): wait for launchd to drop the old job before loading the new 2026-10-04 08:43:28 -05:00
docs fix(appimage): install the desktop entry so the window gets its icon 2026-10-02 12:32:08 -05:00
fuzz feat(network): follow a machine's session and forget removed ones 2026-10-02 11:56:20 -05:00
packaging fix(appstore): drop winit's private blur call that App Review refused 2026-10-03 09:39:12 -05:00
proto/midiharbor/v1 feat(update): say when the daemon is another build and update it on request 2026-10-02 11:56:27 -05:00
scripts First commit 2026-09-28 13:59:10 -05:00
specs fix(service): wait for launchd to drop the old job before loading the new 2026-10-04 08:43:28 -05:00
src First commit 2026-09-28 13:59:10 -05:00
tests fix(service): wait for launchd to drop the old job before loading the new 2026-10-04 08:43:28 -05:00
.gitignore First commit 2026-09-28 13:59:10 -05:00
.goreleaser.yaml feat(update): say when the daemon is another build and update it on request 2026-10-02 11:56:27 -05:00
AGENTS.md First commit 2026-09-28 13:59:10 -05:00
build.rs First commit 2026-09-28 13:59:10 -05:00
Cargo.lock fix(appstore): drop winit's private blur call that App Review refused 2026-10-03 09:39:12 -05:00
Cargo.toml fix(appstore): drop winit's private blur call that App Review refused 2026-10-03 09:39:12 -05:00
clippy.toml First commit 2026-09-28 13:59:10 -05:00
Icon.svg build(icon): give both cable strands and the MIDI pins a glow 2026-10-02 11:55:25 -05:00
LICENSE.txt First commit 2026-09-28 13:59:10 -05:00
Makefile feat(update): say when the daemon is another build and update it on request 2026-10-02 11:56:27 -05:00
README.md feat(release): publish an AppImage for each Linux architecture 2026-09-29 14:35:51 -05:00
rust-toolchain.toml First commit 2026-09-28 13:59:10 -05:00
rustfmt.toml First commit 2026-09-28 13:59:10 -05:00
VERSION First commit 2026-09-28 13:59:10 -05:00

midi-harbor

Midi Harbor manages MIDI connections on macOS, Linux and Windows: virtual ports other applications can use, attached MIDI hardware, RTP-MIDI network sessions to other computers, and Bluetooth LE MIDI devices. A daemon owns every connection and repairs it on its own. A network session that drops is reconnected, hardware that is unplugged and plugged back in picks up where it left off, and notes sounding when a link went away are released rather than left ringing.

The command line and the graphical interface are both clients of the daemon, so closing either changes nothing.

Install

Download a package or archive from the releases, or build this project. midi-harbor-headless leaves out the graphical interface, for machines without a display.

sudo apt install ./midi-harbor_<version>_<arch>.deb    # Debian and Ubuntu
sudo dnf install ./midi-harbor-<version>-1.<arch>.rpm    # Fedora and RHEL
chmod +x Midi-Harbor-<version>-x86_64.AppImage              # any other distribution

The release builds need glibc 2.35 or newer, so RHEL 9 and its rebuilds must build from source.

The Windows executable is not signed, so it warns the first time it runs; Installation says how to let it run.

On Windows, virtual ports need Windows MIDI Services. Windows 11 includes it from its late-2026 update; before that, install Microsoft's Windows MIDI Services SDK Runtime and Tools.

Building

Building needs Rust 1.96 or newer. On Linux it also needs the ALSA, D-Bus and Avahi development files and libclang, and for the graphical interface the xkbcommon and Wayland development files.

On Debian and Ubuntu:

sudo apt install build-essential pkg-config libasound2-dev libdbus-1-dev \
                 libavahi-client-dev libclang-dev
sudo apt install libxkbcommon-dev libwayland-dev    # for the graphical interface

On Fedora and RHEL (enable CodeReady Builder first on RHEL and its rebuilds):

sudo dnf install gcc pkgconf-pkg-config alsa-lib-devel dbus-devel avahi-devel clang-devel
sudo dnf install libxkbcommon-devel wayland-devel    # for the graphical interface

Then:

cargo build --release                        # with the graphical interface
cargo build --release --no-default-features  # without it

Windows builds are cross-compiled with mingw-w64:

rustup target add x86_64-pc-windows-gnu
cargo build --release --target x86_64-pc-windows-gnu

Release packages for every platform are built with GoReleaser in Docker, through make snapshot and make release.

Running as a service

The daemon registers itself with launchd, a systemd user unit, or Task Scheduler, and runs at every login:

midi-harbor service install --start
midi-harbor service status

Running from the command line

To try things out, or to see more of what the daemon is doing, run it in the foreground:

midi-harbor daemon -vv

Config

The setup lives in one YAML file per user, which the daemon writes as things change. midi-harbor config path prints where it is. It can be written by hand; only names and kinds are required:

endpoints:
  - name: Sequencer Bus
    kind: virtual_port
  - name: Studio
    kind: network_port
routes:
  - from: Sequencer Bus
    to: Studio

Run midi-harbor config reload after editing it while the daemon runs.

Usage

Every command has help:

midi-harbor --help

Run with no command, midi-harbor opens the graphical interface.

A basic setup that sends a local virtual port to another computer:

midi-harbor port create "Sequencer Bus"
midi-harbor network create "Studio"
midi-harbor network discover
midi-harbor network connect "Studio" "Stage Mac"
midi-harbor route create "Sequencer Bus" "Studio"

MIDI played into "Sequencer Bus" now reaches "Stage Mac", and keeps reaching it across sleep, network changes, and either machine restarting.

Testing

make test                # unit tests
make test-integration    # the daemon, CLI and protocols over real files and sockets
make test-live           # tests needing real MIDI, a Bluetooth radio or a session peer