マガジン

30分で作る音あてゲーム:3つの音から正解を選ぶ

遊びをつくる

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

増えすぎた部品を脇へよけ、一周できる小さなゲームのコースを完成させる手元。
記事のテーマをもとに制作したAI生成の写真風イメージ。

「お手本の音を聞いたあと、続けて鳴る3つの音から、同じ高さのものを当てる」。これだけの遊びが30分で形になるのか、というのが今回の問いです。結論から言うと、判定まで含めた動くコードは40行ほどで済みます。画像も音声ファイルも要りません。ブラウザ標準のWeb Audio APIが、数値を指定するだけで音を作ってくれるからです。 30分のうち、コードを書く時間は10分もかかりません。残りの時間は、正解と不正解の音をどれだけ離すかという難易度の調整に使います。聞き分けの遊びは、ここで面白さが決まります。 手順は次の流れです。

この記事の目次
  1. 何を聞き分けさせるかを先に決める
  2. 手順1:音を鳴らして出題と判定をつなぐ
  3. 手順2:難しさを自分の耳で決める
  4. 手順3:つまずきやすい点を潰して、公開前に確認する
  5. 次の行動
  6. 参考資料

何を聞き分けさせるかを先に決める

音あてゲームは、聞き分ける対象を一つに絞ると作りやすくなります。この記事では「高さ(周波数)」だけを対象にします。音色や長さは変えません。変数が一つなら、難しすぎるときも簡単すぎるときも、直す場所が迷わず決まります。

ルールは次のとおりです。

  • 「出題」を押すと、お手本の音が1つ鳴ります
  • 少し間を空けて、候補の音が3つ順に鳴ります
  • 3つのうち1つはお手本と完全に同じ高さで、残る2つは少しだけ高い音と低い音です
  • 同じ高さだと思う番号(1〜3)を押すと、正誤と連続正解数が出ます

「ずれの大きさ」を1つの数値(diff)にしておきます。ここだけ変えれば難易度が変わる設計です。

手順1:音を鳴らして出題と判定をつなぐ

ファイルを作る

テキストエディタで新しいファイルを作り、index.html という名前で保存します。下のコードをそのまま貼り付けてください。ブラウザで開けば動きます。サーバーも外部ライブラリも要りません。

html
<!doctype html>
<meta charset="utf-8">
<title>音あて</title>
<button id="go">出題 / もう一度聞く</button>
<button class="a">1</button><button class="a">2</button><button class="a">3</button>
<p id="msg">出題を押してください。</p>
<script>
let ctx, cand = [], ref = 0, streak = 0;
const diff = 0.06; // ずれの割合。小さいほど難しい
function tone(f, when) {
  const t = ctx.currentTime + when;
  const o = ctx.createOscillator(), g = ctx.createGain();
  o.frequency.value = f;
  g.gain.setValueAtTime(0.0001, t);
  g.gain.exponentialRampToValueAtTime(0.3, t + 0.02);
  g.gain.exponentialRampToValueAtTime(0.0001, t + 0.6);
  o.connect(g).connect(ctx.destination);
  o.start(t); o.stop(t + 0.65);
}
function ask() {
  ctx = ctx || new AudioContext();
  ctx.resume();
  if (!cand.length) {
    ref = 220 + Math.random() * 440;
    cand = [ref, ref * (1 + diff), ref / (1 + diff)]
      .sort(() => Math.random() - 0.5);
  }
  tone(ref, 0);
  cand.forEach((f, i) => tone(f, 1 + i * 0.9));
  msg.textContent = "お手本のあとに3つ鳴ります。同じ高さの番号を選んでください。";
}
go.onclick = ask;
document.querySelectorAll(".a").forEach((b, i) => b.onclick = () => {
  if (!cand.length) return;
  const ok = cand[i] === ref;
  streak = ok ? streak + 1 : 0;
  msg.textContent = (ok ? "正解" : "はずれ(正解は" + (cand.indexOf(ref) + 1) + "番)") + " 連続正解:" + streak;
  cand = [];
});
</script>

各部分の役割

tone が音を1つ鳴らす関数です。オシレーター(createOscillator)が波を作り、ゲイン(createGain)が音量を決めます。音量は0.02秒で立ち上げて0.6秒かけて減らします。この包みを付けないと、鳴り始めと終わりに「ぷちっ」というノイズが出やすくなります。

when は「今から何秒後に鳴らすか」を表します。ask の中の cand.forEach は、候補を0.9秒間隔で予約しています。タイマーを自分で組まなくても、音の開始時刻をブラウザの音声用の時計に予約すれば、ずれずに順番どおり鳴ります。

ask の if (!cand.length) は「まだ答えていない問題があるか」を見ています。答える前に出題ボタンをもう一度押すと、同じ問題を聞き直せます。答えると cand が空になるので、次に押したときは新しい問題が作られます。

ref * (1 + diff) と ref / (1 + diff) が、お手本より少し高い音と少し低い音です。高さの差は足し算ではなく割合で作ります。人の耳は音の高さを比で捉えるので、低い音でも高い音でも「同じくらいのずれ」に聞こえやすくなります。

最初に確かめること

ブラウザで開き、「出題」を押してください。音が鳴らないときは、ブラウザやスマートフォンの音量、サイレント設定を確認します。音の再生はボタン操作のあとにしか始められない仕組みになっています。コードの中で AudioContext を ask の中で作っているのは、そのためです。

