音频代码2
My work on this topic has been staggering and long. I have finally gotten this to work however hacked it may be. Because of that I will list some warnings prior to posting the answer:
-
There is still a clicking noise between buffers
-
I get warnings due to the way I use my obj-c classes in the obj-c++ class, so there is something wrong there (however from my research using a pool does the same as release so I dont believe this matters to much):
Object 0x13cd20 of class __NSCFString autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug
-
In order to get this working I had to comment out all AQPlayer references from SpeakHereController (see below) due to errors I couldnt fix any other way. It didnt matter for me however since I am only recording
So the main answer to the above is that there is a bug in AVAssetWriter that stopped it from appending the bytes and writing the audio data. I finally found this out after contacting apple support and have them notify me about this. As far as I know the bug is specific to ulaw and AVAssetWriter though I havnt tried many other formats to verify.
In response to this the only other option is/was to use AudioQueues. Something I had tried before but had brought a bunch of problems. The biggest probably being my lack of knowledge in obj-c++. The class below that got things working is from the speakHere example with slight changes so that the audio is ulaw formatted. The other problems came about trying to get all files to play nicely. However this was easily remedied by changing all filenames in the chain to .mm . The next problem was trying to use the classes in harmony. This is still a WIP, and ties into warning number 2. But my basic solution to this was to use the SpeakHereController (also included in the speakhere example) instead of directly accessing AQRecorder.
Anyways here is the code:
Using the SpeakHereController from an obj-c class
.h
@property(nonatomic,strong)SpeakHereController* recorder;
.mm
[init method]
//AQRecorder wrapper (SpeakHereController) allocation
_recorder =[[SpeakHereController alloc]init];
//AQRecorder wrapper (SpeakHereController) initialization
//technically this class is a controller and thats why its init method is awakeFromNib
[_recorder awakeFromNib];
[recording]
bool buttonState =self.audioRecord.isSelected;
[self.audioRecord setSelected:!buttonState];
if([self.audioRecord isSelected]){
[self.recorder startRecord];
}else{
[self.recorder stopRecord];
}
SpeakHereController
#import "SpeakHereController.h"
@implementationSpeakHereController
@synthesize player;
@synthesize recorder;
@synthesize btn_record;
@synthesize btn_play;
@synthesize fileDescription;
@synthesize lvlMeter_in;
@synthesize playbackWasInterrupted;
char*OSTypeToStr(char*buf,OSType t)
{
char*p = buf;
char str[4],*q = str;
*(UInt32*)str =CFSwapInt32(t);
for(int i =0; i <4;++i){
if(isprint(*q)&&*q !='\\')
*p++=*q++;
else{
sprintf(p,"\\x%02x",*q++);
p +=4;
}
}
*p ='\0';
return buf;
}
-(void)setFileDescriptionForFormat:(CAStreamBasicDescription)format withName:(NSString*)name
{
char buf[5];
constchar*dataFormat =OSTypeToStr(buf, format.mFormatID);
NSString* description =[[NSString alloc] initWithFormat:@"(%d ch. %s @ %g Hz)", format.NumberChannels(), dataFormat, format.mSampleRate,nil];
fileDescription.text = description;
[description release];
}
#pragma mark Playback routines
-(void)stopPlayQueue
{
// player->StopQueue();
[lvlMeter_in setAq:nil];
btn_record.enabled = YES;
}
-(void)pausePlayQueue
{
// player->PauseQueue();
playbackWasPaused = YES;
}
-(void)startRecord
{
// recorder = new AQRecorder();
if(recorder->IsRunning())// If we are currently recording, stop and save the file.
{
[self stopRecord];
}
else// If we're not recording, start.
{
// btn_play.enabled = NO;
// Set the button's state to "stop"
// btn_record.title = @"Stop";
// Start the recorder
recorder->StartRecord(CFSTR("recordedFile.caf"));
[self setFileDescriptionForFormat:recorder->DataFormat() withName:@"Recorded File"];
// Hook the level meter up to the Audio Queue for the recorder
// [lvlMeter_in setAq: recorder->Queue()];
}
}
-(void)stopRecord
{
// Disconnect our level meter from the audio queue
// [lvlMeter_in setAq: nil];
recorder->StopRecord();
// dispose the previous playback queue
// player->DisposeQueue(true);
// now create a new queue for the recorded file
recordFilePath =(CFStringRef)[NSTemporaryDirectory() stringByAppendingPathComponent:@"recordedFile.caf"];
// player->CreateQueueForFile(recordFilePath);
// Set the button's state back to "record"
// btn_record.title = @"Record";
// btn_play.enabled = YES;
}
-(IBAction)play:(id)sender
{
if(player->IsRunning())
{
if(playbackWasPaused){
// OSStatus result = player->StartQueue(true);
// if (result == noErr)
// [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueResumed" object:self];
}
else
// [self stopPlayQueue];
nil;
}
else
{
// OSStatus result = player->StartQueue(false);
// if (result == noErr)
// [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueResumed" object:self];
}
}
-(IBAction)record:(id)sender
{
if(recorder->IsRunning())// If we are currently recording, stop and save the file.
{
[self stopRecord];
}
else// If we're not recording, start.
{
// btn_play.enabled = NO;
//
// // Set the button's state to "stop"
// btn_record.title = @"Stop";
// Start the recorder
recorder->StartRecord(CFSTR("recordedFile.caf"));
[self setFileDescriptionForFormat:recorder->DataFormat() withName:@"Recorded File"];
// Hook the level meter up to the Audio Queue for the recorder
[lvlMeter_in setAq: recorder->Queue()];
}
}
#pragma mark AudioSession listeners
void interruptionListener( void* inClientData,
UInt32 inInterruptionState)
{
SpeakHereController*THIS =(SpeakHereController*)inClientData;
if(inInterruptionState == kAudioSessionBeginInterruption)
{
if(THIS->recorder->IsRunning()){
[THIS stopRecord];
}
elseif(THIS->player->IsRunning()){
//the queue will stop itself on an interruption, we just need to update the UI
[[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueStopped"object:THIS];
THIS->playbackWasInterrupted = YES;
}
}
elseif((inInterruptionState == kAudioSessionEndInterruption)&& THIS->playbackWasInterrupted)
{
// we were playing back when we were interrupted, so reset and resume now
// THIS->player->StartQueue(true);
[[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueResumed"object:THIS];
THIS->playbackWasInterrupted = NO;
}
}
void propListener( void* inClientData,
AudioSessionPropertyID inID,
UInt32 inDataSize,
constvoid* inData)
{
SpeakHereController*THIS =(SpeakHereController*)inClientData;
if(inID == kAudioSessionProperty_AudioRouteChange)
{
CFDictionaryRef routeDictionary =(CFDictionaryRef)inData;
//CFShow(routeDictionary);
CFNumberRef reason =(CFNumberRef)CFDictionaryGetValue(routeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_Reason));
SInt32 reasonVal;
CFNumberGetValue(reason, kCFNumberSInt32Type,&reasonVal);
if(reasonVal != kAudioSessionRouteChangeReason_CategoryChange)
{
/*CFStringRef oldRoute = (CFStringRef)CFDictionaryGetValue(routeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_OldRoute));
if (oldRoute)
{
printf("old route:\n");
CFShow(oldRoute);
}
else
printf("ERROR GETTING OLD AUDIO ROUTE!\n");
CFStringRef newRoute;
UInt32 size; size = sizeof(CFStringRef);
OSStatus error = AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &size, &newRoute);
if (error) printf("ERROR GETTING NEW AUDIO ROUTE! %d\n", error);
else
{
printf("new route:\n");
CFShow(newRoute);
}*/
if(reasonVal == kAudioSessionRouteChangeReason_OldDeviceUnavailable)
{
if(THIS->player->IsRunning()){
[THIS pausePlayQueue];
[[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueStopped"object:THIS];
}
}
// stop the queue if we had a non-policy route change
if(THIS->recorder->IsRunning()){
[THIS stopRecord];
}
}
}
elseif(inID == kAudioSessionProperty_AudioInputAvailable)
{
if(inDataSize ==sizeof(UInt32)){
UInt32 isAvailable =*(UInt32*)inData;
// disable recording if input is not available
THIS->btn_record.enabled =(isAvailable >0)? YES : NO;
}
}
}
#pragma mark Initialization routines
-(void)awakeFromNib
{
// Allocate our singleton instance for the recorder & player object
recorder =newAQRecorder();
player =nil;//new AQPlayer();
OSStatus error =AudioSessionInitialize(NULL, NULL, interruptionListener,self);
if(error) printf("ERROR INITIALIZING AUDIO SESSION! %d\n", error);
else
{
UInt32 category = kAudioSessionCategory_PlayAndRecord;
error =AudioSessionSetProperty(kAudioSessionProperty_AudioCategory,sizeof(category),&category);
if(error) printf("couldn't set audio category!");
error =AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, propListener,self);
if(error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", error);
UInt32 inputAvailable =0;
UInt32 size =sizeof(inputAvailable);
// we do not want to allow recording if input is not available
error =AudioSessionGetProperty(kAudioSessionProperty_AudioInputAvailable,&size,&inputAvailable);
if(error) printf("ERROR GETTING INPUT AVAILABILITY! %d\n", error);
// btn_record.enabled = (inputAvailable) ? YES : NO;
// we also need to listen to see if input availability changes
error =AudioSessionAddPropertyListener(kAudioSessionProperty_AudioInputAvailable, propListener,self);
if(error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", error);
error =AudioSessionSetActive(true);
if(error) printf("AudioSessionSetActive (true) failed");
}
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackQueueStopped:) name:@"playbackQueueStopped" object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackQueueResumed:) name:@"playbackQueueResumed" object:nil];
// UIColor *bgColor = [[UIColor alloc] initWithRed:.39 green:.44 blue:.57 alpha:.5];
// [lvlMeter_in setBackgroundColor:bgColor];
// [lvlMeter_in setBorderColor:bgColor];
// [bgColor release];
// disable the play button since we have no recording to play yet
// btn_play.enabled = NO;
// playbackWasInterrupted = NO;
// playbackWasPaused = NO;
}
# pragma mark Notification routines
-(void)playbackQueueStopped:(NSNotification*)note
{
btn_play.title =@"Play";
[lvlMeter_in setAq:nil];
btn_record.enabled = YES;
}
-(void)playbackQueueResumed:(NSNotification*)note
{
btn_play.title =@"Stop";
btn_record.enabled = NO;
[lvlMeter_in setAq: player->Queue()];
}
#pragma mark Cleanup
-(void)dealloc
{
[btn_record release];
[btn_play release];
[fileDescription release];
[lvlMeter_in release];
// delete player;
delete recorder;
[super dealloc];
}
@end
AQRecorder (.h has 2 lines of importance
#define kNumberRecordBuffers 3
#define kBufferDurationSeconds 5.0
)
#include"AQRecorder.h"
//#include "UploadAudioWrapperInterface.h"
//#include "RestClient.h"
RestClient* restClient;
NSData* data;
// ____________________________________________________________________________________
// Determine the size, in bytes, of a buffer necessary to represent the supplied number
// of seconds of audio data.
intAQRecorder::ComputeRecordBufferSize(constAudioStreamBasicDescription*format,float seconds)
{
int packets, frames, bytes =0;
try{
frames =(int)ceil(seconds * format->mSampleRate);
if(format->mBytesPerFrame >0)
bytes = frames * format->mBytesPerFrame;
else{
UInt32 maxPacketSize;
if(format->mBytesPerPacket >0)
maxPacketSize = format->mBytesPerPacket; // constant packet size
else{
UInt32 propertySize =sizeof(maxPacketSize);
XThrowIfError(AudioQueueGetProperty(mQueue, kAudioQueueProperty_MaximumOutputPacketSize,&maxPacketSize,
&propertySize),"couldn't get queue's maximum output packet size");
}
if(format->mFramesPerPacket >0)
packets = frames / format->mFramesPerPacket;
else
packets = frames; // worst-case scenario: 1 frame in a packet
if(packets ==0) // sanity check
packets =1;
bytes = packets * maxPacketSize;
}
}catch(CAXException e){
char buf[256];
fprintf(stderr,"Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
return0;
}
return bytes;
}
// ____________________________________________________________________________________
// AudioQueue callback function, called when an input buffers has been filled.
voidAQRecorder::MyInputBufferHandler( void* inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
constAudioTimeStamp* inStartTime,
UInt32 inNumPackets,
constAudioStreamPacketDescription* inPacketDesc)
{
AQRecorder*aqr =(AQRecorder*)inUserData;
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;
// int numBytes = inBuffer->mAudioDataByteSize;
// SInt8 *testBuffer = (SInt8*)inBuffer->mAudioData;
//
// for (int i=0; i < numBytes; i++)
// {
// SInt8 currentData = testBuffer[i];
// printf("Current data in testbuffer is %d", currentData);
//
// NSData * temp = [NSData dataWithBytes:currentData length:sizeof(currentData)];
// }
data=[[NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize]retain];
[restClient uploadAudioData:data url:nil];
}
// 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");
}catch(CAXException e){
char buf[256];
fprintf(stderr,"Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
}
}
AQRecorder::AQRecorder()
{
mIsRunning =false;
mRecordPacket =0;
data =[[NSData alloc]init];
restClient =[[RestClient sharedManager]retain];
}
AQRecorder::~AQRecorder()
{
AudioQueueDispose(mQueue, TRUE);
AudioFileClose(mRecordFile);
if(mFileName){
CFRelease(mFileName);
}
[restClient release];
[data release];
}
// ____________________________________________________________________________________
// Copy a queue's encoder's magic cookie to an audio file.
voidAQRecorder::CopyEncoderCookieToFile()
{
UInt32 propertySize;
// get the magic cookie, if any, from the converter
OSStatus err =AudioQueueGetPropertySize(mQueue, kAudioQueueProperty_MagicCookie,&propertySize);
// we can get a noErr result and also a propertySize == 0
// -- if the file format does support magic cookies, but this file doesn't have one.
if(err == noErr && propertySize >0){
Byte*magicCookie =newByte[propertySize];
UInt32 magicCookieSize;
XThrowIfError(AudioQueueGetProperty(mQueue, kAudioQueueProperty_MagicCookie, magicCookie,&propertySize),"get audio converter's magic cookie");
magicCookieSize = propertySize;// the converter lies and tell us the wrong size
// now set the magic cookie on the output file
UInt32 willEatTheCookie =false;
// the converter wants to give us one; will the file take it?
err =AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL,&willEatTheCookie);
if(err == noErr && willEatTheCookie){
err =AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, magicCookieSize, magicCookie);
XThrowIfError(err,"set audio file's magic cookie");
}
delete[] magicCookie;
}
}
voidAQRecorder::SetupAudioFormat(UInt32 inFormatID)
{
memset(&mRecordFormat,0,sizeof(mRecordFormat));
UInt32 size =sizeof(mRecordFormat.mSampleRate);
XThrowIfError(AudioSessionGetProperty( kAudioSessionProperty_CurrentHardwareSampleRate,
&size,
&mRecordFormat.mSampleRate),"couldn't get hardware sample rate");
//override samplearate to 8k from device sample rate
mRecordFormat.mSampleRate =8000.0;
size =sizeof(mRecordFormat.mChannelsPerFrame);
XThrowIfError(AudioSessionGetProperty( kAudioSessionProperty_CurrentHardwareInputNumberChannels,
&size,
&mRecordFormat.mChannelsPerFrame),"couldn't get input channel count");
// mRecordFormat.mChannelsPerFrame = 1;
mRecordFormat.mFormatID = inFormatID;
if(inFormatID == kAudioFormatLinearPCM)
{
// if we want pcm, default to signed 16-bit little-endian
mRecordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
mRecordFormat.mBitsPerChannel =16;
mRecordFormat.mBytesPerPacket = mRecordFormat.mBytesPerFrame =(mRecordFormat.mBitsPerChannel /8)* mRecordFormat.mChannelsPerFrame;
mRecordFormat.mFramesPerPacket =1;
}
if(inFormatID == kAudioFormatULaw){
// NSLog(@"is ulaw");
mRecordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
mRecordFormat.mSampleRate =8000.0;
// mRecordFormat.mFormatFlags = 0;
mRecordFormat.mFramesPerPacket =1;
mRecordFormat.mChannelsPerFrame =1;
mRecordFormat.mBitsPerChannel =16;//was 8
mRecordFormat.mBytesPerPacket =1;
mRecordFormat.mBytesPerFrame =1;
}
}
NSString*GetDocumentDirectory(void)
{
NSArray*paths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString*basePath =([paths count]>0)?[paths objectAtIndex:0]:nil;
return basePath;
}
voidAQRecorder::StartRecord(CFStringRef inRecordFile)
{
int i, bufferByteSize;
UInt32 size;
CFURLRef url;
try{
mFileName =CFStringCreateCopy(kCFAllocatorDefault, inRecordFile);
// specify the recording format
SetupAudioFormat(kAudioFormatULaw /*kAudioFormatLinearPCM*/);
// create the queue
XThrowIfError(AudioQueueNewInput(
&mRecordFormat,
MyInputBufferHandler,
this/* userData */,
NULL /* run loop */, NULL /* run loop mode */,
0/* flags */,&mQueue),"AudioQueueNewInput failed");
// get the record format back from the queue's audio converter --
// the file may require a more specific stream description than was necessary to create the encoder.
mRecordPacket =0;
size =sizeof(mRecordFormat);
XThrowIfError(AudioQueueGetProperty(mQueue, kAudioQueueProperty_StreamDescription,
&mRecordFormat,&size),"couldn't get queue's format");
NSString*basePath =GetDocumentDirectory();
NSString*recordFile =[basePath /*NSTemporaryDirectory()*/ stringByAppendingPathComponent:(NSString*)inRecordFile];
url =CFURLCreateWithString(kCFAllocatorDefault,(CFStringRef)recordFile, NULL);
// create the audio file
XThrowIfError(AudioFileCreateWithURL(url, kAudioFileCAFType,&mRecordFormat, kAudioFileFlags_EraseFile,
&mRecordFile),"AudioFileCreateWithURL failed");
CFRelease(url);
// copy the cookie first to give the file object as much info as we can about the data going in
// not necessary for pcm, but required for some compressed audio
CopyEncoderCookieToFile();
// allocate and enqueue buffers
bufferByteSize =ComputeRecordBufferSize(&mRecordFormat, kBufferDurationSeconds); // enough bytes for half a second
for(i =0; i < kNumberRecordBuffers;++i){
XThrowIfError(AudioQueueAllocateBuffer(mQueue, bufferByteSize,&mBuffers[i]),
"AudioQueueAllocateBuffer failed");
XThrowIfError(AudioQueueEnqueueBuffer(mQueue, mBuffers[i],0, NULL),
"AudioQueueEnqueueBuffer failed");
}
// start the queue
mIsRunning =true;
XThrowIfError(AudioQueueStart(mQueue, NULL),"AudioQueueStart failed");
}
catch(CAXException&e){
char buf[256];
fprintf(stderr,"Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
}
catch(...){
fprintf(stderr,"An unknown error occurred\n");
}
}
voidAQRecorder::StopRecord()
{
// end recording
mIsRunning =false;
// XThrowIfError(AudioQueueReset(mQueue), "AudioQueueStop failed");
XThrowIfError(AudioQueueStop(mQueue,true),"AudioQueueStop failed");
// a codec may update its cookie at the end of an encoding session, so reapply it to the file now
CopyEncoderCookieToFile();
if(mFileName)
{
CFRelease(mFileName);
mFileName = NULL;
}
AudioQueueDispose(mQueue,true);
AudioFileClose(mRecordFile);
}
Please feel free to comment or refine my answer, I will accept it as the answer if its a better solutions. But PLEASE be kind, this was my first attempt messing with this stuff and Im sure it is not the most elegant or proper solution. It was not my intent just the only way I knew to do it.