How to add a shockwave rings on beat with Claude

On every kick drum a thin white ring with a blue glow bursts out of the bottle and races to the edges of the frame, rippling the picture as it passes, and is gone just before the next kick fires another.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. Install the skill (once)

    Show the prompt
    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 shockwave rings on beat to input.mp4
    Where the rings start, 0-1 across and down the frame: 0.5, 0.5
    Seconds a ring takes to reach full size and fade out: 0.45

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 shockwave rings on beat 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.

SettingDefaultWhat it does
X, Y0.5, 0.5Where the rings start, 0-1 across and down the frame.
LIFE0.45Seconds a ring takes to reach full size and fade out. Keep it under the time between kicks for clean gaps.
START_SIZE0.06Ring radius at the kick, as a share of the frame width.
REACH0.95Ring radius when it fades out, as a share of the frame width.
THICK9, 3Ring line width in pixels, at the kick and when it fades out.
COLOR(255, 255, 255)Ring color.
GLOW(70, 170, 255)Color of the soft glow around the ring.
GLOW_BLUR9Glow blur radius in pixels.
BEND12Furthest the ring pushes the picture outward, in pixels. 0 turns the ripple off.
KICK_HZ150The bass band, in Hz, that tells a kick from a snare or hi-hat.
THRESHOLD0.5An onset counts as a kick when the bass reaches this share of its loudest.

Without the skill

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

Show the prompt
Add shockwave rings on the beat to input.mp4 (the music is its own audio track). Find the kick drums with librosa: load the audio mono at 22050 Hz, put 0.1 seconds of silence in front so a kick at 0 seconds is found too, run librosa.onset.onset_detect with hop_length 256 and backtrack=True, and keep only the onsets where the bass (the STFT magnitude summed at 150 Hz and below, divided by its loudest value) reaches 0.5 within the next 8 hops; subtract the 0.1 seconds again. On each kick a ring starts at the center of the frame (50% across, 50% down) and lives 0.45 seconds. With p = seconds since the kick / 0.45, its radius is the frame width times 0.06 + 0.89 * (1 - (1 - p)^3) (so it bursts out and eases), its line width goes from 9 pixels to 3 pixels, and its opacity is (1 - p)^1.5. Draw each ring with Pillow as a white (255, 255, 255) circle outline over a glow: a (70, 170, 255) outline three times as wide, blurred with a 9-pixel Gaussian blur. Before drawing the rings, bend the picture with OpenCV (cv2.remap, reflect at the borders): for each pixel at distance r from the center, let d = (r - radius) / (4 * line width) and move the picture outward by 12 * opacity * 2.33 * d * exp(-d^2) pixels, summed over the rings alive in that frame. Frames with no ring alive stay untouched, so the picture is clean just before every kick. Work frame by frame at 30fps, keep the size and the audio, and save it as output.mp4 (H.264, yuv420p).

Run it without Claude

This is the exact script we tested. Save it as shockwave-rings-on-beat.py and run:

python3 shockwave-rings-on-beat.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Shockwave rings on beat: librosa finds every kick drum, and on each one a thin white ring bursts out of the bottle, bending the picture as it races outward and fades.

Usage: python3 shockwave-rings-on-beat.py input.mp4 output.mp4  (the music is the input's audio track)
Needs ffmpeg, librosa to find the kicks, Pillow to draw the rings and OpenCV to bend the picture
(pip install librosa pillow opencv-python-headless numpy).
"""
import json
import subprocess
import sys
import tempfile

import cv2
import librosa
import numpy as np
from PIL import Image, ImageDraw, ImageFilter

X, Y = 0.5, 0.5             # where the rings start (0-1 across and down the frame)
LIFE = 0.45                 # seconds a ring takes to reach full size and fade out
START_SIZE = 0.06           # ring radius at the kick, as a share of the frame width
REACH = 0.95                # ring radius when it fades out, as a share of the frame width
THICK = 9, 3                # ring line width in pixels, at the kick and when it fades out
COLOR = (255, 255, 255)     # ring color
GLOW = (70, 170, 255)       # color of the soft glow around the ring
GLOW_BLUR = 9               # glow blur radius in pixels
BEND = 12                   # furthest the ring pushes the picture outward, in pixels
KICK_HZ = 150               # the bass band, in Hz, that tells a kick from a snare or hi-hat
THRESHOLD = 0.5             # an onset counts as a kick when the bass reaches this share of its loudest
FPS = 30


def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height", "-of", "json", path]))
    return info["streams"][0]["width"], info["streams"][0]["height"]


def kick_times(path):
    """Seconds of each kick drum: every onset librosa finds in the music where the bass then hits hard."""
    with tempfile.NamedTemporaryFile(suffix=".wav") as wav:
        subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", path, "-vn", "-ac", "1", "-ar", "22050", wav.name], check=True)
        y, sr = librosa.load(wav.name, sr=None)
    hop, lead = 256, 0.1  # analysis step in samples (about 12ms); silence added in front so a kick at 0s is found too
    y = np.concatenate([np.zeros(round(lead * sr), np.float32), y])
    spec = np.abs(librosa.stft(y, hop_length=hop))
    bass = spec[librosa.fft_frequencies(sr=sr) <= KICK_HZ].sum(axis=0)
    bass /= bass.max()
    onsets = librosa.onset.onset_detect(y=y, sr=sr, hop_length=hop, backtrack=True)
    kicks = [f for f in onsets if bass[f:f + 8].max() >= THRESHOLD]   # loud bass within 0.1s of the onset
    return librosa.frames_to_time(kicks, sr=sr, hop_length=hop) - lead


def frames(path):
    """Every frame of the clip at 30fps as an RGB array."""
    ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf", f"fps={FPS}", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                          stdout=subprocess.PIPE)
    while True:
        chunk = ff.stdout.read(w * h * 3)
        if len(chunk) < w * h * 3:
            return
        yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3)


def rings_at(t):
    """(radius, line width, strength) of every ring alive at t: each eases out fast and fades as it grows."""
    live = []
    for kick in kicks:
        p = (t - kick) / LIFE
        if 0 <= p < 1:
            grow = 1 - (1 - p) ** 3
            radius = w * (START_SIZE + (REACH - START_SIZE) * grow)
            live.append((radius, THICK[0] + (THICK[1] - THICK[0]) * p, (1 - p) ** 1.5))
    return live


def bend(img, rings):
    """Push the picture outward in a narrow band around each ring, like a pressure wave passing through."""
    push = np.zeros_like(r)
    for radius, width, fade in rings:
        d = (r - radius) / (width * 4)
        push += BEND * fade * 2.33 * d * np.exp(-d ** 2)   # 2.33 makes the largest push exactly BEND
    return cv2.remap(img, xs - push * ux, ys - push * uy, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)


def draw_rings(rings):
    """The rings as an RGBA layer: a thin bright line over a blurred, tinted glow."""
    glow, line = Image.new("RGBA", (w, h), GLOW + (0,)), Image.new("RGBA", (w, h))
    gd, ld = ImageDraw.Draw(glow), ImageDraw.Draw(line)
    for radius, width, fade in rings:
        box = [cx - radius, cy - radius, cx + radius, cy + radius]
        gd.ellipse(box, outline=GLOW + (round(255 * fade),), width=round(width * 3))
        ld.ellipse(box, outline=COLOR + (round(255 * fade),), width=round(width))
    layer = glow.filter(ImageFilter.GaussianBlur(GLOW_BLUR))
    layer.alpha_composite(line)
    return layer


src, out = sys.argv[1:3]
w, h = probe(src)
kicks = kick_times(src)
cx, cy = X * w, Y * h
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
r = np.hypot(xs - cx, ys - cy)
ux, uy = (xs - cx) / np.maximum(r, 1), (ys - cy) / np.maximum(r, 1)
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                       "-r", str(FPS), "-i", "-", "-i", src, "-map", "0:v", "-map", "1:a?",
                       "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out],
                      stdin=subprocess.PIPE)
for i, img in enumerate(frames(src)):
    rings = rings_at(i / FPS)
    if rings:
        frame = Image.fromarray(bend(img, rings)).convert("RGBA")
        frame.alpha_composite(draw_rings(rings))
        img = np.asarray(frame.convert("RGB"))
    ff.stdin.write(img.tobytes())
ff.stdin.close()
if ff.wait():
    sys.exit("ffmpeg failed")

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