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

iOS AudioFileStream 예제

by 인생여희 2021. 1. 22.

iOS AudioFileStream 예제

 

 

동작원리

 

 

1.AudioFileStream 구조체 생성

 

 

SimpleStreamer.h

 

/*
참고:
네트워크 상태는 시시각각 변할 수 있으므로,
이에 대응하려면 버퍼에 충분한 오디오 데이터가 있어야 한다.
*/
#import <AudioToolbox/AudioToolbox.h>
#import <Foundation/Foundation.h>

// 할당할 오디오 큐 버퍼 개수
#define kNumAQBufs 3

// 배열 내의 패킷 디스크립션 개수
#define kAQMaxPacketDescs 512

// 하드 코딩된 버퍼 사이즈
#define kAQBufSize 1048576. /* 1MB 또는 2**20 */

/*
 오디오 큐 버퍼와 관련 데이터를 저장할 데이터 구조체
 스트리밍 클래스의 인스턴스는 PlayQueueData 구조체 배열을 생성한다.
*/
typedef struct PlayQueueData {
    
    AudioQueueBufferRef buffer;         //버퍼 레퍼런스
    
    //버퍼 사용중 표시(새 버퍼를 큐에 삽입할 때, 큐의 버퍼들이 사용중인지 검사하여 새로운 버퍼를 삽입할지 여부 결정)
    NSCondition *queuedCondition;
    
    UInt32 packetCount;
    AudioStreamPacketDescription packetDescriptors[kAQMaxPacketDescs];
    size_t bytesFilled;
    BOOL inUse;
} PlayQueueData_t;



@interface SimpleStreamer : NSObject <NSURLSessionDataDelegate>
{

    //오디오 스트리밍 클래스 인터페이스는 오디오 소스를 가리키는 URL로 초기화 권장.
    NSURL *url;
    
    //URL에 있는 소스로 부터 데이터를 내려 받기 위해 NSURLConnection의 기능에 의존한다.
    NSURLConnection *networkConnection;
    
    //오디오 파일 스트림 ID
    AudioFileStreamID myAudioStream;
    
    //오디오 큐
    AudioQueueRef playQueue;
    
    //오디오 파일 타입 ID
    AudioFileTypeID fileTypeHint;
    
    
    BOOL queueStarted;
    BOOL queueRunning;
    
    PlayQueueData_t *playQueueDataRecs;
    
    unsigned int currentBufferIndex;
}

@property (readonly) NSURL *url;

@property (readwrite) AudioQueueRef playQueue;

@property (readwrite) AudioFileTypeID fileTypeHint;

@property (readwrite) BOOL queueRunning;

@property (readwrite) BOOL queueStarted;

@property (readwrite) unsigned int currentBufferIndex;

@property (readwrite) PlayQueueData_t *playQueueDataRecs;


- (id)initWithURL:(NSURL *)url;
- (void)play;
- (void)stop;

@end


 

 

2. synthesize 설정

 

#import "SimpleStreamer.h"

@interface SimpleStreamer ()
- (void)startQueue;
@end

@implementation SimpleStreamer

@synthesize url;
//@synthesize dataFormat;
@synthesize playQueue;
@synthesize queueRunning;
@synthesize queueStarted;
@synthesize currentBufferIndex;
@synthesize playQueueDataRecs;
@synthesize fileTypeHint;

 

3.오디오 세션 초기화

 

+ (void)initialize
{
    //오디오 세션 초기화
    AudioSessionInitialize(NULL, NULL, NULL, NULL);
}

 

 

 4.스트리밍 클래스 객체 초기화

 

PlayQueueData_t 구조체의 메모리 할당됨.

PlayQueueData_t 값은 아직 NULL 상태

오디오 포맷을 알기 전까지 오디오 버퍼를 할당할 수 없기 때문이다

 

- (id)initWithURL:(NSURL *)audioUrl
{
    //오디오 스트리밍 클래스 인터페이스는 오디오 소스를 가리키는 URL로 초기화 권장.
    
    if (self = [super init]) {
        url = audioUrl;
        playQueueDataRecs = (PlayQueueData_t *)malloc(sizeof(PlayQueueData_t) * kNumAQBufs); //초기엔 nil
    }
    return self;
}

 

5.스트리밍 시작

 

스트리밍 오디오의 경우 오디오 파일을 가지고 초기화를 하는것이 아니다.

따라서 아직, 오디오 포멧에 대해서 살펴볼 수 없고, 그와 관련된 작업도 불가능하다.

단지, AudioFileStreamID(myAudioStream) 을 선언하고 네트워크 연결을 시작하면된다.

 

- (void)play
{
/*
    콜백 함수를 사용해서 오디오 스트림 생성
    두번째 매개변수 : 프로퍼티 리스너 콜백 함수
    세번째 매개변수 : 데이터 콜백 함수
    네번째 매개변수 : 오디오 포멧에 대한 힌트(여기서는 스트림이 알아서 결정하도록 0 설정)
    스트림에서 충분한 데이터를 받아서 데이터의 특성 및 정보들을 파악 할 수 있게 되면, 이 함수들을 호출하게 된다.
*/
    AudioFileStreamOpen((__bridge void * _Nullable)(self),
                         propertyListenerCallback,
                         audioDataCallback,
                         self.fileTypeHint,
                         &myAudioStream);
    

    //네트워크 연결 생성
    //URL에 있는 소스로 부터 데이터를 내려 받기 위해 NSURLConnection의 기능에 의존한다.
    NSURLRequest *networkRequest = [NSURLRequest requestWithURL:self.url];
    
/*
     NSURLConnection은 곧바로 데이터를 받기 시작한다.
     delegate를 self로 설정해 놓았으므로 커넥션은 내려받은 데이터들을
     스트리밍 객체에 있는 connection didReceiveData: 메소드에 제공한다.(순서3 으로)
*/
     networkConnection = [[NSURLConnection alloc] initWithRequest:networkRequest delegate:self];
}

 

 

6.NSURLConnection 델리게이트 메소드

 

네트워크에서 데이터를 받아 파싱하기

이 메소드는 1kb, 2kb 같은 작은 데이터와 함께 호출되기 때문에 상당히 빈번히 호출 될 수 있다.

 

 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSLog(@"Received %u bytes", (unsigned int)[data length]);
    
    /*
     이 메소드가 호출될 때마다, 읽어 들인 바이트를 오디오 스트림 파서로 전달
     첫번째 매개변수 : 이전에 생성한 스트림 객체 (파싱 함수는 이 매개변수를 이용해서 등록된 콜백 함수를 찾아낸다.)
    */
     AudioFileStreamParseBytes(myAudioStream,
                         (unsigned int)[data length],
                         [data bytes],
                         0);

    /*
     충분한 데이터를 받았다면 오디오의 프로퍼티들을 결정할 수 있게된다.
     프로퍼티 정보가 발견 될 때마다, 프로퍼티 리스너 콜백이 호출 된다.(->순서4)
     */
}

 

 

7. 오디오 파일 스트림 서비스에 대한 프로퍼티 리스너 콜백 함수

 

충분한 데이터를 받았다면 오디오의 프로퍼티들을 결정할 수 있게된다.

프로퍼티 정보가 발견 될 때마다, 프로퍼티 리스너 콜백이 호출 된다.

콜백이 처리를 마친 후에도, 프로퍼티 정보들에 접근 할 수 있다.

데이터 포멧을 알게 되면 즉시 프로퍼티 리스너가 호출되겠지만,

이후 언제라도 오디오 스트림에 있는 프로퍼티들을 다시 찾아 볼 수 있다.

이 정보들은 우리가 만든 변수에 저장할 필요는 없다.

 

