Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Do not re-open these unless the user explicitly asks:
- Lifecycle events emitted: `latency-start`, `latency-recording`, `latency-processing`,
`latency-result`, `latency-error`, `latency-complete`.
All events must set `bubbles: true` and `composed: true`.
- Safari gain: host-controlled via `input-gain` attribute. No internal browser detection.
- Safari gain: host-controlled via the host-gain pattern (gain chain built by the host, processed stream passed as `inputStream`). No internal browser detection, no gain attribute on the component.
- Distribution: npm + CDN are both first-class targets. Validate both before publishing.
- Measurement inherits the host's audio environment: the component does not create an
idealized setup. The host's `AudioContext`, stream constraints, and capture backend
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Removed

- The `input-gain` attribute and `inputGain` property. They were documented and typed but never wired to a `GainNode` — setting them had no effect in any released version. Input gain is permanently a host responsibility: build a gain chain and pass the processed stream as `inputStream` (see [docs/examples/host-gain.md](docs/examples/host-gain.md)). Consumer impact: TypeScript code referencing `inputGain` will no longer compile, and `'input-gain'` is no longer listed in `LatencyTest.observedAttributes` — runtime behavior is otherwise unchanged. Policy note: this project treats removal of documented-but-never-functional API as a minor release; strict semver would classify any public API removal as major.

## [1.1.0] - 2026-06-05

### Changed

- Default `recording-mode="mediarecorder"` upgraded to 2-channel capture (`ChannelMergerNode` + `MediaStreamDestinationNode`), removing start-timing bias; emits `latency-error` if the browser downmixes to mono.

### Added

- `recording-mode="mediarecorder-1ch"` single-channel fallback mode (direct mic stream, start-timing bias present).

## [1.0.2] - 2026-06-03

### Fixed

- Web component, build, and error-handling fixes plus documentation accuracy corrections from the Phases 1–3a review; CI workflow added (test, build, docs, pack verification).

## [1.0.1] - 2026-06-02

### Fixed

- Documentation accuracy and hardening patch; removed a duplicate `inputGain` declaration.

## [1.0.0] - 2026-06-02

### Added

