JavaScript: цикличные таймеры с автокоррекцией

Картинка к посту – это работа Сальвадора Дали «Время течет». Этот выбор отнюдь не случаен и метафоричен по своей сути.

В рамках программирования на JS время может течь не совсем так, как мы это предполагаем. JavaScript однопоточен. Это значит, что различные функции выполняются в определенной фиксированной последовательности.

Но некоторые этапы вычисления могут оказаться очень ресурсоёмкими и занять больше времени, чем требуется. Например, на 2-3 мс больше. И эта неточность постоянно накапливается. Особенно критично это в случаях контролирования переходных процессов. К примеру, выполнения перехода с изменением координаты во времени по кубической кривой (easing) или работы с ритмичным вызовом логики приложения для обновления текущего состояния.

Я сам столкнулся с физической невозможностью точного тайминга посредством стандартных IR.SetTimeout() и IR.SetInterval() пару месяцев назад, работая над небольшим проектом. Рассогласование достигало неприемлемых в этом случае 0,5 сек.

Джеймс Эдвардс, фрилансер веб-разработчик, специализирующийся на разработке JavaScript приложений, предлагает решить задачу «точного» тайминга путем вычитания задержки предыдущего выполнения функции из настоящего. Можно просто измерить разницу в системном времени между итерациями и вычесть её при следующем вызове. В результате Джеймс Эдвардс предложил следующий код для решения данной задачи:

var start = new Date().getTime(),

time = 0,

elapsed = ‘0.0’;

function instance()

{

time += 100;

elapsed = Math.floor(time / 100) / 10;

if(Math.round(elapsed) == elapsed) { elapsed += ‘.0’; }

var diff = (new Date().getTime() — start) — time;

IR.SetTimeout((100 — diff), instance);

}

IR.SetTimeout(100, instance,);

Это довольно простое, но хорошее решение. Преимущество такого подхода в том, что неважно насколько неточен таймер, так как впоследствии небольшая постоянная задержка (3-4 мс) может быть легко компенсирована. В то время как неточность простого таймера носит кумулятивный характер, накапливаясь с каждой итерацией, что в итоге приводит к очень заметной разнице.

Как было сказано выше, с проблемой неточных таймеров я столкнулся при написании проекта для работы с аудио. После глубокого изучения материалов по созданию корректных таймеров в JS и на основе кода, приведенного выше, был написан вот этот код:

//по нажатию на кнопку «play/stop», срабатывает функция включающая таймер

function preciousTimer (step) {

//как и в примерах выше, берем DateStamp для оценки

var start = new Date().getTime(),

time = 0,

/*а эта переменная появилась из необходимости

проводить в четное количество раз больше итераций,

чем шагов в секвенсоре (точность все еще довольно слабенькая)*/

it = 0;

function instance () {

//рассчитываем идеальное время

time += step;

//считаем разницу

var diff = (new Date().getTime()- start) — time;

//выполняем согласно значению итератора

if (it == 4) {

it = 0;

/*место для работы секвенсора с матрицей,

здесь смотрим значения логического массива для

каждого прохода по планке. */

if (m == 8) {

m = 0;

};

for (var i = 0; i < 4; i++) {

if (noteArr[i][m]) {

sound[i].play();

};

};

m++;

};

it++;

//если за время итерации была нажата кнопка паузы,

//выходим из хвостовой рекурсивной цепочки

if (pause) {

return;

};

//вызываем следующую итерацию, с учетом задержки

IR.SetTimeout((step — diff), instance);

};

//а это самый первый вызов функции instance(),

//после которого начинается последовательный вызов итераций

IR.SetTimeout(step, instance);

};

Написанный мной код позволяет воспроизводить музыку все время, без задержки. И это лишь один из вариантов использования подхода, предложенного Джеймсом Эдвардсом. На его основе вы можете самостоятельно написать точные таймеры с автокоррекцией для любых целей.

Оригинал статьи, частично используемой в посте, вы можете прочитать здесь: Сreating accurate timers in JavaScript

Илья Марков,
скрипт-программист iRidium mobile