Scale Volume (0-1) to Logarithmic Volume (0-1)

Status
Not open for further replies.

JayShoe

Well-known member
Hello,

I would like to control the audio mixers logarithmically. I figured I would create a function, that when called will scale the normal volume (float) to a logarithmic volume. I'm looking to do this.

Code:
volumeLogarithmic = VolumeToLogarithmic(volume); // Will rescale the volume to a (pseudo) logarithmic curve

I don't need a perfect log, and I don't want to add the math library to the project. I just want a reasonably scaled step size, probably around maybe 127 steps? I also figured I could rescale the steps based on my slider input etc.

I'm considering a lookup table, or some type of formula that will do this. There aren't many examples that I've found so far that make this easy. Anyone have any idea what this function could look like?

Code:
float VolumeToLogarithmic(float volume) 
{
// ???
return volumeLogarithmic;
}

Thanks!
Jay
 
Last edited:
I believe you should at least create a first prototype using the math library functions like logf() and expf(). Whatever effect you're trying to accomplish might end up being judged more on subjective than a pure math basis. Best to at least make sure you really have the input-to-output curve that truly suits your needs before adding the complexity of optimizations.

When you do have the math the way you want, you could simply write a loop to populate a table. Maybe print the numbers to the serial monitor, and copy them right into a const array in your code. Using the same math lib prototype you verified with real listening tests will mean you only have to do this tedious step once.

If you want to make the most of a small lookup table, you can find very fast linear interpolation code in the audio library sine wave synthesis.

But the very first step is to get it working with the easy math lib functions. I can't emphasize enough the need for real listening tests. Do that before you make everything so much more complicated & difficult to change with speed optimizations.
 
No need for any complicated stufff... this works great:


Code:
static float VolumeToAmplification(int volume) 
//Volume in the Range 0..100
{
 /*
    https://www.dr-lex.be/info-stuff/volumecontrols.html
  
    Dynamic range   a           b       Approximation
    50 dB         3.1623e-3     5.757       x^3
    60 dB         1e-3          6.908       x^4
    70 dB         3.1623e-4     8.059       x^5
    80 dB         1e-4  9.210               x^6
    90 dB         3.1623e-5     10.36       x^6
    100 dB        1e-5          11.51       x^7
*/


float x = volume / 100.0f; //"volume" Range 0..100


#if 0
  float a = 3.1623e-4;
  float b = 8.059f;
  float ampl = a * expf( b * x );
  if (x < 0.1f) ampl *= x*10.0f;
#else  
  //Approximation:
  float ampl = x * x * x * x * x; //70dB
#endif  


  return ampl;
}
Note: the code takes values from 0..100 so you might want to adjust it a bit.
The Approximation is good enough, and is very fast (a multiplication takes only one machine-cycle).

The 70dB were good for me, but you can experiment with different formulas:
2021-01-01 20_27_45-Start.png

Edit: It might be needed to saturate the resulting samples(=after multiplying them with the result from above fu nc).
The ARM Cortex has a nice assembler-command for this: SSAT
 
Last edited:
Hello everyone,

Thanks for your response. This is all very helpful.

No need for any complicated stufff... this works great:

I read that article but couldn't understand it clearly enough to write the function you've provided. Thanks! I've started messing with the code and it does work.

One issue that I'm running into is realizing that the control I'm trying to control is actually an "attenuation" knob. On my DAC it has a 0 - 255 bit register. 255 is -103dB. 201 is 0dB. and 0 is +48dB gain. So in my case I would need to work out how to have an attenuation that runs from 201 to 0, or some similar scale. What I found was that 103dB attenuation is too much. You can't hear anything at around 40-50dB attenuation. So what I did in my project was I now have a "range" setting that is written into the drivers. This allows you to set the range of the volumePCM5242.

Code:
bool AudioControlpcm5242::volumeSetRange(select_wire wires, device dev, channel ch, int setRange)
{
  range = setRange;
  return true;
}


