# Atlas Library — Specification

**Status:** draft for build.

Already built and shipping in Atlas v6.4.0: Reader mode, the `.reel` player, the dial
scrub, the LED bias light, and the `ReelEnc` encoder. Library itself, the streaming
client, and the converters are unbuilt.

Supersedes `ATLAS_READER_SPEC.md`, which described a USB-tethered module. That design is
dead — it required Atlas to act as a USB host, which needs hardware changes to Atlas
(CC resistors and a VBUS source, because its port is wired as a device port).

Print and Play Creative Manufacturing, 162 Locke Street South, Hamilton, Ontario.

---

## 1. What this is

**Library** is a self-powered base station with storage and a speaker. Atlases connect to
it over WiFi in Reader mode and stream content from it.

It exists because of a hardware fact: Atlas has no audio and no room for big files. Its
LittleFS partition is 917,504 bytes total, while a 22-minute episode is roughly 4.9 MB as
a reel and a feature film around 26 MB. Library holds the files and owns the only speaker
in the system; Atlas renders picture and text.

Nothing about Atlas's hardware changes.

---

## 2. Architecture

```
                 ┌──────────────────────────────┐
                 │  LIBRARY                     │
                 │  ESP32 + storage + speaker   │
                 │  WiFi SoftAP + HTTP          │
                 └───────────┬──────────────────┘
                    WiFi     │  video + text streams
              ┌──────────────┼──────────────┐
          ┌───┴───┐      ┌───┴───┐      ┌───┴───┐
          │ Atlas │      │ Atlas │      │ Atlas │
          │ film  │      │ book  │      │ joined│
          └───────┘      └───────┘      └───────┘
                             audio ── Library's speaker
```

### Three independent layers

| Layer | Plays on | Shared? |
|---|---|---|
| **Audio** | Library's speaker | **one channel, shared** |
| **Video** | each Atlas's screen | per-client |
| **Text** | each Atlas's screen | per-client |

There is exactly one speaker, so audio is the single shared resource. Video and text are
per-client and fully independent — one person reads a novel while another watches a film's
picture and a third listens to music, all at once.

A **title** is a bundle of references (`odyssey.reel` + `odyssey.mp3`), so the same audio
file can be music on its own or a film's soundtrack depending on what it is paired with.

### Stream, do not cache

Titles are far larger than Atlas's filesystem, so nothing is downloaded whole. Atlas holds
a **~32 KB ring buffer**, about 8 seconds of cushion, out of its ~236 KB free RAM.

This is affordable because the reel stays compressed on the wire:

| | per client |
|---|---|
| video, 16 fps | **~30 kbps** |
| video, 24 fps | ~56 kbps |
| text | negligible |
| audio | **0 — never crosses the wire** |

Eight viewers is under 500 kbps against 1–4+ Mbps of WiFi. Delta frames are safe here
because HTTP runs over TCP and retransmission is handled for us — the fragility that ruled
compression out over a raw serial link does not exist over TCP.

**Concurrency ceiling is the AP's station count** (4 by default, ~10 configurable), not
bandwidth. That is a direct consequence of serving files rather than frames.

---

## 3. The one-channel audio rule

Library plays exactly one thing with sound at a time.

- Whoever hits play first **starts the show**. Solo playback is a screening with one viewer.
- Anyone else either **joins** it — in sync, with sound — or **watches something else
  silently**.

No arbitration UI, no "claim the speaker" dialog, one sentence to explain. It also gives
people a reason to gather round, which suits a camp room or a shop floor.

### Sync

Library broadcasts a beacon ~4×/sec:

```
{ showId, reelId, frameIndex, libraryMillis }
```

Each Atlas compares its own position and **nudges its next frame deadline** when drift
exceeds a couple of frames, hard-seeking only past ~0.5 s. Gentle correction avoids
visible stutter. The scores are music rather than dialogue, so ±100 ms is imperceptible.

**Late joiners** seek to the nearest keyframe at or before the broadcast frame — the same
index used by the dial scrub.

---

## 4. Controls

One dial, one button. Each function is placed where its feedback exists.

| State | Dial | Short click | Long press |
|---|---|---|---|
| Playing, owns audio | **volume** (live) | pause | back / radio off |
| Paused | **scrub** — jog on a still frame | resume from here | back / radio off |
| Playing, silent or solo | scrub | pause | back / radio off |
| Joined another's show | — | leave show | back / radio off |
| Reading text | page | bookmark | back / radio off |

**Volume is adjusted while playing, scrub while paused** — deliberately, and this is worth
not "fixing" later. Volume needs to be audible while you set it, or you end up pausing,
turning, resuming and discovering it is still wrong. Scrub needs a still frame, and
jogging while paused avoids Library re-seeking the audio on every click.

