Sunday 15 March 2015

iphone - How can I record a conversation / phone call on iOS? -



iphone - How can I record a conversation / phone call on iOS? -

is theoretically possible record phone phone call on iphone?

i'm accepting answers which:

may or may not require phone jailbroken may or may not pass apple's guidelines due utilize of private api's (i don't care; not app store) may or may not utilize private sdks

i don't want answers bluntly saying "apple not allow that". know there no official way of doing it, , not app store application, , know there phone call recording apps place outgoing calls through own servers.

here go. finish working example. tweak should loaded in mediaserverd daemon. record every phone phone call in /var/mobile/media/dcim/result.m4a. sound file has 2 channels. left microphone, right speaker. on iphone 4s phone call recorded when speaker turned on. on iphone 5, 5c , 5s phone call recorded either way. there might little hiccups when switching to/from speaker recording 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 , mic) extaudiofileopenurl(micurl, &micfile); extaudiofileopenurl(speakerurl, &speakerfile); //reading input file sound format (mono lpcm) audiostreambasicdescription inputformat, outputformat; uint32 descsize = sizeof(inputformat); extaudiofilegetproperty(micfile, kextaudiofileproperty_filedataformat, &descsize, &inputformat); int samplesize = inputformat.mbytesperframe; //filling input stream format output file (stereo lpcm) filloutasbdforlpcm(inputformat, inputformat.msamplerate, 2, inputformat.mbitsperchannel, inputformat.mbitsperchannel, true, false, false); //filling output file sound 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 sound 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 info input files uint32 framestoread = buffersizeinsamples; extaudiofileread(micfile, &framestoread, &micbuffer); extaudiofileread(speakerfile, &framestoread, &speakerbuffer); if (framestoread == 0) { break; } //building interleaved stereo buffer - left channel mic, right - speaker (int = 0; < framestoread; i++) { memcpy((char*)mixbuffer.mbuffers[0].mdata + * samplesize * 2, (char*)micbuffer.mbuffers[0].mdata + * samplesize, samplesize); memcpy((char*)mixbuffer.mbuffers[0].mdata + * samplesize * 2 + samplesize, (char*)speakerbuffer.mbuffers[0].mdata + * samplesize, samplesize); } //writing output file - lpcm converted aac extaudiofilewrite(mixfile, framestoread, &mixbuffer); } //closing files extaudiofiledispose(micfile); extaudiofiledispose(speakerfile); extaudiofiledispose(mixfile); //freeing sound 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* info = (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); homecoming 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 sound format audiostreambasicdescription desc; uint32 descsize = sizeof(desc); audiounitgetproperty(unit, kaudiounitproperty_streamformat, kaudiounitscope_input, 0, &desc, &descsize); //opening sound 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 sound format extaudiofilesetproperty(*currentfile, kextaudiofileproperty_clientdataformat, sizeof(desc), &desc); } cfrelease(url); } else { //writing sound buffer extaudiofilewrite(*currentfile, innumberframes, iodata); } osspinlockunlock(currentlock); } homecoming 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 what's going on. audiounitprocess function used processing sound streams in order apply effects, mix, convert etc. hooking audiounitprocess in order access phone call's sound streams. while phone phone call active these streams beingness processed in various ways.

we listening coretelephony notifications in order phone phone call status changes. when receive sound samples need determine come - microphone or speaker. done using componentsubtype field in audiocomponentdescription structure. now, might think, why don't store audiounit objects don't need check componentsubtype every time. did break when switch speaker on/off on iphone 5 because audiounit objects change, recreated. so, open sound files (one microphone , 1 speaker) , write samples in them, simple that. when phone phone call ends receive appropriate coretelephony notification , close files. have 2 separate files sound microphone , speaker need merge. void convert() for. it's pretty simple if know api. don't think need explain it, comments enough.

about locks. there many threads in mediaserverd. sound processing , coretelephony notifications on different threads need kind synchronization. chose spin locks because fast , because chance of lock contention little in our case. on iphone 4s , iphone 5 work in audiounitprocess should done fast possible otherwise hear hiccups device speaker not good.

ios iphone audio audio-recording

No comments:

Post a Comment