Fixed Point Number Converter





4.00/5 (1 vote)
A class to convert fixed point numbers to/from doubles
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:
/// <summary>
/// A class to convert fixed point numbers from/to doubles.
/// </summary>
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; }
}
/// <summary>
/// Is value negative
/// </summary>
public bool IsNegative
{
get { return (ValueAsFixedPoint & _signMask) != 0; }
}
/// <summary>
/// Get decimal part of fixed number
/// </summary>
public int GetDecimal
{
get { return (ValueAsFixedPoint >> _fracWidth) & _decMask; }
}
/// <summary>
/// Get fraction part of fixed point number
/// </summary>
public int GetFraction
{
get { return ValueAsFixedPoint & _fracMask; }
}
private int _val;
/// <summary>
/// Get/Set value with fixed point number
/// </summary>
public int ValueAsFixedPoint
{
get { return _val; }
set { _val = value & _fullMask; }
}
/// <summary>
/// Get/Set value with double
/// </summary>
public double ValueAsDouble
{
get { return ConvertToDouble(_val); }
set { _val = ConvertToFixedPoint(value); }
}
#endregion
/// <summary>
/// Instantiate a fixed point number
/// </summary>
/// <param name="format">fixed point number format</param>
public FixedPoint(string format)
{
// extract pieces from the format definition
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; }
// calculate values to be used later
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;
// calculate format range limits
_maxValue = ConvertToDouble(_signMask - 1);
_minValue = -(_maxValue + ConvertToDouble(1));
}
/// <summary>
/// Convert fixed point number to double
/// </summary>
/// <param name="val">fixed point number</param>
/// <returns></returns>
private double ConvertToDouble(int val)
{
if (_error) return 0;
// do positive numbers
if ((val & _signMask) == 0)
{
double x = val & ~_signMask;
for (var i = 0; i < _fracWidth; ++i)
x = x / 2;
return x;
}
// do negative numbers
else
{
var x = ((~val) & _fullMask & ~_signMask) + 1;
if (x == _signMask)
{
// do this to handle negative boundary condition
var y = ConvertToDouble(_signMask - 1);
var z = ConvertToDouble(1);
return -(y + z);
}
else
{
var y = ConvertToDouble(x);
return -y;
}
}
}
/// <summary>
/// Convert double to fixed point number
/// </summary>
/// <param name="x">value to convert</param>
/// <returns></returns>
private int ConvertToFixedPoint(double x)
{
if (_error) return 0;
// clamp value to format range
x = x > _maxValue ? _maxValue : x;
x = x <= _minValue ? _minValue : x;
// do positive doubles
if (x >= 0)
{
return ConvertToPositiveFixedPoint(x);
}
// and now for negative doubles
else
{
var zz = ConvertToPositiveFixedPoint(-x) - 1;
zz = ~zz & _fullMask;
return zz;
}
}
/// <summary>
/// Converts positive doubles to fixed point number
/// </summary>
/// <param name="x">double to convert to fixed point</param>
/// <returns>fixed point</returns>
private int ConvertToPositiveFixedPoint(double x)
{
// get decimal and fractional parts
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"); // set format
var testVal = fp.ValueAsDouble = 5.1234; // set test value
Console.WriteLine($"test value = {testVal:F6}");
var fpTestVal = fp.ValueAsFixedPoint; // get the converted value
// and the bits and pieces of the fixed point number
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);
// now set 'fp' to test fixed point number
fp.ValueAsFixedPoint = fpTestVal;
// and convert it back to a double, should be the same as you started with
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