audio - How to record perfect loops in iOS and Xcode -
i've been struggling year trying pin down problem , represent others see.
i've been writing app depends on 'garageband' recording. is, want record user 8 beats, , want them able loop this. playing metronome user @ same time (user wearing head phones hearing metronome, recording mic on device)
i can manage turn on recording 4.8 seconds (.6*8 beats), , timer says ran 4.8 seconds, audio recording bit shorter 4.8. it's 4.78, or 4.71 causes loop go out of whack.
i've experimented avaudiorecorder,audioqueue, , audiounits thinking 1 latter methods might result in solving problem.
i using nstimer fire off every .6 seconds playing short blip metronome. after 4 beats, metronome timer's function, turns on recorder metronnome waits 4.6 seconds stops recording.
i'm using time intervals time how long metro runs (looks pretty tight @ 4.800xxx) , comparing duration of audio file different.
i wish attach project, guess i'll have settle attaching header , implementation. test you'll have make project following ib characteristics:
record, play, stop buttons song/track duration label timer duration label debug label
if launch app, hit record, 'counted in' 4 beats, recorder starts. tap finger on desk until recorder stops. after 8 more beats (12 in total) recorder stops.
you can see in displays recorded track little shorter 4.8 seconds, , in cases, lot shorter, causing audio not loop properly.
does know can tighten up? reading.
here's code:
// // viewcontroller.h // speakagain // // created nothing on 2014-03-18. // #import <uikit/uikit.h> #import <foundation/foundation.h> #import "coreaudio/coreaudiotypes.h" #import <audiotoolbox/audioqueue.h> #import <audiotoolbox/audiofile.h> #import <avfoundation/avfoundation.h> @interface viewcontroller : uiviewcontroller { iboutlet uibutton *btnrecord; iboutlet uibutton *btnplay; iboutlet uibutton *btnstop; iboutlet uilabel *debuglabel; iboutlet uilabel *timerduration; iboutlet uilabel *songduration; //uilabel *labeldebug; struct aqrecorderstate { audiostreambasicdescription mdataformat; audioqueueref mqueue; audioqueuebufferref mbuffers[knumberbuffers]; audiofileid maudiofile; uint32 bufferbytesize; sint64 mcurrentpacket; bool misrunning; // 8 }; struct aqrecorderstate aqdata; avaudioplayer *audioplayer; nsstring *songname; nstimer *recordtimer; nstimer *metrotimer; nstimeinterval starttime, endtime, elapsedtime; int inputbuffer; int beatnumber; } @property (nonatomic, retain) iboutlet uibutton *btnrecord; @property (nonatomic, retain) iboutlet uibutton *btnplay; @property (nonatomic, retain) iboutlet uibutton *btnstop; @property (nonatomic, retain) iboutlet uilabel *debuglabel; @property (nonatomic, retain) iboutlet uilabel *timerduration; @property (nonatomic, retain) iboutlet uilabel *songduration; - (ibaction) record; - (ibaction) stop; - (ibaction) play; static void handleinputbuffer (void *aqdata,audioqueueref inaq,audioqueuebufferref inbuffer,const audiotimestamp *instarttime, uint32 innumpackets,const audiostreampacketdescription *inpacketdesc); @end
implementation:
// // viewcontroller.m // speakagain // // created nothing on 2014-03-18. // #import "viewcontroller.h" @interface viewcontroller () @end @implementation viewcontroller @synthesize btnplay, btnrecord,btnstop,songduration, timerduration, debuglabel; - (void)viewdidload { debuglabel.text = @""; songname =[[nsstring alloc ]init]; //nsarray *paths = nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes); //nsstring *documentsdirectory = [paths objectatindex:0]; songname = @"testingqueue.caf"; [super viewdidload]; // additional setup after loading view, typically nib. } - (void)prepareaudioqueue { //struct aqrecorderstate *paqdata; inputbuffer=0; aqdata.mdataformat.mformatid = kaudioformatlinearpcm; aqdata.mdataformat.msamplerate = 44100.0; aqdata.mdataformat.mchannelsperframe = 1; aqdata.mdataformat.mbitsperchannel = 16; aqdata.mdataformat.mbytesperpacket = aqdata.mdataformat.mbytesperframe = aqdata.mdataformat.mchannelsperframe * sizeof (sint16); aqdata.mdataformat.mframesperpacket = 1; // audiofiletypeid filetype = kaudiofileaifftype; audiofiletypeid filetype = kaudiofilecaftype; aqdata.mdataformat.mformatflags = klinearpcmformatflagisbigendian| klinearpcmformatflagissignedinteger| klinearpcmformatflagispacked; audioqueuenewinput (&aqdata.mdataformat,handleinputbuffer, &aqdata,null, kcfrunloopcommonmodes, 0,&aqdata.mqueue); uint32 dataformatsize = sizeof (aqdata.mdataformat); // in mac os x, instead use // kaudioconvertercurrentinputstreamdescription audioqueuegetproperty (aqdata.mqueue,kaudioqueueproperty_streamdescription,&aqdata.mdataformat,&dataformatsize); //verify nsfilemanager *filemanager = [nsfilemanager defaultmanager]; nsarray *paths = nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes); nsstring *documentsdirectory = [paths objectatindex:0]; nsstring *txtpath = [documentsdirectory stringbyappendingpathcomponent:songname]; nslog(@"initializing file"); if ([filemanager fileexistsatpath:txtpath] == yes) { nslog(@"previous file removed"); [filemanager removeitematpath:txtpath error:nil]; } const char *filepath = [txtpath utf8string]; cfurlref audiofileurl = cfurlcreatefromfilesystemrepresentation ( null,(const uint8 *) filepath,strlen (filepath),false ); audiofilecreatewithurl (audiofileurl,filetype,&aqdata.mdataformat, kaudiofileflags_erasefile,&aqdata.maudiofile ); derivebuffersize (aqdata.mqueue,aqdata.mdataformat,0.5,&aqdata.bufferbytesize); (int = 0; < knumberbuffers; ++i) { audioqueueallocatebuffer (aqdata.mqueue,aqdata.bufferbytesize,&aqdata.mbuffers[i]); audioqueueenqueuebuffer (aqdata.mqueue,aqdata.mbuffers[i], 0,null ); } } - (void) metronomefire { if(beatnumber < 5) { //count in time. // play metro beep don't start recording debuglabel.text = @"count in (1,2,3,4)"; [self playsound]; } if(beatnumber == 5) { //start recording aqdata.mcurrentpacket = 0; aqdata.misrunning = true; starttime = [nsdate timeintervalsincereferencedate]; recordtimer = [nstimer scheduledtimerwithtimeinterval:4.8 target:self selector:@selector(killtimer) userinfo:nil repeats:no]; audioqueuestart (aqdata.mqueue,null); debuglabel.text = @"recording 8 beats (1,2,3,4 1,2,3,4)"; [self playsound]; } else if (beatnumber < 12) { //play metronome beats 6-16 [self playsound]; } if(beatnumber == 12) { [metrotimer invalidate]; metrotimer = nil; [self playsound]; } beatnumber++; } - (ibaction) play { nsarray *paths = nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes); nsstring *documentsdirectory = [paths objectatindex:0]; nsstring *txtpath = [documentsdirectory stringbyappendingpathcomponent:songname]; nsurl *url = [nsurl fileurlwithpath:[nsstring stringwithformat:@"%@",txtpath]]; if (audioplayer) { [audioplayer stop]; audioplayer = nil; } nserror *error; audioplayer = [[avaudioplayer alloc] initwithcontentsofurl:url error:&error]; if (audioplayer == nil) { nslog(@"%@",[error description]); } else { [audioplayer play]; [audioplayer setnumberofloops:-1]; } } - (void) killtimer { //this timer function. runs once after 4.8 seconds. [self stop]; } - (ibaction) stop { if (audioplayer) { [audioplayer stop]; audioplayer = nil; } else { if(metrotimer) { [metrotimer invalidate];metrotimer = nil; } //stop audio queue audioqueuestop (aqdata.mqueue,true); aqdata.misrunning = false; audioqueuedispose (aqdata.mqueue,true); audiofileclose (aqdata.maudiofile); //get elapsed time of timer endtime = [nsdate timeintervalsincereferencedate]; elapsedtime = endtime - starttime; //get elapsed time of audio file nsarray *pathcomponents = [nsarray arraywithobjects: [nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes) lastobject], songname, nil]; nsurl *audiofileurl = [nsurl fileurlwithpathcomponents:pathcomponents]; avurlasset* audioasset = [avurlasset urlassetwithurl:audiofileurl options:nil]; cmtime audioduration = audioasset.duration; float audiodurationseconds = cmtimegetseconds(audioduration); //log values nslog(@"track duration: %f",audiodurationseconds); nslog(@"timer duration: %.6f", elapsedtime); //show values on gui songduration.text = [nsstring stringwithformat: @"track duration: %f",audiodurationseconds]; timerduration.text = [nsstring stringwithformat:@"timer duration: %@",[nsstring stringwithformat: @"%.6f", elapsedtime]]; debuglabel.text = @"why duration of track less duration timer ran?"; } } -(void) playsound { nsstring *path = [[nsbundle mainbundle] pathforresource:@"blip2" oftype:@"aif"]; systemsoundid soundid; audioservicescreatesystemsoundid((__bridge cfurlref)[nsurl fileurlwithpath:path], &soundid); audioservicesplaysystemsound (soundid); } - (ibaction) record { [self prepareaudioqueue]; songduration.text = @""; timerduration.text = @""; //debuglabel.text = @"please wait 12 beats (the first 4 count in)"; //init beat number beatnumber = 1; //safe guard if(aqdata.misrunning) { audioqueuestop (aqdata.mqueue,true); aqdata.misrunning = false; audioqueuedispose (aqdata.mqueue,true); audiofileclose (aqdata.maudiofile); } //start count in (metro start recording) //aqdata.mcurrentpacket = 0; //aqdata.misrunning = true; starttime = [nsdate timeintervalsincereferencedate]; metrotimer = [nstimer scheduledtimerwithtimeinterval:.6 target:self selector:@selector(metronomefire) userinfo:nil repeats:yes]; //recordtimer = [nstimer scheduledtimerwithtimeinterval:4.8 target:self selector:@selector(killtimer) userinfo:nil repeats:no]; //audioqueuestart (aqdata.mqueue,null); } static void handleinputbuffer (void *aqdata,audioqueueref inaq,audioqueuebufferref inbuffer,const audiotimestamp *instarttime,uint32 innumpackets,const audiostreampacketdescription *inpacketdesc) { //boiler plate nslog(@"handleinputbuffer"); struct aqrecorderstate *paqdata = (struct aqrecorderstate *) aqdata; if (innumpackets == 0 && paqdata->mdataformat.mbytesperpacket != 0) innumpackets = inbuffer->maudiodatabytesize / paqdata->mdataformat.mbytesperpacket; if (audiofilewritepackets (paqdata->maudiofile,false,inbuffer->maudiodatabytesize,inpacketdesc,paqdata->mcurrentpacket,&innumpackets,inbuffer->maudiodata) == noerr) { paqdata->mcurrentpacket += innumpackets; } if (paqdata->misrunning == 0) return; audioqueueenqueuebuffer (paqdata->mqueue,inbuffer,0,null); } void derivebuffersize(audioqueueref audioqueue,audiostreambasicdescription asbdescription,float64 seconds,uint32 *outbuffersize) { //boiler plate static const int maxbuffersize = 0x50000; int maxpacketsize = asbdescription.mbytesperpacket; if(maxpacketsize == 0) { uint32 maxvbrpacketsize = sizeof(maxpacketsize); audioqueuegetproperty(audioqueue, kaudioqueueproperty_maximumoutputpacketsize, &maxpacketsize, &maxvbrpacketsize); nslog(@"max buffer = %d",maxpacketsize); } float64 numbytesfortime = asbdescription.msamplerate * maxpacketsize * seconds; *outbuffersize = (uint32)(numbytesfortime < maxbuffersize ? numbytesfortime : maxbuffersize); } osstatus setmagiccookieforfile (audioqueueref inqueue, audiofileid infile) { //boiler plate osstatus result = noerr; uint32 cookiesize; if (audioqueuegetpropertysize (inqueue,kaudioqueueproperty_magiccookie,&cookiesize) == noerr) { char* magiccookie =(char *) malloc (cookiesize); if (audioqueuegetproperty (inqueue,kaudioqueueproperty_magiccookie,magiccookie,&cookiesize) == noerr) { result = audiofilesetproperty (infile,kaudiofilepropertymagiccookiedata,cookiesize,magiccookie); } free (magiccookie); } return result; } - (void)didreceivememorywarning { [super didreceivememorywarning]; // dispose of resources can recreated. } @end
this big topic doubt you'll answer big enough re-architect code you've provided. however, can give links supply vast majority of require.
first thing nstimer
never work due synchronisation issues. also, forget audioqueue
, avaudiorecorder
. audiounit
low level enough needs.
have @ answer here:
ios stream audio 1 ios device another
but true goldmine - , knowledge need intimately familiar - tasty pixel's blog. tasty pixel being vendor of loopy hd, kind enough share pretty in depth knowledge.
see:
a simple, fast circular buffer implementation audio processing
developing loopy, part 2: implementation
and
finally, make sure familiar packets, frames, samples, etc. needs sync perfectly.
Comments
Post a Comment