본문 바로가기
ios 뽀개기/objective-c

ios objective c 코어오디오 다루기 4 - 녹음기능

by 인생여희 2018. 11. 8.
반응형

ios objective c 코어오디오 다루기 4 - 녹음기능


//개발자가 오디오 큐를 생성할때 (녹음할때)입력장치에서 캡쳐된 오디오의 버퍼를 애플리케이션에 전달하거나

//(재생할때 )버퍼를 채울것을 요구하기 위한 콜백함수를 제공한다.

//맥의 기본입력장치에서 녹음을 하고 , 캡쳐된 오디오를 파일에 쓴다.




//사용할 오디오 형식과 녹음을 할 파일을 설정

//오디오 큐를 생성

//큐를 시작

//큐를 중지

//파일을 닫는 등의 해제 작업



#import <Foundation/Foundation.h>

#import <AudioToolbox/AudioToolbox.h>


#define kNumberRecordBuffers 3


//오디오 큐 콜백을 녹음하기 위한 사용자 정보 구조체

typedef struct MyRecorder

{

    AudioFileID     recordFile;     //output파일 참조

    SInt64          recordPacket;   //output 파일에서 현재 packet index

    Boolean         running;        //큐가 실행되고 있는지 여부를 추적하는 불리언값

} MyRecorder;



OSStatus MyGetDefaultInputDeviceSampleRate(Float64 *outSampleRate);



#pragma mark - utility functions -



// 제네릭 에러 핸들러. - 만약 에러가 0이 아니면 , 에러메시지를 출력하고 프로그램을 종료 한다.

static void CheckError(OSStatus error, const char *operation)

{

    if (error == noErr)

    {

        return;

    }

    

    char errorString[20];

    //  4 문자 코드로 나타날 때 확인

    *(UInt32 *)(errorString + 1) = CFSwapInt32HostToBig(error);

    // 4바이트의 오류 코드가 정확한 문자로 구성됐는지 여부를 검사하기 위해서 isprint() 함수 사용

    if(isprint(errorString[1]) && isprint(errorString[2]) && isprint(errorString[3]) && isprint(errorString[4]))

    {

        errorString[0] = errorString[5] = '\'';

        errorString[6] = '\0';

    }else

    {

        //그렇지 않으면 정수로 취급 - 결과가 네 문자 코드가 아닌경우 괄호의 값은 -50

        sprintf(errorString, "%d", (int)error);

        fprintf(stderr, "오류 발생 : Error: %s (%s) \n",operation, errorString);

        exit(1);

    }

    

}





//기본 입력 디바이스의 샘플률을 구한다.

//오디오 하드웨어 서비스에서 현재 오디오 입력 장치 얻기 - 참고: ios에서는 오디오 하드웨어 서비스는 존재하지 않는다. 아이폰에서 하드웨어 샘플율을 얻기 위해서 10장에서 다루는 오디오 세션 서비스를 사용한다.

OSStatus MyGetDefaultInputDeviceSampleRate(Float64 *outSampleRate)

{

    OSStatus error;

    AudioDeviceID deviceID = 0;

    

    //입력 디바이스를 얻는다.

    AudioObjectPropertyAddress propertyAddress;

    UInt32 propertySize;

    propertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice;

    propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;

    propertyAddress.mElement = 0;

    propertySize = sizeof(AudioDeviceID);

    error = AudioHardwareServiceGetPropertyData(

                                                kAudioObjectSystemObject,

                                                &propertyAddress,

                                                0,

                                                NULL,

                                                &propertySize,

                                                &deviceID);

    if(error){return error;}

    

    //입력장치의 샘플율 얻기

    propertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate;

    propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;

    propertyAddress.mElement = 0;

    propertySize = sizeof(Float64);

    error = AudioHardwareServiceGetPropertyData(deviceID,

                                                &propertyAddress,

                                                0,

                                                NULL,

                                                &propertySize,

                                                outSampleRate);

    

    return error;

}



//asbd를 위한 녹음 버퍼 크기 계산

static int MyComputeRecordBufferSize(const AudioStreamBasicDescription *format, AudioQueueRef queue, float secods)

{


    int packets, frames, bytes;

    //각 버퍼에 몇개의 프레임이 있는지 알 필요가 있다.

    frames = (int)ceil(secods * format -> mSampleRate);

    

    if (format->mBytesPerFrame > 0 )

    {

        bytes = frames * format->mBytesPerFrame;

    }else

    {

        //고정된 패킷 크기

        UInt32 maxPacketSize;

        if (format->mBytesPerPacket > 0)

        {

            maxPacketSize = format ->mBytesPerPacket;

        }else{

            

            //가능한 가장 큰 패킷 크기 획득

            UInt32 propertySize = sizeof(maxPacketSize);

            CheckError(AudioQueueGetProperty(queue,

                                             kAudioConverterPropertyMaximumOutputPacketSize,

                                             &maxPacketSize,

                                             &propertySize),

                       "couldn't get queue's maximum output packet size");

            

        }

        //얼마나 많은 패킷을 가지는지?

        if(format->mFramesPerPacket >0)

        {

            packets = frames / format->mFramesPerPacket;

        }else

        {   // 최악의 경우 패킷에 하나의 프레임

            packets = frames;

            

        

            if (packets == 0 )          // 오류검사

            {

                packets = 1;

                bytes = packets * maxPacketSize;

            }

        

        }

    }

    return bytes;

}




