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 rewrites a named lutyuv’s lookup table 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.
Module Attributes
Sample rate the envelope is measured at. |
|
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. |
|
Peak brightness boost at a full-strength pulse, in |
|
Peak saturation boost at a full-strength pulse, added to 1.0. |
|
Default |
|
The ffmpeg filters a flash chain is built from. |
|
one per |
Functions
|
A filter fragment that makes the stream it follows pulse with the beat. |
|
Per-video-frame onset strength in |
- muvid.visualize.reactive.DEFAULT_FLASH_LABEL = 'flash'¶
Default
sendcmdlabel for the pulsing lookup table. Distinct per flash, so one filtergraph can carry several without their commands crossing.The label is NOT the
sendcmdtarget, and reading it as one is how muvid#72 came to report the flash as inert.avfilter_graph_send_commandmatches three things and a bare label is none of them, so the target is the instance name — the wholelutyuv@flashspelling, whichflash_filter()derives from the filter it emits rather than composing a second time. Measured on ffmpeg 9.0.1 and 6.1.6, driving a grey source with one full-strength pulse on frame 5 in a graph that also carriesbackground_chain’s own unlabelledlutyuv— and in BOTH graph orders, because the type name’s answer depends on the order:- target plate first flash first (muvid’s own order)
plate flash plate flash
lutyuv@flash unmoved 126 -> 189 unmoved 126 -> 189 <- shipped lutyuv 33 -> 67 unmoved unmoved 126 -> 189 flash unmoved unmoved unmoved unmoved all ffmpeg SIGSEGVs on both builds
sendcmddispatches withAVFILTER_CMD_FLAG_ONE, so the type name reaches only the FIRSTlutyuvthe parser created and stops. Which one that is comes down to where the chains sit in the composed string, and in muvid’s real graph the flash is FIRST —_reactive_planemits[aviz]…lutyuv@flash…[_viz]beforebackground_chain(measured on the composed spectrum graph: char 279 against char 493). So the type name would happen to work today, silently, and break on any reordering; muvid#72’s prediction that it would flash the plate instead is what happens in the other order. The instance name is the address that does not depend on the accident — measured identical in both orders above.allhandsy <lut expression>to every commandable filter in the graph, including thecropinbackground_chain, whoseyis a geometry expression; it fails to parse and the process dies (exit 139, both builds).
- 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, in
eq’s units (an additive offset as a fraction of full scale, -1..1). At rest the filter is a no-op; this is how far a beat pushes it.
- muvid.visualize.reactive.FLASH_COMPONENTS = ('y', 'u', 'v')¶
one per
lutyuvcomponent.eqtook two (brightness,saturation); a LUT is addressed per plane, so chroma costs two commands carrying the same expression.- Type:
The
sendcmdcommands one flash frame sends
- 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', 'lutyuv')¶
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.
This tuple is the probe, so it has to name the filters the chain really contains. It said
eqwhile the point of muvid#69 was to stop needingeq(GPL-only): left stale, an LGPL build — the very build this change exists to serve — would have failed the probe and silently rendered with no flash at all.
- muvid.visualize.reactive.FLASH_SATURATION = 0.8¶
Peak saturation boost at a full-strength pulse, added to 1.0.
- muvid.visualize.reactive.flash_filter(audio, *, fps, duration, workdir, label='flash', brightness=0.25, saturation=0.8, decay=0.5)[source]¶
A filter fragment that makes the stream it follows pulse with the beat.
Computes the envelope, writes the
sendcmdscript intoworkdir, and returns the chain,sendcmd=f=…,lutyuv@<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 lacksFLASH_FILTERS, so a caller can append it unconditionally and still render.The
lutyuvstarts as an identity table (brightness 0, saturation 1); the script drives it. It is a LUT rather than aneqbecauseeqexists only in a GPL-configured ffmpeg (muvid#69) — seebrightness_saturation_lut().- Parameters:
fps (
int) – The render’s frame rate (one command per component per frame).duration (
float|None) – Clamp the flash to this many seconds (None= whole track).workdir (
Path) – Directory to write thesendcmdscript into.label (
str) –sendcmdlabel for this flash’slutyuv.brightness (
float) – Peak brightness boost on a beat.saturation (
float) – Peak saturation boost on a beat.decay (
float) – Per-frame afterglow of a pulse.
- Return type:
- muvid.visualize.reactive.onset_envelope(audio, *, fps, duration=None, sr=22050, decay=0.5)[source]¶
Per-video-frame onset strength in
[0, 1], with phosphor-style decay.Decodes
audioto 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 bydecayper frame so a beat flashes and trails off rather than blinking for a single frame.- Parameters:
- Return type:
- Returns:
One value per frame. Empty if the audio could not be decoded.