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

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

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

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

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

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

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

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

:::

# 早速やってみよう！
### 1. useRef で canvas を管理する
React の **useRef を使って canvas 要素を取得** し、requestAnimationFrame を使って描画を行います。

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

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

```javascript: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 を使って、音の強弱に基づいて簡単な譜面を生成する方法を紹介します。
 これには、音量のピークを検出し、指定したタイミングで「ド⚫︎」や「⚫︎ッ」の譜面を作成するロジックが含まれています。
必要なライブラリ
まず、必要なライブラリをインストールします。

```sh:install
pip install pydub numpy
```

Pythonコード例

```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ミリ秒ごと）で音量を計算します。ここでは、音量のRMS（Root 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): 音量を計算する時間間隔を変更できます。
**注意点**
音楽の分析には限界があり、複雑なメロディやバックグラウンドノイズが多い場合は、完全に正確な譜面を作成するのは難しいです。そのため、音量に基づく簡易的なアプローチとして考えてください。