The script is in tools/ rather than thrown away, because two lanes are
writing Flan in the old spelling right now and their files need the same
pass at merge.
It works on forms, not on text: a keyword becomes a dot only where it sits
in a field-label position inside a brace, so an enum member in value
position, a map key inside an EDN string and a type-position {K V} are all
left alone. :keys keeps its colon -- it names no field.
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 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)
|