Thursday, March 5, 2009

#Designing an analog VU meter in DSP

Here are some notes on how to design a VU meter DSP fx.
I won't be getting much into the signal detection, but mostly into the visual of the plugin. If you wish to know more about it, please feel free to send me an email and I will be happy to respond to your questions.

Here is what we are going to use as an example. This is a very simple JesuSonic fx:

Setting some dimensions
We begin by setting the width and height for the visualization.
Its important to set the dimensions first, because they will determine the radius of the needle and the span of our scale.

We are going to make a rather large VU meter - 425x260px, with needle radius of 200px. You should be able to create an arch withing these dimensions using the needle radius.

Designing the VU meter scale
After the dimensions are set we can start creating our scale.
You can very easily design the scale, try to approximate other scales, or use ready equations.

In this example we are going to design our own scale by roughly approximating to an actual scale. There are a lot of photos of analog VU meter over the net. Different manufacturers have patented different scales. We will create our own (in a way).
Similar to this one for example:

We can use it as our base. In general its very difficult to make the approximation 100% accurate if you don't know the original function. Still there are ways to (try) to do so. For example LaGrange polynomials can help you with that. It may require a lot of points to be interpolated and the resulting function and the algorithm may became much more inefficient than a simple exp(). Also by using interpolation there might be some minor unavoidable and unwanted deviations between points.

First thing to do is to set some scale markings in dB:
-20, -10, -7, -5, -3, -2, -1, 0, +1, +2, +3

Here is how to convert amp signal in dB and then get the value of X (dB -> pixels on the X axis):

//code executed at bigger intervals - 1024 samples for example

//take only abs(input) of input
//6/log(2) is our dB scale
in_db = log(abs(input))*6/log(2);


//xl - is the value of X
//in_db - is the value in decibels
//use numbers 2.1 and 285 to tweak the equation
xl = floor(exp(log(1.055)*2.1*in_db)*285);

xl is the result we are looking for here, after converting amp into dB.
A very useful thing to do is to use some sort of a plotting program which can make calculations and draw some lines at the same time. I've used Adobe Flash which is quite fast to work with.


The vertical lines on the left and right represent our width. Point O is the center the circle. Point A lies on the circle therefore OA = r. Point S0 represents a point from our scale using the above equations. To draw the actual lines from the scale, two additional large arches may be used (colored in blue). If we connect O with S0 an intersection will occur between the new cirlces and OS0. These are points D01, D02. So lines such as D01D02 will be visible on our scale. When we know all the coordinates of the points we can make an arrayand draw these 'markings' on plugin initialization.

Drawing the needle in realtime
After we have the result on the X axis we can start drawing the needle.
Of course if you wish to use a bitmap as the needle you should use a different method here (i.e. calculate angle of rotation).


The math method used here to calculate point A's X,Y coordinates is by using proportional triangles and Pitagor theorem - as simple as it gets. Where: S0A / AO = P1A / AP2 etc. And since we know some of the lengths and coordinates of points we can get point A very easily.

//get y from x and radius - r
//Pitagor and proportional triangles used here
xlt < 212 ? xlt=xlt+m : xlt=xlt-m; // check if x is < of middle point 212

The results - xlt & ylt are the A's x,y coordinates and after we have them, we can draw a line between O and A in realtime.

Needle response
Updating the needle per sample is not advisable so I suggest that you use some sort of a countdown before make the above calculations. After some trial and error you will get an acceptable 'response' speed. You should know that the magnetic-electric system (inductor+constant magnet) of an analog vu-meter has response of 300ms. This is quite slow but of course you can tweak it.

Another characteristic of the needle response is the inertial fallback. This is due to the inductor exponential discharge. After executing the x,y calculation code you can add the fallback by simply subtracting pixels from X (original exp() scale) while multiplying the result by some factor for adding inertia (emulating the discharge).

Limiting x and y of A may also be a good thing since the given log scale in dB can go as low as -350db. Or you can just limit the input gain value to -25,+3.5 db

RMS window
Adding a RMS detector isn't very difficult to make in DSP either. Here is an example:

//***per sample
//cs - current sample counter
//300 - window in ms
//sum - peak values stored here
if (cs == (0.001*300*44100)) {
cs = 0;
sum = 0;
} else {
cs += 1;
//square the abs value of the input
sum += sqr(abs(input));
//***execute at bigger intervals - at sample block for example
//calculate value while converting into dB
//*100/100 is to output the values in the X.XX format
rms = floor(6/log(2)*log(sqrt(sum/cs))*100)/100;

So 'rms' would be our RMS value for a 300ms window.

Thats all...well mostly. If you have any question post a comment here or send me an email.

I leave that task of making the VU meter look good to you.



  • Anonymous

    Hi there,

    great tutorial.

    Could you please post the source code to the Reaper plugin to see the concepts in action? Would really help.


  • Lubomir


    you can get the code from the zip package at:
    the file is called 'vumetergfx'

  • Jason

    amazing tutorial

  • ponchik

    I'm looking for a solution in ActionScript 3. My code is:

    var peak_l = soundChannel.leftPeak;
    var peak_r = soundChannel.rightPeak;
    var rms_l = Math.floor((6/Math.LN2)*Math.log(Math.sqrt(peak_l))*100)/100;
    var rms_r = Math.floor((6/Math.LN2)*Math.log(Math.sqrt(peak_r))*100)/100;
    var rms_xl = Math.floor(Math.exp(Math.log(1.055)*2.1*rms_l)*80);
    var rms_xr = Math.floor(Math.exp(Math.log(1.055)*2.1*rms_r)*80);
    level_meter1.width = rms_xl;
    level_meter.width = rms_xr;
    // 80 is my level_meter (mc) indicator stripe.

    I need apply your //cs & //sum parameters to use RMS and fallback. Your code is below.

    if (cs == (0.001*300*44100)) {
    cs = 0;
    sum = 0;
    } else {
    cs += 1;
    sum += sqr(abs(input));
    rms = floor(6/log(2)*log(sqrt(sum/cs))*100)/100;

    //indicator fall-back
    fallback = rel/2*samplesblock/1024;
    fbi_l = exp(xl/512)*fallback;
    fbi_r = exp(xr/512)*fallback;
    xl > 66 (my 80) ? xl -= fbi_l;
    xr > 66 (my 80) ? xr -= fbi_r;

    Probably I need ByteArray or sound.extract() methods. How do I write the code?

  • Lubomir

    responded to your email, ponchik.