about 3 years ago

Core Audio 實作的方法上有兩種,分別是 Audio Queue 與 Audio Unit。

這兩種實作方式有什麼差別,該怎麼選擇呢?
Audio Queue 相較於 Audio Unit 操作上較簡易方便,但卻無法對聲音做特殊處理,
Audio Unit 在這一點可以針對聲音做混音或 EQ 等處理,
可以依照 Streaming Player 的需求來決定要使用哪一種實作。

Audio Queue

關於 Audio Queue 的介紹與架構可以先看 Apple 的官方說明
About Audio Queue

Get started !

1. Prepare Audio Queue

AudioQueueRef 用來當作表示 audio queue 物件本身
AudioStreamBasicDescription 用來敘述 audio queue 的播放格式

AudioStreamBasicDescription audioStreamDescription;
AudioQueueRef audioQueue;

AudioQueueNewOutput 是用來產生 Audio Queue output 的方法,
要帶的參數有剛剛建立的 AudioQueueRef 與 AudioStreamBasicDescription,
與 Audio Queue 要資料的 callback 與 runloop。

AudioQueueNewOutput(&audioStreamDescription, audioQueueOutputCallback, (__bridge void *)(self), CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &audioQueue);

加上一個隨時能知道 audio queue 狀態的 listener callback。

AudioQueueAddPropertyListener(audioQueue, kAudioQueueProperty_IsRunning, audioQueuePropertyListenerProc, (__bridge void *)(self));

若想要取得 audio queue 資訊,如音量。

AudioQueueGetParameter(audioQueue, kAudioQueueParam_Volume, &deviceVolume)

若想要對 audio queue 設定某些 property,如 codec。

AudioQueueSetProperty(audioQueue, kAudioQueueProperty_HardwareCodecPolicy, &val, sizeof(UInt32));

2. Prepare Audio Data

  • Parse - 將 data 轉成指定格式的 audio data

AudioFileStreamID 表示 stream parser。

AudioFileStreamID audioFileStreamID;

AudioFileStreamOpen 加上 file type 與 listener 跟 parse result 的 callback。

關於 file type 是設定 parser 需 parse 出來的格式,但是在文件內的敘述是 inFileTypeHint,
也就是說其實 parser 會自動判別所需要的格式,但若格式較特殊可以在這邊提示 parser 參照此 type。

AudioFileStreamOpen((__bridge void *)(self), audioFileStreamPropertyListenerProc, audioFileStreamPacketsProc, kAudioFileMP3Type, &audioFileStreamID);

把下載的 data parse 成 audio data

AudioFileStreamParseBytes(audioFileStreamID, (UInt32)[inData length], [inData bytes], 0);

listener callback 的資料會有 parser(inAudioFileStream)、檔案類型(inPropertyID),
可以把它存成 AudioStreamBasicDescription,用來確認之後 parsed data 是否相同格式或是某些計算。

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                           inClientData,
                                            AudioFileStreamID                inAudioFileStream,
                                            AudioFileStreamPropertyID        inPropertyID,
                                            AudioFileStreamPropertyFlags *    ioFlags);
                      
AudioStreamBasicDescription description;
UInt32 descriptionSize = sizeof(description);
AudioFileStreamGetProperty(inAudioFileStream, inPropertyID, &descriptionSize, &description);

parse 出的資料主要有 data length(inNumberBytes)、packet count(inNumberPackets)、
packet data(inInputData) 與 packet description(inPacketDescriptions)。

typedef void (*AudioFileStream_PacketsProc)(
                                            void *                           inClientData,
                                            UInt32                           inNumberBytes,
                                            UInt32                           inNumberPackets,
                                            const void *                  inInputData,
                                            AudioStreamPacketDescription *inPacketDescriptions);
  • Store - 將 parse 出的資料儲存下來

重要資訊有 packet data 與 packet description,我們使用 NSData 來儲存。

NSMutableData *audioData;
NSMutableData *packetDescData;

每次 parse 出來的 packet data append 到 NSData 內。

[audioData appendBytes:inBytes length:inLength];

再來是存 packet description data,以下是 AudioStreamPacketDescription 的 property,
mStartOffset 代表此 packet 的起始位置,mDataByteSize 則是代表此 packet 的大小。

struct  AudioStreamPacketDescription
{
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket;
    UInt32  mDataByteSize;
};

parse 出來的 packet description 內的 mStartOffset 與 mDataByteSize 關係如下圖

parse 出來包含的是一個區塊的 packet data 與 packet description data,
所以每次 packet description 的 mStartOffset 都是從 0 開始,
由於使用 NSData 將 packet data 儲存成一段連續的資料,為了知道每個 packet data 在 NSData 內的實際位置,
每個 packet description 的 mStartOffset 都應該從上次 audio data 存到的位置開始計算。

for (NSUInteger packetIndex = 0; packetIndex < inPacketsCount ; packetIndex ++) {
  inPacketDescriptions[packetIndex].mStartOffset += audioData.length;
}
[packetDescData appendBytes:inPacketDescriptions length:sizeof(AudioStreamPacketDescription) * inPacketsCount];

3. Enqueue Data

啟動之後 audioQueue 就會開始 callback 要資料。

AudioQueueStart(audioQueue, NULL);

enqueue data 需要的資料主要有三個。

1.enqueue 的 packet 數量。

可以透過之前存的 AudioStreamBasicDescription 來計算出一秒有幾個 packet。

- (double)_packetsPerSecond
{
   AudioStreamBasicDescription audioStreamDescription = [delegate usedAudioStreamBasicDescription];
  return audioStreamDescription.mSampleRate / audioStreamDescription.mFramesPerPacket;
}

這樣就能夠決定一次 enqueue 的音檔秒數,假設一次最多只想給 8 秒的 packet 數量。

size_t packetSize = (NSUInteger)([self _packetsPerSecond] * 8.0);
size_t packetCount;
for (packetCount = 0; packetCount < packetSize; packetCount ++) {
}

packetCount 就是這次 enqueue 的 packet 數量。

2.存 audio data 的指標的 array 的指標。

簡單來說就是需要一個雙星號的 array 指標來存 audio data 的指標位置。

const void **data = calloc(packetSize, sizeof(void *));

使用之前存的 packetDescData 當中的 mStartOffset 來算出 audio data 實際資料的位置。

AudioStreamPacketDescription* packetDescriptions = (AudioStreamPacketDescription* )packetDescData.bytes;
for (index = 0; index < packetSize; index ++) {
  data[index] = audioData.bytes + packetDescriptions[readPacketIndex].mStartOffset;
}

data 就是這次 enqueue 的 data 指標。

3.audio data 的 packet description data。

我們所存的 packetDescription 為了要判別每個 packet data 在 NSData 內的位置,
因此有更動過 mStartOffset,現在要還原這個 packet description data,
也就是重新設定 mStartOffset,讓這個 packet description data 是敘述這段 packet data。

size_t offset = 0;
AudioStreamPacketDescription *descs = calloc(packetSize, sizeof(AudioStreamPacketDescription));
for (index = 0; index < packetSize; index ++) {
            memcpy(&(descs[index]), &packetDescriptions[readPacketIndex], sizeof(AudioStreamPacketDescription));
            descs[index].mStartOffset = offset;
            offset += descs[index].mDataByteSize;
}

desc 就是敘述這次 enqueue 的 audio data 資料。


資料準備好之後,就可以開始建立 audioQueueBuffer,須先計算出 buffer 大小。

UInt32 totalSize = 0;

for (index = 0 ; index < packetCount ; index++) {
  totalSize += inPacketDescriptions[index].mDataByteSize;
}
AudioQueueBufferRef buffer;
AudioQueueAllocateBuffer(audioQueue, totalSize, &buffer);
buffer->mAudioDataByteSize = totalSize;

buffer 開好之後就將資料塞進去,然後 enqueue。

for (index = 0 ; index < packetCount ; index++) {
  memcpy(buffer->mAudioData + desc[index].mStartOffset, data[index], desc[index].mDataByteSize);
}
AudioQueueEnqueueBuffer(audioQueue, buffer, packetCount, desc);

後續

Audio Queue 雖然是較底層的 API,但如果深入去了解其實發現操作並不算太難,
上面是從線上取得音檔一直到播出第一秒的聲音為止的步驟,
這是我已經封裝好的 SKAudioQueue,可以給有興趣參考或是想直接使用的人。

← iOS : Facebook ComponentKit 試用 iOS : Audio Streaming ( Audio Unit ) →