I recently asked on the Apple iPhone Developer Forums (developer program subscribers only) what would happen if my app hit a disk full error while recording sound using an Audio Queue. There wasn’t an answer so I set up a test for it and learned a little bit (nothing absolutely definitive though), which I’m sharing here so it will be helpful to others. I suspect it will apply to some extent to file types other than audio as well.
1. How I set up my device in order to make sure there was as little space available to my app as possible:
I loaded a full library of music on my device, then took photos until I got a general “Warning: you are running out of disk space. Please delete some photos or videos.” message, then continued taking photos until I got a follow-up Camera-specific message “Low Disk Space: There is not enough disk space to continue taking photos. Make room by deleting existing videos or photos.”. Note: this wasn’t actually easy to achieve, meaning that the iPhone has a lot of buffer space in reserve at the time that it starts reporting low disk space. This is actually good for all concerned since the potentially error-causing situation is called out well in advance; it’s only bad if you want to get the iPhone full enough to really use up every bit of space and discover it can take more than a half-hour of snapping photos of your living room.
2. The test:
I downloaded the SpeakHere example (again) and added a background thread that would update the overall file size of the recorded file to the fileDescription UILabel every second (on the main thread) so I could see if the file was still growing. I wanted this feedback because if I need to repeat the test, it’s really good to have even a vague idea at what point in time significant things will start happening, especially since I have no idea how long of a test it’s going to end up being.
If you are interested in seeing the same feedback as I did in the SpeakHere sample app, download it from the developer site and add a pointer to an NSThread called *recordThread and the matching @property to the interface of SpeakHereController, and in the implementation synthesize/deallocate recordThread and replace the entire method -(IBAction)record:(id)sender with this block of code (which I have put no effort into making efficient or correct, it’s just supposed to update my label without tying up my time troubleshooting):
- (void) updateLabelWithFileSize:(NSString *)fileSize {
fileDescription.text = fileSize;
}
- (void) startRecordSizeLoop {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent: @"recordedFile.caf"];
NSError *error = nil;
NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:filePath error:&error];
NSString *fileSize = nil;
if(error) {
NSLog(@"Error getting file size of audio file: %@", [error description]);
} else {
NSString *fileSizeAttribute = [fileAttributes objectForKey:@"NSFileSize"];
if([fileSizeAttribute intValue] < 1) {
fileSize = @"error";
} else if([fileSizeAttribute floatValue] / 1024.0 < 1) {
fileSize = @"1k";
} else {
fileSize = [NSString stringWithFormat:@"%dk",[fileSizeAttribute intValue] / 1024];
}
}
[self performSelectorOnMainThread:@selector(updateLabelWithFileSize:) withObject:fileSize waitUntilDone:NO];
[fileManager release];
[NSThread sleepForTimeInterval:1];
if(self.recordThread.isCancelled == FALSE)[self startRecordSizeLoop];
}
- (void) startRecordThreadAutoreleasePool {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self startRecordSizeLoop];
[pool drain];
}
- (void)startRecordThread {
NSThread *recThread = [[NSThread alloc] initWithTarget:self selector:@selector(startRecordThreadAutoreleasePool) object:nil];
self.recordThread = recThread;
[recThread release];
[self.recordThread start];
}
- (void)waitForRecordThreadToFinish {
while (recordThread && ![recordThread isFinished]) {
[NSThread sleepForTimeInterval:0.1];
}
}
- (void)stopRecordThread {
[self.recordThread cancel];
[self waitForRecordThreadToFinish];
self.recordThread = nil;
}
- (IBAction)record:(id)sender
{
if (recorder->IsRunning())
{
[self stopRecord];
[self stopRecordThread];
}
else
{
btn_play.enabled = NO;
btn_record.title = @"Stop";
recorder->StartRecord(CFSTR("recordedFile.caf"));
[self setFileDescriptionForFormat:recorder->DataFormat() withName:@"Recorded File"];
[self startRecordThread];
[lvlMeter_in setAq: recorder->Queue()];
}
}
The other thing I did to save time was to go into the AQRecorder implementation and increase the sample rate for the queue so the space would get consumed faster by adding the line “mRecordFormat.mSampleRate = 96000.0;” to the end of this format definition in void AQRecorder::SetupAudioFormat(UInt32 inFormatID):
mRecordFormat.mBitsPerChannel = 16;
mRecordFormat.mBytesPerPacket = mRecordFormat.mBytesPerFrame = (mRecordFormat.mBitsPerChannel / 8) * mRecordFormat.mChannelsPerFrame;
mRecordFormat.mFramesPerPacket = 1;
Back to my test: I tapped “record” and waited. around 56000k, a warning popped up: “Warning: you are running out of disk space. Please delete some photos or videos.” I dismissed it and let the app continue recording. Receiving this warning wasn’t accompanied by any other changes or restrictions. This seems to be an OS-level thing and not specific to Core Audio, since it’s the identical warning I received in Camera.
At 121764k the number stopped increasing. I recorded myself saying “is this still recording?” so I could listen to the recording and see if it was or wasn’t still recording at the time that the numbers stopped increasing (I expected it wasn’t but wanted to verify). I checked the debugger and none of the error logging that is in the AQ’s input buffer handler had gone off when the file size had stopped increasing. I played back the recording and the last words I recorded were not played back, i.e. when the numbers stopped going up, the recording did apparently stop as expected.
Next step was to see what was going on inside of void AQRecorder::MyInputBufferHandler() when the file stopped increasing so I changed it in the following ways (adding an int callsToMyInputBufferHandler to the beginning of the class implementation):
void AQRecorder::MyInputBufferHandler( void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumPackets,
const AudioStreamPacketDescription* inPacketDesc)
{
AQRecorder *aqr = (AQRecorder *)inUserData;
callsToMyInputBufferHandler++;
printf("MyInputBufferHandler() was called on iteration %d\n", callsToMyInputBufferHandler);
printf("inNumPackets = %d\n", inNumPackets);
printf("inBuffer->mAudioDataByteSize = %d\n", inBuffer->mAudioDataByteSize);
printf("inBuffer->mAudioData = %d\n", inBuffer->mAudioData);
printf("aqr->mIsRunning = %d\n", aqr->mIsRunning);
try {
if (inNumPackets > 0) {
// write packets to file
XThrowIfError(AudioFileWritePackets(aqr->mRecordFile, FALSE, inBuffer->mAudioDataByteSize,
inPacketDesc, aqr->mRecordPacket, &inNumPackets, inBuffer->mAudioData),
"AudioFileWritePackets failed");
aqr->mRecordPacket += inNumPackets;
} else {
NSLog(@"There are no packets available, do not write them to file");
}
// if we're not stopping, re-enqueue the buffer so that it gets filled again
if (aqr->IsRunning()) {
XThrowIfError(AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL), "AudioQueueEnqueueBuffer failed");
} else {
NSLog(@"The audioqueue isn't running, don't re-enqueue the buffer");
}
} catch (CAXException e) {
char buf[256];
fprintf(stderr, "Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
}
NSLog(@"MyInputBufferHandler end iteration %d", callsToMyInputBufferHandler);
}
Some of the stuff being logged here is uninformative, I just went with a kitchen sink approach since I didn’t want to do this too many times. The first thing I noticed as a result of doing this was that what I thought were permanent stops in the recorder were not – they were really long lags between rounds of MyInputBufferHandler() being called. Lags of more than minutes in some cases, about 4 or 5 times before a definitive stop at 125104k. Finally, after a lag of more than 4 minutes, I got an error returned with a complete iteration:
Error: AudioFileWritePackets failed (-40)
The command line utility macerror tells me that -40 means “Mac OS error -40 (posErr): tried to position to before start of file (r/w)” which seems somewhat related to the situation at hand (I guess it’s something that happens with an EOF under low-disk-space conditions).
This recurred through a couple more very laggy iterations and then nothing more happened. This is all the testing I have time for on this issue but I feel like I learned a bit here and can make some decisions about addressing the case in my app.
Conclusions: users will get a warning from the OS that there isn’t enough room some minutes in advance of running out of room. This might be “good enough” since if they decide to proceed they won’t be surprised by weird behavior. When there is really no more room, AudioFileWritePackets will start returning an exception -40, but as far as I can see here it might be returning some time after the exception occured, which means there could be an unexpected loss of something the user was expecting to be recorded, as there was when I spoke to SpeakHere after the buffer calls started to lag.
The -40 is something to go on, but I don’t know that disk full is the only circumstance under which is could occur, which means that using it as a specific indicator of being out of space is probably a bad idea. But, I suspect that any time it appears it probably denotes a significant error, so one strategy that could be used would be to deliver an error alert to the user when -40 is returned and mention that an error occurred and it could be due to lack of disk space. Since this was time-consuming to set up and observe, I hope it helps others avoid having to replicate the same steps.