Seemingly random file corruption using AVAudioRecorder (Sometimes the file can't be played back) - iOS - 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

Related

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.

iOS UIManagedDocument locks app blocks main thread

I inherited a codebase recently from an offshore team and I have no experience with UIManagedDocument. In some situations when the app is first installed the initialization process completely locks up the app. It seems to be directly related to iCloud because when the network is off the app loads right away.
I tried doing various parts of this code in gcd blocks but there is some process that still escapes to the main thread and blocks all input.
Is there anyway to save or open asynchronously?
NSURL *_url = [[self applicationDocumentDirectory] URLByAppendingPathComponent:MyDatabaseName];
self.document = [[MyCustomManagedDocument alloc] initWithFileURL:_url];
self.document.delegate = self;
NSMutableDictionary *options = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,nil];
NSURL *iCloudURL = [self iCloudDocumentsURL];
[options setObject:PrivateName forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:iCloudURL forKey:NSPersistentStoreUbiquitousContentURLKey];
self.document.persistentStoreOptions = options;
if ([[NSFileManager defaultManager] fileExistsAtPath:[_url path]]) {
[self.document openWithCompletionHandler:^(BOOL success){
if (success) {
[self documentIsReady];
}
}];
}
else {
// This can take 10 seconds sometimes.
[self.document saveToURL:_url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){
if (success) {
[self documentIsReady];
}
}];
}

IOS AVAudioSessionModeMeasurement error

Greeting. I am working on the app (IOS 7) that perform some audio management and want to disable all audio preprocessing. When I do
NSError *err;
[audioSession setCategory:AVAudioSessionCategoryRecord error:&err];
[audioSession setCategory:AVAudioSessionModeMeasurement error:&err];
if (err) {
NSLog(#"Audio Session category %# %ld %#", [err domain], (long)[err code], [[err userInfo] description]);
}
this piece of code returns
2014-04-08 00:39:12.573 okolly[2365:60b] 00:39:12.572 ERROR: [0x3b83f18c] AVAudioSessionUtilities.mm:96: getUInt32: -- Category Value Converter failed
2014-04-08 00:39:12.575 okolly[2365:60b] Audio Session category NSOSStatusErrorDomain -50 {
}
both on simulator and on device. If I comment
[audioSession setCategory:AVAudioSessionModeMeasurement error:&err];
app works as expected.
What am I doing wrong? Thank you for help.
AVAudioSessionModeMeasurement is not an AVAudioSession category property, but a mode property, as in setMode:.

AVAudioRecorder averagePowerForChannel always returns -120.0

I'm trying to use AVAudioRecorder's averagePowerForChannel method to monitor input levels on the microphone for an iPad/iPhone app. I have a callback which polls the average level in a loop — on the iPhone it works fine and returns sensible levels, but for some reason on the iPad it always returns -120.0.
Here's some of my setup code:
- (void) setupMic {
if (micInput) {
[micInput release];
micInput = nil;
}
NSURL *newURL = [[NSURL alloc] initFileURLWithPath:#"/dev/null"];
NSMutableDictionary *recordSettings = [[NSMutableDictionary alloc] init];
[recordSettings setObject:[NSNumber numberWithInt:kAudioFormatAppleLossless] forKey: AVFormatIDKey];
[recordSettings setObject:[NSNumber numberWithFloat:22050.0] forKey: AVSampleRateKey];
// [recordSettings setObject:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey];
[recordSettings setObject:[NSNumber numberWithInt:12800] forKey:AVEncoderBitRateKey];
[recordSettings setObject:[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
[recordSettings setObject:[NSNumber numberWithInt: AVAudioQualityLow] forKey: AVEncoderAudioQualityKey];
micInput = [[AVAudioRecorder alloc] initWithURL:newURL settings:recordSettings error:nil];
// [micInput setMeteringEnabled:YES];
[newURL release];
[recordSettings removeAllObjects];
[recordSettings release];
}
As well as my start recording method:
- (void) startRecording {
NSLog(#"startRecording!");
[micInput pause];
[micInput prepareToRecord];
micInput.meteringEnabled = YES;
[micInput record];
[micInput updateMeters];
levelTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:0.0] interval:0.03 target:self selector:#selector(levelTimerCallback:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:levelTimer forMode:NSDefaultRunLoopMode];
}
and a bit of the levelTimer callback:
- (void)levelTimerCallback:(NSTimer *)timer {
[micInput updateMeters];
double avgPowerForChannel = pow(10, (0.05 * [micInput averagePowerForChannel:0]));
[micSprite receiveInput:avgPowerForChannel];
NSLog(#"Avg. Power: %f", [micInput averagePowerForChannel:0]);
...
}
Where on the iPhone, the NSLog statement will return sensible values, and the iPad will always return -120.0.
Note: I'm using this inside of a cocos2d application. For some reason, if I restart the current scene on the iPad, the mic levels will return correct values.
Anyone have any suggestions? I'm seriously at a loss here.
Thanks!
I had the same issue. I found setting the category to AVAudioSessionCategoryPlayAndRecord fixes it:
NSError *error;
[[AVAudioSession sharedInstance]
setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
if (error) {
NSLog(#"Error setting category: %#", [error description]);
}
Yes, I did too. There is only one shared AVAudioSession. Setting its category affects all recorders and players, so setting the category to AVAudioSessionCategoryPlay in one area of the app for example disables recorders in other areas.
Another possible cause of this is really simple, be sure you're calling [recorder record];
That line was accidentally deleted and it took us a while to recognize that as the cause of the -120 values being returned on the meter.
HTH
1) Make sure you have the permission:
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
if([[AVAudioSession sharedInstance] respondsToSelector:#selector(requestRecordPermission:)])
{
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
NSLog(#"permission : %d", granted);
if(granted)[self setupMic];// record ...
}];
}
}
2) Make sure your path is legitimate (/dev/null ok??):
NSString* dir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSArray *pathComponents = [NSArray arrayWithObjects:dir,fileName,nil];
recordedAudioURL = [NSURL fileURLWithPathComponents:pathComponents];
3) activate audioSession
[audioSession setActive:YES error:&error];
4) Check all errors and return values, including
BOOL ok=[micInput prepareToRecord];
ok=ok &&[micInput record];
You need to init the AVAudioSession (it should be done from didMoveToview method, it might be a reason of the problem)
let session = AVAudioSession.sharedInstance()
try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
try session.overrideOutputAudioPort(AVAudioSessionPortOverride.speaker)
try session.setActive(true)
try recorder = AVAudioRecorder(url: getDocumentsDirectory(), settings: settings)
and start recorder
func start() {
recorder?.record()
recorder?.updateMeters()
}
And get the decibels by calling
let decibels = recorder.averagePower(forChannel: 0)
from -120 (min value) up to 0. Noice level of some cafe for example is -20
here's the more detailed example http://www.mikitamanko.com/blog/2017/04/15/swift-how-to-get-decibels/ Note: use the update method as well.

AVAudioRecorder prepareToRecord works, but record fails

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.

Resources