Using map() with Midi controller pots

Status
Not open for further replies.

MatrixRat

Well-known member
Greetings everyone.
Thanks in advance for reading. Am working on an ever evolving Midi controller seeking advice using the map() function for pot calibration.
Before I added map() to the mix, I was getting readings 0 - 1023 from the pots.
In the following code, the pot on A0 outputs values 1=1021. One of my intended Midi target devices wants to see 0 - 1023. I'm sure I can figure a quick and dirty kludge for that one application however I'd like to get the math right as here lurks a bunch of divides and truncated remainders. Am a newcomer to C and all the C's that follow and math ain't one of my strengths..
Hopefully the code is reasonably self-explanatory.

Code:
// This runs and works, stripped to the guts, displaying output on the Serial Monitor. Board type T3_2, USB type Serial. 
// Hook a pot to A0. You can leave A1 floating as is muted See OUTPUT_TYPE
// From Many Knobs Buttons in Teensy examples. Added arrays for per-element Range, Scaling etc.
#include <ResponsiveAnalogRead.h>
const int      A_PINS  = 2; // number of Analog PINS
const int      ANALOG_PINS   [A_PINS] = {  A0,   A1};
const uint16_t CCID          [A_PINS] = {   0,    1}; //uint16_t coz we might be sending NRPN#.
const uint16_t CENTER_VALUE  [A_PINS] = { 511,  511}; //Useful for Center detent pots were detent spot is not at electrical center.
const uint16_t DETENT_WIDTH  [A_PINS] = {  40,    0}; //Allows a dead zone to give us room to climb out of the detent.
const uint16_t INPUT_MINIMUM [A_PINS] = {   0,    0}; //Useful for pots where wiper does not reach track Min.
const uint16_t INPUT_MAXIMUM [A_PINS] = {1023, 1023}; //  "     "   "     "     "     "   "    "     "   Max. 
const uint16_t OUTPUT_MINIMUM[A_PINS] = {   0,    0}; //Useful if you have a string of equal value resistors and say a 6 pos rotary switch.
const uint16_t OUTPUT_MAXIMUM[A_PINS] = {1023,  127}; //You set OUTPUT_MINIMUM to 0 and OUTPUT_MAXIMUM to 5.
const byte     OUTPUT_TYPE   [A_PINS] = {   3,    0}; //0=Muted,1=7-bitCC,2=14-bitCC, 3=14-bit NRPN.
const byte     OUTPUT_CHANNEL[A_PINS] = {  14,   14};
//
uint16_t data[A_PINS];
uint16_t dataLag[A_PINS]; // when lag and new are not the same then update MIDI CC value
ResponsiveAnalogRead analog[] {
                  {ANALOG_PINS[0], true},
                  {ANALOG_PINS[1], true},
                              };
void setup() {  Serial.begin(115200);  }
void loop()  {  getAnalogData();       }

void getAnalogData() {  for (int i = 0; i < A_PINS; i++)
  {        analog[i].update();    // update the ResponsiveAnalogRead object every loop
       if (analog[i].hasChanged())// if the repsonsive value has change then do something with it.
    {
      data[i] = (analog[i].getValue());
//******************** SUBJECT OF QUERY ****************************      
      data[i] = constrain(data[i], INPUT_MINIMUM[i], INPUT_MAXIMUM[i]);
      {
        if ((data[i]) < (CENTER_VALUE  [i]) - (DETENT_WIDTH  [i]))
             {
         (data[i]) = map((data[i]), (INPUT_MINIMUM [i]), (CENTER_VALUE [i]) - (DETENT_WIDTH [i]) , (INPUT_MINIMUM [i]) , (CENTER_VALUE [i]));
             }
      else if ((data[i]) > (CENTER_VALUE  [i]) + (DETENT_WIDTH [i]))
             {
         (data[i]) = map((data[i]), (CENTER_VALUE [i]) + (DETENT_WIDTH [i]), (INPUT_MAXIMUM [i]), (CENTER_VALUE [i]), (INPUT_MAXIMUM [i]));
             }
      else if ((data[i]) < (CENTER_VALUE [i]) + (DETENT_WIDTH [i]) || (data[i]) >= (CENTER_VALUE [i]) - (DETENT_WIDTH [i]))
             {
        (data[i]) = (CENTER_VALUE [i]);
             }
         (data[i]) = map((data[i]), (INPUT_MINIMUM [i]), (INPUT_MAXIMUM [i]), (OUTPUT_MINIMUM [i]), (OUTPUT_MAXIMUM [i]));
      }
      
//************************** END OF SUBJECT ******************************
    if (data[i] != dataLag[i])
      {
      dataLag[i] = data[i];
      Serial.print(data[i]); Serial.println();
    }}}}

Have migrated From Mega and have horsepower to spare here so don't mind using machine cycles for the math.
 
The Arduino map() is not optimal; see this post of mine. In particular, the integer version does not return the rounded values of the floating-point version; mine does.

Let's reconstruct the mappings from your intent. We'll define
  • input_min for the minimum potentiometer reading
  • input_max for the maximum potentiometer reading
  • input_center for the potentiometer reading at the center
  • input_width for half the range of values in potentiometer reading that corresponds to the center
  • output_min for the output minimum value
  • output_center for the output value at potentiometer center
  • output_max for the output maximum value
noting that normally, input_center=(input_min+input_max)/2 if the potentiometers are linear, and the response on both small and large sides are to have the same slope. For adjustment of input_center to be intuitive, output_center=(output_min+output_max)/2 . You only set output_center if you want the low half and high half to have different ranges, similar to how cheap logarithmic pots are implemented using two linear ranges.

