LoginSignup
1
1

More than 1 year has passed since last update.

ReactでSine波を再生するCodePenを作った

Last updated at Posted at 2022-05-11

経緯

ギターのチューニングをするときに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の中にAudioContextOscillatorNodeも全部入っているのに、Hookの外側から見えるのは周波数と再生中フラグと周波数テキストボックスのonChangeイベントとオンオフ切り替えイベントとエラーフラグしか見えないようになっているのが見どころ。

(私見)僕はこう思ったっス

QiitaでCodePenがプレビューできるってもんだからやってみたけど、CodePenってファイルを分けることできないの知らなかった。
でも簡単なTypeScriptなら書けるからどんどん書いていきたい。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1