|  | 
|  | 1 | +'use client'; | 
|  | 2 | + | 
|  | 3 | +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | 
|  | 4 | +import { type VariantProps, cva } from 'class-variance-authority'; | 
|  | 5 | +import { | 
|  | 6 | +  type AnimationPlaybackControlsWithThen, | 
|  | 7 | +  type ValueAnimationTransition, | 
|  | 8 | +  animate, | 
|  | 9 | +  useMotionValue, | 
|  | 10 | +  useMotionValueEvent, | 
|  | 11 | +} from 'motion/react'; | 
|  | 12 | +import { | 
|  | 13 | +  type AgentState, | 
|  | 14 | +  type TrackReference, | 
|  | 15 | +  type TrackReferenceOrPlaceholder, | 
|  | 16 | +  // useMultibandTrackVolume, | 
|  | 17 | +  useTrackVolume, | 
|  | 18 | +} from '@livekit/components-react'; | 
|  | 19 | +import { cn } from '@/lib/utils'; | 
|  | 20 | +import { OscilliscopeShaders, type OscilliscopeShadersProps } from './oscilliscope-shaders'; | 
|  | 21 | + | 
|  | 22 | +const DEFAULT_SPEED = 5; | 
|  | 23 | +const DEFAULT_AMPLITUDE = 0.025; | 
|  | 24 | +const DEFAULT_FREQUENCY = 10; | 
|  | 25 | +const DEFAULT_TRANSITION: ValueAnimationTransition = { duration: 0.2, ease: 'easeOut' }; | 
|  | 26 | + | 
|  | 27 | +function useAnimatedValue<T>(initialValue: T) { | 
|  | 28 | +  const [value, setValue] = useState(initialValue); | 
|  | 29 | +  const motionValue = useMotionValue(initialValue); | 
|  | 30 | +  const controlsRef = useRef<AnimationPlaybackControlsWithThen | null>(null); | 
|  | 31 | +  useMotionValueEvent(motionValue, 'change', (value) => setValue(value as T)); | 
|  | 32 | + | 
|  | 33 | +  const animateFn = useCallback( | 
|  | 34 | +    (targetValue: T | T[], transition: ValueAnimationTransition) => { | 
|  | 35 | +      controlsRef.current = animate(motionValue, targetValue, transition); | 
|  | 36 | +    }, | 
|  | 37 | +    [motionValue] | 
|  | 38 | +  ); | 
|  | 39 | + | 
|  | 40 | +  return { value, controls: controlsRef, animate: animateFn }; | 
|  | 41 | +} | 
|  | 42 | + | 
|  | 43 | +export const audioShaderVisualizerVariants = cva(['aspect-square'], { | 
|  | 44 | +  variants: { | 
|  | 45 | +    size: { | 
|  | 46 | +      icon: 'h-[24px] gap-[2px]', | 
|  | 47 | +      sm: 'h-[56px] gap-[4px]', | 
|  | 48 | +      md: 'h-[112px] gap-[8px]', | 
|  | 49 | +      lg: 'h-[224px] gap-[16px]', | 
|  | 50 | +      xl: 'h-[448px] gap-[32px]', | 
|  | 51 | +    }, | 
|  | 52 | +  }, | 
|  | 53 | +  defaultVariants: { | 
|  | 54 | +    size: 'md', | 
|  | 55 | +  }, | 
|  | 56 | +}); | 
|  | 57 | + | 
|  | 58 | +interface AudioOscilloscopeVisualizerProps { | 
|  | 59 | +  speed?: number; | 
|  | 60 | +  state?: AgentState; | 
|  | 61 | +  audioTrack: TrackReferenceOrPlaceholder; | 
|  | 62 | +  className?: string; | 
|  | 63 | +} | 
|  | 64 | + | 
|  | 65 | +export function AudioOscilloscopeVisualizer({ | 
|  | 66 | +  size = 'lg', | 
|  | 67 | +  state = 'speaking', | 
|  | 68 | +  speed = DEFAULT_SPEED, | 
|  | 69 | +  audioTrack, | 
|  | 70 | +  className, | 
|  | 71 | +}: AudioOscilloscopeVisualizerProps & | 
|  | 72 | +  OscilliscopeShadersProps & | 
|  | 73 | +  VariantProps<typeof audioShaderVisualizerVariants>) { | 
|  | 74 | +  const { | 
|  | 75 | +    value: amplitude, | 
|  | 76 | +    controls: amplitudeControls, | 
|  | 77 | +    animate: animateAmplitude, | 
|  | 78 | +  } = useAnimatedValue(DEFAULT_AMPLITUDE); | 
|  | 79 | +  const { | 
|  | 80 | +    value: frequency, | 
|  | 81 | +    controls: frequencyControls, | 
|  | 82 | +    animate: animateFrequency, | 
|  | 83 | +  } = useAnimatedValue(DEFAULT_FREQUENCY); | 
|  | 84 | +  const { | 
|  | 85 | +    value: opacity, | 
|  | 86 | +    controls: opacityControls, | 
|  | 87 | +    animate: animateOpacity, | 
|  | 88 | +  } = useAnimatedValue(1.0); | 
|  | 89 | + | 
|  | 90 | +  const volume = useTrackVolume(audioTrack as TrackReference, { | 
|  | 91 | +    fftSize: 512, | 
|  | 92 | +    smoothingTimeConstant: 0.55, | 
|  | 93 | +  }); | 
|  | 94 | + | 
|  | 95 | +  useEffect(() => { | 
|  | 96 | +    switch (state) { | 
|  | 97 | +      case 'disconnected': | 
|  | 98 | +        animateAmplitude(0, DEFAULT_TRANSITION); | 
|  | 99 | +        animateFrequency(0, DEFAULT_TRANSITION); | 
|  | 100 | +        animateOpacity(1.0, DEFAULT_TRANSITION); | 
|  | 101 | +        return; | 
|  | 102 | +      case 'listening': | 
|  | 103 | +      case 'connecting': | 
|  | 104 | +        animateAmplitude(DEFAULT_AMPLITUDE, DEFAULT_TRANSITION); | 
|  | 105 | +        animateFrequency(DEFAULT_FREQUENCY, DEFAULT_TRANSITION); | 
|  | 106 | +        animateOpacity([1.0, 0.2], { | 
|  | 107 | +          duration: 0.75, | 
|  | 108 | +          repeat: Infinity, | 
|  | 109 | +          repeatType: 'mirror', | 
|  | 110 | +        }); | 
|  | 111 | +        return; | 
|  | 112 | +      case 'thinking': | 
|  | 113 | +      case 'initializing': | 
|  | 114 | +        animateAmplitude(DEFAULT_AMPLITUDE, DEFAULT_TRANSITION); | 
|  | 115 | +        animateFrequency(DEFAULT_FREQUENCY, DEFAULT_TRANSITION); | 
|  | 116 | +        animateOpacity([1.0, 0.2], { | 
|  | 117 | +          duration: 0.2, | 
|  | 118 | +          repeat: Infinity, | 
|  | 119 | +          repeatType: 'mirror', | 
|  | 120 | +        }); | 
|  | 121 | +        return; | 
|  | 122 | +      case 'speaking': | 
|  | 123 | +      default: | 
|  | 124 | +        animateAmplitude(DEFAULT_AMPLITUDE, DEFAULT_TRANSITION); | 
|  | 125 | +        animateFrequency(DEFAULT_FREQUENCY, DEFAULT_TRANSITION); | 
|  | 126 | +        animateOpacity(1.0, DEFAULT_TRANSITION); | 
|  | 127 | +        return; | 
|  | 128 | +    } | 
|  | 129 | +  }, [state, animateAmplitude, animateFrequency, animateOpacity]); | 
|  | 130 | + | 
|  | 131 | +  useEffect(() => { | 
|  | 132 | +    if (state === 'speaking' && volume > 0) { | 
|  | 133 | +      animateAmplitude(0.02 + 0.4 * volume, { duration: 0 }); | 
|  | 134 | +      animateFrequency(20 + 60 * volume, { duration: 0 }); | 
|  | 135 | +    } | 
|  | 136 | +  }, [state, volume, animateAmplitude, animateFrequency, amplitudeControls, frequencyControls]); | 
|  | 137 | + | 
|  | 138 | +  return ( | 
|  | 139 | +    <OscilliscopeShaders | 
|  | 140 | +      speed={speed} | 
|  | 141 | +      amplitude={amplitude} | 
|  | 142 | +      frequency={frequency} | 
|  | 143 | +      lineWidth={0.005} | 
|  | 144 | +      smoothing={0.001} | 
|  | 145 | +      style={{ opacity }} | 
|  | 146 | +      className={cn( | 
|  | 147 | +        audioShaderVisualizerVariants({ size }), | 
|  | 148 | +        '[mask-image:linear-gradient(90deg,rgba(0,0,0,0)_0%,rgba(0,0,0,1)_20%,rgba(0,0,0,1)_80%,rgba(0,0,0,0)_100%)]', | 
|  | 149 | +        'overflow-hidden rounded-full', | 
|  | 150 | +        className | 
|  | 151 | +      )} | 
|  | 152 | +    /> | 
|  | 153 | +  ); | 
|  | 154 | +} | 
0 commit comments