From: gints@prophet.esd.sgi.com (Gints Klimanis)
Newsgroups: comp.sys.sgi
Subject: FOR paul@comback
Date: 17 Dec 1992 00:46:46 GMT
Organization: Silicon Graphics, Inc.
Distribution: world
NNTP-Posting-Host: prophet.esd.sgi.com

Hi !

Your mail bounces.
/* 
NOTE: this is not actual apanel code.  Send questions to:

	    Gints Klimanis
	    gints@sgi.com

apanel method is a dual peak-metering scheme.  VU meter simulation
(signal average and signal peak) didn't "look" right, 
because the average values never "pushed" up the peak meters.

Most meters on DAT machines are dual peak meters.  The short and long
meter intervals are a fraction of a second and one second, respectively.  
An meter update rate (and short time interval) in excess of 15 renders/sec.
looks crisp.  If the meter intervals are measured in signal samples 
rather than by a timer, the interval lengths must track the sampling
rate so signals in lower sampling rates are not monitored with lower
meter update rates.

The apanel meters are driven by a lookup into a logarithmic table.  A 
data cache miss to index this table 20 times/second is acceptable in 
comparison to the CPU required to render the meters.

Apanel meters go red when the absolute value of the signal equals or 
exceeds 32767, the 0dB headroom level for a sixteen-bit resolution signal.
Since a logarithmic scale exaggerates the lower values, apanel draws
from -60dB to 0dB.  This prevents quiescent noise (also future system dither) 
from  external sources from flickering the lower segments.

Logarithmic metering mandates DC removal  Apanel uses a simple run length 
complementary low pass filter which computes the signal average value.  
Since this filter  is linear phase, the filter output
may be subtracted from the meter peak values to determine the actual meter
level.  Subtracting the low pass filter from the input is the complement
of a low-pass filter operation:  a high pass filter.  Worry not about
time alignment during the subtraction, since we can assume that the DC
level does not change over the short time interval.  The DC level must
be computed in real-time rather than at system login/power up since the level 
varies from machine to machine, with temperature and the line/mic input
attenuation.   Thus, each of the stereo input requires a separate filter.
The mic and line inputs have different DC offsets.

The length of the filter should be long enough to determine the average
signal level but short enough to minimize the settling time.  You can observe
a meter flicker caused by this filter settling by selecting line or 
mic input, input attenution faders at lowest screen position and switching 
between the two input sources.

The filter accumulators and the run length buffer must be cleared when
the signal breaks.  We do this for changes in input sources 
and sampling rates.  An additional opportunity is when meters are toggled
on and off. This provides recovery when different devices (with potentially
different DC offests) are connected to the IRIS audio inputs.
*/
#include "audio.h"

/* linear to log scale for input level meters */
float		levelToDecibelTable[32768];	/* can be reduced in size */

/* run length buffers used to compute DC offset in meter compute routines */
/* important to keep length of run buffer small.  Need to keep it
large enough for an accurate measurement at the high sampling rates.
However, large values take a long time to settle when changing input
levels (thus changing the offset) and sampling rates.
Since it currently does not scale with sampling rate, the settling time
is longer at the lower sampling rates. Seems to be ok.
*/
short		runLengthBufferL[RUN_LENGTH_BUFFER_LENGTH];	
short		runLengthBufferR[RUN_LENGTH_BUFFER_LENGTH];	
#define RUN_LENGTH_BUFFER_INDEX_MODULO	RUN_LENGTH_BUFFER_LENGTH-1
unsigned long	runLengthBufferIndex;

long		dcOffsetL, dcOffsetR;	/* DC filter filter accumulators */

float		meterUpdateRate = 18.0;	/* # short time meter updates / sec
unsigned int	shortBlockLength;	/* length of block used for
							short time peak meter */

#define REFERENCE_LEVEL 32767	/* largest value considered from 
					2's complement 16-bit data */

/* stuff for meter short and long interval storage */
long shortBlockMinL = shortBlockMinR = 0;
long shortBlockMaxL = shortBlockMaxR = 0;
long shortBlockCounter = 0;
long longBlockPeakL = longBlockPeakR = 0;	
					
