Skip to content

Commit 72f1a2c

Browse files
authored
Merge pull request #2928 from Brain-up/fix/background-noise-on-refresh
Fix background noise not playing after a page refresh
2 parents 836a854 + 5f97535 commit 72f1a2c

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

frontend/app/services/audio.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ export default class AudioService extends Service {
355355

356356
startNoiseTask = task(async () => {
357357
let noise = null;
358+
let started = false;
358359
const timeInSeconds = 10;
359360
try {
360361
const [level, url] = [
@@ -365,7 +366,16 @@ export default class AudioService extends Service {
365366
return;
366367
}
367368
noise = await this.getNoise(timeInSeconds, level, url);
369+
// Mirror the word-playback path: a fresh AudioContext (e.g. right after a
370+
// page refresh) starts suspended under the browser autoplay policy, and
371+
// source.start(0) on a suspended context queues silently. Resume before
372+
// starting so background noise plays on first load — not only after a
373+
// lesson restart, which happened to reuse an already-resumed context.
374+
if (this.context && this.context.state === 'suspended' && !isTesting()) {
375+
await this.context.resume();
376+
}
368377
noise.source.start(0);
378+
started = true;
369379
this.noiseNode = noise;
370380
if (url) {
371381
await timeout(toMilliseconds(6000));
@@ -374,7 +384,11 @@ export default class AudioService extends Service {
374384
this.startNoise();
375385
}
376386
} finally {
377-
if (noise) {
387+
// Only stop a source that actually started. The context.resume() above
388+
// adds an await between creating and starting the source, so a cancel
389+
// (e.g. stopNoise) or a resume rejection in that window would otherwise
390+
// call stop() on a never-started node and throw InvalidStateError.
391+
if (noise && started) {
378392
noise.source.stop();
379393
}
380394
}

frontend/tests/unit/services/audio-test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,78 @@ module('Unit | Service | audio', function (hooks) {
287287
);
288288
});
289289
});
290+
291+
module('startNoiseTask', function () {
292+
test('does not create or start noise when the exercise has no noise level', async function (assert) {
293+
class TestAudioService extends AudioService {
294+
get currentExerciseNoiseLevel() {
295+
return 0;
296+
}
297+
get currentExerciseNoiseUrl() {
298+
return null;
299+
}
300+
async getNoise() {
301+
assert.step('getNoise');
302+
return { source: { start() {}, stop() {} }, gainNode: {} };
303+
}
304+
}
305+
this.owner.register('service:audio', TestAudioService);
306+
const service = this.owner.lookup('service:audio');
307+
308+
await service.startNoiseTask.perform();
309+
310+
assert.verifySteps([], 'no noise is created when the level is 0');
311+
});
312+
313+
test('creates and starts the noise source when the exercise has a noise level', async function (assert) {
314+
class TestAudioService extends AudioService {
315+
get currentExerciseNoiseLevel() {
316+
return 50;
317+
}
318+
get currentExerciseNoiseUrl() {
319+
return 'http://example.com/noise.mp3';
320+
}
321+
async getNoise() {
322+
assert.step('getNoise');
323+
return {
324+
source: {
325+
start() {
326+
assert.step('start');
327+
},
328+
stop() {
329+
assert.step('stop');
330+
},
331+
},
332+
gainNode: {},
333+
};
334+
}
335+
}
336+
this.owner.register('service:audio', TestAudioService);
337+
const service = this.owner.lookup('service:audio');
338+
// A running context is the post-restart case; the suspended-context
339+
// resume is production-only (guarded by !isTesting()). close() is needed
340+
// because the service's willDestroy() closes the context on teardown.
341+
service.context = {
342+
state: 'running',
343+
resume: () => Promise.resolve(),
344+
close: () => Promise.resolve(),
345+
};
346+
347+
const instance = service.startNoiseTask.perform();
348+
// The task loops on timeout(6000) after starting; cancel once started.
349+
await new Promise((resolve) => setTimeout(resolve, 20));
350+
instance.cancel();
351+
try {
352+
await instance;
353+
} catch (_e) {
354+
// cancellation is expected
355+
}
356+
357+
// Ordered: the source is created, then started, then stopped on cancel.
358+
assert.verifySteps(
359+
['getNoise', 'start', 'stop'],
360+
'noise is created before it is started, and stopped on cancellation',
361+
);
362+
});
363+
});
290364
});

0 commit comments

Comments
 (0)