AVAudioRecorder prepareToRecord works, but record fails - avaudiorecorder

I just built and tested a basic AVAudioRecorder/AVAudioPlayer sound recorder and player. Eventually I got this working on the device, as well as the simulator. My player/recorder code is in a single UIView subclass. Unfortunately, when I copy the class into my main project, it no longer works (on the device--the simulator is fine). prepareToRecord is working fine, but record isn't. Here is some code:
audioRecorder = [[ AVAudioRecorder alloc] initWithURL:url settings:recordSettings error:&error];
if ([audioRecorder prepareToRecord]){
audioRecorder.meteringEnabled = YES;
if(![audioRecorder record])NSLog(#"recording failed!");
}else {
int errorCode = CFSwapInt32HostToBig ([error code]);
NSLog(#"preparedToRecord=NO Error: %# [%4.4s])" , [error localizedDescription], (char*)&errorCode);
...
I get "recording failed". Anyone have any ideas why this is happening?

The complete solution was to move the [SimpleAudioEngine sharedEngine] initialisation to before my own AVAudioSession setCategory. I think somewhere in SimpleAudioEngine, its doing a setCategory :AVAudioSessionCategoryAmbient. Overriding this with my own setCategory :AVAudioSessionCategoryPlayAndRecord later makes everything work.

Related

Callkit use bluetooth headset as audio output

I'm working on VOIP with Callkit
It work fine, except audio output source
It always output audio by iPhone speaker
some of so answer said set AvAudioSession Option as AVAudioSessionCategoryOptionAllowBluetooth will work, but still failed
and I tried to set bluetooth headset as preferred, like this, failed
by the way, how to make ringtone broadcast by headset?
below is my code, follow the suggestion in this discussion, I configure AVAudioSession right after dialing and get coming call
- (void)getCall:(NSDictionary *)infoDic {
CXCallUpdate *update = [[CXCallUpdate alloc]init];
// config update
NSUUID *uuid = [NSUUID UUID];
[self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
if (error)
NSLog(#"%#", error.localizedDescription);
}];
NSArray *video = #[#(ReceiveVideoReq), #(VideoCalling)];
if ([video containsObject:#(self.client.callStage)])
[ProviderManager configureAudio:true];
else
[ProviderManager configureAudio:false];
}
- (void)dialPhone:(BOOL)isVideo {
CXHandle *handle = [[CXHandle alloc]initWithType:CXHandleTypePhoneNumber value:#"AAAA"];
CXStartCallAction *start = [[CXStartCallAction alloc]initWithCallUUID:uuid handle:handle];
start.video = isVideo;
CXTransaction *trans = [[CXTransaction alloc]initWithAction:start];
[self callControlReq:trans];
[ProviderManager configureAudio:isVideo];
}
+ (void)configureAudio:(BOOL)isVideo {
NSError *error=nil, *sessionError = nil;
AVAudioSession *sess = [AVAudioSession sharedInstance];
[sess setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&sessionError];
if (sessionError)
NSLog(#"ERROR: setCategory %#", [sessionError localizedDescription]);
if (isVideo)
[sess setMode:#"AVAudioSessionModeVideoChat" error:&sessionError];
else
[sess setMode:#"AVAudioSessionModeVoiceChat" error:&sessionError];
if (sessionError) {
NSLog(#"ERROR: setCategory %#", [sessionError localizedDescription]);
}
[sess overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&sessionError];
[[AVAudioSession sharedInstance] setActive:true withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
After research, I found the root case of this issue.
Open setting->Accessibility->Touch->Call Audio Routing, there are three option: Automatic, Bluetooth Headset, Speaker.
Default is Automatic, that means if you answer a phone call(Include callkit), when you answer call by your bluetooth device, audio will route to bluetooth, if you answer the call in iPhone, it will route to receiver.
So this actually not an issue, just system behavior.
And do not try to force switch to bluetooth by using [AVAudioSession setPreferredInput:], it will cause more serious issue.
In my case: I call this force switch to bluetooth after audio connected, it worked, but when the bluetooth device disconnected, my audio completely not work and app not get any audio route change callback, even not work after connected bluetooth device again.

No audio from AVCaptureSession after changing AVAudioSession

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.

AVAudioRecorder not recording in background after audio session interruption ended

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.

AVSpeechSynthesizer won't work when phone locked

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.

Seemingly random file corruption using AVAudioRecorder (Sometimes the file can't be played back) - iOS

In an app I'm currently developing, I've more or less hit a brick wall. In the application you can enter a view which lists all locally saved audio files in a standard table view. From here you can click on them to play them, or hit a record button below to make a new recording, which will afterwards automatically get saved to the apps sandbox.
Now all of this actually works perfectly fine, most of the time. I can sit and make recordings and play them back. I can sit and test this on and on for around 45 with no problems whatsoever. Then suddenly at random times I will hit a very odd problem. The problem is that the recorder all of the sudden starts to save nothing but corrupt files which can't be played back and are exactly 4096 bytes in size, no matter how long you recorder for.
I use the AVAudioRecorder in a totally standard fashion, setting it up like this:
// Specific settings for the audio recording
NSDictionary *recordSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:AVAudioQualityMin],
AVEncoderAudioQualityKey,
[NSNumber numberWithInt:8],
AVEncoderBitRateKey,
[NSNumber numberWithInt: 2],
AVNumberOfChannelsKey,
[NSNumber numberWithFloat:22000.0],
AVSampleRateKey,
nil];
NSError *error = nil;
audioRecorder = [[AVAudioRecorder alloc]
initWithURL:contentURL
settings:recordSettings
error:&error];
audioRecorder.delegate = self;
I start the recording like this:
if (!isRecording) {
isRecording = YES;
[audioRecorder record];
// Start the timer
recordTiming = [NSDate date];
timeCheck = [NSTimer
scheduledTimerWithTimeInterval:1.0f
target:self
selector:#selector(timeCheck)
userInfo:nil
repeats:YES];
}
And then stopping it with this:
if (isRecording) {
[audioRecorder stop];
// Stop the timer
[timeCheck invalidate];
}
I have no idea what could cause this problem to happen. So far I've tried almost everything. I've check to make sure that the recorder object is recycled properly (so you have a new instance for each now recording), and that the old reference is deallocated and so on, to make sure it has nothing to do with interfering objects, but nothing solves it.
Is there anyone who have a slight idea of what could cause problems with corrupted files?
Note: Since the OP is inactive for some days and I guess this question should be answered for public's future reference, thus I post this answer.
The AVAudioSession has to be obtained before start recording.
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *err = nil;
[audioSession setCategory :AVAudioSessionCategoryPlayAndRecord error:&err];
if(err){
NSLog(#"audioSession: %# %d %#", [err domain], [err code], [[err userInfo] description]);
return;
}
err = nil;
[audioSession setActive:YES error:&err];
if(err){
NSLog(#"audioSession: %# %d %#", [err domain], [err code], [[err userInfo] description]);
return;
}
Reference: https://stackoverflow.com/a/9706731/188331

Resources