flan/test/programs/raylib-audio.flan
Joseph Ferano 96ab4c9cf0 Retire the per-type printers, since print says all of it
print-str, print-i64, print-f64, print-bytes, print-line and newline leave
the prelude. print and println are the whole printing surface now, and print
is the better call at every one of the sites that used them: it is the same
structural walk without the newline, so the no-newline case the family was
kept for is covered, and it takes the value as it is. The old print-i64
forced an explicit (i64 x) at every call site, because this language widens
nothing implicitly; that cast is gone from 127 places.

Dropping it moves one answer. hash-grid returns u64, and the cast through
the signed printer showed sand-headless's hash as -2851001042534928384.
print routes a u64 through flan_u64_to_bytes, so it now prints
15595743031174623232 — the same 64 bits, read as the unsigned number they
are. The pinned expectation follows the correction.

test-flan-dev.el and test_session.ml both reached for print-line as "a name
the prelude has"; they reach for rand-seed instead.
2026-09-12 05:32:25 +07:00

203 lines
10 KiB
Plaintext

(import rl "vendor:raylib")
;; raylib's Wave family, headless — and the point of this file is that it IS
;; headless, which the brief for this lane said audio could not be.
;;
;; The split inside raylib's audio module runs exactly along that line. A
;; Sound is a buffer the miniaudio mixer owns and a Music is a decoder feeding
;; one, so neither exists until init-audio-device has found a device. A
;; **Wave** is samples in RAM and four integers describing them, and every
;; call that operates on one — copy, crop, reformat, export, load, decode to
;; floats — runs on the CPU with no device open at all. That is the Image
;; family's situation exactly, and it gets the Image family's kind of test:
;; raylib *computes*, and the answer is a number.
;;
;; Four separate claims are pinned here, and they are different claims:
;;
;; 1. **Wave's layout, from scalars in and fields out.** wave-format is
;; handed three plain integers and rewrites all four u32 fields, one of
;; them — frame-count — *computed* from the ratio of the old and new
;; sample rates. The answers are 16, 16000, 8 and 2: all distinct, and
;; the 16 is named by no argument at all, so a permuted defstruct has
;; nothing to cancel against. This is gen-image-color's argument, which
;; is the strongest shape a headless FFI test has.
;;
;; 2. **The file, as external ground truth.** export-wave writes a RIFF
;; header carrying sample-rate, sample-size and channels; load-wave reads
;; it back. Both ends are dr_wav's and agree with each other, not with
;; whatever field order Flan believes in — the same reason the PNG round
;; trip in raylib-image.flan is not the symmetric trap a store-and-return
;; check is.
;;
;; Its reach is narrower than it looks and the narrowness is worth
;; knowing: exchanging sample-size and channels turns the loaded line
;; into "8 8000 16 16", while exchanging frame-count and sample-rate
;; leaves every line of the round trip untouched. What catches THAT pair
;; is the crop and the reformat, above. Neither claim covers the other.
;;
;; 3. **`data` as a pointer, and the bytes behind it.** load-wave-samples
;; decodes through the buffer. Move `data` up among the integers and
;; every number in this file reads half of a pointer instead.
;;
;; 4. **Crop's byte arithmetic, which is the axis discriminator.** raylib
;; crops at `init-frame * channels * sample-size/8` bytes into the
;; buffer, so cropping one frame out of the middle and decoding it says
;; which frame it was. That is what distinguishes sample-size from
;; channels: exchange them and a one-frame crop lands two bytes off and
;; the sample that comes back is a different number, not the same one
;; mirrored. Axis-aligned geometry could never do this and neither could
;; a round trip.
;;
;; The four integers describing the source wave are 8, 8000, 16 and 1 — all
;; different, for the same reason the test image in raylib-image.flan is 4 by
;; 2 and not square.
;; The samples, as bytes, little-endian 16-bit signed PCM. They are bytes
;; rather than i16 because `data` is (Ptr u8): a wave's element width is
;; `sample-size`, a run-time number, so no Flan type says what the buffer
;; holds. Writing them out is the honest spelling — the file then says what it
;; means about 16-bit PCM instead of hiding it behind a cast.
;;
;; 0, 1000, 2000, 3000, -3000, -2000, -1000, 0 — one cycle, every sample
;; different from its neighbours, both signs present, so a decode that lost
;; the byte order or the sign is a wrong number rather than merely a
;; different one.
(defvar pcm [16 u8])
(defn load-pcm []
(set (at pcm 0) 0x00) (set (at pcm 1) 0x00) ; 0
(set (at pcm 2) 0xE8) (set (at pcm 3) 0x03) ; 1000
(set (at pcm 4) 0xD0) (set (at pcm 5) 0x07) ; 2000
(set (at pcm 6) 0xB8) (set (at pcm 7) 0x0B) ; 3000
(set (at pcm 8) 0x48) (set (at pcm 9) 0xF4) ; -3000
(set (at pcm 10) 0x30) (set (at pcm 11) 0xF8) ; -2000
(set (at pcm 12) 0x18) (set (at pcm 13) 0xFC) ; -1000
(set (at pcm 14) 0x00) (set (at pcm 15) 0x00)) ; 0
(defconst wav-path "/tmp/flan-raylib-audio.wav")
(defn show-wave [name string w rl/Wave]
(print name)
(print " ") (print (.frame-count w))
(print " ") (print (.sample-rate w))
(print " ") (print (.sample-size w))
(print " ") (print (.channels w))
(println ""))
(defn show-bool [name string b bool]
(print name) (print " ")
(println (if b "yes" "no")))
;; A decoded sample is compared with a tolerance and the verdict is printed,
;; not the number. 1000 over a 15- or 16-bit full scale is 0.0305185 or
;; 0.0305176, and this table compares stdout byte for byte at two optimisation
;; levels, so printing the float would pin raylib's choice of divisor rather
;; than Flan's field order. Nothing is lost: a permuted layout is not out by a
;; rounding step, it reads a different buffer.
(defn near? [a f32 b f32] bool
(let [d (- a b)]
(< (if (< d 0.0) (- 0.0 d) d) 0.0005)))
;; One frame out of a wave, decoded to a float.
;;
;; This is shaped the way it is because of a real limit in Flan, not for
;; effect: load-wave-samples answers a (Ptr f32), a pointer cannot be indexed
;; — `at` takes an array, a slice or a string, and there is no pointer
;; arithmetic — so `deref` reaches sample 0 and nothing reaches sample 3. So
;; the wave is cropped to the single frame wanted FIRST, and then sample 0 is
;; the one being asked about.
;;
;; The detour pays for itself. Reading through an index would have exercised
;; only the decoder; cropping first puts raylib's byte-offset arithmetic —
;; init-frame times channels times sample-size over 8 — in front of the
;; decoder, and that arithmetic reads two of the fields this file is trying
;; to pin.
(defn frame-at [w rl/Wave i i32] f32
(let [one (rl/wave-copy w)]
(rl/wave-crop (addr one) i (+ i 1))
(let [s (rl/load-wave-samples one)
v (deref s)]
(rl/unload-wave-samples s)
(rl/unload-wave one)
v)))
(defn show-frame [name string w rl/Wave i i32 want f32]
(show-bool name (near? (frame-at w i) want)))
(defn main [] i32
(rl/set-trace-log-level :warning)
(load-pcm)
;; ── The wave, built by hand ─────────────────────────────────────────
;;
;; Nothing raylib made: four integers Flan chose and a buffer Flan owns. So
;; every number below is raylib reading THIS struct, and there is no
;; raylib-produced struct anywhere for a permutation to hide inside.
(let [src (rl/Wave {:frame-count 8 :sample-rate 8000 :sample-size 16
:channels 1 :data (addr (at pcm 0))})]
(show-bool "valid" (rl/wave-valid? src))
(show-wave "source" src)
;; ── Crop ────────────────────────────────────────────────────────────
;;
;; Frames 2 up to 6, so four of the eight survive and nothing else
;; changes. This is the one call that moves frame-count on its own, which
;; is what separates that field from sample-rate: exchange the two and the
;; crop is asked for frames 2..6 of a wave claiming 8000 of them, and the
;; 4 lands in the sample-rate slot instead.
;;
;; The wave is mono, so raylib 5.5's rename of these parameters from
;; samples to frames is not something this depends on having guessed
;; right — at one channel the two readings coincide.
(let [cropped (rl/wave-copy src)]
(rl/wave-crop (addr cropped) 2 6)
(show-wave "cropped" cropped)
(rl/unload-wave cropped))
;; ── Reformat ────────────────────────────────────────────────────────
;;
;; The strongest case in the file. Three scalars go in — 16000 Hz, 8 bits,
;; 2 channels — and four fields come out: 16, 16000, 8, 2. The 16 is
;; computed, twice the original eight frames because the rate doubled, and
;; it is the only one of the four no argument named. Eight bits rather
;; than sixteen so that sample-size cannot be confused with the frame
;; count it would otherwise equal.
(let [reformatted (rl/wave-copy src)]
(rl/wave-format (addr reformatted) 16000 8 2)
(show-wave "reformatted" reformatted)
(rl/unload-wave reformatted))
;; ── Through the pointer, one frame at a time ────────────────────────
;;
;; See frame-at: each of these crops to a single frame and decodes it, so
;; raylib's byte offset into `data` is what selects the answer. Frames 1,
;; 3 and 4 are +1000, +3000 and -3000 — three different magnitudes and
;; both signs, so neither an offset that is out by a frame nor a decode
;; that read the bytes backwards survives.
(show-frame "frame 1 is +1000" src 1 0.030518)
(show-frame "frame 3 is +3000" src 3 0.091553)
(show-frame "frame 4 is -3000" src 4 -0.091553)
(show-frame "frame 0 is zero" src 0 0.0)
;; ── Out to a file and back ──────────────────────────────────────────
;;
;; dr_wav writes the header from three of the four fields and the payload
;; length from the fourth, and reads all four back out of the file. Both
;; ends are external to Flan and agree with each other, so this is not the
;; round trip that passes for any field order — it is the PNG argument,
;; for audio. It also crosses a path as ptr+len.
;;
;; It does not catch everything, and the header note says which: exchange
;; frame-count and sample-rate and all three lines below stay green. The
;; crop and the reformat are what go red for that pair.
(show-bool "exported" (rl/export-wave src wav-path))
(let [back (rl/load-wave wav-path)]
(show-bool "loaded valid" (rl/wave-valid? back))
(show-wave "loaded" back)
;; And the samples off the file's own copy, so the bytes are checked to
;; have survived the encoder, the decoder and the layout at once.
(show-frame "loaded frame 1 is +1000" back 1 0.030518)
(show-frame "loaded frame 4 is -3000" back 4 -0.091553)
(rl/unload-wave back)))
0)