/* **********************************************************************
 *
 * ComputeShortTimeMeterInterval:	compute new short time interval 
 *					(in samples) and clear filter
 * ********************************************************************** */
    void 
ComputeShortTimeMeterInterval()
{
long	pvBuffer[2];

/* clear meter registers */
    shortBlockMinL = shortBlockMinR = 0;
    shortBlockMaxL = shortBlockMaxR = 0;
    longBlockPeakL = longBlockPeakR = 0;
    shortBlockCounter = 0;

/* shortBlockLength is inversely proportional to sampling rate */
/* this code asks audio hardware for sampling rate.  Otherwise,
    substitute your known sampling rate */
    pvBuffer[0] = AL_INPUT_RATE;
    pvBuffer[1] = 0;
    ALgetparams(AL_DEFAULT_DEVICE, pvBuffer, 2);
/* compute short block length meter update rate is 50 Hz (WHAT !! BUT
SEEMS TO BE RIGHT) */
    shortBlockLength = (unsigned int) ((((float)
pvBuffer[1])/meterUpdateRate) + 0.5);

/* 
 * initialize some filter variables.  This code should be used
 *  to reset the run length filters when the signal continuity is violated.
 *  Apanel runs similar stuff at start up and every time metering is enabled
 */
/* clear run length buffer and set ptr to start at beginning */
    for (i = 0; i < RUN_LENGTH_BUFFER_LENGTH; i++)
	{
	runLengthBufferL[i] = 0;
	runLengthBufferR[i] = 0;
	}
    runLengthBufferIndex = 0;

/* clear filter accumulators */
dcOffsetL = 0;
dcOffsetR = 0;
}	/* ------------------ end ComputeShortTimeMeterInterval() --------------- */

/* **********************************************************************
 *
 * ComputeLevelToMeterLookUpTable:	fill meter logarithmic lookup
 *					table.  These values range between
 *					0.0 .. 1.0.  
 * ********************************************************************** */
    void 
ComputeLevelToMeterLookUpTable()
{
    int	    i;
    int	    minimumLinearLevel = 33;
    int	    maximumLinearLevel = 32767;
    float   topdBLevel, bottomdBLevel;

/* Any values below -60 dB (value 33of32767 linear) are displayed as 0.0 */
/* -60dB = 20*log10(maximumLinearLevel/maximumLinearLevel) */
    /* nuke out minimumLinearLevel positions */
    for (i = 0; i <= minimumLinearLevel; i++)
	{
	levelToDecibelTable[i] = 0.0;
	}

    /* comppute values only between the bottom and top decibel values */
    topdBLevel = 20.0*log10(2.0*((float) maximumLinearLevel));
    bottomdBLevel = 20.0*log10(2.0*((float) minimumLinearLevel));
    for (; i < maximumLinearLevel; i++)
	{
	levelToDecibelTable[i] = topdBLevel - bottomdBLevel - (-20.0 *
log10(float(i) / ((float) REFERENCE_LEVEL)));
	levelToDecibelTable[i] /= topdBLevel - bottomdBLevel;
	}
	levelToDecibelTable[maximumLinearLevel] = 1.0;
}	/* ------------------ end ComputeLevelToMeterLookUpTable()
--------------- */

/* **********************************************************************
 *
 * ComputeDualPeakMeters:	compute positions of short and long
 *				interval peak meters
 * ********************************************************************** */
