hydra

Terminal replacement for Loopback — virtual audio devices and routing on macOS, from a ratatui TUI.
Log | Files | Refs | README | LICENSE

build-driver.sh (4279B)


      1 #!/usr/bin/env bash
      2 # build-driver.sh — build + ad-hoc-sign the Hydra virtual audio driver, WITHOUT Xcode.
      3 #
      4 # Hydra's driver is a rebranded BlackHole (GPL-3.0). An AudioServerPlugIn is just a
      5 # loadable bundle (a `-bundle` Mach-O + Info.plist), so we build it directly with clang
      6 # from the single vendored BlackHole.c — no full Xcode install required, only the
      7 # Command Line Tools. BlackHole exposes its identity + channel count as compile-time
      8 # constants, so we get a first-party "Hydra" device with ZERO edits to upstream source.
      9 #
     10 #   ./scripts/build-driver.sh
     11 #
     12 # Output: driver/build/Hydra.driver (universal arm64+x86_64 where buildable, ad-hoc
     13 # signed). Install is a separate, user-run, sudo step — see install-driver.sh.
     14 set -euo pipefail
     15 
     16 ROOT="$(cd "$(dirname "$0")/.." && pwd)"
     17 UP="$ROOT/driver/upstream"
     18 SRC="$UP/BlackHole/BlackHole.c"
     19 PLIST_TMPL="$UP/BlackHole/BlackHole.plist"
     20 ICON="$UP/BlackHole/BlackHole.icns"
     21 OUT="$ROOT/driver/build"
     22 APP="$OUT/Hydra.driver"
     23 
     24 DRIVER_NAME="Hydra"
     25 BUNDLE_ID="com.ganten.hydra.driver"
     26 # Default 2ch stereo (matches a normal mic / Loopback). Override for DAW use with
     27 # HYDRA_DRIVER_CHANNELS=N.
     28 CHANNELS="${HYDRA_DRIVER_CHANNELS:-2}"
     29 # Default startup sample rate 44100. THIS was the real "one ear in Discord" cause: Hydra
     30 # defaulted to 48000 while the host setup ran at 44100, and the resample in the capture path
     31 # collapsed stereo to one channel. Loopback worked because it matched the host at 44100.
     32 # The device still *supports* 48000 etc.; this only sets the rate it comes up at.
     33 # Override with HYDRA_DRIVER_RATE=48000 if your setup is 48k.
     34 RATE="${HYDRA_DRIVER_RATE:-44100}"
     35 MANUFACTURER="Ganten"
     36 SIGN_ID="${HYDRA_SIGN_ID:--}"   # '-' = ad-hoc
     37 
     38 [ -f "$SRC" ] || { echo "error: $SRC missing — clone upstream (see driver/README.md)." >&2; exit 1; }
     39 
     40 echo "• compiling BlackHole.c → Hydra (${CHANNELS}ch, $BUNDLE_ID)"
     41 rm -rf "$OUT"
     42 mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
     43 
     44 # Rebrand purely via -D overrides (kHas_Driver_Name_Format=false drops the "%ich" suffix,
     45 # so the device shows as a clean "Hydra"). No edits to upstream source.
     46 DEFS=(
     47     -DkDriver_Name="\"$DRIVER_NAME\""
     48     -DkPlugIn_BundleID="\"$BUNDLE_ID\""
     49     -DkPlugIn_Icon="\"$DRIVER_NAME.icns\""
     50     -DkManufacturer_Name="\"$MANUFACTURER\""
     51     -DkNumber_Of_Channels=$CHANNELS
     52     -DkDefault_SampleRate=${RATE}.0
     53     -DkHas_Driver_Name_Format=false
     54     -DkDevice_Name="\"$DRIVER_NAME\""
     55     -DkDevice2_Name="\"$DRIVER_NAME Mirror\""
     56 )
     57 
     58 # This toolchain can drop the cross slice when both -arch flags are passed at once, so
     59 # compile each arch separately and lipo whatever succeeds into the final binary.
     60 # Hydra's manifest reader (device name from devices.json) compiles alongside BlackHole.c.
     61 CFG="$ROOT/driver/hydra_cfg.c"
     62 SLICES=()
     63 for arch in arm64 x86_64; do
     64     obj="$OUT/Hydra-$arch"
     65     if clang -bundle "$SRC" "$CFG" -o "$obj" \
     66         -fobjc-arc -O2 -arch "$arch" -mmacosx-version-min=11.0 \
     67         -framework CoreAudio -framework CoreFoundation -framework Foundation -framework Accelerate \
     68         "${DEFS[@]}" 2>/dev/null; then
     69         SLICES+=("$obj")
     70     else
     71         echo "  (skipping $arch — not buildable on this toolchain)"
     72     fi
     73 done
     74 [ ${#SLICES[@]} -gt 0 ] || { echo "✗ no architecture built" >&2; exit 1; }
     75 lipo -create "${SLICES[@]}" -output "$APP/Contents/MacOS/$DRIVER_NAME"
     76 rm -f "${SLICES[@]}"
     77 
     78 # The CFPlugIn factory symbol must be exported or coreaudiod can't instantiate the driver.
     79 nm -gU "$APP/Contents/MacOS/$DRIVER_NAME" | grep -q "_BlackHole_Create" \
     80     || { echo "✗ factory symbol _BlackHole_Create not exported" >&2; exit 1; }
     81 
     82 echo "• writing Info.plist + resources"
     83 sed -e "s/\${EXECUTABLE_NAME}/$DRIVER_NAME/g" \
     84     -e "s/\${PRODUCT_NAME}/$DRIVER_NAME/g" \
     85     -e "s/\$(PRODUCT_BUNDLE_IDENTIFIER)/$BUNDLE_ID/g" \
     86     -e "s/\$(MARKETING_VERSION)/0.1.0/g" \
     87     "$PLIST_TMPL" > "$APP/Contents/Info.plist"
     88 cp "$ICON" "$APP/Contents/Resources/$DRIVER_NAME.icns"
     89 
     90 echo "• signing (identity: $SIGN_ID)"
     91 codesign --force --deep --sign "$SIGN_ID" "$APP"
     92 codesign --verify --verbose "$APP" 2>&1 | tail -1
     93 
     94 echo
     95 echo "✓ built $APP"
     96 echo "    $(lipo -info "$APP/Contents/MacOS/$DRIVER_NAME" | sed 's/.*: //')"
     97 echo "  install (sudo, restarts audio): ./scripts/install-driver.sh"