@@ -83,24 +83,10 @@ export class WorkspaceAudio {
8383 * @param opt_volume Volume of sound (0-1).
8484 */
8585 async play ( name : string , opt_volume ?: number ) {
86- if ( this . muted || opt_volume === 0 || ! this . context ) {
87- return ;
88- }
86+ if ( ! this . isPlayingAllowed ( ) || opt_volume === 0 ) return ;
8987 const sound = this . sounds . get ( name ) ;
9088 if ( sound ) {
91- // Don't play one sound on top of another.
92- const now = new Date ( ) ;
93- if (
94- this . lastSound !== null &&
95- now . getTime ( ) - this . lastSound . getTime ( ) < SOUND_LIMIT
96- ) {
97- return ;
98- }
99- this . lastSound = now ;
100-
101- if ( this . context . state === 'suspended' ) {
102- await this . context . resume ( ) ;
103- }
89+ await this . prepareToPlay ( ) ;
10490
10591 const source = this . context . createBufferSource ( ) ;
10692 const gainNode = this . context . createGain ( ) ;
@@ -121,6 +107,73 @@ export class WorkspaceAudio {
121107 }
122108 }
123109
110+ /**
111+ * Plays a beep at the given frequency.
112+ *
113+ * @param tone The frequency of the beep to play, in hertz.
114+ * @param duration The duration of the beep, in seconds. Defaults to 0.2.
115+ */
116+ async beep ( tone : number , duration = 0.2 ) {
117+ if ( ! this . isPlayingAllowed ( ) ) return ;
118+ await this . prepareToPlay ( ) ;
119+
120+ const oscillator = this . context . createOscillator ( ) ;
121+ oscillator . type = 'sine' ;
122+ oscillator . frequency . setValueAtTime ( tone , this . context . currentTime ) ;
123+
124+ const gainNode = this . context . createGain ( ) ;
125+ gainNode . gain . setValueAtTime ( 0 , this . context . currentTime ) ;
126+ // Fade in
127+ gainNode . gain . linearRampToValueAtTime ( 0.5 , this . context . currentTime + 0.01 ) ;
128+ // Fade out
129+ gainNode . gain . linearRampToValueAtTime (
130+ 0 ,
131+ this . context . currentTime + duration ,
132+ ) ;
133+
134+ oscillator . connect ( gainNode ) ;
135+ gainNode . connect ( this . context . destination ) ;
136+
137+ oscillator . start ( this . context . currentTime ) ;
138+ oscillator . stop ( this . context . currentTime + duration ) ;
139+ }
140+
141+ /**
142+ * Returns whether or not playing sounds is currently allowed.
143+ *
144+ * @returns False if audio is muted or a sound has just been played, otherwise
145+ * true.
146+ */
147+ private isPlayingAllowed (
148+ this : WorkspaceAudio ,
149+ ) : this is WorkspaceAudio & Required < { context : AudioContext } > {
150+ const now = new Date ( ) ;
151+
152+ if (
153+ this . getMuted ( ) ||
154+ ! this . context ||
155+ ( this . lastSound !== null &&
156+ now . getTime ( ) - this . lastSound . getTime ( ) < SOUND_LIMIT )
157+ ) {
158+ return false ;
159+ }
160+ return true ;
161+ }
162+
163+ /**
164+ * Prepares to play audio by recording the time of the last play and resuming
165+ * the audio context.
166+ */
167+ private async prepareToPlay (
168+ this : WorkspaceAudio & Required < { context : AudioContext } > ,
169+ ) {
170+ this . lastSound = new Date ( ) ;
171+
172+ if ( this . context . state === 'suspended' ) {
173+ await this . context . resume ( ) ;
174+ }
175+ }
176+
124177 /**
125178 * @param muted If true, mute sounds. Otherwise, play them.
126179 */
0 commit comments