void 
ComputeDualPeakMeters()
{
    register long h;

    register short *inBufferPtr;	/* make it point to stereo, time
					interleaved buffer of your choice */

 	for (numStereoSamplesLeft = INPUT_BUFFER_LENGTH; numStereoSamplesLeft;
h += 2, inBufferPtr += 2) 
	    {
	    /* 
	     * compute run length filters.  This will give us a
	     *	majorly low pass filtered version of input signals.
	     *	This value is also the DC offset.  The DC offset is then
	     *	    subtracted from the input signal.  This could very
	     *	    well be a high pass filter if the signal and filter
	     *	    output were time aligned.  However, they are not. 
	     *	This operation works on the assumption that the low pass
	     *	filter output value changes VERY slowly over time. 
	     *	
	     *	Also need to compute for each channel.  DC offset also
	     *	    seems to vary a little with the input attenuation.
	     */
	    /* subtract off value at end of circular runlength buffer */
	    /* and write new input into buffers */
	    dcOffsetL -= (long) runLengthBufferL[runLengthBufferIndex];
	    runLengthBufferL[runLengthBufferIndex] = inBufferPtr[0];

	    dcOffsetR -= (long) runLengthBufferR[runLengthBufferIndex];
	    runLengthBufferR[runLengthBufferIndex] = inBufferPtr[1];

	    /* read input values from input buffer */
	    long inputLeft = (long) inBufferPtr[0];
	    long inputRight = (long) inBufferPtr[1];

	    /* advance and bound buffer index */
	    runLengthBufferIndex++;
	    runLengthBufferIndex &= RUN_LENGTH_BUFFER_INDEX_MODULO;
	    /* accumulate input to signed 32bits:  CAREFUL.  short interval
	    should be short enough to avoid overflow */
	    dcOffsetL += inputLeft;
	    dcOffsetR += inputRight;

	    /* collect maximum negative and positive values of 
		    left/right input samples */
/* should work to subtract DC inside slower update loop.  Do not subtract
	off absolute value */
	    if (inputLeft > shortBlockMaxL) 
		{
		shortBlockMaxL = inputLeft;
		}
	    else if (inputLeft < shortBlockMinL)
		{
		shortBlockMinL = inputLeft;
		}
	    if (inputRight > shortBlockMaxR) 
		{
		shortBlockMaxR = inputRight;
		}
	    else if (inputRight < shortBlockMinR)
		{
		shortBlockMinR = inputRight;
		}
	
	    /* 
	     * update meters every shortBlockLength using dual
	     * peak metering scheme.  Short timepeak interval is 
	     * 1/20 second.  Long time peak interval is 1 second.
	     * The long time peak values are maintained in
	     *	the meter drawing routine 
	     */
		/* IMPORTANT:  >= operation is safeguard for dynamic changes in
		    shortBlockLength.  Such changes occur when the sampling
		    rate is changed. */
	    if ((shortBlockCounter++ >= shortBlockLength))
		{
	    /* only need to remove DC offset at rate which short block
		peak is updated */
	    /* choose greatest short block absolute value */
		if (shortBlockMaxL < -shortBlockMinL)
		    shortBlockMaxL = -shortBlockMinL;

		if (shortBlockMaxR < -shortBlockMinR)
		    shortBlockMaxR = -shortBlockMinR;

	    /* saturate short block max values */
		if (shortBlockMaxL > REFERENCE_LEVEL)
			{
			shortBlockMaxL = REFERENCE_LEVEL;
			}
		if (shortBlockMaxR > REFERENCE_LEVEL)
			{
			shortBlockMaxR = REFERENCE_LEVEL;
			}

	    /* use short block max values to update long block max values */
		if (shortBlockMaxL > longBlockPeakL) 
		    {
		    longBlockPeakL = shortBlockMaxL;
		    }
		if (shortBlockMaxR > longBlockPeakR) 
		    {
		    longBlockPeakR = shortBlockMaxR;
		    }

	    /* look up meter value in decibel table and render on screen */

	    /* AT THIS POINT, WE HAVE THE METER VALUE TO BE DISPLAYED */

	    /* the long block is actually
		lMeter->newLevel(levelToDecibelTable[shortBlockMaxL], 
			levelToDecibelTable[longBlockPeakL]);
		rMeter->newLevel(levelToDecibelTable[shortBlockMaxR], 
			levelToDecibelTable[longBlockPeakR]);
		/* clear long block register every second.  Add your code to
		 ensure they are not cleared every loop iteration. Simple
		to make it a multiple of the short block interval. */
		longBlockPeakL = longBlockPeakR = 0;	

	    /* clear short peak registers and short block counter */
		shortBlockMinL = shortBlockMinR = 0;
		shortBlockMaxL = shortBlockMaxR = 0;
		shortBlockCounter = 0;
		}
	}
    }