StealThis .dev
Remotion Medium

Remotion — Audiogram Waveform Clip

A cinematic podcast audiogram rendered with Remotion at 1920x1080 30fps — 80 vertical waveform bars pulse rhythmically using stacked sine waves to simulate natural speech energy, flanked by a gradient-ringed avatar, bold episode title, guest byline, and a live-updating progress bar with playhead dot that fills from 0 to 100 percent over a six-second clip.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import React from "react";
import {
  AbsoluteFill,
  Composition,
  Easing,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

// ── Constants ─────────────────────────────────────────────────────────────────
const BAR_COUNT = 80;
const SHOW_NAME = "The Signal Podcast";
const EPISODE_TITLE = "EP 24 · The Future of Sound";
const GUEST_NAME = "Maya Chen";
const GUEST_INITIALS = "MC";
const TOTAL_DURATION_S = 6;

const C = {
  bg: "#0a0a0f",
  surface: "#12121a",
  surface2: "#1e1e2e",
  accent: "#a855f7",
  accent2: "#06b6d4",
  accent3: "#ec4899",
  gold: "#f59e0b",
  text: "#f1f5f9",
  muted: "#94a3b8",
} as const;

// ── Audio simulation ──────────────────────────────────────────────────────────
// Stacks multiple sine waves to produce realistic speech-like energy per bar
function simulateBarHeight(
  barIndex: number,
  frame: number,
  totalBars: number
): number {
  const norm = barIndex / totalBars; // 0..1
  // Center emphasis: bars near the middle are naturally taller
  const centerBoost = 1 - Math.abs(norm - 0.5) * 1.6;

  // Wave 1 — slow rhythm (beat)
  const w1 = Math.sin(frame * 0.18 + barIndex * 0.31) * 0.35;
  // Wave 2 — mid frequency (syllables)
  const w2 = Math.sin(frame * 0.42 + barIndex * 0.67 + 1.2) * 0.25;
  // Wave 3 — high frequency (texture)
  const w3 = Math.sin(frame * 0.91 + barIndex * 1.13 + 0.8) * 0.15;
  // Wave 4 — very low (breath)
  const w4 = Math.sin(frame * 0.07 + barIndex * 0.19 + 2.5) * 0.20;
  // Wave 5 — unique per-bar micro-variation
  const w5 = Math.sin(frame * 0.55 + barIndex * 2.37 + barIndex * 0.1) * 0.12;

  const raw = 0.15 + w1 + w2 + w3 + w4 + w5;
  const clamped = Math.max(0.05, Math.min(1, raw));
  return clamped * Math.max(0.1, centerBoost);
}

// ── Radial glow background ─────────────────────────────────────────────────
const RadialGlow: React.FC = () => {
  const frame = useCurrentFrame();
  const { width, height } = useVideoConfig();

  // Slowly breathe the glow opacity
  const glowPulse = interpolate(
    Math.sin(frame * 0.05),
    [-1, 1],
    [0.18, 0.32]
  );

  return (
    <div
      style={{
        position: "absolute",
        inset: 0,
        background: `radial-gradient(ellipse 70% 55% at 50% 62%, rgba(168,85,247,${glowPulse}) 0%, rgba(6,182,212,0.04) 55%, transparent 80%)`,
        pointerEvents: "none",
      }}
    />
  );
};

// ── Avatar / show logo ─────────────────────────────────────────────────────
const ShowAvatar: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const scale = spring({
    frame,
    fps,
    config: { damping: 14, stiffness: 120, mass: 0.8 },
    durationInFrames: 30,
  });

  const opacity = interpolate(frame, [0, 12], [0, 1], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.quad),
  });

  const AVATAR_SIZE = 100;

  return (
    <div
      style={{
        opacity,
        transform: `scale(${scale})`,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      {/* Outer gradient ring */}
      <div
        style={{
          width: AVATAR_SIZE + 8,
          height: AVATAR_SIZE + 8,
          borderRadius: "50%",
          background: "linear-gradient(135deg, #a855f7, #06b6d4, #ec4899)",
          padding: 3,
          boxShadow: "0 0 28px rgba(168,85,247,0.65), 0 0 56px rgba(168,85,247,0.25)",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        {/* Inner avatar circle */}
        <div
          style={{
            width: AVATAR_SIZE,
            height: AVATAR_SIZE,
            borderRadius: "50%",
            background: `linear-gradient(145deg, ${C.surface2}, #2a1f3d)`,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            border: `2px solid ${C.surface}`,
          }}
        >
          <span
            style={{
              fontFamily: "Inter, sans-serif",
              fontWeight: 700,
              fontSize: 32,
              color: C.text,
              letterSpacing: -1,
              background: "linear-gradient(135deg, #a855f7, #ec4899)",
              WebkitBackgroundClip: "text",
              WebkitTextFillColor: "transparent",
            }}
          >
            {GUEST_INITIALS}
          </span>
        </div>
      </div>
    </div>
  );
};

// ── Episode info (title + guest) ───────────────────────────────────────────
const EpisodeInfo: React.FC<{ frame: number }> = ({ frame }) => {
  const titleOpacity = interpolate(frame, [8, 22], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });
  const titleY = interpolate(frame, [8, 22], [16, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.quad),
  });

  const guestOpacity = interpolate(frame, [14, 28], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });
  const guestY = interpolate(frame, [14, 28], [12, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.quad),
  });

  const showOpacity = interpolate(frame, [4, 16], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: 6,
        textAlign: "center",
      }}
    >
      {/* Show name (small badge) */}
      <div
        style={{
          opacity: showOpacity,
          display: "flex",
          alignItems: "center",
          gap: 6,
          background: "rgba(168,85,247,0.15)",
          border: "1px solid rgba(168,85,247,0.3)",
          borderRadius: 20,
          padding: "4px 14px",
        }}
      >
        <div
          style={{
            width: 6,
            height: 6,
            borderRadius: "50%",
            background: C.accent3,
            boxShadow: `0 0 8px ${C.accent3}`,
          }}
        />
        <span
          style={{
            fontFamily: "Inter, sans-serif",
            fontWeight: 600,
            fontSize: 13,
            color: C.accent,
            letterSpacing: 1.2,
            textTransform: "uppercase" as const,
          }}
        >
          {SHOW_NAME}
        </span>
      </div>

      {/* Episode title */}
      <div
        style={{
          opacity: titleOpacity,
          transform: `translateY(${titleY}px)`,
        }}
      >
        <span
          style={{
            fontFamily: "Inter, sans-serif",
            fontWeight: 800,
            fontSize: 38,
            color: C.text,
            letterSpacing: -1,
            lineHeight: 1.1,
          }}
        >
          {EPISODE_TITLE}
        </span>
      </div>

      {/* Guest name */}
      <div
        style={{
          opacity: guestOpacity,
          transform: `translateY(${guestY}px)`,
        }}
      >
        <span
          style={{
            fontFamily: "Inter, sans-serif",
            fontWeight: 400,
            fontSize: 18,
            color: C.muted,
            letterSpacing: 0.3,
          }}
        >
          with{" "}
          <span style={{ color: C.text, fontWeight: 500 }}>{GUEST_NAME}</span>
        </span>
      </div>
    </div>
  );
};

// ── Waveform bar ───────────────────────────────────────────────────────────
const WaveBar: React.FC<{
  index: number;
  frame: number;
  fps: number;
  totalBars: number;
  barWidth: number;
  gap: number;
  maxBarHeight: number;
}> = ({ index, frame, fps, totalBars, barWidth, gap, maxBarHeight }) => {
  // Staggered entrance — each bar grows in with a spring offset by index
  const entranceDelay = Math.floor(index * 0.22);
  const entranceSpring = spring({
    frame: Math.max(0, frame - entranceDelay),
    fps,
    config: { damping: 18, stiffness: 180, mass: 0.5 },
    durationInFrames: 22,
  });

  const audioHeight = simulateBarHeight(index, frame, totalBars);
  const barHeight = Math.max(4, audioHeight * maxBarHeight * entranceSpring);

  // Gradient: center bars lean purple, edges lean cyan
  const norm = index / totalBars;
  const centerDist = Math.abs(norm - 0.5) * 2; // 0 at center, 1 at edges
  const r1 = Math.round(168 + (6 - 168) * centerDist);
  const g1 = Math.round(85 + (182 - 85) * centerDist);
  const b1 = Math.round(247 + (212 - 247) * centerDist);
  const barColor = `rgb(${r1},${g1},${b1})`;

  const glowOpacity = 0.3 + audioHeight * 0.5;

  return (
    <div
      style={{
        width: barWidth,
        height: barHeight,
        background: `linear-gradient(180deg, ${barColor} 0%, rgba(168,85,247,0.4) 100%)`,
        borderRadius: barWidth / 2,
        flexShrink: 0,
        marginLeft: index === 0 ? 0 : gap / 2,
        marginRight: gap / 2,
        boxShadow: `0 0 ${6 + audioHeight * 12}px rgba(${r1},${g1},${b1},${glowOpacity})`,
        alignSelf: "flex-end",
      }}
    />
  );
};

