This class stores numeric values as a fixed point number in the specified format. The stored value can be get/set by a fixed point number or a double.
Introduction
Audio processors often use fixed point numbers to represent audio signals. During development of a GUI to connect to an audio processor, I needed a simple way to display fixed point numbers to the user. This simple class is a fixed point number container which allows the value to be read or written as a fixed point value or as a double
.
Background
Fixed point numbers have a long history, especially before floating point modules became available for CPUs. DSPs will often use fixed number formats since they, in general, do not have floating point modules.
Audio DSPs represent audio signals in fixed point numbers format. An audio format of "1.31
" has a number range or -1
...+1
. In my particular case, I needed to use a "4.20
" format which has a number range of -8
...+8
. "4.20
" format will have 1 sign bit, 3 decimal bits and 20 fractional bit.
Using the Code
The FixedPoint
class is presented below:
public class FixedPoint
{
private readonly int _fracWidth;
private readonly int _decWidth;
private readonly int _fracMask;
private readonly int _decMask;
private readonly int _signMask;
private readonly int _fullMask;
private readonly double _minValue;
private readonly double _maxValue;
#region Properties
private readonly bool _error;
public bool Error
{
get { return _error; }
}
public bool IsNegative
{
get { return (ValueAsFixedPoint & _signMask) != 0; }
}
public int GetDecimal
{
get { return (ValueAsFixedPoint >> _fracWidth) & _decMask; }
}
public int GetFraction
{
get { return ValueAsFixedPoint & _fracMask; }
}
private int _val;
public int ValueAsFixedPoint
{
get { return _val; }
set { _val = value & _fullMask; }
}
public double ValueAsDouble
{
get { return ConvertToDouble(_val); }
set { _val = ConvertToFixedPoint(value); }
}
#endregion
public FixedPoint(string format)
{
var s = format.Split(new char[] { '.' });
if (s.Length != 2) { _error = true; return; }
var b = int.TryParse(s[0], out _decWidth);
if (!b) { _error = true; return; }
b = int.TryParse(s[1], out _fracWidth);
if (!b) { _error = true; return; }
for (var i = 0; i < _fracWidth; ++i) _fracMask = (_fracMask << 1) + 1;
for (var i = 0; i < _decWidth - 1; ++i) _decMask = (_decMask << 1) + 1;
_signMask = 0x1 << (_decWidth + _fracWidth - 1);
for (var i = 0; i < (_fracWidth + _decWidth); ++i) _fullMask = (_fullMask << 1) + 1;
_maxValue = ConvertToDouble(_signMask - 1);
_minValue = -(_maxValue + ConvertToDouble(1));
}
private double ConvertToDouble(int val)
{
if (_error) return 0;
if ((val & _signMask) == 0)
{
double x = val & ~_signMask;
for (var i = 0; i < _fracWidth; ++i)
x = x / 2;
return x;
}
else
{
var x = ((~val) & _fullMask & ~_signMask) + 1;
if (x == _signMask)
{
var y = ConvertToDouble(_signMask - 1);
var z = ConvertToDouble(1);
return -(y + z);
}
else
{
var y = ConvertToDouble(x);
return -y;
}
}
}
private int ConvertToFixedPoint(double x)
{
if (_error) return 0;
x = x > _maxValue ? _maxValue : x;
x = x <= _minValue ? _minValue : x;
if (x >= 0)
{
return ConvertToPositiveFixedPoint(x);
}
else
{
var zz = ConvertToPositiveFixedPoint(-x) - 1;
zz = ~zz & _fullMask;
return zz;
}
}
private int ConvertToPositiveFixedPoint(double x)
{
var dec = Math.Floor(x);
var frac = x - dec;
var val = 0;
var bit = 0x1 << (_fracWidth - 1);
for (var i = 0; i < _fracWidth; ++i)
{
var testVal = val + bit;
var y = ConvertToDouble(testVal);
if (y <= frac)
val = testVal;
bit = bit >> 1;
}
return ((int)dec << _fracWidth) + val;
}
}
A simple test program is shown below:
- First, create an instance of the class with a specified format, "
new FixedPoint("4.20")
" - Set the instance to a test value, in this case, "
fp.ValueAsDouble = 5.1234;
" - Get the fixed point number back, "
fpTestVal = fp.ValueAsFixedPoint;
" - Feed it back into the instance, "
fp.ValueAsFixedPoint = fpTestVal;
" - Retrieve the value back as a
double
, "convertedVal = fp.ValueAsDouble;
" - The test value and returned value should be the same.
void Main()
{
var fp = new FixedPoint("4.20");
var testVal = fp.ValueAsDouble = 5.1234;
Console.WriteLine($"test value = {testVal:F6}");
var fpTestVal = fp.ValueAsFixedPoint;
var sign = fp.IsNegative ? "-" : "+";
var dec = fp.GetDecimal;
var frac = fp.GetFraction;
var h = $"fixed point value = {sign}:{dec:X1}:{frac:X5} {fpTestVal:X6}";
Console.WriteLine(h);
fp.ValueAsFixedPoint = fpTestVal;
var convertedVal = fp.ValueAsDouble;
Console.WriteLine($"converted value = {convertedVal:F6}");
}
The console output would be as follows:
test value = 5.123400
fixed point value = +:5:1F972 51F972
converted value = 5.123400
This class allows a fixed point number of any format as long as the total number of bits do not exceed 32 (the size of an 'int
'). Common formats I have seen so far are 1.15, 1.31, 4.20, 5.23, and 9.23, but you can create and use your own formats.
The attached LINQPad project contains all this code and is an easy way to test various formats. LINQPad is a great tool to check out small code snippets of C#, F#, VB and SQL.
History
- 3rd April, 2020: Initial version