前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FFmpeg4.0+SDL2.0笔记06:Synching Audio

FFmpeg4.0+SDL2.0笔记06:Synching Audio

原创
作者头像
非一
修改2021-04-13 17:58:59
3980
修改2021-04-13 17:58:59
举报
文章被收录于专栏:非一非一

环境

背景:在系统性学习FFmpeg时,发现官方推荐教程还是15年的,不少接口已经弃用,大版本也升了一级,所以在这里记录下FFmpeg4.0+SDL2.0的学习过程。

win10,VS2019,FFmpeg4.3.2,SDL2.0.14

原文地址:http://dranger.com/ffmpeg/tutorial06.html

同步音频

上一章我们通过记录音频时间线的方法来同步视频,这次我们要采用相反的方法,即记录视频时间线来同步音频。后面还有音视频都向系统时间同步。

实现视频时钟

与音频时钟类似,这次我们来实现视频时钟,它记录当前视频的播放进度。

初步来看,视频时钟就是最近一帧视频的PTS,每渲染一帧视频就更新一次。但问题在于,从毫秒级别来看,两帧视频间隔是比较长的(比如40ms),而两帧音频间隔就比较短了(比如10ms),这就导致每次播音频时计算出的音视频时差可能是这样的:比视频快0ms,比视频快10ms,比视频快20ms,比视频快30ms,比视频快0ms,比视频快10ms...这肯定是不行的,我们需要的是一个稳定的值。

因此在计算音视频时差时必须要拿到视频时钟的动态值。动态值计算方法是:上一帧的PTS+(当前系统时间-上一帧播放时的系统时间),与计算音频时钟动态值的方法类似。

有了思路,我们可以写代码了。在VideoState里加上上一帧PTS:videoCurrentPts,上一帧播放时的系统时间:videoCurrentPtsTime,初始化并在渲染视频时更新它们。

代码语言:javascript
复制
typedef struct VideoState {
    ...
    double videoCurrentPts;
    uint64_t videoCurrentPtsTime;
}VideoState;

int openStreamComponent(VideoState* pVideoState, int streamIndex) {
    ...
    //初始化
	pVideoState->videoCurrentPtsTime = av_gettime();
}

void videoRefreshTimer(void* userdata) {
    ...
    //更新
	pVideoState->videoCurrentPts = pVideoPicture->pts;
	pVideoState->videoCurrentPtsTime = av_gettime();
}

然后是获取视频时钟动态值的方法

代码语言:javascript
复制
double getVideoClock(VideoState* pVideoState) {
    double delta;
    
    delta = (av_gettime() - pVideoState->videoCurrentPtsTime) / 1000000.0;
    return pVideoState->videoCurrentPts + delta;
}

区分时钟

有了视频时钟后,我们就可以同步音频了,但在这之前,有一个小问题,之前实现的视频同步代码怎么办?总不能让音视频互相同步吧,或者把之前的代码都注释掉?因此我们定义了一个方法getMasterClock,它会根据当前同步类型来区分是调用getVideoClock还是getAudioClock,亦或其他时钟。

代码语言:javascript
复制
enum {
    kAVSyncAudioMaster,
    kAVSyncVideoMaster,
    kAVSyncExternalMaster,
};

const int kAvSyncDefaultMaster = kAVSyncVideoMaster;

double getMasterClock(VideoState* pVideoState) {
	if (pVideoState->avSyncType==kAVSyncAudioMaster) {
		return getAudioClock(pVideoState);
	}
	else if (pVideoState->avSyncType == kAVSyncVideoMaster) {
		return getVideoClock(pVideoState);
	}
	else {
		return getExternalClock(pVideoState);
	}
}

同步音频

终于到同步音频的部分了。同步方法是根据音视频时钟的差值,计算出需要调整多少音频采样:如果音频比视频慢就丢掉部分采样来加速,如果快则增加一些采样来减速。

因此我们定义一个synchronizeAudio方法来专门处理音频同步。在增/减音频样本前,还要注意两点:

由于音频频率比视频高很多,我们不想每次都处理音频数据。因此synchronizeAudio会先统计缺失同步的次数,只有在连续20次都缺失同步后,这个方法才真正开始工作。是否缺失同步则通过音视频时差是否大于同步阈值来判断。

在计算音视频时差时,还需要做一点微小的调整。是这样的,虽然之前实现了视频时钟的动态值计算,音视频时差不会朝一个方向递增了,但还是会上下波动。可能第一次计算音视频之间差40ms,第二次差50ms,第三次又差35ms了,没有一次能完全准确的代表时差。如果取多个时差的平均值呢?也不行,我们期望的是最近一次时差的权重最大,然后依次递减,计算公式是: 新总时差 = 新时差+系数*旧总时差。公式里的系数能很好的帮我们降低前面时差的权重。

翻译成代码就是这样:

代码语言:javascript
复制
int synchronizeAudio(VideoState* pVideoState, int16_t* samples, int samplesSize, double pts)
{
    int bytesPerSamples;
    double refClock;

    bytesPerSamples = sizeof(int16_t) * pVideoState->pAudioCodecCtx->channels;

    if (pVideoState->avSyncType != kAVSyncAudioMaster) {
        double diff, avgDiff;
        int wantedSize, minSize, maxSize;

        refClock = getMasterClock(pVideoState);
        diff = getAudioClock(pVideoState) - refClock;
        if (fabs(diff) < kAVNoSyncThreshold) {
            //先积累差值
            pVideoState->audioDiffCum = diff + pVideoState->audioDiffCum * pVideoState->audioDiffAvgCoef;
            if (pVideoState->audioDiffAvgCount < kAudioDiffAvgNb) {
                ++pVideoState->audioDiffAvgCount;
            }
            else {
                //当连续累计到20次时才会尝试去同步
                avgDiff = pVideoState->audioDiffCum * (1.0 - pVideoState->audioDiffAvgCoef);
                //缩减或增加采样的代码
                ...
            }
        }
        else {
            //音视频时间差值过大
            pVideoState->audioDiffCum=0;
            pVideoState->audioDiffAvgCount=0;
        }
    }
    return samplesSize;
}        

然后再来计算该增/减多少采样

代码语言:javascript
复制
                if (fabs(avgDiff) >= 0.04) {
					//确保sync后的sample在一定范围内
					wantedSize = samplesSize + ((int)(diff * pVideoState->pAudioCodecCtx->sample_rate) * bytesPerSamples);
					minSize = samplesSize * (1 - kSampleCorrectionPercentMax);
					maxSize = samplesSize * (1 + kSampleCorrectionPercentMax);
					if (wantedSize > maxSize){
						wantedSize = maxSize;
					}
					else if (wantedSize < minSize) {
						wantedSize = minSize;
					}

计算某时差内的音频数据大小公式: 时差*采样率*频道数*单个样本字节数=该时差对应音频数据的字节数。在算出wanted_size后,还要将其调整到一个合理的范围,不然一次调整太多,会有大量噪音或跳跃过大。

调整音频数据

synchronizeAudio会返回一个samplesSize,代表应该将多少字节送入SDL的buffer播放,我们需要在最后修改它。

当wantedSize<samplesSize时,直接截断即可。但当wantedSize>samplesSize时,我们不能单单修改samplesSize,因为后面的buffer是空的,我们得填点什么进去。教程里推荐的做法是全部填入最后一个音频样本。

代码语言:javascript
复制
					if (wantedSize < samplesSize) {
						samplesSize = wantedSize;
					}
					if (wantedSize > samplesSize) {
						//用最后一个sample把所有多出来的空间填满
						uint8_t* lastSample, * ptr;
						int fillSize;
						
						fillSize = wantedSize - samplesSize;
						lastSample = (uint8_t*)samples + samplesSize - bytesPerSamples;
						ptr = lastSample + bytesPerSamples;
						while (fillSize >0) {
							memcpy(ptr, lastSample, bytesPerSamples);
							ptr += bytesPerSamples;
							fillSize -= bytesPerSamples;
						}
						samplesSize = wantedSize;
					}
				}

拿到调整后的音频数据,送入SDL的buffer即可。

代码语言:javascript
复制
void audioCallback(void* userdata, uint8_t* stream, int len) {
    ...

    while (len > 0) {
        if (pVideoState->audioBufIndex >= pVideoState->audioBufSize) {
            //所有音频数据已发出,从队列里再拿一份
            decodedAudioSize = audioDecodeFrame(pVideoState, pVideoState->audioBuf, sizeof(pVideoState->audioBuf), &pts);
            if (decodedAudioSize < 0) {
                //拿不到数据,静音播放
                pVideoState->audioBufSize = kSDLAudioSize;
                memset(pVideoState->audioBuf, 0, pVideoState->audioBufSize);
            }
            else {
                //同步音频数据
                decodedAudioSize = synchronizeAudio(pVideoState, (short*)pVideoState->audioBuf, decodedAudioSize, pts);
                pVideoState->audioBufSize = decodedAudioSize;
            }
            pVideoState->audioBufIndex = 0;
        }
    }
    ...
}

还有一件事别忘了,在之前视频同步的地方加上if判断,保证运行时只存在一种同步方式。

代码语言:javascript
复制
    if (pVideoState->avSyncType != kAVSyncVideoMaster) {
        refClock = getAudioClock(pVideoState);
        diff = pVideoPicture->pts - refClock;

        //cout << "diff " << diff << endl;
        //如果比音频慢的时间超过阈值,就立刻播放下一帧,相反情况则延迟两倍时间
        syncThreshold = delay > kAVSyncThreshold ? delay : kAVSyncThreshold;
        if (fabs(diff) < kAVNoSyncThreshold) {
            if (diff <= -syncThreshold) {
                delay = 0;
            }
            else if (diff >= syncThreshold) {
                delay *= 2;
            }
        }
    }

以上就是音频同步的全部内容了。从最后的效果来看,不太推荐音频同步,因为不管缩减还是增加采样都会打断声音的连续性,一定会被用户察觉,而视频同步只是缩短/增加两帧播放间隔,用户基本察觉不到。

代码:https://github.com/onlyandonly/ffmpeg_sdl_player

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 环境
  • 同步音频
  • 实现视频时钟
  • 区分时钟
  • 同步音频
    • 调整音频数据
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档