본문 바로가기
아이폰 개발/ios 개념&튜토리얼

iOS coreaudio 재생 예제 - mac OS

by 인생여희 2021. 1. 21.

iOS coreaudio 재생 예제 - mac OS

 

1.오디오 재생을 위한 구조체를 선언한다.

 

#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>

//1.오디오 큐 버퍼는 3개로 정의한다. (재생용/대기용/채우는용)
static const int kNumberBuffers = 3;


struct MyPlayer {
    
    //2.AudioStreamBasicDescription는 오디오 스트림의 광범위한 특성를 정의하는 구조체이다.
    AudioStreamBasicDescription mDataFormat;
    
    //3.재생 오디오 큐
    AudioQueueRef mQueue;
    
    //4.오디오 큐 버퍼 포인트의 리스트
    AudioQueueBufferRef mBuffers[kNumberBuffers];
    
    //5.오디오 파일 객체
    AudioFileID mAudioFile;
    
    //6.오디오 큐 버퍼 하나의 사이즈
    UInt32 bufferByteSize;
    
    //7.다음 재생할 패킷의 위치
    SInt64 mCurrentPacket;
    
    //8.playback callback 이 불렸을 때 한번에 읽을 packet 의 갯수
    UInt32 mNumPacketsToRead;
    
    //9.VBR 오디오인 경우 필요한 packet 정보
    AudioStreamPacketDescription *mPacketDescs;
    
    //10.현재 재생 오디오큐가 재생 중인지
    bool mIsRunning;
    
    //11. 파일에서 읽은 전체 데이터 사이즈
    UInt32 readedTotalSize;
    
    //12.콜백 함수가 몇번 호출되었는지 체크
    UInt32 callBackFunctionCallCount;
    
};

@interface ViewController : UIViewController



@end

 

2. 오디오 파일 열기

 

 /* STEP 1 AUDIOFILE 열기 */
    struct MyPlayer mPlayer ={0}; // 구조체 초기화
    
    //참고 : a2002011001-e02.wav : 9,580,594 바이트(디스크에 10.5MB 있음)
    CFURLRef myFileUrl = (__bridge CFURLRef)([NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"a2002011001-e02" ofType:@"wav"]]);

    /* AudioFileID 데이터 할당 */
    OSStatus AudioFileOpenURLResult =
    AudioFileOpenURL(myFileUrl, kAudioFileReadPermission, 0, &mPlayer.mAudioFile);
    printf("STEP 1 오디오파일 열기");
    printf("AudioFileOpenURL 결과 : %d \n" , AudioFileOpenURLResult);
    printf("mPlayer.mAudioFile : %p \n\n" , mPlayer.mAudioFile);
    
    
    CFRelease(myFileUrl);

 

 

3.오디오  데이터 포멧 불러오기

 

 /* STEP2 AUDIODATAFORMAT 불러오기 */
    UInt32 dataFormatSize = sizeof(mPlayer.mDataFormat);
    printf("STEP2 오디오 포멧정보 불러오기\n");
    printf("dataFormatSize 전 : %d \n" , dataFormatSize);

    
    /* 즉 채널이 몇개인지, 형식은 무엇인지, bitrate 는 몇인지 등을 포함한다.오디오 데이터 자체는 포함하지 않고 데이터의 정보만 저장*/
    OSStatus AudioFileGetPropertyResult0 =
    AudioFileGetProperty(mPlayer.mAudioFile, kAudioFilePropertyDataFormat, &dataFormatSize, &mPlayer.mDataFormat);
   
    printf("AudioFileGetProperty0 결과  : %d \n" , AudioFileGetPropertyResult0);
    printf("dataFormatSize 후 : %d \n" , dataFormatSize);
    printf("mSampleRate: %.0f 개\n" , mPlayer.mDataFormat.mSampleRate);
    printf("mBitsPerChannel: %d \n" , mPlayer.mDataFormat.mBitsPerChannel);
    printf("mFramesPerPacket: %d \n" , mPlayer.mDataFormat.mFramesPerPacket);
    printf("mChannelsPerFrame: %d \n" , mPlayer.mDataFormat.mChannelsPerFrame);

 

