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
지정해준 경로에 녹음파일이 생성된다.
*/
'ios 뽀개기 > objective-c' 카테고리의 다른 글
ios AVFoundation으로 오디오 플레이어 만들기 (0) | 2018.11.10 |
---|---|
ios objective c 코어오디오 다루기 5 - 재생기능 (0) | 2018.11.09 |
ios objective c 코어오디오 다루기 3 - AudioStreamBasicDescription 데이터 구조체 (0) | 2018.11.07 |
ios objective c 코어오디오 다루기 2 - AudioStreamBasicDescription으로 다양한 주파수의 음원만들기 (0) | 2018.11.06 |
ios objective c 코어오디오 다루기 1 (0) | 2018.11.06 |
댓글