- Initial release: headless `<latency-test>` Web Component measuring browser round-trip audio latency via MLS and cross-correlation, with `mediarecorder` and `audioworklet` recording modes, ESM/IIFE bundles, TypeScript declarations, VitePress documentation, and live demo.
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
**Dev commands:**
```
npm test # run unit tests (tests/mls.test.js + tests/worker.test.js)
npm run typecheck # tsc --noEmit — validates src/index.d.ts and JS against the types
npm run dev # static file server — serves src/ natively
npm run demo # static file server — serves repo root (demo/ needs built dist/)
npm run build:component # produces dist/latency-test.esm.js + .iife.js
npm run build:component:dev # unminified build for debugging
npm run build:component:legacy # legacy build → dist/latency-test.legacy.{esm,iife}.js (Safari 14 / Chrome 78)
npm run build:component:all # modern + legacy builds (also run by prepublishOnly)
npm run docs:dev # VitePress docs dev server (http://localhost:5173)
npm run docs:build # build VitePress docs
npm run docs:preview # preview built docs locally
Expand Down Expand Up @@ -186,7 +190,7 @@ demo/
| MLS order (nbits) | 15 | Sequence length = 2^15 − 1 = 32767 samples |
| maxLag | 0.600 × sampleRate | 600 ms search window for the correlation peak |
| Reliability threshold | 18 dB | `10 × log10(peakPow / meanEnergy)` |
| Safari gain boost | 50× | Was applied automatically when Safari > v16 and echoCancellation is disabled — now host-controlled via `input-gain` attribute; `getCorrectStreamForSafari()` removed in Phase 1 |
| Safari gain boost | 50× | Was applied automatically when Safari > v16 and echoCancellation is disabled — now host-controlled via the host-gain pattern (see `docs/examples/host-gain.md`); `getCorrectStreamForSafari()` removed in Phase 1 |
| AudioContext latencyHint | 0 | Requests minimum latency |
| Mic constraints | echoCancellation: false, noiseSuppression: false, autoGainControl: false, channelCount: 1 | Essential for accurate measurement |

Expand All @@ -213,7 +217,7 @@ Results are dispatched as `CustomEvent` from the element. The demo page renders

Phases 1–3b are complete. Previous design issues are resolved. Remaining known limitations:

1. **`input-gain` not yet wired** — The attribute is observed and the property is settable, but no `GainNode` is created. Setting `input-gain` has no effect in the current code. Use the host-gain pattern instead (see `docs/examples/host-gain.md`). Deferred to v2.
1. **`input-gain` removed** — The attribute and `inputGain` property were removed (they were never wired to a GainNode). Input gain is permanently a host responsibility: build a gain chain and pass the processed stream as `inputStream` — see `docs/examples/host-gain.md`. Do not reintroduce the attribute.

2. **`signal-type` not yet wired** — Only `"mls"` is implemented. The attribute is observed but `signalType` is never read by `LatencyTestController`. Deferred to v2.

Expand Down Expand Up @@ -244,7 +248,6 @@ Phases 1–3b are complete. The `<latency-test>` Custom Element is implemented w
|---|---|---|
| `recording-mode` | `"mediarecorder"` \| `"mediarecorder-1ch"` \| `"audioworklet"` | Selects the capture backend. `"mediarecorder"`: dual-channel via `ChannelMergerNode` + `MediaStreamDestinationNode`, no start-timing bias, v1 default (implemented). `"mediarecorder-1ch"`: single-channel, direct mic stream, start-timing bias present; use as fallback when browser downmixes stereo to mono (implemented). `"audioworklet"`: raw Float32 PCM, v2 default (implemented). Each mode measures a different pipeline — see Decision #14 in agents/CLAUDE_REVIEW.md. |
| `signal-type` | `"mls"` \| `"chirp"` \| `"golay"` | Selects the test signal. `"mls"` is default. `"chirp"` is a logarithmic sine sweep. `"golay"` uses Golay complementary sequence pairs for high-SNR impulse response measurement. |
| `input-gain` | number \| `0` | Intended to apply a gain multiplier to the input stream before capture. **Not yet wired** — the attribute is observed and the property is settable, but no GainNode is created. Setting it has no effect in the current code. Deferred to v2. |
| `debug` | boolean \| `false` | Enables `console.debug('[latency-test]', ...)` logging at key internal checkpoints. Development/debugging only — no effect on measurement output. Do not use during measurements you intend to record — `startPairSpanMs` is an upper-bound diagnostic span, not a pure inter-call gap, and DevTools being open can perturb scheduling generally. Implemented. |

**External references used during design:**
Expand All @@ -259,7 +262,7 @@ Phases 1–3b are complete. The `<latency-test>` Custom Element is implemented w

- Chrome/Chromium/Edge: Standard behavior, higher latency variability. First-run latency is often higher than subsequent runs — mitigated by a silent AudioBuffer started at the top of every `prepareAudioToPlayAndRecord()` call (cwilso keepalive technique).
- Firefox: Most stable results (std dev often 0), higher absolute latency on Windows
- Safari: Some devices have low microphone levels with `echoCancellation: false`. The `input-gain` attribute is intended to address this but is not yet wired (v2 item). Wired earpods force stereo input (only left channel used).
- Safari: Some devices have low microphone levels with `echoCancellation: false`. Use the host-gain pattern to compensate (see `docs/examples/host-gain.md`). Wired earpods force stereo input (only left channel used).
- iOS: Some devices exhibit aliasing above 12 kHz on audio input, degrading MLS quality. `signal-type="chirp"` (planned, bandlimited to 1500–8000 Hz) will address this — not yet implemented.
- All browsers: require HTTPS (or localhost) for `getUserMedia`

Expand Down
48 changes: 23 additions & 25 deletions agents/CLAUDE_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This file tracks open questions and the planned action plan for converting `weblatencytest` into a reusable Web Component with AudioWorklet-based recording. LLMs should read this file alongside CLAUDE.md before starting any work.

> **Editor's note (2026-06-12):** the `input-gain` attribute / `inputGain` property was dropped entirely — it was never wired to a GainNode and will not be added in any future version. All `input-gain` mentions below (decisions, tables, checklists, d.ts samples) are superseded and kept only as historical record. Input gain is permanently a host responsibility via the host-gain pattern (`docs/examples/host-gain.md`). Current API surface: CLAUDE.md and `docs/api.md`.

---

## Decisions Made
Expand Down Expand Up @@ -118,6 +120,8 @@ Shadow DOM, open mode, with an empty shadow root attached in the constructor. No

**Resolved.**

> **Superseded (2026-06-12):** `input-gain` / `inputGain` was removed entirely and will not return. Do not set `element.inputGain` — use the host-gain pattern (`docs/examples/host-gain.md`). The two paragraphs below are historical.

The `input-gain` attribute is a general-purpose gain multiplier. **The GainNode is not yet wired** — the attribute is observed and the property is set, but no gain is applied in the current code. Wiring the GainNode is deferred to v2. No browser detection lives inside the component.

The hardcoded `getCorrectStreamForSafari()` method in `test.js` is **removed during Phase 1**. Its responsibility moves to the host: if the host knows it is running on Safari ≥ 16 with `echoCancellation: false`, it sets `element.inputGain = 50` before calling `start()`. The demo page will include a code example showing this pattern.
Expand Down Expand Up @@ -240,7 +244,7 @@ Below is the proposed sequence of migration tasks. **No file should be modified
- [x] Implement data-return strategy: MessagePort accumulation, single post on stop (plain arrays, no transferables — MLS-length captures are small enough; SharedArrayBuffer deferred — Q1 still open)
- [x] Worker.js contract unchanged — both paths use `{ command: 'correlation', data1, data2, maxLag }`
- [x] Wire the worklet into the latency test flow alongside the existing MediaRecorder path, selected via `recording-mode` attribute
- [ ] `input-gain` GainNode deferred to v2
- [x] `input-gain` dropped entirely (2026-06-12) — never wired; gain is permanently a host responsibility via the host-gain pattern (docs/examples/host-gain.md)
- [ ] `signal-type="chirp"` bandlimit deferred
- [x] Validate measurement stability across multiple runs (Chrome + Firefox — numbers match MediaRecorder path)

Expand Down Expand Up @@ -368,6 +372,7 @@ Before the first publish, verify the following fields are present in `package.js
"files": [
"dist/",
"README.md",
"CHANGELOG.md",
"LICENSE"
],
"sideEffects": true,
Expand All @@ -385,7 +390,7 @@ Key points:
- No CJS exports — Web Components are browser-only APIs. `require('@adasp/latency-test')` would have no meaningful use. The `main` field points to the ESM bundle and `module` mirrors it; `unpkg`/`jsdelivr` fields point to the IIFE for CDN consumers.
- `types` / `exports["types"]` points to the TypeScript declaration file — consumers get full IntelliSense with no manual setup.
- `unpkg` / `jsdelivr` fields point to the IIFE bundle — so `unpkg.com/@adasp/latency-test` serves the script-tag-compatible version.
- `files` controls what gets included in the published package — only the built output, README, and LICENSE. Everything else (`src/`, `docs/`, `assets/`, config files) is excluded automatically.
- `files` controls what gets included in the published package — only the built output, README, CHANGELOG, and LICENSE. Everything else (`src/`, `docs/`, `assets/`, config files) is excluded automatically. Note: npm does NOT auto-include `CHANGELOG*` — it must be listed explicitly (README and LICENSE are auto-included).
- `sideEffects: true` prevents bundler tree-shaking from removing the `customElements.define()` side effect.
- `prepublishOnly` ensures the build runs before every publish, preventing a stale `dist/` from being published.
- `publishConfig.access: "public"` is **required** for scoped packages (`@scope/name`) — without it npm defaults to private and the publish will either fail or charge for a private package.
Expand All @@ -394,6 +399,8 @@ Key points:

