more logical CLI options

This commit is contained in:
Nick Payne
2025-06-17 16:48:45 +01:00
parent 212191648c
commit 0f550157ae
3 changed files with 60 additions and 35 deletions
+2
View File
@@ -2,6 +2,8 @@ This project is called AudioTee: it is a Swift CLI executable which allows the u
It is designed to be executed as a child process by a host program which can stream its stdout. The original intended use case was to send system audio to a Streaming ASR service.g. AssemblyAI, Speechmatics, etc).
When making code changes, please ensure README.md is kept up-to-date, if relevant.
Some guidance on the Core Audio tap API from Apple:
You create a tap by passing a CATapDescription to AudioHardwareCreateProcessTap. This returns an AudioObjectID for the new tap object. You can destroy a tap using AudioHardwareDestroyProcessTap:
+15 -15
View File
@@ -1,6 +1,6 @@
# AudioTee
AudioTee captures your Mac's system audio output as PCM audio data and writes it to `stdout`, either in base64-encoded JSON (good for humans, easy on terminals) or binary (good for other programs). It uses the [Core Audio taps](https://developer.apple.com/documentation/coreaudio/capturing-system-audio-with-core-audio-taps) API introduced in macOS 14.2 (released in December 2023). You can do whatever you want with this audio - stream it somewhere else, save it to disk, visualize it, etc.
AudioTee captures your Mac's system audio output and writes PCM encoded chunks of it to `stdout` at regular intervals, either in base64-encoded JSON (good for humans, easy on terminals) or binary (good for other programs). It uses the [Core Audio taps](https://developer.apple.com/documentation/coreaudio/capturing-system-audio-with-core-audio-taps) API introduced in macOS 14.2 (released in December 2023). You can do whatever you want with this audio - stream it somewhere else, save it to disk, visualize it, etc.
By default, it taps the audio output from **all** running process and selects the most appropriate audio chunk output format to use based on the presence of a tty. Tap output is forced to `mono` (not configurable) and preserves your output device's sample rate unless you pass a `--convert-to` flag. Only the default output device is currently supported.
@@ -64,22 +64,22 @@ For now, only a subset of the `CATapDescription` (https://developer.apple.com/do
# Tap all system audio (default)
./audiotee
# Tap everything *except* a specific process (by PID)
./audiotee --processes 1234
# Tap only a specific process (by PID)
./audiotee --processes 1234 --no-exclusive
# Exclude multiple specific processes
./audiotee --processes 1234 5678 9012
./audiotee --include-processes 1234
# Tap multiple specific processes
./audiotee --processes 1234 5678 9012 --no-exclusive
./audiotee --include-processes 1234 5678 9012
# Tap everything *except* a specific process (by PID)
./audiotee --exclude-processes 1234
# Exclude multiple specific processes
./audiotee --exclude-processes 1234 5678 9012
```
```bash
# Mute processes being tapped (so they don't play through speakers)
./audiotee --mute muted
./audiotee --mute
# Custom chunk duration (default 0.2 seconds, max 5.0)
./audiotee --chunk-duration 0.1
@@ -212,23 +212,23 @@ Info, error, and debug messages (useful for monitoring):
1. Parse each line as JSON using the envelope structure
2. Use `metadata` message to understand the audio format
3. For `audio` messages, decode `audio_data` from base64 to get raw PCM data
4. Do something with each chuunk of data
4. Do something with each chunk of data
**Binary format:**
1. Parse JSON metadata lines using the envelope structure
2. Use `metadata` message to understand the audio format
3. For `audio` messages, read `audio_length` bytes of raw binary data after the JSON line
4. Do something with each chuunk of data
4. Do something with each chunk of data
**Note**: binary is actually a mixed mode; JSON during boot, JSON packet header information preceding each binary chunk.
## Command Line options
- `--format, -f`: Output format (`json`, `binary`, `auto`) [default: `auto`]
- `--processes`: Process IDs to tap (space-separated, empty = all processes)
- `--mute`: Mute behavior (`unmuted`, `muted`) [default: `unmuted`]
- `--exclusive/--no-exclusive`: Use exclusive mode [default: `--exclusive`]
- `--include-processes`: Process IDs to tap (space-separated, empty = all processes)
- `--exclude-processes`: Process IDs to exclude (space-separated, empty = none)
- `--mute`: Mute processes being tapped
- `--convert-to`: Convert to sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)
- `--chunk-duration`: Audio chunk duration in seconds [default: 0.2, max: 5.0]
+39 -16
View File
@@ -13,21 +13,21 @@ struct AudioTee: ParsableCommand {
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
• auto: Automatically choose based on whether stdout is a terminal (default)
Tap configuration:
• processes: List of process IDs to tap (empty = all processes)
Process filtering:
include-processes: Only tap specified process IDs (empty = all processes)
• exclude-processes: Tap all processes except specified ones
• mute: How to handle processes being tapped
• exclusive: Whether to use exclusive mode
Examples:
audiotee # Auto format (JSON in terminal, binary when piped)
audiotee # Auto format, tap all processes
audiotee --format=json # Always use JSON format
audiotee --format=binary # Always use binary format
audiotee --convert-to=16000 # Convert to 16kHz mono for ASR
audiotee --convert-to=8000 # Convert to 8kHz for telephony
audiotee --processes 1234 # Only tap process 1234
audiotee --processes 1234 5678 9012 # Tap multiple processes
audiotee --mute=muted # Mute processes being tapped
audiotee --no-exclusive # Don't use exclusive mode
audiotee --include-processes 1234 # Only tap process 1234
audiotee --include-processes 1234 5678 9012 # Tap only these processes
audiotee --exclude-processes 1234 5678 # Tap everything except these
audiotee --mute # Mute processes being tapped
"""
)
@@ -35,14 +35,15 @@ struct AudioTee: ParsableCommand {
var format: OutputFormat = .auto
@Option(
name: .long, help: "Process IDs to tap (space-separated for multiple, empty = all processes)")
var processes: [Int32] = []
name: .long, help: "Process IDs to include (space-separated, empty = all processes)")
var includeProcesses: [Int32] = []
@Option(name: .long, help: "Mute behavior for tapped processes")
var mute: TapMuteBehavior = .unmuted
@Option(
name: .long, help: "Process IDs to exclude (space-separated)")
var excludeProcesses: [Int32] = []
@Flag(name: .long, inversion: .prefixedNo, help: "Use exclusive mode to capture all processes")
var exclusive: Bool = true
@Flag(name: .long, help: "Mute processes being tapped")
var mute: Bool = false
@Option(
name: .long,
@@ -54,6 +55,12 @@ struct AudioTee: ParsableCommand {
help: "Audio chunk duration in seconds (default: 0.2)")
var chunkDuration: Double = 0.2
func validate() throws {
if !includeProcesses.isEmpty && !excludeProcesses.isEmpty {
throw ValidationError("Cannot specify both --include-processes and --exclude-processes")
}
}
func run() throws {
setupSignalHandlers()
@@ -68,10 +75,13 @@ struct AudioTee: ParsableCommand {
throw ExitCode.failure
}
// Convert include/exclude processes to TapConfiguration format
let (processes, isExclusive) = convertProcessFlags()
let tapConfig = TapConfiguration(
processes: processes,
muteBehavior: mute,
isExclusive: exclusive
muteBehavior: mute ? .muted : .unmuted,
isExclusive: isExclusive
)
let audioTapManager = AudioTapManager()
@@ -135,4 +145,17 @@ struct AudioTee: ParsableCommand {
return AutoAudioOutputHandler()
}
}
private func convertProcessFlags() -> ([Int32], Bool) {
if !includeProcesses.isEmpty {
// Include specific processes only
return (includeProcesses, false)
} else if !excludeProcesses.isEmpty {
// Exclude specific processes (tap everything except these)
return (excludeProcesses, true)
} else {
// Default: tap everything
return ([], true)
}
}
}