手順2:難しさを自分の耳で決める

聞き分けの遊びは、簡単すぎると作業になり、難しすぎると運任せになります。ちょうどよい点は人によって違うので、自分で測って決めます。以下は結果を示すものではなく、読者が自分で行うための設計です。

測り方

  1. diff を 0.12、0.06、0.03、0.015 の4つから選び、1回につき1つの値で試します
  2. 各値で10問ずつ答え、正解数をメモします
  3. 同じ条件でもう1セット、時間を空けて試します(合計で各値20問)
  4. 表にまとめます。例として次の形です。
  • 0.12、1セット目の正解数:、2セット目の正解数:
  • 0.06、1セット目の正解数:、2セット目の正解数:
  • 0.03、1セット目の正解数:、2セット目の正解数:
  • 0.015、1セット目の正解数:、2セット目の正解数:

結果の読み方

  • すべての値でほぼ全問正解なら、まだ差が大きすぎます。さらに小さい値を試します
  • すべての値で当たったり外れたりが半々なら、diff ではなく別の原因を疑います。候補の音が1つずつしか聞けない点や、聞く環境の騒音が影響している可能性があります
  • 値を小さくするにつれて正解数が下がっていくなら、diff が難易度として働いています。遊び手に合わせた段階を作れます
  • 1セット目と2セット目で大きく違うなら、聞き方が安定していません。10問では少ないかもしれないので、問題数を増やします

目安として、半音(ピアノの隣り合う鍵盤の差)はおよそ6%の周波数差です。diff = 0.06 は半音ほどのずれにあたります。これより小さくすると、音楽の経験がある人でも迷う場面が増えます。聞き分けの得意・不得意には個人差が大きいので、自分の結果を基準に他人の難易度を決めないでください。

段階にする

測り終えたら、diff を固定値から選択式に変えます。たとえばボタンを3つ追加して、「やさしい=0.12」「ふつう=0.06」「むずかしい=0.02」のように切り替えます。値は自分の測定結果に合わせて決めてください。diff を const から let に変え、ボタンで書き換えれば実現できます。

手順3:つまずきやすい点を潰して、公開前に確認する

作っている途中で起こりやすい失敗

  • 音が鳴らない: AudioContext をページ読み込み時に作っていないか確認します。ボタンを押したあとに作ると、再生が許可されやすくなります
  • 音が重なって聞こえる: 出題を連打すると、前回の予約が残ったまま新しい音が重なります。連打を避ける工夫として、鳴っている間はボタンを無効にする方法があります
  • 答えの位置が偏る: 手順1のコードの sort(() => Math.random() - 0.5) は手軽ですが、並びの偏りが出ることが知られている書き方です。最小構成として使っていますが、気になるなら Fisher-Yates 法という並べ替えに置き換えます。偏りがあるかは、200問ほど出して正解位置の数を数えれば確かめられます
  • 正解が耳ではなく見た目でわかる: 候補を押すと音が鳴る仕様にすると、押した順で比べられてしまいます。このコードは自動再生のみにして、比べる手がかりを減らしています
  • 音が大きすぎる: ゲインの 0.3 は控えめな値ですが、ヘッドホンでは大きく感じることがあります。遊び手が調整できる音量スライダーを後から足すと親切です

音が聞こえない人への配慮

聞き分けの遊びは、音が出せない環境や聞こえにくい人には遊びにくくなります。余裕があれば、お手本と候補のそれぞれが鳴る間、画面に小さな光や番号の点滅を出しましょう。ただし点滅を高さの手がかりにしてしまうとゲームが成り立たないので、「いま何番が鳴っているか」だけを示す程度にとどめます。

アソバースに出す前の確認

アソバースへの投稿は、HTMLファイルの直接アップロードのほか、ZIPや公開済みURLにも対応しています。入力、表示の確認、公開の3段階です。今回のように1ファイルで完結する作品なら、index.html をそのままアップロードできます。次の点を確認してください。

  • ファイル名が index.html で、ZIPにする場合は直下に置かれていること
  • 外部のサーバーや外部ライブラリを使っていないこと(今回のコードは使っていません)
  • 公開にはメールアドレスの確認が必要なこと
  • 表示の確認の画面で、ボタンを押したときに実際に音が鳴ること。作品は隔離された枠(sandbox付きのiframe)の中で動くため、公開前のこの確認を省かないでください
  • 記録(連続正解数)は画面を開いている間だけ残る設計にしておくと、保存まわりの心配が減ります

投稿画面の「紹介文と遊び方を作成」を使うと、作品を読み取って説明文の下書きが作られます。「この文章を使う」を押したときだけ反映される仕組みなので、遊び方(「お手本のあとに鳴る3つの音から、同じ高さを選ぶ」)が合っているか、読んで直してから使ってください。

次の行動

まず上のコードを index.html として保存し、ブラウザで10問遊んでみてください。そのあと diff を3段階に変えて、正解数を表に書き込み、自分にとってちょうどよい難しさを決めます。その値を「ふつう」にして、アソバースの投稿画面で表示の確認まで進めば、30分の試作は完成です。

参考資料

  • MDN Web Docs「Web Audio API」