only write PCM to stdout

This commit is contained in:
Nick Payne
2025-07-27 09:53:26 +01:00
parent 98e33b7bcb
commit abaa019bd2
11 changed files with 56 additions and 292 deletions
-5
View File
@@ -189,11 +189,6 @@ class SimpleArgumentParser {
throw ArgumentParserError.invalidValue(optionName, value)
}
return doubleValue as! T
} else if type == OutputFormat.self {
guard let format = OutputFormat(rawValue: value) else {
throw ArgumentParserError.invalidValue(optionName, value)
}
return format as! T
}
throw ArgumentParserError.invalidValue(optionName, value)
+1 -23
View File
@@ -2,7 +2,6 @@ import CoreAudio
import Foundation
struct AudioTee {
var format: OutputFormat = .auto
var includeProcesses: [Int32] = []
var excludeProcesses: [Int32] = []
var mute: Bool = false
@@ -18,11 +17,6 @@ struct AudioTee {
discussion: """
AudioTee captures system audio using Core Audio taps and streams it as structured output.
Output formats:
• json: Base64-encoded audio in JSON messages (safe for terminals)
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
• auto: Automatically choose based on whether stdout is a terminal (default)
Process filtering:
• include-processes: Only tap specified process IDs (empty = all processes)
• exclude-processes: Tap all processes except specified ones
@@ -30,8 +24,6 @@ struct AudioTee {
Examples:
audiotee # Auto format, tap all processes
audiotee --format=json # Always use JSON format
audiotee --format=binary # Always use binary format
audiotee --sample-rate=16000 # Convert to 16kHz mono for ASR
audiotee --sample-rate=8000 # Convert to 8kHz for telephony
audiotee --include-processes 1234 # Only tap process 1234
@@ -42,7 +34,6 @@ struct AudioTee {
)
// Configure arguments
parser.addOption(name: "format", shortName: "f", help: "Output format", defaultValue: "auto")
parser.addArrayOption(
name: "include-processes",
help: "Process IDs to include (space-separated, empty = all processes)")
@@ -62,7 +53,6 @@ struct AudioTee {
var audioTee = AudioTee()
// Extract values
audioTee.format = try parser.getValue("format", as: OutputFormat.self)
audioTee.includeProcesses = try parser.getArrayValue("include-processes", as: Int32.self)
audioTee.excludeProcesses = try parser.getArrayValue("exclude-processes", as: Int32.self)
audioTee.mute = parser.getFlag("mute")
@@ -102,7 +92,6 @@ struct AudioTee {
setupSignalHandlers()
Logger.info("Starting AudioTee...")
Logger.debug("Using output format: \(format)")
// Validate chunk duration
guard chunkDuration > 0 && chunkDuration <= 5.0 else {
@@ -143,7 +132,7 @@ struct AudioTee {
throw ExitCode.failure
}
let outputHandler = createOutputHandler(for: format)
let outputHandler = BinaryAudioOutputHandler()
let recorder = AudioRecorder(
deviceID: deviceID, outputHandler: outputHandler, convertToSampleRate: sampleRate,
chunkDuration: chunkDuration)
@@ -172,17 +161,6 @@ struct AudioTee {
}
}
private func createOutputHandler(for format: OutputFormat) -> AudioOutputHandler {
switch format {
case .json:
return JSONAudioOutputHandler()
case .binary:
return BinaryAudioOutputHandler()
case .auto:
return AutoAudioOutputHandler()
}
}
private func convertProcessFlags() -> ([Int32], Bool) {
if !includeProcesses.isEmpty {
// Include specific processes only
-16
View File
@@ -1,16 +0,0 @@
enum OutputFormat: String, CaseIterable {
case json = "json"
case binary = "binary"
case auto = "auto"
var description: String {
switch self {
case .json:
return "Base64-encoded JSON (terminal-safe)"
case .binary:
return "Binary with JSON headers (pipe-optimised)"
case .auto:
return "Auto-detect based on TTY (default)"
}
}
}
-1
View File
@@ -119,7 +119,6 @@ public class AudioFormatConverter {
return AudioPacket(
timestamp: packet.timestamp,
duration: packet.duration,
peakAmplitude: packet.peakAmplitude,
rawAudioData: outputData
)
}
-3
View File
@@ -3,18 +3,15 @@ import Foundation
public struct AudioPacket {
public let timestamp: Date
public let duration: Double
public let peakAmplitude: Float // useful for level monitoring
public let rawAudioData: Data
public init(
timestamp: Date,
duration: Double,
peakAmplitude: Float,
rawAudioData: Data
) {
self.timestamp = timestamp
self.duration = duration
self.peakAmplitude = peakAmplitude
self.rawAudioData = rawAudioData
}
}
@@ -1,31 +0,0 @@
import Foundation
/// Auto-detecting output handler based on TTY
public class AutoAudioOutputHandler: AudioOutputHandler {
private let handler: AudioOutputHandler
public init() {
// Auto-detect based on whether stdout is a terminal
if isatty(STDOUT_FILENO) != 0 {
handler = JSONAudioOutputHandler()
} else {
handler = BinaryAudioOutputHandler()
}
}
public func handleAudioPacket(_ packet: AudioPacket) {
handler.handleAudioPacket(packet)
}
public func handleMetadata(_ metadata: AudioStreamMetadata) {
handler.handleMetadata(metadata)
}
public func handleStreamStart() {
handler.handleStreamStart()
}
public func handleStreamStop() {
handler.handleStreamStop()
}
}
@@ -5,12 +5,6 @@ public class BinaryAudioOutputHandler: AudioOutputHandler {
public init() {}
public func handleAudioPacket(_ packet: AudioPacket) {
// Create metadata without the audio data
let metadata = BinaryPacketHeader(from: packet)
// Write JSON metadata line
Logger.writeMessage(.audio, data: metadata)
// Write raw binary audio data directly to stdout
FileHandle.standardOutput.write(packet.rawAudioData)
}
@@ -1,23 +0,0 @@
import Foundation
/// Base64-encoded JSON output (terminal-safe)
public class JSONAudioOutputHandler: AudioOutputHandler {
public init() {}
public func handleAudioPacket(_ packet: AudioPacket) {
let jsonPacket = JSONAudioPacket(from: packet)
Logger.writeMessage(.audio, data: jsonPacket)
}
public func handleMetadata(_ metadata: AudioStreamMetadata) {
Logger.writeMessage(.metadata, data: metadata)
}
public func handleStreamStart() {
Logger.writeMessage(.streamStart, data: Optional<String>.none)
}
public func handleStreamStop() {
Logger.writeMessage(.streamStop, data: Optional<String>.none)
}
}
-45
View File
@@ -1,45 +0,0 @@
import Foundation
/// JSON-serializable version of AudioPacket with base64-encoded audio data
public struct JSONAudioPacket: Codable {
public let timestamp: Date
public let duration: Double
public let peakAmplitude: Float
public let audioData: String // base64 encoded audio data
public enum CodingKeys: String, CodingKey {
case timestamp
case duration
case peakAmplitude = "peak_amplitude"
case audioData = "audio_data"
}
public init(from packet: AudioPacket) {
self.timestamp = packet.timestamp
self.duration = packet.duration
self.peakAmplitude = packet.peakAmplitude
self.audioData = packet.rawAudioData.base64EncodedString()
}
}
/// Metadata-only packet for binary output (without base64 audio data)
public struct BinaryPacketHeader: Codable {
public let timestamp: Date
public let duration: Double
public let peakAmplitude: Float
public let audioLength: Int // Length of raw audio data in bytes
public enum CodingKeys: String, CodingKey {
case timestamp
case duration
case peakAmplitude = "peak_amplitude"
case audioLength = "audio_length"
}
public init(from packet: AudioPacket) {
self.timestamp = packet.timestamp
self.duration = packet.duration
self.peakAmplitude = packet.peakAmplitude
self.audioLength = packet.rawAudioData.count
}
}
+2 -2
View File
@@ -24,8 +24,8 @@ public class Logger {
let message = Message(type: type, data: data)
do {
let jsonData = try jsonEncoder.encode(message)
FileHandle.standardOutput.write(jsonData)
FileHandle.standardOutput.write("\n".data(using: .utf8)!)
FileHandle.standardError.write(jsonData)
FileHandle.standardError.write("\n".data(using: .utf8)!)
} catch {
// TODO: handle at some point
}