AVPlayer Does Not Output Audio Through Bluetooth Earphones - ios

I have an avassetwriter capture session to record a video and 2 avplayerqueues to playback the immediately recorded video and the other, to playback past saved videos.
My problem is the audio input does not use my bluetooth earphones, and performs playback and record via the iphone device inputs/outputs.
I did not implement any override to default to the speaker, understand I need to handle a route change via notification observer in the case of toggling between bluetooth and device, and also tried setting the AVAudioSession.sharedInstance() .playbackAndRecording category to .allowBluetooth to no avail.
Any guidance would be appreciated as I have not come across an existing answer online..
let audioSession = AVAudioSession.sharedInstance()
//Executed right before playing avqueueplayer media
do {
try audioSession.setCategory(.playback)
try audioSession.setActive(true)
} catch {
fatalError("Error Setting Up Audio Session")
}
//Executed right after avqueueplayer finishes media
do {
try audioSession.setCategory(.recording, options: [.allowBluetooth])
try audioSession.setActive(true)
} catch {
fatalError("Error Setting Up Audio Session")
}

Did you read carefully the documentation for .allowBluetooth and .allowBluetoothA2DP? These do not mean what you likely think they mean, since passing both is rarely what developers mean. Do not guess what AVFoundation will do based on the names of things; you must read the documentation. The names of things are not obvious, and are actively misleading in several places.
If you want to record from Bluetooth headphones, you cannot support A2DP. A2DP is a unidirectional protocol (playback only). You will need to use HFP, which is what .allowBluetooth means. Remove .allowBluetoothA2DP. Note that this is significantly reduce your audio quality.
If you have distinct periods of recording vs playback, then you want to change your category when you change modes. Do not just set .playAndRecord because you will record at some point and playback at another. If you switch to a playback-only situation, switch to .playback. It is legal to change categories while the session is active (again, see the docs; there are many subtle rules).
You haven't listed what your Bluetooth earphones are, so it's not clear whether they support both A2DP and HFP. That has significant impact on how routing occurs. See the docs for .allowBluetoothA2DP on this.

Related

Does iOS Reduce Speaker Volume for Apps Using a Microphone?

I am developing an Xcode/Swift/SwiftUI app for real-time music visualization. I allow the user to push a button to toggle between microphone-input and file-play input (but never both at the same time). My app runs fine on my Mac and on my iPad, but on my iPhone, the speaker audio is only at half-volume (and appears to be only coming from the back speakers) - even when I am in file-play mode. I have traced the problem to one offending line in my code - namely the declaration
let mic = engine.inputNode // where engine = AVAudioEngine()
When I comment-out this line, the iPhone speaker level (for file-play mode) is fine. But when I un-comment it, the iPhone speaker level is barely audible. Even when I wrap this line inside a conditional if(micEnabled){} construct, the sound level is fine at first; but as soon as I select the microphone and then toggle back to file-play, the volume again decreases.
I suspect that iOS detects when a microphone is declared and automatically reduces the speaker volume to avoid audio feedback. This would make sense because nobody wants music playing when they are speaking on a telephone call. But it would also make sense to provide developers a way to override this feature if they want to handle it themselves. In my case, for the microphone-input case, I purposely assign the audio stream a zero-volume after it is tapped and before going to the speaker.
My source code is available here. All of the audio code is inside the MuVis / Shared / AudioManager.swift class.
Can anyone help me to get the file-play mode to work with full volume on my iPhone - while also allowing the user the option to select microphone-input mode?
Many thanks to Rob Napier for pointing me in the right direction for solving my problem.
As a macOS-only developer, I had ignored AVAudioSession (since it caused compiler errors on macOS). When I converted my MuVis app from macOS-only to multiplatform, I simply started a new Xcode project with the appropriate multiplatform settings, and then pasted my existing code into the shared folder. After cleaning up a few errors (mostly calls to NSObject), it magically worked on all Apple platforms - except for the iPhone audio problem described in my question. After a little research and a lot of trial-and-error, I found that my audio-volume problem is solved by inserting the following code into my setupAudio() function:
#if os(iOS)
// For iOS devices, set the audioSession category, mode, and options:
let session = AVAudioSession.sharedInstance() // Get the singleton instance of an AVAudioSession.
do {
if(filePlayEnabled) {
// This is required by iOS to prevent output audio from going only to the iPhone's rear speaker.
try session.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.default, options: [.defaultToSpeaker])
}
else {
try session.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.default, options: [])
}
} catch { print("Failed to set audioSession category.") }
#endif
Again, thank you Rob.

iOS App Bluetooth Audio Coming out in "Phone Mode."

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.
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.

How to create an iOS alarm clock that runs in the background properly