This file ships with the package and gives consumers typed access to the element, its properties, methods, and event payloads with no manual setup:

> **Superseded (2026-06-12):** the sample below is the original Phase 7 draft. The shipped `src/index.d.ts` differs: `inputGain` was removed entirely (never wired), and `signalType` is `'mls'` only in v1. Do not copy from this sample — `src/index.d.ts` is the canonical file.

```ts
export interface LatencyResultDetail {
latency: number
Expand Down Expand Up @@ -475,30 +482,21 @@ CLAUDE*.md
CODEX_REVIEW.md
```

#### Manual publishing (per release)

```bash
# Log in to npm (only needed once per machine)
npm login

# Build the component bundle
npm run build:component
#### Release checklist (per release — agreed 2026-06-12, supersedes the original manual-publishing steps)

# Dry run — inspect what will be published without actually publishing
npm publish --dry-run
**Ordering matters:** `.github/workflows/docs.yml` deploys GitHub Pages on every push to `main`, so the CDN pin bump and changelog stamp must not reach `main` before `npm publish` — otherwise the live docs reference a CDN version that 404s until publish.

# Publish (scoped packages require --access public on first publish)
npm publish --access public
```

For subsequent releases, bump the version in `package.json` first:

```bash
npm version patch # 1.0.0 → 1.0.1 (bug fix)
npm version minor # 1.0.0 → 1.1.0 (new feature, backwards compatible)
npm version major # 1.0.0 → 2.0.0 (breaking change — e.g. AudioWorklet default)
npm publish
```
1. **On the working branch:** add CHANGELOG.md entries under `[Unreleased]`; run verification — `npm run typecheck`, `npm test`, `npm run build:component:all`, `npm run docs:build`, `npm pack --dry-run` (inspect package contents). Commit → PR → merge to `main`. Docs still pin the previous version at this point — harmless.
2. **Locally on updated `main`:**
- Commit "release: prepare X.Y.Z docs" — stamp `[Unreleased]` → `[X.Y.Z]` with date in CHANGELOG.md and bump every CDN version pin (`docs/install.md`, `docs/index.md`, `docs/examples/vanilla-js.md`).
- `npm version patch|minor|major` — creates its own commit (package.json + package-lock.json) and the `vX.Y.Z` tag. There is no `version` lifecycle script in package.json, hence the separate prep commit.
- `npm publish` — `prepublishOnly` runs `build:component:all` automatically. **Do not push yet.** (`npm login` once per machine; `publishConfig.access: public` is already set.)
- `git push --follow-tags` — only after publish succeeds. This triggers the Pages deploy with now-valid pins.
3. **Post-publish verification:**
- `npm view @adasp/latency-test@X.Y.Z version files types exports`
- Confirm the published `dist/index.d.ts` matches `src/index.d.ts` (inspect the tarball from the registry, not the local `dist/`)
- Fetch the jsDelivr/unpkg ESM and IIFE URLs once the CDN propagates
- Create a GitHub Release on the `vX.Y.Z` tag, copying the CHANGELOG entry

