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

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