背景介绍
通常游戏帧率的微小浮动并不太会影响用户体验,人的视觉感受能力比较低。一般仅需要使用简单的休眠来控制帧率就满足需求。
但在音频播放时,这对定时器迎来巨大的挑战。人的耳朵能感受到的音乐质量通常很高,微小的频率震荡都可能引起不适。曲有误,周郎顾。说的就是这种情况。
更苛刻的是声卡接口的特殊性,对于以60帧采样的音频流,每一块长度为 1 / 60秒。如果播放频率大于60帧,那么由于播放过快,导致块之间形成空隙,听起来会有颤音。如果播放过慢,那么播放缓冲区将随着时间逐渐填满,并逐渐与原始音轨发生滞后差异,造成音画不同步,当缓冲区满,会发生一次强烈的断层。
基本思路
在定时器设计中,完全达到帧间隔保持一致是十分困难的,这完全取决于系统提供的关于时间的接口精度。
这些接口统共两类 第一类,计时器,记录两个时间点的间隔。 第二类,睡眠器,休眠一段时间。
使用这两类时间接口就可以以统计学方式,让一个线程以帧每秒的单位度量,平稳的保持到一个期望的频率。当总任务量小于运算效能时,这在理论上总是成立的。
第一个元件,2幂长度环形队列帧率统计 我们使用定长环形队列来存储每帧时间间隔,取平均值作为统计频率。通常以2的幂作为环形队列长度有很大优势,能使用位移运算提高速度,而64作为渲染频率60帧最接近的长度是首选。首先我们在设置期望频率时将全长缓存变为期望间隔。平均值结算发生在插入新值的地方,仅出队和入队一次加法和一次减法。 组件:c语言2幂采样平均帧率统计_登高思危-少言勿躁的博客-CSDN博客
第二个元件,生成回调的定时循环 我们将回调前的时间点距离上次定时器重置时间记为wt。将回调后的时间点距离上次重置,与上一帧做差,得出当前帧的业务耗时间隔ht,使用帧率统计反馈的统计帧率记为R,期望帧率记为P,则我们使用期望频率平方为基准的反比例函数得出修正帧率为 X=(P x P) / R 修正频率间隔即为xt = 1 / X 本帧休眠间隔即为st=ht - xt
期望频率P为常数,所以这是一个反比例函数。采用这种方式负反馈。在统计帧率越接近期望帧率时修正幅度越小,反之越大。这样能快速修正到期望值。
这样我们的定时器可以轻松恒定在期望帧率小幅震荡。突发性的某几帧异常也能被64长度缓冲区抹平。
1.最终使用案例
https://bitbucket.org/mm_longcheng/mm-core/src/dev/mm/src/core/mmFrameTimer.h
static void MyFrameTimerUpdate(void* obj, double interval)
{
printf("interval: %f\n", interval);
}
static void MyTestTimer(void)
{
int i;
struct mmFrameTimerCallback callback;
struct mmFrameScheduler fs;
mmFrameScheduler_Init(&fs);
mmFrameScheduler_SetFrequency(&fs, 60.0);
callback.obj = &fs;
callback.Update = &MyFrameTimerUpdate;
mmFrameScheduler_SetCallback(&fs, &callback);
for (i = 0; i < 60; i++)
{
mmFrameScheduler_Update(&fs);
// <other process>
mmFrameScheduler_Timewait(&fs);
}
mmFrameScheduler_Destroy(&fs);
}
2.前置数据结构组件
// 字符串
struct mmString;
// 秒表
struct mmClock;
// 帧率统计
struct mmFrameStats;
// 条件超时等待
mmTimewait_USecNearby();
// 最小堆定时任务组件
struct mmTimerHeap;
3.主要数据结构
// obj is mmFrameTimer.
// interval is msec interval.
typedef void(*mmFrameTimerUpdateFunc)(void* obj, double interval);
struct mmFrameTimerCallback
{
mmFrameTimerUpdateFunc Update;
// weak ref. user data for callback.
void* obj;
};
// we adjustment the update frame interval.
// 1. dynamic frequency modulation.
// 2. complement frame.
struct mmFrameTimer
{
struct mmString name;
struct mmClock clock;
struct mmFrameStats status;
struct mmFrameTimerCallback callback;
//
mmAtomic_t locker;
// flag for mmFrameTimerBackground_t. default is MM_FRAME_TIMER_BACKGROUND_RUNING.
mmUInt8_t background;
// flag for active, 1 is active, 0 is deactive, default is 1.
mmUInt8_t active;
//
double frequency_factor;
//
double frequency;
double frequency_dynamic;
double frequency_l;
double frequency_r;
double frame_interval_usec;
//
double wait_usec;
double frame_cost_usec;
};
// Single thread frame timer scheduler.
struct mmFrameScheduler
{
struct mmFrameTimer frame_timer;
struct mmTimerHeap timer_heap;
struct mmClock clock;
struct timeval next_delay;
struct timeval ntime;
struct timespec otime;
pthread_mutex_t signal_mutex;
pthread_cond_t signal_cond;
mmAtomic_t locker;
double update_cost_time;
// default is MM_FRAME_TIMER_MAX_IDLE_DEFAULT
double max_idle_usec;
double frame_cost_usec;
int sleep_usec;
// smooth if we not need timewait.
int smooth_mode;
mmULong_t nearby_time;
// mmFrameTimerMotionStatus_t, default is MM_FRAME_TIMER_MOTION_STATUS_FOREGROUND(0)
mmSInt8_t motion_status;
// mmThreadState_t, default is MM_TS_CLOSED(0)
mmSInt8_t state;
// user data.
void* u;
};
4.主要处理函数
MM_EXPORT_DLL void mmFrameTimer_Update(struct mmFrameTimer* p, double* update_cost_time)
{
if (1 == p->active)
{
double interval = 0;
double wait_usec = 0;
interval = (double)(mmClock_Microseconds(&p->clock));
wait_usec = (p->wait_usec - interval);
// wait the timer is timeout (-inf, 1000) usec.
// most of system msleep(1) not enough precision.
if (1000 >= wait_usec)
{
double wait_usec_mod = 0;
double current_interval = 0;
mmFrameStats_Update(&p->status);
(*(p->callback.Update))(p, (p->status.interval) / (double)(MM_USEC_PER_SEC));
// frequency_dynamic update. dynamic frequency modulation.
p->frequency_dynamic = (p->frequency * p->frequency) / p->status.average;
// frequency_dynamic = clamp(frequency_dynamic, frequency_l, frequency_r)
p->frequency_dynamic = p->frequency_dynamic > p->frequency_l ? p->frequency_dynamic : p->frequency_l;
p->frequency_dynamic = p->frequency_dynamic < p->frequency_r ? p->frequency_dynamic : p->frequency_r;
// frequency_dynamic_usec update.
p->frame_interval_usec = (1.0 / p->frequency_dynamic) * (double)(MM_USEC_PER_SEC);
current_interval = (double)(mmClock_Microseconds(&p->clock));
mmClock_Reset(&p->clock);
p->frame_cost_usec = current_interval - interval;
wait_usec_mod = fmod(wait_usec, p->frame_interval_usec);
p->wait_usec = p->frame_interval_usec - p->frame_cost_usec + wait_usec_mod;
(*update_cost_time) += p->frame_cost_usec;
}
}
else
{
p->wait_usec = p->frame_interval_usec;
}
}
由于系统休眠函数的精度问题,注意wait_usec可能为负数。所以需要使用以下步骤做时间补偿。
wait_usec_mod = fmod(wait_usec, p->frame_interval_usec);
p->wait_usec = p->frame_interval_usec - p->frame_cost_usec + wait_usec_mod;
|