Search code examples
iosswiftavaudiosessionavaudioengine

Route live audio from line-in to AirPods?


Is there a way to route the audio coming from the wired line-in input directly to AirPods?

enter image description here

Currently, I'm creating a .playAndRecord audio session. Paired the AirPods. Later on, with AVAudioEngine I connect the input device directly to the output device.

engine.connect(
    engine.inputNode,
    to: engine.outputNode,
    format: engine.inputNode.inputFormat(forBus: 0)
)

Works nicely, I can hear the live (!) sound from the AirPods microphone directly in my ears. 🎉 But then I connect the line-in cable, unfortunately, it overrides the output as well to the line-in (I can read the metering levels nicely, though). If I connect the AirPods, then again, both the input and the output change to AirPods.

Is there a way to reroute the output device to AirPods, but keep the input device unchanged?


UPDATE: I tried set the input port (to line-in) manually using setPreferredInput, but as soon as the input is selected, the output is also get adjusted (to the wired device).

UPDATE: I tried routing both with AVRoutePickerView and MPVolumeView, but all yielded the same result. If I pick a new output, the input gets modified as well.

UPDATE: AudioBus app does this, I can set wired headphones as input, and AirPods as output, works like a charm. So it is definitely possible, I just have no idea what API to use. Actually, neither AudioBus can share the audio to two sets of AirPods.


Solution

  • Yes! 🎉 However, it only works if the audio session is created with the allowBluetoothA2DP option (according to @RobNapier above). It is an output-only profile, therefore it does not route input from the Bluetooth devices at all.

    A2DP is a stereo, output-only profile intended for higher bandwidth audio use cases, such as music playback. The system automatically routes to A2DP ports if you configure an app’s audio session to use the ambient, soloAmbient, or playback categories.

    Starting with iOS 10.0, apps using the playAndRecord category may also allow routing output to paired Bluetooth A2DP devices. To enable this behavior, pass this category option when setting your audio session’s category.

    You can make it work with a bare-bones sound manager something like below.

    class Sound {
        
        ...
    
        private var session: AVAudioSession {
            AVAudioSession.sharedInstance()
        }
        
        private let engine = AVAudioEngine()
        
        init() {
            setupAudioSession()
        }
        
        func setupAudioSession() {
            do {
                try session.setCategory(
                    .playAndRecord,
                    options: [
                        .allowBluetoothA2DP, // 🔑
                        .allowAirPlay
                    ]
                )
                try session.setActive(true)
            } catch {
                print("Could not configure and activate session. \(error)")
            }
        }
        
        func start() {
            engine.connect(
                engine.inputNode,
                to: engine.outputNode,
                format: engine.inputNode.inputFormat(forBus: 0)
            )           
            do {
                try engine.start()
            } catch {
                print("Could not start engine. \(error)")
            }
        }
    
        ...
    }
    

    If you put a MPVolumeView somewhere on the UI, then you can explicitly select the route to bluetooth devices (and more). But I guess even without that you can change the route by simply physically connect/disconnect the devices themselves.