Click here to Skip to main content
15,891,375 members
Articles / Multimedia / Audio
Tip/Trick

Fixed Point Number Converter

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
3 Apr 2020CPOL2 min read 10.6K   105   1  
A class to convert fixed point numbers to/from doubles
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:

C#
/// <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:

  1. First, create an instance of the class with a specified format, "new FixedPoint("4.20")"
  2. Set the instance to a test value, in this case, "fp.ValueAsDouble = 5.1234;"
  3. Get the fixed point number back, "fpTestVal = fp.ValueAsFixedPoint;"
  4. Feed it back into the instance, "fp.ValueAsFixedPoint = fpTestVal;"
  5. Retrieve the value back as a double, "convertedVal = fp.ValueAsDouble;"
  6. The test value and returned value should be the same.
C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer Renesas
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --