[튜토리얼] CoreAudio를 이용한 음원 재생

iOS에서 개발 할 때 음악 파일에서 단순 재생을 하거나 녹음을 하고자 한다면 AVAudioPlayerAVAudioRecorder를 사용하면 된다. 그러나 재생이나 녹음을 넘어 혼합, 형식 변환, 효과 적용 등 오디오 데이터에 직접 작업을 하길 원한다면 코어오디오 - CoreAudio를 사용해야 한다. AudioUnit은 개발자가 사용할 수 있는 가장 로우 레벨의 인터페이스이다. 오디오 변환 유닛, 오디오 출력 유닛 등을 조합해 오디오 그래프를 생성해서 사용할 수 있고 AudioQueue 또한 이와 같은 AudioUnit을 기반으로 만들어져있다.

Overview

코어오디오를 이용한 음원 재생 튜토리얼에서는 Audio File Service와 AudioQueue 를 이용해 AAC파일을 재생을 할 것이다. 간략하게 다음과 같은 순서를 따라간다

  1. 로컬 경로에 존재하는 AAC파일 에서부터 AudioFileID을 생성한다.
  2. AudioFileID 에서 오디오 데이터 포맷 (AudioStreamBasicDescription)을 추출한다.
  3. AudioQueueRef 를 생성한다. 이 때 2번에서 구한 오디오 데이터 포맷을 적절히 세팅해 준다. 또한 큐에 있는 버퍼를 비웠을 때 불릴 callback 함수를 설정해 준다.
  4. 버퍼를 생성해서 오디오 데이터를 채워준다음 AudioQueueRef에 enqueue 해준다.
  5. Callback 함수에서는 빈 버퍼를 인자로 받아서 오디오 데이터를 AudioQueueRef에 enqueue 해 주도록 구현한다.
  6. AudioQueueStart 를 부르면 재생이 시작되고 버퍼가 비워지면 5번의 callback 함수가 불린다.

Step 0 사용자 구조체 정의

CoreAudio를 이용해서 재생이나 녹음을 할 때 callback 함수를 많이 사용하게 되는데 이 때 인자로 받을 사용자 정의 구조체를 정의해 주어야 한다. callback함수가 불렸을 때 인자로 다 쓴 buffer도 함께 들어오는데 여기에 데이터를 넣을 때 필요한 정보들을 포함해야 한다. 언뜻 이해가 안 갈 수도 있지만 callback함수 구현하는 쪽에서 다시 보게 될 것이니 하나씩 개념만 짚고 넘어가자.

  1. 오디오 큐 버퍼는 3개로 정의한다. (재생용/대기용/채우는용)
  2. AudioStreamBasicDescription는 오디오 스트림의 광범위한 특성를 정의하는 구조체이다.
  3. 재생 오디오 큐
  4. 오디오 큐 버퍼 포인트의 리스트
  5. 오디오 파일 객체
  6. 오디오 큐 버퍼 하나의 사이즈
  7. 다음 재생할 패킷의 위치
  8. playback callback 이 불렸을 때 한번에 읽을 packet 의 갯수
  9. VBR 오디오인 경우 필요한 packet 정보
  10. 현재 재생 오디오큐가 재생 중인지

Step 1 AudioFile 열기

코어 오디오에서의 Audio File Service는 디스크나 메모리로 부터 audio file을 열고, 그것이 포함한 오디오 데이터 형식을 얻거나 설정하고, 파일에 대해 읽기나 쓰기를 할 수 있게 한다. AudioFileOpenURL() 함수는 이미존재하는 audio file을 여는 함수이다.

아래 함수까지 작성했으면 구조체 mPlayer의 playbackFile 이라는 필드에 AudioFileID의 포인터가 할당 되어 있을 것이다.

Step2 AudioDataFormat 불러오기

이제 step1에서 오픈한 AudioFileID로부터 AudioFileGetProperty() 함수를 이용해서 정보를 불러올 차례이다.

두 번째 인자인 AudioFilePropertyID 의 리스트는 애플문서에 정의되어 있다. 여기에서는 AudioStreamBasicDescription 정보를 가져오기 위해서 kAudioFilePropertyDataFormat 상수를 사용했지만 오디오의 AlbumArtwork, PacketCount, Duration 등의 정보도 가져올 수 있으니 확인해보자.

여기서는 가져올 데이터가AudioStreamBasicDescription 타입인 것을 알기 때문에 사이즈가 고정되어 있으나 타입에 따라서 사이즈를 알지 못할 때도 있다. 이런 경우에는 AudioFileGetPropertyInfo()함수를 이용해서 사이즈를 가져온다.

위에 등장한 AudioStreamBasicDescription(ASDB) 가 무엇인지 알아보자.
ASDB는 코어오디오에서 사용하는 오디오 스트림의 광범위한 특성를 정의하는 구조체이다. 즉 채널이 몇개인지, 형식은 무엇인지, bitrate 는 몇인지 등을 포함한다.오디오 데이터 자체는 포함하지 않고 데이터의 정보만 저장한다.

