7.7 KiB
Decisions log
Append-only. Most recent first. Keep entries short — one paragraph max. Reference files with paths.
2026-04-28 — Image cache with capped concurrency; on-main-thread texture upload
gfx/images.jai is a small async image cache. UI calls image_request(item_id, size) from the draw loop; it always returns an *Image immediately whose .loaded flips true once the bytes arrive. Concurrency is capped at MAX_CONCURRENT_IMAGE_FETCHES = 4 via a pending queue (image_pending) drained by image_pump() once per frame — without this, a 500-album library would spawn 500 worker threads at first scroll. Decode + texture upload runs in the http callback (which is already main-thread per jellyfin/async.jai), satisfying OpenGL's no-cross-thread rule. Two cache buckets per item, keyed t:<id> (128 px thumb) and l:<id> (512 px). Sizing is requested from Jellyfin via ?fillHeight=N&fillWidth=N&quality=80 so we get a tight payload instead of full-resolution originals.
2026-04-28 — Visualizer reads a real FFT from the active stream's PCM
audio/fft.jai ships a small in-place radix-2 Cooley-Tukey FFT (~80 lines, no deps). audio/analysis.jai pulls FFT_SIZE=1024 frames from app.current_stream.sound_data.samples around play_cursor each frame, mixes to mono, applies a Hann window, FFTs, then bins magnitudes into SPECTRUM_BINS (=64) log-spaced bands between 30 Hz and the stream's Nyquist. Asymmetric smoothing (fast attack 0.45, slow decay 0.10) gives the classic VU-meter feel. Spectrum decays toward zero when the stream is paused or when there's no LINEAR_SAMPLE_ARRAY data. Since /universal-fetched tracks always decode through dr_libs to LINEAR_SAMPLE_ARRAY, the OGG-pass-through fallback rarely triggers.
2026-04-28 — Track download uses /Audio/{id}/universal, not /stream
/Audio/{id}/stream returns the raw original file. For libraries with M4A / AAC / Opus / ALAC / WMA tracks, dr_mp3/dr_flac can't decode the bytes and Sound_Player can't either — the user's "B-Roll" track came back as format=UNKNOWN then crashed in the diagnostic. /Audio/{id}/universal?container=mp3,flac,ogg,wav&audioCodec=mp3&maxStreamingBitrate=320000 lets the server send unmodified bytes when the source is in our container list, and transcodes to MP3 otherwise. Required params are userId + deviceId; api_key is also added as a query param because some streaming proxies strip the X-Emby-Authorization header. libcurl FOLLOWLOCATION is enabled — /universal 302-redirects to the transcoded stream URL.
2026-04-28 — Keep Sound_Player; add MP3/FLAC decoders, don't swap to miniaudio
User feedback when I proposed replacing Sound_Player with miniaudio: "I wouldn't change sound_player out, I would just add decoding from a few formats." So MP3/FLAC support is added by decoding bytes to s16 PCM via vendored dr_mp3.h/dr_flac.h and feeding the result into Sound_Player as Sound_Data with type = .LINEAR_SAMPLE_ARRAY. OGG and WAV continue to go through Sound.load_audio_data directly. Bigger libraries that prefer streaming (long tracks) might want a real decoder backend, but for now full-decode-to-PCM is simple and works.
2026-04-28 — HTTP runs on a per-request worker thread
All Jellyfin requests block the calling thread (libcurl easy_perform), and on the main thread that froze the UI for hundreds of ms (listings) or seconds (track downloads). Each request now spawns a Thread via Thread.thread_init + thread_start, the worker writes the result, the main loop calls http_pump() each frame which polls thread_is_done and fires the on_done callback. Per-request threads (no shared queue) keep the surface minimal — fine for ~5 concurrent requests; if we ever flood, switch to a thread pool.
2026-04-28 — Auto-advance via release_asset callback + track_finished flag
Sound_Player calls the release_asset callback when a stream finishes (from inside Sound.update(), which runs on the main thread, so it's safe to write app state). The callback sets app.track_finished = true, the main loop reads/clears it after Sound.update() and calls queue_next(). Streams are tagged with a monotonic entity_id (app.track_entity_id) so the callback can ignore stale finishes from streams we already replaced.
2026-04-28 — Pause is a volume mute, not a real pause
Sound_Player doesn't expose a clean per-stream pause. audio_toggle_pause flips user_volume_scale between 0 and 1. The cursor keeps moving while "paused" — so this is only correct for short pauses; the scrub bar is misleading and auto-advance still happens at the real end. Logged in ai/todo.md for a proper fix later.
2026-04-28 — Persist auth token, not password
~/.config/player/config.json stores {server_url, username, auth_token, user_id}. Password is never persisted. On startup, if auth_token and user_id are non-empty we mark logged_in = true and skip to library. If the server has invalidated the token, the first API call fails — user can hit "logout" to clear and re-enter password. Could auto-fall-back to login on 401, not yet wired.
2026-04-28 — GetRect widget wrappers thread loc := #caller_location
When wrapping stateful GetRect widgets (text_input, button, etc.) in a helper proc, the helper must accept loc := #caller_location and pass loc=loc to each widget. If multiple stateful widgets are drawn from the same wrapper, use identifier=N (unique constant per widget) to disambiguate them. See src/ui/views/login_view.jai:text_field. Without this, every call site of the wrapper looks identical to GetRect and you hit the runtime "widget drawn more than once" error.
2026-04-28 — lib/libcurl.so symlink for systems without libcurl-dev
Ubuntu 24.04 ships libcurl.so.4 but not the unversioned libcurl.so symlink (which would come from libcurl4-openssl-dev). The bundled Curl module's linux/lib/libcurl.so requires libssl.so.1.1 which Ubuntu 24.04 doesn't have. Workaround: lib/libcurl.so symlink in this repo pointing at the system libcurl.so.4, plus additional_linker_arguments = ["-Llib"] in build.jai. Curl is imported as Curl :: #import "Curl"()(LINUX_USE_SYSTEM_LIBRARY=true).
2026-04-28 — Vendor Jaison + stb_image, use bundled Curl
Jellyfin returns JSON, so Jaison is the obvious pick. For artist images we'll need PNG/JPG decode → stb_image (single header, ubiquitous). Both vendored under modules/. For HTTP we use the Jai-bundled Curl module rather than vendoring a single-header HTTP lib — libcurl is already the most-used C HTTP client and the bindings exist.
2026-04-28 — Folder-based code split with index.jai per folder
Each subfolder of src/ has an index.jai that #loads its files. src/main.jai only loads the index files. Keeps main.jai tiny per the user's brief.
2026-04-28 — One global app: App struct
Instead of threading state through procs, everything hangs off a single global. Smaller files, less ceremony, fits a vibe-coded project. If it gets messy we'll regret it and split.
2026-04-28 — Simp.set_render_target(window, .LEFT_HANDED) + GetRect_LeftHanded
Per user brief. Left-handed UI coords (y goes down) match how everyone thinks about UI.
2026-04-28 — Artist backdrop images in now-playing view
Image_Size.BACKDROP added to gfx/images.jai — 1920x1080 16:9 images fetched from /Items/{id}/Images/Backdrop/0. Cache key prefix is b: (disk: b_<id>.bin). The backdrop renders full-screen behind the now-playing UI with a 60% black tint overlay so text remains readable. Track.artist_id field added (populated from AlbumArtistId in Jellyfin responses) so we can request the correct backdrop for the current track's artist. Backdrops are fetched lazily when a track plays; if none exists, the view falls back to a solid dark background.