4.비트레이트 계산하기

 

/*
        @설명@
        * 비트 레이트 계산
        BitRate (초당 데이터량) = Sample rate (bits)  * Num of channels * Bits per sample
        예) 44100 * 2 * 16 = 1411200 bits
     */
    
    Float64 bitRate = mPlayer.mDataFormat.mSampleRate * mPlayer.mDataFormat.mChannelsPerFrame * mPlayer.mDataFormat.mBitsPerChannel;
    printf("--> BitRate(초당 데이터량) -1 : %.0f Bits \n" , mPlayer.mDataFormat.mSampleRate * mPlayer.mDataFormat.mChannelsPerFrame * mPlayer.mDataFormat.mBitsPerChannel);
    printf("--> BitRate(초당 데이터량) -2 : %f Bits\n" , bitRate);
    
    /* cbr : 패킷 마다 bitRate 고정, vbr : 패킷마다 bitRate 가변*/
    
    printf("mBytesPerFrame: %d \n" , mPlayer.mDataFormat.mBytesPerFrame);       //cbr : 4 ,   vbr : 0
    printf("mBytesPerPacket: %d \n\n" , mPlayer.mDataFormat.mBytesPerPacket);    //cbr : 4 ,    vbr : 0

 

5.오디오 길이 구하기

 

/* STEP 2 - 2 오디오 길이 구하기 */
    Float64 audioDuration = 0;
    UInt32 thePropSize = sizeof(Float64);
    OSStatus AudioFileGetPropertyResult1 =
    AudioFileGetProperty(mPlayer.mAudioFile, kAudioFilePropertyEstimatedDuration, &thePropSize, &audioDuration);
    
    printf("STEP 2 - 2 오디오 길이 구하기\n");
    printf("AudioFileGetProperty1 결과  : %d \n" , AudioFileGetPropertyResult1);
    printf("--> audioDuration : %f 초\n" , audioDuration);
    //printf("thePropSize : %u \n\n" , thePropSize);
    
    /*
     @설명@
     압축 해제 후 PCM 사이즈 = 초당 데이터량 * 재생시간 / 8 (bytes)
     즉, 1411200 * 195 / 8 = 34398000 bytes 가 된다.
     */
    
    
    Float64 audioDataSize = bitRate * audioDuration / 8;
    
    printf("--> audioDataSize : %f byte \n" , audioDataSize);
    printf("--> audioDataSize : %f mb \n\n" , audioDataSize / (1000 * 1000));

 

6.오디오 큐 생성하기

 

/* STEP 3 AUDIOQUEUE생성하기 */
    /* 3번째 파라미터 위치에 mPlayer 구조체를 넣어주면 MyAQOutputCallback 함수가 불릴 때 인자로 같이 넘어오게 된다. */
    OSStatus AudioQueueNewOutputResult =
    AudioQueueNewOutput(&mPlayer.mDataFormat, MyAQOutputCallback, &mPlayer, NULL, NULL, 0, &mPlayer.mQueue);
    
    printf("STEP 3 오디오 큐 생성하기\n");
    printf("AudioQueueNewOutput 결과  : %d \n\n" , AudioQueueNewOutputResult);

 

 

6-1.오디오 큐 콜백 함수 

이 함수는 AudioQueue가 버퍼에 담긴 오디오 데이터를 재생하는데 모두 소진하고 이 버퍼가 다시 사용될 수 있을 때 불린다.

즉, 이 콜백 함수가 불렸을 때 같이 전달된 버퍼에 오디오 데이터를 밀어 넣는 작업을 수행하여야 한다.

 

 #### 작동 방식 ####

 1.오디오 파일에서 지정된 양의 데이터를 읽어서 오디오 버퍼에 삽입

 2.채워진 오디오 버퍼를 재생 큐에 넣는다.

 3.오디오 파일에서 읽을 데이터가 없을 때 , 오디오 대기열에 중지 할 것을 알린다.

 

 정리 : 재생 큐는 데이터로 채워진 버퍼를 받아서 스피커 하드웨어에 전달 한 뒤 빈 버퍼를 콜백 함수에 전달한다.

       콜백 함수에서 파일로부터 오디오 데이터를 읽어 버퍼에 담아준 뒤 오디오 큐에 전달한다.

 

 

