Issue with AVAudioSession route in iOS 16 - input is always MicrophoneBuiltIn. setPreferredInput method doesn't work.

What this is about:

I have an iOS "Guitar Effect" app that gets audio signal from input, process it and plays the result audio back to user via output. The app dosn't work with BuiltIn microphone of iOS device (because of feedback) - users have to connect guitar via special device: either analog like iRig or digital like iRig HD.

Starting from iOS 16 I face a weird behaviour of the AVAudioSession that breaks my app. In iOS 16 the input of the AVAudioSession Route is always MicrophoneBuiltIn - no matter if I connect any external microphones like iRig device or headphones with microphone. Even if I try to manually switch to external microphone by assigning the preferredInput for AVAudioSession it doesn't change the route - input is always MicrophoneBuiltIn. In iOS 15 and earlier iOS automatically change the input of the route to any external microphone you attach to the iOS device. And you may control the input by assigning preferredInput property for AVAudioSession.

This is an smallest example project to reproduce the issue.

Project Structure:

This is a very small project created to reproduce the issue. All the code is in ViewController class.

  1. I create a playAndRecord AVAudioSession and subscribe for routeChangeNotification notification:
NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: nil)
let audioSession = AVAudioSession.sharedInstance()
do {
  try audioSession.setCategory(AVAudioSession.Category.playAndRecord, options: .mixWithOthers)
  try audioSession.setActive(true, options: [])
} catch {
  print("AVAudioSession init error: \(error)")
}
  1. When I get a notification - I print the list of available audio inputs, preferred input and current audio route:
@objc func handleRouteChange(notification: Notification) {
  print("\nHANDLE ROUTE CHANGE")
  print("AVAILABLE INPUTS: \(AVAudioSession.sharedInstance().availableInputs ?? [])")
  print("PREFERRED INPUT: \(String(describing: AVAudioSession.sharedInstance().preferredInput))")
  print("CURRENT ROUTE: \(AVAudioSession.sharedInstance().currentRoute)\n")
}
  1. I have a button that displays an alert with the list of all available audio inputs and providing the way to set each input as preferred:
@IBAction func selectPreferredInputClick(_ sender: UIButton) {
  let inputs = AVAudioSession.sharedInstance().availableInputs ?? []
  let title = "Select Preferred Input"
  let message = "Current Preferred Input: \(String(describing: AVAudioSession.sharedInstance().preferredInput?.portName))\nCurrent Route Input \(String(describing: AVAudioSession.sharedInstance().currentRoute.inputs.first?.portName))"
  let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
  for input in inputs {
    alert.addAction(UIAlertAction(title: input.portName, style: .default) {_ in
      print("\n\(title)")
      print("\(message) New Preferred Input: \(input.portName)\n")
      do {
        try AVAudioSession.sharedInstance().setPreferredInput(input)
     } catch {
        print("Set Preferred Input Error: \(error)")
     }
    })
  }
  alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
  present(alert, animated: true)
}

iOS 16 Behaviour:

When I launch the app without any external mics attached and initiate the AVAudioSession. Then I attach the iRig device (which is basically the external microphone) and I have the following result: the MicrophoneWired appears in the list of available inputs but input of the route is still MicrophoneBuiltIn. Then I tried to change preferredInput of the AVAudioSession first to MicrophoneWired, then to MicrophoneBuiltIn and then to MicrophoneWired again: No matter what is preferredInput the input device of AudioSession route is MicrophoneBuiltIn. Sorry for image - forum doesn't allow to post the log message:

iOS 15 Behaviour:

Everything is different (and much better) in iOS 15. When I launch the app without any external mics attached and initiate the AVAudioSession. Then I attach the iRig device (which is basically the external microphone) and I have the following result:

The input of the AVAudioSession route is MicrophoneWired. Then I try to change the preferred input of the AVAudioSession and it works fine - the input of the route matches the preferred input of the AVAudioSession. Sorry for image - forum doesn't allow to post the log message:

Conclusion:

Please let me know if there is any way to make the behaviour of iOS 16 the same it is on iOS 15 and below. I searched the release notes of iOS 16 and didn't find any mention of AVAudioSession. If there is no way to do it please let me know what is the proper way to manage input source of the route of AVAudioSession. Any advice is highly appreciated.

Post not yet marked as solved Up vote post of Gibadu Down vote post of Gibadu
2.2k views

Replies

UPDATE:

Apple released iOS 16.1 and it looks like this issue is fixed there.

Hi guys. iphone X - iOS 16.4 installed. setPreferredInput method returns true but still does not changing bluetooth device when i have 2 bluetooth devices connected to my iphone. there are logs

