Examples

Short, runnable snippets for the most common FR_Math tasks. Each example compiles cleanly against the v2.0.0 library with:

cc -Isrc example.c src/FR_math.c -o example
./example

No -lm needed — nothing in FR_Math links the C math library. The 2D example additionally needs src/FR_math_2D.cpp and a C++ compiler (c++ or g++).

1. Basic radix conversion

This first example walks through the three foundational moves: construct a fixed-point number from an integer literal, change its radix without losing data, and round-trip back. It’s the simplest thing you can do with the library and it exercises the I2FR, FR2I, and FR_CHRDX macros.

Caveats: I2FR and FR2I are pure shift macros — they do no range-checking. Passing a value that overflows the integer-part bits will silently wrap. FR_CHRDX going from a higher radix to a lower one throws away the lowest bits, so the round-trip here is only lossless because we go up first.

#include <stdio.h>
#include "FR_defs.h"
#include "FR_math.h"

int main(void)
{
    /* 3.5 at radix 4 = (3 << 4) + (0.5 * 16) = 48 + 8 = 56 = 0x38 */
    s32 a = I2FR(3, 4) | (1 << 3);       /* 3.5  at radix 4 */

    printf("a = %d (raw) = %d + %d/16\n",
           (int)a, (int)FR2I(a, 4), (int)(a & 0xF));

    /* Same 3.5 at radix 8: shift up by 4. */
    s32 a_r8 = FR_CHRDX(a, 4, 8);
    printf("a at radix 8 = %d\n", (int)a_r8);    /* 56 << 4 = 896 */

    /* Change back to radix 4 (no precision lost because 4 < 8). */
    s32 a_r4 = FR_CHRDX(a_r8, 8, 4);
    printf("round-trip = %d (equals a: %s)\n",
           (int)a_r4, a_r4 == a ? "yes" : "no");
    return 0;
}

2. Trig — integer degrees vs radian vs BAM

FR_Math supports three angle conventions and this example hits all three: integer degrees through the legacy FR_Sin / FR_Cos API, the radian-native fr_sin / fr_cos (radian at a chosen input radix), and BAM-native fr_sin_bam / fr_cos_bam. All three paths feed the same 129-entry quadrant cosine table under the hood and should produce nearly identical results.

Caveats: the radix parameter on FR_Sin(deg, radix) is the radix of the degree input, not the output. All sin/cos functions return s15.16 — that is, s32 at radix 16, where 1.0 = 65536 (FR_TRIG_ONE). The values compared below are all in the same s15.16 format.

#include <stdio.h>
#include "FR_math.h"

int main(void)
{
    /* Integer-degree legacy API — returns s15.16 */
    s32 s30_deg = FR_SinI(30);
    s32 c60_deg = FR_CosI(60);

    /* BAM-native core — same s15.16 output */
    s32 s30_bam = fr_sin_bam(FR_DEG2BAM(30));
    s32 c60_bam = fr_cos_bam(FR_DEG2BAM(60));

    /* Radian-native: input is radians at whatever radix you pass.
       Here we use FR_kPI at radix 16 to build pi/6 = 30 degrees. */
    s32 pi_over_6 = FR_kPI / 6;             /* radix 16 */
    s32 s30_rad   = fr_sin(pi_over_6, 16);  /* s15.16 out */

    printf("FR_SinI(30)     = %d   (s15.16)\n", s30_deg);
    printf("fr_sin_bam(30deg)= %d   (s15.16)\n", s30_bam);
    printf("fr_sin(pi/6)    = %d   (s15.16)\n", s30_rad);
    printf("FR_CosI(60)     = %d   (s15.16)\n", c60_deg);
    printf("expected 0.5    = %d   (1 << 15)\n", 1 << 15);
    return 0;
}

All four answers should land within a couple of LSBs of 1 << 15 = 32768, which is 0.5 in s15.16.

3. Square root and hypotenuse

