1212 */
1313// Former goog.module ID: Blockly.WorkspaceAudio
1414
15- import * as userAgent from './utils/useragent.js' ;
1615import type { WorkspaceSvg } from './workspace_svg.js' ;
1716
1817/**
@@ -26,19 +25,26 @@ const SOUND_LIMIT = 100;
2625 */
2726export class WorkspaceAudio {
2827 /** Database of pre-loaded sounds. */
29- private sounds = new Map < string , HTMLAudioElement > ( ) ;
28+ private sounds = new Map < string , AudioBuffer > ( ) ;
3029
3130 /** Time that the last sound was played. */
3231 private lastSound : Date | null = null ;
3332
3433 /** Whether the audio is muted or not. */
3534 private muted : boolean = false ;
3635
36+ /** Audio context used for playback. */
37+ private readonly context ?: AudioContext ;
38+
3739 /**
3840 * @param parentWorkspace The parent of the workspace this audio object
3941 * belongs to, or null.
4042 */
41- constructor ( private parentWorkspace : WorkspaceSvg ) { }
43+ constructor ( private parentWorkspace : WorkspaceSvg ) {
44+ if ( window . AudioContext ) {
45+ this . context = new AudioContext ( ) ;
46+ }
47+ }
4248
4349 /**
4450 * Dispose of this audio manager.
@@ -47,73 +53,26 @@ export class WorkspaceAudio {
4753 */
4854 dispose ( ) {
4955 this . sounds . clear ( ) ;
56+ this . context ?. close ( ) ;
5057 }
5158
5259 /**
5360 * Load an audio file. Cache it, ready for instantaneous playing.
5461 *
55- * @param filenames List of file types in decreasing order of preference (i.e.
56- * increasing size). E.g. ['media/go.mp3', 'media/go.wav'] Filenames
57- * include path from Blockly's root. File extensions matter.
62+ * @param filenames Single-item array containing the URL for the sound file.
63+ * Any items after the first item are ignored.
5864 * @param name Name of sound.
5965 */
60- load ( filenames : string [ ] , name : string ) {
66+ async load ( filenames : string [ ] , name : string ) {
6167 if ( ! filenames . length ) {
6268 return ;
6369 }
64- let audioTest ;
65- try {
66- audioTest = new globalThis [ 'Audio' ] ( ) ;
67- } catch {
68- // No browser support for Audio.
69- // IE can throw an error even if the Audio object exists.
70- return ;
71- }
72- let sound ;
73- for ( let i = 0 ; i < filenames . length ; i ++ ) {
74- const filename = filenames [ i ] ;
75- const ext = filename . match ( / \. ( \w + ) $ / ) ;
76- if ( ext && audioTest . canPlayType ( 'audio/' + ext [ 1 ] ) ) {
77- // Found an audio format we can play.
78- sound = new globalThis [ 'Audio' ] ( filename ) ;
79- break ;
80- }
81- }
82- if ( sound ) {
83- this . sounds . set ( name , sound ) ;
84- }
85- }
86-
87- /**
88- * Preload all the audio files so that they play quickly when asked for.
89- *
90- * @internal
91- */
92- preload ( ) {
93- for ( const sound of this . sounds . values ( ) ) {
94- sound . volume = 0.01 ;
95- const playPromise = sound . play ( ) ;
96- // Edge does not return a promise, so we need to check.
97- if ( playPromise !== undefined ) {
98- // If we don't wait for the play request to complete before calling
99- // pause() we will get an exception: (DOMException: The play() request
100- // was interrupted) See more:
101- // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
102- playPromise . then ( sound . pause ) . catch (
103- // Play without user interaction was prevented.
104- function ( ) { } ,
105- ) ;
106- } else {
107- sound . pause ( ) ;
108- }
10970
110- // iOS can only process one sound at a time. Trying to load more than one
111- // corrupts the earlier ones. Just load one and leave the others
112- // uncached.
113- if ( userAgent . IPAD || userAgent . IPHONE ) {
114- break ;
115- }
116- }
71+ const response = await fetch ( filenames [ 0 ] ) ;
72+ const arrayBuffer = await response . arrayBuffer ( ) ;
73+ this . context ?. decodeAudioData ( arrayBuffer , ( audioBuffer ) => {
74+ this . sounds . set ( name , audioBuffer ) ;
75+ } ) ;
11776 }
11877
11978 /**
@@ -123,8 +82,8 @@ export class WorkspaceAudio {
12382 * @param name Name of sound.
12483 * @param opt_volume Volume of sound (0-1).
12584 */
126- play ( name : string , opt_volume ?: number ) {
127- if ( this . muted ) {
85+ async play ( name : string , opt_volume ?: number ) {
86+ if ( this . muted || opt_volume === 0 || ! this . context ) {
12887 return ;
12988 }
13089 const sound = this . sounds . get ( name ) ;
@@ -138,17 +97,24 @@ export class WorkspaceAudio {
13897 return ;
13998 }
14099 this . lastSound = now ;
141- let mySound ;
142- if ( userAgent . IPAD || userAgent . ANDROID ) {
143- // Creating a new audio node causes lag in Android and iPad. Android
144- // refetches the file from the server, iPad uses a singleton audio
145- // node which must be deleted and recreated for each new audio tag.
146- mySound = sound ;
147- } else {
148- mySound = sound . cloneNode ( ) as HTMLAudioElement ;
100+
101+ if ( this . context . state === 'suspended' ) {
102+ await this . context . resume ( ) ;
149103 }
150- mySound . volume = opt_volume === undefined ? 1 : opt_volume ;
151- mySound . play ( ) ;
104+
105+ const source = this . context . createBufferSource ( ) ;
106+ const gainNode = this . context . createGain ( ) ;
107+ gainNode . gain . value = opt_volume ?? 1 ;
108+ gainNode . connect ( this . context . destination ) ;
109+ source . buffer = sound ;
110+ source . connect ( gainNode ) ;
111+
112+ source . addEventListener ( 'ended' , ( ) => {
113+ source . disconnect ( ) ;
114+ gainNode . disconnect ( ) ;
115+ } ) ;
116+
117+ source . start ( ) ;
152118 } else if ( this . parentWorkspace ) {
153119 // Maybe a workspace on a lower level knows about this sound.
154120 this . parentWorkspace . getAudioManager ( ) . play ( name , opt_volume ) ;
0 commit comments