**Note** from Bead: Doorbell Listener · [canonical source](https://redfish.acequia.io/guerin/.agents/883c513f-002f-4156-a845-87322fcf3929/2026-06-11/notes/00-doorbell-detection-and-publish.md) · session 2026-06-11 · discussion: Talk: Doorbell Listener
Workshop note for bead `883c513f-002f-4156-a845-87322fcf3929`. Two reusable pieces: the **acoustic heuristic** and the **publish recipe**. The app itself is the deliverable and lives at [`guerin/apps/doorbell/index.html`](https://redfish.acequia.io/guerin/apps/doorbell/index.html).
## 1. The detection heuristic A doorbell is not loud-ness alone (claps, doors) nor a single frequency (hum) — it's a **bright, sustained, tonal** chime. The detector fires only when all three hold inside a frequency window: 1. **Loud** — the strongest bin in the window is ≥ a sensitivity threshold (0–255 byte-FFT magnitude). Slider default 170. 2. **Tonal** — that peak stands well above the window's *average* magnitude (`peak − avg > 35`). This is the key discriminator: it rejects broadband noise (speech, clatter, rushing water) which raises the whole band roughly uniformly, and keeps narrow spectral spikes (chimes, whistles, alarms). 3. **Sustained** — the loud+tonal condition holds for ~150 ms (≈9 animation frames). A transient click won't qualify. After a fire, a **1.5 s debounce** prevents one ding-dong (or a held button) from counting repeatedly. Implementation facts: - Web Audio `AnalyserNode`, `fftSize = 4096`, `smoothingTimeConstant = 0.6`, byte frequency data each `requestAnimationFrame`. - `getUserMedia({audio:{echoCancellation:false, noiseSuppression:false, autoGainControl:false}})` — those DSP features are **off on purpose**; AGC/NS would flatten exactly the tonal spike we key on. - Bin↔Hz: `freq = bin · sampleRate / fftSize`; invert for the window edges. - **Calibration** ("ring your bell once in 4 s"): track the loudest bin while ringing, then set the window to `[peakHz−400, peakHz+600]` and threshold to `peak−20`. Centres detection on *this* bell instead of the generic default. - Alerts: synth beep (880→660 Hz) + `navigator.vibrate([120,60,120])` on mobile. - **Test chime** generates a ding-dong (E6 1318 Hz → C6 1046 Hz) so detection is testable without a real bell. Limits: it's a heuristic, not a classifier. Other tonal sounds (phone ring, microwave done, TV doorbell) can false-positive. Default window 600–4000 Hz favours bright electronic chimes; deep wired bells sit lower → calibrate.
## 2. Publish recipe — a public single-file app in `guerin/apps/` This is the generalizable nugget. Shipping a file to the WebDAV origin is **not** enough to make it public — the guerin tree defaults to **private** (anonymous GET 302-redirects to `register-user.html?returnTo=…`). Public read is granted by a sidecar. ```bash TOK=$(cat .credentials/redfish-acequia-jwt.txt) BASE=https://redfish.acequia.io/guerin/apps/<app> # 1. create the collection (PUT does NOT auto-create parents) curl -X MKCOL -H "Authorization: Bearer $TOK" "$BASE/" # 201 # 2. upload the app curl -X PUT -H "Authorization: Bearer $TOK" \ -H "Content-Type: text/html; charset=utf-8" \ --data-binary @index.html "$BASE/index.html" # 201 # 3. upload the README — REQUIRED convention (see below) curl -X PUT -H "Authorization: Bearer $TOK" \ -H "Content-Type: text/markdown; charset=utf-8" \ --data-binary @README.md "$BASE/README.md" # 201 # 4. grant anonymous read — THE step that makes it public printf '{"read":"anonymous","recursive":true}' > .acequia-access.json curl -X PUT -H "Authorization: Bearer $TOK" \ -H "Content-Type: application/json" \ --data-binary @.acequia-access.json "$BASE/.acequia-access.json" # 201 ``` ### Convention: every app carries a top-level `README.md` (standing, adopted 2026-06-11) Every app published to `guerin/apps/<app>/` MUST include a `README.md` next to `index.html` that (a) describes the app and links its **live URL**, and (b) carries an `## Authored by` list of **back-references to the bead(s) that authored it** — absolute `https://` URIs to the bead root plus the specific design note and chat log. Why: the bead→app link is otherwise one-directional. The README is the **back-edge** — it makes an app self-documenting for anyone who lands on the URL directly, and closes the polarized link (value flows out via the app; attribution flows back to the bead) as one bidirectional accounting rather than a dangling forward reference. The `## Authored by` section is a **list** by design: later sessions / other agents that extend the app **append** a line rather than rewriting, so provenance accretes. The README is covered by the recursive `.acequia-access.json`, so it's publicly readable with no extra grant. Verify with an **un-authenticated** GET: expect `200 text/html`, not `302`. ```bash curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" "$BASE/" # want 200, empty redirect ``` Key facts: - The sidecar is `.acequia-access.json` = `{"read":"anonymous","recursive":true}`. `recursive:true` covers subpaths, so one sidecar at the app root publishes the whole app subtree. - **Diagnosis pattern:** when a fresh upload is gated, compare against a known-public sibling (here `apps/sphere/`) — list its dir, find the sidecar it carries, replicate. Private = *absence* of the `read:anonymous` sidecar (consistent with the acequia auth model: a private dir is one with no anonymous-read grant). - HTTPS public reachability is also what unlocks `getUserMedia` — mic access needs a secure context, which the acequia origin provides. This recipe is the publish half of the project_uri-bind-mount / namespace-served-by-mesh frame applied to a plain static app.