かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript FPS と BPM でアニメーションさせたい。(requestAnimationFrame)

ゲームとか音楽プレイヤーを作っていて requestAnimationFrame でループ処理をしていたのですが、FPSBPM で処理を実行できるようにしたかったのでやってみてたメモ

requestAnimationFrame 基本のループ

let requestID;
function loop(now = 0) {
  // 処理
  requestID = requestAnimationFrame(loop);
}
// ループ処理の開始
requestID = requestAnimationFrame(loop);
// ループ処理を停止
function stopAnimation() {
  requestAnimationFrame(requestID)
}

FPS

FPS (Frame Per Second) は 1秒間のコマ数で表される
5FPS なら 1秒間に5コマ、50FPS なら 1秒間に50コマを表示する速度
これを JavaScript で実現するには、実行時間を 1秒をFTPで割った数で割った値の実数部分をコマ数とすれば良い。
JavaScript の 1秒は 1000ms で考えるので、次のような計算になる。

const frame = Math.floot( time / ( 1000 / fps ) );

たいてい毎秒 60 回ですが、一般的に多くのブラウザーでは W3C の勧告に従って、ディスプレイのリフレッシュレートに合わせて行われます。ただし、コールバックの確率は、バックグラウンドのタブや隠れた <iframe> では、パフォーマンス向上やバッテリー消費を減らすために低くなるでしょう。
cf. Window.requestAnimationFrame() - Web API | MDN

requestAnimationFrame はおおよそ 60f/s で動作するので同じフレームが何度も呼び出されないようにしてあげればOK

const fps = 30;
let count = 0;
let requestID;
let time = 0;
let preFrame;

function loop(now = 0) {
  let frame = Math.floor((now - time) / (1000 / fps));
  // fps のフレーム数に達したらもう一度 0 フレームから始める場合
  // frame は 0 から始まるので ftp - 1 まで
  if (frame > fps - 1) {
    time = now;
    count += 1;
    frame = 0;
  }
  // 開始直後 frame がマイナスになることがあるので 0 以上を条件にする
  if (frame >= 0 && preFrame !== frame) {
    // フレームごとの処理
    console.log(count, frame + 1);
  }
  
  preFrame = frame;
  requestID = requestAnimationFrame(loop);
}

!function() {
  time = performance.now();
  requestID = requestAnimationFrame(loop);
}();

BPM

BPM (Beats Per Minute) は 1分間に4分音符の拍数で表される
120 BPM なら 60秒間に 4分音符なら 120回、 8分音符なら4分音符2回分なので 120 * 2回、 2分音符なら4分音符の倍の長さなので 120 * 0.5回。 60秒 をこれらの数で割った値が、各音符の時間間隔になる。計算式にすると次のような感じ

const beat = 16; // 演奏する音符
const beatRate = 60 * 1000 / ( bpm * (beat / 4) );
const frame = Math.floor(time / beatRate);

フレーム計算ができれば後は FTP と同じ

const MINUTES = 60 * 1000; 
const BASE_BEAT = 4; // BPMが4分音符換算なので定数的に扱う
const bpm = 120;
const beat = 16; // 実際にカウントするベースになる音符
const beatRate = MINUTES / (bpm * beat / MINUTES);
let section = 1;
let requestID;
let time = 0;
let preBeatCount;

function loop(now = 0) {
  let beatCount = Math.floor((now - time) / beatRate);
  // beatCount (音符の数)が bpm (1章節) に入る数に達したら beatCount を 0 に戻す
  if (beatCount > beat - 1) {
    time = now;
    section += 1;
    beatCount = 0;
  }
  if (beatCount >= 0 && preBeatCount !== beatCount) {
    // beat 音符毎に実行する処理
    console.log(section, beatCount + 1);
  }
  preBeatCount = beatCount;
  requestID = requestAnimationFrame(loop);
}

!function() {
  time = performance.now();
  requestID = requestAnimationFrame(loop);
}();

SAMPLE

See the Pen requestAnimationFrame FTP / BPM by KIKIKI (@kikiki_kiki) on CodePen.

所感

多分超厳密なものではないと思うけど、 FPS / BPM を実現することが出来ました。
FPS / BPM は一定の間隔で処理をするものなので、テトリスとかを作る場合はレベルに応じてフレームが実行される間隔の時間が短くなる計算にすれば良さそう。

今回コレを作るにあたって BPM とか調べてて、テンポ (tempo) がイタリア語だってことを知りました。
そういえば、ドレミってイタリア語からだったので、日本の音楽は伝統的にイタリアの影響が強いのでしょうか。興味深いです。


[参考]

ピクサー流 創造するちから

ピクサー流 創造するちから