only write PCM to stdout
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,6 @@ public class AudioFormatConverter {
|
||||
return AudioPacket(
|
||||
timestamp: packet.timestamp,
|
||||
duration: packet.duration,
|
||||
peakAmplitude: packet.peakAmplitude,
|
||||
rawAudioData: outputData
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user