diff --git a/README.md b/README.md index 952eaac..f82861b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![License](https://img.shields.io/badge/License-BSD%202--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) [![CI](https://github.com/deftio/fr_math/actions/workflows/ci.yml/badge.svg)](https://github.com/deftio/fr_math/actions/workflows/ci.yml) -[![Coverage](https://img.shields.io/badge/coverage-96%25-brightgreen.svg)](#building-and-testing) +[![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen.svg)](#building-and-testing) [![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://deftio.github.io/fr_math/) -[![Version](https://img.shields.io/badge/version-2.0.8-blue.svg)](release_notes.md) +[![Version](https://img.shields.io/badge/version-2.0.9-blue.svg)](release_notes.md) [![GitHub](https://img.shields.io/badge/GitHub-repo-181717.svg?logo=github)](https://github.com/deftio/fr_math) [![PlatformIO](https://img.shields.io/badge/PlatformIO-library-teal.svg)](https://registry.platformio.org/libraries/deftio/fr_math) @@ -30,30 +30,30 @@ radix — Q16.16 is just the reference point for the table. At other radixes (3-bit, 24-bit, etc.) accuracy will differ due to the number of fractional bits available. - -| Function | Max err (%)*| Avg err (%) | Note | -|---|---:|---:|---| -| sin/cos (BAM) | 0.1526 | 0.0030 | very fast binary angle trig | -| sin/cos (deg) | 0.1526 | 0.0029 | degree input trig fns | -| sin/cos (rad) | 0.1828 | 0.0033 | radian (traditional) trig | -| tan (BAM) | 0.5823 | 0.0008 | binary angle tangent; ±maxint at poles | -| tan (deg) | 0.5311 | 0.0008 | degree input tangent; saturated at poles | -| tan (rad) | 0.0386 | 0.0001 | radian (traditional) tangent | -| asin / acos | 0.7771 | 0.0280 | reverse trig, radian output | -| atan2 | 0.2564 | 0.0237 | reverse tangent, always safe | -| atan | 0.2425 | 0.0155 | reverse tangent, accepts up to maxint | -| sqrt | 0.0000 | 0.0000 | Round-to-nearest | -| log2 | 0.0116 | 0.0016 | shift/add only for speed | -| pow2 | 0.0018 | 0.0004 | shift/add only for speed | -| ln, log10 | 0.0004 | 0.0000 | shift/add only for speed | -| exp | 0.0003 | 0.0000 | shift/add only for speed | -| exp_fast | 0.0009 | 0.0001 | Shift-only scaling | -| pow10 | 0.0005 | 0.0000 | shift/add only for speed | -| pow10_fast | 0.0022 | 0.0002 | Shift-only scaling | -| hypot (exact) | 0.0000 | 0.0000 | Uses 64-bit intermediate | -| hypot_fast8 (8-seg) | 0.0915 | 0.0320 | Shift-only, no multiply | - -*Relative error; reference clamped to 1% of full-scale output. + +| Function | Max err (%)*| Avg err (%) | Note | +|---|---:|---:|---| +| sin/cos (BAM) | 0.1526 | 0.0030 | very fast binary angle trig | +| sin/cos (deg) | 0.1526 | 0.0029 | degree input trig fns | +| sin/cos (rad) | 0.1828 | 0.0033 | radian (traditional) trig | +| tan (BAM) | 0.5823 | 0.0008 | binary angle tangent; ±maxint at poles | +| tan (deg) | 0.5311 | 0.0008 | degree input tangent; saturated at poles | +| tan (rad) | 0.0386 | 0.0001 | radian (traditional) tangent | +| asin / acos | 0.7771 | 0.0280 | reverse trig, radian output | +| atan2 | 0.2564 | 0.0237 | reverse tangent, always safe | +| atan | 0.2425 | 0.0155 | reverse tangent, accepts up to maxint | +| sqrt | 0.0000 | 0.0000 | Round-to-nearest | +| log2 | 0.0116 | 0.0016 | shift/add only for speed | +| pow2 | 0.0018 | 0.0004 | shift/add only for speed | +| ln, log10 | 0.0004 | 0.0000 | shift/add only for speed | +| exp | 0.0003 | 0.0000 | shift/add only for speed | +| exp_fast | 0.0009 | 0.0001 | Shift-only scaling | +| pow10 | 0.0005 | 0.0000 | shift/add only for speed | +| pow10_fast | 0.0022 | 0.0002 | Shift-only scaling | +| hypot (exact) | 0.0000 | 0.0000 | Uses 64-bit intermediate | +| hypot_fast8 (8-seg) | 0.0915 | 0.0320 | Shift-only, no multiply | + +*Relative error; reference clamped to 1% of full-scale output. ### What's in the box diff --git a/VERSION b/VERSION index 815e68d..09843e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.8 +2.0.9 diff --git a/agents.md b/agents.md index 30128db..32339ba 100644 --- a/agents.md +++ b/agents.md @@ -18,7 +18,7 @@ src/ Core library (this is what ships) FR_defs.h Type aliases (s8, s16, s32, u8, u16, u32) FR_math_2D.h/.cpp Optional C++ 2D transform class -tests/ Test suite (7 programs, run via `make test`) +tests/ Test suite (8 programs, run via `make test`) examples/ Arduino .ino sketches + POSIX example docs/ Markdown documentation pages/ HTML documentation (mirrors docs/) @@ -31,7 +31,7 @@ dev/ Development notes and planning (not shipped) ```bash make lib # compile library objects -make test # run full test suite (99% line coverage) +make test # run full test suite (98% line coverage, 100% of reachable branches) make examples # build example programs make size-report # cross-compile size report (Docker) make size-update # size report + patch doc files @@ -126,3 +126,9 @@ FR_Math is published to multiple package registries: - Don't change the `extern "C"` wrapping in `FR_math.h` - Don't hardcode a radix inside library functions — always parameterize - Don't add `#include` dependencies beyond `` +- Don't change the Arduino `u8`/`u16` typedef block in `FR_defs.h` to use + `uint8_t`/`uint16_t` — in Arduino C++ builds it must use + `unsigned char`/`unsigned short` verbatim so it stays typedef-identical + to USBAPI.h on AVR/SAM/SAMD cores while still defining the types on + cores without USBAPI.h (ESP32, RP2040, ...). See GitHub issue #11 and + `make test-arduino-compat`. diff --git a/docs/building.md b/docs/building.md index db8d192..20c435f 100644 --- a/docs/building.md +++ b/docs/building.md @@ -91,7 +91,7 @@ See `release_management.md` for the full step-by-step reference. ## The test suite -Tests live under `tests/` and are split into seven +Tests live under `tests/` and are split into eight binaries to keep compile times low: | Binary | What it checks | @@ -103,9 +103,13 @@ binaries to keep compile times low: | `test_full` | Full-coverage dark-corner cases and round-trips. | | `test_2d_complete` | Extended 2D: matrix composition, inverse, point transforms. | | `test_tdd` | Characterization tests pinned to bit-exact reference values. | +| `test_arduino_compat` | Arduino build compatibility: compiles the headers as ESP32-style and AVR/USBAPI-style cores would (both include orders), plus type-size checks (issue #11). | -The suite covers **99%** of the library source. -Every public symbol is exercised at least once. +The suite covers **98%** of the library source lines and every +mathematically reachable branch. The remainder is defensive code +(clamps and guards) that no input can trigger, kept in place as +insurance against future edits. Every public symbol is exercised +at least once. ### Running a single binary @@ -258,8 +262,17 @@ arduino-cli compile --fqbn arduino:avr:uno examples/arduino_smoke arduino-cli compile --fqbn arduino:avr:uno examples/basic-math arduino-cli compile --fqbn arduino:avr:uno examples/trig-functions arduino-cli compile --fqbn arduino:avr:uno examples/wave-generators + +# Non-AVR cores work the same way, e.g. ESP32: +arduino-cli compile --fqbn esp32:esp32:esp32 examples/arduino_smoke ``` +`make test-arduino-compat` (part of `make test`) simulates the +Arduino build environments on the host — ESP32-style cores with no +USBAPI.h typedefs and AVR-style cores that predefine `u8`/`u16`, +in both include orders — so core-specific type clashes like +issue #11 are caught without any board toolchain installed. + See the [code size table](#code-size-text-section-compiled-with--os) above for exact numbers. With linker dead-code elimination, only the functions you call are linked. diff --git a/docs/getting-started.md b/docs/getting-started.md index 2fefb6d..f2c1b99 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -8,8 +8,26 @@ back. ## Install -FR_Math is a handful of C and C++ files. There is no package -manager integration and no install step. Either: +FR_Math has no dependencies to install — it needs nothing beyond +``, which every C99-or-newer toolchain ships. "Installing" +here only means getting the sources into the project, either from a +package registry or by copying them in directly. + +### From a package manager + +- **Arduino (IDE or CLI)** — open the Library Manager and search for + **FR_Math**, or run `arduino-cli lib install FR_Math`. Then + `#include ` in the sketch. Works on all cores (AVR, + ESP32, RP2040, SAMD, STM32, ...). +- **PlatformIO** — add `lib_deps = deftio/fr_math` to + `platformio.ini`, or run `pio pkg install --library "deftio/fr_math"`. +- **ESP-IDF** — run `idf.py add-dependency "deftio/fr_math"` to pull + it from the ESP Component Registry. + +### Vendoring the sources + +FR_Math is a handful of C and C++ files, so dropping it directly into +a project works just as well. Either: - Copy `src/FR_math.c`, `src/FR_math.h`, `src/FR_defs.h` (and optionally @@ -18,8 +36,8 @@ manager integration and no install step. Either: - Add FR_Math as a git submodule and point the build system at `src/`. -There are no external dependencies beyond ``, -which every C99-or-newer toolchain ships. +Nothing else needs to be added to the build — no libraries to link +(not even `libm`), no configuration step. ```bash git clone https://github.com/deftio/fr_math.git @@ -29,7 +47,7 @@ cd fr_math `build.sh` wipes `build/`, rebuilds the library, examples, and tests, and runs the full test suite. On success -the output shows all tests passing (99% line coverage). +the output shows all tests passing (98% line coverage, 100% of reachable branches). ## A first program @@ -275,7 +293,7 @@ make coverage # coverage report (requires gcov) ``` Run `make test` for a full pass. With `make coverage`, line coverage of -the library sources is about **99%**. See [Building & Testing](building.md) +the library sources is about **98%**, with every mathematically reachable branch exercised. See [Building & Testing](building.md) for targets, cross-compilation, and CI. ## Next steps diff --git a/docs/releases.md b/docs/releases.md index 734eb1b..92688ad 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -4,6 +4,17 @@ Release highlights. For the full per-symbol change log, see [release_notes.md](https://github.com/deftio/fr_math/blob/master/release_notes.md) in the repo. +## v2.0.9 — 2026 + +Arduino/ESP32 build fix and a saturating-add correctness fix. No API changes. + +- **Arduino `u8`/`u16` type error fixed** ([issue #11](https://github.com/deftio/fr_math/issues/11)): sketches failed to compile on ESP32 — and on any core other than AVR/SAM/SAMD — with `'u16' does not name a type`. `FR_defs.h` had skipped its typedefs in Arduino C++ builds, assuming `USBAPI.h` always provides them; it now emits them using `USBAPI.h`'s exact underlying types, which works whether or not that header is present. Thanks to [@beaka](https://github.com/beaka) for the report. +- **`FR_FixAddSat` correctness**: the overflow check relied on signed-overflow UB and was optimized away at `-Os`, so negative overflow returned a wrapped positive value instead of `FR_OVERFLOW_NEG`. Also, `FR_FixAddSat(0, 0)` wrongly returned `FR_OVERFLOW_POS`. Both fixed. +- **New `test_arduino_compat` suite**: compiles the headers as each Arduino core family does (with and without `USBAPI.h` typedefs, both include orders, C and C++), catching core-specific type clashes without a board toolchain. +- **Coverage**: 98% of library source lines, every mathematically reachable branch exercised. + +--- + ## v2.0.8 — 2026 Tangent accuracy rewrite and trig rounding fix. diff --git a/idf_component.yml b/idf_component.yml index 8097972..09fdefd 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,4 +1,4 @@ -version: "2.0.8" +version: "2.0.9" description: "Compact fixed-point math library for embedded systems. Integer-only with caller-selectable radix. Trig, log/exp, sqrt, hypot, wave generators, ADSR, and 2D transforms. Zero dependencies." url: "https://github.com/deftio/fr_math" repository: "https://github.com/deftio/fr_math.git" diff --git a/library.json b/library.json index 17f9649..ab607a9 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "FR_Math", - "version": "2.0.8", + "version": "2.0.9", "description": "Compact fixed-point math library for embedded systems. Integer-only with caller-selectable radix. Trig, log/exp, sqrt, hypot, wave generators, ADSR, and 2D transforms in 4KB of flash. Zero dependencies.", "keywords": [ "fixed-point", diff --git a/library.properties b/library.properties index 47dde32..601fc4a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=FR_Math -version=2.0.8 +version=2.0.9 author=M. A. Chatterjee maintainer=M. A. Chatterjee sentence=Compact fixed-point math library for embedded systems. 4KB flash, zero dependencies, any radix. diff --git a/llms.txt b/llms.txt index 5930e4d..0723213 100644 --- a/llms.txt +++ b/llms.txt @@ -9,7 +9,16 @@ or libraries. Pure C99, zero dependencies beyond ``. - Repository: https://github.com/deftio/fr_math - Documentation: https://deftio.github.io/fr_math/ - License: BSD-2-Clause -- Version: 2.0.8 +- Version: 2.0.9 + +## Installation + +- Arduino: Library Manager, search "FR_Math" (or `arduino-cli lib install FR_Math`), + then `#include `. All cores supported (AVR, ESP32, RP2040, SAMD, ...). +- PlatformIO: `lib_deps = deftio/fr_math` in platformio.ini +- ESP-IDF: `idf.py add-dependency "deftio/fr_math"` +- Vendoring: copy `src/FR_math.c`, `src/FR_math.h`, `src/FR_defs.h` + (plus `src/FR_math_2D.h/.cpp` if 2D transforms are needed) into the project. ## Key concept: radix parameter diff --git a/makefile b/makefile index 196e127..995d380 100644 --- a/makefile +++ b/makefile @@ -48,6 +48,7 @@ help: @echo " test-full Run full coverage tests" @echo " test-2d-complete Run 2D complete coverage tests" @echo " test-tdd Run TDD characterization tests" + @echo " test-arduino-compat Run Arduino environment compatibility tests" @echo "" @echo "Analysis targets:" @echo " accuracy Show accuracy summary table" @@ -127,7 +128,7 @@ run-examples: examples # Build and run tests .PHONY: test -test: dirs examples test-basic test-comprehensive test-2d test-overflow test-full test-2d-complete test-tdd +test: dirs examples test-basic test-comprehensive test-2d test-overflow test-full test-2d-complete test-tdd test-arduino-compat .PHONY: test-tdd test-tdd: $(BUILD_DIR)/test_tdd @@ -140,6 +141,28 @@ $(BUILD_DIR)/test_tdd: $(TEST_DIR)/test_tdd.cpp $(SRC_DIR)/FR_math.c $(SRC_DIR)/ $(CXX) -I$(SRC_DIR) $(LIB_WARN) -Os $(TEST_FLAGS) -c $(SRC_DIR)/FR_math_2D.cpp -o $(BUILD_DIR)/test_tdd_FR_math_2D.o $(CXX) $(CXXFLAGS) $(TEST_FLAGS) $(TEST_DIR)/test_tdd.cpp $(BUILD_DIR)/test_tdd_FR_math.o $(BUILD_DIR)/test_tdd_FR_math_2D.o $(LDFLAGS) -o $@ +# Arduino environment compatibility (issue #11): compile-matrix simulating +# Arduino cores with and without USBAPI.h's u8/u16 typedefs, plus a runtime +# type-size check. ARDUINO=10813 mimics the IDE-defined version macro. +ARDUINO_SIM = -DARDUINO=10813 +.PHONY: test-arduino-compat +test-arduino-compat: dirs + @echo "Running Arduino compatibility tests (issue #11)..." + @$(CXX) -x c++ $(CFLAGS) $(ARDUINO_SIM) -DARDUINO_ARCH_ESP32 -fsyntax-only $(TEST_DIR)/test_arduino_compat.c + @echo " compile C++ ESP32-style core (no USBAPI typedefs): PASS" + @$(CXX) -x c++ $(CFLAGS) $(ARDUINO_SIM) -DFR_TEST_USBAPI_FIRST -fsyntax-only $(TEST_DIR)/test_arduino_compat.c + @echo " compile C++ AVR-style core (USBAPI typedefs before): PASS" + @$(CXX) -x c++ $(CFLAGS) $(ARDUINO_SIM) -DFR_TEST_USBAPI_AFTER -fsyntax-only $(TEST_DIR)/test_arduino_compat.c + @echo " compile C++ (USBAPI typedefs after FR_math.h): PASS" + @$(CC) $(CFLAGS) $(ARDUINO_SIM) -fsyntax-only $(TEST_DIR)/test_arduino_compat.c + @echo " compile C translation unit under Arduino: PASS" + @$(CC) -I$(SRC_DIR) $(LIB_WARN) -Os $(ARDUINO_SIM) -fsyntax-only $(SRC_DIR)/FR_math.c + @$(CXX) -I$(SRC_DIR) $(LIB_WARN) -Os $(ARDUINO_SIM) -fsyntax-only $(SRC_DIR)/FR_math_2D.cpp + @echo " compile library sources under Arduino: PASS" + @$(CC) $(CFLAGS) -c $(SRC_DIR)/FR_math.c -o $(BUILD_DIR)/test_arduino_compat_FR_math.o + @$(CXX) -x c++ $(CXXFLAGS) $(ARDUINO_SIM) $(TEST_DIR)/test_arduino_compat.c -x none $(BUILD_DIR)/test_arduino_compat_FR_math.o $(LDFLAGS) -o $(BUILD_DIR)/test_arduino_compat + @./$(BUILD_DIR)/test_arduino_compat + .PHONY: test-basic test-basic: $(BUILD_DIR)/fr_test @echo "Running basic tests..." diff --git a/pages/guide/building.html b/pages/guide/building.html index 5ab570f..a305bcb 100644 --- a/pages/guide/building.html +++ b/pages/guide/building.html @@ -107,7 +107,7 @@

tools/make_release.sh

The test suite

-

Tests live under tests/ and are split into seven +

Tests live under tests/ and are split into eight binaries to keep compile times low:

@@ -120,10 +120,14 @@

The test suite

+
test_fullFull-coverage dark-corner cases and round-trips.
test_2d_completeExtended 2D: matrix composition, inverse, point transforms.
test_tddCharacterization tests pinned to bit-exact reference values.
test_arduino_compatArduino build compatibility: compiles the headers as ESP32-style and AVR/USBAPI-style cores would (both include orders), plus type-size checks (issue #11).
-

The suite covers 99% of the library source. +

The suite covers 98% of the library source lines +and every mathematically reachable branch; the remainder is defensive +code (clamps and guards) that no input can trigger, kept in place as +insurance against future edits. Every public symbol is exercised at least once.

Running a single binary

@@ -275,7 +279,17 @@

Example: Arduino

# Or try the focused examples: arduino-cli compile --fqbn arduino:avr:uno examples/basic-math arduino-cli compile --fqbn arduino:avr:uno examples/trig-functions -arduino-cli compile --fqbn arduino:avr:uno examples/wave-generators +arduino-cli compile --fqbn arduino:avr:uno examples/wave-generators + +# Non-AVR cores work the same way, e.g. ESP32: +arduino-cli compile --fqbn esp32:esp32:esp32 examples/arduino_smoke + +

make test-arduino-compat (part of make test) +simulates the Arduino build environments on the host — ESP32-style +cores with no USBAPI.h typedefs and AVR-style cores that predefine +u8/u16, in both include orders — so +core-specific type clashes like issue #11 are caught without any +board toolchain installed.

See the code size table above for exact numbers. With linker dead-code elimination, only the functions you call are linked.

diff --git a/pages/guide/getting-started.html b/pages/guide/getting-started.html index e5076bf..2d58081 100644 --- a/pages/guide/getting-started.html +++ b/pages/guide/getting-started.html @@ -25,8 +25,35 @@

Getting Started

Install

-

FR_Math is a handful of C and C++ files. There is no package -manager integration and no install step. Either:

+

FR_Math has no dependencies to install — it needs nothing +beyond <stdint.h>, which every C99-or-newer toolchain +ships. “Installing” here only means getting the sources into +the project, either from a package registry or by copying them in +directly.

+ +

From a package manager

+ +

FR_Math is published to the major embedded package registries:

+ +
    +
  • Arduino (IDE or CLI) — open the Library Manager + and search for FR_Math, or run + arduino-cli lib install FR_Math. Then + #include <FR_math.h> in the sketch. Works on + all cores (AVR, ESP32, RP2040, SAMD, STM32, ...).
  • +
  • PlatformIO — add + lib_deps = deftio/fr_math to + platformio.ini, or run + pio pkg install --library "deftio/fr_math".
  • +
  • ESP-IDF — run + idf.py add-dependency "deftio/fr_math" to pull it + from the ESP Component Registry.
  • +
+ +

Vendoring the sources

+ +

FR_Math is a handful of C and C++ files, so dropping it directly +into a project works just as well. Either:

  • Copy src/FR_math.c, src/FR_math.h, @@ -37,8 +64,8 @@

    Install

    src/.
-

There are no external dependencies beyond <stdint.h>, -which every C99-or-newer toolchain ships.

+

Nothing else needs to be added to the build — no libraries to +link (not even libm), no configuration step.

git clone https://github.com/deftio/fr_math.git
 cd fr_math
@@ -46,7 +73,7 @@ 

Install

build.sh wipes build/, rebuilds the library, examples, and tests, and runs the full test suite. On success -the output shows all tests passing (99% line coverage).

+the output shows all tests passing (98% line coverage, 100% of reachable branches).

A first program

@@ -287,7 +314,7 @@

Running the test suite

make coverage # coverage report (requires gcov)

Run make test for a full pass. With make coverage, -line coverage of the library sources is about 99%. +line coverage of the library sources is about 98%, with every mathematically reachable branch exercised. See Building & Testing for targets, cross-compilation, and CI.

diff --git a/pages/index.html b/pages/index.html index 9713405..b449193 100644 --- a/pages/index.html +++ b/pages/index.html @@ -49,32 +49,32 @@

Measured accuracy

Run make test-tdd to generate the TDD report (build/test_tdd_report.md) with sweeps at radixes 8, 12, 16, and 24.

- - - - - - - - - - - - - - - - - - - - - - - - -
FunctionMax err (%)*Avg err (%)Note
sin/cos (BAM)0.15260.0030very fast binary angle trig
sin/cos (deg)0.15260.0029degree input trig fns
sin/cos (rad)0.18280.0033radian (traditional) trig
tan (BAM)0.58230.0008binary angle tangent; ±maxint at poles
tan (deg)0.53110.0008degree input tangent; saturated at poles
tan (rad)0.03860.0001radian (traditional) tangent
asin / acos0.77710.0280reverse trig, radian output
atan20.25640.0237reverse tangent, always safe
atan0.24250.0155reverse tangent, accepts up to maxint
sqrt0.00000.0000Round-to-nearest
log20.01160.0016shift/add only for speed
pow20.00180.0004shift/add only for speed
ln, log100.00040.0000shift/add only for speed
exp0.00030.0000shift/add only for speed
exp_fast0.00090.0001Shift-only scaling
pow100.00050.0000shift/add only for speed
pow10_fast0.00220.0002Shift-only scaling
hypot (exact)0.00000.0000Uses 64-bit intermediate
hypot_fast8 (8-seg)0.09150.0320Shift-only, no multiply
-

*Relative error; reference clamped to 1% of full-scale output.

+ + + + + + + + + + + + + + + + + + + + + + + + +
FunctionMax err (%)*Avg err (%)Note
sin/cos (BAM)0.15260.0030very fast binary angle trig
sin/cos (deg)0.15260.0029degree input trig fns
sin/cos (rad)0.18280.0033radian (traditional) trig
tan (BAM)0.58230.0008binary angle tangent; ±maxint at poles
tan (deg)0.53110.0008degree input tangent; saturated at poles
tan (rad)0.03860.0001radian (traditional) tangent
asin / acos0.77710.0280reverse trig, radian output
atan20.25640.0237reverse tangent, always safe
atan0.24250.0155reverse tangent, accepts up to maxint
sqrt0.00000.0000Round-to-nearest
log20.01160.0016shift/add only for speed
pow20.00180.0004shift/add only for speed
ln, log100.00040.0000shift/add only for speed
exp0.00030.0000shift/add only for speed
exp_fast0.00090.0001Shift-only scaling
pow100.00050.0000shift/add only for speed
pow10_fast0.00220.0002Shift-only scaling
hypot (exact)0.00000.0000Uses 64-bit intermediate
hypot_fast8 (8-seg)0.09150.0320Shift-only, no multiply
+

*Relative error; reference clamped to 1% of full-scale output.

What’s in the box

diff --git a/pages/releases.html b/pages/releases.html index 96e18bb..d96d1ee 100644 --- a/pages/releases.html +++ b/pages/releases.html @@ -21,6 +21,19 @@

Releases

release_notes.md in the repo.

+

v2.0.9 — 2026

+ +

Arduino/ESP32 build fix and a saturating-add correctness fix. No API changes.

+ +
    +
  • Arduino u8/u16 type error fixed (issue #11): sketches failed to compile on ESP32 — and on any core other than AVR/SAM/SAMD — with 'u16' does not name a type. FR_defs.h had skipped its typedefs in Arduino C++ builds, assuming USBAPI.h always provides them; it now emits them using USBAPI.h's exact underlying types, which works whether or not that header is present. Thanks to @beaka for the report.
  • +
  • FR_FixAddSat correctness: the overflow check relied on signed-overflow UB and was optimized away at -Os, so negative overflow returned a wrapped positive value instead of FR_OVERFLOW_NEG. Also, FR_FixAddSat(0, 0) wrongly returned FR_OVERFLOW_POS. Both fixed.
  • +
  • New test_arduino_compat suite: compiles the headers as each Arduino core family does (with and without USBAPI.h typedefs, both include orders, C and C++), catching core-specific type clashes without a board toolchain.
  • +
  • Coverage: 98% of library source lines, every mathematically reachable branch exercised.
  • +
+ +
+

v2.0.8 — 2026

Tangent accuracy rewrite and trig rounding fix.

diff --git a/pages/version.json b/pages/version.json index f81a375..a26fd82 100644 --- a/pages/version.json +++ b/pages/version.json @@ -1 +1 @@ -{"version":"2.0.8","hex":"0x020008"} +{"version":"2.0.9","hex":"0x020009"} diff --git a/release_notes.md b/release_notes.md index 5ff9659..375e678 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,5 +1,82 @@ # FR_Math Release Notes +## Version 2.0.9 (2026) + +Arduino/ESP32 build fix and a saturating-add correctness fix. No API +changes — `FR_math.h` declarations are identical to 2.0.8. + +### Arduino: `u8`/`u16` did not name a type on non-AVR cores + +- **Fixed** ([issue #11](https://github.com/deftio/fr_math/issues/11)): + sketches failed to compile on ESP32, and on any Arduino core other + than AVR/SAM/SAMD, with `'u16' does not name a type`. Thanks to + [@beaka](https://github.com/beaka) for reporting this, on ESP32 dev + and ESP32-S3 boards with Arduino IDE 2.3.10. +- Cause: `FR_defs.h` skipped its `u8`/`u16` typedefs entirely in Arduino + C++ builds, on the assumption that `USBAPI.h` always supplies them. + That holds only for the AVR/SAM/SAMD cores; ESP32, RP2040, STM32 and + others have no such typedefs, so the types were left undefined. +- Fix: in Arduino C++ builds the typedefs are now emitted using + `USBAPI.h`'s exact underlying types (`unsigned char` / + `unsigned short`). Where `USBAPI.h` is present the duplicate typedef + is identical and legal C++; where it is absent the types are defined. + Both include orders work. + +### `FR_FixAddSat` returned wrong results + +- **Fixed**: `FR_FixAddSat(x, y)` computed `x + y` in signed arithmetic + and then tested the result for wraparound. Signed overflow is + undefined behavior, so optimizers were free to delete the check — at + `-Os`, clang did, and negative overflow returned a wrapped positive + value instead of `FR_OVERFLOW_NEG`. The sum is now formed in unsigned + arithmetic, which is well-defined. +- **Fixed**: `FR_FixAddSat(0, 0)` returned `FR_OVERFLOW_POS` instead of + `0`. The positive-overflow test was `sum <= 0`; it is now `sum < 0`, + since a zero sum from two non-negative operands is legitimate. + +### Testing + +- New `tests/test_arduino_compat.c` and `make test-arduino-compat` + target (now part of `make test`, bringing the suite to eight + binaries). It compiles the headers the way each Arduino core family + does — ESP32-style with no `USBAPI.h` typedefs, AVR-style with them + predefined, in both include orders, as both C and C++ — plus runtime + type-width checks. Catches core-specific type clashes without a board + toolchain installed. +- New branch-coverage tests in `tests/test_full_coverage.c` covering + previously unexercised paths: radian range reduction beyond 2π and + 4π, degree reduction beyond ±360°, `radix == 0` fast paths in + `fr_cos_deg`/`fr_sin_deg`, BAM pole handling in `fr_tan_deg`, + radix-conversion arms in `FR_acos`/`FR_asin`/`FR_atan2` at output + radix 14 and 20, `FR_pow2(x, 0)`, `FR_printNumF` with `radix == 0` + and `prec == 0`, `FR_numstr` leading `+` and >9 fractional digits, + `FR_hypot_fast8` at `INT32_MIN`, and the `fr_wave_noise` / + `fr_adsr_step` clamps. +- Library coverage is now 98% of source lines, with every + mathematically reachable branch exercised. The remainder is defensive + code (clamps and guards) that no input can reach; it is kept in place + deliberately. + +### Documentation + +- **Install instructions corrected**: `docs/getting-started.md`, + `pages/guide/getting-started.html`, and `llms.txt` now show how to + obtain the library from the package registries it is published to + (Arduino Library Manager, PlatformIO, ESP-IDF), alongside copying the + sources in directly. The guide previously said FR_Math had "no + package manager integration", which conflated two separate things: + the library indeed has no *dependencies* to install, but it is and + was distributed through those registries — which is how the reporter + in issue #11 obtained it. +- `docs/building.md` and `pages/guide/building.html`: test-suite table + now lists eight binaries including `test_arduino_compat`, with an + ESP32 `arduino-cli compile` example and a note on what the + compatibility matrix covers. +- `agents.md` records the `FR_defs.h` Arduino typedef invariant in + "What NOT to do", so the issue #11 fix is not undone by a future + cleanup, and the test count is corrected to eight. +- Coverage figures refreshed across README, the guides, and `agents.md`. + ## Version 2.0.8 (2026) Tangent accuracy rewrite and trig rounding fix. diff --git a/scripts/coverage_report.sh b/scripts/coverage_report.sh index 143d792..17ff8df 100755 --- a/scripts/coverage_report.sh +++ b/scripts/coverage_report.sh @@ -117,7 +117,8 @@ parse_coverage() { local percent=0 if [ "$total_lines" -gt 0 ]; then - percent=$((executed_lines * 100 / total_lines)) + # round to nearest, not truncate: 97.99% must not report as 97% + percent=$(((executed_lines * 100 + total_lines / 2) / total_lines)) fi echo "$executed_lines $total_lines $percent $not_executed" @@ -171,7 +172,7 @@ total_lines=$((fr_math_total + fr_2d_total)) total_exec=$((fr_math_exec + fr_2d_exec)) if [ "$total_lines" -gt 0 ] && [ "$total_lines" != "0" ]; then - overall_percent=$((total_exec * 100 / total_lines)) + overall_percent=$(((total_exec * 100 + total_lines / 2) / total_lines)) else overall_percent=0 fi diff --git a/src/FR_defs.h b/src/FR_defs.h index 631126e..fc12694 100644 --- a/src/FR_defs.h +++ b/src/FR_defs.h @@ -64,19 +64,21 @@ typedef signed long long int64_t; #endif /* FR_NO_STDINT */ /* - * Arduino's USBAPI.h typedefs u8 and u16 as unsigned char / unsigned short. - * On AVR, uint8_t/uint16_t resolve to unsigned int types, which are the same - * width but different C++ types — causing a redefinition error. Skip those - * two typedefs when building in an Arduino environment; the Arduino-provided - * types are the same width and work identically. + * Arduino's AVR/SAM/SAMD cores (USBAPI.h, pulled in by Arduino.h) typedef + * u8 as unsigned char and u16 as unsigned short. On AVR, uint16_t is + * unsigned int — the same width as unsigned short but a distinct C++ + * type — so "typedef uint16_t u16;" would clash with USBAPI.h's typedef. + * Other Arduino cores (ESP32, RP2040, STM32, ...) have no USBAPI.h + * typedefs at all and need ours (see GitHub issue #11). * - * The guard checks __cplusplus too because Arduino.h (which pulls in - * USBAPI.h) is only auto-included in .ino/.cpp translation units. - * Plain-C files (.c) compiled by the Arduino build system need our - * typedefs even though the ARDUINO macro is defined for them. + * In Arduino C++ builds we therefore typedef u8/u16 with USBAPI.h's exact + * underlying types: where USBAPI.h is present the duplicate typedef is + * identical and legal C++; where it is absent these provide the types. + * unsigned char / unsigned short are 8/16-bit on all Arduino targets. */ #if defined(ARDUINO) && defined(__cplusplus) - /* Arduino C++ TU — USBAPI.h already provides u8 and u16 */ +typedef unsigned char u8; +typedef unsigned short u16; #else typedef uint8_t u8; typedef uint16_t u16; diff --git a/src/FR_math.c b/src/FR_math.c index 95809f8..1de4ae6 100644 --- a/src/FR_math.c +++ b/src/FR_math.c @@ -708,16 +708,20 @@ s32 FR_FixMulSat(s32 x, s32 y) */ s32 FR_FixAddSat(s32 x, s32 y) { - s32 sum = x + y; + /* Sum in unsigned space: signed overflow is undefined behavior, and + * optimizers legally delete wrap-detection checks written as x + y. */ + s32 sum = (s32)((u32)x + (u32)y); if (x < 0) { - if (y < 0) - return (sum >= 0) ? FR_OVERFLOW_NEG : sum; + if (y < 0 && sum >= 0) + return FR_OVERFLOW_NEG; } else { - if (y >= 0) - return (sum <= 0) ? FR_OVERFLOW_POS : sum; + /* nonneg + nonneg wraps into [INT32_MIN, -2]; a zero sum (0+0) + * is legitimate, so only sum < 0 signals overflow */ + if (y >= 0 && sum < 0) + return FR_OVERFLOW_POS; } return sum; } diff --git a/src/FR_math.h b/src/FR_math.h index a2db262..1937295 100644 --- a/src/FR_math.h +++ b/src/FR_math.h @@ -32,8 +32,8 @@ #ifndef __FR_Math_h__ #define __FR_Math_h__ -#define FR_MATH_VERSION "2.0.8" -#define FR_MATH_VERSION_HEX 0x020008 /* major << 16 | minor << 8 | patch */ +#define FR_MATH_VERSION "2.0.9" +#define FR_MATH_VERSION_HEX 0x020009 /* major << 16 | minor << 8 | patch */ #ifdef FR_CORE_ONLY #define FR_NO_PRINT diff --git a/src/FR_math_2D.cpp b/src/FR_math_2D.cpp index b45ca75..1437e57 100644 --- a/src/FR_math_2D.cpp +++ b/src/FR_math_2D.cpp @@ -5,7 +5,7 @@ * * @copy Copyright (C) <2001-2026> * @author M A Chatterjee - * @version 2.0.8 M. A. Chatterjee, cleaned up naming + * @version 2.0.9 M. A. Chatterjee, cleaned up naming * * This file contains integer math settable fixed point radix math routines for * use on systems in which floating point is not desired or unavailable. diff --git a/src/FR_math_2D.h b/src/FR_math_2D.h index 3eaf7d3..2aca6c0 100644 --- a/src/FR_math_2D.h +++ b/src/FR_math_2D.h @@ -3,7 +3,7 @@ * * @copy Copyright (C) <2001-2026> * @author M A Chatterjee - * @version 2.0.8 M. A. Chatterjee, cleaned up naming + * @version 2.0.9 M. A. Chatterjee, cleaned up naming * * This file contains integer math settable fixed point radix math routines for * use on systems in which floating point is not desired or unavailable. diff --git a/tests/test_arduino_compat.c b/tests/test_arduino_compat.c new file mode 100644 index 0000000..cf39d96 --- /dev/null +++ b/tests/test_arduino_compat.c @@ -0,0 +1,81 @@ +/** + * test_arduino_compat.c - Arduino environment compatibility tests (issue #11) + * + * Verifies that FR_defs.h provides u8/u16/u32 (and friends) in every + * Arduino build flavor. The Makefile target test-arduino-compat compiles + * this file several ways to simulate the environments that matter: + * + * 1. -DARDUINO, C++ : ESP32/RP2040/STM32-style core — no + * USBAPI.h typedefs exist; FR_defs.h must + * supply u8/u16 (the issue #11 failure). + * 2. -DARDUINO, C++, + * -DFR_TEST_USBAPI_FIRST : AVR/SAM/SAMD-style core — USBAPI.h has + * already typedef'd u8/u16 before our + * header is included; must not clash. + * 3. -DARDUINO, C++, + * -DFR_TEST_USBAPI_AFTER : FR_math.h included before Arduino.h; + * USBAPI.h typedefs come second. + * 4. -DARDUINO, plain C : Arduino builds of FR_math.c; USBAPI.h + * never appears in C translation units. + * 5. no -DARDUINO, C and C++ : host builds, unchanged behavior. + * + * This file is written in the common subset of C89 and C++ so the same + * source compiles in every mode above. + */ + +/* Simulated ArduinoCore-avr USBAPI.h typedefs (verbatim underlying types) */ +#if defined(FR_TEST_USBAPI_FIRST) && defined(__cplusplus) +typedef unsigned char u8; +typedef unsigned short u16; +#endif + +#include "FR_math.h" + +#if defined(FR_TEST_USBAPI_AFTER) && defined(__cplusplus) +typedef unsigned char u8; +typedef unsigned short u16; +#endif + +#include + +/* Compile-time checks (C89-compatible negative-array-size trick) */ +typedef char fr_ct_u8_is_1_byte[(sizeof(u8) == 1) ? 1 : -1]; +typedef char fr_ct_u16_is_2_bytes[(sizeof(u16) == 2) ? 1 : -1]; +typedef char fr_ct_u32_is_4_bytes[(sizeof(u32) == 4) ? 1 : -1]; +typedef char fr_ct_u64_is_8_bytes[(sizeof(u64) == 8) ? 1 : -1]; +typedef char fr_ct_u8_unsigned[((u8)-1 > 0) ? 1 : -1]; +typedef char fr_ct_u16_unsigned[((u16)-1 > 0) ? 1 : -1]; +typedef char fr_ct_u32_unsigned[((u32)-1 > 0) ? 1 : -1]; +typedef char fr_ct_s16_signed[((s16)-1 < 0) ? 1 : -1]; +typedef char fr_ct_s32_signed[((s32)-1 < 0) ? 1 : -1]; + +/* The u16-typed public API must be declared and callable (issue #11 was a + failure to even parse these prototypes on ESP32). */ +static s32 use_u16_api(void) +{ + u16 bam = FR_DEG2BAM_I(45); + return fr_sin_bam(bam) + fr_cos_bam(bam); +} + +int main(void) +{ + int fails = 0; + + if (sizeof(u8) != 1 || sizeof(u16) != 2 || sizeof(u32) != 4) { + printf(" type sizes: FAIL\n"); + fails++; + } else { + printf(" type sizes (u8=1, u16=2, u32=4): PASS\n"); + } + + if (use_u16_api() == 0) { + /* sin(45)+cos(45) in s0.15 is decidedly nonzero */ + printf(" u16 BAM API callable: FAIL\n"); + fails++; + } else { + printf(" u16 BAM API callable: PASS\n"); + } + + printf("Arduino compat: %s\n", fails ? "FAIL" : "ALL PASS"); + return fails; +} diff --git a/tests/test_full_coverage.c b/tests/test_full_coverage.c index 36c00f0..e1f6666 100644 --- a/tests/test_full_coverage.c +++ b/tests/test_full_coverage.c @@ -1079,6 +1079,144 @@ int test_constants_complete() { return TEST_PASS; } +/*======================================================= + * Branch-coverage completion tests + * Each test targets a specific previously never-taken + * branch in FR_math.c, identified by gcov -b analysis. + *=======================================================*/ + +static int near_s32(s32 a, s32 b, s32 tol) { + s32 d = a - b; + if (d < 0) d = -d; + return d <= tol; +} + +static int cov_sink(char c) { (void)c; return 0; } + +/* normalize_to_r16 radix>16 arm; reduce_to_2pi both reduction arms */ +int test_cov_radian_reduction() { + /* radix 18 input: sin(pi/6) = 0.5 */ + if (!near_s32(fr_sin(137258, 18), 32768, 80)) return TEST_FAIL; + /* r > 4*pi: sin(5*pi) = 0 */ + if (!near_s32(fr_sin(1029435, 16), 0, 16)) return TEST_FAIL; + /* 2*pi < r <= 4*pi: sin(3*pi) = 0 */ + if (!near_s32(fr_sin(617661, 16), 0, 16)) return TEST_FAIL; + return TEST_PASS; +} + +/* fr_deg_to_bam |d| >= 360-degree reduction, both signs */ +int test_cov_deg_to_bam_reduction() { + if (fr_deg_to_bam(23625728, 16) != fr_deg_to_bam(32768, 16)) + return TEST_FAIL; /* 360.5 deg == 0.5 deg */ + if (fr_deg_to_bam(-23625728, 16) != fr_deg_to_bam(-32768, 16)) + return TEST_FAIL; /* -360.5 deg == -0.5 deg */ + return TEST_PASS; +} + +/* fr_cos_deg/fr_sin_deg radix==0 fast path; >=360deg reductions */ +int test_cov_deg_trig_paths() { + if (!near_s32(fr_cos_deg(45, 0), 46341, 60)) return TEST_FAIL; + if (!near_s32(fr_sin_deg(45, 0), 46341, 60)) return TEST_FAIL; + /* 400.5 deg reduces to 40.5 deg */ + if (fr_cos_deg(26247168, 16) != fr_cos_deg(2654208, 16)) return TEST_FAIL; + if (fr_tan_deg(26247168, 16) != fr_tan_deg(2654208, 16)) return TEST_FAIL; + return TEST_PASS; +} + +/* fr_tan_deg non-cardinal inputs that land exactly on BAM poles */ +int test_cov_tan_deg_pole_bam() { + /* 90 + 2^-16 deg -> bam 0x4000, past the pole: large negative */ + if (fr_tan_deg(5898241, 16) != -FR_TRIG_MAXVAL) return TEST_FAIL; + /* 270 + 2^-16 deg -> bam 0xC000 */ + if (fr_tan_deg(17694721, 16) != -FR_TRIG_MAXVAL) return TEST_FAIL; + return TEST_PASS; +} + +/* FR_FixAddSat overflow wraps. The negative case regression-tests the + * signed-overflow UB bug (optimizer deleted the wrap check at -Os), and + * the 0+0 case regression-tests the old `sum <= 0` false positive. */ +int test_cov_addsat_neg_overflow() { + if (FR_FixAddSat(-2000000000, -2000000000) != (s32)0x80000000) + return TEST_FAIL; + if (FR_FixAddSat(2000000000, 2000000000) != (s32)0x7fffffff) + return TEST_FAIL; + if (FR_FixAddSat(0, 0) != 0) return TEST_FAIL; + return TEST_PASS; +} + +/* FR_acos/FR_asin radix-conversion arms not hit elsewhere */ +int test_cov_invtrig_radix_arms() { + s32 v; + /* clamp path with sign set, out_radix > 16 */ + if (FR_acos(-70000, 16, 20) != (FR_kPI << 4)) return TEST_FAIL; + /* input radix < 15: shift-left normalization; acos(0.5) = pi/3 */ + if (!near_s32(FR_acos(128, 8, 16), 68629, 400)) return TEST_FAIL; + /* small-angle fast path with radix >= out_radix */ + v = FR_acos(65500, 16, 16); + if (v < 1500 || v > 2600) return TEST_FAIL; + /* fast path, negative input, out_radix > 16 */ + v = FR_acos(-65500, 16, 20); + if (!near_s32(v, (FR_kPI << 4) - 2172 * 16, 9000)) return TEST_FAIL; + /* table path with out_radix <= 14: acos(0) = pi/2 at r14 */ + if (!near_s32(FR_acos(0, 16, 14), 25736, 40)) return TEST_FAIL; + /* asin with out_radix > 16: asin(0.5) = pi/6 at r20 */ + if (!near_s32(FR_asin(32768, 16, 20), 549066, 4000)) return TEST_FAIL; + return TEST_PASS; +} + +/* FR_atan2 axis cases and fast paths at out_radix 14 and 20 */ +int test_cov_atan2_radix_arms() { + if (FR_atan2(5, 0, 20) != (FR_kQ2RAD << 4)) return TEST_FAIL; + if (FR_atan2(-5, 0, 20) != -(FR_kQ2RAD << 4)) return TEST_FAIL; + if (FR_atan2(0, -5, 20) != (FR_kPI << 4)) return TEST_FAIL; + if (!near_s32(FR_atan2(1, 100, 14), 164, 30)) return TEST_FAIL; + if (!near_s32(FR_atan2(100, 1, 20), 1636640, 6000)) return TEST_FAIL; + if (!near_s32(FR_atan2(100, 1, 14), 25572, 80)) return TEST_FAIL; + return TEST_PASS; +} + +/* radix==0 / prec==0 arms in FR_pow2, FR_printNumF; FR_numstr edges */ +int test_cov_pow2_print_numstr() { + if (FR_pow2(3, 0) != 8) return TEST_FAIL; + /* radix 0, prec 0: prints "42", returns char count */ + if (FR_printNumF(cov_sink, 42, 0, 0, 0) != 2) return TEST_FAIL; + /* leading '+' sign */ + if (FR_numstr("+2.5", 16) != 163840) return TEST_FAIL; + /* more than 9 fractional digits: extras ignored */ + if (FR_numstr("0.1234567890123", 16) != FR_numstr("0.123456789", 16)) + return TEST_FAIL; + return TEST_PASS; +} + +/* FR_hypot_fast8 INT32_MIN mirror clamps */ +int test_cov_hypot_intmin() { + if (FR_hypot_fast8((s32)0x80000000, 100) < 2140000000) return TEST_FAIL; + if (FR_hypot_fast8(100, (s32)0x80000000) < 2140000000) return TEST_FAIL; + return TEST_PASS; +} + +/* fr_wave_noise -32768 clamp; fr_adsr_step defensive arms */ +int test_cov_noise_adsr_edges() { + fr_adsr_t env; + u32 st = 2; /* lsb 0 -> state 1 -> top bits 0 -> v = -32768 -> clamp */ + if (fr_wave_noise(&st) != -32767) return TEST_FAIL; + + /* idle envelope with no trigger returns 0 */ + fr_adsr_init(&env, 10, 10, 16384, 10); + if (fr_adsr_step(&env) != 0) return TEST_FAIL; + + /* defensive default: unknown state value (struct is caller-owned) */ + env.state = 99; + if (fr_adsr_step(&env) != 0) return TEST_FAIL; + + /* defensive out<0 clamp: force a negative level mid-attack */ + fr_adsr_init(&env, 65535, 0, 16384, 0); + env.state = FR_ADSR_ATTACK; + env.level = -(1 << 28); + if (fr_adsr_step(&env) != 0) return TEST_FAIL; + return TEST_PASS; +} + /* Main test runner */ int main() { printf("\n=== FR_Math Full Coverage Test Suite ===\n\n"); @@ -1130,6 +1268,18 @@ int main() { printf("\nDark-Corner Edge Branches:\n"); RUN_TEST(test_edge_branches); + printf("\nBranch-Coverage Completion:\n"); + RUN_TEST(test_cov_radian_reduction); + RUN_TEST(test_cov_deg_to_bam_reduction); + RUN_TEST(test_cov_deg_trig_paths); + RUN_TEST(test_cov_tan_deg_pole_bam); + RUN_TEST(test_cov_addsat_neg_overflow); + RUN_TEST(test_cov_invtrig_radix_arms); + RUN_TEST(test_cov_atan2_radix_arms); + RUN_TEST(test_cov_pow2_print_numstr); + RUN_TEST(test_cov_hypot_intmin); + RUN_TEST(test_cov_noise_adsr_edges); + printf("\n=== Test Summary ===\n"); printf("Total: %d, Passed: %d, Failed: %d\n", test_count, test_count - fail_count, fail_count);