This example shows the integer-only root family. Both FR_sqrt and FR_hypot take a radix and return at the same radix — no mixed-radix output to worry about. FR_hypot is overflow-safe even when x² + y² would overflow a 32-bit intermediate: it promotes internally to int64_t.

Caveats: both functions return FR_DOMAIN_ERROR (which is 0x80000000, the same bit pattern as FR_OVERFLOW_NEG) on a negative input. Test against the named sentinel explicitly, not with a < 0 test.

#include <stdio.h>
#include "FR_math.h"

int main(void)
{
    const u16 r = 16;

    /* sqrt(2) at radix 16 */
    s32 two  = I2FR(2, r);
    s32 root = FR_sqrt(two, r);
    printf("sqrt(2) raw = 0x%x  (expect ~0x16a09 = 1.41421)\n",
           (unsigned)root);

    /* hypot(3, 4) = 5 exactly */
    s32 three = I2FR(3, r);
    s32 four  = I2FR(4, r);
    s32 h     = FR_hypot(three, four, r);
    printf("hypot(3,4)  = %d (integer part)\n", (int)FR2I(h, r));

    /* Domain-error handling */
    s32 bad = FR_sqrt(-1, r);
    if (bad == FR_DOMAIN_ERROR)
        printf("sqrt(-1) rejected, good.\n");

    /* Fast approximate — no multiply, no 64-bit math */
    s32 h_fast  = FR_hypot_fast(three, four);     /* 4-seg, ~0.4% err */
    s32 h_fast8 = FR_hypot_fast8(three, four);    /* 8-seg, ~0.14% err */
    printf("hypot_fast(3,4)  = %d (integer part)\n", (int)FR2I(h_fast, r));
    printf("hypot_fast8(3,4) = %d (integer part)\n", (int)FR2I(h_fast8, r));
    return 0;
}

4. Logarithm, exponential, decibels

A realistic embedded-audio use case: turn a linear amplitude into dB so you can drive a VU meter with a log scale. This exercises FR_log10 (the genuine mixed-radix logarithm — input radix and output radix are independent), and the macro FR_POW10 (note: it’s a macro, not a function, because it reduces to FR_pow2 after a shift-only rescale).

Caveats: FR_log10 takes three arguments: value, input radix, output radix. Passing two will not compile. The conversion constant 20 for amplitude-to-dB is multiplied by shifting because 20 = 16 + 4, which is cheaper than calling FR_FixMuls on a tiny constant.

#include <stdio.h>
#include "FR_math.h"

/* Convert a linear amplitude (radix 16) into dB at radix 8. */
static s32 amplitude_to_db(s32 amp_r16)
{
    /* log10 output at radix 8 gives us ~0.004 dB resolution */
    s32 l10 = FR_log10(amp_r16, 16, 8);

    /* Multiply by 20: since 20 = 16 + 4, we can do two shifts + add. */
    return (l10 << 4) + (l10 << 2);
}

int main(void)
{
    s32 half = I2FR(1, 16) >> 1;   /* 0.5 at radix 16 */
    s32 db   = amplitude_to_db(half);
    printf("0.5 -> %d.%d dB  (expect ~ -6.02)\n",
           (int)FR2I(db, 8),
           (int)(FR_FRAC(db, 8) * 100 >> 8));

    /* 10^(0.3) is approximately 2.0 */
    s32 x = FR_POW10(FR_NUM(0, 3, 1, 16), 16);
    printf("10^0.3 = 0x%x (expect ~0x20000 = 2.0 at radix 16)\n",
           (unsigned)x);
    return 0;
}

5. Arctangent and atan2

The inverse-trig functions in FR_Math return angles in degrees, not radians — the output fits in an s16 and you can feed it straight back into FR_SinI / FR_CosI without any conversion. This example exercises both FR_atan (single-argument ratio) and FR_atan2 (full-circle, two-argument).