//param: 1.AudioQueueNewOutput에서 전달한 사용자 데이터 구조체, 2.콜백을 부른 audioQueue, 3.채워야 할 buffer
static void MyAQOutputCallback (void *aqData , AudioQueueRef inAQ, AudioQueueBufferRef inBuffer){
    
    printf("\nMyAQOutputCallback 함수 진입\n");
    
    //Callback에서 넘어온 aqData를 직접 구현한 사용자 구조체로 캐스팅
    struct MyPlayer *mPlayer = (struct MyPlayer *) aqData;
    
    
    NSLog(@"mPlayer->mIsRunning : %@" , mPlayer->mIsRunning ? @"yes" : @"no"); //no 면 정지 됨
    
    //오디오 큐가 중지되면 종료
//    if (!mPlayer->mIsRunning){
//        printf("AudioQueue 종료 예정...\n");
//        return;
//    }

    //1. 오디오 버퍼에 오디오 데이터 넣기
    
    //실제로 읽은 데이터의 길이를 저장하기 위한 변수
    UInt32 numBytesReadFromFile = mPlayer->bufferByteSize;
    
    //몇개의 패킷을 읽어야 하는지 (Step 4에서 구한 값)
    UInt32 numPackets = mPlayer->mNumPacketsToRead;
    
    //오디오 파일에서 버퍼 채우기
    //오디오 파일에서 데이터를 읽고 버퍼로 채우기 함수가 끝나고 나면 numPackets 에는 실제로 읽은 packet의 갯수가 저장되어 있다.
    OSStatus AudioFileReadPacketDataResult =
    AudioFileReadPacketData(    mPlayer->mAudioFile,            //읽을 오디오 파일
                            NO,                          //캐시 설정
                            &numBytesReadFromFile,       //입력시 읽을 바이트 수, 출력시 실제 읽은 바이트 수
                            mPlayer->mPacketDescs,
                            mPlayer->mCurrentPacket,       //패킷 인덱스
                            &numPackets,                //입력시 읽을 패킷 수입니다. 출력시 실제로 읽은 패킷 수입니다.
                            inBuffer->mAudioData);        //읽기 패킷을 보관하기 위해 할당하는 메모리

    printf("AudioFileReadPacketData 결과 : %d \n" , AudioFileReadPacketDataResult);

    printf("=> 파일에서 읽은 numPackets : %u \n" , numPackets);
    printf("=> 파일에서 읽은 numBytesReadFromFile : %u \n" , numBytesReadFromFile);
    
    
    if (numPackets > 0) {
        
        mPlayer->readedTotalSize += numBytesReadFromFile;        //파일에서 읽은 바이트 수 누적
        mPlayer->callBackFunctionCallCount += 1;                   //콜백 함수 호출 횟수 누적
        
        printf("=>  mPlayer->mCurrentPacket : %lld \n" ,  mPlayer->mCurrentPacket);
        
        
        //2. 오디오 큐에 오디오 버퍼 ENQUEUE 하기
        //버퍼가 준비가 되었다면 AudioQueueEnqueueBuffer함수를 이용해 해당 버퍼를 오디오 큐에 밀어넣어 준다.
        
        inBuffer->mAudioDataByteSize = numBytesReadFromFile;
        
        
        /* 채워진 버퍼를 재생 큐에 넣기 - 소리 출력 */
        //param : 1.오디오 큐, 2.채워진 버퍼. 3. vbr이면 패킷값, cbr이면 0, NULL
        OSStatus AudioQueueEnqueueBufferResult =
        AudioQueueEnqueueBuffer(mPlayer->mQueue,
                           inBuffer,
                           (mPlayer->mPacketDescs ? numPackets : 0),
                            mPlayer->mPacketDescs);
        
        printf("AudioQueueEnqueueBuffer 결과 : %d \n" , AudioQueueEnqueueBufferResult);
        
        mPlayer->mCurrentPacket += numPackets;  // 현재 읽고 있는 packet의 위치
        
        
    }else{
        
        printf("\n\n AudioQueue 끝 -! \n");
        printf("=> 전체읽은 데이터 사이즈 : %u byte\n" , mPlayer->readedTotalSize);
        printf("=> 함수는 총 %u 번 호출되었습니다.\n" , mPlayer->callBackFunctionCallCount);
        
        //3. 음악이 끝난 경우 오디오 큐 정지시키기
        //더이상 읽어들일 오디오 데이터가 없을 때 오디오 큐를 정지시킨다.
        //멈출 audioQueue , immediately 멈출지 enqeue된 버퍼까지는 모두 재생하고 멈출지
        AudioQueueStop(mPlayer->mQueue, false);
        mPlayer->mIsRunning = false;
        
    }
    
}

 

 

