From a3e92f7ddd15b7a0d110c739f30890d93eb59c43 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> Date: Mon, 6 Feb 2023 18:29:23 +0000 Subject: [PATCH] Fix Safari AudioContext initialisation. (#99) Current version seem to require the user interaction to be on the stack when creating/resuming the context. Switch to a single context and resume it if it starts suspended. Add some clean-up so we can keep the single context for the lifetime of the app (across stop/start). Closes #97 --- src/board/audio/index.ts | 26 +++++++++++++++++++++----- src/board/index.ts | 10 +++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/board/audio/index.ts b/src/board/audio/index.ts index 1c46fd99..132aed91 100644 --- a/src/board/audio/index.ts +++ b/src/board/audio/index.ts @@ -27,11 +27,9 @@ export class Audio { defaultAudioCallback, speechAudioCallback, }: AudioOptions) { - this.context = new AudioContext({ - // The highest rate is the sound expression synth. - sampleRate: 44100, - }); - + if (!this.context) { + throw new Error("Context must be pre-created from a user event"); + } this.muteNode = this.context.createGain(); this.muteNode.gain.setValueAtTime( this.muted ? 0 : 1, @@ -62,6 +60,16 @@ export class Audio { ); } + async createAudioContextFromUserInteraction(): Promise { + this.context = new AudioContext({ + // The highest rate is the sound expression synth. + sampleRate: 44100, + }); + if (this.context.state === "suspended") { + return this.context.resume(); + } + } + playSoundExpression(expr: string) { const soundEffects = parseSoundEffects(replaceBuiltinSound(expr)); const onDone = () => { @@ -138,6 +146,9 @@ export class Audio { boardStopped() { this.stopOscillator(); + this.speech?.dispose(); + this.soundExpression?.dispose(); + this.default?.dispose(); } private stopOscillator() { @@ -188,4 +199,9 @@ class BufferedAudio { } source.start(startTime); } + + dispose() { + // Prevent calls into WASM when the buffer nodes finish. + this.callback = () => {}; + } } diff --git a/src/board/index.ts b/src/board/index.ts index ed53b6b3..deff7fa7 100644 --- a/src/board/index.ts +++ b/src/board/index.ts @@ -230,9 +230,10 @@ export class Board { this.initializePlayButton(); // We start stopped. this.displayStoppedState(); - this.playButton.addEventListener("click", () => - this.notifications.onRequestFlash() - ); + this.playButton.addEventListener("click", async () => { + await this.audio.createAudioContextFromUserInteraction(); + this.notifications.onRequestFlash(); + }); this.updateTranslationsInternal(); this.notifications.onReady(this.getState()); @@ -481,6 +482,9 @@ export class Board { * @returns A promise that resolves when the simulator is stopped. */ async stop(brief: boolean = false): Promise { + // Preemptively stop audio so that we don't call into WASM for more data + this.audio.boardStopped(); + if (this.panicTimeout) { clearTimeout(this.panicTimeout); this.panicTimeout = null;