more logical CLI options
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
+43
-20
@@ -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 --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 # 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 --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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user