Get this effect
Switch to Code in your Claude Desktop App
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/skillsAsk for the effect
Add a falling notes piano roll to input.mp4 Melody: 8 notes, B4 to G5
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 falling notes piano roll 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 |
|---|---|---|
MELODY | 8 notes, B4 to G5 | Right-hand notes as (beat number, MIDI note, length in beats). |
BASS | C4, G4 | Left-hand notes, one per bar. |
LOWEST | 60 | MIDI number of the leftmost key (60 = middle C). |
OCTAVES | 2 | Keyboard length in octaves. |
KEYS_TOP | 0.87 | Top of the keyboard, where notes land, 0-1 down the frame. |
LANE_TOP | 0.42 | Where falling notes first appear, 0-1 down the frame. |
FALL_TIME | 1.2 | Seconds a note takes to fall onto the keys. |
MELODY_COLOR, BASS_COLOR | (255, 196, 40), (255, 70, 150) | Right-hand and left-hand note colors. |
SHADE | 0.75 | Opacity of the dark fade behind the falling notes. |
PIANO_VOLUME | 0.35 | Loudness of the piano notes mixed over the soundtrack (0 = silent). |
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 a falling-notes piano roll (the Synthesia look) to input.mp4 (540x960, 30fps, 6 seconds, a 120 BPM drum loop), with notes landing on the beats listed in beats.json ({"beats": [0.0, 0.5, 1.0, ...]}; beat number n is the n-th time in that list, counting from 0). Along the bottom draw a two-octave piano keyboard, C4 (MIDI 60) to B5 (MIDI 83): 14 white keys spanning from 3% to 97% across the frame, their tops 87% of the way down (y=835) and 10% of the frame height long (96 pixels), pure white (255, 255, 255) with a 1-pixel dark gray (40, 40, 45) outline and 3-pixel rounded corners; black keys (25, 25, 30) 60% as wide as a white key and 62% as long, centered on the line between their two white keys. Draw a 4-pixel deep red (150, 20, 35) strip along the top edge of the keyboard. Behind the notes, darken the frame from 37% of the way down to the bottom with a vertical fade from transparent to (8, 8, 14) at 75% opacity, reaching full strength about 60% of the way down that fade. The notes, as (beat number, MIDI note, length in beats): right hand (3, 72, 1), (4, 76, 1), (5, 79, 1), (6, 78, 1), (7, 74, 1), (8, 71, 1), (9, 75, 1), (10, 79, 2) in amber yellow (255, 196, 40); left hand (4, 60, 4), (8, 67, 4) in pink (255, 70, 150). Each note sounds for 92% of its length (one beat = 0.5 s, so a 1-beat note lasts 0.46 s). Notes on black keys use their color at 80% brightness. Draw each note as a rounded rectangle (5-pixel corner radius) as wide as its key minus 2 pixels on each side, falling straight down at a constant speed: it first appears at 42% down the frame (y=403) 1.2 seconds before it plays, its bottom edge reaches the top of the keyboard exactly when the note starts, and its length in pixels is its duration times that speed (360 pixels per second). Nothing is drawn above y=403: clip the bars there. While a note is playing its bar sits on the keyboard and shrinks from the top until it ends, and its key is filled with the note's color instead of white or black. Add a 10-pixel blurred glow behind the bars. Before 0.3 seconds no note is on screen, and leave the top 36% of the frame untouched. Also mix a simple piano tone for each note into the soundtrack (harmonics 1 to 4 at amplitudes 1/k^2, a 5 ms attack, exponential decay exp(-3t), and a 0.3 second release after the note ends), scaled so its peak is 0.35, without lowering the drums. Use Python with numpy and Pillow to draw the frames (don't use ffmpeg's drawtext filter), composite them with ffmpeg, keep the size, 30fps, 6 second length and the original audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as falling-notes-piano.py and run:
python3 falling-notes-piano.py input.mp4 beats.json output.mp4Show the script
#!/usr/bin/env python3
"""Falling notes piano: colored bars drop onto a two-octave keyboard and light each key as its note plays on the beat.
Usage: python3 falling-notes-piano.py input.mp4 beats.json output.mp4
beats.json holds the music's beat times in seconds: {"beats": [0.0, 0.5, 1.0, ...]}. The notes below are placed
on those beats; for a real song, fill MELODY and BASS from its MIDI file instead.
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import math
import subprocess
import sys
import tempfile
import wave
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
MELODY = [(3, 72, 1), (4, 76, 1), (5, 79, 1), (6, 78, 1), # (beat number, MIDI note, length in beats):
(7, 74, 1), (8, 71, 1), (9, 75, 1), (10, 79, 2)] # the right hand, B4 to G5
BASS = [(4, 60, 4), (8, 67, 4)] # the left hand, C4 then G4, one per bar
LOWEST = 60 # MIDI number of the leftmost key (60 = middle C)
OCTAVES = 2 # keyboard length, C4 to B5
LEFT, RIGHT = 0.03, 0.97 # where the keyboard starts and ends (0-1 across the frame)
KEYS_TOP = 0.87 # top of the keyboard, where notes land (0-1 down the frame)
KEYS_HEIGHT = 0.10 # white key length as a share of the frame height
LANE_TOP = 0.42 # where falling notes appear (0-1 down the frame)
FALL_TIME = 1.2 # seconds a note takes from LANE_TOP to the keys
MELODY_COLOR = (255, 196, 40) # right-hand notes, amber yellow (a little darker on black keys)
BASS_COLOR = (255, 70, 150) # left-hand notes, pink
SHADE = 0.75 # opacity of the dark fade behind the falling notes
GLOW = 10 # glow blur radius in pixels
PIANO_VOLUME = 0.35 # loudness of the piano notes mixed over the soundtrack (0 = silent)
RATE = 48000 # audio sample rate for the piano notes
BLACK = {1, 3, 6, 8, 10} # pitch classes of the black keys (C = 0)
def timed_notes(beats):
"""(start, end, MIDI note, color) in seconds for every note, from beat numbers."""
step = beats[1] - beats[0]
notes = []
for part, color in ((MELODY, MELODY_COLOR), (BASS, BASS_COLOR)):
for beat, note, length in part:
shade = tuple(round(c * 0.8) for c in color) if note % 12 in BLACK else color
notes.append((beats[beat], beats[beat] + length * step * 0.92, note, shade))
return notes
def keyboard_layout():
"""{MIDI note: (x center, width)} in pixels for every key."""
white_w = (RIGHT - LEFT) * w / (OCTAVES * 7)
keys, whites = {}, 0
for note in range(LOWEST, LOWEST + OCTAVES * 12):
if note % 12 in BLACK:
keys[note] = (LEFT * w + whites * white_w, white_w * 0.6)
else:
keys[note] = (LEFT * w + (whites + 0.5) * white_w, white_w)
whites += 1
return keys
def background():
"""The dark fade the notes fall through."""
img = Image.new("RGBA", (w, h))
top = round((LANE_TOP - 0.05) * h)
fade = np.clip(np.linspace(0, SHADE * 255 * 1.6, h - top), 0, SHADE * 255).astype(np.uint8)
img.paste(Image.new("RGBA", (w, h - top), (8, 8, 14, 255)), (0, top), Image.fromarray(np.repeat(fade[:, None], w, axis=1)))
return img
def overlay(t):
speed = (KEYS_TOP - LANE_TOP) * h / FALL_TIME # pixels per second
land = KEYS_TOP * h
bars = Image.new("RGBA", (w, h))
draw = ImageDraw.Draw(bars)
playing = {}
for start, end, note, color in notes:
bottom = min(land, land - (start - t) * speed)
top = land - (end - t) * speed
if bottom > top and bottom > LANE_TOP * h:
x, key_w = keys[note]
draw.rounded_rectangle([x - key_w / 2 + 2, max(top, LANE_TOP * h), x + key_w / 2 - 2, bottom],
radius=min(5, key_w / 4), fill=color)
if start <= t < end:
playing[note] = color
img = Image.alpha_composite(lane, bars.filter(ImageFilter.GaussianBlur(GLOW)))
img = Image.alpha_composite(img, bars)
draw = ImageDraw.Draw(img)
white_bottom = (KEYS_TOP + KEYS_HEIGHT) * h
for black in (False, True): # white keys first, black keys on top
for note, (x, key_w) in keys.items():
if (note % 12 in BLACK) != black:
continue
bottom = land + (white_bottom - land) * (0.62 if black else 1)
fill = playing.get(note, (25, 25, 30) if black else (255, 255, 255))
draw.rounded_rectangle([x - key_w / 2 + 1, land, x + key_w / 2 - 1, bottom], radius=3, fill=fill,
outline=(40, 40, 45), width=1)
draw.rectangle([LEFT * w, land - 4, RIGHT * w, land], fill=(150, 20, 35)) # red felt strip above the keys
return img
def piano(path, duration):
"""Write a simple piano sound for every note: a few harmonics that ring and fade."""
audio = np.zeros(round((duration + 1) * RATE))
for start, end, note, _ in notes:
freq = 440 * 2 ** ((note - 69) / 12)
t = np.arange(round((end - start + 0.3) * RATE)) / RATE
tone = sum(np.sin(2 * np.pi * freq * k * t) / k ** 2 for k in (1, 2, 3, 4))
envelope = np.minimum(t / 0.005, 1) * np.exp(-3 * t) * np.clip((end - start + 0.3 - t) / 0.3, 0, 1)
i = round(start * RATE)
audio[i:i + len(t)] += tone * envelope
audio = audio / max(np.abs(audio).max(), 1e-9) * PIANO_VOLUME
with wave.open(path, "wb") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(RATE)
f.writeframes((audio * 32767).astype(np.int16).tobytes())
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 mix the piano into its soundtrack."""
with tempfile.NamedTemporaryFile(suffix=".wav") as piano_wav:
piano(piano_wav.name, duration)
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src, "-i", piano_wav.name,
"-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
"-filter_complex", "[0:v][2:v]overlay=shortest=1,format=yuv420p[v];"
"[0:a][1:a]amix=inputs=2:duration=first:normalize=0[a]",
"-map", "[v]", "-map", "[a]", "-c:v", "libx264", "-crf", "18", "-c:a", "aac", 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, beats_file, out = sys.argv[1:4]
w, h, rate, fps, duration = probe(src)
with open(beats_file) as f:
notes = timed_notes(json.load(f)["beats"])
keys = keyboard_layout()
lane = background()
render(src, out)





