audioplayer/ai/decisions.md
2026-04-29 06:57:46 +03:00

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.

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.