{
  "slug": "voice-waveform-live",
  "title": "Live voice waveform",
  "tagline": "A live waveform in a recording pill — 64 thin bars rise and fall with \"speech,\" spiking tall mid-speech and shrinking to a dotted line on pauses, scrolling right to left; a full speak-pause-speak-submit performance.",
  "description": "A live waveform in a recording pill — 64 thin bars rise and fall with \"speech,\" spiking tall mid-speech and shrinking to a dotted line on pauses, scrolling ri",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/voice-waveform-live/poster-63e8463e161e.jpg",
    "video": "https://gimgs.net/motion/media/voice-waveform-live/video-0a1198f39906.mp4"
  },
  "variants": [
    {
      "key": "voice-waveform-live",
      "label": "voice-waveform-live",
      "description": "A live waveform in a recording pill — 64 thin vertical bars rise and fall with \"speech,\" spiking tall mid-speech and shrinking to a dotted line on pauses, scrolling right to left; a full speak-pause-speak-submit performance",
      "use": "",
      "video": "https://gimgs.net/motion/media/voice-waveform-live/video-0a1198f39906.mp4",
      "poster": "https://gimgs.net/motion/media/voice-waveform-live/poster-63e8463e161e.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/voice-waveform-live.html",
  "json_url": "https://gimgs.net/motion/voice-waveform-live.json",
  "template_url": "https://gimgs.net/motion/voice-waveform-live/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/voice-waveform-live.md",
  "updated_at": "2026-09-24T07:40:34.152Z",
  "prompt": "## When to use\n\nA \"listening to you speak\" shot for voice-input/AI-assistant features; a passage with no UI content to show that still needs sustained liveliness to hold the frame\n\n## Intention\n\nThe only waveform in the library so far is spectrum-morph-ui — that's a **decorative** music visualizer, a title underline splitting into spectrum bars. This card is a **functional** voice waveform: a live \"I'm listening\" receipt. The difference is causality: spectrum dances to the BGM, it's packaging; this card dances to \"the user speaking,\" it's the product feature itself. The waveform spikes while speaking, shrinks to a row of dots on pause, and scrolls its history off to the left — the viewer reads \"it's really listening\" from the waveform's rise and fall, and a 6s+ shot rides entirely on this liveliness with no other content needed. Division of labor with gauge-readout-moves: a gauge is the mechanical ceremony of \"reporting one value,\" this card is the vital sign of \"continuous listening,\" with no final value.\n\n## Duration & energy\n\n- Duration: ~5s (150f: entrance 12f + speak 1.4s + pause 0.8s + speak 1.4s + submit collapse 0.8s)\n- Energy: Medium (functional liveliness, not showmanship)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n库里唯一的波形是 spectrum-morph-ui——那是标题下划线裂成频谱条的\n**装饰性**音乐可视化。本卡是**功能性**声纹：\"正在听你说\"的实时\n回执。区别在因果：spectrum 跟 BGM 跳、是包装；本卡跟\"用户说话\"\n跳、是产品功能本身。说话时波形高耸、停顿时缩成一排点线、历史向左\n滚出——观众从波形的起伏读出\"它真的在听\"，一段 6s+ 的画面全靠这份\n活性撑住，不需要任何别的内容。与 gauge-readout-moves 分工：仪表是\n\"报一个数值\"的机械仪式，本卡是\"持续监听\"的生命体征，没有终值。\n\n## 动效核心\n- **帧确定的伪随机是命门**：mulberry32 按整数采样点取值 + smoothstep\n  相邻插值（值噪声），无 Math.random——渲染可复现且相邻帧连续\n- 滚动感：第 i 条采样时刻 = f - (N-1-i)·1.6，最右条是\"现在\"，\n  历史向左流；不是条在平移，是每条在读不同时刻的信号\n- 条高 = 说话包络（梯形 seg 说/停两档 + 音节噪声 0.55+0.45·noise）\n  × 中部空间权重 sin^0.8 × 逐条 jitter（0.35+0.65·noise）；\n  静默钳到 5px 点线\n- 提交：按钮 3f 压缩 scale 1→0.82→1 回弹，波形 12f Easing.in 塌缩\n  到 0.06，麦克风辉光随包络亮灭\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 条数/间隙 | 64 条，gap 6px（1320px 宽胶囊） | 少于 40 条读作均衡器；密过 90 条单条读不出起伏 |\n| 滚动速率 | 1.6 帧/采样点 | 快了历史刷太快像故障；慢了\"滚动感\"消失变成原地抖 |\n| 说停包络 | seg 梯形 rise 5f / fall 7f；demo 说 15–57f、停 80–124f | 停顿段必须真的塌成点线——没有\"停\"的对比，\"说\"就不成立 |\n| 音节噪声 | 0.55+0.45·noiseAt(t/4.5) | 去掉音节层波形是平滑山丘，读不出\"一句话的顿挫\" |\n| 中部权重 | sin^0.8 空间包络 | 无权重全场等高像均衡器；指数 >1.5 中间独秀像单根跳 |\n| 静默钳位 | max 5px 点线 | 钳 0 波形消失读作断线；点线是\"在听但你没说\" |\n| 提交塌缩 | 12f Easing.in 到 0.06 + 按钮压缩回弹 3f | 塌缩是\"这段话被收走了\"；淡出读不出提交语义 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **有音轨时波形必须跟人声走**：片子若配了真旁白/人声，包络的说停段要对齐音轨的说停，错位一秒就穿帮；无人声配乐片才可用纯合成包络\n- 与 spectrum-morph-ui 同片可共存（一个是功能一个是装饰），但别用同一种条形视觉——高度/颜色/圆角至少差一档，否则观众读作同一个东西乱入\n- demo 的玻璃拟态胶囊（backdropFilter blur 24px）是 raycast 暗场风格，亮色片子换成实底胶囊即可，声纹参数不变\n- 麦克风用的 emoji 加 grayscale 滤镜是占位——实战换 SVG 图标\n- 声音：本卡画面就是\"声音的形状\"，配人声最顺；提交钮一声轻 pop + 塌缩配短促下滑音\n\n## 参考实现\ndemos/interaction/voice-waveform-live/\n（VoiceWaveformLive.tsx）\n原片出处：raycast-teams 19.5–26.0s\n\n## Reference implementation (Remotion)\n\n- demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx =====\n// voice-waveform-live —— raycast-teams 19.5–26s：\n// 录音胶囊内实时声纹：细竖条随\"说话\"起伏（种子随机+相邻插值），\n// 说话时中部高耸、停顿缩成点线，波形从右往左滚动；右端提交钮。\n// 演：说(0.5–1.9s) → 停(1.9–2.7s) → 说(2.7–4.1s) → 提交(4.1–5s)。\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\n\nconst mulberry32 = (a: number) => () => {\n  let t = (a += 0x6d2b79f5);\n  t = Math.imul(t ^ (t >>> 15), t | 1);\n  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n};\n\n// 值噪声：整数采样点取种子随机值，采样点之间平滑插值\nconst noiseAt = (x: number) => {\n  const i = Math.floor(x);\n  const fr = x - i;\n  const a = mulberry32(i * 7919 + 13)();\n  const b = mulberry32((i + 1) * 7919 + 13)();\n  const s = fr * fr * (3 - 2 * fr); // smoothstep\n  return a + (b - a) * s;\n};\n\n// 说话包络（按\"声音发生时刻\"计）：说→停→说\nconst envelope = (t: number) => {\n  const seg = (a: number, b: number, rise = 5, fall = 7) =>\n    interpolate(t, [a, a + rise, b - fall, b], [0, 1, 1, 0], {\n      extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    });\n  // 两段说话内部再叠音节起伏\n  const talk = Math.max(seg(15, 57), seg(80, 124));\n  const syllable = 0.55 + 0.45 * noiseAt(t / 4.5 + 200);\n  return talk * syllable;\n};\n\nconst N_BARS = 64;\n\nexport const VoiceWaveformLive: React.FC = () => {\n  const f = useCurrentFrame();\n\n  // 提交动作\n  const submitAt = 126;\n  const submitted = f >= submitAt;\n  const btnPress = interpolate(f, [submitAt, submitAt + 3, submitAt + 9], [1, 0.82, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.ease),\n  });\n  // 提交后波形整体塌缩 + 胶囊微缩离场感\n  const collapse = interpolate(f, [submitAt, submitAt + 12], [1, 0.06], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.ease),\n  });\n  const capsuleScale = interpolate(f, [submitAt, submitAt + 20], [1, 0.96], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.ease),\n  });\n\n  // 胶囊入场\n  const inOp = interpolate(f, [0, 12], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.ease),\n  });\n  const inScale = interpolate(f, [0, 14], [1.04, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  const SCROLL = 1.6; // 帧→采样时间比：滚动速度\n\n  const bars = Array.from({ length: N_BARS }).map((_, i) => {\n    // 从右往左滚动：最右条是\"现在\"，越靠左越旧\n    const sampleT = f - (N_BARS - 1 - i) * SCROLL;\n    const env = sampleT < 0 ? 0 : envelope(sampleT);\n    // 空间权重：中部高耸\n    const center = Math.pow(Math.sin((i / (N_BARS - 1)) * Math.PI), 0.8);\n    const jitter = 0.35 + 0.65 * noiseAt(sampleT * 1.7 + i * 0.13);\n    const hRaw = env * center * jitter;\n    const h = Math.max(5, hRaw * 235 * collapse); // 静默=5px 点线\n    return h;\n  });\n\n  const nowEnv = envelope(f);\n  const micGlow = submitted ? 0 : nowEnv;\n\n  return (\n    <AbsoluteFill style={{ background: '#08080a', overflow: 'hidden' }}>\n      {/* 暗场绸缎底光 */}\n      <div style={{\n        position: 'absolute', left: -300, top: -200, width: 2600, height: 1700,\n        background: 'radial-gradient(closest-side, rgba(130,131,140,0.16), rgba(0,0,0,0) 70%)',\n        transform: `translate(${f * 0.6}px, ${f * 0.25}px)`,\n      }} />\n\n      <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>\n        <div\n          style={{\n            width: 1320, height: 300, borderRadius: 150,\n            opacity: inOp,\n            transform: `scale(${inScale * capsuleScale})`,\n            background:\n              'linear-gradient(180deg, rgba(255,255,255,0.5), rgba(255,255,255,0.08) 40%, rgba(0,0,0,0.3))',\n            padding: 2.5, boxSizing: 'border-box',\n          }}\n        >\n          <div\n            style={{\n              width: '100%', height: '100%', borderRadius: 148,\n              background: 'rgba(24,25,29,0.72)',\n              backdropFilter: 'blur(24px)',\n              boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.10), 0 40px 100px rgba(0,0,0,0.55)',\n              display: 'flex', alignItems: 'center', gap: 36,\n              padding: '0 44px', boxSizing: 'border-box',\n            }}\n          >\n            {/* 麦克风圆钮：说话时发亮 */}\n            <div\n              style={{\n                width: 96, height: 96, borderRadius: 48, flexShrink: 0,\n                background: `rgba(255,255,255,${0.08 + micGlow * 0.14})`,\n                border: '2.5px solid rgba(255,255,255,0.28)',\n                boxShadow: `0 0 ${28 * micGlow}px rgba(235,235,245,${micGlow * 0.5})`,\n                display: 'flex', alignItems: 'center', justifyContent: 'center',\n                fontSize: 46, filter: 'grayscale(1)', boxSizing: 'border-box',\n              }}\n            >\n              🎙️\n            </div>\n\n            {/* 声纹条区 */}\n            <div style={{\n              flex: 1, height: 244, display: 'flex', alignItems: 'center',\n              gap: 6, overflow: 'hidden',\n            }}>\n              {bars.map((h, i) => (\n                <div\n                  key={i}\n                  style={{\n                    flex: 1, height: h, borderRadius: 4,\n                    background: `rgba(240,240,248,${0.4 + (h / 235) * 0.6})`,\n                  }}\n                />\n              ))}\n            </div>\n\n            {/* 提交钮：白圆 + 上箭头 */}\n            <div\n              style={{\n                width: 96, height: 96, borderRadius: 48, flexShrink: 0,\n                background: submitted ? '#ffffff' : 'rgba(255,255,255,0.92)',\n                transform: `scale(${btnPress})`,\n                display: 'flex', alignItems: 'center', justifyContent: 'center',\n                boxShadow: submitted\n                  ? '0 0 60px rgba(255,255,255,0.55)'\n                  : '0 8px 24px rgba(0,0,0,0.4)',\n              }}\n            >\n              <svg width=\"44\" height=\"44\" viewBox=\"0 0 24 24\">\n                <path\n                  d=\"M12 20V5M12 5l-6.5 6.5M12 5l6.5 6.5\"\n                  stroke=\"#111114\" strokeWidth=\"3\" strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\" fill=\"none\"\n                />\n              </svg>\n            </div>\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}