Caveats: FR_atan2 takes only two arguments (y, x) and has no radix parameter — it returns degrees in [−180, 180] as s16. The radix argument on FR_atan is the radix of the input ratio, not of the output.

#include <stdio.h>
#include "FR_math.h"

int main(void)
{
    const u16 r = 14;

    /* atan(1) = 45 degrees */
    s16 a = FR_atan(I2FR(1, r), r);
    printf("atan(1) = %d degrees (expect 45)\n", a);

    /* Full-circle atan2 */
    s16 q2 = FR_atan2(I2FR( 1, r), I2FR(-1, r));  /*  135 deg */
    s16 q3 = FR_atan2(I2FR(-1, r), I2FR(-1, r));  /* -135 deg */
    printf("atan2( 1,-1) = %d\n", q2);
    printf("atan2(-1,-1) = %d\n", q3);

    /* asin with out-of-domain input */
    s16 bad = FR_asin(I2FR(2, r), r);
    if (bad == FR_DOMAIN_ERROR)
        printf("asin(2) rejected, good.\n");
    return 0;
}

6. Wave generators + phase accumulator

A 16-sample snapshot of a 1 kHz triangle wave at a 48 kHz sample rate, produced by the canonical NCO (numerically controlled oscillator) pattern: a u16 phase that wraps naturally at 216, driven by a per-sample increment from FR_HZ2BAM_INC.

Caveats: the wave functions are stateless, so you can drive as many voices as you like from independent phase accumulators. fr_wave_tri is bipolar in s0.15 but fr_wave_tri_morph is unipolar — watch the sign convention if you mix them. The phase accumulator must be u16 for the free modular wrap to work.

#include <stdio.h>
#include "FR_math.h"

int main(void)
{
    const u32 SR   = 48000;
    const u32 FREQ = 1000;

    u16 step = FR_HZ2BAM_INC(FREQ, SR);   /* ~1365 */
    u16 phase = 0;

    for (int i = 0; i < 16; i++) {
        s16 tri = fr_wave_tri(phase);
        s16 sqr = fr_wave_sqr(phase);
        printf("%2d: phase=%5u  tri=%6d  sqr=%6d\n",
               i, phase, tri, sqr);
        phase += step;            /* wraps at 2^16 for free */
    }
    return 0;
}

7. ADSR envelope

A linear-segment Attack-Decay-Sustain-Release envelope, the bread-and-butter of synth voices. The envelope is sample-rate agnostic — durations are in samples, not seconds — so the same code works at 1 kHz or 192 kHz. The internal level is held in s1.30, so even a 2-second attack at 48 kHz gets a non-zero per-sample increment.

Caveats: the sustain level is an s16 in s0.15 format — pass a value in [0, 32767], not a raw register count. You must call fr_adsr_init once to set the times, fr_adsr_trigger for note-on, and fr_adsr_release for note-off. The envelope value returned by fr_adsr_step is unipolar — multiply it by your bipolar oscillator to get the amplitude- modulated voice.

#include <stdio.h>
#include "FR_math.h"

int main(void)
{
    fr_adsr_t env;
    fr_adsr_init(&env,
                 /* attack  */ 100,
                 /* decay   */ 200,
                 /* sustain */ 16384,          /* 0.5 in s0.15 */
                 /* release */ 400);

    fr_adsr_trigger(&env);

    for (int i = 0; i < 500; i++) {
        s16 y = fr_adsr_step(&env);
        if (i % 50 == 0) printf("t=%3d  amp=%d\n", i, y);
    }

    fr_adsr_release(&env);

    for (int i = 0; i < 500; i++) {
        s16 y = fr_adsr_step(&env);
        if (i % 50 == 0) printf("rel t=%3d  amp=%d\n", i, y);
        if (env.state == FR_ADSR_IDLE) {
            printf("envelope finished at rel t=%d\n", i);
            break;
        }
    }
    return 0;
}

8. 2D transforms

