macOS
的音頻模塊使用還是和 iOS
有細(xì)微差別的。
今天記錄是的是 使用 AudioQueue
配合 AudioFile
進行播放macOS 本地音頻文件
本文打倉庫代碼為: JBPlayLocalMusicFile.m
CoreAudio
作為Apple
音頻系統(tǒng)中音頻庫的集合,今天需要使用到的庫為:
-
AudioQueue
位于<AudioToolbox/AudioQueue.h>
, 作為輸出模塊,輸入音頻到系統(tǒng)默認(rèn)揚聲器 -
AudioFile
位于<AudioToolbox/AudioFile.h>
, 讀取本地音頻文件,然后將讀取的Buffer
塞入AudioQueue
的音頻隊列中,等待系統(tǒng)揚聲器寵幸。
讀取和播放的大致流程為:
1. 前置說明
全局設(shè)置兩個值分別為
//每個緩沖區(qū)0.5秒的數(shù)據(jù)量
#define kBufferDurationInSeconds 0.5
//分配三個緩沖區(qū)
#define kNumberBuffer 3
-
kBufferDurationInSeconds
代表AudioQueue
每個緩沖區(qū)存儲 0.5秒時間的數(shù)據(jù),最后我們代碼里面會通過計算轉(zhuǎn)換成0.5秒時間的數(shù)據(jù)量的 內(nèi)存控件 -
kNumberBuffer
代表AudioQueue
中緩沖區(qū)的數(shù)量,我們知道至少需要兩個緩沖區(qū),其中一個采集數(shù)據(jù)的緩沖區(qū)A, 另外一個是 回調(diào)函數(shù)的緩沖區(qū)B。 當(dāng)采集的數(shù)據(jù)達到0.5秒的內(nèi)存大小后,A緩沖區(qū)會將所有數(shù)據(jù)轉(zhuǎn)移到B緩沖區(qū),然后A繼續(xù)采集,B在回調(diào)函數(shù)中供用戶使用。但是考慮數(shù)據(jù)的連貫性,如果哪個環(huán)節(jié)出問題,會導(dǎo)致數(shù)據(jù)A緩沖區(qū)不能及時進行數(shù)據(jù)采集,所以需要一個備用的緩沖區(qū)C來進行應(yīng)急。
2. 輔助宏
播放demo只負(fù)責(zé)調(diào)通邏輯和了解學(xué)習(xí)API,并沒有處理錯誤情況,我們將所有的錯誤只做了log輸出,
#include <TargetConditionals.h>
//負(fù)責(zé)將 OSStatus 轉(zhuǎn)成 fourcc
#if TARGET_RT_BIG_ENDIAN
# define FourCC2Str(fourcc) (const char[]){*((char*)&fourcc), *(((char*)&fourcc)+1), *(((char*)&fourcc)+2), *(((char*)&fourcc)+3),0}
#else
# define FourCC2Str(fourcc) (const char[]){*(((char*)&fourcc)+3), *(((char*)&fourcc)+2), *(((char*)&fourcc)+1), *(((char*)&fourcc)+0),0}
#endif
#define printErr(logStr, status) \
if (status != noErr) {\
NSLog(@"==== 出現(xiàn)錯誤: %@ code: %d(%s)", logStr, (int)status, FourCC2Str(status));\
}
日志輸入大概這醬紫CoreAudioDemo[37280:4192311] ==== 出現(xiàn)錯誤: AudioFileGetProperty kAudioFilePropertyPacketSizeUpperBound code: -50()
3. 類初始化與全局變量
- (void)start;
start 為入口函數(shù)- (void)stop
stop 為結(jié)束函數(shù)
在初始化方法中監(jiān)聽 外部的 通知,進行 stop
的調(diào)用
- (instancetype)init {
self = [super init];
_aspds = NULL;
_isDone = FALSE;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(stop) name:JBStopNotification object:nil];
return self;
}
類聲明的變量
@interface JBPlayLocalMusicFile()
{
@public
//這些C的struct 并沒有 get set 方法,不能設(shè)置為 property, 所以直接設(shè)置為成員變量
AudioFileID _audioFile;
AudioQueueRef _mQueue;
AudioStreamPacketDescription *_aspds; //從文件中讀取的包秒數(shù),每次讀取后將其傳入 AudioQueue中,以便能正確解碼和播放
}
@property (nonatomic, assign) AudioStreamBasicDescription mASBD;
@property (nonatomic, assign) BOOL isDone; //是否播放完畢
@property (nonatomic, assign) UInt32 byteSizeInBuffer; //緩沖區(qū)應(yīng)有的字節(jié)數(shù)(0.5秒內(nèi))
@property (nonatomic, assign) UInt32 packetsNumInBuffer; // 緩沖區(qū)應(yīng)對應(yīng)的數(shù)據(jù)包數(shù)量(0.5秒內(nèi))
@property (nonatomic, assign) Float64 readOffsetOfPackets; //讀取了多少 packets
@end
4. 打開本地音頻文件
start
函數(shù)內(nèi)首先調(diào)用openAudioFile
, _audioFile
為聲明的 @public
的成員變量AudioFileID _audioFile;
這里我們使用AudioFileOpenURL
函數(shù) 只讀權(quán)限打開 flac
類型的音頻,并綁定到 _audioFile
的指針句柄中,以后我們操作 這個文件就通過這個全局成員變量進行控制。
//打開 音頻 文件
- (void)openAudioFile{
NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"G_E_M_ 鄧紫棋 - 句號" withExtension:@"flac"];
OSStatus status = AudioFileOpenURL((__bridge CFURLRef)audioURL, kAudioFileReadPermission, 0, &(_audioFile));
printErr(@"AudioFileOpenURL", status);
}
5. 獲取音頻文件里面的 基本流信息
在上一步中我們打開了 音頻文件,并獲取到了指向它的句柄,現(xiàn)在我們操控_audioFile
, 從文件中獲取 AudioStreamBasicDescription
的信息。AudioStreamBasicDescription
作為 Apple 描述音頻流的基本格式,里面的值的含義就不具體說明了,調(diào)用[JBHelper printASBD:asbd];
這個輔助函數(shù)能夠打印處 ASBD 的具體值信息。
值得說明的是,由于我們讀取的是 flac
格式,不是 PCM
這種線性的裸數(shù)據(jù), 所以某些值是空的以0表示,因為可能是VBR(可變比特率)等,需要我們在后面的步驟進行進一步提取。
在這個方法中中我們 將取出的 AudioStreamBasicDescription
保存到全局變量 self.mASBD
中。
- (void)getASBDInFile {
/***
mp3 flac 文件格式, 和PCM 有點差別
2023-07-21 14:58:34.893822+0800 CoreAudioDemo[3193:5836480] planar:false bitsPerchannel:0
2023-07-21 14:58:34.894105+0800 CoreAudioDemo[3193:5836480] flags:
kAppleLosslessFormatFlag_16BitSourceData
kAudioFormatFlagIsFloat
2023-07-21 14:58:34.894247+0800 CoreAudioDemo[3193:5836480]
ASBD:
mSampleRate = 44100
mFormatID = 1718378851(flac)
mFormatFlags = 1
mBytesPerPacket = 0
mFramesPerPacket = 4096
mBytesPerFrame = 0
mChannelsPerFrame = 2
mBitsPerChannel = 0
mReserved = 0
*/
AudioStreamBasicDescription asbd;
UInt32 asbdSize = sizeof(asbd);
OSStatus status = AudioFileGetProperty(_audioFile, kAudioFilePropertyDataFormat, &asbdSize, &asbd);
printErr(@"AudioFileOpenURL", status);
[JBHelper printASBD:asbd];
self.mASBD = asbd;
}
6. 初始化 AudioQueue
在獲取了文件中的 ASBD
后,我們就可以初始化 AudioQueue
了。
- 第一個參數(shù)為 我們在上一步獲取的
_mASBD
的引用 - 第二個參數(shù)為 回調(diào)函數(shù)的名稱,實際上
AudioQueue
會在合適的時候 自動對該函數(shù)進行回調(diào),這也是C
語言常用的回調(diào)模式,該回調(diào)我們會在 后面進行詳細(xì)介紹。 - 第三個參數(shù)為 第二參數(shù)回調(diào)函數(shù) 里面作為參數(shù)回傳回來的值,這樣我們可以在回調(diào)函數(shù)回調(diào)回來的時候訪問我們特定的類的實例,這里橋接成
void *
傳入 - 第四第五為 特定的
Runloop
模式的值,可以不指定 - 第六參數(shù),為預(yù)留參數(shù),只能傳
0
- 最后一個參數(shù)
_mQueue
傳入全局變量AudioQueueRef
的地址, 只有傳入上一級的地址,才能改變當(dāng)前的地址,二級指針的操作。
這樣我們把 作為 輸出 Output
模式的 AudioQueue
的回調(diào)函數(shù)綁定好了,并且將 AudioQueue
也初始化了,
//init audio queue by output param
OSStatus status = AudioQueueNewOutput(&_mASBD,
jbAudioQueueOutputCallback,
(__bridge void *)self,
NULL,
NULL,
0,
&_mQueue);
printErr(@"AudioQueueNewOutput", status);
7. 將文件中的 Encoder Magic Cookie 拷貝到 音頻隊列中
magic cookie
在CoreAudio
表示附加到音頻流中的元數(shù)據(jù)(metadata), 能夠為正常的解碼文件和流提供必要的信息。
某些音頻格式的文件,在播放過程中必須要有 Magic Cookie
的數(shù)據(jù)才能正常的解碼播放。
經(jīng)本地測試 mp3
文件讀取不出Magic Cookie
, 而flac
格式的文件能夠正常讀取出來。
所以我們這里 會嘗試讀取一次,讀取不到就 返回,如果能夠讀取到就直接傳入 AudioQueue
中去。
具體過程詳看代碼和注釋
// MP3獲取不到 magic cookie
- (void)setFileMetaDataToAudioQueue{
//先獲取長度
UInt32 cookieDataSize = 0;
UInt32 isWriteAble = 0;
//注意這里是AudioFileGetPropertyInfo, 獲取長度和是否可以寫
OSStatus status = AudioFileGetPropertyInfo(_audioFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, &isWriteAble);
//有些沒有 magic cookie ,所以不管
if (status != noErr) {
NSLog(@"magic cookie 不存在,忽略掉");
return;
}
if (cookieDataSize <= 0) {
NSLog(@"AudioFileGetPropertyInfo kAudioFilePropertyMagicCookieData get zero size data");
return;
}
//根據(jù)長度獲取對應(yīng)的magic data 的內(nèi)容
Byte *cookieData = malloc(cookieDataSize *sizeof(Byte));
//這里是AudioFileGetProperty
status = AudioFileGetProperty(_audioFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, cookieData);
printErr(@"AudioFileGetProperty kAudioFilePropertyMagicCookieData", status);
//將獲取的MagicCookie 設(shè)置到 AudioQueue 中
status = AudioQueueSetProperty(_mQueue, kAudioQueueProperty_MagicCookie, cookieData, cookieDataSize);
printErr(@"AudioQueueSetProperty kAudioQueueProperty_MagicCookie", status);
// malloc 后必須 free
free(cookieData);
}
8. 計算 每次讀取 需要的大小和包的數(shù)量
我們現(xiàn)在開始計算 每個緩沖區(qū)的大小和包的數(shù)量
這里我們在代碼中 為 兩個全局變量賦值
@property (nonatomic, assign) UInt32 byteSizeInBuffer; //緩沖區(qū)應(yīng)有的字節(jié)數(shù)(0.5秒內(nèi))
@property (nonatomic, assign) UInt32 packetsNumInBuffer; // 緩沖區(qū)應(yīng)對應(yīng)的數(shù)據(jù)包數(shù)量(0.5秒內(nèi))
了解了這個函數(shù)是為獲取上面兩個值后,就可以帶著目的的去看里面的函數(shù)了。
主要是考慮到 _mASBD.mBytesPerPacket
是否為 0 的兩種 case,所以代碼比較復(fù)雜,需要進行兩種判斷。
- (void)calculateSizeOfTime{
//獲取kBufferDurationInSeconds時間的包的數(shù)量
UInt32 totalNumerOfPackets = 0;
if (_mASBD.mFramesPerPacket > 0) {
//每次時間間隔內(nèi)需要收集的樣本數(shù)量
Float64 totalNumberOfSamples = _mASBD.mSampleRate * kBufferDurationInSeconds;
UInt32 totalNumberOfFrames = ceil(totalNumberOfSamples); //將數(shù)據(jù)向上取整
totalNumerOfPackets = totalNumberOfFrames / _mASBD.mFramesPerPacket;
} else {
// 如果mFramesPerPacket==0,則編解碼器在給定時間內(nèi)沒有可預(yù)測的數(shù)據(jù)包大小。
// 在這種情況下,我們將假設(shè)在給定持續(xù)時間內(nèi)最多有 1 個數(shù)據(jù)包來調(diào)整緩沖區(qū)大小
totalNumerOfPackets = 1;
}
UInt32 packetSizeUpperBound = 0;
UInt32 packetSizeUpperBoundSize = sizeof(packetSizeUpperBound);
// 獲取 計算出來的 理論上的 最大 package 大小。非讀取文件
OSStatus status = AudioFileGetProperty(_audioFile,
kAudioFilePropertyPacketSizeUpperBound,
&packetSizeUpperBoundSize,
&packetSizeUpperBound);
printErr(@"AudioFileGetProperty kAudioFilePropertyPacketSizeUpperBound", status);
if (_mASBD.mBytesPerPacket > 0) {
//設(shè)置具體值
self.byteSizeInBuffer = self.mASBD.mBytesPerPacket * totalNumerOfPackets;
} else {
// 獲取理論上最大值
self.byteSizeInBuffer = packetSizeUpperBound * totalNumerOfPackets;
}
//定義一個最大值,以避免 RAM 消耗過大
//并定義一個最小值,以確保我們有一個可以在播放時沒有問題的緩沖區(qū)。太小了會頻繁連續(xù)從文件讀取 IO 消耗比較大
const int maxBufferSize = 0x100000; // 128KB
const int minBufferSize = 0x4000; // 16KB
//調(diào)整成一個中間的適合的值
if(self.byteSizeInBuffer > maxBufferSize) {
self.byteSizeInBuffer = maxBufferSize;
} else if (self.byteSizeInBuffer < minBufferSize) {
self.byteSizeInBuffer = minBufferSize;
}
//調(diào)整后重新計算大小, 這樣可能多分配內(nèi)存,但至少不會內(nèi)存越界
self.packetsNumInBuffer = self.byteSizeInBuffer / packetSizeUpperBound;
}
9. 獲取額外的 AudioStreamPacketDescription 信息
由于我們是非 PCM 的數(shù)據(jù),所以獲取了上面的數(shù)據(jù)后,還不能知己開始數(shù)據(jù)采集,還需要 獲取 除了 AudioStreamBasicDescription
這種音頻流信息外,還需要獲取 Packet
的基本信息。_aspds
為 AudioStreamPacketDescription
的數(shù)組, 包含了 _packetsNumInBuffer
個元素
具體見代碼和注釋
/**
如果音頻基本流描述沒有告訴任何有關(guān)每個數(shù)據(jù)包的字節(jié)數(shù)或每個數(shù)據(jù)包的幀的信息,
那么我們就會遇到 VBR 編碼或通道大小不等的 CBR 的情況。
在任何這些情況下,我們都必須為額外的數(shù)據(jù)包描述分配緩沖區(qū),
這些描述將在處理音頻文件并將其數(shù)據(jù)包讀入緩沖區(qū)時填充。
*/
- (void)allocPacketArray {
BOOL isNeedASPD = _mASBD.mBytesPerPacket == 0 || _mASBD.mFramesPerPacket == 0;
if(isNeedASPD) {
//calloc能夠?qū)?開辟的內(nèi)存自動 設(shè)為0
_aspds = (AudioStreamPacketDescription *)calloc(sizeof(AudioStreamPacketDescription), _packetsNumInBuffer);
} else {
_aspds = NULL;
}
}
10. 開辟AudioQueue 隊列
在我們前面完成一些必要的數(shù)據(jù)計算和AudioQueue的配置,現(xiàn)在可以進行AudioQueue的內(nèi)存申請和配置了。
我們這里使用了kNumberBuffer
3個緩沖區(qū),并將其buffers
保存到這個數(shù)組里面,注意buffers
不使用引用計數(shù),所以在函數(shù)返回后并不會析構(gòu),所以這個使用的局部變量。
- 這里我們使用了
AudioQueueAllocateBuffer
來開辟了self.byteSizeInBuffer
這么多Byte
的內(nèi)存,并關(guān)聯(lián)到_mQueue
中,并對內(nèi)存里面的值進行合適的賦值。最終將開辟的內(nèi)存的首地址保存到buffers[i]
變量中 -
jbAudioQueueOutputCallback
這里開辟內(nèi)存后手動調(diào)用了一下這個回調(diào)函數(shù),是為了在回調(diào)中讀取一次0.5
秒的文件數(shù)據(jù),來進行填充我們剛剛開辟出來的內(nèi)存。 -
self.isDone
代表著,上一步回調(diào)中就把文件的數(shù)據(jù)讀完了,或者出錯了,直接break,然后再 上一級方法中 調(diào)用stop
函數(shù)。
三次循環(huán),代表著開辟出的三個緩沖隊列都按我們的要求,填充完畢。也意味著我們現(xiàn)在的內(nèi)存中包含了 0.5 * 3 = 1.5秒的數(shù)據(jù)。是時候在下一步調(diào)用 Audio start
方法,正式開始播放了.
- (void)allocAudioQueue {
AudioQueueBufferRef buffers[kNumberBuffer];
OSStatus status = noErr;
for(int i = 0 ; i< kNumberBuffer; i++) {
status = AudioQueueAllocateBuffer(_mQueue,
self.byteSizeInBuffer,
&buffers[i]);
printErr(@"AudioQueueAllocateBuffer", status);
//手動調(diào)用回調(diào),用音頻文件中的音頻數(shù)據(jù)填充緩沖區(qū)。
//后續(xù)調(diào)用AudioQueueStart后,會自動觸發(fā)回調(diào)進行調(diào)用
jbAudioQueueOutputCallback((__bridge void *)self, _mQueue, buffers[i]);
if (self.isDone) {
//回調(diào)函數(shù)中設(shè)置為true后,代表剩余時間小于1.5秒
break;
}
}
}
11. 正式播放
前面所有的都配置齊全后就可以直接啟動 音頻隊列了。這時候就可以在默認(rèn)揚聲器里面聽到我們的音樂聲音了。
啟動后 音頻隊列會先消耗上一步,我們手動塞入隊列緩沖區(qū)的樣本。
然后會在特定的時間回調(diào)調(diào)用 jbAudioQueueOutputCallback
這個函數(shù)。
status = AudioQueueStart(_mQueue, NULL);
printErr(@"AudioQueueStart", status);
12. 回調(diào)函數(shù)
我們前面所有的都講了,現(xiàn)在開始理一理回調(diào)函數(shù)里面究竟做了啥。
首先定義這個函數(shù)使用了C的Static 靜態(tài)函數(shù)。
-
inUserData
形參為我們在AudioQueueNewOutput
里面?zhèn)魅氲膮?shù),這個原封不動的回傳了回來 -
inAQ
對音頻隊列的引用, 我們不關(guān)心是3個隊里中的哪一個,系統(tǒng)會自動調(diào)度 -
inBuffer
我們將文件中的音頻數(shù)據(jù)讀取后,需要將數(shù)據(jù)寫入這個內(nèi)存區(qū)域,然后系統(tǒng)會自動解碼播放。
這里的主要流程為:
- 獲取
JBPlayLocalMusicFile
實例對象 -
AudioFileReadPacketData
從文件中讀取音頻數(shù)據(jù),并會填充_aspds
這個音頻格式。最后numberBytes
,numberPackets
這兩個本地變量的值會被改變成實際本地從文件里面讀取的長度和包數(shù)量 - 如果前面無誤的話,需要將上一步獲取的數(shù)據(jù),通過調(diào)用
AudioQueueEnqueueBuffer
填充到 音頻隊列的緩沖區(qū)中,音頻隊列會按照以前設(shè)置的音頻流格式,和本地包的特定的_aspds
數(shù)據(jù)進行綜合解碼和播放。
static void jbAudioQueueOutputCallback(void * inUserData,
AudioQueueRef inAQ, //對音頻隊列的引用
AudioQueueBufferRef inBuffer //需要填充的緩沖區(qū)播放數(shù)據(jù)的引用
) {
JBPlayLocalMusicFile *playClass = (__bridge JBPlayLocalMusicFile *)inUserData;
if (playClass->_isDone) {
return;
}
// 存在局部變量中后,read數(shù)據(jù)的時候會 自動更新讀取到的值
UInt32 numberBytes = playClass.byteSizeInBuffer;
UInt32 numberPackets = playClass.packetsNumInBuffer;
//讀取音頻包內(nèi)容,并在最后一個字段中將讀取到的數(shù)據(jù)填充到 inBuffer 中去
OSStatus status = AudioFileReadPacketData(playClass->_audioFile,
false,
&numberBytes,
playClass->_aspds,
playClass.readOffsetOfPackets,
&numberPackets,
inBuffer->mAudioData);
printErr(@"AudioFileReadPacketData", status);
if (numberBytes <= 0 || numberPackets <= 0) {
NSLog(@"數(shù)據(jù)讀取完畢");
playClass->_isDone = true;
dispatch_async(dispatch_get_main_queue(), ^{
[playClass stop];
});
return;
}
inBuffer->mAudioDataByteSize = numberBytes;
AudioQueueEnqueueBuffer(inAQ,
inBuffer,
(playClass->_aspds ? numberPackets : 0),
playClass->_aspds);
//消費完后,更新下次需要讀取的文件的位置
playClass.readOffsetOfPackets += numberPackets;
}
13. 停止音頻
意外停止和正常播完停止都需要調(diào)用這個函數(shù)。文章來源:http://www.zghlxwxcb.cn/news/detail-607045.html
-
AudioQueueStop
停止隊列 -
AudioQueueDispose
處理音頻隊列,這里會處置其中的資源和緩沖區(qū)等,包括里面會自動調(diào)用AudioQueueAllocateBuffer
對應(yīng)的析構(gòu)函數(shù)AudioQueueFreeBuffer
-
_aspds
如果有,就釋放 -
AudioFileClose
最后關(guān)閉文件。
- (void)stop {
OSStatus status = AudioQueueStop(_mQueue, true);
printErr(@"AudioQueueStop", status);
status = AudioQueueDispose(_mQueue, true);
printErr(@"AAudioQueueDispose", status);
if(_aspds) {
free(_aspds);
}
status = AudioFileClose(_audioFile);
printErr(@"AudioFileClose", status);
self.isDone = true;
NSLog(@"播放結(jié)束");
}
本文地址: https://blog.csdn.net/goldWave01/article/details/131834259 ??文章來源地址http://www.zghlxwxcb.cn/news/detail-607045.html
到了這里,關(guān)于macOS coreAudio 之 AudioQueue 播放本地音頻文件的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!