본문 바로가기
아이폰 개발/ios 개념&튜토리얼

iOS coreaudio 재생 예제 - 아이폰

by 인생여희 2021. 1. 21.

iOS coreaudio 재생 예제 - 아이폰

 

1.오디오 재생을 위한 변수 설정

 

#import <AudioToolbox/AudioToolbox.h>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow * window;


/*
 ASBD (오디오 스트림 기본 설명)를 구성하여 선형 PCM 형식 또는 동일한 크기의 채널이있는 CBR (고정 비트 전송률) 형식을 지정할 수 있습니다.
 가변 비트 전송률 (VBR) 오디오 및 채널의 크기가 다른 CBR 오디오의 경우
 각 패킷은 AudioStreamPacketDescription 구조에 의해 추가로 설명되어야합니다.
 */
@property (nonatomic, assign) AudioStreamBasicDescription streamFormat;

//UInt32 : 42억...
@property (nonatomic, assign) UInt32 bufferSize;
@property (nonatomic, assign) double currentFrequency;
@property (nonatomic, assign) double startingFramCount;


@property (nonatomic, assign) AudioQueueRef audioQueue;




-(OSStatus) fillBuffer:(AudioQueueBufferRef) buffer; //A pointer to an audio queue buffer.




@end

 

#참고

AudioQueueRef

 

 오디오 대기열을 나타내는 불투명 한 데이터 유형을 정의합니다.

 오디오 대기열은 macOS에서 오디오를 녹음하거나 재생하는 데 사용하는 소프트웨어 개체입니다. 다음 작업을 수행합니다.

 1.오디오 하드웨어에 연결

 2.메모리 관리

 3.압축 된 오디오 형식을 위해 필요에 따라 코덱 사용

 4.녹화 또는 재생 조정

 

 오디오 대기열 서비스에 설명 된 기능을 사용하여 오디오 대기열을 생성, 사용 및 폐기합니다.

 

 

AudioQueueBufferRef

 

 각 오디오 대기열에는 연결된 오디오 대기열 버퍼 세트가 있습니다.

 버퍼를 할당하려면 AudioQueueAllocateBuffer 함수를 호출합니다.

 버퍼를 삭제하려면 AudioQueueFreeBuffer 함수를 호출합니다.

 

 VBR 압축 오디오 데이터 형식을 사용하는 경우 대신 AudioQueueAllocateBufferWithPacketDescriptions 함수를 사용할 수 있습니다.

 이 함수는 패킷 설명을 위한 추가 공간이있는 버퍼를 할당합니다.

 mPacketDescriptionCapacity, mPacketDescriptions 및 mPacketDescriptionCount 필드는

 AudioQueueAllocateBufferWithPacketDescriptions로 할당 된 버퍼에만 사용할 수 있습니다.

 

AudioQueueBufferRef 프로퍼티 (속성)

 

- mAudioData

 오디오 데이터는 오디오 대기열 버퍼를 소유했습니다. 버퍼 주소는 변경할 수 없습니다.

 

 - mAudioDataByteSize

 오디오 대기열 버퍼의 mAudioData 필드에있는 유효한 오디오 데이터의 바이트 수이며 처음에는 0으로 설정됩니다.

 콜백은 재생 오디오 대기열에 대해이 값을 설정해야합니다. 녹음의 경우 녹음 오디오 대기열이 값을 설정합니다.

 

 - mAudioDataBytesCapacity

 오디오 대기열 버퍼의 크기 (바이트)입니다. 이 크기는 버퍼가 할당 될 때 설정되며 변경할 수 없습니다.

 

 - mPacketDescriptionCapacity

 mPacketDescriptions 필드에 저장할 수있는 최대 패킷 설명 수입니다.

 

 - mPacketDescriptionCount

 버퍼에있는 유효한 패킷 설명 수입니다. 재생을위한 버퍼를 제공 할 때이 값을 설정합니다. 오디오 대기열은 녹음 대기열에서 버퍼를 반환 할 때이 값을 설정합니다.

 

 - mPacketDescriptions

 버퍼에 대한 AudioStreamPacketDescription 구조의 배열입니다.

 

 - mUserData

 녹음 또는 재생 오디오 대기열을 만들 때 콜백 함수에서 사용하도록 지정하는 사용자 지정 데이터 구조입니다.

 

 

2.생성할 음원 데이터

 

#pragma mark - #defines

#define FOREGROUND_FREQUENCY 880.0
#define BACKGROUND_FREQUENCY 523.25
#define BUFFER_COUNT         3
#define BUFFER_DURATION      0.5

 

 

3. synthesizes

 

#pragma mark @synthesizes

@synthesize streamFormat = _streamFormat;
@synthesize bufferSize;
@synthesize currentFrequency;
@synthesize startingFramCount;
@synthesize audioQueue;

 

4.오류 처리 함수

 

#pragma mark helpers - 공통 에러 메소드. err 이 0이 아니면 , 에러 메시지 출력 후, 프로그램 종료