bool AudioControlpcm5242::volumeSetGain(select_wire wires, device dev, channel ch, float setGain) {
  // Save gain and then change the volume

  if (ch == 0){
  gain1 = setGain;
  gain2 = setGain;
	if(debugToSerialPCM5242) Serial.print("Digital Gain PCM5242 ch0: "); Serial.println(gain1);
  volumePCM5242(wires, dev, both, volume1);
  }
  if (ch == 1){
  gain1 = setGain;
	if(debugToSerialPCM5242) Serial.print("Digital Gain PCM5242 ch1: "); Serial.println(gain1);
  volumePCM5242(wires, dev, left, volume1);
  	}
  if (ch == 2){
  gain2 = setGain;
	if(debugToSerialPCM5242) Serial.print("Digital Gain PCM5242 ch2: "); Serial.println(gain2);
  volumePCM5242(wires, dev, right, volume2);
  }
  return true;
}


bool AudioControlpcm5242::volumePCM5242(select_wire wires, device dev, channel_left_right ch, float level)
{
   if(debugToSerialPCM5242)  Serial.println("AudioControlpcm5242::volumePCM5242(select_wire wires, device dev, channel_left_right ch,float level)");
  
  //normalize 0-1 to 48-255 for digital attenuation | 0 - 47 is digital gain
  //int volume = 255 - ((level * (207 + gain1)) );
  // if(debugToSerialPCM5242) Serial.print("volumePCM5242: "); Serial.println(volume);  

  int attenuation;

  switch (ch)   {

  case left:
    if(debugToSerialPCM5242) Serial.println("Write Left Volume");
    // // Save and set the current volume + gain settings. 
    volume1 = level; // Save the current Digital Volume Level. 
    attenuation = (range - ((level * (range + gain1)) -48) );
    Serial.print("gain: ");Serial.println(gain1);
    Serial.print("range: ");Serial.println(range);
    Serial.print("attenuation: ");Serial.println(attenuation);
      // // Mute when at the bottom of the range
          if (attenuation - 48 == range + gain1)
          {
            attenuation = 255;
          }
    writeRegister(wires, dev, Page_00, 0x3C,  0x00);  // 00: The volume for Left and right channels are independent
    writeRegister(wires, dev, Page_00, 0x3D,  attenuation); // REG_DIGITAL_VOLUME_LEFT
  break;

  case right:
    if(debugToSerialPCM5242) Serial.println("Write Right Volume");
    // // Save and set the current volume + gain settings. 
    volume2 = level; // Save the current Digital Volume Level. 
    attenuation = (range - ((level * (range + gain2)) -48) );
    Serial.print("gain: ");Serial.println(gain1);
    Serial.print("range: ");Serial.println(range);
    Serial.print("attenuation: ");Serial.println(attenuation);
      // // Mute when at the bottom of the range
          if (attenuation - 48 == range + gain1)
          {
            attenuation = 255;
          }
    writeRegister(wires, dev, Page_00, 0x3C,  0x00);  // 00: The volume for Left and right channels are independent
    writeRegister(wires, dev, Page_00, 0X3E,  attenuation); // REG_DIGITAL_VOLUME_RIGHT     
  break;

  case both:
    if(debugToSerialPCM5242) Serial.println("Write Left/ Volume");
    // // Save and set the current volume + gain settings. 
    volume1 = level; // Save the current Digital Volume Level. 
    volume2 = level; // Save the current Digital Volume Level. 
    attenuation = (range - ((level * (range + gain1)) -48) );
    Serial.print("gain: ");Serial.println(gain1);
    Serial.print("range: ");Serial.println(range);
    Serial.print("attenuation: ");Serial.println(attenuation);
      // // Mute when at the bottom of the range
          if (attenuation - 48 == range + gain1)
          {
            attenuation = 255;
          }
    if(debugToSerialPCM5242) Serial.println("Write Left/Right Volume");
    writeRegister(wires, dev, Page_00, 0x3C,  0x01);  // 01: Right channel volume follows left channel setting
    writeRegister(wires, dev, Page_00, 0x3D,  attenuation); // Write Left/Right Volume
  break;
  }

  if(debugToSerialPCM5242) Serial.println("AudioControlpcm5242::volumePCM5242(select_wire wires, device dev, channel_left_right ch,float level)"); 
	if(debugToSerialPCM5242) Serial.println();
  return true;
}

Calling those functions a) set the range and the gain amount allowed on the scale and b) control the 0 - 1 scale within the range. I also have it set that when volume "level" is 0, instead of stopping at 60dB attenuation (or whatever the calculated result is), it goes all the way down to 103dB effectively muting it (although there is also a mute function that I might also add in).

This is how a normal mixer would work... The gain at the top specifies the overall range, more gain allows for a larger range of volume - higher at the top but still zero at zero.