LPCM 포맷이나 모든 패킷이 정보가 동일한 constant bit rate (CBR) 포맷은 ASDB만으로도 충분하지만 패킷마다 bitrate가 다를 수 있는 variable bit rate (VBR) 포맷은 각 패킷의 정보를 담고 있는 AudioStreamPacketDescription 가 추가적으로 필요하다. VBR포맷의 ASBD를 프린트 해 보면 패킷마다 값이 다르기 때문에 mBytesPerPacketmFramesPerPacket 의 값은 0이다.

Step 3 AudioQueue생성하기

코어오디오 에서의 AudioQueue는 오디오 하드웨어의 일부와 통하는 간단한 인터페이스이다. 하드웨어의 일부라 함은 보통 스피커나 마이크를 말한다. AudioQueue는 버퍼 큐와 연결되어 있고 버퍼는 실제 오디오 데이터를 가지고 있는 메모리 블럭이다. 재생 큐는 데이터로 채워진 버퍼를 받아서 스피커 하드웨어에 전달 한 뒤 빈 버퍼를 콜백 함수에 전달한다. 콜백 함수에서 파일로부터 오디오 데이터를 읽어 버퍼에 담아준 뒤 오디오 큐에 전달한다.

아래처럼 코드를 작성해 준다. 아직 콜백함수는 작성하지 않았다. inUserData 위치에 mPlayer 구조체를 넣어주면 callback 함수가 불릴 때 인자로 같이 넘어오게 된다.

Step 4 Buffer 사이즈 계산하기

코어오디오에서 오디오 데이터를 오디오 큐에 전달하기 위해서는 AudioQueueBuffer 라는 객체를 사용한다. 이 버퍼를 생성하기 전에 재생할 오디오 데이터를 분석해서 어떤 사이즈의 버퍼를 사용할 지, 한 번에 몇개의 packet을 읽을지 결정해야 한다.
다음 함수는 사용할 버퍼의 사이즈를 outBufferSize에 저장하고, 읽을 패킷의 갯수를 outNumberPacketsToRead에 저장된다.

  1. ASBDesc.mFramesPerPacket != 0이라면 ASDB에 패킷당 프레임 갯수가 전체 오디오 데이터를 통틀어 일정하다는 뜻이다. 그렇다면 위와 같은 수식으로 주어진 시간 (=seconds)에 처리할 수 있는 패킷의 갯수 (=numPacketsForTime) 를 구할 수 있고 이를 통해 주어진 시간에 필요한 버퍼의 크기(=outBufferSize)를 계산할 수 있다.
  2. 패킷당 프레임 갯수가 다른 경우라면 maxBufferSizemaxPacketSize (AudioFileGetProperty 함수로 구할 수 있음) 를 비교해서 더 큰 값으로 설정해 준다.
  3. 얻은 값이 너무 크다면 maxBufferSize 에 맞춰준다
  4. 얻은 값이 너무 작다면 minBufferSize 에 맞춰준다
  5. 버퍼의 크기와 최대 패킷 크기를 안다면 콜백 한번에 읽을 수 있는 패킷 갯수를 계산한다.

그럼 이제 위 함수를 이용해서 버퍼 사이즈와 읽을 패킷 갯수를 실제로 계산해보자.

  1. AudioFileGetProperty함수를 이용해서 maximum packet size를 구한다.
  2. DeriveBufferSize함수를 이용해서 bufferByteSizemNumberPacketToRead를 구한다. 여기서 주어진 시간은 0.5초 이다.

Step 5 PacketDescription 메모리 설정

아직 세팅할 데이터가 더 남았다 (….)
위에도 말했듯이 모든 Packet 사이즈가 동일한 CBR은 ASBD만으로도 충분한 정보가 되지만 VBR같은 경우는 각 패킷 마다 추가적인 정보를 들고 있어야 한다. 이를 위해 오디오 데이터가 CBR 인지 VBR인지 구분해서 사용자 구조체에 메모리를 할당해 주자.

  1. 해당 오디오 데이터가 VBR인지 구분하는 방법은 간단하다. 위에서 구한 ASBDmBytesPerPacket 이나 mFramesPerPacket 의 값이 0인지 확인하면 된다.
  2. 만약 VBR format 이라면 AudioStreamPacketDescription 의 사이즈 x 한번에 읽을 패킷 갯수 (= mNumPacketsToRead) 만큼 메모리를 할당해 준다. 이 정보는 오디오 큐 콜백 함수에서 오디오 데이터를 읽어들일 때 각각의 패킷을 분석할 때 쓰인다.
  3. CBR format은 AudioStreamPacketDescription 가 필요하지 않으므로 메모리를 할당하지 않는다.

Step 6 Magic Cookie 설정

