ios objective c 코어오디오 다루기 6 - 오디오 파일 변환
//
// main.m
// 6
//
// Created by service on 2018. 11. 12..
// Copyright © 2018년 service. All rights reserved.
//
/*
- mp3 나 aac 와 같은 엔코드된 파일을 .caf 컨테이너 파일로 변환하기
- 하나의 파일에서 데이터를 읽고, 메모리의 일부 패킷을 변환하고, 결과를 다른 파일에 쓰기
- 오디오 파일 서비스 사용함.
*/
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <CoreServices/CoreServices.h>
#define kInputFileLocation CFSTR("/Users/service/Desktop/test_cbr.mp3")
typedef struct MyAudioConverterSettings
{
AudioStreamBasicDescription inputFormat; //입력 스트림의 데이터 형식
AudioStreamBasicDescription outputFormat; //출력 스트림의 데이터 형식
AudioFileID inputFile; //입력 파일의 참조를 추적함
AudioFileID outputFile; //출력 파일의 참조를 추적함
UInt64 inputFilePacketIndex; //읽고 있는 현재 패킷의 수
UInt64 inputFilePacketCount; //입력 파일의 전체 패킷 수
UInt32 inputFilePacketMaxSize; //입력 패킷의 최대 크기
AudioStreamPacketDescription *inputFilePacketDescription; //데이터를 읽을 때마다 입력 파일에서 읽어들이는 배열의 포인터
void *sourceBuffer;
} MyAudioConverterSettings;
OSStatus MyAudioConverterCallback
(
AudioConverterRef inAudioConverter,
UInt32 *ioDataPacketCount,
AudioBufferList *ioData,
AudioStreamPacketDescription **outDataPacketDescription,
void *inUserdata
);
void Convert(MyAudioConverterSettings *mySetting);
//에러 처리기 : 리턴값이 0이 아니면 에러메시지를 출력하고, 프로그램 종료
static void CheckResult(OSStatus result, const char *operation)
{
if (result == noErr) {
return;
}
char errorString[20];
*(UInt32 *)(errorString + 1) = CFSwapInt32HostToBig(result);
if (isprint(errorString[1]) && isprint(errorString[2]) && isprint(errorString[3]) && isprint(errorString[4])) {
errorString[0] = errorString[5] = '\'';
errorString[6] = '\0';
} else
// 포멧이 정수 값일때
sprintf(errorString, "%d", (int)result);
fprintf(stderr, "에러발생: Error: %s (%s)\n", operation, errorString);
exit(1);
}
#pragma mark - audio converter -
OSStatus MyAudioConverterCallback(AudioConverterRef inAudioConverter,
UInt32 *ioDataPacketCount,
AudioBufferList *ioData,
AudioStreamPacketDescription **outDataPacketDescription,
void *inUserData){
//사용자 데이터 객체를 myaudioConverterSettings 구조체로 형변환
MyAudioConverterSettings *audioConverterSettings = (MyAudioConverterSettings *)inUserData;
//실패했을 때 초기화. 오디오 버퍼를 0으로 초기화 할 수 있다
ioData -> mBuffers[0].mData = NULL;
ioData -> mBuffers[0].mDataByteSize = 0;
//얼마나 많은 패킷을 읽을 지 계산
//ioDataPacketCount는 최초에 Convert(0)에서 계산한 packetPerBuffer이지만, 입력의 마지막에 도달할 때 이것보다 적은 패킷이 남아 있다.
//남아 있는 패킷의 수로 ioDataPacketCount를 재설정한다.
//입력파일에서 읽을 수 있는 패킷의 수 결정
//무엇이 남았는지 읽는다.
if (audioConverterSettings->inputFilePacketIndex + *ioDataPacketCount > audioConverterSettings-> inputFilePacketCount) {
*ioDataPacketCount = audioConverterSettings->inputFilePacketCount - audioConverterSettings->inputFilePacketIndex;
}
if(*ioDataPacketCount == 0 ){
return noErr;
}
//읽을 데이터가 있는 경우 오디오 데이터를 받기 위한 버퍼를 준비할 필요가 있다.
//설정 구조체가 가리키는 오래된 버퍼를 free 하고 (오래된 버퍼의 데이터는 이미 소비됐기 때문에)
//읽으려고 하는 패킷의 수를 저장할 충분히 큰 새로운 버퍼를 할당한다.
//채우고 변환할 버퍼 할당
if (audioConverterSettings->sourceBuffer !=NULL) {
free(audioConverterSettings->sourceBuffer);
audioConverterSettings->sourceBuffer = NULL;
}
audioConverterSettings->sourceBuffer = (void *)calloc(1, *ioDataPacketCount * audioConverterSettings->inputFilePacketMaxSize);
//할당된 버퍼를 가지고 소스 파일에서 읽은 패킷을 그 버퍼로 쓸 준비가 됐다.
UInt32 outByteCount = 0;
OSStatus result = AudioFileReadPackets(audioConverterSettings->inputFile,
true,
&outByteCount,
audioConverterSettings->inputFilePacketDescription,
audioConverterSettings->inputFilePacketIndex,
ioDataPacketCount,
audioConverterSettings->sourceBuffer);
#ifdef MAC_OS_X_VERSION_10_7
if (result == kAudioFileEndOfFileError && *ioDataPacketCount) result = noErr;
#else
if (result == eofErr && *ioDataPacketCount) result = noErr;
#endif
else if (result != noErr) return result;
//읽은 것의 결과를 가지고 소스 파일 위치와 audiobuffer 멤버를 업데이트
audioConverterSettings->inputFilePacketIndex += *ioDataPacketCount;
ioData->mBuffers[0].mData = audioConverterSettings->sourceBuffer;
ioData->mBuffers[0].mDataByteSize = outByteCount;
if (outDataPacketDescription)
*outDataPacketDescription = audioConverterSettings->inputFilePacketDescription;
return result;
}
//최종적으로 audioFileWritePackets()로 전달될 수 있는 패킷 버퍼를 채우는 audioconverterfillcomplexbuffer()를 반복적으로 호출하는 동작을 한다.
//오디오 변환기 생성
void Convert(MyAudioConverterSettings *mySettings){
AudioConverterRef audioConverter;
CheckResult(
AudioConverterNew(&mySettings ->inputFormat, &mySettings ->outputFormat, &audioConverter),
"audioConverterNew failed");
//패킷정보 배열을 얼마나 크게 할당해야 하는지 계산
//형식이 가변비트율인지 여부, 버퍼가 적어도 하나의 패킷을 저장할 만큼 충분히 큰지 등이 해당한다.
//패킷 버퍼 배열 크기와 가변 비트율 데이터의 버퍼당 패킷 수를 결정
UInt32 packetsPerBuffer = 0;
UInt32 outputBufferSize = 32 * 1024;
UInt32 sizePerpacket = mySettings -> inputFormat.mBytesPerPacket;
if(sizePerpacket == 0)
{
UInt32 size = sizeof(sizePerpacket);
CheckResult(
AudioConverterGetProperty(audioConverter, kAudioConverterPropertyMaximumOutputPacketSize, &size, &sizePerpacket),
"couldn't get kAudioConverterPropertyMaximumOutputPacketsSize");
// make sure the buffer is large enough to hold at least one packet
if (sizePerpacket > outputBufferSize) {
outputBufferSize = sizePerpacket;
packetsPerBuffer = outputBufferSize / sizePerpacket;
mySettings -> inputFilePacketDescription = (AudioStreamPacketDescription*)malloc(sizeof(AudioStreamPacketDescription) * packetsPerBuffer);
}
}else
{ //참고:
//가변 비트율 데이터는 mBytesPerPacket == 0을 가진 asbd에 의해서 표시됨
//이 경우 변환기에서 최대 패킷 크기를 가진다. 그리고 그 값을 기본 버퍼 크기 (32kb)와 비교하고 버퍼에 적어도 하나의 패킷을 저장할 수 있다고 확신하는 둘 중에 큰것을 취한다.
//그 후에 패킷 정보 배열을 처리해야 한다.
//버퍼에 얼마나 많은 패킷이 들어갈지 결정할 수 있고, 그만큼의 audioStreamPacketDescriptions를 저장할 수 있는 충분한 공간을 할당한다.
//고정 비트율 데이터의 버퍼당 패킷 결정하기
packetsPerBuffer = outputBufferSize / sizePerpacket;
}
//오디오 변환 버퍼를 위해 메모리 할당
UInt8 *outputBuffer = (UInt8 *)malloc(sizeof(UInt8) * outputBufferSize);
//데이터를 변환하고 쓰기위한 반복문
//출력파일의 어디에 있는지 기억하기 위한 카운터.
UInt32 outputFilePacketPosition = 0;
while (1) {
//변환된 데이터를 받기 위해서 AudioBufferList 준비하기
AudioBufferList convertedData;
convertedData.mNumberBuffers = 1;
convertedData.mBuffers[0].mNumberChannels = mySettings->inputFormat.mChannelsPerFrame;
convertedData.mBuffers[0].mDataByteSize = outputBufferSize;
convertedData.mBuffers[0].mData = outputBuffer;
//수신할 준비가 된 패킷의 수를 전달하는 포인터를 사용한다.
//반환될때 실제로 버퍼에 있는 변환된 패킷의 수를 얻는다.
UInt32 ioOutputDataPackets = packetsPerBuffer;
OSStatus error = AudioConverterFillComplexBuffer(
audioConverter,
MyAudioConverterCallback,
mySettings, //콜백을 위한 사용자 데이터 포인터
&ioOutputDataPackets, //패킷수
&convertedData, //변환된 데이터를 수신할 포인터
(mySettings->inputFilePacketDescription ? mySettings->inputFilePacketDescription : nil)); //패킷정보의 배열을 받을 포인터
if (error || !ioOutputDataPackets)
{
break;
}
//변환된 데이터를 오디오 파일에 작성
//변환된 데이터를 출력파일에 작성
CheckResult(
AudioFileWritePackets(mySettings->outputFile, //작성할 파일,
FALSE, //캐시설정
ioOutputDataPackets, //작성할 크기
NULL, //패킷정보의배열(pcm출력 파일은 고정비트율,패킷정보를 제공x)
outputFilePacketPosition / mySettings->outputFormat.mBytesPerPacket, //첫번째 작성하는 패킷의 인덱스
&ioOutputDataPackets, //쓰여진 패킷의수
convertedData.mBuffers[0].mData), //오디오 변환기에서 받은 오디오 데이터의 버퍼 제공
"couldn't write packets to file");
//패킷을 파일에 쓴 이후에 패킷의 수를 각 pcm 패킷에 바이트로 곱함으로써 출력 위치를 업데이트 한다.
outputFilePacketPosition += (ioOutputDataPackets * mySettings->outputFormat.mBytesPerPacket);
}
AudioConverterDispose(audioConverter);
free(outputBuffer);
}
int main(int argc, const char * argv[]) {
//MyAudioConverterSettings을 할당하고 입력파일을 위해 AudioFileID를 열고, 설정 구조체에 AudioFileID를 저장함으로써 시작한다.
MyAudioConverterSettings audioConverterSetting = {0};
//변환을 위한 소스 파일열기
CFURLRef inputFileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, kInputFileLocation, kCFURLPOSIXPathStyle, false);
CheckResult(
AudioFileOpenURL(inputFileURL,
kAudioFileReadPermission,
0,
&audioConverterSetting.inputFile),
"audioFileOpenURL failed");
CFRelease(inputFileURL);
//입력 파일이 열린채로 audiostreambascidescription을 얻음으로써 소스 오디오의 형식을 알게된다.
//kAudioFilePropertyDataFormat을 추출하기 위해서 AudioFileGetProperty을 호출한다.
//파일에서 부터 오디오 데이터 포멧(asbd) 얻기
UInt32 propSize = sizeof(audioConverterSetting.inputFormat);
CheckResult(
AudioFileGetProperty(audioConverterSetting.inputFile,
kAudioFilePropertyDataFormat,
&propSize,
&audioConverterSetting.inputFormat),
"couldn't get file's data format");
//소스 파일의 전체 패킷수와 가능한 최대 패킷의 크기 속성을 추출
//패킷의 수는 얼마나 많은 읽기를 할 것인지 알게 하고, 최대 패킷의 크기는 audiofilereadpackets()에 적절하게 큰 버퍼를 할당한다.
//입력 오디오 파일에서 패킷 수와 최대 패킷 크기 속성을 얻음
propSize = sizeof(audioConverterSetting.inputFilePacketCount);
CheckResult(
AudioFileGetProperty(audioConverterSetting.inputFile,
kAudioFilePropertyAudioDataPacketCount,
&propSize,
&audioConverterSetting.inputFilePacketCount),
"couldn't gert file's packet count");
propSize = sizeof(audioConverterSetting.inputFilePacketMaxSize);
CheckResult(
AudioFileGetProperty(audioConverterSetting.inputFile,
kAudioFilePropertyMaximumPacketSize,
&propSize,
&audioConverterSetting.inputFilePacketMaxSize),
"couldn't get file's max packet size");
//출력 파일을 설정할 준비완료
//일반적인 lpcm 형식을 설명하기 위해서 asbd 생성
//16비트 샘플, 두개의 채널(스테레오), 빅엔디언의 샘플이 해당됨
//파일을 생성하고, audiofileId를 얻기위해서 이 asbd를 audiofilecreateWithURL에 전달한다.
audioConverterSetting.outputFormat.mSampleRate = 44100.0;
audioConverterSetting.outputFormat.mFormatID = kAudioFormatLinearPCM;
audioConverterSetting.outputFormat.mFormatFlags = kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioConverterSetting.outputFormat.mBytesPerPacket = 4;
audioConverterSetting.outputFormat.mFramesPerPacket = 1;
audioConverterSetting.outputFormat.mBytesPerFrame = 4;
audioConverterSetting.outputFormat.mChannelsPerFrame = 2;
audioConverterSetting.outputFormat.mBitsPerChannel = 16;
//출력 파일 생성
CFURLRef outputFileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, CFSTR("myoutputfile.aif"), kCFURLPOSIXPathStyle, false);
CheckResult(
AudioFileCreateWithURL(outputFileURL, kAudioFileAIFFType, &audioConverterSetting.outputFormat, kAudioFileFlags_EraseFile, &audioConverterSetting.outputFile),
"audiofilecreatewithurl failed");
CFRelease(outputFileURL);
fprintf(stdout, "converting..\n");
Convert(&audioConverterSetting);
cleanup:
AudioFileClose(audioConverterSetting.inputFile);
AudioFileClose(audioConverterSetting.outputFile);
printf("done\r");
return 0;
}
/*
정리
- main 에서 입력파일을 열었고, 형식과 패킷의 크기속성을 검사했으며, pcm형식을 선택하여 출력 파일을 생성하였다.
- 패킷을 전송하기 위해 반복적으로 AudioBufferList를 설정하고 압축된 입력 데이터를 PCM으로 변환하기 위해서 AudioConverterFillComplexBuffer()를 호출하는 Converter() 함수를 작성했다.
- 그리고 AudioFileWritePackets() 로 출력 파일에 작성했다.
- 변환기의 입력은 개발자가 작성한 콜백 함수에서 오고, 이때 콜백 함수는 임력 파일에서 반복적으로 읽고, AudioBufferList에 전달된 오디오 데이터와 패킷 정보를 제공한다.
*/
'ios 뽀개기 > objective-c' 카테고리의 다른 글
objective c nsoperation 예제 (1) | 2018.11.27 |
---|---|
ios objective c - http 네트워크 통신 1 (0) | 2018.11.14 |
ios AVFoundation으로 오디오 플레이어 만들기 (0) | 2018.11.10 |
ios objective c 코어오디오 다루기 5 - 재생기능 (0) | 2018.11.09 |
ios objective c 코어오디오 다루기 4 - 녹음기능 (0) | 2018.11.08 |
댓글