This example builds a 2D affine transform from rotation and translation and uses it to map a point. FR_Matrix2D_CPT is the “coordinate point transform” class from FR_math_2D.h. It stores only the top two rows of the affine matrix — the bottom row is always [0, 0, 1], so the class saves both memory and multiplies.

Caveats: the class name is FR_Matrix2D_CPT (the trailing T is part of the name). The point transforms use explicit x, y arguments and output pointers — there is no packaged Point struct. Set the radix at construction; the default is FR_MAT_DEFPREC = 8.

#include <stdio.h>
#include "FR_math.h"
#include "FR_math_2D.h"

int main()
{
    FR_Matrix2D_CPT M(16);      /* work at radix 16 */

    /* Rotate 30 degrees around the origin, then translate by (100, 50). */
    M.setrotate(30);
    M.XlateRelativeI(100, 50);  /* uses the matrix's own radix */

    /* Transform the point (10, 0) integer-in / fixed-out at the matrix radix. */
    s32 xp, yp;
    M.XFormPtI((s32)10, (s32)0, &xp, &yp);
    printf("transformed = (%d, %d)  (integer at radix 16)\n",
           (int)FR2I(xp, 16), (int)FR2I(yp, 16));

    /* Invert and round-trip the transformed point back */
    if (M.inv()) {
        s32 bx, by;
        M.XFormPtI(xp, yp, &bx, &by);
        printf("round trip = (%d, %d)\n",
               (int)FR2I(bx, 16), (int)FR2I(by, 16));
    }
    return 0;
}

9. Formatted output without printf

On a deep-embedded target you often don’t have printf — pulling it in can cost 10 KB of flash. FR_Math’s FR_printNumF takes a caller-supplied putc callback, so you can render a fixed-point number straight to a UART or into a local buffer without linking a single stdio symbol.

Caveats: the signature is FR_printNumF(putc, value, radix, pad, prec) — the sink comes first, not last. The sink is int (*)(char); return value is ignored. The sink here writes into a static buffer; make sure the buffer is large enough for the worst-case output.

#include "FR_math.h"

/* Collect output into a local buffer so we don't need stdio. */
static char buf[64];
static int  cursor = 0;

static int sink(char c)
{
    if (cursor < (int)sizeof(buf) - 1) buf[cursor++] = c;
    return (int)(unsigned char)c;
}

int main(void)
{
    /* pi at radix 16 using the built-in constant */
    s32 x = FR_kPI;
    FR_printNumF(sink, x, 16, /* pad */ 0, /* prec */ 6);
    sink('\n');
    sink('\0');

    /* buf now contains the rendered number, no printf used. */
    return 0;
}

10. Integer-only 2D transform for scanline renderers

The XFormPtI16 fast path takes s16 coordinates in and writes s16 out. It’s a tiny bit lossier than the s32 form, but it sidesteps all the fixed-point conversion on the hot path — useful inside the inner loop of a scanline rasteriser where you already know your coordinates fit in 16 bits.

Caveats: the output is narrowed to s16, so extreme inputs or large matrix entries can overflow silently. The no-translate variant (XFormPtI16NoTranslate) applies only the linear part and is useful for transforming direction vectors rather than positions.

#include <stdio.h>
#include "FR_math.h"
#include "FR_math_2D.h"

int main()
{
    FR_Matrix2D_CPT M;           /* default radix 8 */
    M.setrotate(45);

    s16 xp, yp;
    M.XFormPtI16(100, 0, &xp, &yp);  /* expect ~(71, 71) */
    printf("rotated (100, 0) by 45 deg = (%d, %d)\n", xp, yp);
    return 0;
}

11. String round-trip and radix precision

FR_numstr parses a decimal string into a fixed-point value; FR_printNumF renders a fixed-point value back to a decimal string. Together they form a symmetric pair — the natural I/O layer for config files, serial protocols, or any situation where a human-readable number must survive a round trip through fixed-point representation.