#### Versioning plan

Expand Down Expand Up @@ -567,7 +565,7 @@ git push --follow-tags # pushes tag → triggers the publish workflow
- Read `CLAUDE.md` first for full architectural context before touching any file.
- The cross-correlation algorithm in `worker.js` is correct and validated against published paper results — do not modify it without explicit instruction.
- The 18 dB reliability threshold and 600 ms maxLag are research-derived constants — do not change them without asking.
- The Safari-specific `getCorrectStreamForSafari()` method is **removed in Phase 1**. Gain compensation is now a general `input-gain` property set by the host the component applies whatever value it receives and does no browser detection internally.
- The Safari-specific `getCorrectStreamForSafari()` method is **removed in Phase 1**. Gain compensation is a host responsibility via the host-gain pattern — the host builds a gain chain (`ChannelSplitter` + `GainNode` → `MediaStreamDestination`) and passes the processed stream as `inputStream`; see `docs/examples/host-gain.md`. The component applies no gain and does no browser detection internally. (The former `input-gain` attribute was removed 2026-06-12 — it was never wired; do not reintroduce it.)
- `helper.js` no longer exists — do not reference it or attempt to import from it.
- **Docs homepage expectation management (resolved):** while the package was unpublished, `docs/index.md` was required to carry a visible draft/work-in-progress notice so readers would not assume the package existed. `@adasp/latency-test` is now live on npm and the notice has been removed — this requirement no longer applies.
- **Phase 3 dual-channel capture is not optional:** A WAC 2025 peer reviewer identified that the current single-channel MediaRecorder approach has an uncontrolled timing offset between `mediaRecorder.start()` and `noiseSource.start()`. The AudioWorklet processor must use `numberOfInputs: 2` — mic on input 0, reference signal loopback on input 1 — and cross-correlate the two captures. The `naomiaro/recording-calibration` reference implements this correctly. Do not implement Phase 3 as a direct MediaRecorder-to-AudioWorklet port without adopting this two-channel architecture.
Expand Down
Loading