// ── Waveform container ─────────────────────────────────────────────────────
const Waveform: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const { width } = useVideoConfig();
  const waveformWidth = Math.min(width * 0.8, 1200);
  const totalGapSpace = BAR_COUNT * 3;
  const barWidth = Math.floor((waveformWidth - totalGapSpace) / BAR_COUNT);
  const gap = 3;
  const maxBarHeight = 200;

  const containerOpacity = interpolate(frame, [0, 5], [0, 1], {
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        opacity: containerOpacity,
        display: "flex",
        flexDirection: "row",
        alignItems: "flex-end",
        justifyContent: "center",
        width: waveformWidth,
        height: maxBarHeight + 16,
        padding: "8px 0",
      }}
    >
      {Array.from({ length: BAR_COUNT }, (_, i) => (
        <WaveBar
          key={i}
          index={i}
          frame={frame}
          fps={fps}
          totalBars={BAR_COUNT}
          barWidth={Math.max(4, barWidth)}
          gap={gap}
          maxBarHeight={maxBarHeight}
        />
      ))}
    </div>
  );
};

// ── Progress bar ───────────────────────────────────────────────────────────
const ProgressBar: React.FC<{ frame: number; durationInFrames: number; fps: number }> = ({
  frame,
  durationInFrames,
  fps,
}) => {
  const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const totalSeconds = Math.floor(durationInFrames / fps);
  const elapsedSeconds = Math.min(totalSeconds, Math.floor((frame / fps)));

  const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;

  const barOpacity = interpolate(frame, [20, 35], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.quad),
  });

  return (
    <div
      style={{
        opacity: barOpacity,
        width: "100%",
        maxWidth: 860,
        display: "flex",
        flexDirection: "column",
        gap: 10,
      }}
    >
      {/* Track */}
      <div
        style={{
          width: "100%",
          height: 4,
          background: "rgba(255,255,255,0.1)",
          borderRadius: 2,
          overflow: "hidden",
          position: "relative",
        }}
      >
        {/* Fill */}
        <div
          style={{
            position: "absolute",
            left: 0,
            top: 0,
            height: "100%",
            width: `${progress * 100}%`,
            background: "linear-gradient(90deg, #a855f7, #06b6d4)",
            borderRadius: 2,
            boxShadow: "0 0 10px rgba(168,85,247,0.7)",
          }}
        />
        {/* Playhead dot */}
        <div
          style={{
            position: "absolute",
            top: "50%",
            left: `${progress * 100}%`,
            transform: "translate(-50%, -50%)",
            width: 12,
            height: 12,
            borderRadius: "50%",
            background: "#ffffff",
            boxShadow: "0 0 8px rgba(168,85,247,0.9)",
          }}
        />
      </div>

      {/* Time labels */}
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          fontFamily: "Inter, sans-serif",
          fontSize: 13,
          color: C.muted,
          fontWeight: 500,
          letterSpacing: 0.5,
          fontVariantNumeric: "tabular-nums",
        }}
      >
        <span>{fmt(elapsedSeconds)}</span>
        <span style={{ color: "rgba(148,163,184,0.5)" }}>{fmt(totalSeconds)}</span>
      </div>
    </div>
  );
};

// ── Decorative corner lines ────────────────────────────────────────────────
const CornerAccent: React.FC<{
  x: "left" | "right";
  y: "top" | "bottom";
  frame: number;
}> = ({ x, y, frame }) => {
  const opacity = interpolate(frame, [25, 40], [0, 0.35], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        [x]: 60,
        [y]: 60,
        width: 80,
        height: 80,
        borderTop: y === "top" ? "1px solid rgba(168,85,247,0.5)" : "none",
        borderBottom: y === "bottom" ? "1px solid rgba(168,85,247,0.5)" : "none",
        borderLeft: x === "left" ? "1px solid rgba(168,85,247,0.5)" : "none",
        borderRight: x === "right" ? "1px solid rgba(168,85,247,0.5)" : "none",
        opacity,
      }}
    />
  );
};

