1

I have built an emulated Analog VU Meter for a recording app and have everything hooked up properly and working the way I expect except for one aspect. If you watch this 13-second video of the VU meter in action, you will see that the needle bounces all over the place and is not really what would happen in a real VU meter. For an example of what I am looking for, try out the Apple "Voice Memos" app and see.

My logic so far is easy:

#define VU_METER_FREQUENCY                                      1.0/5.0

- (void)someMethod {
    _updateTimer = [NSTimer 
                    scheduledTimerWithTimeInterval:VU_METER_FREQUENCY
                    target:self 
                    selector:@selector(_refresh) 
                    userInfo:nil 
                    repeats:YES];
}

- (void)_refresh {  
    // if we have no queue, but still have levels, gradually bring them down
    if (_delegate == nil) {
        CFAbsoluteTime thisFire = CFAbsoluteTimeGetCurrent();
        // calculate how much time passed since the last draw
        CFAbsoluteTime timePassed = thisFire - _peakFalloffLastFire;
        needleValue = needleValue - timePassed * VU_METER_LEVEL_FALL_OFF_PER_SECOND;
        if (needleValue < VU_METER_MIN_DB) {
            needleValue = VU_METER_MIN_DB;
            TT_INVALIDATE_TIMER(_updateTimer);
        }
        _peakFalloffLastFire = thisFire;
    } else {
        prevNeedleValue = needleValue;
        needleValue = [_delegate currentDB];
    }
    [self updateNeedle];
}

- (void)updateNeedle {
    [UIView beginAnimations:nil context:NULL]; // arguments are optional
    [UIView setAnimationDuration:VU_METER_FREQUENCY];
    [UIView setAnimationCurve:(needleValue > prevNeedleValue ? UIViewAnimationCurveEaseOut : UIViewAnimationCurveEaseIn)];
    CGFloat radAngle = [self radianAngleForValue:needleValue];
    self.needle.transform = CGAffineTransformMakeRotation(radAngle);
    [UIView commitAnimations];
}

Basically, I setup a timer to run at VU_METER_FREQUENCY and update the needle rotation using a UIView animation with easing that is preferential to keep the needle higher. I am looking for a way to adjust this somehow to provide a smoother needle, with my benchmark being as close as possible to Apple's analog VU Meter. To get the needleValue, I am using AudioQueue's mAveragePower and querying it every time currentDB is called. How can I smooth this?

coneybeare
  • 33,113
  • 21
  • 131
  • 183
  • I hope you wouldn't mind sharing your radianAngleForValue:needleValue: method with me. I'm trying to get my VU meter to work, but it just won't work. I won't mind giving you credit, either ;) – Tristan Mar 24 '11 at 04:57
  • The angle is highly specific to your own implementation. It is dependent on your needle length and scale, which is determined by the image you create to show the values. It took me some time working it out on paper to solve the multiple equations needed and retrieve the values necessary for this function in my setup. This link was valuable in determining how to set it up for yourself: http://neolit123.blogspot.com/2009/03/designing-analog-vu-meter-in-dsp.html#hcon – coneybeare Mar 24 '11 at 19:33
  • Thanks, that'll probably help me figure it out. Sleepless weekends, here I come! As of right now, I have a crappy implementation of a VU meter with a needle that moves sideways, although I kinda do like it, I want the real deal of a VU meter. – Tristan Mar 24 '11 at 23:34

2 Answers2

2

According to Wikipedia, the behavior of a VUMeter is defined in in ANSI specification C16.5-1942. The needle full rise and fall time is supposed to be 300 mSec, averaging loudness over that duration.

I would try a 1-pole low-pass filter on the needle angle to approximate that angular rate, and animate the meter manually on a frame-by-frame basis using CADisplayLink based drawRect animation. View animation might not give the same responsiveness.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153
2

One thing I would suggest is changing this.

#define VU_METER_FREQUENCY    1.0/5.0

That says update 5x a second, the issue is that I think apple will hold 0.2s of samples, so you really are getting an average of the sounds, hence the meter is not really following the highs and lows of the sounds but more of a lower average.

I think this setting can go as high as 1.0/60 (60hz).

As for making the meter smooth, that is a little tricker.

You could do something like this.

  1. Create an array that holds 7-8 values.
  2. every time you get a reading, add it to the array and pop the 7th value of (eg only hold the last seven values.
  3. Find the average of the array (sum the array / divide by the number of elements in the array.
  4. Display this average.

So its a bit like filling up a pipe, and once you stop filling it will take some time for it to empty and needle will slowly fall down.

OR you could only allow the needle to fall down only so much every cycle.

Lets say the needle swings be 0 (lowest value) and 1 (highest value far right hand side). Lets also say you sample at 20hz (20x times a second).

Every time you update the position only allow the needle to rise say 0.1 of a value max and fall only 0.05 of value.

you could do something like this and play with the values to get it nice and smooth.

if newValue>currentMeterValue
   currentMeterValue = Min(currentMeterValue + 0.1, newValue);
else
   currentMeterValue = Max(currentMeterValue - 0.05, newValue);

OR

You simply move the meter at a rate proportionally to the distance between each value (this should smooth it nicely) and actually be close to real meter with a spring pushing against the needle which is powered by an electromagnet.

   currentMeterValue += (newValue - currentMeterValue)/4.0;
John Ballinger
  • 7,380
  • 5
  • 41
  • 51
  • I ended up using an array of samples to filter out any anomalies, and also implemented the smoothing with the MIX, MAX method to curb any large jumps. Using CADisplayLink below, I am getting about 60HZ refresh rate and got rid of the UIView animations completely. – coneybeare Nov 23 '10 at 06:10