マガジン

音の遅れは何ミリ秒から気づくか:自分の耳で測る

遊びをつくる

執筆にClaudeを使用しています。

緑の目印がある時計と食材を並べ、ゆっくりと練習できる温かな台所。
記事のテーマをもとに制作したAI生成イラスト。

キーを押してから音が鳴るまでが何ミリ秒なら「遅い」と感じるのかは、実は一つの数字では答えられません。耳の個人差に加え、使うイヤホンやスピーカー、ブラウザ、OS、鳴らす音の種類でも変わります。他人の数字を借りるより、自分の環境で自分の耳の境目を測るほうが、ゲームを作るときには役に立ちます。 この記事は、その測り方の設計書です。キーを押す瞬間から音を鳴らすまでの間に、遅れを少しずつ足していきます。「あれ、遅れたかも」と感じた値を記録し、次に順番をランダムにして、その感覚が当て推量でないかを確かめます。結果は書きません。やり終えると、自分の「気づく境目」を表す数行の記録が手元に残ります。 測れるのは、環境がもともと持つ遅れの上に足した分です。この点が、読み方を決める最初の前提になります。

この記事の目次
  1. 測っているのは「足した遅れ」の境目
  2. 準備と手順
  3. 記録の取り方と結果の読み方
  4. 誤りやすい点とゲームへの戻し方
  5. 次の行動
  6. 参考資料

測っているのは「足した遅れ」の境目

キーを押してから耳に音が届くまでには、キーボードの検出、ブラウザのイベント処理、音の生成、OSの音声処理、出力機器までの経路があり、それぞれが少しずつ時間を使います。Bluetoothのイヤホンのような無線機器では、この経路が長くなる傾向があります。

今回のプログラムは、このうち「音を鳴らす時刻」だけを指定どおりに後ろへずらします。つまり、記録される「0 ms」は遅れがゼロという意味ではありません。自分の環境が元から持っている遅れだけの状態です。

この区別が大切なのは、結果を2通りに読めるからです。

  • 「足した遅れが○○ msを超えると気づく」は、この環境での境目として読めます。
  • 「人間は○○ msから気づく」とは読めません。環境が違えば元の遅れも違うためです。

ブラウザによっては、出力側の遅れの目安を返す値(Web Audio APIの outputLatency)が取れる場合があります。開発者ツールのコンソールで、後述のページを開いたあとに ctx.outputLatency と入力してみてください。値が返らない環境もあるので、取れたら記録欄に書き添える程度の扱いにします。

準備と手順

準備するもの

  • パソコンと物理キーボード(タッチ画面では入力側の条件が変わるので、別の実験として分けます)
  • いつも作品を遊ぶときのヘッドホン、イヤホン、スピーカーのうち、測りたいもの一つ
  • 静かな部屋、一定の音量
  • 記録用の紙か表計算ソフト
  • 下のHTMLを保存したファイル(例:delay.html)。ブラウザで開くだけで動き、外部ライブラリは使いません
