/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Object in charge of loading, storing, and playing audio for a * workspace. */ 'use strict'; /** * Object in charge of loading, storing, and playing audio for a * workspace. * @class */ goog.module('Blockly.WorkspaceAudio'); /* eslint-disable-next-line no-unused-vars */ const WorkspaceSvg = goog.requireType('Blockly.WorkspaceSvg'); const {globalThis} = goog.require('Blockly.utils.global'); const internalConstants = goog.require('Blockly.internalConstants'); const userAgent = goog.require('Blockly.utils.userAgent'); /** * Class for loading, storing, and playing audio for a workspace. * @param {WorkspaceSvg} parentWorkspace The parent of the workspace * this audio object belongs to, or null. * @constructor * @alias Blockly.WorkspaceAudio */ const WorkspaceAudio = function(parentWorkspace) { /** * The parent of the workspace this object belongs to, or null. May be * checked for sounds that this object can't find. * @type {WorkspaceSvg} * @private */ this.parentWorkspace_ = parentWorkspace; /** * Database of pre-loaded sounds. * @private */ this.SOUNDS_ = Object.create(null); }; /** * Time that the last sound was played. * @type {Date} * @private */ WorkspaceAudio.prototype.lastSound_ = null; /** * Dispose of this audio manager. * @package */ WorkspaceAudio.prototype.dispose = function() { this.parentWorkspace_ = null; this.SOUNDS_ = null; }; /** * Load an audio file. Cache it, ready for instantaneous playing. * @param {!Array} filenames List of file types in decreasing order of * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav'] * Filenames include path from Blockly's root. File extensions matter. * @param {string} name Name of sound. */ WorkspaceAudio.prototype.load = function(filenames, name) { if (!filenames.length) { return; } let audioTest; try { audioTest = new globalThis['Audio'](); } catch (e) { // No browser support for Audio. // IE can throw an error even if the Audio object exists. return; } let sound; for (let i = 0; i < filenames.length; i++) { const filename = filenames[i]; const ext = filename.match(/\.(\w+)$/); if (ext && audioTest.canPlayType('audio/' + ext[1])) { // Found an audio format we can play. sound = new globalThis['Audio'](filename); break; } } if (sound && sound.play) { this.SOUNDS_[name] = sound; } }; /** * Preload all the audio files so that they play quickly when asked for. * @package */ WorkspaceAudio.prototype.preload = function() { for (const name in this.SOUNDS_) { const sound = this.SOUNDS_[name]; sound.volume = 0.01; const playPromise = sound.play(); // Edge does not return a promise, so we need to check. if (playPromise !== undefined) { // If we don't wait for the play request to complete before calling // pause() we will get an exception: (DOMException: The play() request was // interrupted) See more: // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted playPromise.then(sound.pause).catch(function() { // Play without user interaction was prevented. }); } else { sound.pause(); } // iOS can only process one sound at a time. Trying to load more than one // corrupts the earlier ones. Just load one and leave the others uncached. if (userAgent.IPAD || userAgent.IPHONE) { break; } } }; /** * Play a named sound at specified volume. If volume is not specified, * use full volume (1). * @param {string} name Name of sound. * @param {number=} opt_volume Volume of sound (0-1). */ WorkspaceAudio.prototype.play = function(name, opt_volume) { const sound = this.SOUNDS_[name]; if (sound) { // Don't play one sound on top of another. const now = new Date; if (this.lastSound_ !== null && now - this.lastSound_ < internalConstants.SOUND_LIMIT) { return; } this.lastSound_ = now; let mySound; if (userAgent.IPAD || userAgent.ANDROID) { // Creating a new audio node causes lag in Android and iPad. Android // refetches the file from the server, iPad uses a singleton audio // node which must be deleted and recreated for each new audio tag. mySound = sound; } else { mySound = sound.cloneNode(); } mySound.volume = (opt_volume === undefined ? 1 : opt_volume); mySound.play(); } else if (this.parentWorkspace_) { // Maybe a workspace on a lower level knows about this sound. this.parentWorkspace_.getAudioManager().play(name, opt_volume); } }; exports = WorkspaceAudio;