Map/Scale Param to Arbitrary Range


#1

This should be really simple.. shouldn't it?

I'm looking for a way to scale the result of combining a param (frac32.u.map) and an inlet (frac32.positive) to a positive integer between 0 and an arbitrary positive integer maximum value.

I've got as far as:

int32_t result = __USAT(param_a + inlet_b, 27);

In floating-point, you would do it like (super-simple):

result = val * out_max / in_max

but how do you do it in fixed-point?

I'm really sorry to ask such a stupid question. I know this stuff must be very basic. I'm trying to read around it, but nothing about fixed-point maths is making much sense to me right now, unfortunately.

a|x


#2

So I managed to get it to work, but only by casting to float, then back to int32_t.
This does the job:

int32_t map(int32_t val, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max) {
	float x		= (float)val;
	float inMin	= (float)in_min;
	float inMax	= (float)in_max;
	float outMin	= (float)out_min;
	float outMax	= (float)out_max;	
	return (int32_t)((x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin);
}

According to this little test patch I made, it uses 9 cycles. That seems a bit high, for something so simple, so any clues on a fixed-point version still gratefully accepted.

I'm including the file, too, in case anyone wants to have a quick fiddle.

a|x

range-test.axp (3.0 KB)


Help for scale object needed!
#3

A little bump here, too.
If anyone could tell me way to do this without casting to/from float, that would be great! I'm sure that would be more efficient than my method.

Also, if it could be made to work with signed values, too, that would be even better!

a|x


#4

I created custom objects for scaling/mapping fractional positive input to either fract positive or integer positive output.
There are versions with attributes and parameters to set min/max values.

I'd add them to the library, but I'd rather wait until I can eliminate the float casting, so here's a patch, with the objects embedded as object/patches.

The integer output ones might be useful for creating indices for tables, or multi/demultilplexer objects.

a|x

scale and range.axp (22.7 KB)


#5

A fixed point version would need a long division (I'll not get into that now).

For the cases where the max and min limits are attributes, you can precompute the complement of the divisor in the init section, and then swap the division for a multiplication, since division is equivalent to multiplication by the complement:

x / y = x * 1 / y

That would save 13 CPU cycles since a VDIV instruction takes 14 cycles and a VMUL instruction takes 1 cycle.


#6

@DrJustice cool, I'll see if I can do that.
I'm not sure if I trust the cyclecounter object at all, now. If you look at the screenshot above, all the objects are apparently taking less than 10 cycles, and that's with the floating-point casting and calculations.

It doesn't sound like that can be right, from what you're saying.

a|x


#7

Ooops... the division compiles to an FDIVS instruction - perhaps that's single cycle? (I can't find the cycle count for FDIVS with some quick Googling here, although a description of the "DS" execution unit for another ARM variant says 15 cycles). My bad for assuming VDIV.

Anyway the trick of precomputing the complement of a constant (or a seldom updated variable) is an age old optimization trick, since division usually takes longer than a multiplication on simpler processors.

Very useful functions, BTW! :slight_smile:


#8

Cool. The variable the scale depends on is set as an attribute in the object I was originally intending to use this in, so I could easily precomputed the divisor in the init function.

I assume we're talking a float divisor here, rather than an integer, since it had to be less than 0.

Is casting to/from float expensive in itself? I'm imagining not, but I might be wrong.

a|x


#9

Yes, use floats in the init section, something like:
float my_complement = 1.0f / (float) my_divisor;

The float conversions aren't particularly expensive, 1 cycle for the conversion + possibly a 1 cycle register move or load depending on where the data comes from, and you'd usually have to fetch the data from somewhere anyway. When your variables are floats, some things go quicker than with fixed point since you don't have to scale things up and down with left and right sifts (<< and >>). Fixed point arithmetic can actually be numerically better than floating point in many cases. For optimal performance one would have to consider these things carefully on a case by case basis, but it's not super critical for little things like this.

I've not read up on these new fangled ARM CPUs and their various floating points extensions (my ARM experience is a bit vintage), so I'm not too sure what's best these to do these days - would have to trawl docs and write test software...

If we're lucky, Johannes will chime in with some advice.