static void CheckError(OSStatus error, const char *operation){
    
    //오류 없으면 return
    if (error == noErr) return;
    
    char str[20];
    
    //32 비트 정수를 호스트의 기본 바이트 순서에서 빅 엔디안 형식으로 변환합니다.
    *(UInt32 *)(str + 1) = CFSwapInt32HostToBig(error);
    
    
    //c가 출력할 수 있는 문자의 ASCII 코드 값인지 판별
    if (isprint(str[1]) && isprint(str[2]) && isprint(str[3]) && isprint(str[4])) {
        str[0] = str[5] = '\'';
        str[6] = '\0';
    }else
        //str 변수에 error 값 대입
        sprintf(str, "%d" , (int)error);
        
    //출력
    fprintf(stderr, "오류 : %s (%s) \n" , operation , str);
    
    exit(1);
    
}

 

 

5.오디오 큐 세션 인터럽트 함수

 

- (void)audioSessionDidChangeInterruptionType:(NSNotification *)notification
{
    NSLog(@"Interrupted 발생 - !");
    
    AVAudioSessionInterruptionType interruptionType =
    [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    
    NSLog(@"interruptionType : %lu" , (unsigned long)interruptionType);
    
    if (AVAudioSessionInterruptionTypeBegan == interruptionType){
        
        printf("kAudioSessionBeginInterruption \n");
        
    }else if(AVAudioSessionInterruptionTypeEnded == interruptionType){
        
        printf("kAudioSessionEndInterruption \n");
        CheckError(AudioQueueStart(self.audioQueue, 0), "오디오 큐를 재시작 할 수 없습니다...");
        
    }
}

 

 

6.오디오 큐 콜백 함수

 

/*
 - 오디오 큐 콜백 함수 -
 1.파일 or 데이터 읽어서 빈 버퍼 채우기
 2.채워진 버퍼를 재생 큐에 넣기 - AudioQueueEnqueueBuffer
 */

static void MyAQOutputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inCompleteAQBuffer)
{
    printf("MyAQOutputCallback 호출 \n");

    NSLog(@"inCompleteAQBuffer : %u" , inCompleteAQBuffer->mAudioDataByteSize);
    
    AppDelegate *appDelegate = (__bridge AppDelegate *)inUserData;
    
    //ex) fillBuffer 함수 : 오디오 파일에서 읽은 데이터 버퍼에 채우기
    //버퍼 다시 채우기
    CheckError([appDelegate fillBuffer:inCompleteAQBuffer],
             "버퍼를 다시 채울 수 없습니다...");
    
    //inCompleteAQBuffer 에는 파일에서 읽은 데이터가 들어있다!
    
    /* 채워진 버퍼를 재생 큐에 넣기 - 소리 출력*/
    //param : 1.오디오 큐, 2.채워진 버퍼. 3. vbr이면 패킷값, cbr이면 0, NULL
    CheckError(AudioQueueEnqueueBuffer(inAQ,
                                inCompleteAQBuffer,
                                0,
                                NULL),
                                "버퍼를 큐에 enqueue 할 수 없습니다...(refill)");
    
}

 

 

7.버퍼 채우기 함수

 

//버퍼 채우기
/*
 참고 : 실제 파일에서 읽으려면 AudioFileReadPacketData 함수 사용가능
 AudioFileReadPacketData(오디오파일, 캐시설정, 실제읽을바이트, packetDescs, currentPacket, 읽은패킷, 버퍼에서 읽은 데이터)
 */
-(OSStatus) fillBuffer:(AudioQueueBufferRef)buffer{
    
    printf("fillBuffer 호출 \n");
    
    
    double j = self.startingFramCount;
    double cycleLength = 44100. / self.currentFrequency;
    int frame = 0;
    double frameCount = bufferSize / self.streamFormat.mBytesPerFrame;
    
    printf("startingFramCount : %f \n" , j);
    printf("currentFrequency : %f \n" , self.currentFrequency);
    printf("cycleLength : %f \n" , cycleLength);
    printf("bufferSize : %u \n" , bufferSize);
    printf("self.streamFormat.mBytesPerFrame : %u \n" , self.streamFormat.mBytesPerFrame);
    printf("frameCount : %f \n\n" , frameCount);
    
    
    for (frame = 0; frame < frameCount; ++frame)
    {
        SInt16 *data = (SInt16*)buffer->mAudioData;
        (data)[frame] = (SInt16) (sin (2 * M_PI * (j / cycleLength)) * SHRT_MAX);
        
        j += 1.0;
        if (j > cycleLength)
            j -= cycleLength;
    }
    
    self.startingFramCount = j;
    
    //버퍼 채우기
    buffer->mAudioDataByteSize = bufferSize;
    
    
    return noErr;
}

 

 

8. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions 내부

- 구현 순서 -

1.AVAudioSession 설정

2.ASBD 설정

3.오디오 출력 큐 생성(AudioQueueNewOutput) - param : 1.ASBD, 2.callback 함수 , 3.audioQueue