// ── Live badge ─────────────────────────────────────────────────────────────
const LiveBadge: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const scale = spring({
    frame: Math.max(0, frame - 30),
    fps,
    config: { damping: 12, stiffness: 200, mass: 0.6 },
    durationInFrames: 20,
  });

  // Pulsing dot
  const dotOpacity = interpolate(
    Math.sin(frame * 0.2),
    [-1, 1],
    [0.4, 1]
  );

  return (
    <div
      style={{
        transform: `scale(${scale})`,
        position: "absolute",
        top: 56,
        right: 120,
        display: "flex",
        alignItems: "center",
        gap: 6,
        background: "rgba(236,72,153,0.15)",
        border: "1px solid rgba(236,72,153,0.4)",
        borderRadius: 20,
        padding: "5px 14px",
        boxShadow: "0 0 14px rgba(236,72,153,0.2)",
      }}
    >
      <div
        style={{
          width: 7,
          height: 7,
          borderRadius: "50%",
          background: C.accent3,
          boxShadow: `0 0 8px ${C.accent3}`,
          opacity: dotOpacity,
        }}
      />
      <span
        style={{
          fontFamily: "Inter, sans-serif",
          fontWeight: 700,
          fontSize: 11,
          color: C.accent3,
          letterSpacing: 2,
          textTransform: "uppercase" as const,
        }}
      >
        Now Playing
      </span>
    </div>
  );
};

// ── Main composition ───────────────────────────────────────────────────────
export const AudiogramWaveformClip: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps, width, height, durationInFrames } = useVideoConfig();

  return (
    <AbsoluteFill
      style={{
        background: C.bg,
        fontFamily: "Inter, sans-serif",
        overflow: "hidden",
      }}
    >
      {/* Subtle grid overlay */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          backgroundImage:
            "linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px)",
          backgroundSize: "80px 80px",
          pointerEvents: "none",
        }}
      />

      {/* Radial purple glow */}
      <RadialGlow />

      {/* Corner accents */}
      <CornerAccent x="left" y="top" frame={frame} />
      <CornerAccent x="right" y="top" frame={frame} />
      <CornerAccent x="left" y="bottom" frame={frame} />
      <CornerAccent x="right" y="bottom" frame={frame} />

      {/* Live badge */}
      <LiveBadge frame={frame} fps={fps} />

      {/* Main content — centered column */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          gap: 32,
          paddingTop: 20,
          paddingBottom: 60,
        }}
      >
        {/* Avatar */}
        <ShowAvatar frame={frame} fps={fps} />

        {/* Episode info */}
        <EpisodeInfo frame={frame} />

        {/* Waveform visualization */}
        <Waveform frame={frame} fps={fps} />

        {/* Progress bar */}
        <ProgressBar frame={frame} durationInFrames={durationInFrames} fps={fps} />
      </div>

      {/* Bottom branding strip */}
      <div
        style={{
          position: "absolute",
          bottom: 0,
          left: 0,
          right: 0,
          height: 3,
          background: "linear-gradient(90deg, #a855f7, #06b6d4, #ec4899)",
          opacity: 0.7,
        }}
      />
    </AbsoluteFill>
  );
};

// ── Composition config (required export) ──────────────────────────────────
export const compositionConfig = {
  id: "remotion-audiogram",
  component: AudiogramWaveformClip,
  durationInFrames: 180,
  fps: 30,
  width: 1920,
  height: 1080,
};

// ── Remotion Root ─────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
  <Composition
    id="remotion-audiogram"
    component={AudiogramWaveformClip}
    durationInFrames={180}
    fps={30}
    width={1920}
    height={1080}
  />
);

Audiogram Waveform Clip

This Remotion composition renders a social-media-ready podcast audiogram at full 1920×1080 resolution. The centerpiece is a bank of 80 vertical bars whose heights are driven by five stacked sine waves at distinct frequencies and phases — producing organic, speech-like energy patterns that peak in the center and taper toward the edges. Each bar enters with a staggered spring animation in the first 20 frames, growing upward from zero height before settling into continuous rhythmic motion. Bar colors shift smoothly from purple at the center to cyan at the extremes, with a per-bar glow shadow that brightens proportionally to the simulated audio energy.

Above the waveform sits a circular avatar placeholder with a three-stop gradient ring (purple → cyan → pink), a glowing initials monogram, a pill-shaped show-name badge, and a bold episode title that slides up with a cubic ease. A pulsing “Now Playing” badge in the top-right corner adds broadcast authenticity. Below the waveform, a thin gradient track bar fills from left to right over the clip’s six-second duration, with a white playhead dot and elapsed / total time labels rendered in tabular numerals. The dark #0a0a0f background is dressed with a breathing radial purple glow, a subtle grid overlay, and four corner bracket accents that fade in mid-clip.

The entire composition is self-contained TypeScript — no audio file, no external dependencies. All waveform data is computed per-frame with Math.sin, and all motion uses Remotion’s spring() and interpolate() primitives. The design is fully customizable: swap SHOW_NAME, EPISODE_TITLE, GUEST_NAME, and GUEST_INITIALS at the top of the file to brand the clip for any podcast.

Simulated audio data — waveform values are generated mathematically. No real audio file is required.