Skip to content

Commit e65ac7f

Browse files
authored
feat: Add a method to play beep tones (#9612)
1 parent da1db45 commit e65ac7f

1 file changed

Lines changed: 69 additions & 16 deletions

File tree

packages/blockly/core/workspace_audio.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)