Log in

View Full Version : Classic VU meters in AviSynth


raffriff42
7th April 2018, 03:57
Here's a classic VU meter for AviSynth! In two styles!

https://www.dropbox.com/s/w3234qcxt9epvpf/VUmeters-demo-01.jpg?raw=1
GetChannel(1).VUmeter("L", shownums=true, dark=false)

https://www.dropbox.com/s/amrkpn31mb9qw7n/VUmeters-demo-02.jpg?raw=1
GetChannel(2).VUmeter("R", shownums=false, dark=true)

For those who might not know, VU (https://en.wikipedia.org/wiki/VU_meter) ("Volume Unit") meters have a slower response than "peak" meters; they are useful because they show the approximate "subjective loudness" rather than the absolute peak audio level. They are better than peak meters for some purposes - such as sound mixing - but peak meters are still needed to avoid digital clipping. The VU meter presented here has auxiliary peak-reading "LED's" which are all you need, most of the time. I'm hoping people will find this useful, or at least fun to play with.

An attempt was made to make the meter impulse response (the ballistics) as accurate as possible;
(EDIT) after a small tweak, it is probably good enough:
https://www.dropbox.com/s/wozyrx2v696g3js/VUmeters-demo-05.gif?raw=1

Performance-wise, I can watch 30fps HD video in real time, barely. (EDIT new version, much better performance)

By default, "0 VU" on this meter corresponds to "-14 dbfs" on a peak meter such as Histogram.
Which means -18 on Histogram("audiolevels") shows as -4 VU, which is an IEC standard, I think.
There is a 'cal' argument for setting your own preferred reference level.

Requires Wilbert's MinMaxAudio (which is 32-bit only)
http://wilbertdijkhof.com/
http://forum.doom9.org/showthread.php?t=127530

Requires AviSynth+ (http://avisynth.nl/index.php/AviSynth+).

Requires a couple of background images; put these in the same folder as the VUmeter script:
https://www.dropbox.com/s/czk56oail4krmab/VUmeter-blank-05.png?raw=1 https://www.dropbox.com/s/fgnny9i1ticncmj/VUmeter-blank-09.png?raw=1
(first image adapted from vuplayer (http://www.vuplayer.com/other.php) via Wikipedia (https://en.wikipedia.org/wiki/File:Vumeters-audio-5.1.png); dark image by me)

Note the output is RGB32; the pixel format is taken from the ImageSource call.

And here is the script itself: save as "VUmeter-01.avsi"### analog VU meter emulator
## 2018-04-06 raffriff42
## 2018-04-08 speedup, drop shadow

## requires MinMaxAudio
## http://wilbertdijkhof.com/
## http://forum.doom9.org/showthread.php?t=127530

## VU meter references:
## https://en.wikipedia.org/wiki/VU_meter
## https://fr.wikipedia.org/wiki/VU-m%C3%A8tre
## http://www.aes.org/par/v/#VU_meter
## IEC 60268-17
## https://quod.lib.umich.edu/cgi/p/pod/dod-idx/audio-signal-visualisation-and-measurement.pdf
## ?c=icmc;idno=bbp2372.2014.206;format=pdf
##
## meter ballistics emulated with a 2nd order Butterworth low-pass filter
## https://en.wikipedia.org/wiki/Butterworth_filter
## Digital filter designed by mkfilter/mkshape/gencode
## http://www-users.cs.york.ac.uk/~fisher/mkfilter

global g_vu_lite = ScriptDir + "\VUmeter-blank-05.png" ## white BG
global g_vu_dark = ScriptDir + "\VUmeter-blank-09.png" ## black BG

##################################
### show a VU meter
##
## @ C - source of audio, framerate and duration (and nothing else)
## (call GetChannel(n) to meter a single channel)
## @ name - a short name; required - MUST be a valid AviSynth variable name, or "" (empty)
## (suggest "L" for left channel, "R" for right, etc)
## @ cal - set dbfs for "0" VU reading; default -14
## (aprox. IEC std: -4VU @ -18dbfs)
## @ dark - default false, if true, show white-on-dark-background style meter
##
## @ showcal - default true; if false, do NOT note calibration on VU meter
## @ showname - default true; if false, do NOT show name on VU meter
## @ showpeak - always true
## @ shownums - default false; if true, show digital readout of peak level (like Histogram)
##
## NOTE - all options except 'name' apply to all meters in the script (due to reuse of global variables)
##
## @ returns 304x190 clip, RGB32
##
function VUmeter(clip C, string name, float "cal", bool "dark",
\ bool "showcal", bool "showname", bool "showpeak", bool "shownums")
{
showcal = Default(showcal, true)
showname = Default(showname, true)
showpeak = Default(showpeak, true)

global vu_dark = Default(dark, false)
global vu_cal = Default(cal, -14.0)
global vu_nums = Default(shownums, false)

I = ImageSource(vu_dark ? g_vu_dark : g_vu_lite)

frnum = C.FrameRateNumerator
frnum = (C.FrameRate<22.0) ? 3 * frnum
\ : (C.FrameRate<33.0) ? 2 * frnum
\ : frnum

I = I.Trim(0, length=1)
\ .Loop(C.FrameCount)
\ .ChangeFPS(frnum, C.FrameRateDenominator)
\ .AudioDub(C)
## size 304x190
## pivot @ 150, 177
Assert(IsClip(I),
\ "VUmeter: internal error 10")

global N_pre = needle_prerender(I, vu_dark)
Assert(IsClip(N_pre),
\ "VUmeter: internal error 20")

F_pre =
\ face_prerender(I, name, vu_cal, vu_dark, showcal, showname, showpeak, 0)
\ + face_prerender(I, name, vu_cal, vu_dark, showcal, showname, showpeak, 1)
\ + face_prerender(I, name, vu_cal, vu_dark, showcal, showname, showpeak, 2)
\ + face_prerender(I, name, vu_cal, vu_dark, showcal, showname, showpeak, 3)
Assert(IsClip(F_pre),
\ "VUmeter: internal error 30")
Eval("global " + name + "_F_pre = F_pre")
#return Eval("L_F_pre")

ScriptClip(I, """
## specs: analog levels
## - Reference Level (typically +4dBu, valid with tones only)
## - Standard Output Level (10dB above Reference, typical peak levels)
## - Clip Level (6dB above Standard Output Level, "headroom")
## - average (sqrt of rms)

## specs: ballistics
## - rising to 99% of "0VU" w/ 1 kHz sine wave for 300 ms
## - fall time same as rise time
## - overshoot not less than 1% and not more than 1.5%
## (tr ~= 0.35/BW)
## (BW ~= 0.35/tr ~= 1.167 Hz)

Assert(IsClip,
\ "VUmeter: internal error 40")
xv0 = 0.0 xv1 = 0.0 xv2 = 0.0
yv0 = 0.0 yv1 = 0.0 yv2 = 0.0
alow = 0.0 ## lowpass result
for (n=(-16), 0) {
afrm = Slip(n).AudioRMS
afrm = Pow(10.0, afrm/20.0) ## log to lin
afrm = Pow(afrm, 0.5) ## rms to avg

## Digital filter designed by mkfilter/mkshape/gencode A.J. Fisher
## Command line: /www/usr/fisher/helpers/
## mkfilter -Bu -Lp -o 2 -a 1.95e-02 0.0 -l
## (manually tweaked to obtain overshoot)
xv0 = xv1
xv1 = xv2
xv2 = afrm / 289.849123
yv0 = yv1
yv1 = yv2
yv2 = (xv0 + xv2)
\ + 2 * xv1
\ + (-0.8750 * yv0)
\ + ( 1.8271 * yv1)
alow = yv2
}
alow = Max(0.0, alow)
alow = 20.0 * Log10(alow) ## lin to log

## calibrate: set 0 VU == 'cal' dbfs
vu_level = 2*alow - vu_cal + 19.5

## backgound "faceplate" with prerendered peak LED's
amax = AudioMax
amax = Max(-96.0, amax)
B = (amax>(-1.99)) ? """ + name + """_F_pre.Trim(3, length=1)
\ : (amax>(-3.99)) ? """ + name + """_F_pre.Trim(2, length=1)
\ : (amax>(-5.99)) ? """ + name + """_F_pre.Trim(1, length=1)
\ : """ + name + """_F_pre.Trim(0, length=1)
B.Loop(FrameCount)
Assert(IsClip,
\ "VUmeter: internal error 50")

## numeric readout
cnul = $ff000000 ## transparent color
(!vu_nums) ? Last
\ : Subtitle(String(amax, "%0.1f"), size=28,
\ text_color=$808080, halo_color=cnul,
\ align=3, x=172, y=106)

## VU needle
aavg = Float(Min(Max(-40, vu_level), 20))
N = N_pre.Trim(-Round(db_to_deflection(aavg))+55, length=1)
Assert(IsClip(N),
\ "VUmeter: internal error 60")

R = Overlay(N, mask=N.ShowAlpha)
Assert(IsClip(R),
\ "VUmeter: internal error 70")
return R
""")

Assert(IsClip,
\ "VUmeter: internal error 90")
(FrameRateNumerator==C.FrameRateNumerator) ? Last
\ : ChangeFPS(C)
return Last.Trim(0, length=C.FrameCount)
}

##################################
### creates an RGB32 animation of needle deflection
##
## frame 0: 55deg left; frame 55: 0deg; frame 110: 55deg right
##
function needle_prerender(clip C, bool dark)
{
BlankClip(C, pixel_type="RGB32", color=$0)
Animate(0, 110, "needle",
\ -55, dark,
\ 55, dark)
return Trim(0, length=111) #.ShowAlpha

function needle(clip C, int angle, bool dark)
{
pivx = 150 ## pivot point
pivy = 177 ## ' '
offx = 2 ## shadow offset
offy = 6 ## ' '

cpt1 = (dark ? $c04040 : $0) ## pointer color
cpt2 = transparent_color(0.5, cpt1) ## pointer border
cnul = $ff000000 ## transparent

## needle
N = C.subtitle_alpha("l",
\ size=210, font_width=3, align=5, x=pivx, y=pivy,
\ text_color=cpt1, halo_color=cpt2,
\ font_angle=Float(-angle))

## drop shadow
DS = C.Subtitle("l",
\ size=210, font_width=20, align=5, x=pivx+offx, y=pivy+offy,
\ text_color=$0, halo_color=cnul,
\ font_angle=Float(-angle))
\ .Blur(1.0).Blur(1.0).Blur(1.0)

## drop shadow alpha
ds_alpha = (dark) ? $454545 : $454545
DA = C.BlankClip(pixel_type="YV12")
\ .Subtitle("l",
\ size=210, font_width=20, align=5, x=pivx+offx, y=pivy+offy,
\ text_color=ds_alpha, halo_color=cnul,
\ font_angle=Float(-angle))
\ .Blur(1.0).Blur(1.0).Blur(1.0)
\ .ColorYUV(levels="TV->PC")

M = Overlay(
\ N.ShowAlpha("YV12"),
\ DA,
\ mode="add")

N2 = Overlay(DS, N, mode="lighten", mask=M)

return MergeARGB(M, N2, N2, N2)
#\ .Subtitle(String(angle))
}
}

##################################
### 1-frame clip showing the "faceplate" with selected number of peak LED's lit
##
## @ showpeak - always TRUE (to support FALSE, need to create faceplate images without bezels)
##
function face_prerender(clip C, string name, float cal, bool dark,
\ bool showcal, bool showname, bool showpeak, int peak)
{
ctxt = $707070 ## text
cnul = $ff000000 ## transparent color

cred = $e00000 ## LED red
cyel = $e0e000 ## LED yellow
coff = $101030 ## LED off

pksz = 14.0 ## LED size
fcir = "n" ## Marlett filled circle

x1 = 220 ## LED positions
x2 = x1 + (1 * 22)
x3 = x1 + (2 * 22)
y1 = 116.5
y2 = y1
y3 = y1

C
## channel name
(!showname) ? Last
\ : Subtitle(name, size=36, x=24, y=100,
\ text_color=ctxt, halo_color=cnul)

## calibration
scal = String(cal, "%0.0f dbfs = 0 VU")
(!showcal) ? Last
\ : Subtitle(scal, size=12, x=24, y=Height-12,
\ text_color=ctxt, halo_color=cnul)

## peak indicators
(peak<1) ? Last
\ : Subtitle(fcir, font="Marlett", size=pksz,
\ text_color=cyel, halo_color=cnul,
\ x=x1, y=y1, align=5)

(peak<2) ? Last
\ : Subtitle(fcir, font="Marlett", size=pksz,
\ text_color=cyel, halo_color=cnul,
\ x=x2, y=y2, align=5)

(peak<3) ? Last
\ : Subtitle(fcir, font="Marlett", size=pksz,
\ text_color=cred, halo_color=cnul,
\ x=x3, y=y3, align=5)
return Trim(0, length=1)
}

##################################
## dB to deflection angle (HACK)
function db_to_deflection(float dblevel)
{
return Spline(dblevel,
\ -40, 55,
\ -24, 50,
\ -20, 47,
\ -10, 34.5,
\ -7, 25,
\ -5, 16,
\ -3, 4,
\ -2, -2.5,
\ -1, -10,
\ 0, -19,
\ 1, -28,
\ 2, -38.5,
\ 3, -49.5,
\ 5, -54,
\ 20, -55,
\ cubic=false)
}

##################################
### Like Subtitle, but with alpha information
## All arguments are identical to Subtitle, except:
## @ C - must be RGB32
## @ halo_width - if < 1, halo is disabled;
## if >= 1, same as standard Subtitle halo.
## (default = 1)
##
## @ returns RGB32 subtitle with mask channel
##
function subtitle_alpha(clip C, string text, float "x", float "y",
\ int "first_frame", int "last_frame",
\ string "font", int "size",
\ int "text_color", int "halo_color",
\ int "align", int "spc", int "lsp",
\ float "font_width", float "font_angle",
\ bool "interlaced", int "halo_width")
{
Assert(C.IsRGB32,
\ "subtitle_alpha: source clip must be RGB32")

text_color = Default(text_color, $ffff00)
halo_color = Default(halo_color, $0)
halo_width = Default(halo_width, 1)

text_alpha = text_color.BitRShiftU(24).BitAnd($ff)
halo_alpha = halo_color.BitRShiftU(24).BitAnd($ff)

R = C.Subtitle(text, x, y, first_frame, last_frame,
\ font, size, text_color, halo_color, align,
\ spc, lsp, font_width, font_angle, interlaced)

text_color = MakeRGBA(text_alpha, text_alpha, text_alpha, $0)
halo_color = MakeRGBA(halo_alpha, halo_alpha, halo_alpha, $0)

M = C.BlankClip(pixel_type="YV12")
\ .Subtitle(text, x, y, first_frame, last_frame,
\ font, size, $ffffff, (halo_width<1 ? $0 : $ffffff), align,
\ spc, lsp, font_width, font_angle, interlaced)
\ .ColorYUV(levels="TV->PC")

return MergeARGB(M, R, R, R)
}

#######################################
### given 'R', 'G', 'B', 'A' return an Avisynth color
##
function MakeRGBA(int r, int g, int b, int a) {

r = Min(Max(0, r), 255)
g = Min(Max(0, g), 255)
b = Min(Max(0, b), 255)
a = Min(Max(0, a), 255)

return a.BitLShift(24)
\ .BitOr(r.BitLShift(16))
\ .BitOr(g.BitLShift(8))
\ .BitOr(b)
}

#######################################
### "slip" (advance or delay) a clip in time.
## Adds head or tail padding; clip is normally Trimmed later.
##
## @ C - clip to be advanced or delayed
## @ offset - if positive, clip is advanced;
## if negative, clip is delayed
##
function Slip(clip C, int offset)
{
lenTrim = (offset > 0) ? offset : 0
lenPad = (offset < 0) ? -offset : 0

C = (lenPad==0) ? C
\ : C.Trim(0, -1).Loop(lenPad) + C.Trim(0, C.Framecount-lenPad)

C = (lenTrim==0) ? C
\ : C.Trim(lenTrim, 0) + C.Trim(C.Framecount-lenTrim, -1).Loop(lenTrim)
return C
}

#######################################
### given Avisynth color, set its transparency
## @ a - 0.0 = transparent, 1.0 = opaque.
##
function transparent_color(float a, int color) {
a = 255 - Min(Max(0, Round(a * 256.0)), 255)
return BitOr(a.BitLShift(24), BitAnd(color, $ffffff))
}

#####

Sparktank
7th April 2018, 05:07
Awesome! Will have to use this next time I watch something where they break in to a place that has sound sensitive serurity measure (Mission: Impossible, mayhaps?)

poisondeathray
7th April 2018, 15:53
thanks!

johnmeyer
7th April 2018, 16:02
That is an amazingly great piece of work.

WorBry
7th April 2018, 19:48
Don't have immediate use for this myself, but I have to admire your prolific creativity in coming up with these tools rr42.

wonkey_monkey
7th April 2018, 23:19
It just needs a shadow under the indicator, then it'll be perfect...

Edit: forgive my ignorance, because I've always avoided scriptclip like the plague, but doesn't it create and new destroy new filter instances (for subtitle, for example) for every frame? Is that where some of the slowness might come from?

raffriff42
7th April 2018, 23:38
>It just needs a shadow under the indicator, then it'll be perfect
It would be nice, but then it would be even slower. I dunno, I might test it.

>Is [scriptclip] where some of the slowness might come from?
Yes, but MinMaxAudio requires it.

I think what slows this script the most is accessing 16 frames of audio per video frame, for the smoothing filter. I could possibly process at half frame rate, accessing only 8 frames, but the needle movement will not be as smooth.

If this were rewritten as a plugin it could be a lot faster (anybody interested?)

wonkey_monkey
8th April 2018, 00:01
I think what slows this script the most is accessing 16 frames of audio per video frame, for the smoothing filter. I could possibly process at half frame rate, accessing only 8 frames, but the needle movement will not be as smooth.

Is that a fixed 16 frames, or does it vary on framerate? Cos if you had a 30fps and a 60fps version of the same clip, you'd end up a stiffer/looser meter.

raffriff42
8th April 2018, 00:16
It is fixed at 16. The variation is held in check by keeping the framerate at approximately 60fps. If framerate < 22 it is multiplied by 3, else if framerate < 33 it is multiplied by 2. I have not tested it at every framerate yet; maybe a more precise conversion is needed. It's possible the filter section needs a total rethink...

StainlessS
8th April 2018, 00:33
It just needs a shadow under the indicator, then it'll be perfect...

Edit: forgive my ignorance, because I've always avoided scriptclip like the plague, but doesn't it create and new destroy new filter instances (for subtitle, for example) for every frame? Is that where some of the slowness might come from?

Slowness down to Constructor startup, Subtitle is real slow in construction.

EDIT: Raff, how bout some 3D parallax error.
(great job young man :) ) [EDIT: me jests, I dont give a damn about HHHHD whatever, 3D is certainly not summick I care bout].

EDIT: In recent Duplicity thread, was getting ~160FPS using scriptclip and massive amount of decisions(at times) in scriptclip, on my crap
core duo. RT_Subtitle is way way faster for metrics, if you want pretty, then use builtin subtitle (and take a nap).

EDIT: David (or should I say Wonky_Winky), for Scriptclip, pre-prepare eg 4 clips as subtitled, and in scriptclip, (or conditional filter or whatever), select which of those that you require, does away with construction at every frame, pretty fast, give it a try.
I personally prefer to use GScript capability within Scriptclip rather than loads of Conditional/this/that/the other, is well fast if set up well.

raffriff42
8th April 2018, 02:51
Good ideas. I was thinking you could pre-render the needle (with drop shadow) at all required angles (~100 total) and store the images in an array -- I think you do something like that for the font characters in RT_Subtitle??

wonkey_monkey
8th April 2018, 12:36
It's pretty easy to call the Windows GDI+ API to draw shapes directly on RGB32 frames.

StainlessS
8th April 2018, 13:22
think you do something like that for the font characters in RT_Subtitle??

Sorry, RT_Subtitle is fixed font only, same font as the Info.h font used by various plugins for metrics.

It's pretty easy to call the Windows GDI+ API to draw shapes directly on RGB32 frames.

Me smells something good in the over :)

raffriff42
8th April 2018, 14:53
New version, using prerendered stuff as much as possible - double the performance.

...and the needle has got a drop shadow!
https://www.dropbox.com/s/mzjsirpw7ywxph1/VUmeter-demo-21.png?raw=1 https://www.dropbox.com/s/idhg8lkgpwba3dm/VUmeter-demo-22.png?raw=1

StainlessS
8th April 2018, 16:07
Should 'dark' here be vu_dark ? [EDIT: Using defaulted values]


global N_pre = needle_prerender(I, dark)
#return N_pre

F_pre =
\ face_prerender(I, name, cal, dark, showcal, showname, showpeak, 0)
\ + face_prerender(I, name, cal, dark, showcal, showname, showpeak, 1)
\ + face_prerender(I, name, cal, dark, showcal, showname, showpeak, 2)
\ + face_prerender(I, name, cal, dark, showcal, showname, showpeak, 3)


Invalid arguments to function needle_prerender line 70.

EDIT: Cal looks like will have similar probs.

EDIT: To below, thats very nifty raff, very nice. :)

raffriff42
8th April 2018, 16:36
Your are right sir! Thanks, fixed!

Reel.Deel
8th April 2018, 21:51
@raffriff42

Thank you for the script! I'm always interested in more visual audio tools for AviSynth :).

Btw, here's the original program where the VU meter picture came from: http://www.vuplayer.com/other.php
A derivative of the the original analog meter by Ernest Turner:
https://www.gearslutz.com/board/attachments/high-end/233313d1304531238-emi-germanium-mixing-console-vumeter.jpg
https://www.gearslutz.com/board/showpost.php?p=6608904&postcount=22&

raffriff42
8th April 2018, 22:37
Ahh, I searched for VU meter images but missed that. I'll have to credit the source!

(EDIT) Here's a white background created from scratch if anybody wants it:
https://www.dropbox.com/s/h3w1o2hu0nshdhx/VUmeter-blank-11.png?raw=1

wonkey_monkey
8th April 2018, 22:45
Is there any functional use to be made of the needle's overshoot, or is that just aesthetics?

I'm still grappling with all the scriptclip and animate stuff. Doesn't animate() in needle_prerender also construct a new instance of subtitle() for each frame? Isn't there now a reliance on Avisynth caching to not keep calling/create instances?

raffriff42
8th April 2018, 23:34
>Is there any functional use to be made of the needle's overshoot, or is that just aesthetics?
It was carefully studied back in the 1930s, and they felt a little overshoot was the way to go (so yes, aesthetics)
https://www.dropbox.com/s/bl2jqch29ydn70i/VU%20meter%20IEEE-1940%20fig%2C11.jpg?raw=1
(the one with extreme overshoot must have been interesting to work with...)
("new VI" = VU meter) (source pdf) (www.aes.org/aeshc/pdf/chinn_a-new-svi.pdf)


>Doesn't animate() in needle_prerender also construct a new instance of subtitle() for each frame?
Yes, but only on filter startup, not at runtime like before.

Reel.Deel
8th April 2018, 23:52
I tried the the script but I cannot get it to work. With the following script I get this error:

Avisynth open failure:
The script's return value was not a video clip, (Is the undefined value).

FFAudioSource("music.flac") #6min 46sec
audio = last
fps = 60000
den = 1001
length = Round((AudioDuration(audio)*fps)/den)
BlankClip(length, width=512, height=256, fps=fps, fps_denominator=den, pixel_type="RGB32")
AudioDub(last, audio)
GetChannel(2)
VUmeter("R")

raffriff42
9th April 2018, 00:39
Copied your script exactly - it works for me. I don't know. Is there something below the last line on yours?

(UPDATE) added some Assert() calls that should help locate the error if it persists (post #1) (https://forum.doom9.org/showthread.php?t=175370)

Reel.Deel
9th April 2018, 00:57
Hmm, if I import the script it works, if I have it under the script above then I get an error. Strange, I often do this with stuff I'm testing and have not had a problem.

StainlessS
9th April 2018, 01:18
@ R.D,
well,

Colorbars
audio = last
fps = 60000
den = 1001
length = Round((AudioDuration(audio)*fps)/den)
BlankClip(length, width=512, height=256, fps=fps, fps_denominator=den, pixel_type="RGB32")
AudioDub(last, audio)
GetChannel(2)
VUmeter("R")

global g_vu_lite = ScriptDir + "\VUmeter-blank-05.png" ## white BG
global g_vu_dark = ScriptDir + "\VUmeter-blank-09.png" ## black BG


the globals at bottom of above are not initialized, [EDIT: prior to call VUmeter("R")]

So move globals to top of script and works ok,

EDIT: If you duplicate the globals to top of script you get the original error line, unless you put a Return Last somewhere after the original
globals.

spoRv
10th April 2018, 11:42
raffriff42: simple genius, thanks a lot!

:thanks:

Another reason to migrate to avisynth+ :D