The key insight is that the radix controls how many fractional bits you have, and that directly determines how many decimal digits survive the round trip faithfully. The rule of thumb: every ~3.32 bits of radix give you one reliable decimal digit (log10(2) ≈ 0.301, so 1 / 0.301 ≈ 3.32).

RadixFractional bitsReliable decimal digitsSmallest step
88~21/256 ≈ 0.0039
1616~4–51/65536 ≈ 0.0000153
2424~71/16777216 ≈ 0.0000001

This example parses the same string "3.14159265" at three different radixes and prints it back, so you can see exactly where each radix runs out of precision.

#include <stdio.h>
#include "FR_math.h"

/* Buffer-based putc for FR_printNumF — collects output in buf[]. */
static char buf[64];
static int  pos;
static int buf_putc(char c) { buf[pos++] = c; buf[pos] = '\0'; return 0; }

int main(void)
{
    const char *input = "3.14159265";

    printf("Round-trip: \"%s\" through three radixes\n\n", input);
    printf("  radix  bits  parse-hex   printed-back\n");
    printf("  -----  ----  ---------   ---------------\n");

    /* ---- Radix 8: only ~2 reliable decimal digits ---- */
    {
        s32 val = FR_numstr(input, 8);
        pos = 0;
        FR_printNumF(buf_putc, val, 8, 0, 8);
        printf("     8      8  0x%08x  %s\n", (unsigned)val, buf);
        /* Expected: "3.14062500" — digits after the 2nd are
         * unreliable because 8 bits can only distinguish 256
         * fractional levels. 0.14159 rounds to 36/256 ≈ 0.140625. */
    }

    /* ---- Radix 16: ~4–5 reliable decimal digits ---- */
    {
        s32 val = FR_numstr(input, 16);
        pos = 0;
        FR_printNumF(buf_putc, val, 16, 0, 8);
        printf("    16     16  0x%08x  %s\n", (unsigned)val, buf);
        /* Expected: "3.14158630" — good through 5 digits, then
         * quantisation noise appears.  This is the sweet spot for
         * most embedded work: 16 bits of fraction fits in an s32
         * with 15 bits of integer range (±32767). */
    }

    /* ---- Radix 24: ~7 reliable decimal digits ---- */
    {
        s32 val = FR_numstr(input, 24);
        pos = 0;
        FR_printNumF(buf_putc, val, 24, 0, 8);
        printf("    24     24  0x%08x  %s\n", (unsigned)val, buf);
        /* Expected: "3.14159262" — good through 7+ digits.
         * The trade-off: with 24 fractional bits you only have
         * 7 integer bits (±127), so the range is much smaller. */
    }

    /* ---- Practical guidance ----
     *
     * Choose your radix based on the precision you need to
     * preserve through a round trip:
     *
     *   - Config file says "gain = 0.75"?  Radix 8 is plenty.
     *   - Sensor calibration "offset = 1.2345"?  Radix 16.
     *   - Scientific constant "k = 0.0000056"?  Radix 24,
     *     or even radix 28 if you only need a small integer range.
     *
     * FR_numstr + FR_printNumF handle the string side;
     * FR_CHRDX handles radix changes in between.
     */

    return 0;
}

Expected output:

Round-trip: "3.14159265" through three radixes

  radix  bits  parse-hex   printed-back
  -----  ----  ---------   ---------------
     8      8  0x00000324  3.14062500
    16     16  0x0003243f  3.14158630
    24     24  0x03243f6a  3.14159262

The hex column makes the bit-level reality visible: at radix 8 the value is 0x324 — only 10 significant bits — so the decimal rendering can only faithfully reproduce about two fractional digits. At radix 24 the value is 0x03243F6A — 26 significant bits — and seven decimal digits survive. The eighth digit (5 vs 4) shows the quantisation floor: 2^−24 ≈ 6 × 10^−8, so the last digit is always uncertain.

See also