Two claims in these comments were stronger than the permutation runs behind them. The WAV round trip catches sample-size against channels and leaves frame-count against sample-rate entirely green — the crop and the reformat are what catch that pair, and a reader who trusted the round trip would drop exactly the wrong case. The font file listed what it pins and never said that glyph-padding, offset-y and three of each atlas rectangle's four fields are read by nothing here at all. Two more permutations run and recorded while fixing it: GlyphInfo's image moved to the front, which shifts the four ints 24 bytes and collapses the glyph search, and Rectangle's x with width, which moves "measure ABC" to 39 and confirms the advance-0 fallback is the only thing reading a width out of the recs array.
203 lines
10 KiB
Plaintext
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-str name)
|
|
(print-str " ") (print-i64 (i64 (.frame-count w)))
|
|
(print-str " ") (print-i64 (i64 (.sample-rate w)))
|
|
(print-str " ") (print-i64 (i64 (.sample-size w)))
|
|
(print-str " ") (print-i64 (i64 (.channels w)))
|
|
(newline))
|
|
|
|
(defn show-bool [name string b bool]
|
|
(print-str name) (print-str " ")
|
|
(print-line (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)
|