- 關(guān)于iOS平臺(tái)音頻的播放我們已經(jīng)簡(jiǎn)單了解過了棒旗,是了解過簡(jiǎn)單的了誓篱,上一篇章中,播放器需要的PCM的數(shù)據(jù)笼沥,那現(xiàn)在我們就開始講解音頻的解碼蚪燕,就是如何生成PCM數(shù)據(jù)招狸。
- 音頻解碼的過程是和視頻差不多的,
特別是打開文件和讀取流信息這一部分
邻薯,所以如果有重復(fù)的地方我就跳過了裙戏,不明白的小伙伴可以回頭看這篇文章。基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg視頻播放器(一)
基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(一)
基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(二)
基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(三)
正式開始
正式解碼前的準(zhǔn)備工作
-
AVFormatContext
是一個(gè)FFmpeg
解封裝用的結(jié)構(gòu)體厕诡,很多函數(shù)都用到它作為參數(shù)累榜。
+ (void)initialize
{
av_log_set_callback(FFLog);
av_register_all();
}
-
av_register_all()
這個(gè)是所有使用FFmpeg
庫(kù)的時(shí)候都需要調(diào)用的初始化,可以初始化各種組件和協(xié)議等灵嫌。
1.1 解封裝
- (BOOL)openInput:(NSString *)path
{
AVFormatContext * formatCtx = NULL;
formatCtx = avformat_alloc_context();
if (!formatCtx)
{
NSLog(@"打開文件失敗");
return NO;
}
if (avformat_open_input(&formatCtx, [path cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL) < 0)
{
if (formatCtx)
{
avformat_free_context(formatCtx);
}
NSLog(@"打開文件失敗");
return NO;
}
if (avformat_find_stream_info(formatCtx, NULL) < 0)
{
avformat_close_input(&formatCtx);
NSLog(@"無法獲取流信息");
return NO;
}
av_dump_format(formatCtx, 0, [path.lastPathComponent cStringUsingEncoding:NSUTF8StringEncoding], false);
_formatCtx = formatCtx;
return YES;
}
- 以上的代碼之前有解析過壹罚,所以就不重復(fù)解釋了。
1.2 找到音頻流
- (BOOL)findAudioStream {
_audioStream = -1;
for (NSInteger i = 0; i < _formatCtx->nb_streams; i++) {
if (AVMEDIA_TYPE_AUDIO == _formatCtx->streams[i]->codec->codec_type) {
if ([self openAudioStream: i])
break;
}
}
return true;
}
- 遍歷已經(jīng)初始化好的
AVFormatContext
中的流信息寿羞,找到解碼器媒體類型為AVMEDIA_TYPE_AUDIO
的流猖凛,準(zhǔn)備初始化解碼器。
1.3 初始化音頻解碼器相關(guān)
1.3.1 初始化音頻解碼器
AVCodecContext *codecCtx = _formatCtx->streams[audioStream]->codec;
SwrContext *swrContext = NULL;
AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
if(!codec)
return false;
if (avcodec_open2(codecCtx, codec, NULL) < 0)
return false;
- 以上的代碼之前也有解析過绪穆,所以就不重復(fù)解釋了辨泳。
1.3.2 判斷是否需要重采樣
static BOOL audioCodecIsSupported(AVCodecContext *audio)
{
if (audio->sample_fmt == AV_SAMPLE_FMT_S16) {
AieAudioManager * audioManager = [AieAudioManager audioManager];
return (int)audioManager.samplingRate == audio->sample_rate &&
audioManager.numOutputChannels == audio->channels;
}
return NO;
}
-
AV_SAMPLE_FMT_S16
之前有提到過,是播放器需要的一種格式玖院,所以如果解碼上下文中采樣格式不是AV_SAMPLE_FMT_S16
菠红,或者聲道數(shù)、采樣率和播放器不匹配难菌,那就需要重采樣试溯。 - 實(shí)際上還有一種方法,就是根據(jù)音頻的聲道數(shù)郊酒、采樣率去調(diào)節(jié)播放器的參數(shù)來播放音頻遇绞,這種方法我們就先不解釋了,以后有空再做深度的討論燎窘。
1.3.3 音頻重采樣
- 音頻重采樣算是數(shù)字信號(hào)處理中的一個(gè)單獨(dú)的模塊摹闽,拿出來講三天三夜都講不完,初學(xué)的小伙伴就理解成:
第一次采樣的采樣率荠耽、聲道數(shù)钩骇、采樣格式不符合我們要求,所以需要重新采樣
(掩飾了我自己也不懂的事實(shí))铝量。
if (!audioCodecIsSupported(codecCtx)) {
AieAudioManager * audioManager = [AieAudioManager audioManager];
swrContext = swr_alloc_set_opts(NULL,
av_get_default_channel_layout(audioManager.numOutputChannels),
AV_SAMPLE_FMT_S16,
audioManager.samplingRate,
av_get_default_channel_layout(codecCtx->channels),
codecCtx->sample_fmt,
codecCtx->sample_rate,
0,
NULL);
if (!swrContext ||
swr_init(swrContext)) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(codecCtx);
return false;
}
}
-
SwrContext
是重采樣相關(guān)的結(jié)構(gòu)體,但是這個(gè)結(jié)構(gòu)體FFmpeg
是沒有開放給我們的银亲,我們并不能看到它的參數(shù)慢叨,只能通過一系列的方法來操作它。 -
swr_alloc_set_opts()
這個(gè)方法就是設(shè)置SwrContext
參數(shù)的函數(shù)务蝠。
1. 第一個(gè)參數(shù):如果你想重新設(shè)置一個(gè)SwrContext
的參數(shù)拍谐,那就直接傳入這個(gè)SwrContext
,否則傳NULL
,它會(huì)重新生成一個(gè)新的SwrContext
轩拨,并作為返回值返回践瓷。
2. 第二個(gè)參數(shù):重采樣后的通道數(shù),但是這里需要使用av_get_default_channel_layout()
函數(shù)包裹一下亡蓉,獲取默認(rèn)的通道數(shù)晕翠,其實(shí)如果直接傳通道數(shù)也沒有太大的影響。
3. 第三個(gè)參數(shù):重采樣后的采樣格式砍濒,這里肯定是AV_SAMPLE_FMT_S16
淋肾,我們需要的就是這種數(shù)據(jù)。
4. 第四個(gè)參數(shù):重采樣后的采樣率爸邢。
5. 第五個(gè)參數(shù):重采樣前的通道數(shù)樊卓。
6. 第六個(gè)參數(shù):重采樣前的采樣格式。
7. 第七個(gè)參數(shù):重采樣前的采樣率杠河。
8. 最后兩個(gè)參數(shù)沒什么好解釋的碌尔,直接傳0
和NULL
就好了。 -
swr_init()
設(shè)置了參數(shù)之后券敌,才可以進(jìn)行SwrContext
的初始化七扰。
1.3.4 設(shè)置幀率等基本操作
_audioFrame = av_frame_alloc();
if (!_audioFrame) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(codecCtx);
return false;
}
_audioStream = audioStream;
_audioCodecCtx = codecCtx;
_swrContext = swrContext;
AVStream *st = _formatCtx->streams[_audioStream];
avStreamFPSTimeBase(st, 0.025, 0, &_audioTimeBase);
- 上面的模塊依然是和視頻解碼的部分是重復(fù)的,不懂的小伙伴可以去看視頻解碼的文章陪白。
2. 開始解碼
- 解碼過程之前也說了颈走,音視頻的全都一個(gè)樣,看之前的文章咱士。(突然覺得這篇文章全是廢話)立由。
- (NSArray *)decodeFrames:(CGFloat)minDuration
{
if (_audioStream == -1) {
return nil;
}
NSMutableArray * result = [NSMutableArray array];
AVPacket packet;
CGFloat decodedDuration = 0;
BOOL finished = NO;
while (!finished) {
if (av_read_frame(_formatCtx, &packet) < 0) {
NSLog(@"讀取Frame失敗");
break;
}
if (packet.stream_index == _audioStream) {
int pktSize = packet.size;
while (pktSize > 0) {
int gotframe = 0;
int len = avcodec_decode_audio4(_audioCodecCtx,
_audioFrame,
&gotframe,
&packet);
if (len < 0) {
break;
}
if (gotframe) {
AieAudioFrame * frame = [self handleAudioFrame];
frame.type = AieFrameTypeAudio;
if (frame) {
[result addObject:frame];
_position = frame.position;
decodedDuration += frame.duration;
NSLog(@"---當(dāng)前時(shí)間:%f, 持續(xù)時(shí)間: %f , 總時(shí)間:%f", _position, frame.duration, decodedDuration);
if (decodedDuration > minDuration)
finished = YES;
}
}
if (0 == len)
break;
pktSize -= len;
}
}
av_free_packet(&packet);
}
return result;
}
- 唯一不同的是音頻解碼調(diào)用的函數(shù)不同,是
avcodec_decode_audio4()
序厉。
3. 處理解碼后的數(shù)據(jù)
- 解碼后的數(shù)據(jù)還是和之前解碼結(jié)構(gòu)體中的類型是一樣的锐膜,重采樣就是在解碼后再進(jìn)行的,之前只是定義重采樣的結(jié)構(gòu)體而已弛房,并沒有進(jìn)行真正的重采樣道盏。
3.1 真正的重采樣
if (_swrContext) {
const NSUInteger ratio = MAX(1, audioManager.samplingRate / _audioCodecCtx->sample_rate) *
MAX(1, audioManager.numOutputChannels / _audioCodecCtx->channels) * 2;
const int bufSize = av_samples_get_buffer_size(NULL,
audioManager.numOutputChannels,
_audioFrame->nb_samples * ratio,
AV_SAMPLE_FMT_S16,
1);
if (!_swrBuffer || _swrBufferSize < bufSize) {
_swrBufferSize = bufSize;
_swrBuffer = realloc(_swrBuffer, _swrBufferSize);
}
Byte *outbuf[2] = { _swrBuffer, 0 };
numFrames = swr_convert(_swrContext,
outbuf,
_audioFrame->nb_samples * ratio,
(const uint8_t **)_audioFrame->data,
_audioFrame->nb_samples);
if (numFrames < 0) {
return nil;
}
audioData = _swrBuffer;
} else {
if (_audioCodecCtx->sample_fmt != AV_SAMPLE_FMT_S16) {
NSAssert(false, @"bucheck, audio format is invalid");
return nil;
}
audioData = _audioFrame->data[0];
numFrames = _audioFrame->nb_samples;
}
-
ratio
這個(gè)值的計(jì)算其實(shí)我也不是很清楚,我的理解是因?yàn)閕OS都是單聲道的原因文捶,所以最后需要乘以2荷逞。 -
av_samples_get_buffer_size()
計(jì)算給定音頻參數(shù)所需要的緩沖區(qū)大小。
1. 第一個(gè)參數(shù):本來應(yīng)該傳入一個(gè)linesize的指針(數(shù)組)粹排,但是解碼后的_audioFrame-> linesize的大小不一定準(zhǔn)確种远,所以需要重新計(jì)算,所以這里一般傳NULL
就好了顽耳。
2. 第二個(gè)參數(shù):通道數(shù)坠敷。
3. 第三個(gè)參數(shù):每一幀的采樣率妙同,我們平時(shí)所說的音頻采樣率(44100)指的是:每秒鐘采樣的次數(shù)。
4. 第四個(gè)參數(shù):采樣的格式膝迎,肯定是AV_SAMPLE_FMT_S16
粥帚。
5. 第五個(gè)參數(shù):是否對(duì)齊,0
是默認(rèn)對(duì)齊限次,1
是不對(duì)齊芒涡,其實(shí)這里傳哪個(gè)都差不多,沒什么關(guān)系掂恕。 -
_swrContext()
這個(gè)函數(shù)就是進(jìn)行重采樣的方法拖陆。
1. 第一個(gè)參數(shù):SwrContext
重采樣相關(guān)的結(jié)構(gòu)體,一定是要設(shè)置過參數(shù)的懊亡。
2. 第二個(gè)參數(shù):重采樣之后的數(shù)據(jù)依啰,作為參數(shù)的時(shí)候肯定是為空,重采樣結(jié)束之后店枣,里面就是重采樣之后的數(shù)據(jù)了速警。
3. 第三個(gè)參數(shù):重采樣之后每幀的采樣率。
4. 第四個(gè)參數(shù):重采樣之前的數(shù)據(jù)鸯两。
5. 第五個(gè)參數(shù):重采樣之前每幀的采樣率闷旧。
3.2 把數(shù)據(jù)轉(zhuǎn)化成模型
const NSUInteger numElements = numFrames * numChannels;
NSMutableData *data = [NSMutableData dataWithLength:numElements * sizeof(float)];
float scale = 1.0 / (float)INT16_MAX ;
vDSP_vflt16((SInt16 *)audioData, 1, data.mutableBytes, 1, numElements);
vDSP_vsmul(data.mutableBytes, 1, &scale, data.mutableBytes, 1, numElements);
AieAudioFrame *frame = [[AieAudioFrame alloc] init];
frame.position = av_frame_get_best_effort_timestamp(_audioFrame) * _audioTimeBase;
frame.duration = av_frame_get_pkt_duration(_audioFrame) * _audioTimeBase;
frame.samples = data;
- 這里還是使用了加速框架進(jìn)行傅里葉轉(zhuǎn)換,函數(shù)的作用還是和上篇文章說的一樣钧唐,但是播放之前轉(zhuǎn)化了一次忙灼,這里又轉(zhuǎn)化了一次,我也不是很清楚钝侠,知道的小伙伴可以評(píng)論討論下该园。
-
av_frame_get_best_effort_timestamp()
這個(gè)函數(shù)和視頻解碼的時(shí)候一樣,以流中的時(shí)間戳為基礎(chǔ)預(yù)估這一幀的時(shí)間戳帅韧,不過最后的結(jié)果是要乘以基時(shí)里初。 -
av_frame_get_pkt_duration()
是獲取這一幀數(shù)據(jù)持續(xù)的時(shí)間。
結(jié)尾
這一篇章忽舟,我自己還是有幾個(gè)不理解的地方双妨,希望有知道的大佬和我討論下,萬(wàn)分感謝叮阅。
- 由于放了FFmpeg庫(kù)刁品,所以Demo會(huì)很大,下載的時(shí)候比較費(fèi)時(shí)帘饶。
謝謝閱讀哑诊。