I'm making an app that supports both video playback and recording. I always want to allow background audio mixing except for during video playback (during playback, background audio should be muted). Therefore, I use the two methods below while changing the state of playback. When my AVPlayer starts loading, I call MuteBackgroundAudio, and when I dismiss the view controller containing it, I call ResumeBackgroundAudio. This works as expected, and the audio returns successfully after leaving playback.
The issue is that after doing this at least once, whenever I record anything using AVCaptureSession, no sounds gets recorded. My session is configured like so:
AVCaptureDevice *audioDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];
AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
if (error)
{
NSLog(#"%#", error);
}
if ([self.session canAddInput:audioDeviceInput])
{
[self.session addInput:audioDeviceInput];
}
[self.session setAutomaticallyConfiguresApplicationAudioSession:NO];
// ... videoDeviceInput
Note that I have not set usesApplicationAudioSession, so it defaults to YES.
void MuteBackgroundAudio(void)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if ([[AVAudioSession sharedInstance] isOtherAudioPlaying] && !isMuted)
{
isMuted = YES;
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker
error:&error];
if (error)
{
NSLog(#"DEBUG - Set category error %ld, %#", (long)error.code, error.localizedDescription);
}
NSError *error2 = nil;
[[AVAudioSession sharedInstance] setActive:YES
withOptions:0
error:&error2];
if (error2)
{
NSLog(#"DEBUG - Set active error 2 %ld, %#", (long)error.code, error.localizedDescription);
}
}
});
}
void ResumeBackgroundAudio(void)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (isMuted)
{
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *deactivationError = nil;
[audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&deactivationError];
if (deactivationError)
{
NSLog(#"DEBUG - Failed at deactivating audio session, retrying...");
ResumeBackgroundAudio();
return;
}
isMuted = NO;
NSLog(#"DEBUG - Audio session deactivated");
NSError *categoryError = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers
error:&categoryError];
if (categoryError)
{
NSLog(#"DEBUG - Failed at setting category");
return;
}
NSLog(#"DEBUG - Audio session category set to mix with others");
NSError *activationError = nil;
[audioSession setActive:YES error:&activationError];
if (activationError)
{
NSLog(#"DEBUG - Failed at activating audio session");
return;
}
NSLog(#"DEBUG - Audio session activated");
}
});
}
Debugging
I have noticed that the audioSession always needs two tries to successfully deactivate after calling ResumeBackgroundAudio. It seems my AVPlayer does not get deallocated or stopped in time, due to this comment in AVAudioSession.h:
Note that this method will throw an exception in apps linked on or
after iOS 8 if the session is set inactive while it has running or
paused I/O (e.g. audio queues, players, recorders, converters, remote
I/Os, etc.).
The fact that no sound gets recorded bring me to believe the audioSession does not actually get activated, but my logging says it does (always in the second iteration of the recursion).
I got the idea of using recursion to solve this problem from this post.
To clarify, the flow that causes the problem is the following:
Open app with Spotify playing
Begin playback of any content in the app
Spotify gets muted, playback begins (MuteBackgroundAudio)
Playback ends, Spotify starts playing again (ResumeBackgroundAudio)
Start recording
Stop recording, get mad that there is no audio
I've had the exact same issue as you're describing, down to the very last detail ([audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&deactivationError]; failed the first time no matter what)
The only acceptable way, for my case, was to stop then start the AVCaptureSession. I'm not entirely sure but I think this is the same exact way Whatsapp handles it - their camera behaves exactly like mine with the solution I'm suggesting.
Removing and adding the same audio input on the already running session also seemed to work but I got a terrible camera freeze - nothing compared to the session start / stop.
The recursion solution is nice, but I think it would be a great idea to 'throttle' that recursion call (add a short delay & a max retry count of some sort) in case for a legit reason the set fails every time. Otherwise your stack will overflow and your app will crash.
If you or anyone found a better solution to this I would love to know it.
Related
I'm trying to allow for a toggle between bluetooth headsets (airpods in my case) and the phone speaker, using AVAudioSession. I initialize my session as so:
AVAudioSessionCategoryOptions options = (AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker);
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:options error:&error];
Then I try to alternate between output modes as so:
-(void)setIncomingSoundMode:(IncomingSoundMode)incomingSoundMode{
[self removeAudioRouteChangedObserver];
[NNLogger logFromInstance: self message: #"Audio stream setting use speaker" data: #(incomingSoundMode)];
_incomingSoundMode = incomingSoundMode;
AVAudioSession *session = [AVAudioSession sharedInstance];
AVAudioSessionPortDescription *routePort = session.currentRoute.outputs.firstObject;
NSString *portType = routePort.portType;
NSLog(#"current port type: %#",portType);
NSError *audioPortError = nil;
if(incomingSoundMode == IncomingSoundModeSpeaker){
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&audioPortError];
[self muteChannel:NO];
} else if(incomingSoundMode == IncomingSoundModeHeadset){
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&audioPortError];
[self muteChannel:NO];
} else if(incomingSoundMode == IncomingSoundModeBluetooth){
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&audioPortError];
[self muteChannel:NO];
} else if(incomingSoundMode == IncomingSoundModeSilent){
[self muteChannel:YES];
}
if(audioPortError){
NSLog(#"audioPortError - %#",audioPortError.localizedDescription);
}
NSError *sessionError = nil;
[session setActive: YES error:&sessionError];
if(sessionError){
NSLog(#"sessionError - %#",sessionError.localizedDescription);
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addAudioRouteChangedObserver];
});
}
The issue is when I try to override to speaker output while bluetooth headphones are connected - it simply doesn't switch to the speaker. This same functionality works with wired headphones, or when toggling device speaker to headset:
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&audioPortError];
[session setActive: YES error:&sessionError];
Any clues on what I'm doing wrong here??
Thanks
I have exact same issue in the application I'm working on and it's reproducible only on iOS 12+ and only for W1 / H1 chip bluetooth devices (AirPods / new Beats Studio, etc.).
I asked apple audio engineers on WWDC about it (by showing my bug report) and they have confirmed that this is a known issue they're working on. Should be fixed in iOS update.. somewhen later :) To speed things up, you could duplicate this bug report: rdar://49734534
P.S. There is no pretty workaround for this issue, but there are few options:
Use MPVolumeView's route button. It works, and seems like this is
how WhatsApp handles this case.
Disable bluetooth option for AVAudioSession. Far from perfect solution, but may work for some apps.
am trying to use SFSpeechRecognizer for speech to text, after speaking a welcome message to the user via AVSpeechUtterance. But randomly, the speech recognition does not start(after speaking the welcome message) and it throws the error message below.
[avas] ERROR: AVAudioSession.mm:1049: -[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.
It works few times. Am not clear on why is it not working consistently.
I tried the solutions mentioned in other SO posts, where it mentions to check if there are audio players running. I added that check in the speech to text part of the code. It returns false (i.e. no other audio player is running) But still the speech to text does not start listening for the user speech. Can you pls guide me on what is going wrong.
Am testing on iPhone 6 running iOS 10.3
Below are code snippets used:
TextToSpeech:
- (void) speak:(NSString *) textToSpeak {
[[AVAudioSession sharedInstance] setActive:NO withOptions:0 error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionDuckOthers error:nil];
[synthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
AVSpeechUtterance* utterance = [[AVSpeechUtterance new] initWithString:textToSpeak];
utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:locale];
utterance.rate = (AVSpeechUtteranceMinimumSpeechRate * 1.5 + AVSpeechUtteranceDefaultSpeechRate) / 2.5 * rate * rate;
utterance.pitchMultiplier = 1.2;
[synthesizer speakUtterance:utterance];
}
- (void)speechSynthesizer:(AVSpeechSynthesizer*)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance*)utterance {
//Return success message back to caller
[[AVAudioSession sharedInstance] setActive:NO withOptions:0 error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient
withOptions: 0 error: nil];
[[AVAudioSession sharedInstance] setActive:YES withOptions: 0 error:nil];
}
Speech To Text:
- (void) recordUserSpeech:(NSString *) lang {
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:lang];
self.sfSpeechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale];
[self.sfSpeechRecognizer setDelegate:self];
NSLog(#"Step1: ");
// Cancel the previous task if it's running.
if ( self.recognitionTask ) {
NSLog(#"Step2: ");
[self.recognitionTask cancel];
self.recognitionTask = nil;
}
NSLog(#"Step3: ");
[self initAudioSession];
self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
NSLog(#"Step4: ");
if (!self.audioEngine.inputNode) {
NSLog(#"Audio engine has no input node");
}
if (!self.recognitionRequest) {
NSLog(#"Unable to created a SFSpeechAudioBufferRecognitionRequest object");
}
self.recognitionTask = [self.sfSpeechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult *result, NSError *error) {
bool isFinal= false;
if (error) {
[self stopAndRelease];
NSLog(#"In recognitionTaskWithRequest.. Error code ::: %ld, %#", (long)error.code, error.description);
[self sendErrorWithMessage:error.localizedFailureReason andCode:error.code];
}
if (result) {
[self sendResults:result.bestTranscription.formattedString];
isFinal = result.isFinal;
}
if (isFinal) {
NSLog(#"result.isFinal: ");
[self stopAndRelease];
//return control to caller
}
}];
NSLog(#"Step5: ");
AVAudioFormat *recordingFormat = [self.audioEngine.inputNode outputFormatForBus:0];
[self.audioEngine.inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
//NSLog(#"Installing Audio engine: ");
[self.recognitionRequest appendAudioPCMBuffer:buffer];
}];
NSLog(#"Step6: ");
[self.audioEngine prepare];
NSLog(#"Step7: ");
NSError *err;
[self.audioEngine startAndReturnError:&err];
}
- (void) initAudioSession
{
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryRecord error:nil];
[audioSession setMode:AVAudioSessionModeMeasurement error:nil];
[audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
}
-(void) stopAndRelease
{
NSLog(#"Invoking SFSpeechRecognizer stopAndRelease: ");
[self.audioEngine stop];
[self.recognitionRequest endAudio];
[self.audioEngine.inputNode removeTapOnBus:0];
self.recognitionRequest = nil;
[self.recognitionTask cancel];
self.recognitionTask = nil;
}
Regarding the logs added, am able to see all logs till "Step7" printed.
When debugging the code in the device, it consistently triggers break at the below lines (I have exception breakpoints set) though, continue keeps on with the execution. It however happens same way during few successful executions as well.
AVAudioFormat *recordingFormat = [self.audioEngine.inputNode outputFormatForBus:0];
[self.audioEngine prepare];
The reason is audio didn't completely finish, when -speechSynthesizer:didFinishSpeechUtterance: was called, therefore you get such kind of error trying to call setActive:NO. You cant deactivate AudioSession or change any settings during I/O is running. Workaround: wait for several ms (how long read below) and then perform AudioSession deactivation and stuff.
A few words about audio playing completion.
That might seem weird at first glance, but I've spent tones of time to research this issue. When you put last sound chunk to device output you have only approximate timing when it actually will be completed. Look at the AudioSession property ioBufferDuration:
The audio I/O buffer duration is the number of seconds for a single
audio input/output cycle. For example, with an I/O buffer duration of
0.005 s, on each audio I/O cycle:
You receive 0.005 s of audio if obtaining input.
You must provide 0.005 s of audio if providing output.
The typical maximum I/O buffer duration is 0.93 s (corresponding to 4096 sample
frames at a sample rate of 44.1 kHz). The minimum I/O buffer duration
is at least 0.005 s (256 frames) but might be lower depending on the
hardware in use.
So, we can interpret this value as the one chunk playback time. But you still have a small non-calculated duration between this timeline and actual audio playing completion (hardware delay). I would say you need wait about ioBufferDuration * 1000 + delay ms for being sure audio playing complete (ioBufferDuration * 1000 - coz it is duration in seconds), where delay is some quite small value.
More over seems like even Apple developers are also not pretty sure about audio completion time. Quick look at the new audio class AVAudioPlayerNode and func scheduleBuffer(_ buffer: AVAudioPCMBuffer, completionHandler: AVFoundation.AVAudioNodeCompletionHandler? = nil):
#param completionHandler called after the buffer has been consumed by
the player or the player is stopped. may be nil.
#discussion Schedules the buffer to be played following any previously scheduled commands. It is possible for the completionHandler to be called
before rendering begins or before the buffer is played completely.
You can read more about audio processing in Understanding the Audio Unit Render Callback Function (AudioUnit is low-level API that provides fasten access to I/O data).
So I was working on a video capture app that plays background audio (from Spotify or Apple Music) and I'm having a small problem where there's a small audio interruption when I open my app while audio is being played.
Here's what I have for allowing background audio to play (located in my didFinishLaunchingWithOptions in my AppDelegate class:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers
error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
Any clues to stopping that beginning interruption? Thanks!!
EDIT
I should also mention after setting the AVAudioSession I am setting my AVCaptureSession. I initialize it then set the properties.
self.session.usesApplicationAudioSession = YES;
self.session.automaticallyConfiguresApplicationAudioSession = NO;
I think the reason of interruption is you are updating category in every case. You may use below function to check and update category only if it is needed.
-(BOOL) checkAndUpdateCategory {
NSError *error;
AVAudioSession *session = [AVAudioSession sharedInstance];
BOOL result = [session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord];
if(!result) {
result = [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers error:&error];
if(error) {
//Handle Error
NSLOG(#"Error:%#", error);
}
}
return result;
}
I am recording audio in my app, both in foreground and in background. I also handle AVAudioSessionInterruptionNotification to stop recording when interruption begins and start again when it ends. Although in foreground it works as expected, when app is recording in background and I receive a call it doesn't start again recording after call ends. My code is the following:
- (void)p_handleAudioSessionInterruptionNotification:(NSNotification *)notification
{
NSUInteger interruptionType = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
if (self.isRecording && !self.interruptedWhileRecording) {
[self.recorder stop];
self.interruptedWhileRecording = YES;
return;
}
}
if (interruptionType == AVAudioSessionInterruptionTypeEnded) {
if (self.interruptedWhileRecording) {
NSError *error = nil;
[[AVAudioSession sharedInstance] setActive:YES error:&error];
NSDictionary *settings = #{
AVEncoderAudioQualityKey: #(AVAudioQualityMax),
AVSampleRateKey: #8000,
AVFormatIDKey: #(kAudioFormatLinearPCM),
AVNumberOfChannelsKey: #1,
AVLinearPCMBitDepthKey: #16,
AVLinearPCMIsBigEndianKey: #NO,
AVLinearPCMIsFloatKey: #NO
};
_recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:nil];
[self.recorder record];
self.interruptedWhileRecording = NO;
return;
}
}
}
Note that fileURL points to new caf file in a NSDocumentDirectory subdirectory. Background mode audio is configured. I also tried voip and play silence, both to no success.
The NSError in AVAudioSessionInterruptionTypeEnded block is a OSStatus error 560557684 which I haven't found how to tackle.
Any help would be much appreciated.
Error 560557684 is for AVAudioSessionErrorCodeCannotInterruptOthers. This happens when your background app is trying to activate an audio session that doesn't mix with other audio sessions. Background apps cannot start audio sessions that don't mix with the foreground app's audio session because that would interrupt the audio of the app currently being used by the user.
To fix this make sure to set your session category to one that is mixable, such as AVAudioSessionCategoryPlayback. Also be sure to set the category option AVAudioSessionCategoryOptionMixWithOthers (required) and AVAudioSessionCategoryOptionDuckOthers (optional). For example:
// background audio *must* mix with other sessions (or setActive will fail)
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDuckOthers
error:&sessionError];
if (sessionError) {
NSLog(#"ERROR: setCategory %#", [sessionError localizedDescription]);
}
The error code 560557684 is actually 4 ascii characters '!int' in a 32 bit integer. The error codes are listed in the AVAudioSession.h file (see also AVAudioSession):
#enum AVAudioSession error codes
#abstract These are the error codes returned from the AVAudioSession API.
...
#constant AVAudioSessionErrorCodeCannotInterruptOthers
The app's audio session is non-mixable and trying to go active while in the background.
This is allowed only when the app is the NowPlaying app.
typedef NS_ENUM(NSInteger, AVAudioSessionErrorCode)
{
...
AVAudioSessionErrorCodeCannotInterruptOthers = '!int', /* 0x21696E74, 560557684 */
...
I added the following
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
before configuring AVAudioSession and it worked. Still don't know what bugs may appear.
I had this same error when trying to use AVSpeechSynthesizer().speak() while my app was in the background. #progmr's answer solved the problem for me, though I also had to call AVAudioSession.sharedInstance().setActive(true) too.
For completeness, here's my code in Swift 5.
In application(_:didFinishLaunchingWithOptions:):
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback,
options: [.mixWithOthers,
.duckOthers])
} catch let error as NSError {
print("Error setting up AVAudioSession : \(error.localizedDescription)")
}
Then in my view controller:
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch let error as NSError {
print("Error : \(error.localizedDescription)")
}
let speechUtterance = AVSpeechUtterance(string: "Hello, World")
let speechSynth = AVSpeechSynthesizer()
speechSynth.speak(speechUtterance)
Note: When setActive(true) is called, it reduces the volume of anything else playing at the time. To turn the volume back up afterwards, you need to call setActive(false) - for me the best time to do that was once I'd been notified that the speech had finished in the corresponding AVSpeechSynthesizerDelegate method.
I am trying to use AVSpeechSynthesizer when the phone is locked, but the audio stops when I lock the screen. I am using the simulator, not an actual device. I have seen a couple other questions similar to this on this site and I followed their recommendations, but it still does not work.
In the app delegate I set the audio session category to AVAudioSessionCategoryPlayback.
- (void)configureAudioSession{
NSError *error = NULL;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
if(error) {
NSLog(#"Error setting category of audio session: %#",error.description);
}
error = NULL;
[[AVAudioSession sharedInstance] setActive:YES error: &error];
if (error) {
NSLog(#"Error activating audio session: %#",error.description);
}
}
And I checked the 'Audio and Airplay' mode under Project Settings->Capabilities->Background Modes.
Can anyone tell me how to get this to work?
Here's how I got AVSpeechSynthesizer to continue speaking when phone goes idle, if phone gets locked, or if app goes to background. (iOS8)
Step 1) Open up info.plist and add the key "Required background modes". Within this array, add a String called "App plays audio or streams audio/video using AirPlay".
Step 2) Add the following to your app delegate didFinishLaunchingWithOptions:
NSError *error = NULL;
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:&error];
if(error) {
// Do some error handling
}
[session setActive:YES error:&error];
if (error) {
// Do some error handling
}
Step 3) Run on your device and test it out!
I was able to follow this post to get it to work about a month ago:
AVAudioPlayer stops playing on screen lock even though the category is AVAudioSessionCategoryPlayback
Basically, you need to make entry in your app's .plist file. I'm not sure this will work on the simulator, so you will probably want to test it on an actual device.