Jak mogę nagrać rozmowę / rozmowę telefoniczną na iOS?

Czy teoretycznie możliwe jest nagrywanie rozmowy telefonicznej na iPhonie?

Akceptuję odpowiedzi, które:

    Może, ale nie musi, wymagać, aby telefon był jailbroken W przeciwieństwie do innych API, apple nie może korzystać z API (Nie obchodzi mnie to, nie jest dla App Store).]} Może lub nie może używać prywatnych zestawów SDK

Nie chcę odpowiedzi po prostu bez ogródek mówiąc "Apple na to nie pozwala". Wiem, że nie byłoby oficjalnego sposobu, aby to zrobić, a na pewno nie dla aplikacji App Store i Wiem, że istnieją aplikacje do nagrywania połączeń, które umieszczają połączenia wychodzące za pośrednictwem własnych serwerów.

Author: TylerH, 2009-11-27

5 answers

Proszę. Kompletny przykład pracy. Tweak powinien być załadowany do demona mediaserverd. Rejestruje każdą rozmowę telefoniczną w /var/mobile/Media/DCIM/result.m4a. Plik Audio ma dwa kanały. Po lewej mikrofon, po prawej głośnik. W telefonie iPhone 4S połączenie jest nagrywane tylko wtedy, gdy głośnik jest włączony. Na iPhone 5, 5C i 5S połączenie jest nagrywane w obie strony. Podczas przełączania na/z głośnika może wystąpić mała czkawka, ale nagrywanie będzie kontynuowane.

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

Kilka słów o tym, co się dzieje. AudioUnitProcess funkcja służy do przetwarzania strumienie audio w celu zastosowania niektórych efektów, miksowania, konwersji itp. Podłączamy się AudioUnitProcess, aby uzyskać dostęp do strumieni audio rozmowy telefonicznej. Podczas gdy połączenie telefoniczne jest aktywne, strumienie te są przetwarzane na różne sposoby.

Słuchamy powiadomień CoreTelephony w celu zmiany statusu połączenia telefonicznego. Kiedy otrzymujemy próbki audio, musimy określić, skąd pochodzą-mikrofon lub głośnik. Odbywa się to za pomocą pola componentSubType w strukturze AudioComponentDescription. Może się pomyślisz, dlaczego nie przechowuj AudioUnit obiekty, abyśmy nie musieli sprawdzać componentSubType za każdym razem. Zrobiłem to, ale wszystko zepsuje po włączeniu/wyłączeniu głośnika na iPhone 5, ponieważ Obiekty się zmienią, zostaną odtworzone. Tak więc teraz otwieramy pliki audio (jeden dla mikrofonu i jeden dla głośnika)i zapisujemy w nich sample, po prostu. Po zakończeniu rozmowy telefonicznej otrzymamy odpowiednie powiadomienie Coretelefoniczne i zamkniemy pliki. Mamy dwa oddzielne pliki z dźwiękiem z mikrofonu i głośnika, które musimy / align = "left" / Po to jest void Convert(). Jest to dość proste, jeśli znasz API. Chyba nie muszę tego tłumaczyć, komentarze wystarczą.

O zamkach. Istnieje wiele wątków w mediaserverd. Przetwarzanie dźwięku i powiadomienia CoreTelephony są w różnych wątkach, więc potrzebujemy jakiejś synchronizacji. Wybrałem spin locks, ponieważ są szybkie i ponieważ szansa na zablokowanie jest niewielka w naszym przypadku. Na iPhone 4S, a nawet iPhone 5 cała praca w AudioUnitProcess powinna być wykonana tak szybko, jak to możliwe w przeciwnym razie usłyszysz czkawkę z głośnika urządzenia, który oczywiście nie jest dobry.

 67
Author: creker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-30 19:56:32

Tak. [[1]} Nagrywarka Audio przez programistę o nazwie Limneos robi to (i całkiem dobrze). Znajdziesz go na Cydii. Może nagrywać każdy rodzaj rozmowy na iPhone 5 i więcej bez korzystania z żadnych serwerów itp". Połączenie zostanie umieszczone na urządzeniu w pliku Audio. Obsługuje również iPhone 4S, ale tylko dla głośników.

Ten tweak jest znany jako pierwszy tweak w historii, który zdołał nagrać oba strumienie audio bez użycia zewnętrznych serwerów, VOIP lub czegoś podobnego.

Programista umieszczone sygnały dźwiękowe po drugiej stronie połączenia, aby zaalarmować osobę, którą nagrywasz, ale te zostały również usunięte przez hakerów w sieci. Odpowiadając na twoje pytanie, tak, jest to bardzo możliwe, i to nie tylko teoretycznie.

Tutaj wpisz opis obrazka

Czytaj dalej

 9
Author: Segev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:34

Jedynym rozwiązaniem, jakie przychodzi mi do głowy, jest użycie frameworka Core Telephony, a dokładniej właściwości callEventHandler do przechwytywania, gdy nadchodzi połączenie, a następnie użycie Avaudiorecordera do nagrywania głosu osoby za pomocą telefonu (i może trochę głosu osoby na drugiej linii). Oczywiście nie jest to idealne rozwiązanie i działa tylko wtedy, gdy aplikacja jest na pierwszym planie w momencie połączenia, ale może to być najlepsze, co możesz get. Dowiedz się więcej o sprawdzaniu, czy jest połączenie przychodzące tutaj: Czy możemy uruchomić zdarzenie, gdy kiedykolwiek będzie połączenie przychodzące i wychodzące w iphone?.

EDIT:

.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;
}];

Jest to pierwszy raz przy użyciu wielu z tych funkcji, więc nie wiem, czy to jest dokładnie w porządku, ale myślę, że masz pomysł. Nieprzetestowane, ponieważ w tej chwili nie mam dostępu do odpowiednich narzędzi. Opracowano na podstawie materiału źródłowego korzystanie z tych źródeł:

 7
Author: John Farkerson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:25:54

Chyba jakiś sprzęt mógłby to rozwiązać. Podłączony do portu minijack; posiadający słuchawki i mikrofon przechodzący przez mały dyktafon. Ten rejestrator może być bardzo prosty. Podczas gdy nie w rozmowie rejestrator mógł zasilać telefon danymi / nagrywaniem (przez kabel jack). A z prostym przyciskiem start (podobnie jak kontrolki volum na słuchawkach) może być wystarczający do pomiaru czasu nagrywania.

Niektóre setups

 2
Author: hfossli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-28 19:34:39

Apple nie zezwala na to i nie udostępnia dla niego żadnego API.

Jednak na urządzeniu jailbreak jestem pewien, że jest to możliwe. Prawdę mówiąc, myślę, że to już zrobione. Pamiętam, że widziałem aplikację, gdy mój telefon był jailbroken, która zmieniła Twój głos i nagrała połączenie-pamiętam, że była to amerykańska firma oferująca go tylko w Stanach. Niestety nie pamiętam nazwy...

 1
Author: Dimitris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-27 15:33:36