(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)