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

Posted

技术标签:

【中文标题】如何在 iOS 上录制对话/电话?【英文标题】:How can I record a conversation / phone call on iOS? 【发布时间】:2009-11-27 15:25:00 【问题描述】:

理论上可以在 iPhone 上录制电话吗?

我接受以下答案:

可能需要也可能不需要手机越狱 由于使用私有 API,可能会也可能不会通过苹果的指导方针(我不在乎;它不适用于 App Store) 可以使用也可以不使用私有 SDK

我不希望只是直截了当地说“Apple 不允许这样做”的答案。 我知道不会有官方的方式来做这件事,当然对于 App Store 应用程序来说也不会,而且我知道有通话录音应用程序通过它们自己的服务器拨打电话。

【问题讨论】:

【参考方案1】:

给你。完整的工作示例。应该在mediaserverd 守护进程中加载​​调整。它将记录/var/mobile/Media/DCIM/result.m4a中的每个电话。音频文件有两个通道。左边是麦克风,右边是扬声器。在 iPhone 4S 上,仅当扬声器打开时才会记录通话。在 iPhone 5 上,5C 和 5S 通话都以任一方式录制。切换到/从扬声器时可能会出现小问题,但录音会继续。

#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 函数用于处理音频流以应用一些效果、混合、转换等。我们正在挂钩 AudioUnitProcess 以访问电话的音频流。当电话处于活动状态时,这些流正在以各种方式进行处理。

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

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

【讨论】:

正是我想要的! 给你。在 iPhone 4S (iOS 6) 和 iPhone 5 (iOS 7) 上测试的完整工作示例。稍后将在 5C 和 5S 上对其进行测试。我会发布结果。 我认为只剩下一件事——让它与电话会议兼容。我现在处理电话通知的方式不适用于电话会议。 添加了电话会议支持。 @orazran,不,它只能在越狱设备上工作。【参考方案2】:

是的。 Audio Recorder 由名为 Limneos 的开发人员完成(而且做得很好)。你可以在 Cydia 上找到它。它可以在 iPhone 5 及更高版本上录制任何类型的通话,而无需使用任何服务器等。呼叫将以音频文件的形式放置在设备上。它还支持 iPhone 4S,但仅限扬声器。

众所周知,此调整是第一个在不使用任何 3rd 方服务器、VOIP 或类似工具的情况下成功录制两个音频流的调整。

开发人员在通话的另一端发出哔哔声以提醒您正在录音的人,但这些声音也被网络上的黑客删除了。回答你的问题,是的,很有可能,而且不仅仅是理论上。

进一步阅读

https://***.com/a/19413363/202451 http://forums.macrumors.com/showthread.php?t=1566350 https://github.com/nst/iOS-Runtime-Headers

【讨论】:

很好的答案。不过,如果它不支持 iOS 7 和 iphone 5s,这已经过时了。 @hfossli 谁说它不支持 iOS 7 和 iPhone 5s?在带有 iOS 7 的 iPhone 5 上运行良好,它支持 iPhone 5S。 :) @hfossli 不知道。 但是他是怎么做到的呢?我渴望知识。 @hfossli 我猜想,很多试验和错误,很多摆弄苹果私有 api 和可能一些低级 c 将所有东西粘合在一起。 github.com/nst/iOS-Runtime-Headers【参考方案3】:

我能想到的唯一解决方案是使用Core Telephony 框架,更具体地说是callEventHandler 属性,在来电时进行拦截,然后使用AVAudioRecorder 来记录对方的声音接电话的人(也许是对方声音中的一小部分人)。这显然不是完美的,只有当您的应用程序在调用时处于前台时才有效,但它可能是您能得到的最好的。在此处查看更多关于确定是否有来电的信息:Can we fire an event when ever there is Incoming and Outgoing call in 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;
];

这是第一次使用其中许多功能,所以不确定这是否完全正确,但我想你明白了。未经测试,因为我目前无法使用正确的工具。使用以下来源编译:

Recording voice in background using AVAudioRecorder

http://prassan-warrior.blogspot.com/2012/11/recording-audio-on-iphone-with.html

Can we fire an event when ever there is Incoming and Outgoing call in iphone?

【讨论】:

在赏金结束前我无法测试这个。所以它仍然是高度理论化的。 啊,太糟糕了。该死的苹果! :) 是否可以让它记录更长的时间——比如 2 小时或更长时间? 有人试试吗?【参考方案4】:

Apple 不允许,也不为其提供任何 API。

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

【讨论】:

是的,我认为这种应用程序仅适用于拨出呼叫,因为它们通过服务器路由呼叫,并在它通过时记录它。 Cydia 中提供了一些类似的应用程序。 是的,正如 TechZen 所说,我说的是 SpoofApp。 是的,SpoofApp 可能会在他们的服务器上记录,因为它在 AppStore 上并且它会记录调用。无论如何,我相信您可以在越狱的 iPhone 上录制通话。这是一台电脑。当您拥有不受限制的访问权限(以及所需的技能)时,您可以做任何您想做的事情。 不一定。可能是 iPhone 的手机部分绕过了软件。 根本没有软件?可能。即使在这种情况下,由于您可以访问麦克风,您可以从中轮询数据并记录它......无论如何,这些都是疯狂的猜测。有越狱设备经验的人可能会给我们更多启发。【参考方案5】:

我想一些硬件可以解决这个问题。连接到 minijack 端口;有耳塞和麦克风通过一个小录音机。这个记录器可以很简单。当不在通话中时,录音机可以为手机提供数据/录音(通过插孔电缆)。只需一个简单的开始按钮(就像耳塞上的音量控制)就足以为录音计时。

一些设置

http://www.danmccomb.com/posts/483/how-to-record-iphone-conversations-using-zoom-h4n/ http://forums.macrumors.com/showthread.php?t=346430

【讨论】:

以上是关于如何在 iOS 上录制对话/电话?的主要内容,如果未能解决你的问题,请参考以下文章

UCMA 录制电话交谈

iOS - 在Objective c中打电话时录制语音

如何在 iOS 上录制 PCM 音频文件?

设备锁定时如何继续录制视频?

如何在 iOS 上使用 ProRes 编解码器录制视频?

iphone编程-如何记录通话