0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ちょっと息抜き。連打できる丈夫な楽器を作ろう(pyはおまけ)

Last updated at Posted at 2025-05-06

はじめに

React で「太⚪︎の達人」風のゲームを作る場合、Canvas を使用することで高速な描画が可能になり、太⚫︎の連打(高頻度の入力)にも対応できます。以下の手順で導入するとスムーズに実装できます。

↓この過去記事の続編です
https://qiita.com/Ryu-990/items/bc609c6e35270bad2bbc

Q. 最近流行りのuseOptimisticじゃダメなの?
useOptimistic は主に「サーバーとの同期を待たずに、即座に UI の状態を更新する」ためのフックですが、太⚫︎の達人のような連打を伴う高速な入力処理には向いていません。
いいね機能でハートマーク打つ位ならいいかもしれませんが(笑)。

理由
1. useOptimistic は「楽観的 UI 更新」に特化
 useOptimistic は、ネットワーク通信が発生するケース (API への更新リクエストなど) で、レスポンスを待たずに UI を更新するためのもの。
 しかし、太⚫︎の連打のようなリアルタイム入力には、そもそもサーバー通信の遅延がない前提のほうが望ましい。いつノルマクリアしたか分からなくなるでしょ?

API との同期を前提に useOptimistic を使うと、ネットワーク遅延が発生したときに UI のずれや整合性の問題が起こる可能性がある。

2. 高速な入力に対するパフォーマンス最適化が必要
useOptimistic は useState と似た挙動をするが、React の再レンダリングが発生するため、連打のたびにUI の更新が走るとパフォーマンスが低下する。

太⚫︎の達人のようなゲームでは、レンダリングを最小限に抑えることが重要になります。

早速やってみよう!

1. useRef で canvas を管理する

React の useRef を使って canvas 要素を取得 し、requestAnimationFrame を使って描画を行います。

実装の流れ
useRef で canvas を取得
useEffect で描画ループを開始
requestAnimationFrame を使ってアニメーションを実装
太⚫︎の入力処理を追加

2. 基本的な Canvas のセットアップ

TaikoGame.js
import React, { useRef, useEffect, useState } from "react";

