|
class AudioSessionManager: NSObject, ObservableObject { |
|
@Published var microphones: [AVCaptureDevice] = [] |
|
var captureSession: AVCaptureSession = .init() |
|
var audioOutput: AVCaptureAudioDataOutput? |
|
var configured: Bool = false |
|
|
|
private var audioInput: AVCaptureDeviceInput? |
|
|
|
let dataOutputQueue = DispatchQueue(label: "audio_queue", |
|
qos: .userInteractive, |
|
attributes: [], |
|
autoreleaseFrequency: .workItem) |
|
|
|
override init() { |
|
super.init() |
|
microphones = listMicrophones() |
|
} |
|
|
|
func configure(activeMicrophoneName: String?) { |
|
configureAudioSession(activeMicrophoneName ?? nil) { isSuccessful in |
|
if isSuccessful { |
|
self.configured = true |
|
} else { |
|
self.configured = false |
|
} |
|
} |
|
} |
|
|
|
deinit { |
|
if captureSession.isRunning { |
|
captureSession.stopRunning() |
|
} |
|
for input in captureSession.inputs { |
|
captureSession.removeInput(input) |
|
} |
|
for output in captureSession.outputs { |
|
captureSession.removeOutput(output) |
|
} |
|
audioOutput = nil |
|
logger.info("Audio Capture Session deinit") |
|
} |
|
|
|
func checkAudioAuthorization(completion: @escaping (Bool) -> Void) { |
|
switch AVCaptureDevice.authorizationStatus(for: .audio) { |
|
case .authorized: |
|
completion(true) |
|
case .notDetermined: |
|
AVCaptureDevice.requestAccess(for: .audio) { granted in |
|
completion(granted) |
|
} |
|
default: |
|
logger.error("App requires audio access") |
|
completion(false) |
|
} |
|
} |
|
|
|
func configureAudioSession(_ activeMicrophoneName: String?, completion: @escaping (Bool) -> Void) { |
|
checkAudioAuthorization { isAuthorized in |
|
guard isAuthorized else { |
|
completion(false) |
|
return |
|
} |
|
self.captureSession.beginConfiguration() |
|
|
|
var microphone: AVCaptureDevice |
|
|
|
if let activeDevice = self.microphones.first(where: { $0.localizedName == activeMicrophoneName}) { |
|
microphone = activeDevice |
|
} else { |
|
guard let audioDevice = self.microphones.first else { |
|
completion(false) |
|
return |
|
} |
|
microphone = audioDevice |
|
} |
|
|
|
// Add audio input |
|
do { |
|
self.audioInput = try AVCaptureDeviceInput(device: microphone) |
|
if let audioInput = self.audioInput { |
|
self.captureSession.addInput(audioInput) |
|
} |
|
} catch { |
|
completion(false) |
|
logger.error("Error configuring audio input: \(error)") |
|
} |
|
|
|
// Add audio output |
|
self.audioOutput = AVCaptureAudioDataOutput() |
|
if let audioOutput = self.audioOutput { |
|
if self.captureSession.canAddOutput(audioOutput) { |
|
self.captureSession.addOutput(audioOutput) |
|
|
|
let audioConnection = audioOutput.connection(with: .audio) |
|
audioConnection?.audioChannels.forEach { channel in |
|
channel.isEnabled = true |
|
} |
|
|
|
// Audio settings configured to be compatible with whisper.cpp |
|
let audioSettings: [String: Any] = [ |
|
AVFormatIDKey: kAudioFormatLinearPCM, |
|
AVSampleRateKey: 16000, // Set sample rate to 16kHz |
|
AVNumberOfChannelsKey: 1 // Mono |
|
] |
|
|
|
audioOutput.audioSettings = audioSettings |
|
} |
|
} |
|
|
|
self.captureSession.commitConfiguration() |
|
completion(true) |
|
} |
|
} |
|
|
|
func listMicrophones() -> [AVCaptureDevice] { |
|
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInMicrophone, .externalUnknown], mediaType: .audio, position: .unspecified) |
|
return discoverySession.devices |
|
} |
|
|
|
func changeDevice(selectedDevice: String) { |
|
if let name = microphones.first?.localizedName, selectedDevice == "" { |
|
switchMicrophone(to: name) |
|
} else { |
|
switchMicrophone(to: selectedDevice) |
|
} |
|
} |
|
|
|
func switchMicrophone(to name: String) { |
|
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInMicrophone, .externalUnknown], mediaType: .audio, position: .unspecified) |
|
|
|
if let device = discoverySession.devices.first(where: { $0.localizedName == name }) { |
|
if let existingInput = audioInput { |
|
captureSession.removeInput(existingInput) |
|
} |
|
|
|
do { |
|
audioInput = try AVCaptureDeviceInput(device: device) |
|
if let newInput = audioInput { |
|
captureSession.addInput(newInput) |
|
} |
|
} catch { |
|
print("Error switching microphone: \(error)") |
|
} |
|
} |
|
} |
|
|
|
func startCapture() { |
|
captureSession.startRunning() |
|
} |
|
|
|
func stopCapture() { |
|
captureSession.stopRunning() |
|
} |
|
} |
|
|
Hi @cgfarmer4, thanks for implementing this! Would you mind adding a lil license to this Gist? Thanks!