결국 프로퍼티 리스너 함수는 오디오 패킷을 생성할 준비가 될 때까지는 아무일도 하지 않는다.

오디오 패킷을 생성할 준비가 되면, 필요한 모든 정보를 찾아볼 수 있게 되는데,

이는 모든 메타데이터를 내려 받았고, 재생을 시작할 준비가 되었음을 의미한다.

 

프로퍼티 리스너 콜백은 매개변수로 넘어온 inPropertyID를 검사하여 새로운 프로퍼티 정보가 있는지 알아본다.

만약 inPropertyID 값이 kAudioFileStreamProperty_ReadyToProducePackets 아니라면 아무일도 하지 않는다.

스트림이 패킷을 만들어낼 준비가 되면 이전에 스트림이 생성되었을 때 하지 못했던 작업을 완료하기 위해 아래와 같은 작업을 한다.

     

우선 오디오 포맷 프로퍼티를 확인하고나서 재생 큐를 생성한다.

재생큐를 생성할 때, self 변수를 사용하여 스트리밍객체에 재생큐를 설정한다.

또한 AudioQueueNewOutput에도 self를 넘겨주어 postPlayCallback 함수에서 사용할 수 있도록 한다.

     

이제 오디오 버퍼를 생성할 수 있을 정도로 충분한 정보를 얻었으므로, 콜백은 오디오 버퍼를 생성한다.

버퍼생성은 한번만 하고, 필요한 만큼 버퍼를 재사용한다.

self의 currentBufferIndex를 0으로 설정한다. 이렇게 하면 배열에 있는 첫번째 버퍼가 현재버퍼가 된다.

그리고 이 버퍼를 사용중으로 설정한다.

 

재생 큐를 생성하였으니, 오디오를 재생할 준비가 되었다.

새로 데이터가 들어오면 AudioFileStreamParseBytes()에게 넘겨줄 것이다.

그 후에는 오디오 데이터 콜백인 audioDataCallBack()이 호출 된다.

 

