Zenn
⏲️

ReactでWebで使えるメトロノームを作ってみた

2025/04/11に公開

はじめに

Webで使えるメトロノームをNext.jsを使用して作成しました。
その工程などを書いていこうと思います。
作ったサイトはこちらです。
https://web-metronome.vercel.app/

環境

  • Next.js v15.2.4
  • React v19
  • TypeScript v5
  • Vercel

ライブラリ

  • TailwindCSS v4
  • zustand v5.0.3
  • Lucide React v0.487

作るメトロノームの特徴

今回作るメトロノームの特徴は以下です。

  • 拍数やクリック音を変更できる
  • テンポの倍率を変更できる
  • サイトを閉じても状態が保持される

実装するために行ったこと

サイトを離れてもデータが保存されるようにする

今回は、状態管理ライブラリ 「zustand」 を使用して、グローバルに使用できるStateを作成しました。
zustandの機能として、StateをlocalStorageに保存されるようにできるというものがあります。
それを利用し、BPMなどのデータをサイトが離れても保存されるようにしました。
二つに分けてStateを作成し、BPMなど一部のデータをlocalStorageに保存されるようにしています。

src/stores/metronomeSettings.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type BeatType = "None" | "Normal" | "Strong"

interface MetronomeSettingsState {
    bpm: number
    setBpm: (bpm: number) => void
    beatTypes: BeatType[]
    setBeatTypes: (beatType: BeatType[]) => void
    beat: number
    setBeat: (beat: number) => void
}

export const useMetronomeSettings = create<MetronomeSettingsState>()(persist(
    (set) => ({
        bpm: 100,
        setBpm: (bpm) => set(() => ({ bpm })),
        beatTypes: ["Normal", "Normal", "Normal", "Normal"],
        setBeatTypes: (beatTypes) => set(() => ({ beatTypes })),
        beat: 4,
        setBeat: (beat) => set(() => ({ beat }))
    }),
    {
        name: "metronome-settings"
    }
))
src/stores/metronomeStatus.ts
import { create } from 'zustand'

interface MetronomeState {
    isPlaying: boolean,
    setIsPlaying: (isPlaying: boolean) => void,
    currentBeatCount: number
    setCurrentBeatCount: (beatCount: number) => void
}

export const useMetronomeStatus = create<MetronomeState>((set) => ({
    isPlaying: false,
    setIsPlaying: (isPlaying) => set(() => ({ isPlaying })),
    currentBeatCount: 0,
    setCurrentBeatCount: (beatCount) => set(() => ({ currentBeatCount: beatCount })),
}))

ビート数、クリック音、テンポ倍率などの実装

beatTypes で拍数やクリック音などを管理しています。
また、今回は一定間隔に音を鳴らす機構として、setIntervalを使用しました。
setIntervalは遅延が少しあるようで、メトロノームには不向きかもしれませんが、今回は妥協して使用しました。
別のいい方法を知っている方は教えてください〜
また、音を鳴らすために、今回はHTMLAudioElementを使用しました。
しかし、play()メソッドは、クリック音などを連続で鳴らす時、音が再生中だとplay()を実行しても最初から再生されることはありません。
なので、実行毎にcurrentTimeを0にすることで解決しています。

src/hooks/useMetronome.ts
export const useMetronome = () => {
    const {
        isPlaying,
        currentBeatCount,
        setCurrentBeatCount,
    } = useMetronomeStatus()
    const {
        beatTypes,
        bpm,
        beat
    } = useMetronomeSettings()
    const beatCountRef = useRef(currentBeatCount)
    const strongClickRef = useRef<HTMLAudioElement>(null)
    const normalClickRef = useRef<HTMLAudioElement>(null)

    useEffect(() => {
        if (!isPlaying) return

        if (!strongClickRef.current || !normalClickRef.current) {
            strongClickRef.current = new Audio("/click_strong.mp3")
            normalClickRef.current = new Audio("/click_normal.mp3")
        }

        const interval = setInterval(() => {
            if (beatCountRef.current < beatTypes.length - 1) {
                beatCountRef.current += 1
            } else {
                beatCountRef.current = 0
            }
            setCurrentBeatCount(beatCountRef.current)
            if (beatTypes.length > beatCountRef.current) {
                if (beatTypes[beatCountRef.current] === "Strong") {
                    if (!strongClickRef.current) return
                    strongClickRef.current.currentTime = 0
                    strongClickRef.current.play()
                } else if (beatTypes[beatCountRef.current] === "Normal") {
                    if (!normalClickRef.current) return
                    normalClickRef.current.currentTime = 0
                    normalClickRef.current?.play()
                }
            }
        }, 60 / bpm * (4 / beat) * 1000)

        return () => clearInterval(interval)
    }, [isPlaying, beatTypes, bpm, beat])
}

デプロイ先

デプロイはVercelを使用しました。
Next.jsソースをGitHubにpushするだけで自動的にデプロイし公開できるため、愛用しています。

さいごに

今回作ったサイトは以下です。
楽器の練習などにぜひお使いください!
https://web-metronome.vercel.app/

Discussion

ログインするとコメントできます