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

ios objective c 코어오디오 다루기 5 - 재생기능

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

ios objective c 코어오디오 다루기 5 - 재생기능


/*

 오디오 큐는 오디오 스트림을 처리하기 위해서 버퍼의 큐를 사용한다.

 즉 애플리케이션은 큐에 버퍼를 제공함으로써 설정하고,

 큐는 개발자 코드가 실행돼야 하는 시점에 개발자 코드의 콜백 함수로 버퍼를 전달한다.

 

 버퍼크기를 가늠하고, 매직쿠키를 관리하는 것은 녹음하기와 비슷하지만 얍축된 오디오 데이터의 버퍼를 기술하는

 AudioStreamPacketDescription을 사용한다.

 

 

 */

#define kPlaybackFileLocation CFSTR("/Users/service/Library/Developer/Xcode/DerivedData/4-biuofoahmxwspaaibakhduwwgnmi/Build/Products/Debug/output2.caf")

#import <Foundation/Foundation.h>

#import <AudioToolbox/AudioToolbox.h>


void CalculateBytesForTime (AudioFileID inAudioFile, AudioStreamBasicDescription inDesc, Float64 inSeconds, UInt32 *outBufferSize, UInt32 *outNumPackets);



//하나는 재생, 하나는 채워지고, 하나는 지연되는 경우에 사용

#define kNumberPlaybackBuffers 3


//재생 오디오 큐 콜백을 위한 사용자 정보 구조체

typedef struct MyPlayer{

    

    AudioFileID                         playbackFile;       //출력파일 참조

    SInt64                              packetPosition;     //출력파일의 현재 패킷 인덱스

    UInt32                              numPacketsToRead;   //파일에서 부터 읽기 위한 패킷의 수

    AudioStreamPacketDescription          *packetDescs;       //버퍼를 읽기 위한 패킷 디스크립션의 배열

    Boolean                             isDone;

    

} MyPlayer;




// 제네릭 에러 핸들러. - 만약 에러가 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);

    }

    

}


//큐의 버퍼 크기를 계산하고, 각 버퍼에 몇 개의 패킷을 읽을 수 있는지 알게하는 함수

void CalculateBytesForTime (AudioFileID inAudioFile, AudioStreamBasicDescription inDesc, Float64 inSeconds, UInt32 *outBufferSize, UInt32 *outNumPackets)

{

    

    // we need to calculate how many packets we read at a time, and how big a buffer we need.

    // we base this on the size of the packets in the file and an approximate duration for each buffer.

    //

    // first check to see what the max size of a packet is, if it is bigger than our default

    // allocation size, that needs to become larger

    UInt32 maxPacketSize;

    UInt32 propSize = sizeof(maxPacketSize);

    CheckError(AudioFileGetProperty(inAudioFile, kAudioFilePropertyPacketSizeUpperBound,

                                    &propSize, &maxPacketSize), "couldn't get file's max packet size");

    

    static const int maxBufferSize = 0x10000; // limit size to 64K

    static const int minBufferSize = 0x4000; // limit size to 16K

    

    if (inDesc.mFramesPerPacket) {

        Float64 numPacketsForTime = inDesc.mSampleRate / inDesc.mFramesPerPacket * inSeconds;

        *outBufferSize = numPacketsForTime * maxPacketSize;

    } else {

        // if frames per packet is zero, then the codec has no predictable packet == time

        // so we can't tailor this (we don't know how many Packets represent a time period

        // we'll just return a default buffer size

        *outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;

    }

    

    // we're going to limit our size to our default

    if (*outBufferSize > maxBufferSize && *outBufferSize > maxPacketSize)

        *outBufferSize = maxBufferSize;

    else {

        // also make sure we're not too small - we don't want to go the disk for too small chunks

        if (*outBufferSize < minBufferSize)

            *outBufferSize = minBufferSize;

    }

    *outNumPackets = *outBufferSize / maxPacketSize;

}


//오디오 파일에서 오디오 큐로 매직 쿠기 복사 - 파일에서 쿠키 속성을 얻고, 재생 큐에 설정한다.

static void MyCopyEncoderCookieToQueue(AudioFileID theFile, AudioQueueRef queue ) {

    UInt32 propertySize;

    OSStatus result = AudioFileGetPropertyInfo (theFile, kAudioFilePropertyMagicCookieData, &propertySize, NULL);

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

    {

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

        CheckError(AudioFileGetProperty (theFile, kAudioFilePropertyMagicCookieData, &propertySize, magicCookie), "get cookie from file failed");

        CheckError(AudioQueueSetProperty(queue, kAudioQueueProperty_MagicCookie, magicCookie, propertySize), "set cookie on queue failed");

        free(magicCookie);

    }

}




#pragma mark - audio queue -


static void MyAQOutputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inCompleteAQBuffer)

