AudioTee Swift CLI repo
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Protocol for handling audio output in different formats
|
||||
public protocol AudioOutputHandler {
|
||||
func handleAudioPacket(_ packet: AudioPacket)
|
||||
func handleMetadata(_ metadata: AudioStreamMetadata)
|
||||
func handleStreamStart()
|
||||
func handleStreamStop()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
/// Binary output with JSON headers (pipe-optimised)
|
||||
public class BinaryAudioOutputHandler: AudioOutputHandler {
|
||||
public init() {}
|
||||
|
||||
public func handleAudioPacket(_ packet: AudioPacket) {
|
||||
// Create metadata without the audio data
|
||||
let metadata = AudioPacketMetadata(from: packet)
|
||||
|
||||
// Write JSON metadata line
|
||||
Logger.writeMessage(.audio, data: metadata)
|
||||
|
||||
// Write raw binary audio data directly to stdout
|
||||
FileHandle.standardOutput.write(packet.rawAudioData)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
|
||||
// Unified message types for all AudioTee output
|
||||
public enum MessageType: String, Codable {
|
||||
// Stream lifecycle
|
||||
case metadata
|
||||
case streamStart = "stream_start"
|
||||
case streamStop = "stream_stop"
|
||||
|
||||
// Audio data
|
||||
case audio
|
||||
|
||||
// Logging
|
||||
case info
|
||||
case error
|
||||
case debug
|
||||
}
|
||||
|
||||
// Base message envelope that wraps all outputs
|
||||
public struct Message<T: Codable>: Codable {
|
||||
public let timestamp: Date
|
||||
public let type: MessageType
|
||||
public let data: T?
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case timestamp
|
||||
case type = "message_type"
|
||||
case data
|
||||
}
|
||||
|
||||
public init(type: MessageType, data: T? = nil) {
|
||||
self.timestamp = Date()
|
||||
self.type = type
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
// Simple log data for logging messages
|
||||
public struct LogData: Codable {
|
||||
public let message: String
|
||||
public let context: [String: String]?
|
||||
|
||||
public init(message: String, context: [String: String]? = nil) {
|
||||
self.message = message
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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 AudioPacketMetadata: 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user