Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 3 additions & 64 deletions core/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,68 +326,7 @@ function bindDocumentEvents() {
*/
function loadSounds(pathToMedia: string, workspace: WorkspaceSvg) {
const audioMgr = workspace.getAudioManager();
audioMgr.load(
[
pathToMedia + 'click.mp3',
pathToMedia + 'click.wav',
pathToMedia + 'click.ogg',
],
'click',
);
audioMgr.load(
[
pathToMedia + 'disconnect.wav',
pathToMedia + 'disconnect.mp3',
pathToMedia + 'disconnect.ogg',
],
'disconnect',
);
audioMgr.load(
[
pathToMedia + 'delete.mp3',
pathToMedia + 'delete.ogg',
pathToMedia + 'delete.wav',
],
'delete',
);

// Bind temporary hooks that preload the sounds.
const soundBinds: browserEvents.Data[] = [];
/**
*
*/
function unbindSounds() {
while (soundBinds.length) {
const oldSoundBinding = soundBinds.pop();
if (oldSoundBinding) {
browserEvents.unbind(oldSoundBinding);
}
}
audioMgr.preload();
}

// These are bound on mouse/touch events with
// Blockly.browserEvents.conditionalBind, so they restrict the touch
// identifier that will be recognized. But this is really something that
// happens on a click, not a drag, so that's not necessary.

// Android ignores any sound not loaded as a result of a user action.
soundBinds.push(
browserEvents.conditionalBind(
document,
'pointermove',
null,
unbindSounds,
true,
),
);
soundBinds.push(
browserEvents.conditionalBind(
document,
'touchstart',
null,
unbindSounds,
true,
),
);
audioMgr.load([`${pathToMedia}click.mp3`], 'click');
audioMgr.load([`${pathToMedia}disconnect.mp3`], 'disconnect');
audioMgr.load([`${pathToMedia}delete.mp3`], 'delete');
}
108 changes: 37 additions & 71 deletions core/workspace_audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
*/
// Former goog.module ID: Blockly.WorkspaceAudio

import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js';

/**
Expand All @@ -26,19 +25,26 @@ const SOUND_LIMIT = 100;
*/
export class WorkspaceAudio {
/** Database of pre-loaded sounds. */
private sounds = new Map<string, HTMLAudioElement>();
private sounds = new Map<string, AudioBuffer>();

/** Time that the last sound was played. */
private lastSound: Date | null = null;

/** Whether the audio is muted or not. */
private muted: boolean = false;

/** Audio context used for playback. */
private readonly context?: AudioContext;

/**
* @param parentWorkspace The parent of the workspace this audio object
* belongs to, or null.
*/
constructor(private parentWorkspace: WorkspaceSvg) {}
constructor(private parentWorkspace: WorkspaceSvg) {
if (window.AudioContext) {
this.context = new AudioContext();
}
}

/**
* Dispose of this audio manager.
Expand All @@ -47,73 +53,26 @@ export class WorkspaceAudio {
*/
dispose() {
this.sounds.clear();
this.context?.close();
}

/**
* 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 filenames Single-item array containing the URL for the sound file.
* Any items after the first item are ignored.
* @param name Name of sound.
*/
load(filenames: string[], name: string) {
async load(filenames: string[], name: string) {
if (!filenames.length) {
return;
}
let audioTest;
try {
audioTest = new globalThis['Audio']();
} catch {
// 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;
}
}
const response = await fetch(filenames[0]);
const arrayBuffer = await response.arrayBuffer();
this.context?.decodeAudioData(arrayBuffer, (audioBuffer) => {
this.sounds.set(name, audioBuffer);
});
}

/**
Expand All @@ -123,8 +82,8 @@ export class WorkspaceAudio {
* @param name Name of sound.
* @param opt_volume Volume of sound (0-1).
*/
play(name: string, opt_volume?: number) {
if (this.muted) {
async play(name: string, opt_volume?: number) {
if (this.muted || opt_volume === 0 || !this.context) {
return;
}
const sound = this.sounds.get(name);
Expand All @@ -138,17 +97,24 @@ export class WorkspaceAudio {
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;

if (this.context.state === 'suspended') {
await this.context.resume();
}
mySound.volume = opt_volume === undefined ? 1 : opt_volume;
mySound.play();

const source = this.context.createBufferSource();
const gainNode = this.context.createGain();
gainNode.gain.value = opt_volume ?? 1;
gainNode.connect(this.context.destination);
source.buffer = sound;
source.connect(gainNode);

source.addEventListener('ended', () => {
source.disconnect();
gainNode.disconnect();
});

source.start();
} else if (this.parentWorkspace) {
// Maybe a workspace on a lower level knows about this sound.
this.parentWorkspace.getAudioManager().play(name, opt_volume);
Expand Down
Binary file removed media/click.ogg
Binary file not shown.
Binary file removed media/click.wav
Binary file not shown.
Binary file removed media/delete.ogg
Binary file not shown.
Binary file removed media/delete.wav
Binary file not shown.
Binary file removed media/disconnect.ogg
Binary file not shown.
Binary file removed media/disconnect.wav
Binary file not shown.
Loading