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/
#AVAudioEngine Tutorial : https://www.raywenderlich.com/5154-avaudioengine-tutorial-for-ios-getting-started
https://developer.apple.com/audio/
'아이폰 개발 > ios 개념&튜토리얼' 카테고리의 다른 글
iOS 코어오디오 재생 예제자료 (0) | 2021.01.21 |
---|---|
iOS coreaudio 재생 예제 - 아이폰 (0) | 2021.01.21 |
ios 파일생성 파일저장 파일삭제 (0) | 2021.01.12 |
ios Photos framework 로 사진첩만들기 (0) | 2021.01.09 |
ios collectionview xib 파일 & customcell 예제 (0) | 2021.01.07 |