{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-vocoder-panel",
  "type": "registry:ui",
  "title": "Vocoder Panel",
  "description": "A controlled channel-vocoder panel with paired analysis and carrier filter banks, logarithmic frequency guides, grouped range, envelope and output controls, and optional live spectra. Includes a pure band-map model for tests and server rendering.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-knob.json",
    "https://pluginui.com/r/core-spectrum.json"
  ],
  "files": [
    {
      "path": "components/ui/vocoder-panel.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/vocoder-panel\n * Copyright © [Maru](https://maru.audio)\n *\n * Maru UI Kit — https://maru.audio/ui-kit\n *\n * Installed from the Maru UI registry. This file is yours now: edit it\n * freely, it will not be overwritten unless you re-run the add command.\n *\n * Keep this item’s companion files and CSS imports together. Dependencies\n * declared by the registry are installed with the source.\n */\n/** Controlled band vocoder. The host supplies measurements; this view never analyzes audio. */\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport { Knob, type KnobProps } from \"@/components/ui/knob\"\nimport {\n  frequencyPosition,\n  vocoderBandMap,\n  vocoderLevelDb,\n  VOCODER_LEVEL_FLOOR_DB,\n  type VocoderBandAnalysis,\n  type VocoderBandCount,\n} from \"maruaudio/ui/shared/vocoder-model\"\n\nexport type VocoderSource = \"external\" | \"self\" | \"white\" | \"pink\" | \"brown\"\nexport type VocoderDisplay = \"mapping\" | \"modulator\" | \"carrier\"\n\nexport interface VocoderValues {\n  bands: VocoderBandCount\n  carrierMinHz: number\n  carrierMaxHz: number\n  modulatorMinHz: number\n  modulatorMaxHz: number\n  qStart: number\n  qEnd: number\n  attackMs: number\n  releaseMs: number\n  gainDb: number\n  mix: number\n}\nexport type VocoderKnobKey = Exclude<keyof VocoderValues, \"bands\">\n\nexport interface VocoderPanelProps {\n  values: VocoderValues\n  onChange: (patch: Partial<VocoderValues>) => void\n  paths?: Partial<Record<keyof VocoderValues, string>>\n  /** Module faces supply schema-bound controls with host automation gestures. */\n  renderKnob?: (key: VocoderKnobKey, label: string) => ReactNode\n  /** Actual filter-bank envelope and wet-output RMS, in linear full scale. */\n  analysis?: VocoderBandAnalysis | null\n  analysisState?: \"idle\" | \"unavailable\"\n  /** Used only for configured band positions while no live frame is available. */\n  sampleRate?: number\n  width?: number | string\n  className?: string\n  style?: CSSProperties\n}\nconst labelStyle: CSSProperties = {\n  color: token.textMuted,\n  fontSize: 10,\n  letterSpacing: \"0.04em\",\n}\nconst frequencyLabel = (hz: number) =>\n  hz >= 1000 ? `${Number((hz / 1000).toFixed(1))}k` : `${Math.round(hz)}`\n\nfunction BandLevels({\n  values,\n  analysis,\n  sampleRate,\n}: Pick<VocoderPanelProps, \"values\" | \"analysis\" | \"sampleRate\">) {\n  const configured = vocoderBandMap({ ...values, sampleRate })\n  const inputHz =\n    analysis?.inputHz ?? configured.map((band) => band.modulatorHz)\n  const outputHz =\n    analysis?.outputHz ?? configured.map((band) => band.carrierHz)\n  const x = (hz: number) => frequencyPosition(hz) * 1000\n  const y = (amplitude: number) =>\n    6 + (vocoderLevelDb(amplitude) / VOCODER_LEVEL_FLOOR_DB) * 80\n  const input = inputHz.map(\n    (hz, index) => [x(hz), y(analysis?.inputEnvelope[index] ?? 0)] as const\n  )\n  const output = outputHz.map(\n    (hz, index) => [x(hz), y(analysis?.outputRms[index] ?? 0)] as const\n  )\n  const line = (points: readonly (readonly [number, number])[]) =>\n    points.map(([px, py], index) => `${index ? \"L\" : \"M\"}${px},${py}`).join(\" \")\n  const area = (points: readonly (readonly [number, number])[]) =>\n    `${line(points)} L${points.at(-1)![0]},86 L${points[0]![0]},86 Z`\n  return (\n    <div style={{ position: \"relative\", height: 110, paddingLeft: 26 }}>\n      {[0, -24, -48, -72].map((db) => (\n        <span\n          key={db}\n          aria-hidden=\"true\"\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 6 + (db / VOCODER_LEVEL_FLOOR_DB) * 80 - 5,\n            font: `9px ${token.fontMono}`,\n            color: token.textMuted,\n          }}\n        >\n          {db}\n        </span>\n      ))}\n      <div style={{ position: \"relative\", height: 92 }}>\n        <svg\n          viewBox=\"0 0 1000 92\"\n          width=\"100%\"\n          height={92}\n          preserveAspectRatio=\"none\"\n          role=\"img\"\n          aria-label={\n            analysis\n              ? \"Live vocoder band levels: input envelope and wet output RMS, −72 to 0 dBFS, logarithmic frequency\"\n              : \"Configured vocoder band centers, logarithmic frequency; no live levels\"\n          }\n          data-vocoder-levels={analysis ? \"live\" : \"idle\"}\n          style={{ display: \"block\", overflow: \"visible\" }}\n        >\n          <desc>\n            Input filter envelopes and wet output band RMS. Fixed dBFS scale;\n            not an FFT. Output includes gain and mix, excludes dry audio.\n          </desc>\n          {[0, -24, -48, -72].map((db) => (\n            <line\n              key={db}\n              x1={0}\n              x2={1000}\n              y1={6 + (db / VOCODER_LEVEL_FLOOR_DB) * 80}\n              y2={6 + (db / VOCODER_LEVEL_FLOOR_DB) * 80}\n              stroke={token.border}\n              strokeOpacity={0.65}\n              vectorEffect=\"non-scaling-stroke\"\n            />\n          ))}\n          {[20, 100, 1000, 20000].map((hz) => (\n            <line\n              key={hz}\n              x1={x(hz)}\n              x2={x(hz)}\n              y1={6}\n              y2={86}\n              stroke={token.border}\n              strokeOpacity={0.4}\n              vectorEffect=\"non-scaling-stroke\"\n            />\n          ))}\n          {analysis ? (\n            <>\n              <path\n                data-vocoder-curve=\"input\"\n                d={area(input)}\n                fill={token.accent2}\n                fillOpacity={0.06}\n              />\n              <path\n                data-vocoder-curve=\"output\"\n                d={area(output)}\n                fill={token.accent}\n                fillOpacity={0.09}\n              />\n              {input.map(([px, py], index) => (\n                <line\n                  key={index}\n                  x1={px}\n                  y1={py}\n                  x2={output[index]![0]}\n                  y2={output[index]![1]}\n                  stroke={token.textMuted}\n                  strokeOpacity={0.22}\n                  strokeDasharray=\"2 3\"\n                  vectorEffect=\"non-scaling-stroke\"\n                />\n              ))}\n              <path\n                d={line(input)}\n                fill=\"none\"\n                stroke={token.accent2}\n                strokeWidth={1.5}\n                vectorEffect=\"non-scaling-stroke\"\n              />\n              <path\n                d={line(output)}\n                fill=\"none\"\n                stroke={token.accent}\n                strokeWidth={1.5}\n                vectorEffect=\"non-scaling-stroke\"\n              />\n            </>\n          ) : null}\n          {input.map(([px, py], index) => (\n            <g key={index}>\n              <desc>{`Band ${index + 1}: ${frequencyLabel(inputHz[index]!)} Hz → ${frequencyLabel(outputHz[index]!)} Hz${analysis ? ` · input ${vocoderLevelDb(analysis.inputEnvelope[index]!).toFixed(1)} dBFS · wet output ${vocoderLevelDb(analysis.outputRms[index]!).toFixed(1)} dBFS` : \" · configured centers\"}`}</desc>\n              <line\n                x1={px}\n                x2={px}\n                y1={py - 2}\n                y2={py + 2}\n                stroke={token.accent2}\n                strokeOpacity={analysis ? 1 : 0.35}\n                strokeWidth={3}\n                vectorEffect=\"non-scaling-stroke\"\n              />\n              <line\n                x1={output[index]![0]}\n                x2={output[index]![0]}\n                y1={output[index]![1] - 2}\n                y2={output[index]![1] + 2}\n                stroke={token.accent}\n                strokeOpacity={analysis ? 1 : 0.35}\n                strokeWidth={3}\n                vectorEffect=\"non-scaling-stroke\"\n              />\n            </g>\n          ))}\n        </svg>\n        {[20, 100, 1000, 20000].map((hz, index) => (\n          <span\n            key={hz}\n            aria-hidden=\"true\"\n            style={{\n              position: \"absolute\",\n              top: 94,\n              left: `${frequencyPosition(hz) * 100}%`,\n              transform:\n                index === 0\n                  ? undefined\n                  : index === 3\n                    ? \"translateX(-100%)\"\n                    : \"translateX(-50%)\",\n              whiteSpace: \"nowrap\",\n              font: `9px ${token.fontMono}`,\n              color: token.textMuted,\n            }}\n          >\n            {frequencyLabel(hz)}\n            {index === 3 ? \" Hz\" : \"\"}\n          </span>\n        ))}\n      </div>\n    </div>\n  )\n}\n\nconst GROUPS: readonly {\n  title: string\n  color?: string\n  knobs: readonly {\n    key: VocoderKnobKey\n    label: string\n    min: number\n    max: number\n    curve?: KnobProps[\"curve\"]\n    valueDisplay?: KnobProps[\"valueDisplay\"]\n    unit?: string\n    step?: number\n    bipolar?: boolean\n  }[]\n}[] = [\n  {\n    title: \"Input range\",\n    color: token.accent2,\n    knobs: [\n      {\n        key: \"modulatorMinHz\",\n        label: \"Low\",\n        min: 20,\n        max: 4000,\n        curve: \"log\",\n        valueDisplay: \"hz\",\n      },\n      {\n        key: \"modulatorMaxHz\",\n        label: \"High\",\n        min: 1000,\n        max: 20000,\n        curve: \"log\",\n        valueDisplay: \"hz\",\n      },\n    ],\n  },\n  {\n    title: \"Carrier range\",\n    color: token.accent,\n    knobs: [\n      {\n        key: \"carrierMinHz\",\n        label: \"Low\",\n        min: 20,\n        max: 4000,\n        curve: \"log\",\n        valueDisplay: \"hz\",\n      },\n      {\n        key: \"carrierMaxHz\",\n        label: \"High\",\n        min: 1000,\n        max: 20000,\n        curve: \"log\",\n        valueDisplay: \"hz\",\n      },\n    ],\n  },\n  {\n    title: \"Focus\",\n    knobs: [\n      { key: \"qStart\", label: \"Low Q\", min: 0.25, max: 20, curve: \"log\" },\n      { key: \"qEnd\", label: \"High Q\", min: 0.25, max: 20, curve: \"log\" },\n    ],\n  },\n  {\n    title: \"Envelope\",\n    knobs: [\n      {\n        key: \"attackMs\",\n        label: \"Attack\",\n        min: 0.1,\n        max: 250,\n        curve: \"log\",\n        unit: \"ms\",\n      },\n      {\n        key: \"releaseMs\",\n        label: \"Release\",\n        min: 1,\n        max: 1000,\n        curve: \"log\",\n        unit: \"ms\",\n      },\n    ],\n  },\n  {\n    title: \"Output\",\n    knobs: [\n      {\n        key: \"gainDb\",\n        label: \"Gain\",\n        min: -24,\n        max: 18,\n        step: 0.1,\n        valueDisplay: \"db\",\n        bipolar: true,\n      },\n      {\n        key: \"mix\",\n        label: \"Mix\",\n        min: 0,\n        max: 1,\n        step: 0.01,\n        valueDisplay: \"percent\",\n      },\n    ],\n  },\n]\n\nexport function VocoderPanel({\n  values,\n  onChange,\n  paths,\n  renderKnob,\n  analysis,\n  analysisState = \"unavailable\",\n  sampleRate,\n  width = 680,\n  className,\n  style,\n}: VocoderPanelProps) {\n  return (\n    <section\n      className={className}\n      aria-label=\"Vocoder controls\"\n      style={{\n        width,\n        maxWidth: \"100%\",\n        minWidth: 0,\n        boxSizing: \"border-box\",\n        color: token.text,\n        fontFamily: token.font,\n        ...style,\n      }}\n    >\n      <div\n        style={{\n          padding: \"8px 10px 6px\",\n          background: token.surface,\n          borderRadius: 5,\n        }}\n      >\n        <header\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 14,\n            marginBottom: 7,\n            ...labelStyle,\n          }}\n        >\n          <span\n            style={{ color: token.accent2 }}\n            aria-description=\"Measured analysis filter envelopes\"\n          >\n            ● Input\n          </span>\n          <span\n            style={{ color: token.accent }}\n            aria-description=\"Measured wet output band RMS; includes gain and mix, excludes dry audio\"\n          >\n            ● Output\n          </span>\n          <span\n            style={{ fontSize: 9, marginLeft: \"auto\" }}\n            data-vocoder-analysis-status=\"\"\n          >\n            {analysis\n              ? \"dBFS\"\n              : analysisState === \"idle\"\n                ? \"Idle\"\n                : \"No analysis\"}\n          </span>\n          <label style={{ display: \"flex\", alignItems: \"center\", gap: 5 }}>\n            Bands\n            <select\n              aria-label=\"Vocoder bands\"\n              data-audio-param-path={paths?.bands}\n              value={values.bands}\n              onChange={(event) =>\n                onChange({\n                  bands: Number(event.target.value) as VocoderBandCount,\n                })\n              }\n              style={{\n                background: token.cap,\n                border: `1px solid ${token.border}`,\n                borderRadius: 4,\n                color: token.text,\n                font: `10px ${token.font}`,\n                height: 24,\n                padding: \"0 4px\",\n              }}\n            >\n              {[8, 12, 16].map((count) => (\n                <option key={count} value={count}>\n                  {count}\n                </option>\n              ))}\n            </select>\n          </label>\n        </header>\n        <BandLevels\n          values={values}\n          analysis={analysis}\n          sampleRate={sampleRate}\n        />\n      </div>\n      <div\n        style={{\n          display: \"flex\",\n          flexWrap: \"wrap\",\n          gap: \"10px 6px\",\n          marginTop: 12,\n        }}\n      >\n        {GROUPS.map((group) => (\n          <fieldset\n            key={group.title}\n            style={{\n              flex: \"1 1 94px\",\n              minWidth: 0,\n              margin: 0,\n              padding: 0,\n              border: 0,\n            }}\n          >\n            <legend\n              style={{\n                ...labelStyle,\n                color: group.color ?? token.textMuted,\n                padding: 0,\n                margin: \"0 auto 6px\",\n                textAlign: \"center\",\n              }}\n            >\n              {group.title}\n            </legend>\n            <div style={{ display: \"flex\", justifyContent: \"center\", gap: 5 }}>\n              {group.knobs.map(({ key, label, ...knob }) => (\n                <div key={key} style={{ minWidth: 0 }}>\n                  {renderKnob ? (\n                    renderKnob(key, label)\n                  ) : (\n                    <Knob\n                      {...knob}\n                      path={paths?.[key]}\n                      label={label}\n                      value={values[key]}\n                      onChange={(value) => onChange({ [key]: value })}\n                      variant=\"small\"\n                      activeColor={group.color}\n                    />\n                  )}\n                </div>\n              ))}\n            </div>\n          </fieldset>\n        ))}\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/vocoder-panel.tsx"
    }
  ]
}