4.큐에 버퍼 할당(AudioQueueAllocateBuffer) - param : 1.오디오 큐, 2.버퍼 사이즈, 3. 버퍼_Ref

5.버퍼 채우기 - fillBerffer

6.버퍼를 큐에 넣기(AudioQueueEnqueueBuffer) - param : 1. 오디오 큐, 2.채워진 버퍼_Ref

7.오디오 큐 시작(AudioQueueStart)

 

 

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    

    NSError *error = nil;
    
    /* 1.AVAudioSession 설정 */
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionDidChangeInterruptionType:)
    name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];


    if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]) {
        
        NSLog(@"오디오 세션 카테고리를 셋팅할 수 없습니다 : %@" , error);
    }else{
        NSLog(@"오디오 세션 카테고리를 셋팅 성공");
    }
    
    
    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
        NSLog(@"오디오 세션을 활성화 시킬 수 없습니다 : %@" , error);
    }else{
        NSLog(@"오디오 세션 활성화 성공");
    }
    
    
    /* 2.ASBD 설정 */
    self.currentFrequency = FOREGROUND_FREQUENCY;
    _streamFormat.mSampleRate = 44100.0;
    _streamFormat.mFormatID = kAudioFormatLinearPCM;
    _streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    
    _streamFormat.mChannelsPerFrame = 1;
    _streamFormat.mFramesPerPacket = 1;
    _streamFormat.mBitsPerChannel = 16;
    _streamFormat.mBytesPerFrame = 2;
    _streamFormat.mBytesPerPacket = 2;
    

    
    /* 3.오디오 출력 큐 생성 */
    /* param : 1.ASBD, 2.callback 함수 , 3.audioQueue */
    CheckError(AudioQueueNewOutput(&_streamFormat,
                             MyAQOutputCallback,
                             (__bridge void * _Nullable)(self),
                             NULL,
                             kCFRunLoopCommonModes,
                             0,
                             &audioQueue),
                            "출력 오디오 큐를 생성할 수 없습니다.");
    
    
    /* 4.버퍼 생성 및 큐에 넣어주기 */
    AudioQueueBufferRef buffers[BUFFER_COUNT];
    bufferSize = BUFFER_DURATION * self.streamFormat.mSampleRate * self.streamFormat.mBytesPerFrame;
    NSLog(@"bufferSize : %u" , bufferSize);
    
    for (int i = 0; i < BUFFER_COUNT; i++) {
        
        //4-1 버퍼 할당
        /* param : 1.오디오 큐, 2.버퍼 사이즈, 3. 버퍼 포인터 */
        CheckError(AudioQueueAllocateBuffer(audioQueue,
                                     bufferSize,
                                     &buffers[i]),
                                     "오디오 큐 버퍼를 할당할 수 없습니다...");
        
        //4-2 버퍼 채우기
        CheckError([self fillBuffer:buffers[i]],
                 "버퍼를 채울 수 없습니다...(priming)");
        
        
        //4-3 버퍼를 큐에 넣기
        /* param : 1. 오디오 큐, 2.채워진 버퍼_ Ref */
        CheckError(AudioQueueEnqueueBuffer(audioQueue,
                                    buffers[i],
                                    0,
                                    NULL),
                                    "버퍼를 큐에 enqueue 할 수 없습니다...(priming)");
        
        
    }
    
    /* 5. 오디오 큐 시작 */
    CheckError(AudioQueueStart(audioQueue,
                          NULL),
                          "오디오 큐를 시작할 수 없습니다...");
    
    return YES;
}

 

 

9.백그라운드, 포그라운드 진입시 음원 변경 함수

 

//백그라운드 진입 감지 메소드
-(void)applicationDidEnterBackground:(UIApplication *)application{
    
    //소리 변경
    self.currentFrequency = BACKGROUND_FREQUENCY;
    
}



//포그라운드 진입 감지 메소드
-(void)applicationWillEnterForeground:(UIApplication *)application{
        
    //1.세션 다시 활성화
    NSError *error = nil;
    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
        NSLog(@"오디오 세션을 재 활성화 시킬 수 없습니다 : %@" , error);
    }
    
    //2. 오디오 큐 재시작
    CheckError(AudioQueueStart(self.audioQueue,
                         0),
               "오디오 큐를 다시 시작 할 수 없습니다....");
    
    //3.소리 변경
    self.currentFrequency = FOREGROUND_FREQUENCY;
}

 

 

예제자료1

 

 

참고사이트(현재 시간체크 관련)

https://stackoverflow.com/questions/30377711/precise-time-of-audio-queue-playback-finish

https://stackoverflow.com/questions/707377/how-to-retrieve-a-valid-mhosttime-using-audioqueues

https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/CoreAudioOverview/CoreAudioEssentials/CoreAudioEssentials.html

https://stackoverflow.com/questions/3395244/how-to-check-current-time-and-duration-in-audioqueue