Show files
A show file (*.show.toml) composes a music video from Cycletron songs. For
each section of each song it sets what's on screen: the visual mode, the
camera, the cuts and transitions, colors, text, the song's code, and effects.
- Live: open the file in Cycletron. The set plays with the show file in a pane beside the song, and edits take effect as you type.
- Rendered:
render-viz.mjswrites the same video to an MP4.
Both cut the same video. Every frame is a function of song time, so what you preview live is what renders.
Three shows come with the app, in your library's Demos/Shows/ (ui/shows/
in the repo): agency-ost plays the Agency album, feature-tour uses every
setting with a comment on each section saying what it shows, and
cycletron-trailer is the trailer.
Contents: Quick start · How a show is built · Key reference · Modes · Palettes · Camera moves · Transitions · Text · Code · Playing live · Rendering · Recipes · Gotchas
Quick start
[show]
out = "renders/my-video.mp4"
[defaults]
code = "hide"
camera = "drift"
[[song]]
source = "../Songs/my-song.strudel"
mode = "tunnel"
[song.sections]
intro = "lissajous"
drop = { shots = ["iso-city", "plasma"], every = 2, transition = "flash", strobe = "kick" }
Play it: open the file in Cycletron (⌘O, or click it in the file tree). Render it:
cd ui # from the cycletron checkout
node scripts/render-viz.mjs path/to/my.show.toml
How a show is built
Songs and sections
A show is a list of songs ([[song]]), played in order. Each song needs a
form: a pickRestart selector whose labels name its sections.
"<intro@2 verse@2 drop@4 outro>".slow(4).pickRestart({
intro: stack(…), verse: stack(…), drop: stack(…), outro: stack(…),
})
A section's length in bars is its weight times the slow factor. Here
intro@2 under .slow(4) is 8 bars, and outro is 4. The labels (intro,
drop, …) are what [song.sections] refers to. To list a song's sections
with their bars, run cargo run -p song-check -- song.strudel from the
Cycletron repo. A flat MIDI import (one $: track per instrument) gets a form
from node ui/scripts/section-song.mjs song.strudel --sections intro:0-5,…,
which slices every track at the bars you name.
Bars
Every position and length in a show is in bars (cycles), so the edit
stays locked to the music at any tempo. Fractions are fine: 0.5 is half a
bar, and 0.25 is one beat in 4/4.
Layers
A setting comes from the most specific place it's written:
[defaults]: every song and section.[[song]]: that song's sections.[song.sections].<label>: that one section.- A shot inside
shots: that one shot (onlymode,bars,camera,palette).
Anything a layer leaves out comes from the layer above it. Table-valued
settings (camera, code, strobe) layer field by field. For example,
code = { place = "right" } in a section keeps the song's follow and
size, and moves only the column.
Writing sections
A section is either a mode name, or a table:
[song.sections]
intro = "lissajous" # just a mode
drop = { mode = "plasma", palette = "vapor" } # an inline table (one line)
[song.sections.break] # or a full table: easier for long ones
mode = "forest-drive"
letterbox = 2.39
text = [
{ say = "first line", at = 1 },
{ say = "second line", at = 3 },
]
TOML doesn't allow line breaks inside { … }, except inside a list's
[ … ]. Use the [song.sections.label] form for anything long.
Strictness
Every key and value is checked. A typo or bad value fails with its exact
location (song[1].sections.drop.camera.kick: expected 0 to 4, not 5). Live,
the error is marked on its line in the show pane, and the last good version
keeps playing.
Key reference
[show]
| Key | Default | Value |
|---|---|---|
title |
— | The show's name. |
out |
— | Where a render goes, relative to the show file. Required to render unless --out is given. |
size |
"1920x1080" |
The render's frame size, "WIDTHxHEIGHT". |
fps |
60 |
The render's frame rate. |
[[song]]
| Key | Default | Value |
|---|---|---|
source |
required | The .strudel file, relative to the show file (~ works). |
mode |
required unless shots |
The mode for sections not listed. |
shots |
— | Modes to cut between through every section not listed. See shots. |
passes |
1 |
Times through the form before the next song. |
fade |
none | { in = 2, out = 4 }: bars fading up from black at the start, and down to black at the end of the last pass. |
performance |
auto | A performance-export directory to render from. Without it, the render exports the song itself. |
gain_db |
0 |
Render volume. |
sensitivity |
1 |
How strongly the visuals react to the audio. |
sections |
— | The per-section table. |
A song can also set any key from the next table, for all its sections.
Anywhere: [defaults], [[song]], or a section
| Key | Default | Value |
|---|---|---|
code |
"off" (all the code, shown) |
How the song's code shows over the visuals. Set "hide" for visuals only. See code. |
camera |
false |
true, false, a move, or { move, punch, kick }. See camera moves. |
titles |
false |
A title card on each section's downbeat. |
transition |
"cut" |
How each shot is entered. See transitions. |
every |
4 |
Bars each shot holds when it doesn't say. |
palette |
"neon" |
Colors the visuals are drawn in. See palettes. |
letterbox |
false |
Cinema bars cropping to an aspect: 2.39 (scope), 2.76, 1.85… Must be more than 1 and at most 4. |
grain |
0 |
Film grain, 0 to 1. |
scanlines |
true |
The faint scanline texture. |
strobe |
false |
A white flash on each hit: "kick", "snare" or "beat", or { on = "kick", strength = 0.3 }. Strength runs 0 to 1, default 0.6. |
speed |
1 |
How fast the visuals move, against the music's pace: 0.5 is slow motion, 2 double, 0 frozen, 4 at most. Timing stays on the beat. |
Sections only
| Key | Value |
|---|---|
mode |
Holds through the section, replacing any shots from the song. |
shots |
Cuts between modes through the section. Give mode or shots, not both. |
title |
The section's title card text, in place of // LABEL. It turns that card on even where titles = false. |
text |
Words on screen. See text. |
Shots
shots = ["iso-city", "cockpit", "tunnel"] # each holds `every` bars
shots = [
{ mode = "iso-city", bars = 2, palette = "sunset" },
{ mode = "cockpit", camera = "shake" }, # bars from `every`
]
Shots play in order and repeat until the section ends. The last one is cut
short if it would run past the end. A shot can set its own mode, bars,
camera and palette. Each shot starts a fresh camera move, and each cut
uses the section's transition.
Modes
| Mode | What it draws | Driven by |
|---|---|---|
iso-city |
An isometric city: each sound a district, each hit a building, lit as the playhead crosses it. Traffic follows the drums. | the pattern |
neon-circuit |
The pattern as a circuit board in the same isometric view: traces, vias and lit chips. | the pattern |
cockpit |
A vector flight display. Kicks are armored targets, snares winged craft, notes diamonds, intercepted on their onsets. | the pattern |
forest-drive |
A country road at dusk. Every tree on the verge is a note, reaching the windshield on its beat. | the pattern |
graveyard |
From the Halloween pack. An 8-bit graveyard at night. Every note is a skeleton rising from its grave to dance; kicks thump the ground, snares strike lightning, hats send bats across the moon, held notes rise as ghosts. | the pattern |
haunted-house |
From the Halloween pack. An 8-bit haunted house. Notes light windows by floor and pitch with a dancer inside; kicks swing the front door, hats flare the porch lanterns, held notes puff ghosts from the chimney, snares strike lightning behind it. | the pattern |
monster-band |
From the Halloween pack. An 8-bit stage where every track is a monster playing its instrument: a skeleton drummer whose kit fires per drum, Frankenstein on bass, a werewolf on guitar, a mummy on horn, a vampire at the organ, a witch on the synth, Boos for the rest. Each plays on its own track's notes. | the pattern |
ghost-fog |
From the Halloween pack. 8-bit ghosts drifting over a swamp, one per note, in their track's colour, with will-o'-wisps on the hats. Calm; for intros and outros. | the pattern |
tunnel |
Octagonal rings flying at you, one per beat, each reaching the gate on time. | the pattern |
marble-core |
Concentric rings, one per track, with a playhead arm sweeping once per bar. | the pattern |
marble-drop |
A Galton board: every hit drops a marble that fills a histogram. | the pattern |
matrix-rain |
Glyph streams, one per note, each track in its own band of columns. | the pattern |
spot-field |
Optical spot clouds, one per track, bursting on hits, with long-exposure trails. | the pattern |
lens-bench |
A blueprint optical bench with a live ray trace; notes ride the rays as pulses. | the pattern + audio |
kaleidoscope |
Particle bursts mirrored eight ways, one band per track. | the pattern |
plasma |
Metaballs as a contour map, each track its own color. | the pattern |
attractor |
A Lorenz attractor kicked by each hit. | the pattern |
vector-nexus |
Wireframe polyhedra in a ticked ring. | audio |
lissajous |
The waveform against a delayed copy of itself, with phosphor trails. Calm; good for opens and outros. | audio |
ascii-scope |
A phosphor scope that etches ASCII artwork where the beam passes. | audio |
wave-terrain |
The spectrum as a ridge landscape scrolling toward you. | audio |
flame-graph |
The spectrum as one flame, bass to air, with embers. | audio |
logo |
The Cycletron icon and wordmark in ASCII. The screen shows the spectrum, the antennae flash on kick and snare, the grille lights with the bands. For end cards. | the pattern + audio |
Modes "driven by the pattern" draw the song's actual notes and tracks: what you see is the code. The others react to the sound.
Palettes
| Palette | Feel |
|---|---|
neon |
The app's own: cyan, magenta, yellow. |
ice |
Pale blue and white. |
vapor |
Pink, cyan and lemon, vaporwave. |
sunset |
Coral, violet and gold. |
amber |
Orange phosphor terminal. |
blood |
Reds on near-black. |
toxic |
Acid green and yellow. |
pumpkin |
From the Halloween pack. Pumpkin orange, violet and moon yellow on purple-black, with slime green and bone. |
A palette recolors the mode, and the code overlay with it. Give each shot its own for a color change on every cut.
Camera moves
| Move | What it does |
|---|---|
drift |
A slow zoom and pan across the section, leaning the other way each section. |
push |
Zooms in across the section. |
pull |
Zooms out across the section. |
orbit |
A slow rotation, alternating direction each section. |
still |
No movement: only the punch and kicks. |
shake |
A handheld wobble. |
Two strengths, scaling from 1 (0 turns one off, 4 is the maximum):
punch: the slam out on each cut (a section or shot start). There's none at a song's very start.kick: the bump on each kick drum in the pattern.
camera = "orbit"
camera = { move = "shake", punch = 2, kick = 3 }
camera = { kick = 0 } # keep the move from above, no kick bumps
Transitions
How each shot, and each section, is entered.
| Kind | Default bars | Effect |
|---|---|---|
cut |
— | A hard cut. |
flash |
0.25 | A white flash decaying into the new shot. |
fade |
1 | Dissolves from the previous shot's last frame. At a song's very start it fades up from black instead. |
black |
0.5 | Comes up from black. |
glitch |
0.25 | Bands of the frame tear sideways, tinted, then settle. |
transition = "fade"
transition = { kind = "glitch", bars = 0.15 }
A kind alone takes its default bars; { kind, bars } sets them.
Text
Section title cards
With titles = true, each section opens with a card: a flash, then
// LABEL glitches in, holds about 1.3 seconds, and fades. A small
03 / 07 counter sits above it. title = "…" replaces the text, and also
turns that one card on.
Lines of text
text = [
{ say = "every sound a district", at = 1.5, hold = 2.4 },
{ say = "THIS VIDEO IS CODE.", at = 2, hold = 2.4, style = "card" },
{ say = "shots = [\"iso-city\", …]", at = 2.2, hold = 2, place = "top" },
]
| Field | Default | Value |
|---|---|---|
say |
required | The words. Quotes inside need \". |
at |
0 |
Bars into the section it appears. |
hold |
2 |
Bars it stays before fading. |
place |
bottom (lines), center (cards) |
bottom, center or top. |
style |
line |
line is a caption that types in. card is a big glitching card, like a title. |
Text has no background box: a thin dark outline keeps it legible over the
visuals. Long text shrinks to fit the frame width. text is set per section
and isn't inherited.
Code
The song's own code, drawn over the visuals the way Stage Mode draws it, with the playing notes lit.
code = "solo"
code = { follow = "solo", place = "right", size = 22, opacity = 0.6, scrim = false, type = 2 }
| Field | Default | Value |
|---|---|---|
follow |
off |
hide shows none. off shows all of it, still. fold collapses the sections not playing. solo shows only the playing section, under a breadcrumb of the form. |
size |
Stage Mode's setting | Text size in px at 1080p, 10–96. Smaller text makes a narrower column too. |
place |
left |
left, center or right. |
opacity |
1 |
0–1, to sit the code back behind the visuals. |
scrim |
true |
The dark backing behind each row. false puts the code straight on the visuals. |
type |
none | Bars over which the code types itself in from the section's downbeat. The playing notes light up once it's typed. |
Playing live
- New: in the file tree, New File and a name ending
.show.toml. It starts out playing the song that's open. - Open: ⌘O, click a
.show.tomlin the file tree, or drag it onto the window. The first song opens in the editor and plays from its start, and the show opens in a pane beside it. A show with a mistake opens with the mistake marked, and starts playing once it's fixed. - Edit: changes in the show pane take effect about 0.3 s after you stop typing. An error is marked on its line, and the last good version keeps playing. ▶ marks the line setting the playing section. Edits to the song itself apply as usual and re-cue the show.
- Suggestions: the pane suggests the keys that fit where you're typing,
and their values: modes, palettes, moves, the song's section names, and
songs in your library for
source. They open as you type; ⌃Space opens them anywhere. Hover a key to see what it does and what it takes. - Save: ⌘S saves whichever editor you last typed in, the show pane or the song.
- Stage: ⌘⇧F shows the visuals full-frame with the code layer. ⌘⇧V toggles the visuals behind the editor. The camera, text, palettes and effects play in both.
- Commands (⌘⇧P palette): Show: Next Song, Show: Previous Song (switch on the next bar), Stop Show.
- The set: each song plays its form
passestimes, then the next starts on the downbeat at its own tempo. The last song loops. If the playing song has unsaved edits, the set waits, then moves on at the next section once you save. Edits made in the last bars before the next song starts are saved when it does. - Closing: closing the pane or opening another file ends the show. An unsaved show asks Save / Discard / Cancel.
Rendering
cd ui # from the cycletron checkout
node scripts/render-viz.mjs path/to/show.toml [flags]
| Flag | Value |
|---|---|
--song n |
Only the nth song, counting from 1. |
--section label |
Only that section, from the first song that has it. Add --song to skip exporting the others. |
--from s, --to s |
Trim to these seconds of what's rendered. |
--size WxH, --fps n |
Override the show's settings, e.g. for quick previews. |
--out file |
Override the show's out. |
--jobs n |
Browsers rendering at once. The default is sized to the machine's cores. |
--fresh |
Re-export the songs even when the cached exports are current. |
Previews
# one section, small and fast
node scripts/render-viz.mjs my.show.toml --song 1 --section drop --size 960x540 --fps 30 --out /tmp/drop.mp4
# the whole set, small: about 10 s per minute of video
node scripts/render-viz.mjs my.show.toml --size 640x360 --fps 24 --out /tmp/preview.mp4
A full 1080p60 render takes about as long as the video plays, on 8 cores.
Exports and audio
Rendering needs each song's mix and section timing (a performance export).
The first render of a song makes one, in about 20 s, and caches it in
~/.cache/cycletron/performance. It's redone when the song file changes.
The render carries the raw mix, at the level the visuals are tuned to react to. To master it, re-encode the audio only:
ffmpeg -i video.mp4 -af "acompressor=threshold=-26dB:ratio=2:attack=15:release=150:makeup=3dB,volume=15dB,alimiter=limit=0.84" -c:v copy video-master.mp4
Or swap in a separately mastered WAV:
ffmpeg -i video.mp4 -i master.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 320k -shortest video-master.mp4
Songs play back to back, each starting on the downbeat after the previous one ends. In a render the video ends with the last song, which doesn't loop.
Recipes
A cut on every bar, each shot its own color
[song.sections.drop]
shots = [
{ mode = "iso-city", palette = "sunset" },
{ mode = "tunnel", palette = "vapor" },
{ mode = "matrix-rain", palette = "toxic" },
]
every = 1
transition = "flash"
strobe = { on = "kick", strength = 0.12 }
camera = { move = "drift", punch = 1.5 }
A cinematic break
[song.sections.break]
mode = "forest-drive"
palette = "sunset"
letterbox = 2.39
grain = 0.45
transition = "black"
camera = { move = "drift", punch = 0 }
Code typing itself in, then lighting up
[song.sections.intro]
mode = "neon-circuit"
code = { follow = "solo", size = 34, type = 1.6 }
camera = { move = "push", punch = 0 }
Lyrics, a line per phrase
[song.sections.verse]
text = [
{ say = "11 PM.", at = 0, hold = 2 },
{ say = "the phone lights up", at = 2, hold = 2 },
{ say = "and I'm already gone", at = 4, hold = 3 },
]
Big word slams on the beat
text = [
{ say = "LINK.", at = 3, hold = 0.2, style = "card" },
{ say = "OSC.", at = 3.25, hold = 0.2, style = "card" },
{ say = "GO.", at = 3.75, hold = 0.2, style = "card" },
]
Visuals only, cards only on chosen sections
[defaults]
code = "hide"
titles = false
[song.sections.drop]
title = "THE DROP" # this one section gets a card
A song that fades in and out, played twice
[[song]]
source = "../Songs/x.strudel"
mode = "lissajous"
passes = 2
fade = { in = 2, out = 4 }
Gotchas
- The code shows by default (
code = "off"), in Stage and in renders. Putcode = "hide"in[defaults]for visuals only, and turn it on per section. - A
centerline on a section's downbeat sits on that section's title card. Useplace = "bottom"or start it a bar later. - Cards and centered code overlap. Move the code (
place = "right"), fade it (opacity), or hide it for that section. orbitshows the frame's corners in very wide windows (the live visuals behind the editor). Stage and renders are 16:9 and fine.- The kick strobe on four-on-the-floor flashes four times a bar. Keep
strengtharound 0.1–0.35 or it washes the frame grey. - The opening fade: a
fadetransition into a song's first section comes up from black, live and rendered alike. passesand the last song: live, the last song loops forever, and dips to black at the end of its passes if it hasfade.out. A render ends with it.- Section labels must match the song's
pickRestartlabels exactly. A label the song doesn't have fails with the list of the ones it does. modevsshots: a section's ownmodereplaces the song'sshots. Giving both in one section is an error.- The palette applies to the code overlay too, since the code is drawn in the theme's colors.
From docs/SHOW_FILES.md at v0.1.0-alpha.27.