iOS:制作簡易的 AAC 播放器 —— 了解音頻的播放流程
本文字?jǐn)?shù):1872字
預(yù)計(jì)閱讀時(shí)間:8分鐘
常用的播放文件,如 mp3、aac 都是已經(jīng)封裝的音頻格式,將它們的文件提供到系統(tǒng)音頻庫或者第三方音頻庫,如 AVPlayer、IJKPlayer 等這些框架和播放器,然后聲音就會(huì)由揚(yáng)聲器或耳機(jī)播放出來。如果讀者對(duì)這些神奇的過程有興趣,那就進(jìn)入本次的了解旅程。
《iOS 視音頻入門系列》
之前的都是針對(duì)圖像,而本次來聊聊音頻。
前言
本次將基于 iOS 實(shí)現(xiàn)對(duì)aac(ADTS)文件的播放來講解音頻的播放流程。它將包括如下內(nèi)容,可能大伙對(duì)這些名稱有所耳聞:解封裝、解編碼等。流媒體的播放其實(shí)就是基于這些步驟來實(shí)現(xiàn)的。
_______ ______________ ________
| | | | | |
| aac | demuxer | audio | decoder | audio |
| file | ---------> | encoded data | ---------> | raw |
|_______| |______________| |________|AAC
aac 的應(yīng)用很廣泛,在直播 rtmp 和 http-flv 中也使用的此音頻格式。
?AAC,全稱Advanced Audio Coding,是一種專為聲音數(shù)據(jù)設(shè)計(jì)的文件壓縮格式。與MP3不同,它采用了全新的算法進(jìn)行編碼,更加高效,具有更高的“性價(jià)比”。利用AAC格式,可使人感覺聲音質(zhì)量沒有明顯降低的前提下,更加小巧。
Demo 使用的 aac 音頻信息如下:
Input #0, aac, from 'video.aac':
Duration: 00:00:30.45, bitrate: 133 kb/s
Stream #0:0: Audio: aac (LC), 44100 Hz, stereo, fltp, 133 kb/s解封裝
解封裝的最重要的目的是將 音頻 的 音頻的信息 & 音頻的編碼數(shù)據(jù) 分離出來。
而 aac 是由多個(gè) header + es body 組成。header 固定 7 個(gè)字節(jié),所以只需每次讀取 7 個(gè)字節(jié),然后再讀出 size,就能得到得到對(duì)應(yīng)塊的數(shù)據(jù),從而得到 raw 數(shù)據(jù)。
// 讀取 header
int head_buf_size = 7;
int *head_buf = malloc(head_buf_size);
fread(head_buf, 1, head_buf_size, _in_file);
// 讀取 size
int s1 = ((int)(*(((uint8_t *)head_buf) + 3))&0x3) << 11;
int s2 = ((int)(*(((uint8_t *)head_buf) + 4))) << 3;
int s3 = (int)(*(((uint8_t *)head_buf) + 5)) >> 5;
int size = s1 + s2 + s3;
// 讀取 raw
int raw_buf_size = size - head_buf_size;
int *raw_buf = malloc(raw_buf_size);
fread(raw_buf, 1, raw_buf_size, _in_file);header 還包含其他音頻信息,它們是解碼時(shí)需要的,如采樣率、聲道數(shù)等
int head_buf_size = 7;
int *head_buf = malloc(head_buf_size);
fread(head_buf, 1, head_buf_size, file);
// 采樣率標(biāo)識(shí)
int freqIdx = ((int)(*(((uint8_t *)head_buf) + 2))&0x3C) >> 2;
// 聲道數(shù)
int c1 = ((int)(*(((uint8_t *)head_buf) + 2))&0x1) << 2;
int c2 = ((int)(*(((uint8_t *)head_buf) + 3))&0xC0) >> 6;
int chanCfg = c1 + c2;
// 返回
complete(freqIdx == 3 ? 48000 : 44100, chanCfg);上面的格式是 aac 的格式協(xié)議制定的,只要按照要求讀取即可,就是哪幾個(gè)字節(jié),哪幾個(gè)bit 對(duì)應(yīng)的哪些內(nèi)容,它的值表示什么都是約定的。
Demo 只讀出了 aac 的 raw、sample_rate、channels。
解碼
解碼的是使用 AudioConverterRef,可能有人問這里的細(xì)節(jié),但這里不會(huì)展開,因?yàn)橛悬c(diǎn)超綱,大伙可以搜搜 AAC解碼原理 之類的標(biāo)題(PS:別問,問就是不廢??)。
而使用 AudioConverterRef 就可以輕松的完成解碼,只需配置輸入&輸出的 AudioStreamBasicDescription,然后將讀取的 raw 寫入即可得到解碼后的數(shù)據(jù)。
// 輸入
- (AudioStreamBasicDescription)createAACAduioDes {
UInt32 channels = _channels;
AudioStreamBasicDescription audioDes ={0};
audioDes.mSampleRate = _sampleRate;
audioDes.mFormatID = kAudioFormatMPEG4AAC;
audioDes.mFormatFlags = kMPEG4Object_AAC_LC;
audioDes.mBytesPerPacket = 0;
audioDes.mFramesPerPacket = 1024;
audioDes.mBytesPerFrame = 0;
audioDes.mChannelsPerFrame = channels;
audioDes.mBitsPerChannel = 0;
audioDes.mReserved = 0;
return audioDes;
}
// 輸出
- (AudioStreamBasicDescription)createPCMAduioDes {
UInt32 bytesPerSample = sizeof(SInt32);
UInt32 channels = _channels;
AudioStreamBasicDescription audioDes ={0};
audioDes.mSampleRate = _sampleRate;
audioDes.mFormatID = kAudioFormatLinearPCM;
audioDes.mFormatFlags = kLinearPCMFormatFlagIsNonInterleaved | kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
audioDes.mBytesPerPacket = bytesPerSample;
audioDes.mFramesPerPacket = 1;
audioDes.mBytesPerFrame = bytesPerSample;
audioDes.mChannelsPerFrame = channels;
audioDes.mBitsPerChannel = 8 * bytesPerSample;
audioDes.mReserved = 0;
return audioDes;
}// decode
AudioConverterFillComplexBuffer(self->_audioConverter,
inputDataProc,/*輸入數(shù)據(jù)的函數(shù)*/
(__bridge void * _Nullable)(self),
&ioOutputDataPacketSize,
outAudioBufferList,/*構(gòu)造返回的數(shù)據(jù)*/
NULL);以上代碼的過程就是將上一步解封裝后的 data 轉(zhuǎn)成 AudioBufferList,再通過 AudioConverter 解碼,從輸出的 AudioBufferList 獲取 pcm 數(shù)據(jù)。
播放
播放使用AudioUnit & AUGraph,這是 iOS 底層的音頻框架,通過它還可以進(jìn)行很多有趣的自定義,如變音。對(duì)于 AudioUnit 的學(xué)習(xí)內(nèi)容就很多,可以自行搜索學(xué)習(xí)即可。此處播放,只需按 Demo 操作固定步驟即可。
OSStatus status;
status = NewAUGraph(&_auGraph);
[self addAUNode];
status = AUGraphOpen(_auGraph);
[self getAUsFromNodes];
[self setAUProperties];
[self makeAUConnects];
CAShow(_auGraph);
status = AUGraphInitialize(_auGraph);到這一步,只要把 encode raw 給到 AudioUnit 就可以播放。此音頻播放器屬于消費(fèi)者模式,它是反向跟前面要數(shù)據(jù)來播放,如下:
___________ ______________
| | 1.playback | |
| demuxer | <--------- | AudioUnit |
| decoder | ---------> | |
|___________| 2.pcm |______________| AudioUnit 向 decoder 要 pcm 數(shù)據(jù) decoder 將解碼數(shù)據(jù)給過去,數(shù)據(jù)夠直接提供,數(shù)據(jù)不夠向 demuxer 要 demuxer 去解封裝更多源數(shù)據(jù),返回給 decoder 解碼
// 數(shù)據(jù)不夠
if (self.data.length < size) {
// 解封裝
NSData *d = [_reader read_aac_raw_buf];
if (d == nil) {
return nil;
}
// 解碼
AudioBufferList *b = [_decoder decodeAudioSamepleBuffer:d];
// 保存
[self.data appendBytes:b->mBuffers[0].mData length:b->mBuffers[0].mDataByteSize];
}
// 返回 AudioUint 需要的數(shù)據(jù)
NSData *b = [self.data subdataWithRange:NSMakeRange(0, size)];這樣處理可以避免一次性將 aac 解碼,封裝的目的就是降低文件大小,方便傳輸和保存。如果全部解碼出來,對(duì)于高音質(zhì)的音頻文件就會(huì)生成很大的文件,占用大量的內(nèi)存,并且播放時(shí),快進(jìn)或者直接結(jié)束,沒有播放的部分即浪費(fèi)空間又浪費(fèi)算力。因此需要播放的部分才去解封裝 & 解碼更好。
?這里的 video.aac 文件大概是 500kb,解碼后的 pcm 是 5.5M
播放器 Demo
QHAudioConverterMan: acc 播放器 https://gitee.com/chenqihui/qhaudio-converter-man
Demo 代碼只供參考,暫沒優(yōu)化。
轉(zhuǎn)碼
?訊飛語音能力一般情況下支持如下音頻格式:
pcm(pcm_s16le),wav,speex(speex-wb) 采樣率為16000 或者 8000. 推薦使用16000,比特率為16bit 單聲道
而這里可以使用 AudioConverterRef 進(jìn)行轉(zhuǎn)碼,讓 pcm 符合要求
下面是 44100_32f_1 to 16000_16f_1 的波形圖:

雖然波形圖看著差不多,但其實(shí)還是有很大的不同。導(dǎo)致轉(zhuǎn)碼失真,應(yīng)該是某些參數(shù)設(shè)置有誤差或者本人寫的讀取測(cè)試代碼有誤。由于這里只是順便寫的轉(zhuǎn)碼代碼,暫時(shí)忽略它。
而使用 ffmpeg 命令的轉(zhuǎn)碼,如下:

ffmpeg -ac 1 -ar 44100 -f f32le -i of111.pcm -ac 1 -ar 16000 -f s16le ff_out.pcm參考
音頻屬性相關(guān):聲道、采樣率、采樣位數(shù)、樣本格式、比特率https://www.cnblogs.com/yongdaimi/p/10722355.html#_label5
AAC ADTS格式分析
https://zhuanlan.zhihu.com/p/162998699 AAC ADTS格式分析 https://blog.csdn.net/tantion/article/details/82743942
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取