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

2021-04-13 17:58:59 浏览数 (1)

环境

背景:在系统性学习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

0 人点赞