経緯
ギターのチューニングをするときに440HzのSine波が手軽に再生できたら便利だと思ったから。
どうせならスマホとかブラウザでいつでもSine波が再生できるやつをTypeScriptでReactで作ってみた。
こんなんできましたけど
See the Pen eYVzyQO by Masami,Nishide (@nishidemasami) on CodePen.
こだわり
React
だけじゃなく、テキストフィールドやボタンの見た目も良くするためにMUI
も使った。
本当はRedux-toolkit
も使いたかったけど、今回はuseStateHook
の中に全部ぶち込んだ。
コード全文
// TypeScriptのためのwindow.webkitAudioContextの型定義
declare global {
interface Window {
AudioContext: typeof AudioContext;
webkitAudioContext: typeof AudioContext;
}
}
type State = {
/** 再生中かどうかフラグ */
playing: boolean;
/** PLAY/STOPボタンonClickイベント */
onClickPlayStopButton: () => void;
/** 周波数 */
frequency: string;
/** 周波数テキストフィールドonChangeイベント */
onChangeFrequencyTextField: (e: React.ChangeEvent<HTMLInputElement>) => void;
/** エラーフラグ */
isError: boolean;
};
const useStateHook: () => State = () => {
// Web Audio API AudioContextインスタンス
const [audioContext] = React.useState<AudioContext>(
new (window.AudioContext || window.webkitAudioContext)()
);
// Web Audio API OscillatorNodeインタフェース
const oscillator = React.useRef<OscillatorNode>();
// 再生中かどうかフラグ ※初期値:false
const [playing, setPlayingFlag] = React.useState(false);
// 周波数 ※初期値:440Hz
const [frequency, setFrequency] = React.useState(String(440));
// エラーフラグ ※初期値:false
const [isError, setIsError] = React.useState(false);
// PLAY/STOPボタンonClickイベント
const onClickPlayStopButton = React.useCallback(() => {
setPlayingFlag(!playing);
}, [playing]);
// 周波数テキストフィールドonChangeイベント
const onChangeFrequencyTextField = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setFrequency(e.target.value);
},
[]
);
// エラー判定
React.useEffect(() => {
/** 周波数テキストフィールドに1以上の数字が入力された時以外はエラー */
const error = Number.isNaN(Number(frequency)) || Number(frequency) <= 0;
setIsError(error);
if (error) {
setPlayingFlag(false);
}
}, [frequency]);
// サイン波停止/再生切り替え
React.useEffect(() => {
if (oscillator.current) {
oscillator.current.stop();
oscillator.current.disconnect();
oscillator.current = undefined;
}
if (!isError && playing) {
oscillator.current = audioContext.createOscillator();
oscillator.current.type = "sine";
oscillator.current.frequency.value = Number(frequency);
oscillator.current.connect(audioContext.destination);
oscillator.current.start();
}
}, [audioContext, frequency, isError, playing]);
return {
playing,
onClickPlayStopButton,
frequency,
onChangeFrequencyTextField,
isError
};
};
const App: React.VFC = () => {
const {
playing,
onClickPlayStopButton,
frequency,
onChangeFrequencyTextField,
isError
} = useStateHook();
return (
<>
<div>
<MaterialUI.TextField
error={isError}
label="Frequency"
onChange={onChangeFrequencyTextField}
type="number"
value={frequency}
/>
</div>
<div>
<MaterialUI.Button
color={playing ? "error" : "primary"}
disabled={isError}
onClick={onClickPlayStopButton}
variant="contained"
>
{playing ? "🔇STOP" : "🔊PLAY"} {frequency}HZ SINE WAVE
</MaterialUI.Button>
</div>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
見どころ
ごちゃっとしたuseStateHook
の中にAudioContext
もOscillatorNode
も全部入っているのに、Hook
の外側から見えるのは周波数と再生中フラグと周波数テキストボックスのonChange
イベントとオンオフ切り替えイベントとエラーフラグしか見えないようになっているのが見どころ。
(私見)僕はこう思ったっス
QiitaでCodePenがプレビューできるってもんだからやってみたけど、CodePenってファイルを分けることできないの知らなかった。
でも簡単なTypeScriptなら書けるからどんどん書いていきたい。