I have written a class to quantize values to arbitrary bit depths within a specified amplitude. It is meant to process one value at a time, and has two ways of quantizing values.
I wrote the class so that i could create objects of it as a property for another class, so that the quantization process can easily be included into other, more complex operations. This class I wrote, however, has some things I am quite sure about whether they were solved optimally.
My Header file, Quantizer.h, looks like this:
#pragma once
#include <cmath>
class CQuantizer
{
public:
~CQuantizer();
enum qType {
lin_midrise,
lin_midtread,
numberOfTypes
};
CQuantizer(CQuantizer::qType type = CQuantizer::lin_midtread,
int nBits=4, float amplitude = 1.f);
float processOneSample(float in);
void setNBits(int bits);
void setAmplitude(float amplitude);
void setType(CQuantizer::qType type);
private:
int m_nBits;
float m_amplitude;
CQuantizer::qType m_type;
float processOneSampleLinMidtread(float in);
float processOneSampleLinMidrise(float in);
float (CQuantizer::*processSampleFunc)(float);
};
The first thing I am wondering about is the enum I used for the types: is it a good idea to include numberOfTypes as last element for later loops through the enum etc.?
Also, I saw somewhere else for a similar class that as some kind of switch, function pointers were used, which I did for the processOneSample-method.
In this scenario, is it appropriate to do so?
Also, below I've included my implementation:
#include "Quantizer.h"
CQuantizer::~CQuantizer()
{
}
CQuantizer::CQuantizer(CQuantizer::qType type, int nBits, float amplitude)
{
setType(type);
setNBits(nBits);
setAmplitude(amplitude);
}
float CQuantizer::processOneSample(float in)
{
if (m_amplitude == 0.f) {
// watch out for zero-amplitude
return 0.f;
}else{
// use function specified by type
return (this->*processSampleFunc)(in);
}
}
void CQuantizer::setNBits(int bits)
{
m_nBits = bits;
}
void CQuantizer::setAmplitude(float amplitude)
{
m_amplitude = amplitude;
}
void CQuantizer::setType(CQuantizer::qType type)
{
m_type = type;
switch (m_type) {
case CQuantizer::lin_midtread:
processSampleFunc = &CQuantizer::processOneSampleLinMidtread;
break;
case CQuantizer::lin_midrise:
processSampleFunc = &CQuantizer::processOneSampleLinMidrise;
break;
}
}
float CQuantizer::processOneSampleLinMidtread(float in)
{
// upscaling
float out = std::roundf( (in / m_amplitude) * powf(2.f, m_nBits - 1.f));
// check for upper boundary
if (out > powf(2.f, m_nBits - 1.f) - 1.f) {
out = powf(2.f, m_nBits - 1.f) - 1.f;
}
// check for lower boundary
if (out < -powf(2.f, m_nBits - 1.f)) {
out = -powf(2.f, m_nBits - 1.f);
}
// downscaling
return m_amplitude * out / powf(2.f, m_nBits - 1.f);
}
float CQuantizer::processOneSampleLinMidrise(float in)
{
// upscaling
float out = std::round((in / m_amplitude) * powf(2.f, m_nBits - 1.f) + 0.5f) - 0.5f;
// check for upper boundary
if (out > powf(2.f, m_nBits - 1.f) - 0.5f) {
out = powf(2.f, m_nBits - 1.f) - 0.5f;
}
// check for lower boundary
if (out < -powf(2.f, m_nBits - 1.f) + 0.5f) {
out = -powf(2.f, m_nBits - 1.f) + 0.5f;
}
//downscaling
return m_amplitude * out / powf(2.f, m_nBits - 1.f);
}
In my processing methods, i feel like there is too much going on: is there a smarter way for me to round these values and ensure they are within a set range?
(The effect I'd like to achieve with this class is that for a given amplitude, there can only be \$2^{\text{nBits}}\$ possible output values within a certain range.)