muvid.visualize.visuals

Visual strategies: how the picture of an audio-driven video is produced.

A strategy is a function from a VisualContext (the audio, the optional cover, the duration, the canvas) to a VisualPlan (the ffmpeg inputs and filter chains that yield one video stream). muvid.visualize.video owns the muxing, encoding, and loudness; a strategy only says what the frames look like.

That split is what keeps this open-closed: the built-ins are registered by name ("still", "ken_burns", "cqt", …), and anything else you can express as a callable — a librosa/matplotlib animation, a projectM render, a shader — plugs in through the same seam, either by returning a VisualPlan or by returning the path of a silent video it rendered itself.

>>> sorted(list_visuals())
['bars', 'cqt', 'ken_burns', 'scope', 'spectrum', 'still', 'waves']

Conventions a strategy must honour:

  • ffmpeg input 0 is always the audio. Inputs a plan adds are numbered from 1, in the order they appear in VisualPlan.inputs.

  • To react to the audio, set uses_audio=True and consume the [aviz] label — a dedicated copy of the audio, split off so the output track stays untouched.

  • Emit exactly one video stream, labelled VisualPlan.video.

Module Attributes

REACTIVE_COVER_FRACTION

Over a reactive background the cover fills nearly the whole frame (with a little padding), and is made slightly transparent so the visualizer plays on through it rather than being hidden behind an opaque card.

SCOPE_COVER_ALPHA

The vectorscope fills the whole frame, so it keeps the same big cover as the other visuals but a touch more transparent, letting the line-work read both through the cover and in the space around it.

SCOPE_BG_DIM

The vectorscope's plate is darker still, so its sparse line-work has the least competition of any visual.

DEFAULT_TINT

Default accent for the line/bar visualizers, applied as a tint.

REACTIVE_BG_SATURATION

Reactive backgrounds are pushed dark and near-neutral so the teal accent reads (a bright, saturated blurred cover both tints everything its own colour and, under the screen blend, washes the accent out to white).

Visual

context in, plan out.

Functions

bars_visual(ctx)

Frequency bars (the classic EQ look), via ffmpeg's showfreqs.

cqt_visual(ctx)

Constant-Q transform bars — pitch-aligned, the most musical reactive look.

ken_burns_visual(ctx)

A slow pan/zoom across the cover, lasting exactly as long as the song.

list_visuals()

The names of every registered visual strategy.

register_visual(name)

Register a visual strategy under name (the open-closed seam).

resolve_visual(visual, ctx)

Turn visual (a name, or any callable) into a VisualPlan.

scope_visual(ctx)

The stereo Lissajous figure, via ffmpeg's avectorscope.

spectrum_visual(ctx)

A scrolling spectrogram, via ffmpeg's showspectrum.

still_visual(ctx)

The cover, composed on a 16:9 canvas, held for the whole song.

waves_visual(ctx)

The waveform, via ffmpeg's showwaves.

Classes

VisualContext(audio, image, duration, size, fps)

Everything a visual strategy needs to know about the render.

VisualPlan([inputs, filters, video, ...])

The ffmpeg fragments that render one strategy's video stream.

muvid.visualize.visuals.DEFAULT_TINT = 'colorchannelmixer=rr=0.16:gg=0.80:bb=0.85'

Default accent for the line/bar visualizers, applied as a tint. Several ffmpeg visualizers ignore their colors option (showfreqs draws white whatever you ask for), so rather than fight each filter we render them white and recolour the whole visualization here — one knob, identical accent across every method, so a whole album’s videos read as one release. It is a colorchannelmixer mapping white (r=g=b) to a light teal; override per call with the tint option (an empty string leaves the visualizer’s own colour).

muvid.visualize.visuals.REACTIVE_BG_SATURATION = 0.18

Reactive backgrounds are pushed dark and near-neutral so the teal accent reads (a bright, saturated blurred cover both tints everything its own colour and, under the screen blend, washes the accent out to white). The sharp centred cover still carries the artwork’s colour; only the surround is muted.

