diff --git a/packages/blockly/core/workspace_audio.ts b/packages/blockly/core/workspace_audio.ts index 46f18ce6108..7d27277a0ce 100644 --- a/packages/blockly/core/workspace_audio.ts +++ b/packages/blockly/core/workspace_audio.ts @@ -83,24 +83,10 @@ export class WorkspaceAudio { * @param opt_volume Volume of sound (0-1). */ async play(name: string, opt_volume?: number) { - if (this.muted || opt_volume === 0 || !this.context) { - return; - } + if (!this.isPlayingAllowed() || opt_volume === 0) return; const sound = this.sounds.get(name); if (sound) { - // Don't play one sound on top of another. - const now = new Date(); - if ( - this.lastSound !== null && - now.getTime() - this.lastSound.getTime() < SOUND_LIMIT - ) { - return; - } - this.lastSound = now; - - if (this.context.state === 'suspended') { - await this.context.resume(); - } + await this.prepareToPlay(); const source = this.context.createBufferSource(); const gainNode = this.context.createGain(); @@ -121,6 +107,73 @@ export class WorkspaceAudio { } } + /** + * Plays a beep at the given frequency. + * + * @param tone The frequency of the beep to play, in hertz. + * @param duration The duration of the beep, in seconds. Defaults to 0.2. + */ + async beep(tone: number, duration = 0.2) { + if (!this.isPlayingAllowed()) return; + await this.prepareToPlay(); + + const oscillator = this.context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(tone, this.context.currentTime); + + const gainNode = this.context.createGain(); + gainNode.gain.setValueAtTime(0, this.context.currentTime); + // Fade in + gainNode.gain.linearRampToValueAtTime(0.5, this.context.currentTime + 0.01); + // Fade out + gainNode.gain.linearRampToValueAtTime( + 0, + this.context.currentTime + duration, + ); + + oscillator.connect(gainNode); + gainNode.connect(this.context.destination); + + oscillator.start(this.context.currentTime); + oscillator.stop(this.context.currentTime + duration); + } + + /** + * Returns whether or not playing sounds is currently allowed. + * + * @returns False if audio is muted or a sound has just been played, otherwise + * true. + */ + private isPlayingAllowed( + this: WorkspaceAudio, + ): this is WorkspaceAudio & Required<{context: AudioContext}> { + const now = new Date(); + + if ( + this.getMuted() || + !this.context || + (this.lastSound !== null && + now.getTime() - this.lastSound.getTime() < SOUND_LIMIT) + ) { + return false; + } + return true; + } + + /** + * Prepares to play audio by recording the time of the last play and resuming + * the audio context. + */ + private async prepareToPlay( + this: WorkspaceAudio & Required<{context: AudioContext}>, + ) { + this.lastSound = new Date(); + + if (this.context.state === 'suspended') { + await this.context.resume(); + } + } + /** * @param muted If true, mute sounds. Otherwise, play them. */