西出正美です。有益なことや無益なことなどいろいろ書いています。

Reactでホワイトノイズを鳴らす

Reactでホワイトノイズを鳴らしたいこと、ありますよね。
僕はあるので自分用にメモです。

↓ React + TypeScript でホワイトノイズを鳴らしてみました。[参考文献]
※音が鳴ります

loading...

パソコンで音を鳴らすのはかんたんなんですけどスマホはガードが固いらしくて、ユーザのなんらかの動作をトリガーとして再生しないと再生されないみたいです。

なので今回は useCallback で宣言して onClick から呼びました。

↓ これが今回のコードです

import React from 'react';

import { VolumeDown, VolumeUp } from '@mui/icons-material';
import { Button, CircularProgress, Grid, Slider } from '@mui/material';

// webkit用Typescript型宣言
declare global {
  interface Window {
    webkitAudioContext: AudioContext;
  }
}

const ReactAudioTest: React.FunctionComponent = () => {
  // 音量:初期値25
  const [volume, setVolume] = React.useState(25);
  // 再生状態:初期値false
  const [playingFlag, setPlayingFlag] = React.useState(false);
  // AudioContext:初期値undefined
  const [audioContext, setAudioContext] = React.useState<
    AudioContext | undefined
  >(undefined);
  // 音源ソース:初期値undefined
  const [source, setSourceState] = React.useState<
    AudioBufferSourceNode | undefined
  >(undefined);
  // 音量調節:初期値undefined
  const [gainState, setGainState] = React.useState<GainNode | undefined>(
    undefined
  );

  React.useEffect(() => {
    // 初期化
    const newAudioContext = new (window.AudioContext ||
      window.webkitAudioContext)();
    setAudioContext(newAudioContext);
    const gainNode = newAudioContext.createGain();
    gainNode.gain.value = volume / 100;
    setGainState(gainNode);

    return () => {
      // unmount時の後始末
      if (source) {
        source.buffer = null; // メモリリーク防止
      }
      if (newAudioContext) {
        newAudioContext.close();
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  React.useEffect(() => {
    // 音量変更
    if (gainState) {
      gainState.gain.value = volume / 100;
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [volume]);

  // スマホなどでは迷惑な自動再生を防ぐ目的で
  // ユーザの操作に伴う再生でしか音を再生できない
  const onClickToStartAndStop = React.useCallback(() => {
    if (audioContext && gainState) {
      if (playingFlag) {
        // 停止
        if (source) {
          source.stop();
          source.buffer = null; // メモリリーク防止
        }
      } else {
        // 再生
        const channels = 1; // モノラル
        const frameCount = audioContext.sampleRate * 2.0; // 2秒

        const myArrayBuffer = audioContext.createBuffer(
          channels,
          frameCount,
          audioContext.sampleRate
        );

        for (let channel = 0; channel < channels; channel += 1) {
          // ホワイトノイズ生成
          const nowBuffering = myArrayBuffer.getChannelData(channel);
          for (let i = 0; i < frameCount; i += 1) {
            nowBuffering[i] = Math.random() * 2 - 1;
          }
        }
        const buffersource = audioContext.createBufferSource();
        setSourceState(buffersource);
        buffersource.buffer = myArrayBuffer;
        buffersource.loop = true;
        buffersource.connect(gainState).connect(audioContext.destination);
        buffersource.start();
      }
      setPlayingFlag(!playingFlag);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [playingFlag, audioContext, gainState]);

  if (!audioContext || !gainState) {
    return (
      <React.Fragment>
        <CircularProgress />
        loading...
      </React.Fragment>
    );
  }

  return (
    <div className="text-center h-fit w-full">
      <Grid container>
        <Grid item></Grid>
        <Grid item>
          <VolumeDown />
        </Grid>
        <Grid item xs={10}>
          <Slider
            min={0}
            max={100}
            defaultValue={50}
            value={volume}
            onChange={(__, newValue) => {
              if (typeof newValue === 'number') {
                setVolume(newValue);
              }
            }}
            valueLabelDisplay="auto"
            aria-labelledby="continuous-slider"
          />
        </Grid>
        <Grid item>
          <VolumeUp />
        </Grid>
        <Grid item></Grid>
      </Grid>

      <Button
        variant="outlined"
        color={playingFlag ? 'info' : 'primary'}
        onClick={onClickToStartAndStop}
      >
        ホワイトノイズ{playingFlag ? '停止' : '再生'}
      </Button>
    </div>
  );
};

export { ReactAudioTest };
NISHIDEMASAMI.GITHUB.IO
NISHIDE, Masami

西出正美です。有益なことや無益なことなどいろいろ書いています。

©NISHIDE, Masami Some Rights Reserved