Here you go. Full working example. Tweak must be loaded into the mediaserverd daemon. He will record every phone call in /var/mobile/Media/DCIM/result.m4a . The audio file has two channels. On the left is a microphone, on the right is a speaker. On iPhone 4S, a call is recorded only when the speaker is on. On iPhone 5, 5C and 5S, the call is recorded anyway. There may be slight hiccups when switching to / from the speaker, but recording will continue.
#import <AudioToolbox/AudioToolbox.h> #import <libkern/OSAtomic.h> //CoreTelephony.framework extern "C" CFStringRef const kCTCallStatusChangeNotification; extern "C" CFStringRef const kCTCallStatus; extern "C" id CTTelephonyCenterGetDefault(); extern "C" void CTTelephonyCenterAddObserver(id ct, void* observer, CFNotificationCallback callBack, CFStringRef name, void *object, CFNotificationSuspensionBehavior sb); extern "C" int CTGetCurrentCallCount(); enum { kCTCallStatusActive = 1, kCTCallStatusHeld = 2, kCTCallStatusOutgoing = 3, kCTCallStatusIncoming = 4, kCTCallStatusHanged = 5 }; NSString* kMicFilePath = @"/var/mobile/Media/DCIM/mic.caf"; NSString* kSpeakerFilePath = @"/var/mobile/Media/DCIM/speaker.caf"; NSString* kResultFilePath = @"/var/mobile/Media/DCIM/result.m4a"; OSSpinLock phoneCallIsActiveLock = 0; OSSpinLock speakerLock = 0; OSSpinLock micLock = 0; ExtAudioFileRef micFile = NULL; ExtAudioFileRef speakerFile = NULL; BOOL phoneCallIsActive = NO; void Convert() { //File URLs CFURLRef micUrl = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)kMicFilePath, kCFURLPOSIXPathStyle, false); CFURLRef speakerUrl = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)kSpeakerFilePath, kCFURLPOSIXPathStyle, false); CFURLRef mixUrl = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)kResultFilePath, kCFURLPOSIXPathStyle, false); ExtAudioFileRef micFile = NULL; ExtAudioFileRef speakerFile = NULL; ExtAudioFileRef mixFile = NULL; //Opening input files (speaker and mic) ExtAudioFileOpenURL(micUrl, &micFile); ExtAudioFileOpenURL(speakerUrl, &speakerFile); //Reading input file audio format (mono LPCM) AudioStreamBasicDescription inputFormat, outputFormat; UInt32 descSize = sizeof(inputFormat); ExtAudioFileGetProperty(micFile, kExtAudioFileProperty_FileDataFormat, &descSize, &inputFormat); int sampleSize = inputFormat.mBytesPerFrame; //Filling input stream format for output file (stereo LPCM) FillOutASBDForLPCM(inputFormat, inputFormat.mSampleRate, 2, inputFormat.mBitsPerChannel, inputFormat.mBitsPerChannel, true, false, false); //Filling output file audio format (AAC) memset(&outputFormat, 0, sizeof(outputFormat)); outputFormat.mFormatID = kAudioFormatMPEG4AAC; outputFormat.mSampleRate = 8000; outputFormat.mFormatFlags = kMPEG4Object_AAC_Main; outputFormat.mChannelsPerFrame = 2; //Opening output file ExtAudioFileCreateWithURL(mixUrl, kAudioFileM4AType, &outputFormat, NULL, kAudioFileFlags_EraseFile, &mixFile); ExtAudioFileSetProperty(mixFile, kExtAudioFileProperty_ClientDataFormat, sizeof(inputFormat), &inputFormat); //Freeing URLs CFRelease(micUrl); CFRelease(speakerUrl); CFRelease(mixUrl); //Setting up audio buffers int bufferSizeInSamples = 64 * 1024; AudioBufferList micBuffer; micBuffer.mNumberBuffers = 1; micBuffer.mBuffers[0].mNumberChannels = 1; micBuffer.mBuffers[0].mDataByteSize = sampleSize * bufferSizeInSamples; micBuffer.mBuffers[0].mData = malloc(micBuffer.mBuffers[0].mDataByteSize); AudioBufferList speakerBuffer; speakerBuffer.mNumberBuffers = 1; speakerBuffer.mBuffers[0].mNumberChannels = 1; speakerBuffer.mBuffers[0].mDataByteSize = sampleSize * bufferSizeInSamples; speakerBuffer.mBuffers[0].mData = malloc(speakerBuffer.mBuffers[0].mDataByteSize); AudioBufferList mixBuffer; mixBuffer.mNumberBuffers = 1; mixBuffer.mBuffers[0].mNumberChannels = 2; mixBuffer.mBuffers[0].mDataByteSize = sampleSize * bufferSizeInSamples * 2; mixBuffer.mBuffers[0].mData = malloc(mixBuffer.mBuffers[0].mDataByteSize); //Converting while (true) { //Reading data from input files UInt32 framesToRead = bufferSizeInSamples; ExtAudioFileRead(micFile, &framesToRead, &micBuffer); ExtAudioFileRead(speakerFile, &framesToRead, &speakerBuffer); if (framesToRead == 0) { break; } //Building interleaved stereo buffer - left channel is mic, right - speaker for (int i = 0; i < framesToRead; i++) { memcpy((char*)mixBuffer.mBuffers[0].mData + i * sampleSize * 2, (char*)micBuffer.mBuffers[0].mData + i * sampleSize, sampleSize); memcpy((char*)mixBuffer.mBuffers[0].mData + i * sampleSize * 2 + sampleSize, (char*)speakerBuffer.mBuffers[0].mData + i * sampleSize, sampleSize); } //Writing to output file - LPCM will be converted to AAC ExtAudioFileWrite(mixFile, framesToRead, &mixBuffer); } //Closing files ExtAudioFileDispose(micFile); ExtAudioFileDispose(speakerFile); ExtAudioFileDispose(mixFile); //Freeing audio buffers free(micBuffer.mBuffers[0].mData); free(speakerBuffer.mBuffers[0].mData); free(mixBuffer.mBuffers[0].mData); } void Cleanup() { [[NSFileManager defaultManager] removeItemAtPath:kMicFilePath error:NULL]; [[NSFileManager defaultManager] removeItemAtPath:kSpeakerFilePath error:NULL]; } void CoreTelephonyNotificationCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) { NSDictionary* data = (NSDictionary*)userInfo; if ([(NSString*)name isEqualToString:(NSString*)kCTCallStatusChangeNotification]) { int currentCallStatus = [data[(NSString*)kCTCallStatus] integerValue]; if (currentCallStatus == kCTCallStatusActive) { OSSpinLockLock(&phoneCallIsActiveLock); phoneCallIsActive = YES; OSSpinLockUnlock(&phoneCallIsActiveLock); } else if (currentCallStatus == kCTCallStatusHanged) { if (CTGetCurrentCallCount() > 0) { return; } OSSpinLockLock(&phoneCallIsActiveLock); phoneCallIsActive = NO; OSSpinLockUnlock(&phoneCallIsActiveLock); //Closing mic file OSSpinLockLock(&micLock); if (micFile != NULL) { ExtAudioFileDispose(micFile); } micFile = NULL; OSSpinLockUnlock(&micLock); //Closing speaker file OSSpinLockLock(&speakerLock); if (speakerFile != NULL) { ExtAudioFileDispose(speakerFile); } speakerFile = NULL; OSSpinLockUnlock(&speakerLock); Convert(); Cleanup(); } } } OSStatus(*AudioUnitProcess_orig)(AudioUnit unit, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inNumberFrames, AudioBufferList *ioData); OSStatus AudioUnitProcess_hook(AudioUnit unit, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inNumberFrames, AudioBufferList *ioData) { OSSpinLockLock(&phoneCallIsActiveLock); if (phoneCallIsActive == NO) { OSSpinLockUnlock(&phoneCallIsActiveLock); return AudioUnitProcess_orig(unit, ioActionFlags, inTimeStamp, inNumberFrames, ioData); } OSSpinLockUnlock(&phoneCallIsActiveLock); ExtAudioFileRef* currentFile = NULL; OSSpinLock* currentLock = NULL; AudioComponentDescription unitDescription = {0}; AudioComponentGetDescription(AudioComponentInstanceGetComponent(unit), &unitDescription); //'agcc', 'mbdp' - iPhone 4S, iPhone 5 //'agc2', 'vrq2' - iPhone 5C, iPhone 5S if (unitDescription.componentSubType == 'agcc' || unitDescription.componentSubType == 'agc2') { currentFile = &micFile; currentLock = &micLock; } else if (unitDescription.componentSubType == 'mbdp' || unitDescription.componentSubType == 'vrq2') { currentFile = &speakerFile; currentLock = &speakerLock; } if (currentFile != NULL) { OSSpinLockLock(currentLock); //Opening file if (*currentFile == NULL) { //Obtaining input audio format AudioStreamBasicDescription desc; UInt32 descSize = sizeof(desc); AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &desc, &descSize); //Opening audio file CFURLRef url = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)((currentFile == &micFile) ? kMicFilePath : kSpeakerFilePath), kCFURLPOSIXPathStyle, false); ExtAudioFileRef audioFile = NULL; OSStatus result = ExtAudioFileCreateWithURL(url, kAudioFileCAFType, &desc, NULL, kAudioFileFlags_EraseFile, &audioFile); if (result != 0) { *currentFile = NULL; } else { *currentFile = audioFile; //Writing audio format ExtAudioFileSetProperty(*currentFile, kExtAudioFileProperty_ClientDataFormat, sizeof(desc), &desc); } CFRelease(url); } else { //Writing audio buffer ExtAudioFileWrite(*currentFile, inNumberFrames, ioData); } OSSpinLockUnlock(currentLock); } return AudioUnitProcess_orig(unit, ioActionFlags, inTimeStamp, inNumberFrames, ioData); } __attribute__((constructor)) static void initialize() { CTTelephonyCenterAddObserver(CTTelephonyCenterGetDefault(), NULL, CoreTelephonyNotificationCallback, NULL, NULL, CFNotificationSuspensionBehaviorHold); MSHookFunction(AudioUnitProcess, AudioUnitProcess_hook, &AudioUnitProcess_orig); }
A few words about what is happening. The AudioUnitProcess function AudioUnitProcess used to process audio streams, to apply some effects, mix, convert, etc. We connect AudioUnitProcess to access the audio streams of phone calls. While a phone call is active, these flows are handled in various ways.
We listen to CoreTelephony notifications to get phone status changes. When we get sound samples, we need to determine where they came from - a microphone or speaker. This is done using the componentSubType field in the AudioComponentDescription structure. Now you might think why we do not store AudioUnit objects, so we do not need to check componentSubType every time. I did this, but it will break everything when you turn the speaker on / off on the iPhone 5, because the AudioUnit objects change, they are recreated. So, now we open the audio files (one for the microphone and one for the speakers) and record the samples in them, just like that. Upon completion of the telephone call, we will receive a corresponding notification to CoreTelephony and close the files. We have two separate audio files from the microphone and speaker, which we need to combine. To do this, use void Convert() . This is pretty simple if you know the API. I don’t think I need to explain this, there are enough comments.
About castles. There are many threads in mediaserverd . The sound processing and notifications of CoreTelephony belong to different streams, so we need some synchronization. I chose spin-locks because they are fast, and because the probability of blocking in our case is small. On iPhone 4S and even iPhone 5, all work in AudioUnitProcess should be done as quickly as possible, otherwise you will hear hiccups from the device’s speaker, which is clearly not good.