The dim is MULTIPLICATIVE (muvid#70), so this value is not comparable with the additive 0.5 it replaces — it is the constant that lands the plate’s mean display luma where the offset left it, measured over four photographs (as PNGs; on JPEG covers the OLD chain landed somewhere else, because an additive offset is range-dependent and a scaling is not — see canvas._DIM_PIVOT).

The washout this comment warns about was then measured rather than assumed: under screen, out = P + V(1 - P), so the accent’s contribution is what a brighter plate would eat. Isolated by rendering the real composed graph twice, once with the visualizer branch removed — which under screen leaves the plate alone — and differencing: over six covers (three photographs, each as PNG and as JPEG) the accent’s frame-mean added luma moves by at most 0.10/255. State the instrument with any number here: the accent is sparse, so a frame mean is small and a mean over the visualizer’s own pixels is not, and only the ratio is portable. It still reads.

muvid.visualize.visuals.REACTIVE_COVER_FRACTION = 0.95

Over a reactive background the cover fills nearly the whole frame (with a little padding), and is made slightly transparent so the visualizer plays on through it rather than being hidden behind an opaque card.

muvid.visualize.visuals.SCOPE_BG_DIM = 0.965

The vectorscope’s plate is darker still, so its sparse line-work has the least competition of any visual. Named rather than spelled at the call site for the same reason spectrum_visual’s is: muvid#70 named three constants to retune and there were four sites, the fourth being a bare literal nothing pointed at. Multiplicative like the other two, and measured the same way — the value that lands the plate where the additive 0.55 left it.

muvid.visualize.visuals.SCOPE_COVER_ALPHA = 0.72

The vectorscope fills the whole frame, so it keeps the same big cover as the other visuals but a touch more transparent, letting the line-work read both through the cover and in the space around it.

muvid.visualize.visuals.Visual

context in, plan out. Returning a path to an already-rendered silent video is also accepted (see resolve_visual()).

Type:

A strategy

alias of Callable[[VisualContext], VisualPlan | Path | str]

class muvid.visualize.visuals.VisualContext(audio, image, duration, size, fps, layout=<factory>, title=None, title_style=None, workdir=<factory>, options=<factory>)[source]

Bases: object

Everything a visual strategy needs to know about the render.

audio

The audio file (ffmpeg input 0).

image

The cover art, if the caller supplied one.

duration

Audio duration in seconds.

size

Canvas size (width, height).

fps

Output frame rate.

layout

How the cover sits on the canvas.

title

Title to burn in, if any.

title_style

How to draw that title.

workdir

A directory the strategy may write intermediate files into.

options

Strategy-specific knobs, passed straight through by the caller.

require_image(visual)[source]

The cover image, or a ValueError naming what to do instead.

Return type:

Path

class muvid.visualize.visuals.VisualPlan(inputs=<factory>, filters=<factory>, video='vbg', uses_audio=False, has_cover=False, has_title=False, still=None)[source]

Bases: object

The ffmpeg fragments that render one strategy’s video stream.

inputs

Extra ffmpeg input argument groups (each ends with -i PATH), numbered from input 1.

filters

filter_complex chains, joined with ; by the renderer.

video

Label of the video stream the chains emit.

uses_audio

The plan consumes the [aviz] audio copy.

has_cover

The plan already placed the cover; the renderer must not overlay it again.

has_title

The plan already burnt in the title; the renderer must not draw it again.

still

When set, the video is this static image — the renderer takes a much cheaper path (encode one short segment, then loop it) and ignores inputs/filters.

muvid.visualize.visuals.bars_visual(ctx)[source]

Frequency bars (the classic EQ look), via ffmpeg’s showfreqs.

Rendered white; colour comes from the accent tint (see DEFAULT_TINT).

Return type:

VisualPlan

muvid.visualize.visuals.cqt_visual(ctx)[source]

Constant-Q transform bars — pitch-aligned, the most musical reactive look.

Rendered white; colour comes from the accent tint (recolour it with the tint option — not a per-filter colour, which the tint would multiply).

showcqt can draw two panes: the bargraph, and beneath it a sonogram that keeps every past frame and scrolls it downward, so the music leaves a trail of where it has been. The sonogram is off by default (bars only, the cleanest read). Give sono_fraction a value in (0, 1) to hand that share of the frame’s height to the trail — the bargraph keeps the rest.

Return type:

VisualPlan

Options:
sono_fraction: share of the height given to the scrolling sonogram,

0 (default, bars only) to just under 1.

sono_v / bar_v: sonogram and bargraph volume (sensitivity). sono_g / bar_g: sonogram and bargraph gamma (contrast). Plus the shared background/cover keys of _reactive_plan().

Examples

>>> ctx = VisualContext(Path("a.wav"), None, 10.0, (1920, 1080), 24)
>>> "sono_h=0" in cqt_visual(ctx).filters[0]  # bars only, by default
True
>>> trail = replace(ctx, options={"sono_fraction": 0.6})
>>> f = cqt_visual(trail).filters[0]
>>> "bar_h=432" in f and "sono_h=648" in f  # 40% bars / 60% trail
True
muvid.visualize.visuals.ken_burns_visual(ctx)[source]

A slow pan/zoom across the cover, lasting exactly as long as the song.

Renders through the burns package (via mixing.video). By default it pans across the composed canvas rather than the raw cover, so a square or portrait image still fills a 16:9 frame instead of being letterboxed by the pan.

Frames are rendered in Python (Pillow), so this is by far the slowest visual — budget several times the song’s duration. The ffmpeg-native visuals are an order of magnitude faster.

Return type:

VisualPlan

Options:

source: "canvas" (default) or "image" — what to pan across.

muvid.visualize.visuals.list_visuals()[source]

The names of every registered visual strategy.

Return type:

list[str]

muvid.visualize.visuals.register_visual(name)[source]

Register a visual strategy under name (the open-closed seam).

Return type:

Callable[[Callable[[VisualContext], VisualPlan | Path | str]], Callable[[VisualContext], VisualPlan | Path | str]]

Examples

>>> @register_visual("black")
... def _black(ctx):
...     w, h = ctx.size
...     return VisualPlan(filters=[f"color=c=black:s={w}x{h}[vbg]"])
>>> "black" in list_visuals()
True
>>> _ = _VISUALS.pop("black")  # (keep the registry tidy for the next doctest)
muvid.visualize.visuals.resolve_visual(visual, ctx)[source]

Turn visual (a name, or any callable) into a VisualPlan.

"auto" picks the cheapest strategy that suits the inputs: a still cover when there is an image, an audio-reactive CQT when there is not.

A callable may return a VisualPlan, or the path of a silent video it rendered itself — the latter is the escape hatch for backends that do not express themselves as an ffmpeg filtergraph (librosa/matplotlib, projectM, a headless-browser capture…).

Raises:

ValueError – visual names a strategy that is not registered.

Return type:

VisualPlan

muvid.visualize.visuals.scope_visual(ctx)[source]

The stereo Lissajous figure, via ffmpeg’s avectorscope.

Tuned for drama and dynamics. Anti-aliased lines, mirror=xy to fill all four quadrants symmetrically, and a generous zoom so the figure spills into the space around the cover. Crucially the amplitude scale is linear, not sqrt: a compressive scale keeps a steady mix pinned wide the whole time (a constant scribble), whereas linear lets the figure breathe — wide and full when the music swells, small and calm when it settles. The cover stays the same big size as the other visuals, a touch more transparent so the line-work reads through it too.

Options: zoom, scale (lin/sqrt/cbrt/log), mirror (none/x/y/xy), draw, plus the shared background/cover keys.

Return type:

VisualPlan

muvid.visualize.visuals.spectrum_visual(ctx)[source]

A scrolling spectrogram, via ffmpeg’s showspectrum.

A log frequency axis spreads out the low-mid range where music lives (a linear axis crams it into the bottom edge), lifted gain makes the detail read, and overlap quickens the right-to-left scroll. Rendered over a darker background so it stands out. The default green colormap is tinted to the teal accent; pass a color to use ffmpeg’s colormap instead.

The whole spectrogram also pulses with the beat: a precomputed onset envelope (see muvid.visualize.reactive) drives a sendcmd-controlled brightness/saturation flash, so attacks in the music read as flashes at the live leading edge instead of the display feeling merely synced to playback. The flash is appended last, after the recolour, so it modulates the colours the frame actually shows.

Options: color (colormap — the teal tint applies only to the default), gain, saturation, overlap (scroll speed, 0–1), flash (bool, default on), flash_brightness, flash_saturation, plus the shared background/cover keys.

Return type:

VisualPlan

muvid.visualize.visuals.still_visual(ctx)[source]

The cover, composed on a 16:9 canvas, held for the whole song.

Return type:

VisualPlan

muvid.visualize.visuals.waves_visual(ctx)[source]

The waveform, via ffmpeg’s showwaves.

Rendered white; colour comes from the accent tint. options={"mode": ...} sets the waveform shape (cline/line/p2p/point).

Return type:

VisualPlan