Web Audio playback with per-challenge gain and limiter
3 changed files+19−3
web/build.pymodified+11−1View file
@@ -29,9 +29,19 @@ def main():
2929 sol["fields"]["path"] = f"{rel_dir.as_posix()}/{sol['fields']['path']}"
3030 entry["submissions"].append(sub)
3131 shutil.copytree(ROOT / "solutions", out / "solutions")
32- (out / "manifest.json").write_text(json.dumps(manifest, indent=1))
3332 shutil.copyfile(ROOT / "web" / "index.html", out / "index.html")
3433 shutil.copyfile(ROOT / "web" / "viewer.js", out / "viewer.js")
34+ # the shared Web Audio player, from the installed challenge-tools package
35+ from challenge_tools.build import player_js
36+
37+ (out / "player.js").write_text(player_js())
38+ # suggested playback gains, when the challenge repository is checked out beside us (as in the workflow)
39+ gains = {}
40+ for cj in sorted((ROOT / "challenge-repo" / "challenges").glob("*/challenge.json")) if (ROOT / "challenge-repo").exists() else []:
41+ ch = json.loads(cj.read_text())
42+ gains[ch["challenge_id"]] = (ch.get("playback") or {}).get("gain_db")
43+ manifest["playback_gain_db"] = gains
44+ (out / "manifest.json").write_text(json.dumps(manifest, indent=1))
3545 print(f"site written to {out}")
3646
3747
web/index.htmlmodified+2−1View file
@@ -15,7 +15,6 @@ th, td { border-bottom: 1px solid var(--line); padding: 6px 8px; text-align: lef
1515 td.num { text-align: right; font-variant-numeric: tabular-nums; }
1616 .muted { color: var(--muted); }
1717 .wrap { overflow-x: auto; }
18-audio { width: 180px; height: 28px; }
1918 canvas { width: 100%; height: 260px; display: block; border: 1px solid var(--line); border-radius: 4px; }
2019 .controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin: 8px 0; }
2120 .controls label { display: flex; gap: 6px; align-items: center; }
@@ -38,8 +37,10 @@ button { font: inherit; padding: 4px 10px; }
3837 <p id="fieldStatus" class="muted">Loading…</p>
3938
4039 <h2>Submissions</h2>
40+<div id="playback"></div>
4141 <div id="submissions"></div>
4242 </main>
43+<script src="player.js"></script>
4344 <script type="module" src="viewer.js"></script>
4445 </body>
4546 </html>
web/viewer.jsmodified+6−1View file
@@ -19,7 +19,7 @@ for (const [cid, ch] of Object.entries(manifest.challenges)) {
1919 if (s.fields) fieldSolutions.push({ label: `${sub.submission_id}/${s.solution_id}`, path: s.fields.path });
2020 return `<tr><td>${esc(s.solution_id)}</td><td>${esc(s.method || "")}<br><span class="muted"><code>${esc(JSON.stringify(s.parameters || {}))}</code></span></td>` +
2121 `<td class="num">${s.timing_seconds ?? ""}</td><td class="muted">${esc((s.more_accurate_than || []).join(", "))}</td>` +
22- `<td><audio controls preload="none" src="${esc(s.output)}"></audio><br><a href="${esc(s.output)}">wav</a>${s.fields ? ` <a href="${esc(s.fields.path)}">fields</a>` : ""}</td></tr>`;
22+ `<td><div class="cplayer" data-src="${esc(s.output)}"></div><br><a href="${esc(s.output)}">wav</a>${s.fields ? ` <a href="${esc(s.fields.path)}">fields</a>` : ""}</td></tr>`;
2323 }).join("");
2424 const wrap = document.createElement("div");
2525 wrap.className = "wrap";
@@ -28,6 +28,11 @@ for (const [cid, ch] of Object.entries(manifest.challenges)) {
2828 }
2929 }
3030
31+const gains = manifest.playback_gain_db || {};
32+const firstGain = Object.values(gains).find((g) => typeof g === "number");
33+ChallengePlayer.controls(document.getElementById("playback"), { gainDb: typeof firstGain === "number" ? firstGain : 20, limiter: true });
34+ChallengePlayer.mount();
35+
3136 // ---------------------------------------------------------------- field viewer
3237 const select = document.getElementById("fieldSelect");
3338 const status = document.getElementById("fieldStatus");