void propertyListenerCallback (void *inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *ioFlags)
{
    SimpleStreamer *self = (__bridge SimpleStreamer *)inClientData;
    OSStatus err = noErr;
    UInt32 propertySize;
    
    // (단계0) -  프로퍼티 체크
    // 프로퍼티 리스너 콜백은 매개변수로 넘어온 inPropertyID를 검사하여 새로운 프로퍼티 정보가 있는지 알아본다.
    // 만약 inPropertyID 값이 kAudioFileStreamProperty_ReadyToProducePackets 아니라면 아무일도 하지 않는다.
    if (inPropertyID == kAudioFileStreamProperty_ReadyToProducePackets) {
        
        //오디오 패킷을 생성할 준비가 된 스트림
        
        //(단계1) - 오디오 포맷(형식) 정보 획득
        //우선 오디오 포맷 프로퍼티를 확인하고나서 재생 큐를 생성한다.
        AudioStreamBasicDescription dataFormat;
        propertySize = sizeof(dataFormat);
        
        err = AudioFileStreamGetProperty(inAudioFileStream,
                                   kAudioFileStreamProperty_DataFormat,
                                   &propertySize,
                                   &dataFormat);
        
        
        //(단계2) - 재생 큐 생성
        //재생큐를 생성할 때, self 변수를 사용하여 스트리밍객체에 재생큐를 설정한다.
        //또한 AudioQueueNewOutput에도 self를 넘겨주어 postPlayCallback 함수에서 사용할 수 있도록 한다.
        AudioQueueRef playQueue;
        err = AudioQueueNewOutput(&dataFormat,
                              postPlayCallback,
                              (__bridge void * _Nullable)(self),
                              NULL,
                              kCFRunLoopCommonModes,
                              0,
                              &playQueue);
        
        //오디오 재생큐 할당
        [self setPlayQueue:playQueue];
        
        
        
        //(단계3) - 오디오 버퍼구조체 설정
        /*
            이제 오디오 버퍼를 생성할 수 있을 정도로 충분한 정보를 얻었으므로, 콜백은 오디오 버퍼를 생성한다.
            버퍼 생성은 한번만 하고, 필요한 만큼 버퍼를 재사용한다.
            self의 currentBufferIndex를 0으로 설정한다. 이렇게 하면 배열에 있는 첫번째 버퍼가 현재버퍼가 된다.
            그리고 이 버퍼를 사용중으로 설정한다.
         */
        for (int i=0; i<kNumAQBufs; i++) {
            
            //NSCondition 초기화
            self.playQueueDataRecs[i].queuedCondition = [[NSCondition alloc] init];
            
            //재생큐에 버퍼 할당
            err = AudioQueueAllocateBuffer(playQueue,
                                      kAQBufSize,
                                      &(self.playQueueDataRecs[i].buffer));
           
            if (err != noErr) {
                NSLog(@"Failed to allocate buffer: %d", err);
            }
            
        }
        
        self.currentBufferIndex = 0;
        
        
        // 데이터를 기록하기 시작할, 초기버퍼 잠금
        // 첫번째 버퍼 사용중으로 설정
        NSCondition *queuedCondition = (NSCondition*)self.playQueueDataRecs[0].queuedCondition;
        
        NSLog(@"Locking condition");
        
        [queuedCondition lock];
        
        self.playQueueDataRecs[0].inUse = YES;
        
        NSLog(@"Unocking condition");
        
        [queuedCondition unlock];
        
        
        //매직쿠키 정보 획득(MPEG4 AAC와 같은 압축 포맷의 경우를 위해)
        //파일 스트림에서 가져와서 오디오 큐에 설정한다.
        err = AudioFileStreamGetPropertyInfo(inAudioFileStream,
                                      kAudioFileStreamProperty_MagicCookieData,
                                      &propertySize,
                                      NULL);
       
        //매직 쿠키 데이터를 담을 메모리 할당
        void *magicCookie = calloc(1, propertySize);
        
        
        
        err = AudioFileStreamGetProperty( inAudioFileStream,
                                    kAudioFileStreamProperty_MagicCookieData,
                                    &propertySize,
                                    magicCookie);
        
        //매직 쿠키 => 재생 큐에 할당
        if (err == noErr) {
            
            err = AudioQueueSetProperty( playQueue,
                                    kAudioFileStreamProperty_MagicCookieData,
                                    magicCookie,
                                    propertySize);
        }
        
        //메직 쿠키 메모리 해제
        free(magicCookie);
        
    }
    
    
}

 

 

8. 오디오 데이터 콜백 함수

 

오디오 데이터 콜백의 주된 작업은 파싱된 오디오 패킷들을 받아 현재 버퍼에 넣는것이다.

버퍼가 가득차게 되면, 유틸리티 함수인 enqueueCurrentBuffer() 를 이용해서 오디오 큐에 버퍼를 추가한다.

 

콜백으로 들어오는 데이터에 포함된 오디오 패킷의 개수는 알 수 없다.

패킷의 개수는 네트워크로부터 얼마나 많은 데이터를 받았느냐에 따라 결정되기 때문이다.

 

오디오데이터 콜백함수는 내부적으로 패킷의 개수만큼 루프를 돌게 되어 있다.

일단 버퍼가 패킷 데이터를 담을 수 있을 정도로 충분한 용량을 가졌는지 확인한다.

만약 용량이 부족하면 enqueueCurrentBuffer()를 호출하여 현재 버퍼를 큐에 추가하고 다음 버퍼를 가져온다.

 

위 과정을 거치고 나면 현재 버퍼는 패킷 정보를 담을 만큼 큰 버퍼가 된다.

(새로 가져온 버퍼 혹은 패킷을 담을 만큼 공간이 남아 있는 이전 버퍼든 상관없이.)

