Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions BlackHole/BlackHole.c
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ struct ObjectInfo {
#define kNumber_Of_Channels 2
#endif

// Null device support for audio termination (/dev/null equivalent)
// When kNumber_Of_Channels is 1 and kNull_Device_Mode is true, device discards all data
#define kNull_Device_Mode (kNumber_Of_Channels == 1 && defined(kCreate_Null_Device))
#define kSupports_Null_Device true

#ifndef kEnableVolumeControl
#define kEnableVolumeControl true
#endif
Expand Down Expand Up @@ -334,6 +339,8 @@ static const UInt32 kDevice_SampleRatesSize = sizeof
#define kBytes_Per_Channel (kBits_Per_Channel/ 8)
#define kBytes_Per_Frame (kNumber_Of_Channels * kBytes_Per_Channel)
#define kRing_Buffer_Frame_Size ((65536 + kLatency_Frame_Size))

// For null devices, we still use normal buffer allocation but data gets discarded
static Float32* gRingBuffer = NULL;


Expand Down Expand Up @@ -3283,7 +3290,7 @@ static OSStatus BlackHole_SetStreamPropertyData(AudioServerPlugInDriverRef inDri
case kAudioStreamPropertyPhysicalFormat:
// Changing the stream format needs to be handled via the
// RequestConfigChange/PerformConfigChange machinery. Note that because this
// device only supports 2 channel 32 bit float data, the only thing that can
// device supports fixed channel count 32 bit float data, the only thing that can
// change is the sample rate.
FailWithAction(inDataSize != sizeof(AudioStreamBasicDescription), theAnswer = kAudioHardwareBadPropertySizeError, Done, "BlackHole_SetStreamPropertyData: wrong size for the data for kAudioStreamPropertyPhysicalFormat");
FailWithAction(((const AudioStreamBasicDescription*)inData)->mFormatID != kAudioFormatLinearPCM, theAnswer = kAudioDeviceUnsupportedFormatError, Done, "BlackHole_SetStreamPropertyData: unsupported format ID for kAudioStreamPropertyPhysicalFormat");
Expand Down Expand Up @@ -4568,11 +4575,10 @@ static OSStatus BlackHole_DoIOOperation(AudioServerPlugInDriverRef inDriver, Aud
memcpy((Float32*)ioMainBuffer + firstPartFrameSize * kNumber_Of_Channels, gRingBuffer, secondPartFrameSize * kNumber_Of_Channels * sizeof(Float32));

// Finally we'll apply the output volume to the buffer.
if(kEnableVolumeControl)
{
vDSP_vsmul(ioMainBuffer, 1, &gVolume_Master_Value, ioMainBuffer, 1, inIOBufferFrameSize * kNumber_Of_Channels);
}

if(kEnableVolumeControl)
{
vDSP_vsmul(ioMainBuffer, 1, &gVolume_Master_Value, ioMainBuffer, 1, inIOBufferFrameSize * kNumber_Of_Channels);
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
- Zero-channel audio termination device (BlackHole 0ch)
- Acts as `/dev/null` for audio - accepts and discards all audio data
- Useful for applications requiring audio output without actual sound playback
- Supports audio software development, testing, and advanced routing workflows
- Included in build system and installer script

### Changed

## [0.6.1] - 2025-02-06
Expand Down
91 changes: 87 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Sponsor: https://github.com/sponsors/ExistentialAudio

## Features

- Builds 2, 16, 64, 128, and 256 audio channels versions
- Builds 0, 2, 16, 64, 128, and 256 audio channels versions
- **Zero-channel variant** for audio termination (acts as `/dev/null` for audio)
- Customizable channel count, latency, hidden devices
- Customizable mirror device to allow for a hidden input or output
- Supports 8kHz, 16kHz, 44.1kHz, 48kHz, 88.2kHz, 96kHz, 176.4kHz, 192kHz, 352.8kHz, 384kHz, 705.6kHz and 768kHz sample rates
Expand All @@ -42,20 +43,72 @@ Sponsor: https://github.com/sponsors/ExistentialAudio

![Audio MIDI Setup](Images/audio-midi-setup.png)

## Zero-Channel Audio Termination (BlackHole 0ch)

BlackHole 0ch is a special variant that acts as an **audio termination device** - essentially `/dev/null` for audio. This device accepts audio output from applications but silently discards all audio data instead of playing it or routing it elsewhere.

### Use Cases

- **Audio Software Development**: Applications that require an audio output device but don't need actual audio playback
- **Automated Testing**: Testing audio applications without producing sound output
- **Audio Routing**: Terminating specific audio streams in complex routing setups
- **Broadcast/Streaming**: Discarding unwanted audio channels while keeping others
- **System Administration**: Preventing certain applications from producing audio output

### How It Works

- Appears as "BlackHole 0ch" in Audio MIDI Setup and application audio device lists
- Applications can select it as a normal audio output device
- All audio sent to this device is silently discarded with zero latency
- Uses minimal system resources (no actual audio processing or buffering)
- Maintains proper timing and synchronization for applications that depend on audio device timing

### Example Usage

```bash
# Route audio from an application to null device (no sound output)
# Select "BlackHole 0ch" as output in the application's audio settings

# Useful for applications that require audio output but you want silence:
# - Screen recording software (record video without audio)
# - Audio testing tools (test without hearing output)
# - Background music apps (disable audio while keeping app running)
```

## Installation Instructions

### Option 1: Download Installer
### Option 1: Download Installer (Standard Variants)

1. [Download the latest installer](https://existential.audio/blackhole)
2. Close all running audio applications
3. Open and install package

### Option 2: Install via Homebrew
*Available variants: 2ch, 16ch, 64ch, 128ch, 256ch*

### Option 2: Install via Homebrew (Standard Variants)

- 2ch: `brew install blackhole-2ch`
- 16ch: `brew install blackhole-16ch`
- 64ch: `brew install blackhole-64ch`

### Option 3: Manual Build (Zero-Channel Variant)

The zero-channel audio termination device must be built manually:

```bash
# Build the driver
xcodebuild -project BlackHole.xcodeproj -configuration Release -target BlackHole \
CONFIGURATION_BUILD_DIR=build PRODUCT_BUNDLE_IDENTIFIER=audio.existential.BlackHole0ch \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \
'GCC_PREPROCESSOR_DEFINITIONS=$GCC_PREPROCESSOR_DEFINITIONS kNumber_Of_Channels=0 kPlugIn_BundleID="audio.existential.BlackHole0ch" kDriver_Name="BlackHole"'

# Install the driver
sudo cp -R build/BlackHole.driver /Library/Audio/Plug-Ins/HAL/BlackHole0ch.driver
sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod
```

See [Developer Guides](#developer-guides) for complete build instructions.

## Uninstallation Instructions

### Option 1: Use Uninstaller
Expand Down Expand Up @@ -125,11 +178,39 @@ For more specific details [visit the Wiki](https://github.com/ExistentialAudio/B
Please support our hard work and continued development. To request a license [contact Existential Audio](mailto:[email protected]).

### Build & Install

#### Standard Build
After building, to install BlackHole:

1. Copy or move the built `BlackHoleXch.driver` bundle to `/Library/Audio/Plug-Ins/HAL`
2. Restart CoreAudio using `sudo killall -9 coreaudiod`

#### Building Zero-Channel Variant

To build the zero-channel audio termination device (BlackHole 0ch):

```bash
xcodebuild \
-project BlackHole.xcodeproj \
-configuration Release \
-target BlackHole \
CONFIGURATION_BUILD_DIR=build \
PRODUCT_BUNDLE_IDENTIFIER=audio.existential.BlackHole0ch \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
'GCC_PREPROCESSOR_DEFINITIONS=$GCC_PREPROCESSOR_DEFINITIONS kNumber_Of_Channels=0 kPlugIn_BundleID="audio.existential.BlackHole0ch" kDriver_Name="BlackHole"'
```

Then install:
```bash
sudo cp -R build/BlackHole.driver /Library/Audio/Plug-Ins/HAL/BlackHole0ch.driver
sudo chown -R root:wheel /Library/Audio/Plug-Ins/HAL/BlackHole0ch.driver
sudo chmod -R 755 /Library/Audio/Plug-Ins/HAL/BlackHole0ch.driver
sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod
```

The zero-channel device will appear as "BlackHole 0ch" in Audio MIDI Setup and can be used to terminate audio streams.

### Customizing BlackHole

The following pre-compiler constants may be used to easily customize a build of BlackHole.
Expand All @@ -150,12 +231,14 @@ kDevice2_HasInput
kDevice2_HasOutput

kLatency_Frame_Size
kNumber_Of_Channels
kNumber_Of_Channels // Set to 0 for audio termination device
kSampleRates
```

They can be specified at build time with `xcodebuild` using `GCC_PREPROCESSOR_DEFINITIONS`.

**Note**: Setting `kNumber_Of_Channels=0` creates a special audio termination device that discards all audio data, useful for applications that require an audio output but don't need actual sound output.

Example:

```bash
Expand Down
53 changes: 53 additions & 0 deletions scripts/build_all_variants.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash
set -e

echo "🔨 Building all BlackHole variants..."
echo ""

variants=(0 2 16 64 128 256)

for channels in "${variants[@]}"; do
echo "📦 Building BlackHole ${channels}ch..."

if [ "$channels" -eq 0 ]; then
bundle_id="audio.existential.BlackHole0ch"
build_dir="build_0ch"
description="Audio termination device"
else
bundle_id="audio.existential.BlackHole${channels}ch"
build_dir="build_${channels}ch"
description="Audio loopback device"
fi

xcodebuild \
-project BlackHole.xcodeproj \
-configuration Release \
-target BlackHole \
CONFIGURATION_BUILD_DIR="$build_dir" \
PRODUCT_BUNDLE_IDENTIFIER="$bundle_id" \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
"GCC_PREPROCESSOR_DEFINITIONS=\$GCC_PREPROCESSOR_DEFINITIONS kNumber_Of_Channels=$channels kPlugIn_BundleID=\"$bundle_id\" kDriver_Name=\"BlackHole\"" \
> "$build_dir.log" 2>&1

if [ -d "$build_dir/BlackHole.driver" ]; then
echo "✅ BlackHole ${channels}ch built successfully ($description)"
else
echo "❌ BlackHole ${channels}ch build failed"
echo " Check $build_dir.log for details"
fi
done

echo ""
echo "🎉 Build complete! Built variants:"
for channels in "${variants[@]}"; do
build_dir="build_${channels}ch"
if [ -d "$build_dir/BlackHole.driver" ]; then
echo " ✅ BlackHole ${channels}ch: $build_dir/BlackHole.driver"
fi
done

echo ""
echo "📋 To install a specific variant:"
echo " sudo cp -R build_Xch/BlackHole.driver /Library/Audio/Plug-Ins/HAL/BlackHoleXch.driver"
echo " sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod"
115 changes: 115 additions & 0 deletions scripts/create_0ch_installer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/bin/bash
set -euo pipefail

# Creates installer for BlackHole 0-channel variant only
# This is separate from the main installer to allow optional distribution
# Run this script from the BlackHole repo's root directory.

driverName="BlackHole"
devTeamID="Q5C99V536K" # ⚠️ Replace this with your own developer team ID
notarize=false # Set to true if you have notarization setup
notarizeProfile="notarize" # ⚠️ Replace this with your own notarytool keychain profile name

############################################################################

# Basic Validation
if [ ! -d BlackHole.xcodeproj ]; then
echo "This script must be run from the BlackHole repo root folder."
echo "For example:"
echo " cd /path/to/BlackHole"
echo " ./scripts/create_0ch_installer.sh"
exit 1
fi

version=`cat VERSION`

# Version Validation
if [ -z "$version" ]; then
echo "Could not find version number. VERSION file is missing from repo root or is empty."
exit 1
fi

echo "Building BlackHole 0ch installer (version $version)..."

channels=0
ch="${channels}ch"
driverVariantName="$driverName$ch"
bundleID="audio.existential.$driverVariantName"

# Build
echo "Building driver..."
xcodebuild \
-project BlackHole.xcodeproj \
-configuration Release \
-target BlackHole CONFIGURATION_BUILD_DIR=build \
PRODUCT_BUNDLE_IDENTIFIER=$bundleID \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
"GCC_PREPROCESSOR_DEFINITIONS=\$GCC_PREPROCESSOR_DEFINITIONS kNumber_Of_Channels=$channels kPlugIn_BundleID=\"$bundleID\" kDriver_Name=\"$driverName\""

# Generate a new UUID
uuid=$(uuidgen)
awk '{sub(/e395c745-4eea-4d94-bb92-46224221047c/,"'$uuid'")}1' build/BlackHole.driver/Contents/Info.plist > Temp.plist
mv Temp.plist build/BlackHole.driver/Contents/Info.plist

mkdir -p Installer/root
driverBundleName=$driverVariantName.driver
mv build/BlackHole.driver Installer/root/$driverBundleName
rm -rf build

echo "Creating installer package..."

# Create package with pkgbuild
chmod 755 Installer/Scripts/preinstall
chmod 755 Installer/Scripts/postinstall

pkgbuild \
--root Installer/root \
--scripts Installer/Scripts \
--install-location /Library/Audio/Plug-Ins/HAL \
"Installer/$driverName.pkg"
rm -rf Installer/root

# Create installer with productbuild
cd Installer

echo "<?xml version=\"1.0\" encoding='utf-8'?>
<installer-gui-script minSpecVersion='2'>
<title>$driverName: Audio Termination Device ($ch) $version</title>
<welcome file='welcome.html'/>
<license file='../LICENSE'/>
<conclusion file='conclusion.html'/>
<domains enable_anywhere='false' enable_currentUserHome='false' enable_localSystem='true'/>
<pkg-ref id=\"$bundleID\"/>
<options customize='never' require-scripts='false' hostArchitectures='x86_64,arm64'/>
<volume-check>
<allowed-os-versions>
<os-version min='10.10'/>
</allowed-os-versions>
</volume-check>
<choices-outline>
<line choice=\"$bundleID\"/>
</choices-outline>
<choice id=\"$bundleID\" visible='true' title=\"$driverName $ch (Audio Termination)\" start_selected='true'>
<pkg-ref id=\"$bundleID\"/>
</choice>
<pkg-ref id=\"$bundleID\" version=\"$version\" onConclusion='RequireRestart'>$driverName.pkg</pkg-ref>
</installer-gui-script>" > distribution.xml

# Build
installerPkgName="$driverVariantName-$version.pkg"
productbuild \
--distribution distribution.xml \
--resources . \
--package-path $driverName.pkg $installerPkgName
rm distribution.xml
rm -f $driverName.pkg

echo "✅ BlackHole 0ch installer created: Installer/$installerPkgName"
echo ""
echo "📋 This installer creates an audio termination device (/dev/null for audio)"
echo " that appears as 'BlackHole 0ch' in Audio MIDI Setup"
echo ""
echo "⚠️ Note: This is an experimental feature for development and testing use"

cd ..
Loading