...

понедельник, 17 февраля 2014 г.

[Из песочницы] JavaScript: цикличные таймеры с автокоррекцией


В посте в повествовательной и не очень манере рассказывается о различных реализациях «точных» таймеров на JS. Материал рассчитан на новичков… Добро пожаловать под кат.


Как заметили многие, на картинке к посту изображены часы из работы Дали «Время течет», выбор отнюдь не случаен и метафоричен по своей сути. Ибо, в рамках программирования на JS, время может течь не совсем так, как мы это предполагаем. JS однопоточен по своей сути, что порождает очередь выполнения функций, а очередь подразумевает непременный порядок следования. И если некоторые из этапов вычислений оказываются излишне ресурсоёмкими, мы имеем явное расхождение требуемого с результатом исполнения. Особенно критично это в случаях небиблиотечного контролирования переходных процессов. К примеру: выполнения перехода по кубической кривой (easing), или работы с ритмичным вызовом логики приложения для обновления текущего состояния. Пару месяцев назад, в качестве «weekend project», я выбрал для себя написание простого пошагового секвенсера (wiki), и столкнулся с физической невозможностью точного тайминга на среднеслабых и слабых системах посредством стандартных setTimeout() и setInterval(). Рассогласование достигало непримиримых в этом случае полусекунд. В поисках решения, я наткнулся на отличную статью по этой теме. А сам пост, в некоем роде, — вольный перевод оной.


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



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'; }

document.title = elapsed;

var diff = (new Date().getTime() - start) - time;
window.setTimeout(instance, (100 - diff));
}

window.setTimeout(instance, 100);


Все довольно просто, посмотрим на результаты. Вот демо на JSfiddle с комментариями на русском языке, для сравнения работы обычных таймеров и таймеров с автокоррекцией.

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


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



//по нажатию на кнопку "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;
};

//вызываем следующую итерацию, с учетом задержки
window.setTimeout(instance, (step - diff));
};

//а это самый первый вызов функции instance(),
//после которого начинается последовательный вызов итераций
setTimeout(instance, step);
};


Кто-то уже наверняка задался вопросом: а как же быть с переполнением стека вызовов. В данном конкретном случае, его размер колеблется от 10 до 17 позиций, что мало для любого современного браузера. Однако с увеличением темпа, либо вместе с ростом количества перерасчетных итераций, может случится и приступ удушья у оного и необходимо будет задуматься о реализации .tail() — подобных вызовов. Но об этом уже совсем другая история.


Также нельзя не упомянуть про метод window.performance.now(), который возвращает число с плавающей запятой, значащее количество миллисекунд, прошедшее с загрузки страницы (не совсем точное определение) Следовательно после десятичной запятой у нас будет уже субмиллисекундное разрешение, что очень очень хорошо. Используя это значение, можно по схожему методу вычислить рассогласование с точностью до десятой доли миллисекунды, и более точно выполнить предзапуск последующей итерации.


Посмотреть секвенсер вживую можно здесь: stepograph.hol.es (webkit required)

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


Спасибо за внимание!


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий