2026-10-21 05:24:03 +00:00
|
|
|
|
# Handoff notes
|
|
|
|
|
|
|
|
|
|
|
|
For whoever picks this up: what is solid, what is held together with care, and what a refactor should and should not
|
|
|
|
|
|
do. Read ARCHITECTURE.md first.
|
|
|
|
|
|
|
|
|
|
|
|
## Where the seams are
|
|
|
|
|
|
|
|
|
|
|
|
- `index.html` is ~1,900 lines in one script. It grew feature by feature in one long session. The sections are
|
|
|
|
|
|
commented and ordered sensibly, but state is one global `state` object and functions reach into each other freely.
|
2026-10-21 06:01:48 +00:00
|
|
|
|
- brain.js plugs into the page through `hooks` (`djPrompt`, `jobMade`, `stationOpened`, `reacted`); it no longer
|
|
|
|
|
|
overrides page functions by assignment. Anything else loaded after the page should do the same.
|
|
|
|
|
|
- Playback is one small state machine (`state.phase`: idle → cooking → holding → playing → ended) with named
|
|
|
|
|
|
transitions; see ARCHITECTURE.md › The player's state machine. `state.holding` is a read-only view of the phase.
|
2026-10-21 05:24:03 +00:00
|
|
|
|
- The crackle exists twice: as the side element (room tone) and as the player's own source (the hold). That is
|
|
|
|
|
|
deliberate (iOS needs the hold in the same element) but easy to misread.
|
2026-10-21 06:01:48 +00:00
|
|
|
|
- `db.all()` is still used where a filter is needed (a station's tracks, the prune in `remember`, the trail, shuffle,
|
|
|
|
|
|
the library, the brain). Single rows go through `db.get`; tapes through `tapeById` (KEPT is virtual). An index on
|
|
|
|
|
|
`spec.stationId` would remove most of the remaining `db.all()` calls.
|
2026-10-21 05:24:03 +00:00
|
|
|
|
- Several things are computed from the sheet in more than one place (section-at-time, mmss, kind chips).
|
|
|
|
|
|
|
2026-10-21 06:01:48 +00:00
|
|
|
|
## Found while refactoring playback, and deliberately left alone
|
|
|
|
|
|
|
|
|
|
|
|
- `playJob` on an errored render (`job.err`) calls `crackleOff()` and leaves the phase where it was; if the player was
|
|
|
|
|
|
holding, the crackle keeps looping in the player until skip. Pre-existing; the machine reproduces it.
|
|
|
|
|
|
- `advance()` with nothing to play (`!job`) calls `crackleOff()` and returns without `bed()`; the room tone comes back
|
|
|
|
|
|
on the next state change. Pre-existing.
|
|
|
|
|
|
- `#skip` during a hold pauses the crackle-in-player; the phase stays `holding` (paused) until the landing track takes
|
|
|
|
|
|
over through `toPlaying`. That is how it behaved before; `bed()` stays off meanwhile, as before.
|
|
|
|
|
|
- `setSource` now refuses to revoke the crackle element's object URL (the old `swapNow` could, on a rarely reachable
|
|
|
|
|
|
branch). The only intended behaviour change; it can only prevent a dead crackle.
|
|
|
|
|
|
- `toIdle` resolves a parked `queueBehindHold` promise when the station changes, so that `playJob` returns instead of
|
|
|
|
|
|
leaking; before, it stayed pending forever. Harmless either way.
|
|
|
|
|
|
- `prime()` still sets `state.audio.src` itself for the unlock clip and for the TAP TO PLAY start; it is the one source
|
|
|
|
|
|
write outside the machine, by design.
|
|
|
|
|
|
- brain.js's `drawPalette` and the library tiles call `playJob` directly with a resolved job; fine, but they bypass
|
|
|
|
|
|
`advance()`, so the session's back/forward list does not learn about them (as before).
|
|
|
|
|
|
- The `#play` button's `tapStarted` guard (ignore the click that `prime()` already used to start a resume) is timing
|
|
|
|
|
|
based (800 ms). Untouched.
|
|
|
|
|
|
- STORAGE.md listed four `db` calls and two stores; the code has `db.get` and a `tapes` store, so the doc was brought up
|
|
|
|
|
|
to the code (the contract itself is unchanged).
|
|
|
|
|
|
|
2026-10-21 05:24:03 +00:00
|
|
|
|
## Known rough edges
|
|
|
|
|
|
|
|
|
|
|
|
- iOS volume sliders are inert (platform); the welcome/ident may fall back to Venice if Runware times out on a
|
|
|
|
|
|
spoken-tag render (now avoided: spoken breaks are TTS).
|
|
|
|
|
|
- The mashup reference uses OfflineAudioContext; untested headlessly.
|
|
|
|
|
|
- The quota is per-person-per-day only; KV free tier caps total writes at ~1,000/day.
|
|
|
|
|
|
- Share links made before the render seed was included re-roll the song.
|
|
|
|
|
|
- `KEEP_UNKEPT = 40` unkept tracks roll off; nothing warns.
|
|
|
|
|
|
- Light theme: a few hard-coded colours may remain in rarely-seen sheets.
|
2026-10-21 06:01:48 +00:00
|
|
|
|
- The harness's resume mode checks the TAP TO PLAY wait but cannot press the tap: headless Chrome has no real user
|
|
|
|
|
|
activation. The tap half is a phone test.
|
2026-10-21 05:24:03 +00:00
|
|
|
|
|
|
|
|
|
|
## What a refactor should keep
|
|
|
|
|
|
|
2026-10-21 06:01:48 +00:00
|
|
|
|
- Every invariant in ARCHITECTURE.md › Playback invariants, and the harness green (both modes) before and after.
|
2026-10-21 05:24:03 +00:00
|
|
|
|
- The sheet as the unit (everything derives from `spec`), covers as a pure function of it.
|
|
|
|
|
|
- One static page, no build step, no framework, no keys in the page.
|
|
|
|
|
|
- STORAGE.md's contract (chai's DB adapter targets it).
|
|
|
|
|
|
|
2026-10-21 09:36:43 +00:00
|
|
|
|
## Player rules (each one cost a regression on the phone; don't relearn them)
|
|
|
|
|
|
|
|
|
|
|
|
- **Player changes go to `dev` first** (dev.gandora.lol), get tapped through on an iPhone, then merge to `main`.
|
|
|
|
|
|
Anything touching `startNow / toHolding / toPlaying / bed / crackleOn / prime / setSource` counts. Features that
|
|
|
|
|
|
don't touch the player (modes, DJ sheet, stats, covers) may go straight to main.
|
|
|
|
|
|
- Run `sh tools/harness.sh safari` as well as the Chrome modes: WebKit is the iPhone's engine (play() policy, one
|
|
|
|
|
|
audio session) and the Chrome loop mode currently hangs on hb-5.
|
|
|
|
|
|
- iOS ignores `audio.volume`. A level is baked into the sample (`makeCrackle(seconds, gain)`), never set on the element.
|
|
|
|
|
|
- Never set `state.playing = true` before `play()` resolves. The flag follows the promise (`playNow`); a refusal shows
|
|
|
|
|
|
TAP TO PLAY. Setting it early gives a "playing" UI with no sound and a play button that needs two taps.
|
|
|
|
|
|
- Only one `<audio>` carries the station. A second element (previews) can interrupt the first on iOS; after one
|
|
|
|
|
|
plays, expect the next `play()` on the main element to be refused and handled by TAP TO PLAY.
|
|
|
|
|
|
- The hold between tracks must keep `ended` firing: hold on a real looping clip with its own URL (silence when room
|
|
|
|
|
|
tone is OFF), never on `SILENCE`, which the `ended` handler ignores.
|
|
|
|
|
|
- The crackle bed is Web Audio and must never start over the music: `bedStart` refuses while the phase is playing and
|
|
|
|
|
|
the element is playing or a play() is pending.
|
|
|
|
|
|
|
2026-10-21 05:24:03 +00:00
|
|
|
|
## What's next (agreed directions, not started)
|
|
|
|
|
|
|
|
|
|
|
|
- Text embeddings for every sheet, audio embeddings (Gemini Embedding 2) for kept/reacted tracks; the taste map
|
|
|
|
|
|
laid out in embedding space; segment-level vectors with the timed reactions (`spec.hits`).
|
|
|
|
|
|
- Free tier = shared allowance on the worker (the key box is parked); pro = longer tracks, XL renders, sync.
|
|
|
|
|
|
- Room-tone picker (vinyl / rain / tape / brown / drone in key).
|
|
|
|
|
|
- Butterchurn (Milkdrop) as a fourth look.
|
|
|
|
|
|
- Taste file export/import.
|