如何在iOS上录制对话/电话?

在iPhone上录制电话理论上是可能的吗?

我接受的答案是:

  • 可能会或可能不需要电话被越狱
  • 可能会或可能不会通过苹果的指导原则,因为使用私有API(我不在乎;它不适用于App Store)
  • 可能会或可能不会使用私人SDK

我不想直截了当地说“苹果不允许这样做”。 我知道没有官方的做法,当然也不适用于App Store应用程序,我知道有通话录音应用程序通过他们自己的服务器进行拨出电话。

干得好。 完整的工作示例。 调整应该加载在mediaserverd守护进程中。 它会logging每个电话/var/mobile/Media/DCIM/result.m4a 。 audio文件有两个通道。 左边是麦克风,右边是扬声器。 在iPhone 4S上,只有当扬声器开启时才会logging通话。 在iPhone 5上,5C和5S呼叫以任一方式logging。 从扬声器切换或从扬声器切换时可能会有小的打嗝,但录音将继续进行。

 #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); } 

几句话是关于怎么回事。 AudioUnitProcess函数用于处理audiostream,以便应用某些效果,混合,转换等。我们正在挂接AudioUnitProcess以访问电话的audiostream。 在通话过程中,这些数据stream正在以各种方式进行处理。

我们正在侦听CoreTelephony通知,以便获取电话状态更改。 当我们收到audio样本时,我们需要确定它们来自哪里 – 麦克风或扬声器。 这是使用AudioComponentDescription结构中的componentSubType字段完成的。 现在,您可能会想,为什么我们不存储AudioUnit对象,以便我们不需要每次都检查componentSubType 。 我做到了这一点,但是当你在iPhone 5上开关扬声器时,它会打破一切,因为AudioUnit对象将会改变,它们会被重新创build。 所以,现在我们打开audio文件(一个用于麦克风,另一个用于扬声器)并在其中写入样本,就这么简单。 当电话结束时,我们将收到适当的CoreTelephony通知并closures文件。 我们有两个单独的文件,包含我们需要合并的麦克风和扬声器的audio。 这是void Convert()的作用。 如果您知道API,这非常简单。 我不认为我需要解释它,评论是足够的。

关于锁。 mediaserverd有很multithreading。 audio处理和CoreTelephony通知在不同的线程上,所以我们需要某种同步。 我select自旋锁是因为它们速度很快,因为在我们的例子中锁争用的可能性很小。 在iPhone 4S甚至iPhone 5上, AudioUnitProcess所有工作都应该尽快完成,否则您会听到设备扬声器的打嗝,这显然不是很好。

是。 录音机由一个名叫Limneos的开发者做了(而且相当好)。 你可以在Cydia上find它。 它可以在iPhone 5上logging任何types的呼叫,而无需使用任何服务器等。 通话将被放置在设备上的一个audio文件中。 它也支持iPhone 4S,但只用于扬声器。

这种调整是有史以来第一次设法logging这两个audiostream,而不使用任何第三方服务器,VOIP或类似的东西。

开发者在通话的另一边发出嘟嘟声,提醒你正在录制的人,但是那些被networking上的黑客也删除了。 回答你的问题,是的,这是非常可能的,而不仅仅是理论上的。

在这里输入图像描述

进一步阅读

我能想到的唯一的解决scheme是使用核心电话框架,更具体地说, callEventHandler属性来拦截来电时,然后使用AVAudioRecorderlogging用电话的人的声音(也许另一个人的声音中的一点点)。 这显然不是完美的,只有当你的应用程序在通话时处于前台才能工作,但它可能是最好的,你可以得到。 有关发现是否有来电的详细信息,请参阅以下内容: 可以在iPhone中有传入和传出电话时触发事件吗? 。

编辑:

。H:

 #import <AVFoundation/AVFoundation.h> #import<CoreTelephony/CTCallCenter.h> #import<CoreTelephony/CTCall.h> @property (strong, nonatomic) AVAudioRecorder *audioRecorder; 

viewDidLoad中:

 NSArray *dirPaths; NSString *docsDir; dirPaths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); docsDir = dirPaths[0]; NSString *soundFilePath = [docsDir stringByAppendingPathComponent:@"sound.caf"]; NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath]; NSDictionary *recordSettings = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:AVAudioQualityMin], AVEncoderAudioQualityKey, [NSNumber numberWithInt:16], AVEncoderBitRateKey, [NSNumber numberWithInt: 2], AVNumberOfChannelsKey, [NSNumber numberWithFloat:44100.0], AVSampleRateKey, nil]; NSError *error = nil; _audioRecorder = [[AVAudioRecorder alloc] initWithURL:soundFileURL settings:recordSettings error:&error]; if (error) { NSLog(@"error: %@", [error localizedDescription]); } else { [_audioRecorder prepareToRecord]; } CTCallCenter *callCenter = [[CTCallCenter alloc] init]; [callCenter setCallEventHandler:^(CTCall *call) { if ([[call callState] isEqual:CTCallStateConnected]) { [_audioRecorder record]; } else if ([[call callState] isEqual:CTCallStateDisconnected]) { [_audioRecorder stop]; } }]; 

AppDelegate.m:

 - (void)applicationDidEnterBackground:(UIApplication *)application//Makes sure that the recording keeps happening even when app is in the background, though only can go for 10 minutes. { __block UIBackgroundTaskIdentifier task = 0; task=[application beginBackgroundTaskWithExpirationHandler:^{ NSLog(@"Expiration handler called %f",[application backgroundTimeRemaining]); [application endBackgroundTask:task]; task=UIBackgroundTaskInvalid; }]; 

这是第一次使用这些function,所以不知道这是否是完全正确的,但我认为你明白了。 未经testing,因为目前我无法使用正确的工具。 编译使用这些来源:

我想有些硬件可以解决这个问题。 连接到小型港口; 有耳塞和麦克风穿过小录音机。 这个录音机可以很简单。 在没有通话的情况下,录音机可以通过数据/录音(通过插孔电缆)给手机供电。 用一个简单的开始button(就像耳塞上的音量控制器)可以足够用于定时logging。

一些设置

苹果不允许它,并不提供任何API。

但是,在一个越狱的设备上,我相信这是可能的。 事实上,我认为它已经完成了。 我记得当我的手机遭到越狱后,看到一个应用程序,改变了你的声音,并logging了电话 – 我记得这是一家美国公司,只在美国提供。 不幸的是我不记得这个名字…