//오디오 큐의 매직 쿠키를 오디오 파일에 복사

static void MyCopyEncoderCookieToFile(AudioQueueRef queue, AudioFileID theFile)

{

    UInt32 propertySize;

    

    OSStatus result = AudioQueueGetPropertySize(queue,

                                                kAudioConverterCompressionMagicCookie,

                                                &propertySize);

    if(result == noErr && propertySize >0)

    {

        

        //쿠키 데이터를 패치 하고 얻기,

        Byte *magicCookie = (Byte *)malloc(propertySize);

        CheckError(AudioQueueGetProperty(queue,

                                         kAudioQueueProperty_MagicCookie,

                                         magicCookie,

                                         &propertySize),

                                        "get audio queue's magic cookie");

        //매직쿠키를 출력파일에 셋팅

        CheckError(AudioFileSetProperty(theFile,

                                        kAudioFilePropertyMagicCookieData,

                                        propertySize,

                                        magicCookie),

                                        "set audio fhile's magic cookie");

        free(magicCookie);

    }

}


#pragma mark - audio queue -

//오디오 큐 콜백 함수, 인풋버퍼가 다 찼을때 호출된다.


static void MyAQInputCallback(void *inUserData,

                              AudioQueueRef inQueue,

                              AudioQueueBufferRef inBuffer,

                              const AudioTimeStamp *inStartTime,

                              UInt32 inNumPackets,

                              const AudioStreamPacketDescription *inPacketDesc)

{

    

    MyRecorder *recorder = (MyRecorder *)inUserData;

    

    //inNumPackets이 0 보다 크면, 버퍼는 오디오 데이터를 얻는다.

    // in the format we specified (AAC)

    //캡쳐된 패킷을 오디오 파일에 쓴다.

    if (inNumPackets >0)

    {

        // 파일에 패킷을 쓴다.

        CheckError(AudioFileWritePackets(recorder->recordFile,

                                         FALSE,

                                         inBuffer->mAudioDataByteSize,

                                         inPacketDesc,

                                         recorder->recordPacket,

                                         &inNumPackets,

                                         inBuffer->mAudioData),

                                        "AudioFileWritePackets failed");

        //패킷 인덱스 증가

        recorder -> recordPacket += inNumPackets;

    

        

        //사용된 버퍼를 다시 큐에 넣음

        if(recorder->running)

        {

            CheckError(AudioQueueEnqueueBuffer(inQueue,

                                               inBuffer,

                                               0,

                                               NULL),

                                                "AudioQueueEnqueueBuffer failed");

        }

    }

}


// 기본장치에서 입력을 캡쳐하기 위한 오디오 큐를 생성하고,

//사용자가 중지하기 전까지 계속 실행하고, 마지막에 모든것을 해제하는 것이다.