현재 버퍼에 오디오 데이터를 복사하기 위해 playQueueData 구조체에 있는 AudioQueueBufferRef에 대한 레퍼런스를 얻어온다.

memcpy()를 호출하면 들어온 패킷에 있는 정보가 buffer로 복사된다.

그리고 현재 패킷에 대한 상세 정보를 추가하고, 패킷 count 값과 버퍼의 바이트들을 갱신한다.

 

마지막으로 현재 버퍼에 대한 두 번째 검사를 한다.

이번에는 다른 패킷을 처리할 수 있는 공간이 더 남아 있는지 보기 위해서다.

이전 검사의 경우와 마찬가지로 버파가 가득찬 경우 enqueueCurrentBuffer()를 이용해서,

버퍼를 큐에 추가하고 다음 버퍼를 가져온다.

 

데이터를 큐에 추가하는 작업은 enqueueCurrentBuffer()이라는 유틸리티 함수를 통해서 이루어진다.

이렇게 하는 이유는 여러 곳에서 이 함수를 호출하기 때문이기도 하고, 코드가 길어서 일일이 복사해서 쓰면,

모양새가 좋지 않기 때문이다.

 

void audioDataCallback (void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescriptions)
{
    NSLog(@"Got audio data in callback: %d bytes, %d packets", inNumberBytes, inNumberPackets);
    
    SimpleStreamer *self = (__bridge SimpleStreamer *)inClientData;
    
    //들어오는 패킷들을 검사한다
    for (int i=0; i<inNumberPackets; i++) {
        @synchronized(self) {
            if (self.queueStarted && (!self.queueRunning)) {
                
                // 큐가 실행 중이 아니면 작업을 중단한다.
                NSLog(@"bailing out of audioDataCallback");
                return;
            }
        }
        
        // 현재 패킷 데이터의 사이즈와 오프셋을 얻어온다.
        SInt64 packetOffset = inPacketDescriptions[i].mStartOffset;
        SInt64 packetSize = inPacketDescriptions[i].mDataByteSize;
        
        // 현재 버퍼에 충분한 공간이 남아 있는지 확인한다.
        size_t bufSpaceRemaining = kAQBufSize - self.playQueueDataRecs[self.currentBufferIndex].bytesFilled;
        if (bufSpaceRemaining < packetSize) {
            
            // 현재 버퍼에 충분한 공간이 없으므로, 현재버퍼를 큐에 삽입후, 다음버퍼를 이용한다.
            enqueueCurrentBuffer(self);
        }
        
        // 데이터를 오디오 큐 버퍼로 복사한다.
        AudioQueueBufferRef fillBuf = self.playQueueDataRecs[self.currentBufferIndex].buffer;
       
        memcpy((char*)fillBuf->mAudioData + self.playQueueDataRecs[self.currentBufferIndex].bytesFilled,
               (const char *)inInputData + packetOffset, packetSize);
        
        
            // 패킷 디스크립션 정보를 채운다.
        self.playQueueDataRecs[self.currentBufferIndex].packetDescriptors[self.playQueueDataRecs[self.currentBufferIndex].packetCount] = inPacketDescriptions[i];
        self.playQueueDataRecs[self.currentBufferIndex].packetDescriptors[self.playQueueDataRecs[self.currentBufferIndex].packetCount].mStartOffset = self.playQueueDataRecs[self.currentBufferIndex].bytesFilled;
        
        // 현재 버퍼에 채워진 바이트와 패킷의 양을 추적한다.
        self.playQueueDataRecs[self.currentBufferIndex].bytesFilled += packetSize;
        self.playQueueDataRecs[self.currentBufferIndex].packetCount += 1;
        
        // 패킷 공간을 모두 소비했는지 확인한다.
        size_t packetDescriptorsRemaining = kAQMaxPacketDescs - self.playQueueDataRecs[self.currentBufferIndex].packetCount;
        if (packetDescriptorsRemaining == 0) {
            // 현재 버퍼에 더 이상의 패킷 디스크립터가 없으므로, 현재 버퍼를 큐에 삽입한다.
            enqueueCurrentBuffer(self);
        }
    }
}

 

 