7.오디오 버퍼 사이즈 계산하기

/* STEP 4 BUFFER 사이즈 계산하기  : DeriveBufferSize 함수 */
    
    UInt32 maxPacketSize;
    UInt32 propertySize = sizeof(maxPacketSize);
    
    /* AudioFileGetProperty함수를 이용해서 maximum packet size를 구한다. */
    OSStatus AudioFileGetPropertyResult2 =
    AudioFileGetProperty(mPlayer.mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize);
    
    printf("STEP 4 BUFFER 사이즈 계산하기\n");
    printf("AudioFileGetPropertyResult2 결과  : %d \n" , AudioFileGetPropertyResult2);
    printf("maxPacketSize  : %u \n" , maxPacketSize);
    printf("propertySize  : %u \n\n" , propertySize);
    
    
    DeriveBufferSize(mPlayer.mDataFormat, maxPacketSize, 0.5, &mPlayer.bufferByteSize, &mPlayer.mNumPacketsToRead);
    
    
    printf("\n");
    printf("AFTER DeriveBufferSize 함수\n");
    printf("maxPacketSize  : %u \n" , maxPacketSize);
    printf("propertySize  : %u \n" , propertySize);
    printf("mPlayer.bufferByteSize  : %u byte\n" , mPlayer.bufferByteSize);
    printf("mPlayer.mNumPacketsToRead  : %u byte\n" , mPlayer.mNumPacketsToRead);
    printf("mPlayer.bufferByteSize  : %.0u KB\n" , mPlayer.bufferByteSize /  1000);
    printf("mPlayer.mNumPacketsToRead  : %.0u KB\n" , mPlayer.mNumPacketsToRead / 1000);
    
    /*
     mPlayer.bufferByteSize  :    88200 byte     =>  176400 byte
     mPlayer.mNumPacketsToRead  : 22050 byte    =>  44100 byte
     */

 

7-1. 오디오 버퍼 계산 함수

 

 코어오디오에서 오디오 데이터를 오디오 큐에 전달하기 위해서는 AudioQueueBuffer 라는 객체를 사용한다.

 이 버퍼를 생성하기 전에 재생할 오디오 데이터를 분석해서 어떤 사이즈의 버퍼를 사용할 지,

 한 번에 몇개의 packet을 읽을지 결정해야 한다.

 

 다음 함수는 사용할 버퍼의 사이즈를 outBufferSize에 저장하고,

 읽을 패킷의 갯수를 outNumberPacketsToRead에 저장된다.

 