After doing this, it turns out that a logarithmic slider is not as important as I thought it once was when I was sliding from 0dB to 103dB. As Paul pointed out, it's important to test it because I'm happy with the results as they stand now. I set the range to 80 (~ 40dB) and it sounds good. Today, I am thinking about modifying your code to control the range as specified logarithmically. The problem I was having yesterday was that I was getting only about 50% of the slider down and it was already hitting zero. I think something is backwards, and I suspect it's regarding the difference between attenuation and volume, as well as the "scale" that the logarithmic function needs specified. I'm not sure reversing the log will work (IE log = 1 - log). So i have to think about the math. :)

This is what I've been working with... Trying to create a simple library to simply get a logarithmic volume control function wherever needed.

Code:
#include "level2Log.h"


float level2Log::process(float level)
{
//Volume in the Range 0..100

//https://forum.pjrc.com/threads/65695-Scale-Volume-(0-1)-to-Logarithmic-Volume-(0-1)?p=265249&viewfull=1#post265249

 /*
    https://www.dr-lex.be/info-stuff/volumecontrols.html
  
    Dynamic range   a           b       Approximation
    50 dB         3.1623e-3     5.757       x^3
    60 dB         1e-3          6.908       x^4
    70 dB         3.1623e-4     8.059       x^5
    80 dB         1e-4  9.210               x^6
    90 dB         3.1623e-5     10.36       x^6
    100 dB        1e-5          11.51       x^7
*/


float x = level; //"volume" Range 0..1


#if 0
  float a = 1e-5;
  float b = 11.51;
  float ampl = a * expf( b * x );
  if (x < 0.1f) ampl *= x*10.0f;
#else  
  //Approximation:
  float ampl = x * x * x * x * x; //70 dB
#endif  

if (ampl >= 1) ampl = 1;

// Serial.print("amplitude: "); Serial.println(ampl);

return ampl;
}

float level2Log::processReverse(float level)
{
level = 1 - level;
//Volume in the Range 0..100

//https://forum.pjrc.com/threads/65695-Scale-Volume-(0-1)-to-Logarithmic-Volume-(0-1)?p=265249&viewfull=1#post265249

 /*
    https://www.dr-lex.be/info-stuff/volumecontrols.html
  
    Dynamic range   a           b       Approximation
    50 dB         3.1623e-3     5.757       x^3
    60 dB         1e-3          6.908       x^4
    70 dB         3.1623e-4     8.059       x^5
    80 dB         1e-4  9.210               x^6
    90 dB         3.1623e-5     10.36       x^6
    100 dB        1e-5          11.51       x^7
*/


float x = level; //"volume" Range 0..1


#if 0
  float a = 1e-5;
  float b = 11.51;
  float ampl = a * expf( b * x );
  if (x < 0.1f) ampl *= x*10.0f;
#else  
  //Approximation:
  float ampl = x * x * x * x * x; //70dB
#endif  

if (ampl >= 1) ampl = 1;

// Serial.print("amplitude: "); Serial.println(ampl);

return ampl;
}

Code:
#ifndef level2Log_h_
#define level2Log_h_

class level2Log
{
public:
float process(float level);
float processReverse(float level);
protected:
private:
};

#endif

This way I can just do this if I want.

Code:
volumePCM5242(wire, A, both, level2Log.processReverse(level));

But the processReverse I don't think is right... If I get anywhere with this I'll post it. If anyone knows how to reverse the code to get a better "attenuation" curve please let me know.

Jay
 
The problem I was having yesterday was that I was getting only about 50% of the slider down and it was already hitting zero.

Maybe use the Arduino map() function to first linearly scale your input to the range you want to put into that logarithmic function.

https://www.arduino.cc/reference/en/language/functions/math/map/

Teensy's version of map() works the same as Arduino, but it has a nice extension for floating point. If you put your number into a "float" or "double" variable, then map() does all its work using floating point math, so you get a more accurate result than the normal way of rounding/truncating to the nearest integer. So even if your initial input is an integer, put it into a float and give that variable to the input of map(). Then give map()'s output to the logarithmic function. And again, if the logarithmic behavior is as you like but you just need the output linearly scaled to a different range, if modifying the non-linear code isn't simple, you can just use map() again to linearly scale it to any range you like.
 
Status
Not open for further replies.
Back
Top