아직도 설정이 끝나지 않았다. 오디오에서 magic cookie 란 MPEG4나 AAC 와 같은 압축 오디오 데이터 형식에서 사용하는 오디오 메타 데이터이다. 세부적인 내용은 몰라도 되며 오디오 데이터에서 magic cookie를 꺼내서 오디오 큐에 세팅해 줘야 한다.

  1. Magic cookie 데이터의 크기를 잘 모르기 때문에 AudioFileGetPropertyInfo 함수를 이용해서 오디오 데이터에 실제로 magic cookie가 존재하는지, 존재 한다면 사이즈를 구해서 cookieSize 에 할당해 준다.
  2. AudioFileGetPropertyInfo 함수에 대한 결과가 error가 아니고 (즉 magic cookie가 존재하고) 사이즈를 구했다면 AudioFileGetProperty 함수를 이용해서 실제로 magic cookie 정보를 불러온다.
  3. 오디오 데이터에서 불러온 magic cookie 정보를 AudioQueue에게 전달한다.

Step 7 AudioQueueOutputCallback 함수 구현

지루한 설정은 끝났고, 이제 CoreAudio에서 제일 중요한 부분을 구현할 차례이다. 위의 Step 3에서 AudioQueue를 새로 생성할 때 연결 해 주었던 AudioQueueOutputCallback 함수를 떠올려보자. 이 함수는 AudioQueue가 버퍼에 담긴 오디오 데이터를 재생하는데 모두 소진하고 이 버퍼가 다시 사용될 수 있을 때 불린다. 즉, 이 콜백 함수가 불렸을 때 같이 전달된 버퍼에 오디오 데이터를 밀어 넣는 작업을 수행하여야
한다.

콜백 함수에서 해야 할 일은 3가지이다.

1. 오디오 버퍼에 오디오 데이터 넣기

아래는 AudioFile에서 데이터를 읽어서 오디오 버퍼로 밀어넣는 작업을 하게 해주는 AudioFileReadPackets 함수의 정의이다.

위 함수를 이용해서 오디오 버퍼를 채워주는 과정을 구현 하면 아래와 같다.

  1. Callback에서 넘어온 aqData를 직접 구현한 사용자 구조체로 캐스팅
  2. 실제로 읽은 데이터의 길이를 저장하기 위한 변수
  3. 몇개의 패킷을 읽어야 하는지 (Step 4에서 구한 값)

위 스텝이 끝나고 나면 numPackets 에는 실제로 읽은 packet의 갯수가 저장되어 있다.

2. 오디오 큐에 오디오 버퍼 Enqueue 하기

버퍼가 준비가 되었다면 AudioQueueEnqueueBuffer함수를 이용해 해당 버퍼를 오디오 큐에 밀어넣어 준다.

3. 음악이 끝난 경우 오디오 큐 정지시키기

더이상 읽어들일 오디오 데이터가 없을 때 오디오 큐를 정지시킨다.

Step 8 Allocate AudioQueueBuffers

돌이켜보면 Step 4 에서 오디오 버퍼의 사이즈만 계산하고 실제로 오디오 큐 버퍼를 생성한 적은 없다. 이제 모든 준비가 거의 끝나가니 오디오 AudioQueueAllocateBuffer 함수를 이용해 큐 버퍼를 생성하고 세팅해 보자. kNumberBuffers은 Step 0에서 3개로 세팅하였다. 사실 버퍼 갯수는 더 많아도 상관없지만 보편적으로 3개 정도로 맞춘다. (스트리밍의 경우 네크워크의 상태에 따라서 데이터를 많이 받아 놓아야 할 수도 있기 때문에 더 많은 것이 일반적이지만 로컬 파일 재생의 경우는 3개로도 충분하다.)

AudioQueueAllocateBuffer 함수를 이용해서 Buffer를 생성한 뒤 MyAQOutputCallback 함수를 불러주었다. 이렇게 하는 이유는 AudioQueue 가 실제로 start 하기 전에 버퍼에 데이터를 미리 채워서 enqueue 해 놓으려는 의도이다.

Step 9 Start Playing

자, 이제 코어오디오로 음원을 재생할 모든 준비는 끝났다!! 바로 재생하려면 두번째 인자에 NULL을 넣고 시간차를 두고 재생 하고자 하면 AudioTimeStamp 객체를 만들어 넣어주면 된다.

Step 10 Finish Playing

파일 재생을 마쳤을 때는 꼭 AudioFileIDAudioQueue를 정리해 줘야 한다. AudioQueueDispose 함수의 두번째 인자는 dispose를 바로 진행할 지, enqueue 된 버퍼를 전부 소진 한 후 dispose 할지 결정한다.

  • 임성빈

    안녕하세요.
    요즘 코어 오디오로 믹싱을 하려고 찾아보다가 연락드립니다.
    혹시 질문을 좀 주고 받을 수 있는 창구가 있을까요..?

    • eungen joo

      코멘트 달아주세요 ㅎㅎ 근데 저도 잘 몰라서.. 도움이 될 수 있을지 모르겠습니다

      • 임성빈

        혹시 audio queue 를 superpowered decode 와 함께 사용해 보신적이 있으신지..
        원래 따로 플레이어를 만들어 놓은게 있는데 메모리를 따로 decode 된 걸로 할당하여 플레이 하는.. 근데 이게 네이티브나 코코스는 잘 돌아가는데 유니티에서는 소리가 나지 않는
        현상이 있어서… 이런 현상을 아시는지..