Skip to content

Commit dc536e9

Browse files
authored
RSDK-12152 RSDK-12153 AudioIn and AudioOut component wrappers (#655)
1 parent 0cf8d04 commit dc536e9

File tree

11 files changed

+502
-1
lines changed

11 files changed

+502
-1
lines changed

src/audio-common.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export interface AudioProperties {
2+
/** List of audio codecs supported by the device */
3+
supportedCodecs: string[];
4+
/** Current sample rate in Hz */
5+
sampleRateHz: number;
6+
/** Maximum number of audio channels supported (e.g., 1 for mono, 2 for stereo) */
7+
numChannels: number;
8+
}
9+
10+
/** Common audio codec constants */
11+
export const AudioCodec = {
12+
MP3: 'mp3',
13+
PCM16: 'pcm16',
14+
PCM32: 'pcm32',
15+
PCM32_FLOAT: 'pcm32float',
16+
AAC: 'aac',
17+
OPUS: 'opus',
18+
FLAC: 'flac',
19+
WAV: 'wav',
20+
} as const;
21+
22+
export type AudioCodecType = (typeof AudioCodec)[keyof typeof AudioCodec];
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Resource, Struct } from '../../types';
2+
import type { AudioInfo } from '../../gen/common/v1/common_pb';
3+
import type { AudioProperties } from '../../audio-common';
4+
5+
export interface AudioChunk {
6+
audioData: Uint8Array;
7+
audioInfo?: AudioInfo;
8+
startTimeNs: bigint;
9+
endTimeNs: bigint;
10+
sequence: number;
11+
requestID: string;
12+
}
13+
14+
/** Represents a device that takes audio input. */
15+
16+
export interface AudioIn extends Resource {
17+
/**
18+
* Stream audio from the device.
19+
*
20+
* @example
21+
*
22+
* ```ts
23+
* const audioIn = new VIAM.AudioInClient(machine, 'my_audio_in');
24+
* const stream = audioIn.getAudio(VIAM.AudioCodec.PCM16, 3, 0n, {});
25+
* ```
26+
*/
27+
getAudio(
28+
codec: string,
29+
durationSeconds: number,
30+
previousTimestamp?: bigint,
31+
extra?: Struct
32+
): AsyncIterable<AudioChunk>;
33+
34+
/**
35+
* Return the audio input properties.
36+
*
37+
* @example
38+
*
39+
* ```ts
40+
* const audioIn = new VIAM.AudioInClient(machine, 'my_audio_in');
41+
* const properties = await audioIn.getProperties();
42+
* ```
43+
*/
44+
getProperties: () => Promise<AudioProperties>;
45+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// @vitest-environment happy-dom
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { GetAudioResponse } from '../../gen/component/audioin/v1/audioin_pb';
5+
import { GetPropertiesResponse } from '../../gen/common/v1/common_pb';
6+
import { RobotClient } from '../../robot';
7+
import { type AudioChunk } from './audio-in';
8+
import { AudioInClient } from './client';
9+
import { AudioCodec } from '../../audio-common';
10+
vi.mock('../../robot');
11+
12+
import type { PartialMessage } from '@bufbuild/protobuf';
13+
import { createClient, createRouterTransport } from '@connectrpc/connect';
14+
import {
15+
createWritableIterable,
16+
type WritableIterable,
17+
} from '@connectrpc/connect/protocol';
18+
import { AudioInService } from '../../gen/component/audioin/v1/audioin_connect';
19+
20+
let audioin: AudioInClient;
21+
22+
let testAudioStream: WritableIterable<PartialMessage<GetAudioResponse>>;
23+
24+
const testProperties = new GetPropertiesResponse({
25+
supportedCodecs: [AudioCodec.PCM16, AudioCodec.MP3, AudioCodec.PCM32_FLOAT],
26+
sampleRateHz: 48_000,
27+
numChannels: 2,
28+
});
29+
30+
describe('AudioInClient tests', () => {
31+
beforeEach(() => {
32+
testAudioStream =
33+
createWritableIterable<PartialMessage<GetAudioResponse>>();
34+
35+
const mockTransport = createRouterTransport(({ service }) => {
36+
service(AudioInService, {
37+
getAudio: () => {
38+
return testAudioStream;
39+
},
40+
getProperties: () => {
41+
return testProperties;
42+
},
43+
});
44+
});
45+
46+
RobotClient.prototype.createServiceClient = vi
47+
.fn()
48+
.mockImplementation(() => createClient(AudioInService, mockTransport));
49+
50+
audioin = new AudioInClient(new RobotClient('host'), 'test-audio-in');
51+
});
52+
53+
afterEach(() => {
54+
vi.clearAllMocks();
55+
});
56+
57+
describe('getAudio tests', () => {
58+
it('getAudio streams audio chunks', async () => {
59+
const audioChunks: AudioChunk[] = [];
60+
61+
const streamProm = (async () => {
62+
for await (const chunk of audioin.getAudio(
63+
AudioCodec.PCM16,
64+
1.1,
65+
BigInt(0)
66+
)) {
67+
audioChunks.push(chunk);
68+
}
69+
})();
70+
71+
await testAudioStream.write({
72+
audio: {
73+
audioData: new Uint8Array([4, 5, 6]),
74+
audioInfo: {
75+
codec: AudioCodec.PCM16,
76+
sampleRateHz: 48_000,
77+
numChannels: 2,
78+
},
79+
startTimestampNanoseconds: BigInt(1000),
80+
endTimestampNanoseconds: BigInt(2000),
81+
sequence: 1,
82+
},
83+
requestId: 'test-request-1',
84+
});
85+
86+
await testAudioStream.write({
87+
audio: {
88+
audioData: new Uint8Array([7, 8, 9]),
89+
audioInfo: {
90+
codec: AudioCodec.PCM16,
91+
sampleRateHz: 48_000,
92+
numChannels: 2,
93+
},
94+
startTimestampNanoseconds: BigInt(2000),
95+
endTimestampNanoseconds: BigInt(3000),
96+
sequence: 2,
97+
},
98+
requestId: 'test-request-1',
99+
});
100+
101+
testAudioStream.close();
102+
await streamProm;
103+
104+
expect(audioChunks.length).toEqual(2);
105+
106+
const chunk1 = audioChunks[0]!;
107+
expect(chunk1.audioData).toEqual(new Uint8Array([4, 5, 6]));
108+
expect(chunk1.sequence).toEqual(1);
109+
110+
const chunk2 = audioChunks[1]!;
111+
expect(chunk2.audioData).toEqual(new Uint8Array([7, 8, 9]));
112+
expect(chunk2.sequence).toEqual(2);
113+
});
114+
});
115+
116+
describe('getProperties tests', () => {
117+
it('getProperties returns audio properties', async () => {
118+
const properties = await audioin.getProperties();
119+
120+
expect(properties.supportedCodecs).toEqual([
121+
AudioCodec.PCM16,
122+
AudioCodec.MP3,
123+
AudioCodec.PCM32_FLOAT,
124+
]);
125+
expect(properties.sampleRateHz).toEqual(48_000);
126+
expect(properties.numChannels).toEqual(2);
127+
});
128+
});
129+
});

src/components/audio-in/client.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { RobotClient } from '../../robot';
2+
import type { Options } from '../../types';
3+
4+
import { Struct, type JsonValue } from '@bufbuild/protobuf';
5+
import type { CallOptions, Client } from '@connectrpc/connect';
6+
import { AudioInService } from '../../gen/component/audioin/v1/audioin_connect';
7+
import { GetAudioRequest } from '../../gen/component/audioin/v1/audioin_pb';
8+
import { GetPropertiesRequest } from '../../gen/common/v1/common_pb';
9+
import { type AudioIn, type AudioChunk } from './audio-in';
10+
import { doCommandFromClient } from '../../utils';
11+
12+
/*
13+
* A gRPC-web client for the AudioIn component.
14+
*
15+
* @group Clients
16+
*/
17+
export class AudioInClient implements AudioIn {
18+
private client: Client<typeof AudioInService>;
19+
public readonly name: string;
20+
private readonly options: Options;
21+
public callOptions: CallOptions = { headers: {} as Record<string, string> };
22+
23+
constructor(client: RobotClient, name: string, options: Options = {}) {
24+
this.client = client.createServiceClient(AudioInService);
25+
this.name = name;
26+
this.options = options;
27+
}
28+
29+
async *getAudio(
30+
codec: string,
31+
durationSeconds: number,
32+
previousTimestamp = 0n,
33+
extra = {},
34+
callOptions = this.callOptions
35+
): AsyncIterable<AudioChunk> {
36+
const request = new GetAudioRequest({
37+
name: this.name,
38+
codec,
39+
durationSeconds,
40+
previousTimestampNanoseconds: previousTimestamp,
41+
extra: Struct.fromJson(extra),
42+
});
43+
44+
this.options.requestLogger?.(request);
45+
46+
const stream = this.client.getAudio(request, callOptions);
47+
48+
// Yield chunks as they arrive
49+
for await (const resp of stream) {
50+
if (!resp.audio) {
51+
continue;
52+
}
53+
yield {
54+
audioData: resp.audio.audioData,
55+
audioInfo: resp.audio.audioInfo,
56+
startTimeNs: resp.audio.startTimestampNanoseconds,
57+
endTimeNs: resp.audio.endTimestampNanoseconds,
58+
sequence: resp.audio.sequence,
59+
requestID: resp.requestId,
60+
};
61+
}
62+
}
63+
64+
async getProperties(callOptions = this.callOptions) {
65+
const request = new GetPropertiesRequest({
66+
name: this.name,
67+
});
68+
69+
this.options.requestLogger?.(request);
70+
71+
const response = await this.client.getProperties(request, callOptions);
72+
73+
return {
74+
supportedCodecs: response.supportedCodecs,
75+
sampleRateHz: response.sampleRateHz,
76+
numChannels: response.numChannels,
77+
};
78+
}
79+
80+
async doCommand(
81+
command: Struct,
82+
callOptions = this.callOptions
83+
): Promise<JsonValue> {
84+
return doCommandFromClient(
85+
this.client.doCommand,
86+
this.name,
87+
command,
88+
this.options,
89+
callOptions
90+
);
91+
}
92+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Resource, Struct } from '../../types';
2+
import type { AudioInfo } from '../../gen/common/v1/common_pb';
3+
import type { AudioProperties } from '../../audio-common';
4+
5+
/** Represents a device that outputs audio. */
6+
export interface AudioOut extends Resource {
7+
/**
8+
* Play audio on the device.
9+
*
10+
* @example
11+
*
12+
* ```ts
13+
* const audioOut = new VIAM.AudioOutClient(machine, 'my_audio_out');
14+
* const audioData = new Uint8Array([...]); // Your audio data
15+
* const audioInfo = { codec: 'pcm16', sampleRateHz: 48000, numChannels: 2 };
16+
* await audioOut.play(audioData, audioInfo);
17+
* ```
18+
*
19+
* @param audioData - The audio data to play
20+
* @param audioInfo - Information about the audio format (optional, required
21+
* for raw pcm data)
22+
*/
23+
play: (
24+
audioData: Uint8Array,
25+
audioInfo?: AudioInfo,
26+
extra?: Struct
27+
) => Promise<void>;
28+
29+
/**
30+
* Return the audio output properties.
31+
*
32+
* @example
33+
*
34+
* ```ts
35+
* const audioOut = new VIAM.AudioOutClient(machine, 'my_audio_out');
36+
* const properties = await audioOut.getProperties();
37+
* ```
38+
*/
39+
getProperties: () => Promise<AudioProperties>;
40+
}

0 commit comments

Comments
 (0)