void DeriveBufferSize(    AudioStreamBasicDescription  ASBDesc, // 1

                      UInt32 maxPacketSize,
                      Float64 seconds,
                      UInt32 *outBufferSize,
                      UInt32 *outNumPacketsToRead)
{

    printf("DeriveBufferSize 함수 진입\n");
    
    static const int maxBufferSize = 0x10000; // 320KB
    static const int minBufferSize = 0x4000;  // 16KB
    
    printf("maxBufferSize : %d \n" , maxBufferSize);
    printf("minBufferSize : %d \n" , minBufferSize);
    
    
    printf("ASBDesc.mFramesPerPacket :  %u \n" , ASBDesc.mFramesPerPacket);
    printf("ASBDesc.mSampleRate : %f \n" , ASBDesc.mSampleRate);
    
    //ASBD에 패킷당 프레임 개수가 일정
    if (ASBDesc.mFramesPerPacket != 0) {
    
        //주어진 시간 (=seconds)에 처리할 수 있는 패킷의 갯수 (=numPacketsForTime)
        Float64 numPacketsForTime =
        ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;
        
        printf("numPacketsForTime : %.0f byte\n" , numPacketsForTime);
        
         //주어진 시간에 필요한 버퍼의 크기(=outBufferSize)를 계산
        *outBufferSize = numPacketsForTime * maxPacketSize;
        
        printf("*outBufferSize : %u byte\n" , *outBufferSize);
        
    }else{
        //maxBufferSize 와 maxPacketSize (AudioFileGetProperty 함수로 구할 수 있음) 를 비교해서 더 큰 값으로 설정해 준다.
    
    
        *outBufferSize =
        maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;
    }
    
    //얻은 값이 너무 크다면 maxBufferSize 에 맞춰준다
    if (*outBufferSize > maxBufferSize &&
        *outBufferSize > maxPacketSize)
    {
        
        *outBufferSize = maxBufferSize;
    
        
    }else {
        
        // 얻은 값이 너무 작다면 minBufferSize 에 맞춰준다
        if (*outBufferSize < minBufferSize)
            *outBufferSize = minBufferSize;
    }
    
    //버퍼의 크기와 최대 패킷 크기를 안다면 콜백 한번에 읽을 수 있는 패킷 갯수를 계산한다.
    *outNumPacketsToRead = *outBufferSize / maxPacketSize;
    
    printf("*outNumPacketsToRead : %u byte\n" , *outNumPacketsToRead);
}

 

8.PACKETDESCRIPTION 메모리 설정

 

/* STEP 5 PACKETDESCRIPTION 메모리 설정 */
    // VBR format : ASBD에 mBytesPerPacket 이나 mFramesPerPacket  : 0
    bool isFormatVBR = (mPlayer.mDataFormat.mBytesPerPacket == 0 ||
                       mPlayer.mDataFormat.mFramesPerPacket == 0);

    
    printf("\n");
    printf("STEP 5 PACKETDESCRIPTION 메모리 설정\n");
    printf("VBR : 1 / CBR : 0 결과 : %d\n" , isFormatVBR);
    
    if (isFormatVBR) {
        
        
    /*
         VBR format 이라면 AudioStreamPacketDescription 의 사이즈 X
         한번에 읽을 패킷 갯수 (= mNumPacketsToRead) 만큼 메모리를 할당해 준다.
         이 정보는 오디오 큐 콜백 함수에서 오디오 데이터를 읽어들일 때 각각의 패킷을 분석할 때 쓰인다.
    */
        
        mPlayer.mPacketDescs =
        (AudioStreamPacketDescription *)malloc(mPlayer.mNumPacketsToRead * sizeof(AudioStreamPacketDescription));
    }else{
        
        //CBR format은 AudioStreamPacketDescription 필요없음
        mPlayer.mPacketDescs = NULL;
    }

 

9.매직쿠키 설정

 

/*
     STEP 6 MAGIC COOKIE 설정
     magic cookie : MPEG4나 AAC 와 같은 압축 오디오 데이터 형식에서 사용하는 오디오 메타데이터이다.
     오디오 데이터에서 magic cookie를 꺼내서 오디오 큐에 세팅.
     */
    printf("\n");
    printf("STEP 6 MAGIC COOKIE 설정\n");
    
    UInt32 cookieSize = sizeof(UInt32);
    
    /*
     Magic cookie 데이터의 크기를 잘 모르기 때문에 아래 함수를 이용.
     오디오 데이터에 실제로 magic cookie가 존재하는지, 존재 한다면 사이즈를 구해서 cookieSize 에 할당
     */
    OSStatus getMagicCookieResult =
    AudioFileGetPropertyInfo(mPlayer.mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, NULL);
    
    
    if (getMagicCookieResult == 0 && cookieSize) {
        
        printf("MAGIC COOKIE 존재합니다 \n");
        
        char* magicCookie = (char *)malloc(cookieSize);
        
        //실제로 magic cookie 정보를 불러온다.
        AudioFileGetProperty(mPlayer.mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, magicCookie);
        
        //오디오 데이터에서 불러온 magic cookie 정보를 AudioQueue에게 전달
        AudioQueueSetProperty(mPlayer.mQueue, kAudioQueueProperty_MagicCookie, magicCookie, cookieSize);
        
        
        free(magicCookie);
        
        
    }else{
        
        printf("MAGIC COOKIE 존재하지 않습니다. \n\n");
        
    }//if - end

 