I would like to insert an alarm clock function in an iOS app I am developing, and as a reference, I installed a popular App called "Alarmy."
I managed to keep my app running in the background, just using AVAudioSession properties; however, I noticed that the app consumes a lot of battery during the phone sleep.
After some testing, I think this is due to the app activating the speakers (and keeping them ON) immediately after the AVAudioSession activation.
Even if there is no actual sound playing until the audioPlayer.play(atTime: audioPlayer.deviceCurrentTime + Double(seconds)) is triggered, if I get very very close to my iPhone 7 speakers, I can hear the little buzzing sound that indicates that the speakers are ON. This implicates that the speakers are playing an "empty sound" de facto.
This buzzing sound does not exist when I set the alarm with Alarmy; it just starts playing when it is supposed to.
I did not find any other way to maintain my app in the background and play an alarm sound at a specified time. There are Local Notifications, of course, but they do not allow to play a sound when the phone is silenced.
Going back to "Alarmy," I've seen that they are not only able to play a background alarm without any need to activate the speakers first, but they are also able to put the volume at the max level in the background. Are they maybe triggering some other iOS background mode to achieve those, perhaps using Background Fetch or Processing in some clever way? Is there any known way to replicate those behaviors?
Thanks in advance!
Here is the current code I use to set the alarm:
private func setNewAlarm(audioPlayer: AVAudioPlayer, seconds: Int, ringtone: String) {
do {
self.setNotificationAlarm(audioPlayer: audioPlayer, seconds: seconds, ringtone: ringtone, result: result)
//This calls the method I use to set a secondary alarm using local notifications, just in case the user closes the app
try AVAudioSession.sharedInstance().setActive(false)
try AVAudioSession.sharedInstance().setCategory(.playback, options: [ .mixWithOthers])
try AVAudioSession.sharedInstance().setActive(true)
} catch let error as NSError {
print("AVAudioSession error: \(error.localizedDescription)")
}
audioPlayer.prepareToPlay()
audioPlayer.play(atTime: audioPlayer.deviceCurrentTime + Double(seconds))
result(true)
}

Can I use ReplayKit to record both microphone and system audio?

There is not much documentation online about this because it's an odd task. I am trying to record my screen, the internal microphone, and the system audio at the same time using ReplayKit.
Here is how I am recording my screen right now:
if([self.screenRecorder isAvailable]){
[self.screenRecorder setMicrophoneEnabled:YES];
[self.screenRecorder startRecordingWithHandler:nil];
}
When this runs, the user is prompted to record with the microphone, or without the microphone. Could I possibly do both? Is there a workaround? If I choose microphone, when my app plays sound, the microphone gets disabled.
If anyone could propose a possible solution that does not involve replaykit, that would be greatly appreciated too!
Thanks
yes, it's possible, you can using AVAudioEngine which provide manual rendering mode, two playerNode (audio app, audio mic) into mixerNode and render.
So after looking into this you can also just do this very simply using the AVAudioSession API:
let audioSession = AVAudioSession.sharedInstance()
try! audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, with: AVAudioSessionCategoryOptions.mixWithOthers)

AVAudioSession mode switch volume difference

I'm trying to combine media playback with VoIP feature (via Twilio) for iOS 9 and 8.While an audio stream plays in the background, I connect or disconnect a Voice Conference session which results in a volume jump from value X to value Y. This jump can be heard, as well as observed by a [AVAudioSession sharedInstance].outputVolume value change.I would like to prevent this jump and keep the volume at a constant level, unless the user manually decides to change it.Further investigation showed that while AVAudioSession's category is set to AVAudioSessionCategoryPlayAndRecord, switching between modes[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeDefault error:&error]and[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeVoiceChat error:&error]causes the app to operate in two completely separate volume scales, respectively.i.e there a volume for Mode "Default" and a completely unrelated volume for Mode "Voice Chat".AVAudioSession's documentation seems to omit any mention of volume in relation to mode/category switches and I can't find anything relevant on the interwebs...
Appreciate any help.
When setting your play and record category, pass AVAudioSessionCategoryOptionDefaultToSpeaker as an option:
[[AVAudioSession sharedInstance] AVAudioSessionCategoryPlayAndRecord withOptions: AVAudioSessionCategoryOptionDefaultToSpeaker error:&error];
This overrides the default play-and-record behaviour of switching from the speaker to the much quieter receiver. The reason for this being that play-and-record was designed for telephony, where you'd be holding the phone to year ear & presumably wouldn't want to have your hearing damaged by loud sounds.
Megan from Twilio here.
I'm not most familiar with the iOS SDK but you should be able to control connection audio from TCDevice parameters incomingSoundEnabled, outgoingSoundEnabled, and disconnectSoundEnabled as documented here.
Otherwise, I would suggest looking at the sharedInstance properties of AVAudioSession that the Twilio SDK calls upon as demonstrated in this post:
setCategory:error:
setActive:error:
overrideOutputAudioPort:error:
Please let me know if this helps.

Resources