UI/Components/Audio waveforms/Linear Waveform

Linear Waveform

A customizable linear audio waveform visualization component.

Animate a linear waveform, often used for audio visualizations.

0:00 / 0:10

Installation

CLI

npx shadcn@latest add "https://clippkit.com/r/linear-waveform"

Manual

Create a new file, for example, at src/components/clippkit/linear-waveform.tsx (or your preferred location) and paste the following code into it.

/**
 * Free Remotion Template Component
 * ---------------------------------
 * This template is free to use in your projects!
 * Credit appreciated but not required.
 *
 * Created by the team at https://www.reactvideoeditor.com
 *
 * Happy coding and building amazing videos! 🎉
 */
 
"use client";
 
import React, { useEffect, useState } from "react";
import {
  createSmoothSvgPath,
  MediaUtilsAudioData,
  visualizeAudioWaveform,
} from "@remotion/media-utils";
import { useCurrentFrame, useVideoConfig } from "remotion";
 
// Helper function to generate waveform samples
const generateWaveformSamples = (
  audioData: MediaUtilsAudioData | null | undefined,
  numberOfSamples: number,
  frame: number,
  waveSpeed: number,
  fps: number
): number[] => {
  if (audioData) {
    return visualizeAudioWaveform({
      fps,
      frame,
      audioData,
      numberOfSamples,
      windowInSeconds: 1 / fps, // Visualize a single frame's worth of audio
    });
  }
  return Array(numberOfSamples)
    .fill(0)
    .map((_, i) => {
      return (
        Math.sin(frame / waveSpeed + (i / numberOfSamples) * 2 * Math.PI) *
          0.5 +
        0.5
      );
    });
};
 
// Helper function to calculate SVG path from samples
const calculateSvgPath = (
  samples: number[],
  finalWidth: number,
  finalHeight: number,
  waveAmplitude: number,
  strokeWidth: number,
  numberOfSamples: number
): string => {
  const points = samples.map((sample, i) => {
    const x = (i / Math.max(1, numberOfSamples - 1)) * finalWidth;
    let y = Math.round((sample - 0.5) * waveAmplitude + finalHeight / 2);
    y = Math.max(strokeWidth / 2, Math.min(finalHeight - strokeWidth / 2, y));
    return { x, y };
  });
 
  if (points.length > 1) {
    return createSmoothSvgPath({ points }) as string;
  }
  if (points.length === 1) {
    return `M 0 ${points[0].y} L ${finalWidth} ${points[0].y}`;
  }
  return "";
};
 
interface LinearWaveformProps {
  audioData?: MediaUtilsAudioData | null;
  numberOfSamples?: number;
  strokeColor?: string;
  strokeWidth?: number;
  fillColor?: string;
  waveAmplitude?: number;
  waveSpeed?: number;
  containerStyle?: React.CSSProperties;
  height?: string | number;
  width?: string | number;
}
 
export function LinearWaveform({
  audioData,
  numberOfSamples = 64,
  strokeColor = "var(--foreground)",
  strokeWidth = 2,
  fillColor = "none",
  waveAmplitude = 100,
  waveSpeed = 10,
  containerStyle,
  height: propHeight,
  width: propWidth,
}: LinearWaveformProps) {
  const frame = useCurrentFrame();
  const { width: videoWidth, height: videoHeight, fps } = useVideoConfig();
 
  let computedWidth: number;
  if (typeof propWidth === "number") {
    computedWidth = propWidth;
  } else if (typeof propWidth === "string" && propWidth.endsWith("%")) {
    const percentage = parseFloat(propWidth.substring(0, propWidth.length - 1));
    computedWidth = !isNaN(percentage)
      ? (percentage / 100) * videoWidth
      : videoWidth;
  } else {
    computedWidth = videoWidth;
  }
 
  let computedHeight: number;
  if (typeof propHeight === "number") {
    computedHeight = propHeight;
  } else if (typeof propHeight === "string" && propHeight.endsWith("%")) {
    const percentage = parseFloat(
      propHeight.substring(0, propHeight.length - 1)
    );
    computedHeight = !isNaN(percentage)
      ? (percentage / 100) * videoHeight
      : videoHeight;
  } else {
    computedHeight = videoHeight;
  }
 
  const finalWidth = computedWidth;
  const finalHeight = computedHeight;
 
  const [svgPath, setSvgPath] = useState("");
 
  useEffect(() => {
    const waveformData = generateWaveformSamples(
      audioData,
      numberOfSamples,
      frame,
      waveSpeed,
      fps
    );
 
    const newPath = calculateSvgPath(
      waveformData,
      finalWidth,
      finalHeight,
      waveAmplitude,
      strokeWidth,
      numberOfSamples
    );
 
    setSvgPath(newPath);
  }, [
    frame,
    audioData,
    numberOfSamples,
    waveAmplitude,
    waveSpeed,
    finalWidth,
    finalHeight,
    fps,
    strokeWidth,
  ]);
 
  return (
    <div
      style={{
        width: finalWidth,
        height: finalHeight,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        overflow: "hidden",
        backgroundColor: "transparent",
        ...containerStyle,
      }}
    >
      <svg
        viewBox={`0 0 ${finalWidth} ${finalHeight}`}
        width={finalWidth}
        height={finalHeight}
        style={{
          width: finalWidth,
          height: finalHeight,
        }}
      >
        <path
          d={svgPath}
          stroke={strokeColor}
          strokeWidth={strokeWidth}
          fill={fillColor}
        />
      </svg>
    </div>
  );
}
Update the import paths in your Remotion compositions if you placed the file in a different location than shown in the usage examples.

