UKSoundFileRecorder
Language: Objective-C, Author: Uli Kursterer
License: MIT/X11
This is a Cocoa class that uses CoreAudio to record sound from the system's default sound input and writes it to a file.
UKSoundFileRecorder source preview
//
// UKSoundFileRecorder.m
// UKSoundFileRecorder
//
// Created by Uli Kusterer on 14.07.07.
// Copyright 2007 M. Uli Kusterer. All rights reserved.
//
// -----------------------------------------------------------------------------
// Headers:
// -----------------------------------------------------------------------------
#import "UKSoundFileRecorder.h"
#import "NSString+NDCarbonUtilities.h"
#import <sys/param.h> // for MAX().
// -----------------------------------------------------------------------------
// Private method prototypes:
// -----------------------------------------------------------------------------
@interface UKSoundFileRecorder (UKSoundFileRecorderPrivateMethods)
-(void) cleanUp;
-(NSString*) setupAudioFile; // Returns error string, NIL on success.
-(NSString*) configureAU; // Returns error string, NIL on success.
-(AudioBufferList*) allocateAudioBufferListWithNumChannels: (UInt32)numChannels size: (UInt32)size;
-(void) destroyAudioBufferList: (AudioBufferList*)list;
@end
@implementation UKSoundFileRecorder
// -----------------------------------------------------------------------------
// defaultOutputFormat:
// Returns a dictionary containing our default output format for the sound
// data. This is what you get if you don't call setOutputFormat:.
// -----------------------------------------------------------------------------
+(NSDictionary*) defaultOutputFormat
{
static NSDictionary* sDict = nil;
if( !sDict )
{
sDict = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithDouble: 44100.0], UKAudioStreamSampleRate,
UKAudioStreamFormatMPEG4AAC, UKAudioStreamFormat,
[NSNumber numberWithUnsignedInt: 1024], UKAudioStreamFramesPerPacket,
[NSNumber numberWithUnsignedInt: 2], UKAudioStreamChannelsPerFrame,
UKAudioOutputFileTypeM4A, UKAudioOutputFileType,
nil];
}
return sDict;
}
// -----------------------------------------------------------------------------
// * DESIGNATED INITIALIZER:
// -----------------------------------------------------------------------------
-(id) init
{
self = [super init];
if( self )
{
outputFormat = [[[self class] defaultOutputFormat] retain]; // Apply a sensible default.
}
return self;
}
// -----------------------------------------------------------------------------
// * CONVENIENCE INITIALIZER:
// -----------------------------------------------------------------------------
-(id) initWithOutputFilePath: (NSString*)ofp
{
self = [self init];
if( self )
{
[self setOutputFilePath: ofp];
}
return self;
}
// -----------------------------------------------------------------------------
// * DESTRUCTOR:
// -----------------------------------------------------------------------------
-(void) dealloc
{
NS_DURING
[self cleanUp]; // cleanUp calls stop, which may throw.
NS_HANDLER
NSLog(@"[UKSoundFileRecorder dealloc]: Ignoring exception during clean-up. %@ : %@",[localException name],[localException reason]);
NS_ENDHANDLER
[self destroyAudioBufferList: audioBuffer];
[outputFilePath release];
outputFilePath = nil;
[outputFormat release];
outputFormat = nil;
[super dealloc];
}
// -----------------------------------------------------------------------------
// Delegate accessors:
// -----------------------------------------------------------------------------
-(void) setDelegate: (id)dele
{
delegate = dele; // Don't retain delegate, it's very likely our owner. Wouldn't want a retain circle!
delegateWantsTimeChanges = (delegate && [delegate respondsToSelector: @selector(soundFileRecorder:reachedDuration:)]);
}
-(id) delegate
{
return delegate;
}
// -----------------------------------------------------------------------------
// AudioInputProc:
// Callback function that is called by the audio unit on its high-priority
// thread when we have sound input. Here's where we write out the data
// to the file. Try not to do too much here.
// -----------------------------------------------------------------------------
OSStatus AudioInputProc( void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList* ioData)
{
UKSoundFileRecorder * afr = (UKSoundFileRecorder*)inRefCon;
OSStatus err = noErr;
// Render into audio buffer
err = AudioUnitRender( afr->audioUnit, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, afr->audioBuffer);
if( err )
fprintf( stderr, "AudioUnitRender() failed with error %i\n", err );
// Write to file, ExtAudioFile auto-magicly handles conversion/encoding
// NOTE: Async writes may not be flushed to disk until a the file
// reference is disposed using ExtAudioFileDispose
err = ExtAudioFileWriteAsync( afr->outputAudioFile, inNumberFrames, afr->audioBuffer);
if( err != noErr )
{
char formatID[5] = { 0 };
*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
formatID[4] = '\0';
fprintf(stderr, "ExtAudioFileWrite FAILED! %d '%-4.4s'\n",err, formatID);
return err;
}
UInt64 nanos = AudioConvertHostTimeToNanos( inTimeStamp->mHostTime -afr->startHostTime );
afr->currSeconds = ((double)nanos) * 0.000000001;
if( afr->delegateWantsTimeChanges ) // Don't waste time syncing to other threads if nobody is listening:
[afr performSelectorOnMainThread: @selector(notifyDelegateOfTimeChange) withObject: nil waitUntilDone: NO];
return err;
}
// Used by our AudioInputProc to easily call this delegate method from another thread:
-(void) notifyDelegateOfTimeChange
{
if( isRecording ) // In case we queued one up but were already finished by the time it got executed.
[delegate soundFileRecorder: self reachedDuration: currSeconds];
}
// -----------------------------------------------------------------------------
// outputFilePath Accessors:
// The file at outputFilePath mustn't exist yet.
// -----------------------------------------------------------------------------
-(void) setOutputFilePath: (NSString*)ofp
{
if( outputFilePath != ofp )
{
[self willChangeValueForKey: @"outputFilePath"];
[outputFilePath release];
outputFilePath = [ofp copy];
[self cleanUp]; // Make sure we recreate our objects for the new format.
[self didChangeValueForKey: @"outputFilePath"];
}
}
-(NSString*) outputFilePath
{
return outputFilePath;
}
// Handy method for hooking up this object to a text field:
-(IBAction) takeOutputFilePathFrom: (id)sender
{
[self setOutputFilePath: [sender stringValue]];
}
// -----------------------------------------------------------------------------
// outputFormat Accessors:
// -----------------------------------------------------------------------------
-(void) setOutputFormat: (NSDictionary*)inASBD
{
if( outputFormat != inASBD )
{
[outputFormat release];
outputFormat = [inASBD retain];
[self cleanUp]; // Make sure we recreate our objects for the new format.
}
}
-(NSDictionary*) outputFormat
{
return outputFormat;
}
// -----------------------------------------------------------------------------
// prepare:
// Set up all the CoreAudio magic that makes this work.
// -----------------------------------------------------------------------------
-(void) prepare
{
NSString* errStr = nil;
errStr = [self configureAU];
if( errStr == nil )
errStr = [self setupAudioFile];
if( errStr )
{
[self cleanUp];
[NSException raise: @"UKSoundFileRecorderCantPrepare" format: @"%@.", errStr];
}
}
// -----------------------------------------------------------------------------
// isRecording accessor:
// Returns YES if we are currently recording, NO otherwise.
// -----------------------------------------------------------------------------
-(BOOL) isRecording
{
return isRecording;
}
// -----------------------------------------------------------------------------
// start:
// Start recording sound, like, right now.
// -----------------------------------------------------------------------------
-(void) start: (id)sender
{
if( isRecording )
return;
if( !audioUnit )
[self prepare];
// Start pulling audio data:
startHostTime = AudioGetCurrentHostTime();
OSStatus err = AudioOutputUnitStart( audioUnit );
if( err == noErr )
{
[self willChangeValueForKey: @"isRecording"];
isRecording = YES;
[self didChangeValueForKey: @"isRecording"];
if( delegate && [delegate respondsToSelector: @selector(soundFileRecorderWasStarted:)] )
[(NSObject*)delegate soundFileRecorderWasStarted: self];
}
else
[NSException raise: @"UKSoundFileRecorderCantStart" format: @"Could not start recording (ID=%d)", err];
}
// -----------------------------------------------------------------------------
// stop:
// Stop recording sound and flush the file to disk.
// -----------------------------------------------------------------------------
-(void) stop: (id)sender
{
if( isRecording )
{
if( audioUnit != NULL )
{
// Stop pulling audio data
OSStatus err = AudioOutputUnitStop( audioUnit );
if( err != noErr )
[NSException raise: @"UKSoundFileRecorderCantStop" format: @"Could not stop recording (ID=%d)", err];
}
[self willChangeValueForKey: @"isRecording"];
isRecording = NO;
[self didChangeValueForKey: @"isRecording"];
if( delegate && [delegate respondsToSelector: @selector(soundFileRecorderWasStopped:)] )
[(NSObject*)delegate soundFileRecorderWasStopped: self];
[self cleanUp]; // Make sure file gets flushed to disk.
[[NSWorkspace sharedWorkspace] noteFileSystemChanged: outputFilePath]; // Make sure Finder updates file size.
}
}
@end
@implementation UKSoundFileRecorder (UKSoundFileRecorderPrivateMethods)
// -----------------------------------------------------------------------------
// cleanUp:
// Called in various places, but also in the destructor, to tear down our
// CoreAudio stuff. This also causes the file to be written to disk.
// This is essentially the opposite to -prepare.
// -----------------------------------------------------------------------------
-(void) cleanUp
{
// Stop pulling audio data.
[self stop: self];
// Dispose our audio file reference.
// Also responsible for flushing async data to disk.
if( outputAudioFile )
{
ExtAudioFileDispose( outputAudioFile );
outputAudioFile = nil;
}
if( audioUnit )
{
CloseComponent( audioUnit );
audioUnit = NULL;
}
}
// -----------------------------------------------------------------------------
// setupAudioFile:
// Init our ExtAudioFileRef object so it writes the correct kind of audio
// to the correct file system location.
//
// Returns NIL on success, an error string on failure.
// -----------------------------------------------------------------------------
-(NSString*) setupAudioFile
{
OSStatus err = noErr;
AudioConverterRef conv = NULL;
NSString* outputDirectory = [outputFilePath stringByDeletingLastPathComponent];
NSString* outputFileName = [outputFilePath lastPathComponent];
FSRef parentDirectory;
AudioStreamBasicDescription desiredOutputFormat;
NSString* fileFormatStr = [outputFormat objectForKey: UKAudioOutputFileType];
AudioFileTypeID fileFormat = fileFormatStr ? UKAudioStreamFormatIDFromString( fileFormatStr ) : kAudioFileM4AType;
UKAudioStreamDescriptionFromDictionary( outputFormat, &desiredOutputFormat );
if( ![outputDirectory getFSRef: &parentDirectory] )
return [NSString stringWithFormat: @"Could not get reference to directory \"%@\"",outputDirectory];
if( outputAudioFile != NULL ) // Have an audio file already? Get rid of that.
[self cleanUp];
// Create new MP4 file (kAudioFileM4AType)
err = ExtAudioFileCreateNew( &parentDirectory, (CFStringRef)outputFileName, fileFormat, &desiredOutputFormat, NULL, &outputAudioFile );
if( err != noErr )
{
char formatID[5];
*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
formatID[4] = '\0';
return [NSString stringWithFormat: @"Could not create the audio file (ID=%d/'%-4.4s')",err, formatID];
}
// Inform the file what format the data is we're going to give it, should be pcm
// You must set this in order to encode or decode a non-PCM file data format.
err = ExtAudioFileSetProperty( outputAudioFile, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), &actualOutputFormat);
if( err != noErr )
{
char formatID[5];
*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
formatID[4] = '\0';
return [NSString stringWithFormat: @"Could not set up data format for output file (ID=%d/'%-4.4s')",err, formatID];
}
// If we're recording from a mono source, setup a simple channel map to split to stereo
if( deviceFormat.mChannelsPerFrame == 1 && actualOutputFormat.mChannelsPerFrame == 2)
{
// Get the underlying AudioConverterRef
UInt32 size = sizeof(AudioConverterRef);
err = ExtAudioFileGetProperty( outputAudioFile, kExtAudioFileProperty_AudioConverter, &size, &conv );
if( conv )
{
// This should be as large as the number of output channels,
// each element specifies which input channel's data is routed to that output channel
SInt32 channelMap[] = { 0, 0 };
err = AudioConverterSetProperty( conv, kAudioConverterChannelMap, 2 * sizeof(SInt32), channelMap );
}
}
// Initialize async writes thus preparing it for IO
err = ExtAudioFileWriteAsync( outputAudioFile, 0, NULL );
if( err != noErr )
{
char formatID[5];
*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
formatID[4] = '\0';
return [NSString stringWithFormat: @"Could not initialize asynchronous writing (ID=%d/'%-4.4s')",err, formatID];
}
return nil;
}
// -----------------------------------------------------------------------------
// configureAU:
// Create our Audio Unit that gives us data from the microphone.
//
// Returns NIL on success, an error string on failure.
// -----------------------------------------------------------------------------
-(NSString*) configureAU
{
Component component = NULL;
ComponentDescription description;
OSStatus err = noErr;
UInt32 param;
AURenderCallbackStruct callback;
if( audioUnit )
{
CloseComponent( audioUnit );
audioUnit = NULL;
}
// Open the AudioOutputUnit
// There are several different types of Audio Units.
// Some audio units serve as Outputs, Mixers, or DSP
// units. See AUComponent.h for listing
description.componentType = kAudioUnitType_Output;
description.componentSubType = kAudioUnitSubType_HALOutput;
description.componentManufacturer = kAudioUnitManufacturer_Apple;
description.componentFlags = 0;
description.componentFlagsMask = 0;
if( component = FindNextComponent( NULL, &description ) )
{
err = OpenAComponent( component, &audioUnit );
if( err != noErr )
{
audioUnit = NULL;
return [NSString stringWithFormat: @"Couldn't open AudioUnit component (ID=%d)", err];
}
}
// Configure the AudioOutputUnit
// You must enable the Audio Unit (AUHAL) for input and output for the same device.
// When using AudioUnitSetProperty the 4th parameter in the method
// refers to an AudioUnitElement. When using an AudioOutputUnit
// for input the element will be '1' and the output element will be '0'.
// Enable input on the AUHAL
param = 1;
err = AudioUnitSetProperty( audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, ¶m, sizeof(UInt32) );
if(err == noErr)
{
// Disable Output on the AUHAL
param = 0;
err = AudioUnitSetProperty( audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, ¶m, sizeof(UInt32) );
if( err != noErr )
{
[self cleanUp];
return [NSString stringWithFormat: @"Couldn't set EnableIO property on the audio unit (ID=%d)", err];
}
}
// Select the default input device
param = sizeof(AudioDeviceID);
err = AudioHardwareGetProperty( kAudioHardwarePropertyDefaultInputDevice, ¶m, &inputDeviceID );
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Couldn't get default input device (ID=%d)", err];
}
// Set the current device to the default input unit.
err = AudioUnitSetProperty( audioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &inputDeviceID, sizeof(AudioDeviceID) );
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Failed to hook up input device to our AudioUnit (ID=%d)", err];
}
// Setup render callback
// This will be called when the AUHAL has input data
callback.inputProc = AudioInputProc;
callback.inputProcRefCon = self;
err = AudioUnitSetProperty( audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &callback, sizeof(AURenderCallbackStruct) );
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Could not install render callback on our AudioUnit (ID=%d)", err];
}
// get hardware device format
param = sizeof(AudioStreamBasicDescription);
err = AudioUnitGetProperty( audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &deviceFormat, ¶m );
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Could not install render callback on our AudioUnit (ID=%d)", err];
}
// Twiddle the format to our liking
audioChannels = MAX( deviceFormat.mChannelsPerFrame, 2 );
actualOutputFormat.mChannelsPerFrame = audioChannels;
actualOutputFormat.mSampleRate = deviceFormat.mSampleRate;
actualOutputFormat.mFormatID = kAudioFormatLinearPCM;
actualOutputFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved;
if( actualOutputFormat.mFormatID == kAudioFormatLinearPCM && audioChannels == 1 )
actualOutputFormat.mFormatFlags &= ~kLinearPCMFormatFlagIsNonInterleaved;
#if __BIG_ENDIAN__
actualOutputFormat.mFormatFlags |= kAudioFormatFlagIsBigEndian;
#endif
actualOutputFormat.mBitsPerChannel = sizeof(Float32) * 8;
actualOutputFormat.mBytesPerFrame = actualOutputFormat.mBitsPerChannel / 8;
actualOutputFormat.mFramesPerPacket = 1;
actualOutputFormat.mBytesPerPacket = actualOutputFormat.mBytesPerFrame;
// Set the AudioOutputUnit output data format
err = AudioUnitSetProperty( audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &actualOutputFormat, sizeof(AudioStreamBasicDescription));
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Could not change the stream format of the output device (ID=%d)", err];
}
// Get the number of frames in the IO buffer(s)
param = sizeof(UInt32);
err = AudioUnitGetProperty( audioUnit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Global, 0, &audioSamples, ¶m );
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Could not determine audio sample size (ID=%d)", err];
}
// Initialize the AU
err = AudioUnitInitialize( audioUnit );
if(err != noErr)
{
[self cleanUp];
return [NSString stringWithFormat: @"Could not initialize the AudioUnit (ID=%d)", err];
}
// Allocate our audio buffers
audioBuffer = [self allocateAudioBufferListWithNumChannels: actualOutputFormat.mChannelsPerFrame size: audioSamples * actualOutputFormat.mBytesPerFrame];
if( audioBuffer == NULL )
{
[self cleanUp];
return [NSString stringWithFormat: @"Could not allocate buffers for recording (ID=%d)", err];
}
return nil;
}
// -----------------------------------------------------------------------------
// allocateAudioBufferListWithNumChannels:size:
// Create our audio buffer list. A buffer list is the storage we use in
// our AudioInputProc to get the sound data and hand it on to the sound
// file writer.
// -----------------------------------------------------------------------------
-(AudioBufferList*) allocateAudioBufferListWithNumChannels: (UInt32)numChannels size: (UInt32)size
{
AudioBufferList* list = NULL;
UInt32 i = 0;
list = (AudioBufferList*) calloc( 1, sizeof(AudioBufferList) + numChannels * sizeof(AudioBuffer) );
if( list == NULL )
return NULL;
list->mNumberBuffers = numChannels;
for( i = 0; i < numChannels; ++i )
{
list->mBuffers[i].mNumberChannels = 1;
list->mBuffers[i].mDataByteSize = size;
list->mBuffers[i].mData = malloc(size);
if(list->mBuffers[i].mData == NULL)
{
[self destroyAudioBufferList: list];
return NULL;
}
}
return list;
}
// -----------------------------------------------------------------------------
// destroyAudioBufferList:size:
// Dispose of our audio buffer list. A buffer list is the storage we use in
// our AudioInputProc to get the sound data and hand it on to the sound
// file writer.
// -----------------------------------------------------------------------------
-(void) destroyAudioBufferList: (AudioBufferList*)list
{
UInt32 i = 0;
if( list )
{
for( i = 0; i < list->mNumberBuffers; i++ )
{
if( list->mBuffers[i].mData )
free( list->mBuffers[i].mData );
}
free( list );
}
}
@end
UKSoundFileRecorder header preview
//
// UKSoundFileRecorder.h
// UKSoundFileRecorder
//
// Created by Uli Kusterer on 14.07.07.
// Copyright 2007 M. Uli Kusterer. All rights reserved.
//
/*
A class that records audio from standard sound input into a file.
To use, simply create a new UKSoundFileRecorder object and point it at
a file on disk using -setOutputFilePath:. You can also specify an output
format if you wish, default is 44000kHz AAC Stereo.
The class /should/ be KVC/KVO compliant, but this hasn't been extensively
tested yet. Please let me know of any problems you have using this with
bindings.
*/
// -----------------------------------------------------------------------------
// Headers:
// -----------------------------------------------------------------------------
#import <Cocoa/Cocoa.h>
#import <AudioUnit/AudioUnit.h>
#import <AudioToolbox/AudioToolbox.h>
#import "UKAudioStreamBasicDescription.h"
// -----------------------------------------------------------------------------
// UKSoundFileRecorder:
// -----------------------------------------------------------------------------
@interface UKSoundFileRecorder : NSObject
{
AudioBufferList * audioBuffer;
AudioUnit audioUnit;
ExtAudioFileRef outputAudioFile;
NSString * outputFilePath;
AudioDeviceID inputDeviceID;
UInt32 audioChannels;
UInt32 audioSamples;
AudioStreamBasicDescription actualOutputFormat;
AudioStreamBasicDescription deviceFormat;
NSDictionary* outputFormat;
double currSeconds;
UInt64 startHostTime;
BOOL isRecording;
id delegate;
BOOL delegateWantsTimeChanges;
}
+(NSDictionary*) defaultOutputFormat;
//-(id) init; // Designated initializer. You can use -init and then do setOutputFilePath: or use -initWithOutputFilePath:.
-(id) initWithOutputFilePath: (NSString*)ofp;
// Setup:
-(void) setOutputFilePath: (NSString*)ofp;
-(NSString*) outputFilePath;
-(IBAction) takeOutputFilePathFrom: (id)sender; // Calls [self setOutputFilePath: [sender stringValue]].
-(void) setOutputFormat: (NSDictionary*)inASBD; // Keys for this dictionary can be found in UKAudioStreamBasicDescription.h and below.
-(NSDictionary*) outputFormat;
-(void) setDelegate: (id)dele;
-(id) delegate;
// Recording:
-(void) start: (id)sender;
-(BOOL) isRecording;
-(void) stop: (id)sender;
// You probably don't need this:
-(void) prepare; // Called as needed by start:, if nobody called it before that.
@end
// -----------------------------------------------------------------------------
// Additional OutputFormat keys:
// -----------------------------------------------------------------------------
#define UKAudioOutputFileType @"UKAudioOutputFileFormat" // This is not an HFS OSType, nor a file suffix!!! These are equivalent to AudioFileTypeID, just that they've been stringified using UKStringFromAudioStreamFormatID().
#define UKAudioOutputFileTypeAIFF @"AIFF"
#define UKAudioOutputFileTypeAIFC @"AIFC"
#define UKAudioOutputFileTypeWAVE @"WAVE"
#define UKAudioOutputFileTypeSoundDesigner2 @"Sd2f"
#define UKAudioOutputFileTypeNext @"NeXT"
#define UKAudioOutputFileTypeMP3 @"MPG3"
#define UKAudioOutputFileTypeAC3 @"ac-3"
#define UKAudioOutputFileTypeAAC_ADTS @"adts"
#define UKAudioOutputFileTypeMPEG4 @"mp4f"
#define UKAudioOutputFileTypeM4A @"m4af"
#define UKAudioOutputFileTypeCAF @"caff"
// -----------------------------------------------------------------------------
// Delegate protocol:
// -----------------------------------------------------------------------------
@interface NSObject (UKSoundFileRecorderDelegate)
// Sent on a successful start:
-(void) soundFileRecorderWasStarted: (UKSoundFileRecorder*)sender;
// Sent while we're recording:
-(void) soundFileRecorder: (UKSoundFileRecorder*)sender reachedDuration: (NSTimeInterval)timeInSeconds;
// Sent after a successful stop:
-(void) soundFileRecorderWasStopped: (UKSoundFileRecorder*)sender;
@end
Download Archive
Dependencies:
Compatible with:
- Mac OS X 10.3
- Mac OS X 10.4 PPC
- Mac OS X 10.4 Intel

Excellent example! It is the first time I saw a complete example using no C++ code at all, just pure Objective-C and C.