---
title: "How to add a waveform progress bar with Claude (tested prompt and script)"
description: "The whole soundtrack sits under the bottle as a row of white bars, tall on each drum hit, that turn pink from left to right as the track plays. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/waveform-progress-bar
---

# How to add a waveform progress bar with Claude

The whole soundtrack sits under the bottle as a row of white bars, tall on each drum hit, that turn pink from left to right as the track plays.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Add a waveform progress bar to input.mp4
   Number of waveform bars: 48
   Where the panel sits (0-1 down the frame) and its height as a share of the frame: 0.87, 0.10
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Add a waveform progress bar to **@clip1.mp4**, put a sale badge on **@clip2.mp4**, give **@clip3.mp4** a VHS look, then join all three with crossfades.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting      | Default           | What it does                                                                      |
| ------------ | ----------------- | --------------------------------------------------------------------------------- |
| `BARS`       | `48`              | Number of waveform bars. More bars show more detail but each one gets thinner.    |
| `Y, PANEL_H` | `0.87, 0.10`      | Where the panel sits (0-1 down the frame) and its height as a share of the frame. |
| `PLAYED`     | `(255, 70, 150)`  | Color of the bars already played.                                                 |
| `UNPLAYED`   | `(240, 240, 245)` | Color of the bars still to come.                                                  |
| `SHADE`      | `150`             | Opacity (0-255) of the dark panel behind the bars. 0 removes it.                  |
| `MIN_BAR`    | `0.12`            | Shortest bar as a share of the tallest, so quiet parts still show.                |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add a waveform progress bar to input.mp4 (540x960, 30fps, 6 seconds, with a drum track). Read the whole soundtrack as mono, split it into 48 equal slices in time, and take each slice's peak absolute sample divided by the loudest sample in the clip, then its square root, with a floor of 0.12. Near the bottom draw a dark rounded panel: black at 150/255 opacity, from 5% to 95% across the frame, 10% of the frame height tall, centered 87% down, corner radius a quarter of its height. Inside it draw the 48 values as vertical bars with fully rounded ends, evenly spaced from 9% to 91% across the frame (each bar 60% of its slot wide), centered vertically on the panel's center, each reaching value x 36% of the panel height above and below the center. Bars still to come are (240, 240, 245); the playhead runs from 9% to 91% across over the full length of the video (at 9% on the first frame, at 91% on the last), and everything left of it is drawn in pink (255, 70, 150) instead, so a bar can be part pink at the playhead. The panel is there from the first frame and nothing else in the frame changes. Use Python with numpy and Pillow to read the audio and draw it and ffmpeg to put it on the video (don't use ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `waveform-progress-bar.py` and run:

```
python3 waveform-progress-bar.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Waveform progress bar: the whole soundtrack drawn as a row of bars that turn pink from left to right as it plays.

Usage: python3 waveform-progress-bar.py input.mp4 output.mp4  (the input needs an audio track)
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import math
import subprocess
import sys

import numpy as np
from PIL import Image, ImageDraw

BARS = 48                    # number of waveform bars across the panel
Y = 0.87                     # panel center (0-1 down the frame)
PANEL_H = 0.10               # panel height as a share of the frame height
PANEL_X = (0.05, 0.95)       # panel left and right edges (0-1 across the frame)
BARS_X = (0.09, 0.91)        # where the first bar starts and the last bar ends
SHADE = 150                  # opacity (0-255) of the dark rounded panel
PLAYED = (255, 70, 150)      # color of the bars already played
UNPLAYED = (240, 240, 245)   # color of the bars still to come
MIN_BAR = 0.12               # shortest bar as a share of the tallest, so quiet parts still show
RATE = 48000                 # audio samples per second read from the input

def audio(path):
    """The soundtrack as mono floats from -1 to 1."""
    raw = subprocess.check_output(["ffmpeg", "-v", "error", "-i", path, "-f", "s16le", "-ac", "1", "-ar", str(RATE), "-"])
    return np.frombuffer(raw, np.int16).astype(np.float32) / 32768

def bar_heights(sound):
    """Peak loudness (MIN_BAR-1) of each of the BARS equal slices of the whole track."""
    peaks = np.array([np.abs(part).max() if len(part) else 0 for part in np.array_split(sound, BARS)])
    return np.maximum(np.sqrt(peaks / max(peaks.max(), 1e-6)), MIN_BAR)   # square root lifts quieter hits

def bars(color, heights):
    """The panel with every bar drawn in one color."""
    img = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(img)
    ph = round(h * PANEL_H)
    top = round(h * Y - ph / 2)
    draw.rounded_rectangle([w * PANEL_X[0], top, w * PANEL_X[1], top + ph], radius=ph // 4, fill=(0, 0, 0, SHADE))
    slot = w * (BARS_X[1] - BARS_X[0]) / BARS
    bw = slot * 0.6
    for i, v in enumerate(heights):
        x = w * BARS_X[0] + i * slot + (slot - bw) / 2
        half = v * ph * 0.36
        draw.rounded_rectangle([x, h * Y - half, x + bw, h * Y + half], radius=bw / 2, fill=color)
    return img

def overlay(t):
    """The unplayed panel with the played bars pasted over it up to the playhead."""
    img = unplayed.copy()
    edge = round(w * (BARS_X[0] + (BARS_X[1] - BARS_X[0]) * min(t / duration, 1)))
    if edge > 0:
        img.paste(played.crop((0, 0, edge, h)), (0, 0))
    return img

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])

def render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
heights = bar_heights(audio(src))
unplayed, played = bars(UNPLAYED, heights), bars(PLAYED, heights)
render(src, out)
```

## More music-synced visuals

- [AI voice orb **AI voice orb**](https://www.recharm.com/effects/ai-voice-orb)
- [Audio-reactive blob **Audio-reactive blob**](https://www.recharm.com/effects/audio-reactive-blob)
- [Audio-reactive particles **Audio-reactive particles**](https://www.recharm.com/effects/audio-reactive-particles)
- [Audio-reactive text **Audio-reactive text**](https://www.recharm.com/effects/audio-reactive-text)
- [Audio waveform **Audio waveform**](https://www.recharm.com/effects/audio-waveform)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
