Skip to content

Commit 08abb53

Browse files
authored
Merge pull request #15 from idsinge/webcomponent
feat: rebuild demo as responsive grid with audio info table and AudioWorklet panel
2 parents cc31254 + 5c5a41f commit 08abb53

10 files changed

Lines changed: 496 additions & 129 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ docs/.vitepress/cache
66
docs/.vitepress/.temp
77
.agents
88
.claude
9+
.opencode

CLAUDE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ tests/
9090
worker.test.js — unit tests for calculateCrossCorrelation() and findPeakAndMean() (node:test)
9191
```
9292

93+
```
94+
demo/
95+
index.html — Public integration showcase: loads dist/latency-test.iife.js; grid of 8 demo panels
96+
style.css — Demo-only styles: card grid, panels, result boxes, event log, audio info table
97+
js/
98+
common.js — Shared setup: getUserMedia + AudioContext (created once), card-grid toggle, audio info
99+
table (AC properties, track settings/constraints/capabilities, stream info, browser),
100+
activity indicator, startTest() helper, resultHTML() formatter
101+
minimal.js — Panel: Minimal — single run, simplest integration
102+
multi-run.js — Panel: Multi-Run — configurable run count with mean/std/min/max aggregates
103+
context-share.js — Panel: Context Share — demonstrates host-managed AudioContext & MediaStream pattern
104+
mode-toggle.js — Panel: Mode Toggle — runs MediaRecorder then AudioWorklet sequentially for A/B comparison
105+
audioworklet.js — Panel: AudioWorklet — recording-mode="audioworklet", multi-run, aggregate stats
106+
lifecycle.js — Panel: Lifecycle — logs all six latency-* events with timestamps
107+
debug.js — Panel: Debug Mode — intercepts console.debug to surface [latency-test] lines on-page
108+
host-gain.js — Panel: Host Gain — ChannelSplitter + GainNode chain for low-level mics (e.g. Safari)
109+
```
110+
93111
**Deleted files (no longer in repo):**
94112
- `src/style.css` — removed
95113
- `src/scripts/helper.js` — removed (contained all canvas drawing: waveform, cross-correlation, histogram)

demo/index.html

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<body>
1111

1212
<header>
13-
<a href="#panel-minimal" class="skip-link">Skip to demo</a>
13+
<a href="#card-grid" class="skip-link">Skip to demos</a>
1414
<h1>@adasp/latency-test · Demo</h1>
1515
<p>Validates the built IIFE bundle — load component via <code>&lt;script&gt;</code> tag</p>
1616
<span id="activity-indicator" class="activity-idle"></span>
@@ -22,20 +22,59 @@ <h1>@adasp/latency-test · Demo</h1>
2222
</div>
2323

2424
<div id="demo-section" hidden>
25-
<div class="tablist" role="tablist" aria-label="Demo patterns">
26-
<button role="tab" id="tab-minimal" aria-selected="true" aria-controls="panel-minimal" tabindex="0">Minimal</button>
27-
<button role="tab" id="tab-multi" aria-selected="false" aria-controls="panel-multi" tabindex="-1">Multi-Run</button>
28-
<button role="tab" id="tab-context" aria-selected="false" aria-controls="panel-context" tabindex="-1">Context Share</button>
29-
<button role="tab" id="tab-mode" aria-selected="false" aria-controls="panel-mode" tabindex="-1">Mode Toggle</button>
30-
<button role="tab" id="tab-lifecycle" aria-selected="false" aria-controls="panel-lifecycle" tabindex="-1">Lifecycle</button>
31-
<button role="tab" id="tab-debug" aria-selected="false" aria-controls="panel-debug" tabindex="-1">Debug Mode</button>
32-
<button role="tab" id="tab-gain" aria-selected="false" aria-controls="panel-gain" tabindex="-1">Host Gain</button>
25+
26+
<details id="audio-info" hidden>
27+
<summary>Audio Session Info</summary>
28+
<table>
29+
<thead class="sr-only">
30+
<tr>
31+
<th scope="col">Property</th>
32+
<th scope="col">Value</th>
33+
</tr>
34+
</thead>
35+
<tbody></tbody>
36+
</table>
37+
</details>
38+
39+
<div id="card-grid" class="card-grid">
40+
<button class="demo-card" data-panel="panel-minimal" aria-controls="panel-minimal" aria-expanded="false">
41+
<span class="card-title">Minimal</span>
42+
<span class="card-desc">Single test run, simplest integration</span>
43+
</button>
44+
<button class="demo-card" data-panel="panel-multi" aria-controls="panel-multi" aria-expanded="false">
45+
<span class="card-title">Multi-Run</span>
46+
<span class="card-desc">Multiple runs with mean, std, min, max aggregates</span>
47+
</button>
48+
<button class="demo-card" data-panel="panel-context" aria-controls="panel-context" aria-expanded="false">
49+
<span class="card-title">Context Share</span>
50+
<span class="card-desc">Host-managed AudioContext and MediaStream</span>
51+
</button>
52+
<button class="demo-card" data-panel="panel-mode" aria-controls="panel-mode" aria-expanded="false">
53+
<span class="card-title">Mode Toggle</span>
54+
<span class="card-desc">MediaRecorder vs AudioWorklet A/B comparison</span>
55+
</button>
56+
<button class="demo-card" data-panel="panel-audioworklet" aria-controls="panel-audioworklet" aria-expanded="false">
57+
<span class="card-title">AudioWorklet</span>
58+
<span class="card-desc">Raw Float32 PCM capture — v2 default backend</span>
59+
</button>
60+
<button class="demo-card" data-panel="panel-lifecycle" aria-controls="panel-lifecycle" aria-expanded="false">
61+
<span class="card-title">Lifecycle</span>
62+
<span class="card-desc">All six lifecycle events logged in real-time</span>
63+
</button>
64+
<button class="demo-card" data-panel="panel-debug" aria-controls="panel-debug" aria-expanded="false">
65+
<span class="card-title">Debug Mode</span>
66+
<span class="card-desc">Internal checkpoint logging for development</span>
67+
</button>
68+
<button class="demo-card" data-panel="panel-gain" aria-controls="panel-gain" aria-expanded="false">
69+
<span class="card-title">Host Gain</span>
70+
<span class="card-desc">GainNode chain for low mic levels (e.g. Safari)</span>
71+
</button>
3372
</div>
3473

35-
<main>
74+
<div id="demo-panel-area">
3675

37-
<!-- ─── Tab: Minimal ─── -->
38-
<div id="panel-minimal" class="tabpanel" role="tabpanel" aria-labelledby="tab-minimal" aria-hidden="false">
76+
<!-- ─── Panel: Minimal ─── -->
77+
<div id="panel-minimal" class="demo-panel" hidden>
3978
<h2>Minimal Integration</h2>
4079
<p class="desc">Single test run using the <code>mediarecorder</code> backend. The simplest way to validate the component works.</p>
4180
<div class="controls">
@@ -66,8 +105,8 @@ <h2>Minimal Integration</h2>
66105
</details>
67106
</div>
68107

69-
<!-- ─── Tab: Multi-Run ─── -->
70-
<div id="panel-multi" class="tabpanel" role="tabpanel" aria-labelledby="tab-multi" aria-hidden="true">
108+
<!-- ─── Panel: Multi-Run ─── -->
109+
<div id="panel-multi" class="demo-panel" hidden>
71110
<h2>Multi-Run with Aggregates</h2>
72111
<p class="desc">Configure the number of test runs. Results are accumulated and displayed with mean, standard deviation, min, and max.</p>
73112
<div class="controls">
@@ -118,8 +157,8 @@ <h2>Multi-Run with Aggregates</h2>
118157
</details>
119158
</div>
120159

121-
<!-- ─── Tab: Context Share ─── -->
122-
<div id="panel-context" class="tabpanel" role="tabpanel" aria-labelledby="tab-context" aria-hidden="true">
160+
<!-- ─── Panel: Context Share ─── -->
161+
<div id="panel-context" class="demo-panel" hidden>
123162
<h2>Host-Managed AudioContext &amp; Stream</h2>
124163
<p class="desc">The host creates its own <code>AudioContext</code> and <code>MediaStream</code>, then passes them via properties. The component never closes or stops what it didn't create.</p>
125164
<div class="controls">
@@ -155,8 +194,8 @@ <h2>Host-Managed AudioContext &amp; Stream</h2>
155194
</details>
156195
</div>
157196

158-
<!-- ─── Tab: Mode Toggle ─── -->
159-
<div id="panel-mode" class="tabpanel" role="tabpanel" aria-labelledby="tab-mode" aria-hidden="true">
197+
<!-- ─── Panel: Mode Toggle ─── -->
198+
<div id="panel-mode" class="demo-panel" hidden>
160199
<h2>Capture Backend Comparison</h2>
161200
<p class="desc">Run <code>MediaRecorder</code> and <code>AudioWorklet</code> back-to-back on the same source stream for direct A/B comparison.</p>
162201
<div class="controls">
@@ -208,8 +247,50 @@ <h3>AudioWorklet</h3>
208247
</details>
209248
</div>
210249

211-
<!-- ─── Tab: Lifecycle ─── -->
212-
<div id="panel-lifecycle" class="tabpanel" role="tabpanel" aria-labelledby="tab-lifecycle" aria-hidden="true">
250+
<!-- ─── Panel: AudioWorklet ─── -->
251+
<div id="panel-audioworklet" class="demo-panel" hidden>
252+
<h2>AudioWorklet Backend</h2>
253+
<p class="desc">Captures raw Float32 PCM directly in an <code>AudioWorkletProcessor</code> — no codec round-trip, sample-accurate timing. This is the v2 default backend. Use when you need the most accurate round-trip measurement.</p>
254+
<div class="controls">
255+
<label>Runs:
256+
<input type="number" id="aw-count" value="5" min="1" max="50">
257+
</label>
258+
<button class="btn btn-primary" id="aw-start">Start</button>
259+
<button class="btn btn-secondary" id="aw-stop" disabled>Stop</button>
260+
</div>
261+
<div id="aw-current" class="result-box" style="display:none">
262+
<div class="latency" id="aw-current-latency"></div>
263+
<div class="detail" id="aw-current-detail"></div>
264+
</div>
265+
<div id="aw-aggregate" class="result-box aggregate" style="display:none">
266+
<div class="stat" id="aw-mean">Mean: —</div>
267+
<div class="stat" id="aw-stats">SD (Standard Deviation): — · Min: — · Max: —</div>
268+
</div>
269+
<latency-test id="aw-tester" recording-mode="audioworklet"></latency-test>
270+
<details class="config-snippet">
271+
<summary>Configuration</summary>
272+
<pre><code>&lt;latency-test id="aw-tester" recording-mode="audioworklet"&gt;&lt;/latency-test&gt;
273+
274+
&lt;script&gt;
275+
const tester = document.querySelector('#aw-tester')
276+
tester.inputStream = hostStream // host-managed MediaStream
277+
tester.audioContext = hostAudioContext // host-managed AudioContext
278+
tester.numberOfTests = 5
279+
280+
tester.addEventListener('latency-result', e =&gt; {
281+
console.log(`${e.detail.latency.toFixed(2)} ms · ${e.detail.reliable ? 'reliable' : 'unreliable'}`)
282+
})
283+
tester.addEventListener('latency-complete', e =&gt; {
284+
console.log(`AW mean: ${e.detail.mean.toFixed(2)} ms`)
285+
})
286+
287+
tester.start()
288+
&lt;/script&gt;</code></pre>
289+
</details>
290+
</div>
291+
292+
<!-- ─── Panel: Lifecycle ─── -->
293+
<div id="panel-lifecycle" class="demo-panel" hidden>
213294
<h2>Lifecycle Event Log</h2>
214295
<p class="desc">All six lifecycle events are logged in real-time with timestamps.</p>
215296
<div class="controls">
@@ -243,8 +324,8 @@ <h2>Lifecycle Event Log</h2>
243324
</details>
244325
</div>
245326

246-
<!-- ─── Tab: Debug Mode ─── -->
247-
<div id="panel-debug" class="tabpanel" role="tabpanel" aria-labelledby="tab-debug" aria-hidden="true">
327+
<!-- ─── Panel: Debug Mode ─── -->
328+
<div id="panel-debug" class="demo-panel" hidden>
248329
<h2>Debug Mode</h2>
249330
<div class="callout-warn">
250331
<strong>Development only.</strong> Debug mode enables <code>console.debug('[latency-test]', …)</code> logging at internal checkpoints. Having debug on — or DevTools open — can perturb browser scheduling and affect latency accuracy. Do not use for measurements you intend to record.
@@ -296,12 +377,12 @@ <h2>Debug Mode</h2>
296377
</details>
297378
</div>
298379

299-
<!-- ─── Tab: Host Gain ─── -->
300-
<div id="panel-gain" class="tabpanel" role="tabpanel" aria-labelledby="tab-gain" aria-hidden="true">
380+
<!-- ─── Panel: Host Gain ─── -->
381+
<div id="panel-gain" class="demo-panel" hidden>
301382
<h2>Host-Controlled Input Gain</h2>
302383
<p class="desc">The host builds a <code>GainNode</code> chain and passes the processed stream to the component. Use this when your recording setup has low mic levels — common on Safari ≥ 16 with <code>echoCancellation: false</code> (empirical value: 50×). A <code>ChannelSplitterNode</code> isolates the left channel, which also handles wired earpods on Safari that force a stereo stream with signal only on the left.</p>
303384
<div class="callout-warn">
304-
This tab measures a different pipeline than direct-stream mode (an extra browser-managed Web Audio / MediaStream bridge). Results are self-consistent but should not be compared directly to other tabs.
385+
This panel measures a different pipeline than direct-stream mode (an extra browser-managed Web Audio / MediaStream bridge). Results are self-consistent but should not be compared directly to other panels.
305386
</div>
306387
<div class="controls">
307388
<label><input type="checkbox" id="gain-chain-toggle" checked> Use gain chain</label>
@@ -346,14 +427,16 @@ <h2>Host-Controlled Input Gain</h2>
346427
</details>
347428
</div>
348429

349-
</main>
430+
</div><!-- #demo-panel-area -->
431+
350432
</div><!-- #demo-section -->
351433

352434
<script src="js/common.js"></script>
353435
<script src="js/minimal.js"></script>
354436
<script src="js/multi-run.js"></script>
355437
<script src="js/context-share.js"></script>
356438
<script src="js/mode-toggle.js"></script>
439+
<script src="js/audioworklet.js"></script>
357440
<script src="js/lifecycle.js"></script>
358441
<script src="js/debug.js"></script>
359442
<script src="js/host-gain.js"></script>

demo/js/audioworklet.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
;(function () {
2+
'use strict'
3+
4+
const tester = document.getElementById('aw-tester')
5+
const startBtn = document.getElementById('aw-start')
6+
const stopBtn = document.getElementById('aw-stop')
7+
const countInput = document.getElementById('aw-count')
8+
const currentBox = document.getElementById('aw-current')
9+
const currentLatency = document.getElementById('aw-current-latency')
10+
const currentDetail = document.getElementById('aw-current-detail')
11+
const aggregateBox = document.getElementById('aw-aggregate')
12+
const meanEl = document.getElementById('aw-mean')
13+
const statsEl = document.getElementById('aw-stats')
14+
15+
let runNum = 0
16+
17+
tester.addEventListener('latency-recording', () => {
18+
startBtn.disabled = true
19+
stopBtn.disabled = false
20+
currentBox.style.display = 'block'
21+
currentDetail.textContent = 'Recording…'
22+
})
23+
tester.addEventListener('latency-processing', () => {
24+
currentDetail.textContent = 'Processing…'
25+
})
26+
tester.addEventListener('latency-result', e => {
27+
runNum++
28+
currentLatency.textContent = `${e.detail.latency.toFixed(2)} ms`
29+
currentDetail.textContent = `Run ${runNum} · Ratio: ${e.detail.ratio.toFixed(2)} dB · ${e.detail.reliable ? '✅' : '⚠️'}`
30+
})
31+
tester.addEventListener('latency-complete', e => {
32+
startBtn.disabled = false
33+
stopBtn.disabled = true
34+
if (e.detail.results.length > 0) {
35+
aggregateBox.style.display = 'block'
36+
meanEl.textContent = `Mean: ${e.detail.mean.toFixed(2)} ms (${e.detail.results.length} runs)`
37+
statsEl.textContent = `SD (Standard Deviation): ${e.detail.std.toFixed(2)} · Min: ${e.detail.min.toFixed(2)} · Max: ${e.detail.max.toFixed(2)}${e.detail.aborted ? ' ⚠️ Aborted' : ''}`
38+
}
39+
})
40+
tester.addEventListener('latency-error', e => {
41+
startBtn.disabled = false
42+
stopBtn.disabled = true
43+
currentDetail.style.color = '#c62828'
44+
currentDetail.textContent = e.detail.message
45+
})
46+
47+
startBtn.addEventListener('click', async () => {
48+
startBtn.disabled = true
49+
runNum = 0
50+
aggregateBox.style.display = 'none'
51+
currentBox.style.display = 'none'
52+
tester.numberOfTests = countInput.value
53+
await window.startTest(tester)
54+
})
55+
56+
stopBtn.addEventListener('click', () => {
57+
tester.stop()
58+
startBtn.disabled = false
59+
stopBtn.disabled = true
60+
})
61+
})()

0 commit comments

Comments
 (0)