9. 현재 버퍼를 큐에 삽입

 

enqueueCurrentBuffer 는 현재 버퍼를 AudioQueueEnqueueBuffer() 에 넘겨주어 재생 될 수 있도록한다.

나머지는 큐가 부드럽게 동작하도록 해주기만 하면 된다.

 

먼저 현재 버퍼를 '사용중'으로 표시하고 큐에 넣는다.

곧바로 -startQueue 메소드를 호출하여 오디오가 실제 재생되도록 한다.

지금까지 데이터 패킷들과 버퍼들을 처리했으니 음원이 스피커로 흘러나와야 정상이다.

 

현재 버퍼를 큐에 넣었으니 다음 버퍼를 처리할 차례다.

currentBufferIndex를 증가시킴으로써 다음 버퍼를 가져올 수 있게 되는데,

이때 currentBufferIndex 값이 실제 버퍼들 값보다 커지게 되는일이 없도록 처리 해야 한다.

 

NSArray를 사용한 경우 배열의 크기를 벗어나 접근을 시도하게 되면 곧 바로 런타임 에러가 발생하게 된다.

C스타일의 배열은 C언어의 접근 방식을 따르는데, 이는 실제 선언된 배열의 크기를 넘어서 접근하더라도,

에러 없이 넘어갈 수 있다는 것을 의미한다.

 

결국 배열의 바로 뒤쪽에 있던 누군가의 메모리값을 바꿔버리는 수도 있다.

다행히도 다른 프로그램의 메모리 영역을 침범한 대부분의 프로그램은 즉시 종료된다.

하지만 항상 그렇게 되는 것이 아니다.

 

메모리를 침범해도 프로그램이 종료되지 않는 상태가 되면 언젠가는 문제가 발생할 소지를 짊어진채,

프로그램이 수행되는 결과를 초래한다.

이러한 문제를 피하는 간단한 방법은 항상 currentBufferIndex 값이 -init에서 생성한 버퍼의 개수를 가리키는

kNumAQBufs 값보다 작거나 같은지 검사하는 것이다.

 

!네트워크 속도가 빨라서 음원재생 속도 보다 데이터를 가져오는 속도가 훨씬 빠를 수도 있다.

이런 경우에는 버퍼들이 바로 가득차게 되고 큐에 삽입된다.

그리고 얼마 지나지 않아 모든 버퍼를 다 사용해 버리게 된다.

이런 상황에서 다음 버퍼를 가져다가 데이터를 넣게 되면 이미 큐에 들어 있는 버퍼의 데이터를 덮어쓰게 된다.

 

- 해결 방법 -

 

각 버퍼는 큐에 추가 될때 '사용중'으로 설정되고 나서 재생되며,

재생이 완료되고 나면 '사용중' 설정이 지워진다.

이점을 이용해서 currentBufferIndex 가 증가하면 enqueueCurrentBuffer() 함수는 다음 버퍼가 '사용중' 인지 확인한다.

결론적으로 다음 버퍼가 '사용중'이 아닌 상태가 될 때까지 기다리게 된다.

 

이를 PlayQueueData 구조체의 NSCondition 객체로 처리할 수 있다.

이 객체에 -wait를 호출하면 해당 객체에 -signal이 호출될 때까지 실행이 중단된다.

포스트 플레이 콜백 함수에서 -signal 을 호출해서 실행을 재개해 주면 된다는 것을 알 수 있다.

enqueueCurrentBuffer() 함수는 오디오 데이터 콜백에서 호출되는 것이므로 -signal이 호출될 때까지는,

더 이상 패킷을 받을 수 없다는 것을 의미한다.

 

콜 스택을 확인해 보면, 현재버퍼(!)가 재생될 때까지는 AudioFileStreamParseBytes()가 반환되지 않음을 알 수 있다.

