|
| 1 | +"""One number, the same number, on every target audioif runs on. |
| 2 | +
|
| 3 | +``import audioif_util`` -- a pure-Python tier with no dependencies beyond |
| 4 | +``struct``, so it imports on CPython, MicroPython and CircuitPython alike. |
| 5 | +Nothing here touches the DSP; it exists so the numbers *handed* to the DSP |
| 6 | +stop depending on which interpreter derived them. |
| 7 | +
|
| 8 | +## Why this is needed at all |
| 9 | +
|
| 10 | +Python's float is the interpreter's ``mp_float_t``. On CPython and on a |
| 11 | +desktop MicroPython that is a **double**; on an ESP32-P4, an ESP32-S3, an |
| 12 | +RP2040 and any MicroPython built with ``MICROPY_FLOAT_IMPL_FLOAT`` it is a |
| 13 | +**single**. So:: |
| 14 | +
|
| 15 | + node.mix = 0.35 |
| 16 | +
|
| 17 | +is not one setting. It is ``0.34999999403953552`` on a board and |
| 18 | +``0.34999999999999998`` on a desktop, and a node that runs its dry/wet blend |
| 19 | +from it renders different bytes on the two. That is not a bug in the node, |
| 20 | +and no amount of care inside the kernel can fix it: the number was already |
| 21 | +two different numbers before it arrived. |
| 22 | +
|
| 23 | +`docs/correctness-standard.md` holds our nodes to every target rendering |
| 24 | +them identically, so a setting derived in Python arithmetic passes through |
| 25 | +:func:`float32` before it reaches a node. On a single-precision target that |
| 26 | +call is the identity. On a double one it rounds to the value the board would |
| 27 | +have held. Both then agree, which is the whole point. |
| 28 | +
|
| 29 | +The same rule is what audiocomponents#75 needs on the class side: a class |
| 30 | +computing ``360 * 4 ** (macro / 127)`` for a filter frequency is deriving a |
| 31 | +setting in Python, and its board and its desktop land a ULP apart until that |
| 32 | +derivation ends in :func:`float32`. |
| 33 | +
|
| 34 | +## And why printing needs its own function |
| 35 | +
|
| 36 | +:func:`float32_bits` is for output rather than settings. ``"%.6f" % value`` |
| 37 | +is not one string across interpreters even when ``value`` is bit-for-bit the |
| 38 | +same float32: six decimals of a single-precision number is seven significant |
| 39 | +digits, and MicroPython's single-precision formatter is not correctly |
| 40 | +rounded that far -- it prints ``-5.836908`` where the value is |
| 41 | +``-5.836907386779785``. A probe that prints a float at that width is |
| 42 | +comparing formatters, not DSP. The bit pattern is exact everywhere and |
| 43 | +strictly more sensitive than six decimals, so it is what the parity probes |
| 44 | +print. |
| 45 | +""" |
| 46 | + |
| 47 | +import struct |
| 48 | + |
| 49 | +__all__ = ("float32", "float32_bits") |
| 50 | + |
| 51 | + |
| 52 | +def float32(value): |
| 53 | + """``value`` rounded to the nearest IEEE-754 single, as a float. |
| 54 | +
|
| 55 | + The identity on any interpreter whose float already *is* a single, and a |
| 56 | + rounding on one whose float is a double -- which is exactly what makes |
| 57 | + the result the same number on both. |
| 58 | +
|
| 59 | + Idempotent: ``float32(float32(x)) == float32(x)`` everywhere. |
| 60 | +
|
| 61 | + **One stated limit, measured rather than assumed.** A magnitude past |
| 62 | + single-precision range is the one input on which the three runtimes do |
| 63 | + not agree: CPython's ``struct`` raises ``OverflowError`` and |
| 64 | + MicroPython's and CircuitPython's return an infinity. Keep settings |
| 65 | + inside the range a board can hold -- every audio setting is -- and this |
| 66 | + never arises; a value out there is a defect on the board too, and one |
| 67 | + that would render as silence rather than as an error. |
| 68 | + """ |
| 69 | + return struct.unpack("<f", struct.pack("<f", value))[0] |
| 70 | + |
| 71 | + |
| 72 | +def float32_bits(value): |
| 73 | + """``value`` as its IEEE-754 single bit pattern, lower-case hex. |
| 74 | +
|
| 75 | + Eight characters, little-endian byte order, e.g. ``"f2c7bac0"`` for |
| 76 | + -5.8369074. Exact on every interpreter, unlike a decimal conversion of |
| 77 | + the same number, so it is what a cross-target probe prints for a float |
| 78 | + it has to compare. |
| 79 | + """ |
| 80 | + return struct.pack("<f", value).hex() |
0 commit comments