본문 바로가기
ios 뽀개기/objective-c

ios objective c 코어오디오 다루기 6 - 오디오 파일 변환

by 인생여희 2018. 11. 13.
반응형

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에 전달된 오디오 데이터와 패킷 정보를 제공한다.

 

 */


반응형

댓글