10.버퍼 큐에 할당하기

 

//STEP 8 ALLOCATE AUDIOQUEUEBUFFERS
    mPlayer.mCurrentPacket = 0;             //현재 패킷 위치
    mPlayer.readedTotalSize = 0;            //총 읽은 파일 사이즈 계산
    mPlayer.callBackFunctionCallCount = 0;    //콜백 함수 호출 횟수
    
    for (int i = 0; i < kNumberBuffers; ++i) {
        
        //Step 4 에서 오디오 버퍼의 사이즈만 계산하고 실제로 오디오 큐에 버퍼를 생성한 적은 없다.
        //버퍼 할당
        /* param : 1.오디오 큐, 2.버퍼 사이즈, 3. 버퍼 포인터 */
        AudioQueueAllocateBuffer(mPlayer.mQueue,
                            mPlayer.bufferByteSize,
                            &mPlayer.mBuffers[i]);
    
        
        //AudioQueue 가 실제로 start 하기 전에 버퍼에 데이터를 미리 채워서 enqueue 해 놓으려는 의도.
        //버퍼 채우기
        MyAQOutputCallback( &mPlayer,
                        mPlayer.mQueue,
                        mPlayer.mBuffers[i]);
    
    }
    

 

11.오디오 큐 시작

 

mPlayer.mIsRunning = true;
    
    //AudioQueueStart함수는 자체 스레드에서 오디오 대기열을 시작
    OSStatus AudioQueueStartResult =
    AudioQueueStart(mPlayer.mQueue,     //시작할 오디오 대기열
                 NULL               //오디오 대기열이 즉시 재생
                );
    
    printf("\nnSTEP 9 START PLAYING \n");
    printf("AudioQueueStart 결과  : %d \n" , AudioQueueStartResult);
    
    
    //사용자 지정 구조의 mIsRunning필드를 정기적으로 폴링하여 오디오 대기열이 중지되었는지 확인
    do {
        
        //오디오 대기열의 스레드를 포함하는 실행 루프를 실행
        CFRunLoopRunInMode (
                        kCFRunLoopDefaultMode,  //런 루프에 기본 모드를 사용
                        0.25,               //런 루프의 실행 시간을 0.25초로 설정
                        false);
        
        
    } while (mPlayer.mIsRunning);
     
    
    
    printf("\n\n큐에 재생 중인 버퍼 체크 시작\n");
    
    
    //오디오 대기열이 중지 된 후 현재 재생중인 오디오 대기열 버퍼에 완료 할 시간이 있는지 확인하기 위해 실행 루프를 조금 더 오래 실행
    CFRunLoopRunInMode (kCFRunLoopDefaultMode,1,false);

    
    printf("\n\n큐에 재생 중인 버퍼 체크 종료");
    
    
    //AudioQueueDispose (mPlayer.mQueue,true);
    //AudioFileClose (mPlayer.mAudioFile);
    //free (mPlayer.mPacketDescs);

 

#자료다운로드 1

#참고자료 2

 

 

참고사이트 

#디지털 오디오 정보 : https://m.blog.naver.com/PostView.nhn?blogId=kimyoseob&logNo=220760163474&proxyReferer=

 

#듀토리얼 : https://dundinstudio.com/ios-core-audio-tutorial/

 

#애플 문서 : https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW2

 

#AVAudioEngine Tutorial : https://www.raywenderlich.com/5154-avaudioengine-tutorial-for-ios-getting-started

 

https://developer.apple.com/audio/

 

#코어 오디오 개요: https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/CoreAudioOverview/CoreAudioEssentials/CoreAudioEssentials.html