즉, 현재 버퍼가 재생될 때까지 데이터를 읽고 재생시키는 전체 작업이 중단된다는 것을 의미한다.

 

이 startQueue 메소드는 큐가 시작되지 않았을 때에만 AudioQueueStart를 호출한다.

버퍼를 큐에 넣고 나서 큐를 시작시켜야 한다.

따라서 enqueueCurentBuffer()는 버퍼를 큐에 넣은 후 startQueue 메소드를 호출 한다.

 

(!)현재버퍼: 실제의미로는 현재버퍼가 아니고, 큐에 삽입된지 가장 오래된 버퍼.

즉, 이제 큐에서 풀로 돌아오게될 버퍼를 의미한다.

아직 재생되어 큐에서 나오지도 않은 버퍼가 현재 버퍼가 된 이유는 currentBufferIndex를 증가시켰기 때문이다.

 

void enqueueCurrentBuffer(SimpleStreamer *self)
{
    OSStatus err = noErr;
    
    @synchronized(self) {
        if ((self.queueStarted == YES) && (self.queueRunning == NO)) {
            // 큐가 중단된 경우 데이터를 큐에 삽입하지 않는다.
            NSLog(@"bailing out of enqueueCurrentBuffer");
            return;
        }
    }
    
    NSLog(@"Enqueueing %u bytes", (unsigned int)self.playQueueDataRecs[self.currentBufferIndex].bytesFilled);
    
    // 현재 버퍼를 '사용중' 으로 만든다.
    self.playQueueDataRecs[self.currentBufferIndex].inUse = YES;
    
    // 버퍼의 데이터 크기를 설정
    AudioQueueBufferRef fillBuf = self.playQueueDataRecs[self.currentBufferIndex].buffer;
    fillBuf->mAudioDataByteSize = (unsigned int)self.playQueueDataRecs[self.currentBufferIndex].bytesFilled;
    
    
    // 버퍼를 큐에 삽입
    err = AudioQueueEnqueueBuffer([self playQueue], fillBuf, self.playQueueDataRecs[self.currentBufferIndex].packetCount, self.playQueueDataRecs[self.currentBufferIndex].packetDescriptors);
    
    if (err) {
        //버퍼를 큐에 삽입하지 못함..
        NSLog(@"Could not enqueue buffer");
        return;
    }
    
    
    // 재생 큐가 실행되고 있지 않다면 실행시킨다.
    [self startQueue];
    
    // 다음 버퍼를 사용한다.
    self.currentBufferIndex++;
    if (self.currentBufferIndex >= kNumAQBufs) {
        self.currentBufferIndex = 0;
    }
    
    // 새로운 버퍼가 사용중이라면 해당 버퍼가 pool로 반환될 때까지 기다린다.
    NSCondition *queuedCondition = (NSCondition *)self.playQueueDataRecs[self.currentBufferIndex].queuedCondition;
    
    NSLog(@"Locking condition");
    
    [queuedCondition lock];
    
    @synchronized(self) {
        if (self.queueStarted && (!self.queueRunning)) {
            // 큐 실행이 멈춘 경우에는 버퍼를 기다리지 않는다.
            NSLog(@"failing out of enqueueCurrentBuffer (2)");
            return;
        }
    }
    
    while (self.playQueueDataRecs[self.currentBufferIndex].inUse) {
        NSLog(@"Waiting on in-use buffer");
        [queuedCondition wait];
    }
    
    [queuedCondition unlock];
}

 

10. startQueue 작성

 

오디오 큐가 실행 중이 아니라면 실행시킨다.

이 startQueue 메소드는 큐가 시작되지 않았을 때에만 AudioQueueStart를 호출한다.

버퍼를 큐에 넣고 나서 큐를 시작시켜야 한다.

따라서 enqueueCurentBuffer()는 버퍼를 큐에 넣은 후 startQueue 메소드를 호출한다.

 

