Search code examples
swiftmacoscocoaaudio

List all available audio devices


I want to list all available audio devices in swift to provide a selection for input and output. My application should listen on a audio channel and "write" to another. I do not want the system default!

let devices = AVCaptureDevice.devices(for: .audio)

print(devices.count)

for device in devices {
    print(device.localizedName)
}

The Code lists 0 devices. But I expect at least the internal output.

Some links to CoreAudio, AudioToolbox and AVFoundation that explain the audio source selection would be nice.


Solution

  • If you want something like a actionSheet and need to switch between audio devices seamlessly. Use this code.

    Code

    import Foundation
    import AVFoundation
    import UIKit
    
    @objc class AudioDeviceHandler: NSObject {
        
        @objc static let shared = AudioDeviceHandler()
        
        /// Present audio device selection alert
        /// - Parameters:
        ///   - presenterViewController: viewController where the alert need to present
        ///   - sourceView: alertController source view in case of iPad
        @objc func presentAudioOutput(_ presenterViewController : UIViewController, _ sourceView: UIView) {
            let speakerTitle = "Speaker"
            let headphoneTitle = "Headphones"
            let deviceTitle = (UIDevice.current.userInterfaceIdiom == .pad) ? "iPad" : "iPhone"
            let cancelTitle = "Cancel"
            
            var deviceAction = UIAlertAction()
            var headphonesExist = false
            let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
            
            guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {
                print("No inputs available ")
                return
            }
            
            for audioPort in availableInputs {
                switch audioPort.portType {
                case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE :
                    let bluetoothAction = UIAlertAction(title: audioPort.portName, style: .default) { _ in
                        self.setPreferredInput(port: audioPort)
                    }
                    
                    if isCurrentOutput(portType: audioPort.portType) {
                        bluetoothAction.setValue(true, forKey: "checked")
                    }
                    
                    optionMenu.addAction(bluetoothAction)
                    
                case .builtInMic, .builtInReceiver:
                    
                    deviceAction = UIAlertAction(title: deviceTitle, style: .default, handler: { _ in
                        self.setToDevice(port: audioPort)
                    })
                    
                case .headphones, .headsetMic:
                    headphonesExist = true
                    
                    let headphoneAction = UIAlertAction(title: headphoneTitle, style: .default) { _ in
                        self.setPreferredInput(port: audioPort)
                    }
                    
                    if isCurrentOutput(portType: .headphones) || isCurrentOutput(portType: .headsetMic) {
                        headphoneAction.setValue(true, forKey: "checked")
                    }
                    
                    optionMenu.addAction(headphoneAction)
                    
                case .carAudio:
                    let carAction = UIAlertAction(title: audioPort.portName, style: .default) { _ in
                        self.setPreferredInput(port: audioPort)
                    }
                    
                    if isCurrentOutput(portType: audioPort.portType) {
                        carAction.setValue(true, forKey: "checked")
                    }
                    optionMenu.addAction(carAction)
                    
                default:
                    break
                }
            }
            
            // device actions only required if no headphone available
            if !headphonesExist {
                if (isCurrentOutput(portType: .builtInReceiver) ||
                    isCurrentOutput(portType: .builtInMic)) {
                    deviceAction.setValue(true, forKey: "checked")
                }
                optionMenu.addAction(deviceAction)
            }
            
            // configure speaker action
            let speakerAction = UIAlertAction(title: speakerTitle, style: .default) { _ in
                self.setOutputToSpeaker()
            }
            if isCurrentOutput(portType: .builtInSpeaker) {
                speakerAction.setValue(true, forKey: "checked")
            }
            optionMenu.addAction(speakerAction)
            
            // configure cancel action
            let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
            optionMenu.addAction(cancelAction)
            
            optionMenu.modalPresentationStyle = .popover
            if let presenter = optionMenu.popoverPresentationController {
                presenter.sourceView = sourceView
                presenter.sourceRect = sourceView.bounds
            }
            
            presenterViewController.present(optionMenu, animated: true, completion: nil)
            
            // auto dismiss after 5 seconds
            DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
                optionMenu.dismiss(animated: true, completion: nil)
            }
        }
        
        @objc func setOutputToSpeaker() {
            do {
                try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
            } catch let error as NSError {
                print("audioSession error turning on speaker: \(error.localizedDescription)")
            }
        }
        
        fileprivate func setPreferredInput(port: AVAudioSessionPortDescription) {
            do {
                try AVAudioSession.sharedInstance().setPreferredInput(port)
            } catch let error as NSError {
                print("audioSession error change to input: \(port.portName) with error: \(error.localizedDescription)")
            }
        }
        
        fileprivate func setToDevice(port: AVAudioSessionPortDescription) {
            do {
                // remove speaker if needed
                try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
                // set new input
                try AVAudioSession.sharedInstance().setPreferredInput(port)
            } catch let error as NSError {
                print("audioSession error change to input: \(AVAudioSession.PortOverride.none.rawValue) with error: \(error.localizedDescription)")
            }
        }
        
        @objc func isCurrentOutput(portType: AVAudioSession.Port) -> Bool {
            AVAudioSession.sharedInstance().currentRoute.outputs.contains(where: { $0.portType == portType })
        }
        
    }
    

    How to use

    class ViewController: UIViewController {
    
        @IBOutlet weak var audioButton: UIButton!
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        }
    
        @IBAction func selectAudio(_ sender: Any) {
            // present audio device selection action sheet
            AudioDeviceHandler.shared.presentAudioOutput(self, audioButton)
        }
        
    }
    

    Result

    enter image description here