Files
blockly/core/workspace_audio.ts
Christopher Allen b0a7c004a9 refactor(build): Delete Closure Library (#7415)
* fix(build): Restore erroneously-deleted filter function

  This was deleted in PR #7406 as it was mainly being used to
  filter core/ vs. test/mocha/ deps into separate deps files -
  but it turns out also to be used for filtering error
  messages too.  Oops.

* refactor(tests): Migrate advanced compilation test to ES Modules

* refactor(build): Migrate main.js to TypeScript

  This turns out to be pretty straight forward, even if it would
  cause crashing if one actually tried to import this module
  instead of just feeding it to Closure Compiler.

* chore(build): Remove goog.declareModuleId calls

  Replace goog.declareModuleId calls with a comment recording the
  former module ID for posterity (or at least until we decide
  how to reformat the renamings file.

* chore(tests): Delete closure/goog/*

  For the moment we still need something to serve as base.js for
  the benefit of closure-make-deps, so we keep a vestigial
  base.js around, containing only the @provideGoog declaration.

* refactor(build): Remove vestigial base.js

  By changing slightly the command line arguments to
  closure-make-deps and closure-calculate-chunks the need to have
  any base.js is eliminated.

* chore: Typo fix for PR #7415
2023-08-31 00:24:47 +01:00

152 lines
4.3 KiB
TypeScript

/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Object in charge of loading, storing, and playing audio for a
* workspace.
*
* @class
*/
// Former goog.module ID: Blockly.WorkspaceAudio
import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Prevent a sound from playing if another sound preceded it within this many
* milliseconds.
*/
const SOUND_LIMIT = 100;
/**
* Class for loading, storing, and playing audio for a workspace.
*/
export class WorkspaceAudio {
/** Database of pre-loaded sounds. */
private sounds = new Map<string, HTMLAudioElement>();
/** Time that the last sound was played. */
private lastSound_: Date | null = null;
/**
* @param parentWorkspace The parent of the workspace this audio object
* belongs to, or null.
*/
constructor(private parentWorkspace: WorkspaceSvg) {}
/**
* Dispose of this audio manager.
*
* @internal
*/
dispose() {
this.sounds.clear();
}
/**
* Load an audio file. Cache it, ready for instantaneous playing.
*
* @param 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 name Name of sound.
*/
load(filenames: string[], name: string) {
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) {
this.sounds.set(name, sound);
}
}
/**
* Preload all the audio files so that they play quickly when asked for.
*
* @internal
*/
preload() {
for (const sound of this.sounds.values()) {
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(
// Play without user interaction was prevented.
function () {},
);
} 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 name Name of sound.
* @param opt_volume Volume of sound (0-1).
*/
play(name: string, opt_volume?: number) {
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;
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() as HTMLAudioElement;
}
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);
}
}
}