From 0f550157ae57697b8d6d85f3452575a592afb81f Mon Sep 17 00:00:00 2001 From: Nick Payne Date: Tue, 17 Jun 2025 16:48:45 +0100 Subject: [PATCH] more logical CLI options --- .cursorrules | 2 ++ README.md | 30 +++++++++--------- Sources/CLI/AudioTee.swift | 63 ++++++++++++++++++++++++++------------ 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/.cursorrules b/.cursorrules index 1520027..5afb763 100644 --- a/.cursorrules +++ b/.cursorrules @@ -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: diff --git a/README.md b/README.md index 0865d16..7f19cad 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/Sources/CLI/AudioTee.swift b/Sources/CLI/AudioTee.swift index cd41ecc..3dd4857 100644 --- a/Sources/CLI/AudioTee.swift +++ b/Sources/CLI/AudioTee.swift @@ -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) + } + } }