2023-04-19 16:41:17.078299+0300 MyVoipApp[1973:718702] [RNCallKeep][setAudioRoute] - myAudioSession availableInputs : (
    "<AVAudioSessionPortDescription: 0x2816cfdb0, type = MicrophoneBuiltIn; name = iPhone Microphone; UID = Built-In Microphone; selectedDataSource = Front>",
    "<AVAudioSessionPortDescription: 0x2816cfd70, type = BluetoothHFP; name = Rugby R6+; UID = 6F:E2:79:95:D2:B2-tsco; selectedDataSource = (null)>",
    "<AVAudioSessionPortDescription: 0x2816cfe00, type = BluetoothHFP; name = Lenovo thinkplus XT80; UID = 17:AA:ED:01:9A:11-tsco; selectedDataSource = (null)>"
)
2023-04-19 16:41:17.079001+0300 MyVoipApp[1973:718702] [RNCallKeep][setAudioRoute] - setPreferredInput to : <AVAudioSessionPortDescription: 0x2816cfd70, type = BluetoothHFP; name = Rugby R6+; UID = 6F:E2:79:95:D2:B2-tsco; selectedDataSource = (null)>
2023-04-19 16:41:17.103690+0300 MyVoipApp[1973:718702] [RNCallKeep][setAudioRoute] - setPreferredInput  success: 1
2023-04-19 16:41:18.212558+0300 MyVoipApp[1973:718695] [[RNCallKeep][getSelectedAudioRoute]] currentRoute: <AVAudioSessionRouteDescription: 0x2816e9de0, 
inputs = (
    "<AVAudioSessionPortDescription: 0x2816e9e10, type = BluetoothHFP; name = Lenovo thinkplus XT80; UID = 17:AA:ED:01:9A:11-tsco; selectedDataSource = (null)>"
); 
outputs = (
    "<AVAudioSessionPortDescription: 0x2816e9e50, type = BluetoothHFP; name = Lenovo thinkplus XT80; UID = 17:AA:ED:01:9A:11-tsco; selectedDataSource = (null)>"
)>
2023-04-19 16:41:18.213045+0300 MyVoipApp[1973:718695] [RNCallKeep][getAudioRoutes] <__NSArrayI 0x281b743f0>(
<AVAudioSessionPortDescription: 0x2816fecc0, type = MicrophoneBuiltIn; name = iPhone Microphone; UID = Built-In Microphone; selectedDataSource = Front>,
<AVAudioSessionPortDescription: 0x2816e50a0, type = BluetoothHFP; name = Rugby R6+; UID = 6F:E2:79:95:D2:B2-tsco; selectedDataSource = (null)>,
<AVAudioSessionPortDescription: 0x2816e23d0, type = BluetoothHFP; name = Lenovo thinkplus XT80; UID = 17:AA:ED:01:9A:11-tsco; selectedDataSource = (null)>
)

this code i use for setAudioRoute

RCT_EXPORT_METHOD(setAudioRoute: (NSString *)uuid
                  uid:(NSString *)uid
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
    NSLog(@"[RNCallKeep][setAudioRoute] - uid: %@", uid);
#endif
    @try {
        NSError* err = nil;
        AVAudioSession* myAudioSession = [AVAudioSession sharedInstance];
        NSString *category = [myAudioSession category];
        NSUInteger options = [myAudioSession categoryOptions];
        if(![category isEqualToString:AVAudioSessionCategoryPlayAndRecord] && (options != AVAudioSessionCategoryOptionAllowBluetooth) && (options !=AVAudioSessionCategoryOptionAllowBluetoothA2DP))
        {
            BOOL isCategorySetted = [myAudioSession setCategory:AVAudioSessionCategoryPlayAndRecord
                                                    withOptions:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowAirPlay error:&err];
            if (!isCategorySetted)
            {
                NSLog(@"setCategory failed");
                [NSException raise:@"setCategory failed" format:@"error: %@", err];
            }
        }
        BOOL isCategoryActivated = [myAudioSession setActive:YES error:&err];
        if (!isCategoryActivated)
        {
            NSLog(@"[RNCallKeep][getAudioInputs] setActive failed");
            [NSException raise:@"setActive failed" format:@"error: %@", err];
        }
        if ([uid isEqualToString:RNCallKeepBuiltInSpeakerUid]) {
            BOOL isOverrided = [myAudioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&err];
            if(!isOverrided){
                [NSException raise:@"overrideOutputAudioPort failed" format:@"error: %@", err];
            }
        } else {
            [myAudioSession overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&err];
            NSArray *ports = [RNCallKeep getAudioInputs];
            for (AVAudioSessionPortDescription *port in ports) {
                if ([port.UID isEqualToString:uid]) {
#ifdef DEBUG
                    NSLog(@"[RNCallKeep][setAudioRoute] - myAudioSession availableInputs : %@", ports);
                    NSLog(@"[RNCallKeep][setAudioRoute] - setPreferredInput to : %@", port);
#endif
                    BOOL isSetted = [myAudioSession setPreferredInput:(AVAudioSessionPortDescription *)port error:&err];
                    if(!isSetted){
                        [NSException raise:@"setPreferredInput failed" format:@"error: %@", err];
                    }
#ifdef DEBUG
                    NSLog(@"[RNCallKeep][setAudioRoute] - setPreferredInput  success: %d", isSetted);
#endif
                    break;
                }
            }
        }
        resolve(uid);
    }
    @catch ( NSException *e ){
        NSLog(@"[RNCallKeep][setAudioRoute] exception: %@",e);
        reject(@"Failure to set audio route", e, nil);
    }
}

Is there any workaround for switching between bluetooth devices?