On a shared screening the owner pausing to jog pauses it for the room, which is the
socially correct behaviour.

### Scrub acceleration — required for long-form

At one keyframe per 2 s, crossing a 90-minute film one click at a time is ~2,700 clicks.
Dial **velocity must scale the jump**: a slow turn nudges ~2 s, a fast spin moves minutes.
Show a scrub bar and a time readout while paused.

A fixed step is fine for a 30-second montage and useless on a feature, so this has to be
designed in rather than retrofitted.

---

## 5. Atlas UX

```
  READER                      [wifi]
  ─────────────────────────────────
  > ▶ NOW PLAYING  Steamboat  0:14
    ♪ Cab Calloway            3:21
    ▤ The Odyssey (book)      p.42
      Felix                   0:19
      Betty Boop              0:28
```

- `▶` a live screening — click to join, in sync, with sound
- `♪` audio title — starts the audio channel
- `▤` text — paginated, no audio involvement
- no glyph — available on Library

Entering Reader brings the radio up; long-press drops it. The corner indicator makes it
visible when the radio is live, so battery cost is never a surprise.

---

## 6. Content formats

### Library normalizes, Atlas renders

Library has the CPU, RAM and storage, so it does all messy format work and hands Atlas
only what Atlas can consume directly:

- unzips EPUB and serves **plain text**
- decodes MP3/WAV **to its own speaker**
- serves `.reel` as-is

Atlas never learns what a ZIP or an ID3 tag is. Every new format is then a Library-side
change, not a firmware release.

### `.reel` v4

Documented in full in `ATLAS_READER_SPEC.md` §5 — header, packet layout, PackBits,
SH1106 framebuffer order, keyframe index. Only the transport around it changed when this
became a network format.

**v4 shipped in Atlas v6.5.0** and removed the length ceiling: frame counts and index
frame numbers are 32-bit, so the format is good for ~74,000 hours. What remains is the
4 GB `videoBytes` field, which matches FAT32's own file-size limit — about 15 hours of
film. All six existing reels were re-encoded to v4 and verified.

Two changes are still outstanding for long-form:

**Keyframes on a timer, not only at cuts.** The encoder emits one per shot, which is right
for a 30-second montage and useless for a film with a five-minute static scene — there
would be nothing to seek to. Long-form needs a forced keyframe every ~2 s. Costs roughly
10% file size.

**A subsampled index in RAM.** The firmware caps its in-memory index at 64 entries, which
suits a montage. An episode at one keyframe per 2 s needs ~660; a feature ~3,600. Atlas
should fetch the index, **subsample it** for the scrub bar, and use **HTTP Range requests**
to jump to a byte offset. The index then does triple duty: dial scrub, late-join, and
seeking a film.

**Automatic scene detection** is what makes per-shot levels possible on a feature — nobody
is hand-segmenting 90 minutes. The desktop encoder must run ffmpeg scene detection and
calibrate each detected cut separately. Without it every night scene is mush and every
daylight scene blows out. It is the single biggest quality lever in the pipeline.

### Encoding rules that must not be lost

Both are counter-intuitive and both were learned the hard way:

**Levels are measured per SHOT, never per frame.** Per-frame auto-levels look better on a
still and are much worse in motion — the dither crawls, and because every frame then
differs everywhere, delta compression collapses and the file roughly triples.

**Ordered dithering, not error diffusion.** With an ordered Bayer matrix a pixel's
threshold depends only on its coordinates, so a static picture dithers identically frame
after frame and the XOR delta is empty. Floyd–Steinberg looks marginally better per still
and destroys the compression.

---

## 7. Converters

**Web tool** — right for clips. Decode with WebCodecs or ffmpeg.wasm, then the pipeline
from `ReelEnc.cs`: crop/scale → per-shot levels → Bayer or hard threshold → SH1106 pack →
PackBits → key/delta.

**Desktop app — known future need, not a blocker.** A browser tab transcoding a two-hour
film is not reasonable. The same encoder wants to exist as a desktop drop-target for
full-length material. Clips first, features later.

---

## 8. Open questions

1. Library MCU and storage — a classic ESP32 works here (WiFi, and its DAC could even
   drive the amp), since nothing needs USB host any more. SD is the obvious storage.
2. AP station limit — 4 default, ~10 configurable. Sets the real viewer ceiling.
3. Radio coexistence. Reader gates WiFi on and off, and **BLE must be down before WiFi
   comes up** — the primer records what radio contention does to this device.
4. Battery. WiFi is a real draw on Atlas during playback and unavoidable for long titles.
5. Cache policy — worth caching short reels in LittleFS for instant offline replay even
   though long titles stream?
