AudioTee Swift CLI repo

This commit is contained in:
Nick Payne
2025-06-11 15:33:40 +01:00
commit 4a8bc657e3
26 changed files with 1559 additions and 0 deletions
+49
View File
@@ -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)
}
}
+97
View File
@@ -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)
}
}
}