3

I have an iOS app which is producing text to speech (TTS) audio (AVSpeechSynthesizer). One user is saying that the audio over his car Bluetooth speaker is coming out in "phone mode" (presumably the audio when making or receiving phone calls) as opposed to "music mode" the way that apps like Youtube and the music and maps apps are. This also causes the handling of incoming phone calls not to work properly with the car Bluetooth speaker.

Unfortunately, I am at a loss to understand why, or even that there is a distinction between "phone" and "music" mode. When using the phone's speakers, there is no such problem with handling incoming phone calls. The issue is only with Bluetooth.

The AVAudioSession initialization code is as follows.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(AVAudioSession.Category.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
            try session.overrideOutputAudioPort(AVAudioSession.PortOverride.none)
            try session.setActive(true, options: .notifyOthersOnDeactivation)
            
        } catch let error {
            print("audioSession properties weren't set. Error: \(error.localizedDescription)")
        }
        
        return true
    }

Also, the AVSpeechSynthesizer code is as follows

let synthesizer = AVSpeechSynthesizer()
let utterance = AVSpeechUtterance(string: newText)
synthesizer.speak(utterance)

Is there anything else this code should be doing, or perhaps is doing wrong?

Thanks in advance.

Peter Jacobs
  • 1,657
  • 2
  • 13
  • 29
  • Note that `.none` is the default for `overrideOutputAudioPort`, but is the opposite of `defaultToSpeaker`, so it's not quite clear what you're trying to do by passing both. – Rob Napier Aug 10 '21 at 21:23
  • Obviously it was not clear to me either, but I gather `session.overrideOutputAudioPort(AVAudioSession.PortOverride.none)` should be removed? – Peter Jacobs Aug 10 '21 at 21:58
  • That one definitely (since it's the default). The question is whether you want defaultToSpeaker (which says to play audio through the phone speaker rather than the phone receiver) – Rob Napier Aug 10 '21 at 22:42
  • I want it to play through the Bluetooth speaker (which is the receiver?) when connected. – Peter Jacobs Aug 10 '21 at 23:14
  • The "receiver" is the small speaker at the top of the phone (the one you listen to for phone calls). The "speaker" is the ones at the bottom of the phone. `defaultToSpeaker` only applies when there's no external device (like Bluetooth). All the words in this space are a bit weird. – Rob Napier Aug 10 '21 at 23:27
  • Yes, I think it's the wording that is confusing me the most. Since I do want to use the car's Bluetooth speakers when paired to them, I guess then I don't want to use `defaultToSpeaker?` – Peter Jacobs Aug 11 '21 at 00:01
  • `defaultToSpeaker` only applies when there is no external (such as Bluetooth) device. In that case, you probably do want the speaker. Make sure to read the docs on all of these options. They mostly do a good job of explaining how they all work (even allowBluetooth, which the docs explain means "HFP"). You don't want to guess with Bluetooth. It's not obvious how things work (but it's not magic, either). https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions – Rob Napier Aug 11 '21 at 00:29
  • 1
    I read it before, but I got a bit lost in the terminology. Then I did not immediately equate “phone mode” (as described by the user) to HFP. I will reread with this in mind. Thanks again for your help! – Peter Jacobs Aug 11 '21 at 00:59

1 Answers1

3

What you're calling "phone mode" is HFP (Hands Free Profile). You've included .allowBluetooth which means "prefer using HFP." (It's a very confusing enum name.)

What you're calling "music mode" is A2DP, which you're allowing via .allowBluetoothA2DP.

However, A2DP is not bidirectional, which you're requesting with .playAndRecord. So the session uses HFP.

The audio quality of HFP is notably worse than A2DP.

For TTS, there shouldn't be a need for a microphone, so you can replace .playAndRecord with .play (and I'd probably drop .allowBluetooth). If you require a microphone for some other purpose, you should drop .allowBluetoothA2DP, and there's no (standard) way to avoid using HFP to communicate over Bluetooth.

There are non-standard ways to solve this if you were the manufacturer of the car and the app. You could open a second A2DP channel to the phone, or you could implement a proprietary microphone protocol over BLE or iAP2. But there's no way to do this with standard devices while talking to an iPhone. (If both devices support aptX, there are some other options, but iPhones don't and I haven't heard any hints that they will.)

Note that you can change the category and options, and activate or deactivate the session at any time. So if you need the microphone sometimes, you can switch to .playAndRecord only when you need it and minimize the impact on users when they don't need the microphone.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Hi Rob, thanks very much for your answer. Yes, the app does need the microphone, as it also uses voice recognition when asking the user various questions. Before I was actually activating and deactivating the session and calling `setCategory` to switch between `.play` and `.playAndRecord` when doing TTS and speech recognition, respectively, but that had the side effect of dropping the Bluetooth connection or causing an annoying beep whenever the session was deactivated and reactivated. – Peter Jacobs Aug 10 '21 at 21:52
  • Yeah, if you're listening to the Bluetooth microphone and want to play Bluetooth audio to the same device, you're going to have to use HFP. There's no other standard protocol that does that and the iPhone supports. That means it will interfere with users' phone calls. One possible solution is to use the car's speakers, but the phone's microphone (just take out `allowBluetooth` to get that). But it may confuse users if they don't realize which microphone is in use. – Rob Napier Aug 10 '21 at 22:44
  • Some things like the Amazon Alexa app do what you're describing by incorporating custom firmware into the Bluetooth device. Earbud manufacturers do a lot of work to make Alexa work seamlessly; Amazon's app can't do it by itself. Depending on what you're doing, you might explore their Voice Interoperability Initiative; you might be able to ride their coattails. I haven't dug into it. https://developer.amazon.com/en-US/alexa/voice-interoperability – Rob Napier Aug 10 '21 at 22:51
  • Thanks, I’ll have a look. – Peter Jacobs Aug 10 '21 at 23:17
  • 1
    I think using the car speakers and the iPhone microphone might be acceptable. That’s definitely better than interfering with users’ phone calls. Also, seems the audio quality is much better not using HFP. – Peter Jacobs Aug 10 '21 at 23:20
  • 1
    Works great now, user is happy. Thanks a lot, Rob. – Peter Jacobs Aug 12 '21 at 02:50
  • @RobNapier could you help check out this question about CarPlay and audio quality? https://stackoverflow.com/q/76222065/1032900 Thanks so much as you seem to be the best expert in this area – hyouuu May 10 '23 at 19:44
  • I believe your question is a duplicate of this one. It is impossible with standard Bluetooth protocols to have a microphone and high-quality audio over the same Bluetooth channel. As above "One possible solution is to use the car's speakers, but the phone's microphone (just take out allowBluetooth to get that)." Or if you control the hardware, you can explore making multiple connections. But there is not solution using the standards. Is there more to the question? – Rob Napier May 10 '23 at 20:08