All files / web/src/components/admin MicLevelMeter.tsx

0% Statements 0/123
0% Branches 0/1
0% Functions 0/1
0% Lines 0/123

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                                                                                                                                                                                                                                       
'use client'

import { useEffect, useRef } from 'react'
import { css } from '../../../styled-system/css'

const BAR_COUNT = 24
const BAR_GAP = 1
const BAR_WIDTH = 3
const BAR_HEIGHT = 18

/** Segment color: green → yellow → red */
function barColor(i: number, total: number): string {
  const pct = i / total
  if (pct < 0.55) return '#3fb950'
  if (pct < 0.8) return '#d29922'
  return '#f85149'
}

interface MicLevelMeterProps {
  stream: MediaStream | null
}

export function MicLevelMeter({ stream }: MicLevelMeterProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const ctxRef = useRef<AudioContext | null>(null)
  const analyserRef = useRef<AnalyserNode | null>(null)
  const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null)
  const rafRef = useRef<number>(0)

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas || !stream) {
      // Clear canvas when no stream
      if (canvas) {
        const ctx = canvas.getContext('2d')
        if (ctx) {
          ctx.clearRect(0, 0, canvas.width, canvas.height)
          // Draw dim bars
          for (let i = 0; i < BAR_COUNT; i++) {
            const x = i * (BAR_WIDTH + BAR_GAP)
            ctx.fillStyle = '#21262d'
            ctx.fillRect(x, 0, BAR_WIDTH, BAR_HEIGHT)
          }
        }
      }
      return
    }

    const audioCtx = new AudioContext()
    ctxRef.current = audioCtx

    const analyser = audioCtx.createAnalyser()
    analyser.fftSize = 256
    analyser.smoothingTimeConstant = 0.7
    analyserRef.current = analyser

    const source = audioCtx.createMediaStreamSource(stream)
    source.connect(analyser)
    sourceRef.current = source

    const dataArray = new Uint8Array(analyser.frequencyBinCount)
    const ctx = canvas.getContext('2d')!
    const dpr = window.devicePixelRatio || 1

    // Scale canvas for high-DPI
    canvas.width = (BAR_COUNT * (BAR_WIDTH + BAR_GAP) - BAR_GAP) * dpr
    canvas.height = BAR_HEIGHT * dpr
    ctx.scale(dpr, dpr)

    function draw() {
      rafRef.current = requestAnimationFrame(draw)
      analyser.getByteFrequencyData(dataArray)

      // Compute RMS level (0-1)
      let sum = 0
      for (let i = 0; i < dataArray.length; i++) {
        const v = dataArray[i] / 255
        sum += v * v
      }
      const rms = Math.sqrt(sum / dataArray.length)
      // Map to bar count with some headroom boost
      const level = Math.min(1, rms * 3.5)
      const litBars = Math.round(level * BAR_COUNT)

      ctx.clearRect(0, 0, BAR_COUNT * (BAR_WIDTH + BAR_GAP), BAR_HEIGHT)

      for (let i = 0; i < BAR_COUNT; i++) {
        const x = i * (BAR_WIDTH + BAR_GAP)
        if (i < litBars) {
          ctx.fillStyle = barColor(i, BAR_COUNT)
          ctx.fillRect(x, 0, BAR_WIDTH, BAR_HEIGHT)
        } else {
          ctx.fillStyle = '#21262d'
          ctx.fillRect(x, 0, BAR_WIDTH, BAR_HEIGHT)
        }
      }
    }

    draw()

    return () => {
      cancelAnimationFrame(rafRef.current)
      source.disconnect()
      analyser.disconnect()
      audioCtx.close()
      ctxRef.current = null
      analyserRef.current = null
      sourceRef.current = null
    }
  }, [stream])

  const totalWidth = BAR_COUNT * (BAR_WIDTH + BAR_GAP) - BAR_GAP

  return (
    <canvas
      ref={canvasRef}
      data-element="mic-level-meter"
      className={css({ flexShrink: 0 })}
      style={{ width: `${totalWidth}px`, height: `${BAR_HEIGHT}px` }}
      title="Microphone input level"
    />
  )
}