Domanda

È teoricamente possibile registrare una telefonata su iPhone?

Accetto risposte che:

  • può richiedere o meno il jailbreak del telefono
  • può o meno approvare le linee guida di apple a causa dell'uso di API private (non mi interessa; non è per l'App Store)
  • può o meno utilizzare SDK privati ??

Non voglio risposte solo dicendo senza mezzi termini " Apple non lo consente " ;. So che non ci sarebbe un modo ufficiale per farlo, e certamente non per un'applicazione dell'App Store, e so che ci sono app di registrazione delle chiamate che effettuano chiamate in uscita attraverso i propri server.

È stato utile?

Soluzione

Ecco qua. Esempio di lavoro completo. Tweak dovrebbe essere caricato nel demone mediaserverd . Registrerà ogni telefonata in /var/mobile/Media/DCIM/result.m4a . Il file audio ha due canali. Sinistra è il microfono, a destra è l'altoparlante. Su iPhone 4S la chiamata viene registrata solo quando l'altoparlante è acceso. Su iPhone 5, le chiamate 5C e 5S vengono registrate in entrambi i modi. Potrebbero verificarsi piccoli singhiozzi quando si passa da / verso l'altoparlante ma la registrazione continuerà.

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

Qualche parola su ciò che sta succedendo. La funzione AudioUnitProcess viene utilizzata per elaborare flussi audio al fine di applicare alcuni effetti, mixare, convertire ecc. Stiamo collegando AudioUnitProcess per accedere ai flussi audio delle chiamate telefoniche. Mentre la telefonata è attiva, questi flussi vengono elaborati in vari modi.

Stiamo ascoltando le notifiche di CoreTelephony al fine di ottenere modifiche allo stato delle chiamate telefoniche. Quando riceviamo campioni audio, dobbiamo determinare da dove provengono: microfono o altoparlante. Questo viene fatto usando il campo componentSubType nella struttura AudioComponentDescription . Ora, potresti pensare, perché non archiviamo oggetti AudioUnit in modo che non abbiamo bisogno di controllare componentSubType ogni volta. L'ho fatto, ma si romperà tutto quando si accende / spegne l'altoparlante su iPhone 5 perché gli oggetti AudioUnit cambieranno, verranno ricreati. Quindi, ora apriamo i file audio (uno per il microfono e uno per l'altoparlante) e scriviamo campioni in essi, semplici come quello. Al termine della telefonata, riceveremo la notifica CoreTelephony appropriata e chiuderemo i file. Abbiamo due file separati con audio da microfono e altoparlante che dobbiamo unire. Questo è a cosa serve void Convert () . È abbastanza semplice se conosci l'API. Non credo di doverlo spiegare, i commenti sono sufficienti.

Informazioni sui blocchi. Ci sono molti thread in mediaserverd . Le notifiche di elaborazione audio e CoreTelephony sono su thread diversi, quindi abbiamo bisogno di una sorta di sincronizzazione. Ho scelto i lucchetti di spin perché sono veloci e perché nel nostro caso la possibilità di contesa di lucchetti è piccola. Su iPhone 4S e persino iPhone 5 tutto il lavoro in AudioUnitProcess dovrebbe essere eseguito il più velocemente possibile, altrimenti sentirai singhiozzi dall'altoparlante del dispositivo che ovviamente non va bene.

Altri suggerimenti

Sì. Audio Recorder di uno sviluppatore di nome Limneos lo fa (e abbastanza bene). Lo puoi trovare su Cydia. Può registrare qualsiasi tipo di chiamata su iPhone 5 e versioni successive senza utilizzare alcun server ecc. ' La chiamata verrà inserita sul dispositivo in un file audio. Supporta anche iPhone 4S ma solo per altoparlanti.

Questo tweak è noto per essere stato il primo tweak in assoluto che è riuscito a registrare entrambi i flussi audio senza utilizzare server di terze parti, VOIP o qualcosa di simile.

Lo sviluppatore ha emesso dei segnali acustici sull'altro lato della chiamata per avvisare la persona che stai registrando ma anche questi sono stati rimossi dagli hacker attraverso la rete. Per rispondere alla tua domanda, Sì, è molto possibile, e non solo teoricamente.

inserisci qui la descrizione dell

Ulteriori letture

L'unica soluzione che mi viene in mente è di usare Core Telephony , e più in particolare il callEventHandler , per intercettare quando arriva una chiamata e quindi usare una AVAudioRecorder per registrare la voce della persona con il telefono (e forse un po 'della persona sulla voce dell'altra linea). Questo ovviamente non è perfetto e funzionerebbe solo se la tua applicazione è in primo piano al momento della chiamata, ma potrebbe essere la migliore che puoi ottenere. Ulteriori informazioni su come scoprire se c'è una telefonata in arrivo qui: Possiamo lanciare un evento quando ci sono mai chiamate in entrata e in uscita in iphone? .

Modifica

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

Questa è la prima volta che usi molte di queste funzionalità, quindi non sono sicuro che sia esattamente giusto, ma penso che tu abbia avuto l'idea. Non testato, poiché al momento non ho accesso agli strumenti giusti. Compilato utilizzando queste fonti:

Apple non lo consente e non fornisce alcuna API per esso.

Tuttavia, su un dispositivo jailbreak sono sicuro che sia possibile. È un dato di fatto, penso che sia già stato fatto. Ricordo di aver visto un'app quando il mio telefono era in jailbreak che ha cambiato la tua voce e registrato la chiamata - Ricordo che era una società americana che la offriva solo negli Stati Uniti. Purtroppo non ricordo il nome ...

Suppongo che alcuni hardware potrebbero risolvere questo problema. Collegato alla porta minijack; avere auricolari e un microfono che passa attraverso un piccolo registratore. Questo registratore può essere molto semplice. Mentre non è in conversazione, il registratore potrebbe alimentare il telefono con i dati / la registrazione (tramite il cavo jack). E con un semplice pulsante di avvio (proprio come i controlli volum sugli auricolari) potrebbe essere sufficiente per sincronizzare la registrazione.

Alcune configurazioni

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top