Skip to content

Commit 7468b30

Browse files
bfirshclaude
andcommitted
Resume AudioContext on user interaction to fix Chrome audio
Chrome and other browsers block AudioContext from starting until a user gesture occurs on the page. This caused no sound when navigating directly to a ROM page. Fix by listening for keydown/mousedown/touchstart events and calling audioCtx.resume() on the first interaction. Fixes #368 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3413b34 commit 7468b30

File tree

3 files changed

+32
-3
lines changed

3 files changed

+32
-3
lines changed

src/cpu.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,7 @@ class CPU {
346346
// zero page, then add Y. Page-crossing dummy read as in case 8.
347347
let zpAddr = this.loadDirect(opaddr + 2);
348348
addr =
349-
this.loadDirect(zpAddr) |
350-
(this.loadDirect((zpAddr + 1) & 0xff) << 8);
349+
this.loadDirect(zpAddr) | (this.loadDirect((zpAddr + 1) & 0xff) << 8);
351350
baseHigh = (addr >> 8) & 0xff;
352351
if ((addr & 0xff00) !== ((addr + this.REG_Y) & 0xff00)) {
353352
this.load((addr & 0xff00) | ((addr + this.REG_Y) & 0xff));

src/nes.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,12 @@ class NES {
129129
// Must go dot-by-dot near VBlank set (scanline 0, dot 1), VBlank
130130
// clear (scanline 20, dot 1), sprite 0 hit, or scanline boundaries.
131131
if (
132-
!(ppu.scanline === 0 && ppu.vblankPending && ppu.curX <= 1 && finalCurX > 1) &&
132+
!(
133+
ppu.scanline === 0 &&
134+
ppu.vblankPending &&
135+
ppu.curX <= 1 &&
136+
finalCurX > 1
137+
) &&
133138
!(ppu.scanline === 20 && ppu.curX <= 1 && finalCurX > 1) &&
134139
finalCurX < 341 &&
135140
(ppu.spr0HitX < ppu.curX || ppu.spr0HitX >= finalCurX)

web/src/Speakers.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,34 @@ export default class Speakers {
2828
this.scriptNode = this.audioCtx.createScriptProcessor(1024, 0, 2);
2929
this.scriptNode.onaudioprocess = this.onaudioprocess;
3030
this.scriptNode.connect(this.audioCtx.destination);
31+
32+
// Chrome and other browsers require a user gesture before AudioContext can
33+
// start. If suspended, resume on the first user interaction.
34+
// See https://github.com/bfirsh/jsnes/issues/368
35+
if (this.audioCtx.state === "suspended") {
36+
this._resumeOnInteraction = () => {
37+
if (this.audioCtx) {
38+
this.audioCtx.resume();
39+
}
40+
this._removeResumeListeners();
41+
};
42+
document.addEventListener("keydown", this._resumeOnInteraction);
43+
document.addEventListener("mousedown", this._resumeOnInteraction);
44+
document.addEventListener("touchstart", this._resumeOnInteraction);
45+
}
46+
}
47+
48+
_removeResumeListeners() {
49+
if (this._resumeOnInteraction) {
50+
document.removeEventListener("keydown", this._resumeOnInteraction);
51+
document.removeEventListener("mousedown", this._resumeOnInteraction);
52+
document.removeEventListener("touchstart", this._resumeOnInteraction);
53+
this._resumeOnInteraction = null;
54+
}
3155
}
3256

3357
stop() {
58+
this._removeResumeListeners();
3459
if (this.scriptNode) {
3560
this.scriptNode.disconnect(this.audioCtx.destination);
3661
this.scriptNode.onaudioprocess = null;

0 commit comments

Comments
 (0)