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).
|
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:
|
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:
|
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
|
||||||
|
|
||||||
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.
|
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)
|
# Tap all system audio (default)
|
||||||
./audiotee
|
./audiotee
|
||||||
|
|
||||||
# Tap everything *except* a specific process (by PID)
|
|
||||||
./audiotee --processes 1234
|
|
||||||
|
|
||||||
# Tap only a specific process (by PID)
|
# Tap only a specific process (by PID)
|
||||||
./audiotee --processes 1234 --no-exclusive
|
./audiotee --include-processes 1234
|
||||||
|
|
||||||
# Exclude multiple specific processes
|
|
||||||
./audiotee --processes 1234 5678 9012
|
|
||||||
|
|
||||||
# Tap multiple specific processes
|
# 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
|
```bash
|
||||||
# Mute processes being tapped (so they don't play through speakers)
|
# 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)
|
# Custom chunk duration (default 0.2 seconds, max 5.0)
|
||||||
./audiotee --chunk-duration 0.1
|
./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
|
1. Parse each line as JSON using the envelope structure
|
||||||
2. Use `metadata` message to understand the audio format
|
2. Use `metadata` message to understand the audio format
|
||||||
3. For `audio` messages, decode `audio_data` from base64 to get raw PCM data
|
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:**
|
**Binary format:**
|
||||||
|
|
||||||
1. Parse JSON metadata lines using the envelope structure
|
1. Parse JSON metadata lines using the envelope structure
|
||||||
2. Use `metadata` message to understand the audio format
|
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
|
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.
|
**Note**: binary is actually a mixed mode; JSON during boot, JSON packet header information preceding each binary chunk.
|
||||||
|
|
||||||
## Command Line options
|
## Command Line options
|
||||||
|
|
||||||
- `--format, -f`: Output format (`json`, `binary`, `auto`) [default: `auto`]
|
- `--format, -f`: Output format (`json`, `binary`, `auto`) [default: `auto`]
|
||||||
- `--processes`: Process IDs to tap (space-separated, empty = all processes)
|
- `--include-processes`: Process IDs to tap (space-separated, empty = all processes)
|
||||||
- `--mute`: Mute behavior (`unmuted`, `muted`) [default: `unmuted`]
|
- `--exclude-processes`: Process IDs to exclude (space-separated, empty = none)
|
||||||
- `--exclusive/--no-exclusive`: Use exclusive mode [default: `--exclusive`]
|
- `--mute`: Mute processes being tapped
|
||||||
- `--convert-to`: Convert to sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)
|
- `--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]
|
- `--chunk-duration`: Audio chunk duration in seconds [default: 0.2, max: 5.0]
|
||||||
|
|
||||||
|
|||||||
+39
-16
@@ -13,21 +13,21 @@ struct AudioTee: ParsableCommand {
|
|||||||
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
|
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
|
||||||
• auto: Automatically choose based on whether stdout is a terminal (default)
|
• auto: Automatically choose based on whether stdout is a terminal (default)
|
||||||
|
|
||||||
Tap configuration:
|
Process filtering:
|
||||||
• processes: List of process IDs to tap (empty = all processes)
|
• 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
|
• mute: How to handle processes being tapped
|
||||||
• exclusive: Whether to use exclusive mode
|
|
||||||
|
|
||||||
Examples:
|
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=json # Always use JSON format
|
||||||
audiotee --format=binary # Always use binary format
|
audiotee --format=binary # Always use binary format
|
||||||
audiotee --convert-to=16000 # Convert to 16kHz mono for ASR
|
audiotee --convert-to=16000 # Convert to 16kHz mono for ASR
|
||||||
audiotee --convert-to=8000 # Convert to 8kHz for telephony
|
audiotee --convert-to=8000 # Convert to 8kHz for telephony
|
||||||
audiotee --processes 1234 # Only tap process 1234
|
audiotee --include-processes 1234 # Only tap process 1234
|
||||||
audiotee --processes 1234 5678 9012 # Tap multiple processes
|
audiotee --include-processes 1234 5678 9012 # Tap only these processes
|
||||||
audiotee --mute=muted # Mute processes being tapped
|
audiotee --exclude-processes 1234 5678 # Tap everything except these
|
||||||
audiotee --no-exclusive # Don't use exclusive mode
|
audiotee --mute # Mute processes being tapped
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,14 +35,15 @@ struct AudioTee: ParsableCommand {
|
|||||||
var format: OutputFormat = .auto
|
var format: OutputFormat = .auto
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: .long, help: "Process IDs to tap (space-separated for multiple, empty = all processes)")
|
name: .long, help: "Process IDs to include (space-separated, empty = all processes)")
|
||||||
var processes: [Int32] = []
|
var includeProcesses: [Int32] = []
|
||||||
|
|
||||||
@Option(name: .long, help: "Mute behavior for tapped processes")
|
@Option(
|
||||||
var mute: TapMuteBehavior = .unmuted
|
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")
|
@Flag(name: .long, help: "Mute processes being tapped")
|
||||||
var exclusive: Bool = true
|
var mute: Bool = false
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: .long,
|
name: .long,
|
||||||
@@ -54,6 +55,12 @@ struct AudioTee: ParsableCommand {
|
|||||||
help: "Audio chunk duration in seconds (default: 0.2)")
|
help: "Audio chunk duration in seconds (default: 0.2)")
|
||||||
var chunkDuration: Double = 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 {
|
func run() throws {
|
||||||
setupSignalHandlers()
|
setupSignalHandlers()
|
||||||
|
|
||||||
@@ -68,10 +75,13 @@ struct AudioTee: ParsableCommand {
|
|||||||
throw ExitCode.failure
|
throw ExitCode.failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert include/exclude processes to TapConfiguration format
|
||||||
|
let (processes, isExclusive) = convertProcessFlags()
|
||||||
|
|
||||||
let tapConfig = TapConfiguration(
|
let tapConfig = TapConfiguration(
|
||||||
processes: processes,
|
processes: processes,
|
||||||
muteBehavior: mute,
|
muteBehavior: mute ? .muted : .unmuted,
|
||||||
isExclusive: exclusive
|
isExclusive: isExclusive
|
||||||
)
|
)
|
||||||
|
|
||||||
let audioTapManager = AudioTapManager()
|
let audioTapManager = AudioTapManager()
|
||||||
@@ -135,4 +145,17 @@ struct AudioTee: ParsableCommand {
|
|||||||
return AutoAudioOutputHandler()
|
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