Usage

Once the LinearWaveform component is added to your project (either via CLI or Manually), you can integrate it into your Remotion project by importing it and defining a Composition.

Prerequisite

Ensure you have a Remotion project set up. If not, please refer to the Remotion documentation to get started.

Project Structure Example

Here’s an example folder layout showing where to place the component and how it fits into a typical Remotion project

app/main.tsx
"use client";
 
import React from "react";
import { MediaUtilsAudioData, useAudioData } from "@remotion/media-utils";
import { Player } from "@remotion/player";
import { Audio } from "remotion"; // Import Audio
 
import LinearWaveform from "../components/linear-waveform";
 
// Media source for the demo
const MEDIA_SRC =
  "https://rwxrdxvxndclnqvznxfj.supabase.co/storage/v1/object/public/sounds//moon-landing.mp3";
 
// Define props for the new composition component
interface AudioWaveformCompositionProps {
  linearWaveformProps: Omit<
    React.ComponentProps<typeof LinearWaveform>,
    "audioData"
  > & { audioData?: MediaUtilsAudioData | null };
  mediaSrc: string;
}
 
// New component combining LinearWaveform and Audio
const AudioWaveformComposition: React.FC<AudioWaveformCompositionProps> = ({
  linearWaveformProps,
  mediaSrc,
}) => {
  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <LinearWaveform {...linearWaveformProps} />
      <Audio src={mediaSrc} />
    </div>
  );
};
 
export function LinearWaveformDemo() {
  const audioData = useAudioData(MEDIA_SRC);
 
  const linearWaveformProps = React.useMemo(
    () => ({
      numberOfSamples: 108,
      strokeColor: "var(--foreground)",
      strokeWidth: 2,
      fillColor: "none",
      waveAmplitude: 100,
      waveSpeed: 3,
      audioData,
      width: "50%",
    }),
    [audioData]
  );
 
  // Calculate duration in frames for the player for better readability
  const playerDurationInFrames = audioData
    ? Math.floor(audioData.durationInSeconds * 30)
    : 300;
 
  console.log("audioData", audioData);
  return (
    <Player
      component={AudioWaveformComposition} // Use the new component
      inputProps={{ linearWaveformProps, mediaSrc: MEDIA_SRC }} // Pass props directly for AudioWaveformComposition
      durationInFrames={playerDurationInFrames} // Use the pre-calculated duration
      compositionWidth={640}
      compositionHeight={360}
      fps={30}
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "transparent",
      }}
      controls
      loop
    />
  );
}

Define a Composition

In your Remotion project's entry file (commonly src/Root.tsx, src/index.tsx, app/main.tsx), import LinearWaveform and define a Composition.

app/main.tsx (or equivalent)
import { Player } from "@remotion/player";

import LinearWaveform from "@/components/clippkit/linear-waveform"; // Assuming you placed it in src/components/clippkit

export default function MyVideoComposition() {
  const waveformProps = {
    barCount: 50,
    barColor: "lightblue",
    waveAmplitude: 120,
    waveSpeed: 15,
  };

  return (
    <Player
      component={LinearWaveform}
      inputProps={waveformProps}
      durationInFrames={300}
      compositionWidth={640}
      compositionHeight={360}
      fps={30}
      style={{
        width: "100%",
        height: "100%",
      }}
      autoPlay
      controls
      loop
    />
  );
}

API Reference

The component exported as LinearWaveform (e.g., from apps/docs/registry/default/ui/linear-waveform.tsx or your project's component path) accepts the following props to customize its appearance and animation:

PropTypeDefault ValueDescription
barCountnumber40Number of bars in the waveform.
barWidthnumber12Width of each bar in pixels.
barGapnumber4Gap between each bar in pixels.
barColorstring"white"Color of the bars. Accepts any valid CSS color value.
barBorderRadiusstring"6px"Border radius of the bars.
waveAmplitudenumber100Maximum height of the wave.
waveSpeednumber10Speed of the wave animation. Higher value means slower animation.
randomnessnumber50Amount of random height variation applied to each bar. Set to 0 for no randomness.
heightstring | numberVideo config heightExplicit height for the waveform container. Overrides video config height.
widthstring | numberVideo config widthExplicit width for the waveform container. Overrides video config width.
containerStyleReact.CSSProperties{}Custom CSS styles for the main container div.
barStyleReact.CSSProperties{}Custom CSS styles for individual bars. Applied to each bar div.
barShadowstring"0 0 10px rgba(59, 130, 246, 0.6)"Box shadow style for each bar.
containerShadowstring"inset 0 0 100px rgba(59, 130, 246, 0.2)"Inner box shadow style for the container.
containerBackdropFilterstring"blur(8px)"Backdrop filter style for the container (e.g., for a frosted glass effect).

On this page