はじめに
音楽プレイヤーの設計について、比較する機会があったので記事にしてみました。
音楽プレイヤーの設計をしてみます。
まずはデザインを起こします。
※ 下のバーは見づらいのですが、音量バーを表しています。
HTMLでコーディングを行います。
<div
data-playing="false"
data-stopped="true"
data-loading="false"
>
<div>
<dl>
<div>
<dt>Title</dt>
<dd>Latinbeat</dd>
</div>
<div>
<dt>Artist</dt>
<dd>sena</dd>
</div>
</dl>
<div>
<button>
<span>Play</span>
</button>
<button>
<span>Next</span>
</button>
</div>
<input
type="range"
min="0"
max="100"
defaultValue="100"
aria-label="音量調整"
/>
</div>
<img
src="/image/image_disc_red.png"
width="240"
height="240"
alt="ディスク"
/>
</div>
必要な要素の宣言
宣言的UIで実装するため、音楽プレイヤーに必要な要素を宣言します。
そこで取得が必要な要素と、必要な操作を洗い出します。
必要な要素
- 現在再生中の音楽の情報(タイトル・アーティスト名)
- 再生・一時停止などの状態
- 現在の音量
- 音楽が準備(読み込み)中かどうか
必要な操作
- 再生・一時停止を切り替える
- 次の曲に切り替える
- 音量を変更する
以上が必要そうです。
宣言的UIで実装してみましょう
古来と現在のの宣言的UIを比較しながら実装してみましょう。
古来の宣言的UI
老舗音楽プレイヤーのfoobar2000 SDKでの、実装を考えてみると以下になります。
class Music : public CWindowImpl<Music>, private play_callback_impl_base {
/** ボタンを押したときの関数 **/
void onPlayPause(UINT, int, CWindow) {}
void onNext(UINT, int, CWindow) {}
void onChangeVolume(UINT, int, CWindow) {}
/** コールバック(継承) **/
// 再生状態が変化したら実行される
void on_playback_starting(play_control::t_track_command p_command, bool p_paused) {}
// 曲が読み込まれたら実行される
void on_playback_new_track(metadb_handle_ptr p_track) {}
// 曲が停止したら実行される
void on_playback_stop(play_control::t_stop_reason p_reason) {}
// 曲が一時停止したら実行される
void on_playback_pause(bool p_state) {}
// 音量が変化したら実行される
void on_volume_change(float p_new_val) {}
}
広義の宣言的UI自体は昔から存在しており、例えばfoobar2000 SDKのコードは機能ごとに実装されたクラスを継承することで表現しています。
何かを変更したタイミングで、都度コールバックが呼び出されます。 このコールバックでメンバ変数を変更して、再描画をし直す必要があります。
最近の宣言的UI(React)
変数を変更した後に再描画が自動的にされるようになりました。このことをデータバインディングと言います。
これにより、コールバック関数を書く必要が無くなり見やすくなりました。
export default function Music() {
const { playback } = useMusicGetPlaybackStatus(); // 再生状態を取得
const { music } = useMusicGetNowPlaying(); // 現在再生中のMusicを取得
const { stop, playOrPause, next, changeVolume } = useMusicControl(); // Musicを操作
const { volume } = useMusicGetPlaybackVolume(); // Volumeを取得
const { playbackLoading } = useMusicGetPlaybackLoading(); // 準備中か取得
const onStop = () => {
stop();
};
const onPlayPause = () => {
playOrPause();
};
const onNext = () => {
next();
};
const onChangeVolume = (e: ChangeEvent<HTMLInputElement>) => {
changeVolume(Number(e.target.value) / 100);
};
return (
<>
<div
data-playing={playback === 'playing'}
data-stopped={playback === 'stopped'}
data-loading={playbackLoading}
>
<div>
<dl>
<dt>
<span>{music?.title ?? ''}</span>
</dt>
<dd>{music?.artist ?? ''}</dd>
</dl>
<div className={styles.playbackController}>
<button
disabled={playbackLoading}
onClick={onPlayPause}
>
<span>{playback === 'playing' ? 'Pause' : 'Play'}</span>
</button>
<button
disabled={playbackLoading}
onClick={onNext}
>
<span>Next</span>
</button>
</div>
<input
type="range"
min={0}
max={100}
defaultValue={100}
onInput={onChangeVolume}
aria-label={'音量調整'}
/>
</div>
<img
src={`/image/image_disc_${music?.disc_color ?? 'red'}.png`}
width="240"
height="240"
alt=""
onClick={onPlayPause}
/>
</div>
</>
);
}
まとめ
宣言的UIを使用することでコンポーネントの見やすさと再描画の自動化が実現されますが、特にReactなどの最近のライブラリを活用することで、状態の取得や操作の宣言がさらにシンプルになりました。
今回は音楽プレイヤーのコンポーネントを作成してみましたが、宣言的UIを使用してを実装することで、再生状態、音量、曲の情報などを簡単に管理でき、Viewとの分離も容易に出来ます。
一方、古来の方法・やり方を学ぶことでより体系的に学ぶことが出来るので、さらに理解を深めたい方には別言語や昔のコードを触ることをオススメします。
新しいメンバーを募集しています
Sena / Engineer
生涯に亘り技術を極めていきたい。