- (void)startQueue
{
    if (!queueStarted) {
        AudioQueueStart(playQueue, NULL);
        @synchronized(self) {
            queueStarted = queueRunning = YES;
        }
    }
}

 

 

11. postPlayCallback 함수 작성

 

네트워크에서 오디오 데이터를 얻었고, 파싱하고 난 뒤 재생까지 했다.

이제 남은 일은 버퍼가 바닥나지 않게 확인해줘야 한다.

오디오 큐를 생성할 때 propertyListenerCallback() 으로 등록했던 postPlayCallback() 함수가 위 작업을 처리한다.

 

void postPlayCallback (void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef  inBuffer)
{
    SimpleStreamer *self = (__bridge SimpleStreamer *)aqData;
    PlayQueueData_t *currentBufferData = NULL;
    
    NSLog(@"Handling output buffer");
    
    // inBuffer에 해당하는 playQueueDataRecs 항목을 찾습니다.
    // playQueueDataRecs 에서 inBuffer 찾기
    for (int i=0; i<kNumAQBufs; i++) {
        if (self.playQueueDataRecs[i].buffer == inBuffer) {
            currentBufferData = &(self.playQueueDataRecs[i]);
            break;
        }
    }
    
    if (currentBufferData != NULL) {
        
        //버퍼를 사용 가능한 상태로 설정하여, 새로운 오디오 데이터를 담을 수 있게 한다.
        NSCondition *queuedCondition = (NSCondition *)currentBufferData->queuedCondition;
        NSLog(@"Locking condition");
        [queuedCondition lock];
        currentBufferData->inUse = NO;
        
        // 버퍼의 패킷과 바이트 count를 재설정
        currentBufferData->packetCount = currentBufferData->bytesFilled = 0;
        
        // enqueueCurrentBuffer가 대기중인 경우 조건에 신호를 보냅니다.
        // enqueueCurrentBuffer가 기다리는 경우가 있으므로, signal을 호출한다.
        
        NSLog(@"Signalling condition");
        
        [queuedCondition signal];
        [queuedCondition unlock];
    }
}

 

 

12. 정지 함수 및 NSURLConnection 델리게이트 함수

 

- (void)stop
{
    [networkConnection cancel];
    //[networkConnection release];
    AudioQueueStop(playQueue, true);
    AudioFileStreamClose(myAudioStream);
    AudioQueueDispose(playQueue, YES);
}


// NSURLConnection delegate method
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"Connection failed: %@", error);
}

// NSURLConnection delegate method
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Connection finished loading");
}

 

 

 

13.호출 해서 사용하기

 

 

ViewController.h

 

#import "SimpleStreamer.h"
#import <AudioToolbox/AudioToolbox.h>
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
{
    SimpleStreamer *streamer;
}


@property (strong, nonatomic) IBOutlet UIButton *play;


@end

 

 

ViewController.m

 

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

//https://file-examples.com/index.php/sample-audio-files/sample-mp3-download/
- (IBAction)play:(id)sender {
    
    NSString *urlString = @"https://transom.org/wp-content/uploads/2004/03/20040310.palin_.explosion1.mp3";

    //NSString *urlString = @"https://firebasestorage.googleapis.com/v0/b/fileuploadwebtest.appspot.com/o/b.mp3?alt=media&token=d8771880-d204-4816-ab42-33cc26b65ec2";
   
    NSURL *url = [NSURL URLWithString:urlString];
    if (streamer == nil) {
        streamer = [[SimpleStreamer alloc] initWithURL:url];
    }
    
    //스트리밍 객체의 생성과 재생 시작 사이에 오디오 스트리밍 파일 포멧 Hint 설정
    streamer.fileTypeHint = kAudioFileMP3Type;
    //streamer.fileTypeHint = 0;
    [streamer play];
}

@end

 

 

 

 

참고 : iPhone Advanced Projects 도서. 한빛미디어