Takazudo Modular Styleguide

AudioDemo/OscillatorDemo

Default

Component Source

oscillator-demo.tsx

'use client';

import { useState, useRef, useCallback, useEffect } from 'preact/hooks';
import BrowserOnly from '../browser-only';
import { useAudioContext } from './use-audio-context';
import WaveformDisplay from './waveform-display';
import TimeScaleControls from './time-scale-controls';
import useScopeWorklet from './use-scope-worklet';
import { DEFAULT_FFT_SIZE } from './constants';
import { stopSource, disconnectNode } from './cleanup-nodes';
import ButtonGroup from './button-group';
import { adStyles } from './audio-demo-styles';
import { inputValue } from './utils';

type WaveformType = 'sine' | 'triangle' | 'sawtooth' | 'square';

const WAVEFORM_TYPES: WaveformType[] = ['sine', 'triangle', 'sawtooth', 'square'];

function OscillatorDemoInner() {
  const { ensureResumed } = useAudioContext();
  const [isPlaying, setIsPlaying] = useState(false);
  const [waveform, setWaveform] = useState<WaveformType>('sine');
  const [frequency, setFrequency] = useState(440);
  const [volume, setVolume] = useState(50);
  const [timeScale, setTimeScale] = useState(1);

  const oscRef = useRef<OscillatorNode | null>(null);
  const gainRef = useRef<GainNode | null>(null);
  const analyserRef = useRef<AnalyserNode | null>(null);
  const scope = useScopeWorklet();

  const handleToggle = useCallback(async () => {
    if (isPlaying) {
      stopSource(oscRef);
      disconnectNode(gainRef);
      disconnectNode(analyserRef);
      scope.disconnect();
      setIsPlaying(false);
      return;
    }

    const ctx = await ensureResumed();

    const oscillator = ctx.createOscillator();
    const gainNode = ctx.createGain();
    const analyser = ctx.createAnalyser();
    analyser.fftSize = DEFAULT_FFT_SIZE;

    oscillator.type = waveform;
    oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
    gainNode.gain.setValueAtTime(volume / 100, ctx.currentTime);

    oscillator.connect(gainNode);
    gainNode.connect(analyser);
    const worklet = await scope.ensureWorklet(ctx);
    gainNode.connect(worklet);
    analyser.connect(ctx.destination);

    oscillator.start();

    oscRef.current = oscillator;
    gainRef.current = gainNode;
    analyserRef.current = analyser;
    setIsPlaying(true);
  }, [isPlaying, waveform, frequency, volume, ensureResumed, scope]);

  const handleWaveformChange = useCallback((type: WaveformType) => {
    setWaveform(type);
    if (oscRef.current) {
      oscRef.current.type = type;
    }
  }, []);

  const handleFrequencyChange = useCallback((e: Event) => {
    const freq = inputValue(e);
    setFrequency(freq);
    if (oscRef.current) {
      oscRef.current.frequency.setValueAtTime(freq, oscRef.current.context.currentTime);
    }
  }, []);

  const handleVolumeChange = useCallback((e: Event) => {
    const vol = inputValue(e);
    setVolume(vol);
    if (gainRef.current) {
      gainRef.current.gain.setValueAtTime(vol / 100, gainRef.current.context.currentTime);
    }
  }, []);

  useEffect(() => {
    return () => {
      stopSource(oscRef);
      disconnectNode(gainRef);
      disconnectNode(analyserRef);
      scope.disconnect();
    };
  }, [scope]);

  return (
    <div className={adStyles.demoContainer}>
      <div className={adStyles.controlsSection}>
        <ButtonGroup
          label="Waveform"
          options={WAVEFORM_TYPES}
          value={waveform}
          onChange={handleWaveformChange}
        />

        <div className={adStyles.controlGroup}>
          <label htmlFor="oscillator-frequency" className={adStyles.controlLabel}>
            Frequency: <span className={adStyles.valueDisplay}>{frequency} Hz</span>
          </label>
          <input
            id="oscillator-frequency"
            type="range"
            min="20"
            max="2000"
            value={frequency}
            onChange={handleFrequencyChange}
            className={adStyles.slider}
          />
        </div>

        <div className={adStyles.controlGroup}>
          <label htmlFor="oscillator-volume" className={adStyles.controlLabel}>
            Volume: <span className={adStyles.valueDisplay}>{volume}%</span>
          </label>
          <input
            id="oscillator-volume"
            type="range"
            min="0"
            max="100"
            value={volume}
            onChange={handleVolumeChange}
            className={adStyles.slider}
          />
        </div>

        <button
          type="button"
          aria-pressed={isPlaying}
          className={`${adStyles.playButton} ${isPlaying ? adStyles.playButtonActive : ''}`}
          onClick={handleToggle}
        >
          {isPlaying ? 'Stop' : 'Play'}
        </button>

        <TimeScaleControls value={timeScale} onChange={setTimeScale} />
      </div>

      <WaveformDisplay isPlaying={isPlaying} timeScale={timeScale} scope={scope} />
    </div>
  );
}

export default function OscillatorDemo() {
  return <BrowserOnly>{() => <OscillatorDemoInner />}</BrowserOnly>;
}

Story Source

oscillator-demo.stories.tsx

import OscillatorDemo from './oscillator-demo';

export const meta = {
  title: 'AudioDemo/OscillatorDemo',
};

export const Default = () => <OscillatorDemo />;