const TaikoGame = () => {
  const canvasRef = useRef(null);
  const [notes, setNotes] = useState([]);
  const [score, setScore] = useState(0);

  // ノーツを更新する関数
  const updateNotes = () => {
    setNotes((prevNotes) =>
      prevNotes
        .map((note) => ({ ...note, x: note.x - 5 })) // ノーツを左に移動
        .filter((note) => note.x > -50) // 画面外のノーツを削除
    );
  };

  // キーボードまたはタップで入力
  const handleHit = () => {
    setScore((prev) => prev + 1);
  };

  // 描画関数
  const draw = (ctx) => {
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    // 太⚫︎を描画
    ctx.fillStyle = "brown";
    ctx.fillRect(50, 200, 100, 100);

    // ノーツを描画
    ctx.fillStyle = "red";
    notes.forEach((note) => {
      ctx.beginPath();
      ctx.arc(note.x, 250, 20, 0, Math.PI * 2);
      ctx.fill();
    });

    // スコア表示
    ctx.fillStyle = "black";
    ctx.font = "20px Arial";
    ctx.fillText(`Score: ${score}`, 10, 30);
  };

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext("2d");

    let animationFrameId;
    
    const render = () => {
      updateNotes();
      draw(ctx);
      animationFrameId = requestAnimationFrame(render);
    };

    render();

    return () => cancelAnimationFrame(animationFrameId);
  }, [notes, score]);

  // ノーツを追加する (デモ用)
  useEffect(() => {
    const interval = setInterval(() => {
      setNotes((prev) => [...prev, { x: 400 }]); // 右からノーツが出現
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return (
    <div
      tabIndex={0}
      onKeyDown={handleHit} // キーボードで反応
      onClick={handleHit} // クリックで反応
      style={{ textAlign: "center" }}
    >
      <h1>React ⚫︎の達人</h1>
      <canvas ref={canvasRef} width={400} height={400} style={{ border: "1px solid black" }} />
    </div>
  );
};

export default TaikoGame;

3. ポイント解説

useRef で canvas の描画を管理

canvasRef を useRef で取得し、useEffect で canvas に描画する。
requestAnimationFrame でスムーズな描画

requestAnimationFrame を使ってノーツの移動を滑らかにする。
ノーツの管理とスコアの更新

useState でノーツとスコアを管理し、useEffect で定期的にノーツを生成する。
入力の処理

onKeyDown(キーボード入力)や onClick(タップ入力)で太⚪︎を叩けるようにする。

4. さらに改良するには?

onPointerDown を追加すると、スマホのタッチ操作にも対応可能
音をつける (Audio API で ド⚫︎ や ⚫︎ッ の音を再生)
複数のノーツを用意して、「ド⚫︎」「⚫︎ッ」を判定

このように Canvas を活用することで、React でも太⚫︎の連打に耐えられる高速な描画が可能になります!

番外編ミッション:PythonでMP3ファイルから譜面のコードを作成せよ!

音楽ファイル(MP3)の音の強弱(音量)に基づいて譜面を自動的に生成するPythonスクリプトを作成するためには、音楽の波形を分析して、強弱を検出する必要があります。これを実現するために、pydub や librosa といったライブラリを使うことができます。
以下に、pydub と numpy を使って、音の強弱に基づいて簡単な譜面を生成する方法を紹介します。
これには、音量のピークを検出し、指定したタイミングで「ド⚫︎」や「⚫︎ッ」の譜面を作成するロジックが含まれています。
必要なライブラリ
まず、必要なライブラリをインストールします。

install
pip install pydub numpy

Pythonコード例

generateNotes.py
from pydub import AudioSegment
import numpy as np
import os

# MP3ファイルをロード
def load_audio(file_path):
    audio = AudioSegment.from_mp3(file_path)
    return audio

# 音量(dB)を計算
def calculate_volume(audio):
    # オーディオを短いチャンクに分割して音量を計算
    chunk_size = 100  # ミリ秒単位
    samples = []
    for i in range(0, len(audio), chunk_size):
        chunk = audio[i:i+chunk_size]
        rms = chunk.rms  # Root Mean Square 音量
        samples.append(rms)
    return samples

# 音の強弱に基づいて譜面を作成
def generate_notes(file_path):
    audio = load_audio(file_path)
    volumes = calculate_volume(audio)
    
    notes = []
    time_unit = 100  # ミリ秒単位(音量を測るための時間間隔)
    threshold_d*n = 10000  # ド⚫︎の音量閾値
    threshold_*a = 5000  # ⚫︎ッの音量閾値

    for i, volume in enumerate(volumes):
        time = i * time_unit
        if volume > threshold_d*n:
            notes.append({"time": time, "type": "d*n"})
        elif volume > threshold_*a:
            notes.append({"time": time, "type": "*a"})
    
    return notes

# 譜面を出力する
def export_notes(notes):
    notes_js = "export const notes = [\n"
    for note in notes:
        notes_js += f"  {{ time: {note['time']}, type: '{note['type']}' }},\n"
    notes_js += "];"
    return notes_js

# メイン処理
if __name__ == "__main__":
    mp3_file = "your_song.mp3"  # 解析したいMP3ファイルのパス
    if not os.path.exists(mp3_file):
        print(f"ファイル {mp3_file} が見つかりません")
        exit(1)

    notes = generate_notes(mp3_file)
    notes_js = export_notes(notes)
    print(notes_js)

処理の流れ
音楽の読み込み: pydub を使ってMP3ファイルを読み込みます
音量の計算: 音楽の各チャンク100ミリ秒ごとで音量を計算しますここでは音量のRMSRoot Mean Squareを使用しています
譜面の生成: 音量に基づいて音量が閾値ここでは d*n  *a の閾値を超えたタイミングに譜面を生成します
JS形式で出力: 譜面データを export const notes の形式で出力します
出力例
このスクリプトを実行すると次のようなJS形式の譜面データがコンソールに出力されます
export const notes = [
  { time: 100, type: 'd*n' },
  { time: 300, type: '*a' },
  { time: 500, type: 'd*n' },
  { time: 700, type: '*a' },
  // 他のノーツも続きます...
];

調整

音量の閾値 (threshold_d*n と threshold_*a): これらの値を調整することで、どの音量で「⚫︎ン」や「⚫︎ッ」を出すかを変更できます。大きい音量で「⚫︎ン」、小さい音量で「⚫︎ッ」といった感じです。
チャンクサイズ (chunk_size): 音量を計算する時間間隔を変更できます。
注意点
音楽の分析には限界があり、複雑なメロディやバックグラウンドノイズが多い場合は、完全に正確な譜面を作成するのは難しいです。そのため、音量に基づく簡易的なアプローチとして考えてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?