⏲️
ReactでWebで使えるメトロノームを作ってみた
はじめに
Webで使えるメトロノームをNext.jsを使用して作成しました。
その工程などを書いていこうと思います。
作ったサイトはこちらです。
環境
- 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するだけで自動的にデプロイし公開できるため、愛用しています。
さいごに
今回作ったサイトは以下です。
楽器の練習などにぜひお使いください!
Discussion