muvid.visualize.reactive

Precomputed audio-reactivity: pulse a video filter in time with the music.

Because muvid renders from an audio file, not a live stream, the whole loudness envelope is knowable up front. This module turns that envelope into an ffmpeg sendcmd script that modulates a named filter (brightness/saturation) frame by frame — a beat-reactive “flash” baked deterministically into the render. No realtime, and no dependency beyond numpy and the ffmpeg muvid.visualize already requires.

It is a general seam, not spectrum-specific: any visual can attach a flash to a named filter in its chain (see flash_filter()). The vectorscope reacts to the music through its own amplitude; the spectrogram uses this.

The whole chain degrades to nothing rather than to an error: a track that will not decode, or an ffmpeg build without FLASH_FILTERS, yields an empty fragment, so a visual can append it unconditionally.

muvid.visualize.reactive.DEFAULT_FLASH_LABEL = 'flash'

Default sendcmd label for the pulsing eq. Distinct per flash, so one filtergraph can carry several without their commands crossing.

muvid.visualize.reactive.ENVELOPE_SR = 22050

Sample rate the envelope is measured at. Low is fine — we only need a per-frame loudness curve, not audio quality.

muvid.visualize.reactive.FLASH_BRIGHTNESS = 0.25

Peak brightness boost at a full-strength pulse (ffmpeg eq brightness, -1..1). At rest the filter is a no-op; this is how far a beat pushes it.

muvid.visualize.reactive.FLASH_DECAY = 0.5

Per-frame persistence of a pulse, 0 (no trail) to <1 (longer afterglow), so a beat flashes and fades rather than blinking for a single frame.

muvid.visualize.reactive.FLASH_FILTERS = ('sendcmd', 'eq')

The ffmpeg filters a flash chain is built from. Both are core filters, but a stripped build can omit either — and the flash is a garnish, so a build that cannot do it should render the visual without the flash rather than fail.

muvid.visualize.reactive.FLASH_SATURATION = 0.8

Peak saturation boost at a full-strength pulse, added to 1.0 (ffmpeg eq).

muvid.visualize.reactive.flash_filter(audio: str | Path, *, fps: int, duration: float | None, workdir: Path, label: str = 'flash', brightness: float = 0.25, saturation: float = 0.8, decay: float = 0.5) str[source]

A filter fragment that makes the stream it follows pulse with the beat.

Computes the envelope, writes the sendcmd script into workdir, and returns the chain ,sendcmd=f=…,eq@<label>=… to append after the visual filter (e.g. showspectrum).

Returns "" — a fragment that changes nothing — when the audio yields no envelope or this ffmpeg build lacks FLASH_FILTERS, so a caller can append it unconditionally and still render.

The eq starts as a no-op (brightness=0:saturation=1); the script drives it. eval=frame so it re-reads every frame.

Parameters:
  • audio – The track whose beats drive the flash.

  • fps – The render’s frame rate (one command pair per frame).

  • duration – Clamp the flash to this many seconds (None = whole track).

  • workdir – Directory to write the sendcmd script into.

  • labelsendcmd label for this flash’s eq.

  • brightness – Peak brightness boost on a beat.

  • saturation – Peak saturation boost on a beat.

  • decay – Per-frame afterglow of a pulse.

muvid.visualize.reactive.onset_envelope(audio: str | Path, *, fps: int, duration: float | None = None, sr: int = 22050, decay: float = 0.5) list[float][source]

Per-video-frame onset strength in [0, 1], with phosphor-style decay.

Decodes audio to mono, measures frame-wise loudness, takes the half-wave-rectified rise in loudness (an onset/transient measure, so sustained loud passages don’t stay lit — only attacks do), scales it robustly to [0, 1], then lets each pulse fade by decay per frame so a beat flashes and trails off rather than blinking for a single frame.

Parameters:
  • audio – The track to analyse.

  • fps – Video frame rate — one envelope value per frame.

  • duration – Clamp the envelope to this many seconds (defaults to the whole track).

  • sr – Analysis sample rate.

  • decay – Per-frame persistence of a pulse, 0 (no trail) to <1 (longer afterglow).

Returns:

One value per frame. Empty if the audio could not be decoded.