API Reference

Every public symbol, grouped by topic. Each entry lists the radix convention, the precision, and the error / saturation behaviour. All types are from FR_defs.h: s8 s16 s32 s64 for signed and u8 u16 u32 u64 for unsigned integers (these are aliases for the <stdint.h> types).

Reading this reference

Most entries list inputs, output, radix handling and precision separately, because in a mixed-radix library those four things are what actually lets you plan an arithmetic pipeline without hidden quantisation. If you are new to fixed-point, the Fixed-Point Primer explains the notation first; come back here once you’re comfortable reading s15.16 and s0.15.

Radix convention — the deliberate choice

FR_Math never stores the radix alongside the value. A fixed-point number in this library is just an s16 or s32; the radix lives in the caller’s head (or in a #define). That sounds scary, but it’s the reason the library is useful on small cores:

The rule of thumb is: every function that accepts a radix argument interprets all of its fixed-point inputs and outputs at that radix, unless the entry says otherwise. When two functions in a pipeline use different radixes, use FR_CHRDX(value, from_r, to_r) to rebase the value explicitly — that is the single chokepoint for conversion and the one place rounding happens.

A concrete example of the discipline:

/* Good: all three values live at radix 16 until we commit. */
s32 a   = I2FR(3, 16);                /* 3.0 at s15.16   */
s32 b   = FR_sqrt(I2FR(2, 16), 16);   /* sqrt(2) at s15.16 */
FR_ADD(a, 16, b, 16);                 /* a += b, both at radix 16  */

/* Commit back to an integer count at the end.
 * FR2I truncates; for round-to-nearest, add half an LSB first:
 *   a += (1 << (16 - 1));
 */
int n = FR2I(a, 16);

You never see a radix field on any of these values. The shifts live in the macro parameters. That’s the deal the library makes with you: pay attention to the radix at each call site, and in return get float-like ergonomics with integer-only codegen.

Types and constants

Integer type aliases

SymbolMeaning
s8 s16 s32 s64Signed integer typedefs (int8_t, int16_t, int32_t, int64_t).
u8 u16 u32 u64Unsigned integer typedefs (uint8_t, uint16_t, uint32_t, uint64_t).

Sentinel return values (FR_defs.h)

SymbolValueUsed by
FR_OVERFLOW_POS0x7FFFFFFF (INT32_MAX)Saturating ops when the true result exceeds +231.
FR_OVERFLOW_NEG0x80000000 (INT32_MIN)Saturating ops when the true result is below −231.
FR_DOMAIN_ERROR0x80000000 (INT32_MIN)Functions with an invalid input, e.g. FR_sqrt(-1), FR_log2(0), FR_asin(2.0). Shares the bit pattern of FR_OVERFLOW_NEG, so don’t mix a ≤ FR_OVERFLOW_NEG check with a domain check — test for the exact sentinel.

Common numerical constants (FR_math.h)

All constants below are stored at radix 16 (s15.16). The prefix FR_k means “constant”; FR_kr means “reciprocal” (1/x). If you use them at a different radix, change radix once with FR_CHRDX rather than re-computing.

SymbolValueMeaning
FR_kPREC16The radix of every FR_k* constant in this file.
FR_kE178145e ≈ 2.71828.
FR_krE241091/e ≈ 0.36788.
FR_kPI205887π ≈ 3.14159.
FR_krPI208611/π ≈ 0.31831.
FR_kDEG2RAD1144π/180 ≈ 0.01745.
FR_kRAD2DEG3754936180/π ≈ 57.29578.
FR_kQ2RAD102944π/2 ≈ 1.57080 (1 quadrant in radians).
FR_kRAD2Q417222/π ≈ 0.63662.
FR_kLOG2E94548log2(e) ≈ 1.44270.
FR_krLOG2E454261/log2(e) = ln(2) ≈ 0.69315.
FR_kLOG2_10217706log2(10) ≈ 3.32193.
FR_krLOG2_10197281/log2(10) = log10(2) ≈ 0.30103.
FR_kSQRT292682√2 ≈ 1.41421.
FR_krSQRT2463411/√2 ≈ 0.70711.

Conversions and scalar macros

Integer ↔ fixed-point

The bridge between a plain int and an FR_Math fixed-point value is just a shift, but the library gives it names so call sites read as intent:

MacroInputsOutputEffect
I2FR(i, r) i: integer; r: target radix in bits s32 at radix r (i) << (r). No bounds check. Use when you know |i| fits in 32 − r signed bits.
FR2I(x, r) x: fixed-point at radix r integer (x) >> (r). Truncates toward −∞ (C’s signed shift). FR2I(-1, 4) == -1, not 0.
FR_INT(x, r) x: fixed-point at radix r integer Truncates toward zero. FR_INT(-1, 4) == 0. Useful when you want C’s normal integer-cast behaviour.
FR_NUM(i, f, d, r) i: integer part; f: decimal fraction digits; d: number of digits in f; r: target radix s32 at radix r Build a fixed-point literal from decimal. FR_NUM(12, 34, 2, 10) is 12.34 at s.10. Rounds toward zero; for round-to-nearest, add half an LSB at the call site.
FR_numstr(s, r) s: null-terminated decimal string (e.g. "3.14159"); r: target radix s32 at radix r Runtime string-to-fixed-point parser (inverse of FR_printNumF). Handles signs, leading whitespace, and leading-zero fractions like "0.05". Up to 9 fractional digits. No malloc, no strtod, no libm. Returns 0 for NULL or empty input.
FR2D(x, r) x: fixed-point at radix r double Debug-only: x / (double)(1 << r). Pulls in libm — compile it out of release builds.
D2FR(d, r) d: double; r: target radix s32 at radix r Debug-only: (s32)(d * (1 << r)). Same caveat as above.

Changing radix

MacroInputsOutputNotes
FR_CHRDX(x, r_cur, r_new) x: fixed-point value at radix r_cur same value at radix r_new Expands to (x) >> (r_cur-r_new) if shrinking, or (x) << (r_new-r_cur) if growing. Growing is lossless; shrinking loses the bottom (r_cur−r_new) fractional bits. No rounding.

Fractional part, floor, ceiling

MacroInputsOutputNotes
FR_FRAC(x, r) x: fixed-point at radix r fractional part, at radix r, always ≥ 0 Returns |x| & ((1 << r) - 1). Loses the sign of x.
FR_FRACS(x, xr, nr) x: at radix xr; nr: destination radix fractional part at radix nr Convenience wrapper: fractional part of x, rescaled to nr.
FR_FLOOR(x, r) x: fixed-point at radix r same value at radix r with fractional bits cleared Integer part only, but the result stays at radix r so you can keep using it in the same pipeline.
FR_CEIL(x, r) as above as above Same idea, rounds up.
FR_ABS(x) any signed integer or fixed-point same type, non-negative Conditional: ((x) < 0 ? -(x) : (x)). Evaluates x twice — don’t pass an expression with side effects.
FR_SGN(x) any signed integer or fixed-point 0 or −1 Arithmetic shift of the sign bit. Returns 0 for x ≥ 0 and −1 for x < 0. Not the classic −1/0/+1 sign function.
FR_ISPOW2(x) any integer 0 or nonzero Classic x & (x-1) trick. Returns nonzero when x is a power of two (including 0).

Interpolation

MacroInputsOutputNotes
FR_INTERP(x0, x1, delta, prec) x0, x1: endpoints (any radix, same radix as each other); delta: blend at radix prec in [0, 1]; prec: radix of delta Same radix as x0/x1 x0 + ((x1 - x0) * delta) >> prec. Linear lerp. Extrapolates outside [0, 1]; you own the overflow check.
FR_INTERPI(x0, x1, delta, prec) as above, but delta is unsigned and masked into [0, (1<<prec)) as above The “I” version forces delta into range, so it’s safer when delta comes from an untrusted upstream (e.g. a running counter).

Utility macros

MacroInputsOutputNotes
FR_MIN(a, b) a, b: any comparable values Smaller of the two values Evaluates arguments more than once — do not pass expressions with side effects.
FR_MAX(a, b) a, b: any comparable values Larger of the two values Same caveat as FR_MIN.
FR_CLAMP(x, lo, hi) x: value to clamp; lo, hi: bounds x clamped to [lo, hi] Equivalent to FR_MIN(FR_MAX(x, lo), hi).
FR_DIV(x, xr, y, yr) x: numerator at radix xr; y: denominator at radix yr s32 at radix xr Pre-scales the numerator in a 64-bit intermediate and rounds to nearest (adds half the divisor before truncating, with correct sign handling). Worst-case error ≤ 0.5 LSB. Works correctly across the full Q16.16 range.
FR_DIV_TRUNC(x, xr, y, yr) x: numerator at radix xr; y: denominator at radix yr ((s64)(x) << (yr)) / (s32)(y) Truncating division (rounds toward zero). This was the behaviour of FR_DIV in v2.0.0; use it when you need exact backward compatibility or when the truncation bias is acceptable.
FR_DIV32(x, xr, y, yr) x: numerator at radix xr; y: denominator at radix yr ((s32)(x) << (yr)) / (s32)(y) 32-bit-only truncating path — requires |x| < 2(31 − yr) to avoid overflow. Use on tiny targets (PIC, AVR, 8051) where 64-bit ops pull in unwanted compiler runtime code.
FR_MOD(x, y) x, y: integers or fixed-point at the same radix (x) % (y) Standard C remainder. Sign of the result follows x (C99+).

Arithmetic

FR_Math splits arithmetic into three flavours. The macros (FR_ADD, FR_SUB) are mixed-radix, inline, and wrap on overflow. The s.16 helper functions (FR_FixMuls, FR_FixMulSat, FR_FixAddSat) are hardcoded to radix 16, emit int64_t intermediates, and optionally saturate. And the scalar macros (FR_ABS, FR_SGN) listed above in the conversions section handle sign.

Mixed-radix add/subtract macros

Important: FR_ADD and FR_SUB use compound assignment. They modify their first argument in place — they are not expressions you can chain in a C expression the way you’d use a + b. Think of them as “x += realign(y)”.

MacroInputsOutputEffect / precision
FR_ADD(x, xr, y, yr) x: lvalue at radix xr; y: value at radix yr x is updated in place, still at radix xr x += FR_CHRDX(y, yr, xr). If yr > xr, the bottom yr − xr bits of y are lost. Wraps on overflow — use a wider register or FR_FixAddSat when the sum can exceed range.
FR_SUB(x, xr, y, yr) as above as above Same semantics with subtraction. Wraps on overflow.

Gotcha: FR_ADD(i, ir, j, jr) is not generally equal to FR_ADD(j, jr, i, ir) — because the first operand is the one whose radix the result keeps. The header explicitly calls this out.

Example:

s32 a = I2FR(3, 4);    /* 3.0 at s.4  = 48  */
s32 b = I2FR(2, 8);    /* 2.0 at s.8  = 512 */

FR_ADD(a, 4, b, 8);    /* a now holds 5.0 at s.4 = 80                      */
                       /* b is shifted DOWN from radix 8 to radix 4 first, */
                       /* losing the bottom 4 fractional bits of b.        */

Saturating radix-16 helpers

These are regular s32-returning functions (not macros), and they are hardcoded to radix 16. Use them for the common s15.16 pipeline. If you need a different radix, use FR_CHRDX on the inputs and outputs, or build a thin wrapper.

FunctionInputsOutputPrecision / saturation
s32 FR_FixMuls(s32 x, s32 y) x, y: s15.16 (x × y) at s15.16 Promotes to int64_t, adds 0.5 LSB (+0x8000), shifts right by 16. Rounds to nearest. Wraps on overflow — no clamp. Use when you have a formal bound on the product.
s32 FR_FixMulSat(s32 x, s32 y) as above as above Same round-to-nearest, but clamps to FR_OVERFLOW_POS / FR_OVERFLOW_NEG on over/underflow. Prefer by default.
s32 FR_FixAddSat(s32 x, s32 y) x, y: any signed s32 at the same radix (radix is not rescaled) saturated sum at the same radix Classic sign-watching saturating add: returns FR_OVERFLOW_POS or FR_OVERFLOW_NEG if adding two same-sign values would flip the sign.

Shift-only scaling macros

These macros exist specifically for CPUs without a hardware multiplier (8051, low-end PICs, 68HC11, MSP430E, parts of Cortex-M0+). Each one approximates a multiplication by a floating-point constant using only shifts and adds. They evaluate their argument multiple times, so don’t pass side-effecting expressions. Precision is stated per entry — if you need better, use the multiply-by-FR_kXXX path and shift down by FR_kPREC instead.

MacroApproximatesRelative error
FR_SMUL10(x)× 10exact (it’s (x << 3) + (x << 1)).
FR_SDIV10(x)÷ 10≈ 4e-4.
FR_SLOG2E(x)× log2(e) (≈ 1.4427)≈ 3e-5, for converting pow2 → exp.
FR_SrLOG2E(x)× ln(2) (≈ 0.6931)≈ 3e-5, for converting log2 → ln.
FR_SLOG2_10(x)× log2(10) (≈ 3.3219)≈ 3e-5, for converting pow2 → pow10.
FR_SrLOG2_10(x)× log10(2) (≈ 0.3010)≈ 3e-5, for converting log2 → log10.
FR_DEG2RAD(x)× π/180≈ 1.6e-4.
FR_RAD2DEG(x)× 180/π≈ 2.1e-6.
FR_RAD2Q(x)× 2/π (radians → quadrants)small — used on the legacy quadrant path.
FR_Q2RAD(x)× π/2 (quadrants → radians)small.

These are the “quadrant macros” from the v1 API. They were written for chips where a 32-bit multiply was measured in dozens of cycles and a shift was one cycle. They still have value today on the smallest MCUs and any time you want an angle conversion that can’t accidentally overflow an intermediate.

Angle conventions and BAM

FR_Math supports three ways to express an angle. Every trig function in the library eventually reduces to one of these and funnels through a single core routine, so the only reason there are multiple APIs is your convenience.

Why u16 for BAM (not s32)?

The choice is deliberate. BAM is designed to be the state of a phase accumulator, and u16 gives you modular wraparound for free. Every time you do phase += increment on a u16, the result is implicitly taken modulo 65536 — which is exactly one full revolution. No glitch at the wrap, no explicit reduction step, no conditional branch.

An s32 would lose this property. You would have to write phase = (phase + increment) & 0xFFFF on every tick, the sign bit would be a trap, and the upper 16 bits would be dead weight. 16 bits is also an exact match for the table’s useful resolution: 2 quadrant bits + 7 index bits + 7 interpolation bits = 16. Going wider would only add noise, not precision.

“But what if I want to pass in any signed angle without worrying about conversion?” That is exactly what FR_CosI(deg), FR_Cos(deg, radix), and fr_cos(rad, radix) are for. All three take signed inputs and reduce them to BAM for you. The only place you actually see a u16 is at the internal fr_cos_bam / fr_sin_bam boundary, which you only call by hand if you want a phase accumulator (and at that point, u16 is the right type anyway).

Conversion macros

Four macros cover every conversion between the three representations:

MacroDirectionWorst-case error
FR_DEG2BAM(deg)integer degrees → BAM≤ 0.5 LSB BAM (≈ 0.0028°), no accumulation.
FR_BAM2DEG(bam)BAM → integer degreesTruncation only (± 0.5°).
FR_RAD2BAM(rad, r)radians (at radix r) → BAM≈ 0.036% (constant 10430 ≈ 65536 / 2π).
FR_BAM2RAD(bam, r)BAM → radians (at radix r)≈ 0.006% (constant 6434 ≈ 2π × 1024).

Worked example: keeping precision on chips without a multiplier

The constants in these macros are chosen so a small-MCU compiler — or a human writing assembly — can drop back to pure shift-and-add and lose no precision. This is a technique that pre-dates hardware multipliers and may not be obvious to newer programmers, so it’s worth walking through.

Take FR_BAM2DEG:

#define FR_BAM2DEG(bam)  ((s16)(((s32)(u16)(bam) * 45) >> 13))

Why 45 and 13? Because the degrees-per-BAM ratio is 360 / 65536 = 45 / 8192, and 8192 is 1 << 13. So bam * 45 >> 13 is an exact rational multiply by the ratio, with one rounding step at the end. The constant 45 is small and factors as 32 + 8 + 4 + 1, so on a chip with no multiply instruction the compiler (or you) can expand it into four shifts and three adds:

/* bam * 45 as shift-and-add: 45 = (1<<5) + (1<<3) + (1<<2) + 1 */
u32 x  = ((u32)bam << 5)
       + ((u32)bam << 3)
       + ((u32)bam << 2)
       +  (u32)bam;
s16 deg = (s16)(x >> 13);

Four shifts plus three adds — cheap on an 8051, AVR, or any hand-written DSP inner loop — and the answer has at most ±0.5 LSB of truncation error. The same discipline applies to the other direction: in FR_DEG2BAM the divide-by-360 is a compile-time constant, so any optimising compiler folds it into a multiply-by-reciprocal (or, on a weaker toolchain, a runtime call that you can inline yourself).

Takeaway: whenever you see an “oddly specific” constant in FR_Math — 45, 10430, 6434, 0xB505, etc. — assume it was picked so the multiply either collapses to shift-and-add on a small core, or matches a power-of-two shift on the other side of the expression. It’s not magic, it’s just rational arithmetic with the right denominator.

Using the macros

/* Sine of 42 degrees, result in s15.16. */
s32 y = fr_sin_bam(FR_DEG2BAM(42));

/* Phase accumulator at 440 Hz on a 48 kHz stream. */
u16 phase = 0;
u16 inc   = (u16)(((u32)440 << 16) / 48000u); /* BAM per sample  */
for (int n = 0; n < nsamples; ++n) {
    buf[n] = fr_sin_bam(phase);                /* s15.16 sine    */
    phase += inc;                              /* wraps at 65536 */
}

/* Radians (at radix 16) to BAM and back. */
s32 rad_16 = 1L << 16;            /* 1.0 rad */
u16 bam    = FR_RAD2BAM(rad_16, 16);
s32 back   = FR_BAM2RAD(bam, 16);

Trigonometry

Every trig function in FR_Math — integer-degree, radian, or BAM — funnels through a single 129-entry quadrant cosine table, gFR_COS_TAB_Q (128 intervals plus a sentinel for the interpolation). Every wrapper converts its angle to BAM and calls the same core routine. The internal table stores s0.15 values; the output is shifted to s15.16 (radix 16), giving an exact 1.0 representation. Cardinal angles produce exact results: cos(0°) = 65536, sin(90°) = 65536, etc. FR_TRIG_ONE (65536) is the exact 1.0 value at this radix.

BAM-native (the core)

FunctionSignatureOutput
fr_cos_bams32 fr_cos_bam(u16 bam)s15.16, range [−65536, +65536]. Exact at cardinal angles.
fr_sin_bams32 fr_sin_bam(u16 bam)s15.16, range [−65536, +65536]. Exact at cardinal angles. Defined as fr_cos_bam(bam − FR_BAM_QUADRANT).

Radian-native

FunctionSignatureNotes
fr_coss32 fr_cos(s32 rad, u16 radix)rad is interpreted at the given radix. Result is s15.16.
fr_sins32 fr_sin(s32 rad, u16 radix)Same convention. Result is s15.16.
fr_tans32 fr_tan(s32 rad, u16 radix)Returns at radix 16 (FR_TRIG_OUT_PREC). Computed as (sin << 16) / cos; saturates to ±INT32_MAX (FR_TRIG_MAXVAL) near π/2 + kπ where cos → 0.

Integer-degree wrappers (legacy API)

The uppercase legacy API takes an angle in degrees. FR_SinI, FR_CosI and FR_TanI take plain integer degrees — the trailing I denotes integer. The variants without the I suffix (FR_Sin, FR_Cos, FR_Tan) accept a radix argument and treat the degree value as fixed-point, so you can pass fractional degrees like 42.375°.

SymbolSignatureKind
FR_SinIFR_SinI(deg)s32 (s15.16)Macro: fr_sin_bam(FR_DEG2BAM(deg)). Zero-cost inline.
FR_CosIFR_CosI(deg)s32 (s15.16)Macro: fr_cos_bam(FR_DEG2BAM(deg)).
FR_TanIs32 FR_TanI(s16 deg)Function. Returns at radix 16; saturates to ±INT32_MAX near 90° / 270°.
FR_Sins32 FR_Sin(s16 deg, u16 radix)deg is fixed-point at radix. Returns s15.16.
FR_Coss32 FR_Cos(s16 deg, u16 radix)Same.
FR_Tans32 FR_Tan(s16 deg, u16 radix)Returns at radix 16; saturates to ±INT32_MAX near 90° / 270°.

Degree wrappers on the BAM path

If you’re using the lowercase family and want to skip the radix entirely, two convenience macros cover pure integer degrees:

MacroExpansion
fr_cos_deg(deg)fr_cos_bam(FR_DEG2BAM(deg))
fr_sin_deg(deg)fr_sin_bam(FR_DEG2BAM(deg))

Inverse trigonometry

Every inverse-trig function in FR_Math returns the angle in radians as s32 at a caller-specified output radix. This makes the inverse functions symmetric with the forward trig functions. The out_radix parameter lets you choose precision independently of the input radix.

FunctionSignatureOutput range
FR_atans32 FR_atan(s32 input, u16 radix, u16 out_radix)[−π/2, +π/2] radians at out_radix. input interpreted at radix.
FR_atan2s32 FR_atan2(s32 y, s32 x, u16 out_radix)Full-circle [−π, +π] radians at out_radix.
FR_asins32 FR_asin(s32 input, u16 radix, u16 out_radix)[−π/2, +π/2] radians at out_radix. Returns FR_DOMAIN_ERROR for |input| > 1.
FR_acoss32 FR_acos(s32 input, u16 radix, u16 out_radix)[0, +π] radians at out_radix. Same domain check as FR_asin.

Note: To convert the radian result to degrees, use FR_RAD2DEG.

Logarithm and exponential

The logarithm functions are genuinely mixed-radix: they take three arguments — the input value, the input radix, and a separate output radix. That matters because logarithms compress a huge dynamic range into a much smaller one, so the useful fractional precision of the answer is often very different from the radix of the input.

All three logarithms share a single implementation: find the leading bit position of the input (that gives you the integer part of log2), then interpolate a 65-entry mantissa table for the fractional part, then scale the result to the requested output radix. FR_ln and FR_log10 multiply the log2 result by the appropriate radix-28 constant via FR_MULK28 (FR_krLOG2E_28 or FR_krLOG2_10_28) before returning.

Logarithms

Function Inputs Output Domain / precision
FR_log2 s32 input at radix in_r
u16 in_r — input radix
u16 out_r — output radix
s32 at radix out_r. Worst-case error ≤ 4 LSB at Q16.16. Domain: input > 0. Returns FR_DOMAIN_ERROR for input ≤ 0.
FR_ln Same shape as FR_log2. s32 at radix out_r. Natural log. Same domain. Internally computed as FR_log2(x) × ln(2).
FR_log10 Same shape as FR_log2. s32 at radix out_r. Common log. Same domain. Internally computed as FR_log2(x) / log2(10).

Worked example. You have an audio-meter input at radix 16, and you want the level in “deci-bels-ish” units as a plain integer (no fractional bits):

s32 x      = FR_NUM(2, 0, 0, 16);   /* 2.0 at radix 16 */
s32 log2_x = FR_log2(x, 16, 0);     /* output at radix 0 → plain integer */
/* log2_x == 1 */

If you instead wanted four fractional bits in the answer, ask for out_r = 4 and you’ll get 16 = 1.0 << 4. Because input and output radix are independent, you can freely compress a radix-16 input into a radix-4 output or expand a radix-4 input into a radix-16 output, whichever suits your downstream math.

Exponentials

The exponential functions use a single core routine (FR_pow2) plus base-conversion macros that rescale the input. FR_EXP and FR_POW10 use FR_MULK28 (a radix-28 constant multiply via 64-bit intermediate) for high accuracy. Shift-only variants (FR_EXP_FAST, FR_POW10_FAST) are available for targets where 32×32→64 multiply is expensive.

Symbol Kind Inputs Output Notes
FR_pow2 Function s32 input at radix (exponent)
u16 radix
s32 at the same radix. Domain: input up to 30 << radix; above that, saturates to FR_OVERFLOW_POS. Negative inputs are floored toward −∞ before splitting into integer + fractional parts. Uses a 65-entry lookup table (260 bytes) with linear interpolation.
FR_EXP Macro Same as FR_pow2. s32 at radix. Expands to FR_pow2(FR_MULK28(input, FR_kLOG2E_28), radix). Uses a radix-28 constant for ~9 digits of precision in the base conversion.
FR_POW10 Macro Same as FR_pow2. s32 at radix. Expands to FR_pow2(FR_MULK28(input, FR_kLOG2_10_28), radix). Saturates around input ≥ 9 << radix.
FR_EXP_FAST Macro Same as FR_pow2. s32 at radix. Shift-only variant: FR_pow2(FR_SLOG2E(input), radix). No multiply instruction. Lower accuracy (~5–10 LSB at Q16.16).
FR_POW10_FAST Macro Same as FR_pow2. s32 at radix. Shift-only variant: FR_pow2(FR_SLOG2_10(input), radix). No multiply instruction.
FR_MULK28 Macro s32 x, radix-28 constant k s32 at the same radix as x. Multiplies a fixed-point value by a radix-28 constant using a 64-bit intermediate. Rounds to nearest. Used internally by FR_EXP, FR_POW10, FR_ln, FR_log10.

Worked example. Compute e1.5 at radix 16:

s32 x = FR_NUM(1, 5, 1, 16);   /* 1.5 at radix 16 */
s32 y = FR_EXP(x, 16);          /* ≈ 4.4817 at radix 16 */

FR_EXP vs FR_EXP_FAST. FR_EXP uses FR_MULK28 which requires a single 32×32→64 multiply (available on all Cortex-M, RISC-V RV32IM, x86, etc.). The radix-28 constant has ~9 decimal digits of precision, so the base conversion adds negligible error. FR_EXP_FAST uses the shift-only macro FR_SLOG2E which avoids all multiply instructions but introduces ~5–10 LSB of extra error at Q16.16. Use FR_EXP_FAST on 8-bit targets (AVR, 8051) where 64-bit multiply is very expensive.

Roots

Function Inputs Output Notes
FR_sqrt s32 input at radix
u16 radix
s32 at the same radix. Domain: input ≥ 0. Returns FR_DOMAIN_ERROR for negative input. Digit-by-digit integer isqrt on an int64_t accumulator — deterministic 32-iteration cost, no floating point anywhere. Rounds to nearest (remainder > root → +1). Worst-case error is ±0.5 LSB at the input radix.
FR_hypot s32 x, s32 y both at radix
u16 radix
s32 at the same radix. Overflow-safe magnitude: computes sqrt(x² + y²) without an intermediate 32-bit overflow by promoting the sum of squares to int64_t. Accepts the full s32 input range; output saturates at FR_OVERFLOW_POS only if the true hypot exceeds 231−1 at the given radix.
FR_hypot_fast s32 x, s32 y (any radix) s32 at the same radix. Fast approximate magnitude using 4-segment piecewise-linear shift-only arithmetic. ∼0.4% peak error. No multiply, no 64-bit, no ROM table. Based on the method of US Patent 6,567,777 B1 (public domain). No radix parameter needed — the algorithm is scale-invariant.
FR_hypot_fast8 s32 x, s32 y (any radix) s32 at the same radix. 8-segment variant. ∼0.14% peak error. Same shift-only approach, more branches.

Wave generators

The wave generators are the same family of synth-style shapes you’d find in a hardware LFO: square, PWM, triangle, saw, morphing triangle, and noise. Every one of them is a pure function with no internal state — you drive them from a phase accumulator of your own, which lets you instance as many independent oscillators as you like without worrying about shared globals.

The phase is a u16 BAM value (full circle = 65536), so advancing a phase accumulator modulo 216 is free: phase += inc; wraps automatically because the variable is 16-bit. Combine that with FR_HZ2BAM_INC and you get a classic NCO (numerically controlled oscillator) in two lines of code.

The generators

Function Inputs Output Shape
fr_wave_sqr u16 phase (BAM) s16 in s0.15, bipolar [−32767, +32767] 50% square. Returns +32767 for the first half of the cycle and −32767 for the second.
fr_wave_pwm u16 phase (BAM)
u16 duty — threshold; 32768 = 50%
s16 s0.15, bipolar. Variable-duty pulse. Returns +32767 while phase < duty, else −32767. duty = 0 means always low; duty = 65535 means almost always high.
fr_wave_tri u16 phase (BAM) s16 s0.15, bipolar. Symmetric triangle. Peaks at phase = 16384 (quarter cycle) and phase = 49152.
fr_wave_saw u16 phase (BAM) s16 s0.15, bipolar. Rising sawtooth. Linear ramp from −32767 at phase = 0 to +32767 at phase = 65535, then snaps back.
fr_wave_tri_morph u16 phase (BAM)
u16 break_point — BAM position of the peak
s16 in [0, 32767] — unipolar. Variable-symmetry triangle. With break_point = 32768 you get a symmetric triangle; with break_point near 0 or 65535 you get a ramp-up or ramp-down saw. Output is unipolar — subtract 16384 and double if you need it bipolar.
fr_wave_noise u32 *state — non-zero seed s16 s0.15, bipolar. LFSR / xorshift pseudorandom noise. Caller owns the 32-bit state; each call advances it in place. Seed with any non-zero value. Period is 232 − 1.

Phase increment helper

Macro Inputs Output Notes
FR_HZ2BAM_INC(hz, sample_rate) Integer hz, integer sample_rate (both Hz) u16 phase increment Per-sample BAM delta for a target frequency. Caller must ensure hz < sample_rate/2 (Nyquist); the macro does not check. Both args are evaluated once.

Worked example — 440 Hz triangle at 48 kHz

u16 phase = 0;
u16 inc   = FR_HZ2BAM_INC(440, 48000);    /* ~601 */

for (int i = 0; i < n_samples; ++i) {
    out[i] = fr_wave_tri(phase);           /* s0.15 bipolar */
    phase += inc;                           /* wraps mod 2^16 for free */
}

ADSR envelope

The envelope generator is a five-state machine — Idle, Attack, Decay, Sustain, Release — with linear segments between states. It’s sample-rate agnostic: durations are expressed in samples, not seconds, so the same envelope code runs at any rate from 1 kHz (servo loops) to 192 kHz (audio).

Internal state lives in a caller-allocated fr_adsr_t struct. No malloc, no globals; you can instance as many envelopes as you want. The internal level is held in s1.30 rather than s0.15 so that very long envelopes at high sample rates still get a non-zero per-sample increment — a 2-second attack at 48 kHz would underflow s0.15 arithmetic in the first sample.

The struct

typedef struct fr_adsr_s {
    u8  state;        /* FR_ADSR_IDLE|ATTACK|DECAY|SUSTAIN|RELEASE */
    s32 level;        /* current envelope value, s1.30 */
    s32 sustain;      /* sustain target,         s1.30 */
    s32 attack_inc;   /* per-sample increment during attack */
    s32 decay_dec;    /* per-sample decrement during decay */
    s32 release_dec;  /* per-sample decrement during release */
} fr_adsr_t;

The FR_ADSR_* state constants are exposed so you can check env.state == FR_ADSR_IDLE to know when an envelope has finished releasing and it’s safe to recycle the voice.

The API

Function Inputs Output Effect
fr_adsr_init fr_adsr_t *env
u32 attack_samples
u32 decay_samples
s16 sustain_level_s015 — s0.15, range [0, 32767]
u32 release_samples
void Sets the per-sample increments from the sample counts and stores the sustain target in s1.30. Envelope state becomes FR_ADSR_IDLE. Safe to call on an active envelope to retune it.
fr_adsr_trigger fr_adsr_t *env void Note-on. Resets level to 0 and state to FR_ADSR_ATTACK. Call each time a voice should start.
fr_adsr_release fr_adsr_t *env void Note-off. Jumps directly to FR_ADSR_RELEASE from whatever state the envelope was in — so releasing mid-attack is valid and produces a clean fade from the current level.
fr_adsr_step fr_adsr_t *env s16 in s0.15, range [0, 32767] Advance one sample and return the current envelope value. The returned value is unipolar (never negative) — multiply your bipolar oscillator by this envelope and you get an amplitude-modulated voice.

Worked example — 440 Hz triangle with an envelope

fr_adsr_t env;
fr_adsr_init(&env,
             4800,      /* 100 ms attack  @ 48 kHz */
             9600,      /* 200 ms decay   */
             24576,     /* sustain = 0.75 in s0.15 */
             19200);    /* 400 ms release */

u16 phase = 0, inc = FR_HZ2BAM_INC(440, 48000);

fr_adsr_trigger(&env);
for (int i = 0; i < 48000; ++i) {           /* 1 second of tone */
    if (i == 24000) fr_adsr_release(&env);  /* note-off at 500 ms */
    s32 tone = fr_wave_tri(phase);              /* bipolar s0.15   */
    s32 amp  = fr_adsr_step(&env);          /* unipolar s0.15  */
    out[i]   = (s16)((tone * amp) >> 15);    /* multiplied       */
    phase += inc;
}

2D transforms (FR_math_2D.h)

FR_Matrix2D_CPT (“Coordinate Point Transform”) is a lightweight C++ class wrapping the top two rows of a 3×3 affine matrix at a configurable radix. The bottom row is always [0, 0, 1] — that’s what makes the transforms affine — so it is never stored, which saves both memory and multiplies on the transform path.

The class has no virtual methods, no dynamic allocation, and no hidden state beyond the six stored matrix elements, a radix, and a “fast” flag that remembers when the off-diagonal elements are zero (pure scale / translate). That last flag lets the transform methods take a shorter path when you haven’t rotated or sheared.

The struct

struct FR_Matrix2D_CPT {
    s32 m00, m01, m02;   /* row 0 */
    s32 m10, m11, m12;   /* row 1 */
    u16 radix;           /* radix used by the elements */
    int fast;            /* set by checkfast(): 1 if m01==0 and m10==0 */
    /* ... methods below ... */
};

Construction and identity

MethodEffect
FR_Matrix2D_CPT(u16 radix = FR_MAT_DEFPREC) Constructor. FR_MAT_DEFPREC = 8, so the default is a radix-8 matrix. Calls ID() to load the identity.
void ID() Load the identity matrix.
void set(s32 a00, s32 a01, s32 a02, s32 a10, s32 a11, s32 a12, u16 nRadix = FR_MAT_DEFPREC) Set all six elements and the radix in one call.
bool checkfast() Recompute the fast flag after manual edits to m01 / m10. The transform methods check this flag — if you bypass the setters, call this.

Composition

MethodInputsEffect
void setrotate(s16 deg) Integer degrees. Overwrite the 2×2 upper-left with a rotation matrix. Uses FR_SinI / FR_CosI (BAM core).
void setrotate(s16 deg, u16 deg_radix) Fixed-point degrees at the given radix. Same, but lets you pass fractional degrees (e.g. 42.5° at deg_radix = 1).
void XlateI(s32 x, s32 y) Integer pixel offsets. Set the translation elements to (x, y), scaling by the matrix’s own radix. Overwrites whatever was in m02 / m12.
void XlateI(s32 x, s32 y, u16 nRadix) Integer offsets at an explicit radix. As above but with a caller-supplied radix.
void XlateRelativeI(s32 x, s32 y) Integer deltas. Add to the current translation rather than overwriting it.
void XlateRelativeI(s32 x, s32 y, u16 nRadix) Integer deltas at an explicit radix. As above with caller-supplied radix.
void add(const FR_Matrix2D_CPT *pAdd) Pointer to source matrix (same radix). Element-wise this += *pAdd.
void sub(const FR_Matrix2D_CPT *pSub) Pointer to source matrix (same radix). Element-wise this −= *pSub.
operator=, operator+=, operator−=, operator*= Reference to another matrix (or s32 scalar for *=). Idiomatic C++ convenience wrappers around the above.

Determinant and inverse

MethodInputsOutputNotes
s32 det() None s32 at the matrix radix Computes the determinant of the upper-left 2×2 block (the affine translation does not contribute). Worth checking for zero before calling inv().
bool inv(FR_Matrix2D_CPT *nInv) Pointer to a different destination matrix true on success, false on singular or aliased (nInv == this) Out-of-place inverse; the source is preserved.
bool inv() None true on success, false on singular In-place inverse. Cheaper than the out-of-place form if you don’t need the original.

Point transforms

All point transforms take explicit x, y coordinates and write to caller-supplied output pointers — there is no packaged Point struct. This keeps the ABI simple and lets you feed integer coordinates, s16 coordinates, or anything else without wrapping them.

Method Inputs Output Notes
XFormPtI(s32 x, s32 y, s32 *xp, s32 *yp, u16 r) s32 integer coordinates; explicit shift r. Writes transformed s32 to *xp, *yp. Shifts the 2×2 product down by r at the end — pass matrix.radix to get integer output.
XFormPtI(s32 x, s32 y, s32 *xp, s32 *yp) Integer x, y. Transformed coordinates, shifted by this->radix. Convenience overload that uses the matrix’s stored radix.
XFormPtINoTranslate(s32 x, s32 y, s32 *xp, s32 *yp, u16 r) Integer x, y; explicit shift. Transformed coordinates. Applies the 2×2 linear part only; ignores m02 / m12. Useful for rotating vectors (directions) rather than points.
XFormPtI16(s16 x, s16 y, s16 *xp, s16 *yp) s16 integer coordinates. s16 output. Fast path for sub-32-bit coordinate work. Internally promotes to s32 during the multiply-add, then narrows back. Watch for overflow on extreme inputs.
XFormPtI16NoTranslate(s16 x, s16 y, s16 *xp, s16 *yp) s16 integer coordinates. s16 output. Linear-only variant of the s16 path.

The fast flag on the matrix causes all of these methods to skip the cross-term multiply-adds when m01 and m10 are both zero (pure scale / translate), saving two multiplies per point.

Formatted output

FunctionSignature
FR_printNumDPretty-print an s32 as signed decimal.
FR_printNumFPretty-print an s32 as fixed-point at a given radix (e.g. “3.14159”).
FR_printNumHPretty-print an s32 as hex.

All three take a user-supplied putc callback so you can direct output at a UART, an in-memory buffer, or stdout without pulling in printf.

See also