In other words,
  1. input_min .. input_center-input_width output_min .. output_center
  2. input_center-input_width .. input_center+input_width output_center
  3. input_center+input_width .. input_max output_center .. output_max

Rewriting OP's code, dropping the indices, and using the above variable names and my preferred code style (I just find mine more readable; others disagree!), we get
Code:
        input = constrain(input, input_min, input_max);
        if (input < input_center - input_width) {
            output = map(input, input_min, input_center-input_width, input_min, input_center);
        } else
        if (input > input_center + input_width) {
            output = map(input, input_center + input_width, input_max, input_center, input_max);
        } else
        if (input < input_center + input_width || input >= input_center - input_width) {
            output = input_center;
        }
    output = map(output, input_min, input_max, output_min, output_max);
There are three issues with the code:
  1. input_center+input_width is not caught by the if clauses
  2. The third check is unnecessary, it should be just a straightforward else .
  3. Remapping twice introduces quantization error. It is better to map to the output range directly.
    Furthermore, the latter mapping uses the same slope for both sides, so the choice of input center just moves the dead zone, it does not make it centered.
I would rewrite the getAnalogData() function something along the lines of
Code:
void getAnalogData()
{
    for (int i = 0; i < A_PINS; i++) {
        analog[i].update();
        if (!analog[i].hasChanged())
            continue;

        const int32_t  OUTPUT_CENTER = (OUTPUT_MIN[i] + OUTPUT_MAX[i]) / 2;
        const int32_t  input = analog[i].getValue();

        if (input <= INPUT_MINIMUM[i]) {
            data[i] = OUTPUT_MINIMUM[i];
        } else
        if (input < INPUT_CENTER[i] - INPUT_WIDTH[i]) {
            data[i] = map(input, INPUT_MINIMUM[i], INPUT_CENTER[i]-INPUT_WIDTH[i], OUTPUT_MINIMUM[i], OUTPUT_CENTER);
        } else
        if (input <= INPUT_CENTER[i] + INPUT_WIDTH[i]) {
            data[i] = OUTPUT_CENTER;
        } else
        if (input < INPUT_MAXIMUM[i]) {
            data[i] = map(input, INPUT_CENTER[i]+INPUT_WIDTH[i], INPUT_MAXIMUM[i], OUTPUT_CENTER, OUTPUT_MAXIMUM[i]);
        } else {
            data[i] = OUTPUT_MAXIMUM[i];
        }

        if (data[i] != dataLag[i]) {
            dataLag[i] = data[i];
            Serial.println(data[i]);
        }
    }
}
I would also use lowercase names for the constants, as I only use uppercase names for preprocessor macros, but that's just my coding style. Also INPUT_WIDTH is really input center halfwidth, maximum deviation from the input center in input units that still yields the center output value. It might be useful to point that out in the comment.
 
Thank you Nominal Animal for your thoughtful response. I've not considered using log pots and usually don't if looking for decent behavior in audio for level or pan. Even so-called linear pots are anything but at either end of the resistance track so in this kind of application I'd be looking at an input range of say 8 - 1015. In reality, not all pots are created equal, especially consumer grade hence my desire for individual tweakability.
Center detent linear are of interest as a future project is going to be a panel with at least a hundred pots for tweaking a Roland digital Synth. Some will be center detent and like any other pot are not created equal, which is why I want to move the center point as well.
I hadn't thought of dual slope and now you mention it, I think there's a few places in the Roland where it would be very useful.
Thank you for those code and readability tips, will explore and see what can be learned by using them.
All the best.
 
Thank you again Nominal Animal.
Following on from my previous post, I've played with and implemented your suggested code and it works. The quantization error you highlighted had made itself obvious with noisy output so I'd figured I was using noisy math.

My next exercise is to apply it to one of my creations. Six noisy Megas, five noisy led displays, One Mega as the controller squished into a small space.

I checked out your link re
Code:
template <class T, class A, class B, class C, class D>
long map(T _x, A _x_min, B _x_max, C _out_from, D _out_to, typename std::enable_if<std::is_integral<T>::value >::type* = 0)
{
	long x = _x, x_min = _x_min, x_max = _x_max, out_from = _out_from, out_to = _out_to;
	long x_range = x_max - x_min, out_range = out_to - out_from, r;

	// (1 - abs(r/x_range)) is the smallest fraction rounded away from zero.
	// The sign of r is the same as the sign of x_range*out_range.
	if (x_range > 0) {
		if (out_range > 0) {
			r = (x_range - 1) / 2;
		} else
		if (out_range < 0) {
			r = (1 - x_range) / 2;
		} else {
			r = 0;
		}
	} else
	if (x_range < 0) {
		if (out_range > 0) {
			r = (x_range + 1) / 2;
		} else
		if (out_range < 0) {
			r = (-1 - x_range) / 2;
		} else {
			r = 0;
		}
	} else {
		r = 0;
	}

	return out_from + ((x - x_min) * out_range + r) / x_range;
}
and get the gist that it is an alternative to the Arduino map() function that's smart enough to recognise and use incoming variable types. Not having encountered template<> yet, Ive got my hand up for guidance as to how to implement it. Please?
 
Change the second word on the second line, map, to say my_map, then copy-paste all of it to your Arduino source file, before the code that uses it (so, before getAnalogData function). In your own code (at least in the getAnalogData function, use my_map instead of map. That should do it.
 
Have just finished testing the same mods on an earlier MEGA based controller. Cleaned it right up. Next, copy paste same into another MEGA based rig featuring many more pots, multiplexers and a pile of buttons. When done will get back to the Teensy one and have a play with my_map.

Many thanks and have a great day.
 
Status
Not open for further replies.
Back
Top