int main(int argc, const char * argv[]) {


    //오디오 큐를 위해 MyRecorder 구조체와 ASBD를 생성

    MyRecorder recorder = {0};

    AudioStreamBasicDescription recordFormat = {0};

    memset(&recordFormat, 0, sizeof(recordFormat));

    

    

    //출력 데이터 포멧을 acc로 설정(AAC 로 녹음을 하길 원한다)

    recordFormat.mFormatID = kAudioFormatMPEG4AAC;

    recordFormat.mChannelsPerFrame = 2;

    

    // 인풋디바이스의 샘플율을 얻는다.

    // we use this to adapt the output data format to match hardware capabilities

    MyGetDefaultInputDeviceSampleRate(&recordFormat.mSampleRate);

    

    

    //mBytesPerPacket과 같은 ASBD 필드 중의 몇가지는 엔코딩 형식의 세부사항에 의존하고, 가변적일 수 있다.(개발자가 알 수 없음)

    //pcm외에 다른 형식에 대해서 채울 수 있는 것을 채우고, 코어 오디오가 나머지를 처리하게 됨.

    //AudioFormatGetProperty로 asbd 채우기

    UInt32 propSize = sizeof(recordFormat);

    CheckError(AudioFormatGetProperty(kAudioFormatProperty_FormatInfo,

                                      0,

                                      NULL,

                                      &propSize,

                                      &recordFormat),

                                    "AudioFormatGetProperty Filed");

    

    //위에 코드까지 형식 설정완료함(^)

    

    //입력을 위한 오디오 큐 생성

    AudioQueueRef queue = {0};

    CheckError(AudioQueueNewInput(&recordFormat,            //asbd(녹음할 형식)

                                  MyAQInputCallback,        //callback

                                  &recorder,               //user data 포인터

                                  NULL,                     //run loop

                                  NULL,                     //run loop mode

                                  0,                        //flags (always 0)

                                  &queue),                  //audioQueueRef를 수신하는 포인터를 나타냄

                                 "AudioQueueNewInput failed");

    

    

    //오디오 큐에 채워진 asbd 추출

    //코어 오디오가 큐를 위해서 코덱을 준비하기 전까지 어떤필드들을 체워야 할지 모르기 때문에 아래 코드 필요

    //이제 좀더 세부적인 asbd를 가지고 캡쳐된 파일을 녹음할 파일을 생성할 수 있다.

    UInt32 size = sizeof(recordFormat);

    CheckError(AudioQueueGetProperty(queue,

                                     kAudioConverterCurrentOutputStreamDescription,

                                     &recordFormat,

                                     &size),

                                    "couldn't get queue's format");

    

    

    //출력을 위한 오디오 파일 생성

    CFURLRef myFileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,

                                                       CFSTR("./output2.caf"),

                                                       kCFURLPOSIXPathStyle,

                                                       false);

    CFShow(myFileURL);

    CheckError(AudioFileCreateWithURL(myFileURL,

                                      kAudioFileCAFType,

                                      &recordFormat,

                                      kAudioFileFlags_EraseFile,

                                      &recorder.recordFile), "audiofileCreateWithURL failed");

    CFRelease(myFileURL);

    

    //큐는 매직 쿠키도 제공한다. 매직 쿠키는 주어진 코덱이 유일하고, asbd가 이미 처리하지 않은 데이터의 불명확 블록이다.

    //매직 쿠키를 처리하는 편의 함수 호출

    MyCopyEncoderCookieToFile(queue, recorder.recordFile);

    

    //allocate and enqueue buffers

    //녹음의 경우 큐는 캡쳐된 오디오로 이런 버퍼를 채우고 콜백함수로 전달한다.

    //MyComputeRecordBufferSize는 asbd, 오디오 큐, 버퍼 기간 초를 취하고, 최적의 크기를 반환한다.

    int bufferByteSize = MyComputeRecordBufferSize(&recordFormat, queue, 0.5);// enough bytes for half a second

    //버퍼할당과 큐에 삽입

    int bufferIndex;

    for(bufferIndex = 0; bufferIndex < kNumberRecordBuffers; ++bufferIndex)

    {

        AudioQueueBufferRef buffer;

        CheckError(AudioQueueAllocateBuffer(queue, bufferByteSize, &buffer), "audioqueueAllocateBuffer failed");

        CheckError(AudioQueueEnqueueBuffer(queue, buffer, 0, NULL), "audioQueueEnqueuBuffer failed");

    }

    

    

    // start the queue. this function return immedatly and begins

    // invoking the callback, as needed, asynchronously.

    // 오디오 큐 시작

    recorder.running = TRUE;

    CheckError(AudioQueueStart(queue, NULL), "audio Queue start failed");

    

    //and wait

    //녹음을 계속하ㅏ기 위해서 stdin에서 대기

    printf("recording, press <return> to stop : \n");

    getchar();

    

    //end recording

    //사용자가 녹음을 완료했을 때 큐를 중지하여 동작을 중지할 수 있다.

    printf("*recording done * \n");

    recorder.running = FALSE;

    CheckError(AudioQueueStop(queue, TRUE),"audioqueue stop failed" );

    

    //매직 쿠키 편의 함수 재호출

    // a codec may update its magic cookie at the end of an encoding session

    // so reapply it to the file now

    MyCopyEncoderCookieToFile(queue, recorder.recordFile);

    

    

    //오디오 큐와 오디오 파일을 해제

cleanup:

    AudioQueueDispose(queue, TRUE);

    AudioFileClose(recorder.recordFile);



    return 0;

}








/*

 로그내용

 2018-11-07 14:55:35.592512+0900 4[1511:319917] 317:  ca_debug_string: inPropertyData == NULL

 <CFURL 0x102095010 [0x7fffa6835980]>{string = ./output2.caf, encoding = 134217984

 base = <CFURL 0x1020a0e20 [0x7fffa6835980]>{string = file:///Users/service/Library/Developer/Xcode/DerivedData/4-biuofoahmxwspaaibakhduwwgnmi/Build/Products/Debug/, encoding = 134217984, base = (null)}}

 recording, press <return> to stop :

 

 녹음파일이 생성되는중..............................

 

 *recording done *

 Program ended with exit code: 0


 지정해준 경로에 녹음파일이 생성된다.

 */









반응형

댓글