It seems that my app is not working with AirPods. Right now I'm using this code for the playback and record:
let audioSession = AVAudioSession.sharedInstance()
do { try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, with: AVAudioSessionCategoryOptions.defaultToSpeaker)
}catch {
print("audioSession properties weren't set because of an error.")
}
Will it be enough if I change defaultToSpeaker to allowBluetooth?
P.S. I know it's quite a stupid question because it'd be much simpler to just change this line and check, but I don't have AirPods with me right now, so the only option for me is to upload the new build to Testflight (and I want to do this with minimum iterations).
update: (quite naive approach — but all I need is to use bluetooth headphones if they are available):
func selectDevice(audioSession: AVAudioSession) {
var headphonesExist = false
var bluetoothExist = false
var speakerExist = false
let currentRoute = AVAudioSession.sharedInstance().currentRoute
for output in audioSession.currentRoute.outputs {
print(output)
if output.portType == AVAudioSessionPortHeadphones || output.portType == AVAudioSessionPortHeadsetMic {
headphonesExist = true
}
if output.portType == AVAudioSessionPortBluetoothA2DP || output.portType == AVAudioSessionPortBluetoothHFP {
bluetoothExist = true
}
if output.portType == AVAudioSessionPortBuiltInSpeaker {
speakerExist = true
}
}
if bluetoothExist == true {
do { try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, with: AVAudioSessionCategoryOptions.allowBluetooth) } catch {
print("error with audiosession: bluetooth")
}
}
}
You need to add support for bluetooth to your options parameter like so:
[AVAudioSessionCategoryOptions.defaultToSpeaker, .allowBluetoothA2DP]
.allowBluetoothA2DP will allow for high quality audio output to the bluetooth device and restrict microphone input on said device, while .allowBluetooth will default HFP compatible (input/output) bluetooth devices to the lower quality HFP, which supports microphone input.
Here is a full source to request proper permission.
All you have to do is to add a mode with '.allowBluetoothA2DP'
I've applied on Swift 5
func requestPermissionForAudioRecording(){
recordingSession = AVAudioSession.sharedInstance()
do {
// only with this without options, will not capable with your Airpod, the Bluetooth device.
// try recordingSession.setCategory(.playAndRecord, mode: .default)
try recordingSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowAirPlay, .allowBluetoothA2DP])
try recordingSession.setActive(true)
recordingSession.requestRecordPermission() { allowed in
DispatchQueue.main.async {
if allowed {
// Recording permission has been allowed
self.recordingPermissionGranted = true
} else {
// failed to record!
}
}
}
} catch let err {
// failed to record!
print("AudioSession couldn't be set!", err)
}
}
Related
The main issue here is, when the microphone input is added, the device loses any haptic system sounds.
I setup the audio session here:
let audioSession = AVAudioSession.sharedInstance()
do {
try self.audioSession.setCategory(.playAndRecord, options: .mixWithOthers)
try self.audioSession.setAllowHapticsAndSystemSoundsDuringRecording(true)
try self.audioSession.setActive(true)
} catch { }
I make sure I am using setAllowHapticsAndSystemSoundsDuringRecording.
Though out the app, I am adding the microphone and removing it on-demand:
do {
let microphonePermission = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio)
if microphonePermission != .denied && microphonePermission != .restricted {
let audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!)
if self.session.canAddInput(audioDeviceInput) {
self.session.addInput(audioDeviceInput)
}
else { print("Could not add audio device input to the session.") }
} else {
}
}
catch { print("Could not create audio device input: \(error).") }
As soon as the microphone is added, it loses haptic feedback and system sounds.
I am using AVFoundation / AudioKit in order to record the internal microphone of the iPhone / iPad. It should be possible to continue using the app after switching the output between BluetoothA2DP and the internal speaker. The microphone should continue to take the input from the internal microphone of the device. And it does. Everything is working fine but only until I want to change the output device.
func basicAudioSetup(){
// microphone
self.microphone = AKMicrophone()
// select input of device
if let input = AudioKit.inputDevice {
try! self.microphone?.setDevice(input)
}
AKSettings.sampleRate = 44100
AKSettings.channelCount = 2
AKSettings.playbackWhileMuted = true
AKSettings.enableRouteChangeHandling = false
AKSettings.useBluetooth = true
AKSettings.allowAirPlay = true
AKSettings.defaultToSpeaker = true
AKSettings.audioInputEnabled = true
// init DSP
self.dsp = AKClock(amountSamples: Int32(self.amountSamples), amountGroups: Int32(self.amountGroups), bpm: self.bpm, iPad:self.iPad)
self.masterbusTracker = AKAmplitudeTracker(self.dsp)
self.mixer.connect(input: self.masterbusTracker)
self.player = AKPlayer()
self.mixer.connect(input: self.player)
self.microphone?.stop()
self.microphoneTracker = AKAmplitudeTracker(self.microphone)
self.microphoneTracker?.stop()
self.microphoneRecorder = try! AKNodeRecorder(node: self.microphone)
self.microphoneMonitoring = AKBooster(self.microphoneTracker)
self.microphoneMonitoring?.gain = 0
self.mixer.connect(input: self.microphoneMonitoring)
AudioKit.output = self.mixer
// the following line is actually happening inside a customized AudioKit.start() function to make sure that only BluetoothA2DP is used for better sound quality:
try AKSettings.setSession(category: .playAndRecord, with: [.defaultToSpeaker, .allowBluetoothA2DP, .allowAirPlay, .mixWithOthers])
do {
try AudioKit.start()
}catch{
print("AudioKit did not start")
}
// adding Notifications to manually restart the engine.
NotificationCenter.default.addObserver(self, selector: #selector(self.audioRouteChangeListener(notification:)), name: NSNotification.Name.AVAudioSessionRouteChange, object: nil)
}
#objc func audioRouteChangeListener(notification:NSNotification) {
let audioRouteChangeReason = notification.userInfo![AVAudioSessionRouteChangeReasonKey] as! UInt
let checkRestart = {
print("ROUTE CHANGE")
do{
try AudioKit.engine.start()
}catch{
print("error rebooting engine")
}
}
if audioRouteChangeReason == AVAudioSessionRouteChangeReason.newDeviceAvailable.rawValue ||
audioRouteChangeReason == AVAudioSessionRouteChangeReason.oldDeviceUnavailable.rawValue{
if Thread.isMainThread {
checkRestart()
} else {
DispatchQueue.main.async(execute: checkRestart)
}
}
}
I noticed, that when the microphone is connected, AVAudioSessionRouteChange is never called when switching from the internal speaker to Bluetooth. I do receive messages when starting with and switching from Bluetooth to the internal speaker:
[AVAudioEngineGraph.mm:1481:Start: (err = PerformCommand(*ioNode,
kAUStartIO, NULL, 0))
What does this message exactly mean? I tried everything, from manually disconnecting all inputs of the engine / de- and reactivating the session to rebuilding the whole chain. Nothing works.
Theoretically the input source is not changing, because it is staying on the input of the phone. Any help highly appreciated.
FYI: I am using a customized version of AudioKit library where I removed its internal Notifications of AVAudioSessionRouteChange to avoid unwanted Doppelgaenger. This customized library also sets the session category and options internally for the same reason and to ensure, that only BluetoothA2DP is used.
So after a lot of searching I was able to find the code block that allows background audio to play while at the same time recording video.
I have pasted said code block below.
fileprivate func setBackgroundAudioPreference() {
guard allowBackgroundAudio == true else {
return
}
guard audioEnabled == true else {
return
}
do{
if #available(iOS 10.0, *) {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
} else {
let options: [AVAudioSession.CategoryOptions] = [.mixWithOthers, .allowBluetooth]
let category = AVAudioSession.Category.playAndRecord
let selector = NSSelectorFromString("setCategory:withOptions:error:")
AVAudioSession.sharedInstance().perform(selector, with: category, with: options)
}
try AVAudioSession.sharedInstance().setActive(true)
session.automaticallyConfiguresApplicationAudioSession = false
}
catch {
print("[SwiftyCam]: Failed to set background audio preference")
}
}
However, I have one small issue. For some reason when the camera loads the background Audio volume is reduced. When I record a video with instagram the audio doesn't get reduced and it still records is there any way I can change my current code block to not lower the volume while recoding with video?
I read the documentation and apparently .duckOthers option should be the only option that reduces the volume. But this one does as well
Okay so I found the answer after diving further into some of the documentation.
Updated code posted below. All you have to do is set the .defaultToSpeaker option
fileprivate func setBackgroundAudioPreference() {
guard allowBackgroundAudio == true else {
return
}
guard audioEnabled == true else {
return
}
do{
if #available(iOS 10.0, *) {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP,.defaultToSpeaker])
} else {
let options: [AVAudioSession.CategoryOptions] = [.mixWithOthers, .allowBluetooth]
let category = AVAudioSession.Category.playAndRecord
let selector = NSSelectorFromString("setCategory:withOptions:error:")
AVAudioSession.sharedInstance().perform(selector, with: category, with: options)
}
try AVAudioSession.sharedInstance().setActive(true)
session.automaticallyConfiguresApplicationAudioSession = false
}
catch {
print("[SwiftyCam]: Failed to set background audio preference")
}
}
I am using AVAudioSession to listen to voice input. it works fine for wired headphones but it is not working for connected bluetooth device. Following is the code I am using to set input to bluetooth mic
func setupSessionForRecording() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.allowBluetooth])
} catch let error as NSError {
debugPrint("Error in listening "+error.localizedDescription)
}
var inputsPriority: [(type: String, input: AVAudioSessionPortDescription?)] = [
(AVAudioSessionPortLineIn, nil),
(AVAudioSessionPortHeadsetMic, nil),
(AVAudioSessionPortBluetoothHFP, nil),
(AVAudioSessionPortUSBAudio, nil),
(AVAudioSessionPortCarAudio, nil),
(AVAudioSessionPortBuiltInMic, nil),
]
for availableInput in audioSession.availableInputs! {
guard let index = inputsPriority.index(where: { $0.type == availableInput.portType }) else { continue }
inputsPriority[index].input = availableInput
}
guard let input = inputsPriority.filter({ $0.input != nil }).first?.input else {
fatalError("No Available Ports For Recording")
}
do {
try audioSession.setPreferredInput(input)
try audioSession.setMode(AVAudioSessionModeMeasurement)
try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
try audioSession.setPreferredIOBufferDuration(10)
} catch {
fatalError("Error Setting Up Audio Session")
}
}
This code stops taking input from device mic and I also get sound in bluetooth headset that it is ready to listen but it doesn't pick any input from device.
Also,
When I am trying to play any audio into bluetooth headset It doesn't work. Here is the code to play audio
do {
let output = AVAudioSession.sharedInstance().currentRoute.outputs[0].portType
if output == "Receiver" || output == "Speaker"{
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
}
else{
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
}
print("Voice Out \(output)" )
} catch let error as NSError {
print("audioSession error: \(error.localizedDescription)")
os_log("Error during changing the current audio route: %#" , log: PollyVoiceViewController.log, type: .error, error)
} catch {
os_log("Unknown error during changing the current audio route", log: PollyVoiceViewController.log, type: .error)
}
do {
let soundData = try Data(contentsOf: url as URL)
self.audioPlayer = try AVAudioPlayer(data: soundData)
self.audioPlayer?.prepareToPlay()
self.audioPlayer?.volume = 3.0
self.audioPlayer?.delegate = self
self.audioPlayer?.play()
} catch let error as NSError {
print("Error getting the audio file"+error.description)
}
The reason is: BluetoothHFP is not available in AVAudioSessionModeMeasurement mode
AVAudioSessionModeMeasurement
After you set try audioSession.setMode(AVAudioSessionModeMeasurement), the audioSession.availableInputs is not contain the BluetoothHFP.
This mode is intended for apps that need to minimize the amount of system-supplied signal processing to input and output signals. If recording on devices with more than one built-in microphone, the primary microphone is used.
And in the document of setPreferredInput(_:)
The AVAudioSessionPortDescription must be in the availableInputs array.
The value of the inPort parameter must be one of the AVAudioSessionPortDescription objects in the availableInputs array. If this parameter specifies a port that is not already part of the current audio route and the app’s session controls audio routing, this method initiates a route change to use the preferred port.
And it must set up after setting the mode.
You must set a preferred input port only after setting the audio session’s category and mode and activating the session.
In my app I want to check for the AVAudio portTypes already connected to the phone. The code below works for BluetoothA2DP and Headphones, but not BluetoothHFP when I connect the phone to my car handsfree. Can anyone help me?! I think have been through all the SO posts on Handsfree/AV/Bluetooth, and many others, but can't work out why it is not recognising the BluetoothHFP output portType.
import AVFoundation
func startCheckAVConnection() {
// set the AVAudioSession to allow bluetooth. This do/try/catch doesn't seem to make a difference if it is here or not.
do{
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: AVAudioSessionCategoryOptions.allowBluetooth)
} catch{
print(error)
}
// Check possible outputs for handsfree
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
if outputs.count != 0 {
for output in outputs {
if output.portType == AVAudioSessionPortBluetoothA2DP {
peripheralLabel.text = "connected to BluetoothA2DP"
} else if output.portType == AVAudioSessionPortBluetoothHFP { // NOT RECOGNISED
peripheralLabel.text = "connected to BluetoothHFP"
} else if output.portType == AVAudioSessionPortHeadphones {
peripheralLabel.text = "connected to Headphones"
}
}
} else {
peripheralLabel.text = "Please connect handsfree"
}
// Add observer for audioRouteChangeListener
NotificationCenter.default.addObserver(
self,
selector: #selector(TopVC.audioRouteChangeListener(_:)),
name: NSNotification.Name.AVAudioSessionRouteChange,
object: nil)
}
I needed to add setActive after setCategory.
do { try AVAudioSession.sharedInstance().setActive(true)
print("setActive")
} catch {
print(error)
}
Try changing your category setup to:
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: AVAudioSessionCategoryOptions.allowBluetooth)
HFP doesn't work with multi-route category AFAICT.