{

    MyPlayer *aqp = (MyPlayer*)inUserData;

    if (aqp->isDone) return;

    

    // read audio data from file into supplied buffer

    UInt32 numBytes;

    UInt32 nPackets = aqp->numPacketsToRead;

    //파일에서 읽기 위한 함수 AudioFileReadPackets

    CheckError(AudioFileReadPackets(aqp->playbackFile,

                                    false,

                                    &numBytes,

                                    aqp->packetDescs,

                                    aqp->packetPosition,

                                    &nPackets,

                                    inCompleteAQBuffer->mAudioData),

               "AudioFileReadPackets failed");

    

    // enqueue buffer into the Audio Queue

    // if nPackets == 0 it means we are EOF (all data has been read from file)

    if (nPackets > 0)

    {

        inCompleteAQBuffer->mAudioDataByteSize = numBytes;

        AudioQueueEnqueueBuffer(inAQ,

                                inCompleteAQBuffer,

                                (aqp->packetDescs ? nPackets : 0),

                                aqp->packetDescs);

        aqp->packetPosition += nPackets;

    }

    else

    {

        CheckError(AudioQueueStop(inAQ, false), "AudioQueueStop failed");

        aqp->isDone = true;

    }

}






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


    //MyPlayer구조체 할당

    MyPlayer player = {0};

    //입력을 위한 오디오 파일 열기

   CFURLRef myFileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,

                                  kPlaybackFileLocation,

                                  kCFURLPOSIXPathStyle,

                                  false);

    

    CheckError(AudioFileOpenURL(myFileURL,

                                kAudioFileReadPermission,

                                0,

                                &player.playbackFile), "audio file open url failed");

    

    CFRelease(myFileURL);

    

    

    //오디오 파일에서 asbd(오디오 데이터 포멧)획득

    AudioStreamBasicDescription dataFormat;

    UInt32 propSize = sizeof(dataFormat);

    CheckError(AudioFileGetProperty(player.playbackFile,

                                    kAudioFilePropertyDataFormat,

                                    &propSize,

                                    &dataFormat), "couldn't get file's data format");

    

    

    //dataFormat을 가졌기 때문에 AudioQueueNewOutput()함수로 재생을 위한 오디오 큐를 생성할 준비가 됐음.

    //녹음큐를 설정하는 방법과 같다.

    AudioQueueRef queue;

    CheckError(AudioQueueNewOutput(&dataFormat,

                                   MyAQOutputCallback,

                                   &player,

                                   NULL,

                                   NULL,

                                   0,

                                   &queue), "audioQueueNewOutput failed");

    

    //재생 버퍼 크기와 패킷의 수를 계산하기 위한 편의 함수 호출

    UInt32 bufferByteSize;

    //읽을 파일, asbd, 초단위 버퍼기간을 취하고, 적절한 버퍼 크기와 각 콜백에서 파일로 부터 읽을 오디오 패킷수를 나타내는 변수를 설정한다.

    CalculateBytesForTime(player.playbackFile, dataFormat,  0.5, &bufferByteSize, &player.numPacketsToRead);

   

    

    //패킷 정보 배열에 메모리 할당

    //pocketDescs는 파일에서 읽은 각 오디오 버퍼의 내용을 나타내는 패킷 정보의 배열이다.

    //패킷정보가 필요한지 여부는 오디오 형식이 가변(패킷정보가 필요함) 비트율인지 고정(패킷정보가 필요치 않음)인지에 달렸다

    bool isFormatVBR = (dataFormat.mBytesPerPacket == 0 || dataFormat.mFramesPerPacket == 0);

    if (isFormatVBR)

        player.packetDescs = (AudioStreamPacketDescription*)malloc(sizeof(AudioStreamPacketDescription) * player.numPacketsToRead);

    else

        player.packetDescs = NULL;

    

    

    //재생을 위해서 오디오 파일의 속성으로써 매직쿠키를 읽고 , 큐에 작성

    MyCopyEncoderCookieToQueue(player.playbackFile, queue);

    

    

    //녹음할때는 빈 버퍼를 큐에 전달했고, 콜백에서 채워진 버퍼를 받았다.

    //재생할때는 콜백에서 빈 버퍼를 받고, 재생된 오디오로 그것을 채워야한다.

    //재생 버퍼를 할당하고, 큐에 넣는다.

    AudioQueueBufferRef    buffers[kNumberPlaybackBuffers];

    player.isDone = false;

    player.packetPosition = 0;

    int i;

    for (i = 0; i < kNumberPlaybackBuffers; ++i)

    {

        CheckError(AudioQueueAllocateBuffer(queue, bufferByteSize, &buffers[i]), "AudioQueueAllocateBuffer failed");

        

        // manually invoke callback to fill buffers with data

        MyAQOutputCallback(&player, queue, buffers[i]);

        

        // EOF (the entire file's contents fit in the buffers)

        if (player.isDone)

            break;

    }

    

    

    //재생 오디오 큐 시작 - 이 시점에서 오디오 큐는 재생할 준비가 된 오디오 세 개의 오디오 데이터의 버퍼를 가진다.

    //재생을 시작할때, 큐는 각 버퍼의 내용을 재생하고, 파일에서 새로운 오디오로 버퍼를 다시 채우기 위해서 콜백함 수를 호출한다.

    CheckError(AudioQueueStart(queue, NULL), "AudioQueueStart failed");

    

    // and wait

    printf("Playing...\n");

    do

    {

        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, false);

    } while (!player.isDone /*|| gIsRunning*/);

    

    

    //큐가 버퍼에 있는 오디오를 재생하도록 보장하기 위해 지연

    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 2, false);

    

    

    

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

    player.isDone = true;

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

    

cleanup:

    AudioQueueDispose(queue, TRUE);

    AudioFileClose(player.playbackFile);

    

    

    return 0;

}



/*

 출력

 Playing...

 Program ended with exit code: 0

 */


반응형

댓글