html
<!doctype html>
<meta charset="utf-8">
<title>音の遅れテスト</title>
<p id="m"></p><p id="o"></p>
<script>
const ASC = false; // true: 昇順・遅れを表示 / false: ランダム・非表示
const steps = [0, 10, 20, 30, 50, 80, 120, 200];
const reps = ASC ? 1 : 3;
const trials = [];
const log = [];
let i = 0, ctx;
for (const s of steps) for (let k = 0; k < reps; k++) trials.push(s);
if (!ASC) for (let k = trials.length - 1; k > 0; k--) {
  const r = Math.floor(Math.random() * (k + 1));
  [trials[k], trials[r]] = [trials[r], trials[k]];
}
const m = document.getElementById("m");
const o = document.getElementById("o");
function beep(ms) {
  ctx = ctx || new AudioContext();
  const t = ctx.currentTime + ms / 1000;
  const osc = ctx.createOscillator();
  const g = ctx.createGain();
  osc.type = "triangle";
  osc.frequency.value = 440;
  g.gain.setValueAtTime(0.0001, t);
  g.gain.exponentialRampToValueAtTime(0.4, t + 0.005);
  g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
  osc.connect(g).connect(ctx.destination);
  osc.start(t);
  osc.stop(t + 0.2);
}
function show() {
  if (i < trials.length) {
    m.textContent = (i + 1) + " / " + trials.length + (ASC ? "  遅れ " + trials[i] + " ms" : "");
    return;
  }
  m.textContent = "終了";
  o.textContent = steps.map(s => { const r = log.filter(x => x.ms === s);
    return s + " ms: 遅れありと答えた " + r.filter(x => x.said).length + " / " + r.length; }).join("  |  ");
}
addEventListener("keydown", e => {
  if (e.repeat || i >= trials.length) return;
  if (e.code === "Space") { e.preventDefault(); beep(trials[i]); }
  else if (e.key === "b") beep(0);
  else if (e.key === "j" || e.key === "f") { log.push({ ms: trials[i], said: e.key === "j" }); i++; show(); }
});
show();
</script>

操作は3つだけです。

  • スペースキー:現在の試行の音を鳴らします。
  • Jキー:「遅れていた」と回答して次の試行へ進みます。
  • Fキー:「遅れていない」と回答して次の試行へ進みます。

Bキーは、遅れを足さない基準音を鳴らします。回答は記録されず、進行もしません。

手順1:上昇法で目安をつける

最初は ASC を true にして開きます。8段階(0、10、20、30、50、80、120、200 ms)が小さい順に1回ずつ並び、画面に現在の遅れが出ます。

  1. Bキーを5回ほど押して、基準音に耳を慣らします。最初の再生は準備の都合で挙動が違うことがあるため、練習として捨てます。
  2. 試行ごとにスペースキーを1回だけ押します。聞き直しはしません。
  3. 「遅れていた」と感じたらJ、感じなければFを押します。
  4. 最後まで進むと、段階ごとの回答が表示されます。最初にJを押した段階をメモします。

上昇法の長所は、境目のおよその位置が数分でつかめることです。短所は、遅れが増えていくと自分で知っているため、期待が回答に混ざることです。この段階の数字は、あくまで目安として扱います。

手順2:ランダム順の盲検で確かめる

次に ASC を false にして開き直します。各段階が3回ずつ、順番をシャッフルして並びます。画面には何問目かだけが出て、今の遅れは見えません。操作は手順1と同じです。

  • 迷ったときは、回答を保留せずどちらかに決めます。
  • 目を閉じるか画面から視線を外し、画面の変化を手がかりにしないようにします。
  • 24回の試行が終わったら、集計をそのまま記録します。

この方法は、上昇法で出た目安が「本当に聞き分けた結果」なのか「期待による思い込み」なのかを切り分けるためのものです。

記録の取り方と結果の読み方

記録は、次の表のように「段階ごとに何回中何回、遅れありと答えたか」で残します。表の上には、条件を一行ずつ書き添えておきます。

  • 日付と時刻
  • 機器(キーボードの種類、有線か無線か、イヤホンやスピーカーの種類)
  • ブラウザとOS
  • 音量の設定
  • 取れた場合は outputLatency の値
  • 0、試行数:3、「遅れあり」と答えた数:、割合:
  • 10、試行数:3、「遅れあり」と答えた数:、割合:
  • 20、試行数:3、「遅れあり」と答えた数:、割合:
  • 30、試行数:3、「遅れあり」と答えた数:、割合:
  • 50、試行数:3、「遅れあり」と答えた数:、割合:
  • 80、試行数:3、「遅れあり」と答えた数:、割合:
  • 120、試行数:3、「遅れあり」と答えた数:、割合:
  • 200、試行数:3、「遅れあり」と答えた数:、割合:

読み方は次の順に進めます。

  1. 0 msの行を最初に見ます。 遅れを足していないのに「あり」と答えた回数が、この実験での勘違いの量です。ここが多いなら、自分の判断基準が不安定ということです。他の段階の読み取りも慎重にします。
  2. 段階が上がるにつれて「あり」が増えているかを見ます。 増えていれば、境目を読み取れる状態です。どの段階でも半々に近ければ、その範囲では聞き分けられていないと読めます。
  3. 境目の候補を決めます。 「あり」が3回中2回以上になり、それより大きい段階でもその状態が続く、最小の段階を候補とします。80 msで多いのに120 msで少ないなど、順序が崩れる場合は、ばらつきが大きいと読みます。
  4. 境目の前後を細かく測り直します。 たとえば候補が30 msと50 msの間にあるなら、steps を [0, 35, 40, 45, 50] のように書き換え、reps を5〜10に増やして、もう一度ランダム順で測ります。3回では偶然の影響が大きいためです。
  5. 条件を変えて繰り返します。 有線と無線、スピーカーとイヤホンなど、環境を一つだけ変えて同じ手順をやり直し、境目が動くかを比べます。

上昇法の目安とランダム順の結果が近ければ、その値を自分の目安として使えます。大きくずれるなら、上昇法は期待に引きずられていた可能性が高く、ランダム順の結果のほうを信頼します。

誤りやすい点とゲームへの戻し方

起こりやすい誤りを挙げます。

  • 押し直して聞き比べる。 1試行で複数回鳴らすと、2回目以降は音のタイミングを予想して聞くことになります。1試行1回を守ります。
  • 疲れた状態で続ける。 24回を一気にやると集中が落ちます。途中で休むか、日を分けます。
  • 別の環境の結果を混ぜる。 イヤホンを替えたり、無線から有線に替えたりした記録は、別の表にします。
  • 0 msを「遅れなし」と信じる。 先に述べたとおり、環境の元の遅れが含まれています。
  • 音量や音色を変える。 立ち上がりの鋭い音は遅れが目立ちやすく、柔らかい音は目立ちにくいといった違いが出えます。比べるなら、音を固定してから環境を変えます。
  • 画面の変化と混同する。 画面を見ていると、視覚の変化と音のずれが判断に混ざります。音だけで判断します。

測り終えたら、結果をゲームの設計に結びつけます。

  • 自分の境目は、その環境での一つの目安にすぎません。他の人は違う可能性があるので、遊ぶ人の機器をそのまま再現できないという前提で、遅れの余裕を見込みます。
  • 押してから鳴る経路のうち、自分で減らせるのは次のような部分です。効果音は事前に読み込んでおく、音の仕組みはユーザーの最初の操作で起動しておく、再生のたびに新しい仕組みを作らない、などです。
  • 遅れの影響が大きいのは、打鍵と音が直結するリズムや演奏系です。物語を読み進めるゲームなどでは、許容できる幅が広いことが多いでしょう。作ろうとしているゲームがどちらに近いかを、判断の基準に加えます。

自分の境目をつかんだあとは、アソバースの公式作品で、押したときの手応えを確かめてみる方法もあります。「雨音のピアノ」はキーボード(A W S E D F T G Y H U J)で弾く作品で、「WAVE / WAVE」はパッドの演奏を録音できる作品です。どちらもログインなしで遊べます。遅れの感じ方は機器しだいなので、この実験と同じ機器で試して、自分の測定値とどう結びつくかは作品を開いて確かめてください。

次の行動

まず delay.html を作り、Bキーで耳を慣らしてから、ASC = true の上昇法で一回りしてみてください。そのあと ASC = false に変えて24回のランダム順を行い、0 msの行と境目の候補を表に残します。環境を一つ変えて同じ手順を繰り返すと、自分のゲームに必要な遅れの予算が見えてきます。

参考資料

  • MDN Web Docs「Web Audio API」
  • W3C「Web Audio API」仕様