vOOice/VoiceInk/Services/AudioDeviceConfiguration.swift
2025-02-22 11:52:41 +05:45

166 lines
6.2 KiB
Swift

import Foundation
import AVFoundation
import CoreAudio
import os
class AudioDeviceConfiguration {
private static let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "AudioDeviceConfiguration")
/// Configures audio session for a specific device
/// - Parameter deviceID: The ID of the audio device to configure
/// - Returns: A tuple containing the configured format and any error that occurred
static func configureAudioSession(with deviceID: AudioDeviceID) throws -> AudioStreamBasicDescription {
var propertySize = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
var streamFormat = AudioStreamBasicDescription()
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamFormat,
mScope: kAudioDevicePropertyScopeInput,
mElement: kAudioObjectPropertyElementMain
)
// First, ensure the device is ready
var isAlive: UInt32 = 0
var aliveSize = UInt32(MemoryLayout<UInt32>.size)
var aliveAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyDeviceIsAlive,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
let aliveStatus = AudioObjectGetPropertyData(
deviceID,
&aliveAddress,
0,
nil,
&aliveSize,
&isAlive
)
if aliveStatus != noErr || isAlive == 0 {
logger.error("Device \(deviceID) is not alive or ready")
throw AudioConfigurationError.failedToGetDeviceFormat(status: aliveStatus)
}
// Get the device format
let status = AudioObjectGetPropertyData(
deviceID,
&propertyAddress,
0,
nil,
&propertySize,
&streamFormat
)
if status != noErr {
logger.error("Failed to get device format: \(status)")
throw AudioConfigurationError.failedToGetDeviceFormat(status: status)
}
// Ensure we're using a standard PCM format
streamFormat.mFormatID = kAudioFormatLinearPCM
streamFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked
return streamFormat
}
/// Sets up an audio device for the audio unit
/// - Parameters:
/// - deviceID: The ID of the audio device
/// - audioUnit: The audio unit to configure
static func configureAudioUnit(_ audioUnit: AudioUnit, with deviceID: AudioDeviceID) throws {
var deviceIDCopy = deviceID
let propertySize = UInt32(MemoryLayout<AudioDeviceID>.size)
// First disable the audio unit
let resetStatus = AudioUnitReset(audioUnit, kAudioUnitScope_Global, 0)
if resetStatus != noErr {
logger.error("Failed to reset audio unit: \(resetStatus)")
}
logger.info("Configuring audio unit for device ID: \(deviceID)")
let setDeviceResult = AudioUnitSetProperty(
audioUnit,
kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global,
0,
&deviceIDCopy,
propertySize
)
if setDeviceResult != noErr {
logger.error("Failed to set audio unit device: \(setDeviceResult)")
logger.error("Device ID: \(deviceID)")
if let deviceName = AudioDeviceManager.shared.getDeviceName(deviceID: deviceID) {
logger.error("Failed device name: \(deviceName)")
}
throw AudioConfigurationError.failedToSetAudioUnitDevice(status: setDeviceResult)
}
logger.info("Successfully configured audio unit")
// Add a small delay to allow the device to settle
Thread.sleep(forTimeInterval: 0.1)
}
/// Sets the default input device for recording
/// - Parameter deviceID: The ID of the audio device
static func setDefaultInputDevice(_ deviceID: AudioDeviceID) throws {
var deviceIDCopy = deviceID
let propertySize = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
let setDeviceResult = AudioObjectSetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
propertySize,
&deviceIDCopy
)
if setDeviceResult != noErr {
logger.error("Failed to set input device: \(setDeviceResult)")
throw AudioConfigurationError.failedToSetInputDevice(status: setDeviceResult)
}
}
/// Creates a device change observer
/// - Parameters:
/// - handler: The closure to execute when device changes
/// - queue: The queue to execute the handler on (defaults to main queue)
/// - Returns: The observer token
static func createDeviceChangeObserver(
handler: @escaping () -> Void,
queue: OperationQueue = .main
) -> NSObjectProtocol {
return NotificationCenter.default.addObserver(
forName: NSNotification.Name("AudioDeviceChanged"),
object: nil,
queue: queue,
using: { _ in handler() }
)
}
}
enum AudioConfigurationError: LocalizedError {
case failedToGetDeviceFormat(status: OSStatus)
case failedToSetAudioUnitDevice(status: OSStatus)
case failedToSetInputDevice(status: OSStatus)
case failedToGetAudioUnit
var errorDescription: String? {
switch self {
case .failedToGetDeviceFormat(let status):
return "Failed to get device format: \(status)"
case .failedToSetAudioUnitDevice(let status):
return "Failed to set audio unit device: \(status)"
case .failedToSetInputDevice(let status):
return "Failed to set input device: \(status)"
case .failedToGetAudioUnit:
return "Failed to get audio unit from input node"
}
}
}