AudioTee Swift CLI repo
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
|
||||
public class Logger {
|
||||
nonisolated(unsafe) private static let dateFormatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [
|
||||
.withInternetDateTime,
|
||||
.withFractionalSeconds,
|
||||
]
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private static let jsonEncoder: JSONEncoder = {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .custom { date, encoder in
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(dateFormatter.string(from: date))
|
||||
}
|
||||
return encoder
|
||||
}()
|
||||
|
||||
// Write any message with the unified envelope
|
||||
public static func writeMessage<T: Codable>(_ type: MessageType, data: T? = nil) {
|
||||
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)!)
|
||||
} catch {
|
||||
// TODO: handle at some point
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for different message types
|
||||
public static func info(_ message: String, context: [String: String]? = nil) {
|
||||
let logData = LogData(message: message, context: context)
|
||||
writeMessage(.info, data: logData)
|
||||
}
|
||||
|
||||
public static func error(_ message: String, context: [String: String]? = nil) {
|
||||
let logData = LogData(message: message, context: context)
|
||||
writeMessage(.error, data: logData)
|
||||
}
|
||||
|
||||
public static func debug(_ message: String, context: [String: String]? = nil) {
|
||||
let logData = LogData(message: message, context: context)
|
||||
writeMessage(.debug, data: logData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import AVFoundation
|
||||
import AudioToolbox
|
||||
import CoreAudio
|
||||
import Foundation
|
||||
|
||||
// MARK: - Audio Device Utilities
|
||||
|
||||
/// Checks if an audio device is valid and alive
|
||||
func isAudioDeviceValid(_ deviceID: AudioObjectID) -> Bool {
|
||||
var address = getPropertyAddress(selector: kAudioDevicePropertyDeviceIsAlive)
|
||||
|
||||
var isAlive: UInt32 = 0
|
||||
var size = UInt32(MemoryLayout<UInt32>.size)
|
||||
let status = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &isAlive)
|
||||
|
||||
let valid = status == kAudioHardwareNoError && isAlive == 1
|
||||
|
||||
Logger.debug(
|
||||
"Checked device validity",
|
||||
context: [
|
||||
"device_id": String(deviceID),
|
||||
"status": String(status),
|
||||
"is_alive": String(isAlive),
|
||||
"valid": String(valid),
|
||||
])
|
||||
return valid
|
||||
}
|
||||
|
||||
/// Creates an AudioObjectPropertyAddress with the given selector and optional scope/element
|
||||
func getPropertyAddress(
|
||||
selector: AudioObjectPropertySelector,
|
||||
scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal,
|
||||
element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain
|
||||
) -> AudioObjectPropertyAddress {
|
||||
return AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: element)
|
||||
}
|
||||
|
||||
/// Translates an array of process IDs to AudioObjectIDs using Core Audio
|
||||
/// Returns an array of AudioObjectIDs for valid processes
|
||||
/// Throws an error if any PIDs cannot be translated
|
||||
func translatePIDsToProcessObjects(_ pids: [Int32]) throws -> [AudioObjectID] {
|
||||
guard !pids.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
var processObjects: [AudioObjectID] = []
|
||||
var failedPIDs: [Int32] = []
|
||||
|
||||
for pid in pids {
|
||||
var address = getPropertyAddress(selector: kAudioHardwarePropertyTranslatePIDToProcessObject)
|
||||
var processObject: AudioObjectID = 0
|
||||
var size = UInt32(MemoryLayout<AudioObjectID>.size)
|
||||
var mutablePid = pid // Create mutable copy for the API call
|
||||
|
||||
let status = AudioObjectGetPropertyData(
|
||||
AudioObjectID(kAudioObjectSystemObject),
|
||||
&address,
|
||||
UInt32(MemoryLayout<pid_t>.size),
|
||||
&mutablePid,
|
||||
&size,
|
||||
&processObject
|
||||
)
|
||||
|
||||
if status == kAudioHardwareNoError && processObject != kAudioObjectUnknown {
|
||||
processObjects.append(processObject)
|
||||
Logger.debug(
|
||||
"Translated PID to process object",
|
||||
context: [
|
||||
"pid": String(pid),
|
||||
"process_object": String(processObject),
|
||||
])
|
||||
} else {
|
||||
failedPIDs.append(pid)
|
||||
Logger.debug(
|
||||
"Failed to translate PID to process object",
|
||||
context: [
|
||||
"pid": String(pid),
|
||||
"status": String(status),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// Throw error if any PIDs failed to translate
|
||||
if !failedPIDs.isEmpty {
|
||||
throw AudioTeeError.pidTranslationFailed(failedPIDs)
|
||||
}
|
||||
|
||||
return processObjects
|
||||
}
|
||||
|
||||
extension String {
|
||||
func print(to fileHandle: FileHandle) {
|
||||
if let data = (self + "\n").data(using: .utf8) {
|
||||
fileHandle.write(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user