Terrain sound visualizer
Use it in Tella
- Open any video in Tella, then open the AI chat.
- Paste the skill:
tellahq/community-skills/terrain-sound-visualizer - Then ask for what you want, for example:
Make a 10 second synthwave terrain visualizer for our background music with the title "Orbit Hours" glowing above the horizon.
Use it in Claude Code
Add it to Claude Code, or any agent that reads skills:
npx skills add tellahq/community-skills --skill terrain-sound-visualizerInstructions
What the AI chat follows when the skill is pasted, from its SKILL.md.
Terrain sound visualizer
A hidden-line wireframe landscape flies toward the camera. Each row of hills is raised by the sound at the moment it appears on the horizon and keeps that height as it rolls closer, so the track's shape passes underneath; the horizon glow brightens with the level, stars twinkle above it, and one big title floats in the sky. It is a full-frame, opaque picture: its own clip, an intro card, or b-roll over a stretch of the video. Builds on tella-motion-design (timing, one look, every graphic has an exit) and tella-title-cards for the type; tella-audio-sync for beat placement.
Ask once
Work out yourself, never ask: whether the video has speech (get_transcript) or background music (get_video backgroundMusic), the song attached to the message if there is one, brand colours from the user's message or the video's existing graphics, and a title suggestion from the video's name or the user's message. Then one ask_form, every field prefilled, so the user can just submit:
- Sound (required, pick): "The video's own audio" (default when it has speech or music), "A Tella track", "The song I attached" (default when a song is attached).
- Tella track (pick, used only with "A Tella track"): up to 12 from
list_libraryscopedefault, typemusic, beat-driven ones first. PrefillModern Tech Pulse(tested: 120 bpm, beat confidence 0.92, quiet until its drop on the 17.032 s downbeat). - Title (optional, text): at most 16 characters, one line, 1–3 words; prefilled from the video's name. Empty means no type, just the landscape.
- Colours (required, pick, each option with a small swatch picture): Violet and cyan (default), Sunset, Emerald, Ice, Brand colours (from the message or the video).
- Start (number, seconds): where the sound range begins. For the video's audio, on its timeline: 0 or the stretch the user named. For a Tella track or a song, the offset into the track: prefill a downbeat where it gets loud (17.032 for Modern Tech Pulse), else 0.
- Length (slider 4–30 s, default 10).
- Where (required, pick): "Its own clip" (default; a new video, nothing of theirs changes), "Intro card at the start of this video", "B-roll over this stretch" (only with the video's own audio: the clip's sound keeps playing under it, so a track or song would drive hills nobody hears). If the user picks b-roll with a track or a song, build it with the video's own audio and say so in the plan.
Media: the only media is the sound. A song comes from the message's attachments first, then the video's own library (list_library scope video); if they picked "The song I attached" and there is none, ask_question "Attach the song (MP3, WAV or M4A) to the chat", suggested answers "Use a Tella track instead" / "Use the video's own audio". Never use a teammate's video or recording that wasn't offered, and never invent a real artist. If the user skips the form, build with the prefilled values.
Where the sound in the result comes from
Reading levels or beats drives the picture only; it does not put sound in a new clip. For each choice:
- Its own clip, the video's voice: in a document without
videoId,addMediathe clip's source,node.patchitssourceInto the range start (ticks, 254016000000 a second) andcommon.hidden: true; its sound still plays.create_cliptype: motionwith thedocumentIdmakes the new video. - Its own clip or intro card, music (a Tella track, a song, or the video's background music): play it as an audio node inside the document, so Start is honoured. It needs a
sourceId: a song attachment has one; for a Tella track or the video's background music, download itsurl(list_libraryscopedefault, orget_videobackgroundMusic),create_sourcekind: audiowith its duration and PUT the file. ThenaddMedia {nodeId: music, start: 0, sourceId, duration: length}andnode.patch {node: music, patch: {sourceIn: Start in ticks}}(a top-level field on the audio node). Don't also set background music:set_background_musicalways starts at 0 with a 2 s fade-in and has no offset. Tested with Modern Tech Pulse from 17.032 s: the clip's audio matched the track from the drop. - Intro card in their video:
create_cliptype: motionwith theirvideoId(it lands last), thenreorder_cliporder: 0; untested. A voice range or music plays from the document as above. If their video has background music, it keeps playing under the card too: tell the user, and never replace it without asking. - B-roll (the video's own audio only): a motion document with the video's
videoId, placed withadd_layoutmedia: {type: motion, documentId}. The clip's own audio plays under it; the curves are on that clip's clock. A layout belongs to one clip:add_layouttakes aclipId, andstartTimeMs/durationMsare on that clip's playback timeline (cuts applied), so keep the shot inside the one clip (shorten it or start it earlier). Only when the range truly runs across a cut, split it there: one document and oneadd_layoutper clip, eachtransitionStyle: "hardCut", the second document starting at the frame the first ended on, keys on each clip's own clock (untested). Thenlist_layoutsand make sure the layout after each time-ranged b-roll is hardCut too (it can come back with a spring).
The sound as numbers
Curves: lvl (0–1, the level now) and h0…h15, the same level delayed by k × 0.4 s. Key each with setCurve every 1/30 s, easing: linear (at most 1000 keys, so 33 s a document).
- A clip's voice:
get_audio_levelsover the range,stepMs20. Map dB to 0–1 between the 10th and 97th percentile, then a one-pole follower, attack 0.04 s, release 0.3 s. - Music, a track or a song:
get_audio_levelscan't hear it.get_music_beats {sourceId}of the same source the audio node plays; times are source time, so document time = beat time − Start (keep beats from Start to Start + length). Each beat is a pulse falling off over 0.12 s (downbeats 1.0, others 0.6) on a floor of 0.18; smooth with attack 0.06 s, release 0.45 s. When Start is 0 and the track opens quietly, multiply by a 0 → 1 ramp over its intro. Belowconfidence0.1 use a slow 0.3–0.6 drift and say so. - Grant's example peaks at 0.62 on downbeats, about 0.42 on other beats and rests near 0.23 between them; aim for that range.
The look (1920×1080, from the example)
- Terrain (
references/terrain.sksl, nodeviz, a full fill):speed3,histStep0.4,height3.2,cell0.5,horizon0.5 (the horizon at y 540). Colours as RGBA 0–1:
| Palette | sky | haze | ground | lineColor | peakColor | title ramp end |
|---|---|---|---|---|---|---|
| Violet and cyan | #04050C | #4B2C8F | #070612 | #32AAB1 | #FFFFFF | #B598FF |
| Sunset | #0B0410 | #9C2A5C | #10050C | #FF8A3D | #FFE7C2 | #FF9E6B |
| Emerald | #020806 | #0F5A3C | #03100A | #3BE58A | #EFFFF4 | #7CF0B0 |
| Ice | #03060C | #1F4F8F | #050A14 | #7FD3FF | #FFFFFF | #9CCBFF |
Brand colours: the darkest brand colour (darkened to under 8 % lightness) as sky and ground, a mid tone as haze, the brightest saturated one as lineColor, white peaks. Keep the ground dark; light grounds are untested.
- Stars (
references/stars.sksl, nodestars, a#00000000fill aboveviz):horizon0.5,density0.3,brightness1. They fade out before the horizon glow. - Title (node
title, above both): Oxanium 700, 150 px, letter spacing −0.035 em, line height 1, centred in a 1600 px box at x 160, y 282, colour#F6F3FF. Oxanium is the user's uploaded font in the example: use it whenlist_motion_fontslists it inlibraryFonts, else Space Grotesk 700 at the same size (tested, reads the same). Effects, innermost first: ablur(styleBlur), anfx.gradientRamp(skyReflection) from (960, 282) to (960, 432), colours title, title, ramp end,colorCount3, and adropShadowglow (whiteGlow)#FFFFFFB3, dx/dy 0, radius 36. Transform:perspective1000,rotateX30°,origin(960, 357), so the type leans back over the land. - Motion (on a 10 s canvas; for other lengths keep the entrance and move the exit with the end): opacity 0 → 1 from 0.85 to 1.5 s, ease
0.22, 1, 0.36, 1; translateY 30 → 0 from 0.85 to 1.8 s, same ease; blur 28 → 0 from 0.85 to 1.7 s, easeOutCubic. Scale 1.2 → 1 linear from 0.85 s to end − 0.2 s, a slow push back. Exit: blur 0 → 28 from end − 0.8 s and opacity 1 → 0 from end − 0.7 s, both easeInCubic, gone at end − 0.2 s. The landscape runs the whole time.
Timing
The title starts at 0.85 s. With beats, move the title's first key to 0.1 s before the first downbeat after 0.8 s; with a voice, to the first word. Never open on silence: the first second has to move.
Building it in Tella Motion
- Document: own clip,
create_motion_documentwithoutvideoId,canvas.setthe duration, add the sound (above), thencreate_clip; b-roll, with the video'svideoId. Name test or new videos after the title. - One batch: 17
setCurves;addShapefillviz(#000000);addEffectkind: shader,effectId: shader, the file's full text assource, every uniform given (time[0],level[0],hA…hD[0,0,0,0] plus the look);bindPropertyfield:common/effects/0/effect/uniforms/time/values/0to{time: seconds},…/level/values/0to{curve: lvl},…/hA/values/0……/hD/values/3toh0…h15in order; thenstarsthe same way with itstimebound. 42 ops in all. - Title:
addTitle(font slug ormi_…id,letterSpacing−0.035),addEffectdropShadow,fx.gradientRampandblurwitheffectIds, thenmoveEffectstyleBlurto 0 andskyReflectionto 1. Key opacity, translateY, scaleX/scaleY and the blur with onenode.patchoncommonas{type: keyframes, timeDomain: local, keyframes: [{id, time, value, easing}]}, times in ticks; the blur's radii go in the nested form{common: {effects: {styleBlur: {effect: {radiusX, radiusY}}}}}(dotted keys are refused). - To recolour,
node.patchviz{common: {effects: {shader: {effect: {uniforms: {sky: {type: values, values: [r,g,b,1]}, …}}}}}}and the ramp'scolor1–color3the same way.
Check
get_motion_frameat full size on a downbeat, in the quietest moment and mid-title-entrance: crisp lines near the camera melting into the haze, no hard band at the horizon, the title clear of the horizon by at least 50 px and not clipped by the lean.- A 2 fps
get_motion_preview: loud and quiet stretches must look different. If not, raiseheightor lower the curve's floor. - The result isn't silent: download a 30 fps
get_video_previewof the clip and check it has an audio stream with sound in the range, and that the tall hills arrive with the loud parts. - Report the sound source and how the levels were read (measured or built from beats), the range, the palette and the font used.
Gotchas
- Tella refuses a shader over 65,536 operations a pixel. Swapping the terrain's own
vnoisefortella_noise2dpushes it over, so it keeps itsfract(sin)hash; Tella warns that may draw faint grid lines on export GPUs (not seen in previews, export untested). The stars usetella_hash2dand are fine. - A broken shader poisons the document's renders: fix it in a fresh document.
get_motion_frametakesstartTimeMs;timeMssilently gives frame 0.- Behind a talking head (a cut-out presenter over the landscape) is untested; check the person stays sharp and the title clears their head.
Variations
- No title: drop the
titlenode; raisehorizonto 0.42 for more land (untested). - Lower energy (a calm voice):
height2.2, release 0.5 s (untested).
Files
The other files in the skill, such as reference videos it copies from.