tape.card audio player

Firmware

Five components and one store. Specified here, written once the brain is chosen — but the parts that are portable across every candidate brain are already written, as JavaScript, in tape/lib/, and tested.

Status: specified, not written. The data model, the wire protocol and the power budget exist as tested code today. The target is now fixed: ESP32-S3 + ESP-IDF/ESP-ADF — see the parts.

Components

CardWatcher   poll the PN532 at 8 Hz → present / absent, debounced
              · a card is only "gone" after 3 consecutive missed reads
              · the RF field is pulsed 10 ms in every 125 — a continuous
                field costs 50–80 mA and ends the battery version
              · two or more targets in the field → refuse, blink, wait
                (a deck of cards is a stack by nature)
Library       manifest.json → binary index, cached until mtime changes
              card id → playlist → track files → resume position
Player        queue · decode · volume · save position every 15 s and on removal
              ESP-ADF decodes MP3 · AAC · WAV · OGG · Opus · FLAC · AMR,
              so whatever the parent's phone recorded simply plays
  OutputStage   begin() · write(pcm) · setVolume()      ← one interface
                I²S → MAX98357A → 4Ω driver             ← one implementation
Net           SoftAP always (192.168.4.1, captive portal) + station mode
              once it knows your WiFi. Up when idle, down when playing.
Ingest        HTTP: a file input, a name field, a progress bar, plus /api/*
              — NOT the studio; the box cannot serve a microphone

The rule

Ingest is awake only when nothing is playing. WiFi comes up when the box is idle and goes down when a card lands. That removes the ESP32’s WiFi/Bluetooth coexistence problem, removes SD contention between the decoder and the uploader, and removes a whole class of audio glitch — at no cost anyone notices, because nobody uploads a story while their child is listening to one.

State machine

BOOT ──▶ IDLE ──card arrives──▶ PLAYING ──track ends, none left──▶ FINISHED
          ▲  │                     │  ▲                                │
          │  │                     │  └────── back / next ─────────────┘
          │  └──enrol requested──▶ ENROLLING ──blank tag written──▶ IDLE
          │                                                             
          ├──────────── card leaves (save position) ────────────────────┘
          │
          └──30 min quiet──▶ DEEP SLEEP ──button──▶ IDLE

IDLE       WiFi up, ingest serving, field pulsing        ~13 mA
PLAYING    WiFi down, ingest asleep, audio out live     ~129 mA
ENROLLING  armed for 60 s; the next blank tag on the pad is written and bound
FINISHED   card still on the pad, nothing left to play; LED off, no auto-repeat
CROWDED    two or more tags in the field; blink, play nothing, wait
DEEP SLEEP ~0.02 mA — bounded by the cell's self-discharge, not the circuit

Draws come from the power budget. The rule that WiFi and audio are never both awake is what stops PLAYING and IDLE ever adding up — and it is why one 18650 gives about twenty hours of stories.

What is already written and tested

The model is shared between the firmware and the studio so the two cannot drift, and it lives in this repository as dependency-free ES modules that run in a browser and in node:

Porting the model to the box is then a transcription with a test suite to check it against, rather than a second design.

The API the box serves

GET    /api/hello              version handshake
GET    /api/manifest           the catalogue
GET    /api/events             SSE: card | playback | enrolled | ingest | error
POST   /api/title              upload a recording
DELETE /api/title/:titleId
PUT    /api/card/:cardId       bind a card to a playlist
DELETE /api/card/:cardId
POST   /api/enroll             write the next blank tag on the pad
POST   /api/wifi               join a home network (also enables station mode)
POST   /api/say                speak a line through the box

Plus the receiving page at / on the box: a file input, a name field and a progress bar. It is deliberately not the studio — the box cannot serve a microphone, so it does the half it is allowed to do while the recording happens on the HTTPS side or in the phone’s own voice-memo app.