Skip to content

Commit a892e21

Browse files
google-genai-botcopybara-github
authored andcommitted
ADK changes
PiperOrigin-RevId: 839135527
1 parent 56bf9fd commit a892e21

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

src/app/core/services/audio-playing.service.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ describe('AudioPlayingService', () => {
5454
expect(service).toBeTruthy();
5555
});
5656

57+
function createMockSource() {
58+
return jasmine.createSpyObj(
59+
'AudioBufferSourceNode', ['stop', 'start', 'connect']);
60+
}
61+
5762
describe('playAudio', () => {
5863
it('should not play audio if the buffer is empty', () => {
5964
spyOn<any>(service, 'playPCM').and.callThrough();
@@ -75,5 +80,79 @@ describe('AudioPlayingService', () => {
7580
service.playAudio(buffer);
7681
expect((service as any).playPCM).toHaveBeenCalledWith(combinedBuffer);
7782
});
83+
84+
it('should schedule audio sources correctly', () => {
85+
const buffer = [new Uint8Array([1, 2])];
86+
const mockSource = createMockSource();
87+
mockAudioContext.createBufferSource.and.returnValue(mockSource);
88+
89+
service.playAudio(buffer);
90+
91+
expect(mockAudioContext.createBufferSource).toHaveBeenCalled();
92+
expect(mockSource.start).toHaveBeenCalled();
93+
});
94+
95+
it('removes source from schedule after playback finishes', () => {
96+
const mockSource = createMockSource();
97+
mockAudioContext.createBufferSource.and.returnValue(mockSource);
98+
const scheduledAudioSources =
99+
(service as any).scheduledAudioSources as Set<any>;
100+
101+
service.playAudio([new Uint8Array([1])]);
102+
103+
// Assert that the source was added and the onended handler was attached.
104+
expect(scheduledAudioSources.has(mockSource)).toBeTrue();
105+
expect((mockSource as any).onended).toEqual(jasmine.any(Function));
106+
107+
// Simulate the audio finishing by calling the onended handler.
108+
if ((mockSource as any).onended) {
109+
(mockSource as any).onended();
110+
}
111+
112+
// Assert that the source was removed from the set.
113+
expect(scheduledAudioSources.has(mockSource)).toBeFalse();
114+
});
115+
});
116+
117+
describe('stopAudio', () => {
118+
it('should stop all scheduled sources', () => {
119+
const mockSource1 = createMockSource();
120+
const mockSource2 = createMockSource();
121+
122+
mockAudioContext.createBufferSource.and.returnValues(
123+
mockSource1, mockSource2);
124+
125+
// Play two audio clips
126+
service.playAudio([new Uint8Array([1])]);
127+
service.playAudio([new Uint8Array([2])]);
128+
129+
service.stopAudio();
130+
131+
expect(mockSource1.stop).toHaveBeenCalled();
132+
expect(mockSource2.stop).toHaveBeenCalled();
133+
});
134+
135+
it('should reset scheduling time so next audio plays immediately', () => {
136+
const mockSource1 = createMockSource();
137+
const mockSource2 = createMockSource();
138+
mockAudioContext.createBufferSource.and.returnValues(
139+
mockSource1, mockSource2);
140+
141+
// Play a long audio clip (duration 1s from mock)
142+
service.playAudio([new Uint8Array([1])]);
143+
144+
// Advance time slightly
145+
mockAudioContext.currentTime = 0.5;
146+
147+
// Stop audio
148+
service.stopAudio();
149+
150+
// Play another clip
151+
service.playAudio([new Uint8Array([2])]);
152+
153+
// The second clip should start at current time (0.5), not after the first
154+
// clip (1.0)
155+
expect(mockSource2.start).toHaveBeenCalledWith(0.5);
156+
});
78157
});
79158
});

src/app/core/services/audio-playing.service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {AudioPlayingService as AudioPlayingServiceInterface} from './interfaces/
2424
export class AudioPlayingService implements AudioPlayingServiceInterface {
2525
private audioContext = new AudioContext({sampleRate: 22000});
2626
private lastAudioTime = 0;
27+
private scheduledAudioSources = new Set<AudioBufferSourceNode>();
2728

2829
playAudio(buffer: Uint8Array[]) {
2930
const pcmBytes = this.combineAudioBuffer(buffer);
@@ -32,6 +33,18 @@ export class AudioPlayingService implements AudioPlayingServiceInterface {
3233
this.playPCM(pcmBytes);
3334
}
3435

36+
stopAudio() {
37+
for (const source of this.scheduledAudioSources) {
38+
source.onended = null;
39+
source.stop();
40+
}
41+
42+
this.scheduledAudioSources.clear();
43+
// Reset lastAudioTime to the current time so that any new audio played
44+
// after stopping will start immediately.
45+
this.lastAudioTime = this.audioContext.currentTime;
46+
}
47+
3548
private combineAudioBuffer(buffer: Uint8Array[]) {
3649
if (buffer.length === 0) return undefined;
3750

@@ -68,11 +81,17 @@ export class AudioPlayingService implements AudioPlayingServiceInterface {
6881
const source = this.audioContext.createBufferSource();
6982
source.buffer = buffer;
7083
source.connect(this.audioContext.destination);
84+
source.onended = () => {
85+
this.scheduledAudioSources.delete(source);
86+
};
87+
this.scheduledAudioSources.add(source);
7188

7289
const currentTime = this.audioContext.currentTime;
7390
const startTime = Math.max(this.lastAudioTime, currentTime);
7491
source.start(startTime);
7592

93+
// Update lastAudioTime to the end time of the current buffer to ensure
94+
// subsequent audio starts after this one finishes.
7695
this.lastAudioTime = startTime + buffer.duration;
7796
}
7897
}

src/app/core/services/interfaces/audio-playing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const AUDIO_PLAYING_SERVICE =
2525
*/
2626
export declare abstract class AudioPlayingService {
2727
abstract playAudio(buffer: Uint8Array[]): void;
28+
abstract stopAudio(): void;
2829
}

0 commit comments

Comments
 (0)