Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/client/src/BaseConversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UserTranscriptionEvent,
} from "./utils/events";
import type { InputConfig } from "./utils/input";
import { OutputConfig } from "./utils/output";

export type Role = "user" | "ai";

Expand All @@ -28,12 +29,14 @@ export type Status =
export type Options = SessionConfig &
Callbacks &
ClientToolsConfig &
InputConfig;
InputConfig &
OutputConfig;

export type PartialOptions = SessionConfig &
Partial<Callbacks> &
Partial<ClientToolsConfig> &
Partial<InputConfig>;
Partial<InputConfig> &
Partial<OutputConfig>;

export type ClientToolsConfig = {
clientTools: Record<
Expand Down
11 changes: 8 additions & 3 deletions packages/client/src/VoiceConversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,22 @@ export class VoiceConversation extends BaseConversation {
// some browsers won't allow calling getSupportedConstraints or enumerateDevices
// before getting approval for microphone access
preliminaryInputStream = await navigator.mediaDevices.getUserMedia({
audio: true,
audio: fullOptions.deviceId ? { deviceId: fullOptions.deviceId } : true,
});

await applyDelay(fullOptions.connectionDelay);
connection = await Connection.create(options);
[input, output] = await Promise.all([
Input.create({
...connection.inputFormat,
preferHeadphonesForIosDevices: options.preferHeadphonesForIosDevices,
preferHeadphonesForIosDevices:
fullOptions.preferHeadphonesForIosDevices,
deviceId: fullOptions.deviceId,
}),
Output.create({
...connection.outputFormat,
outputDeviceId: fullOptions.outputDeviceId,
}),
Output.create(connection.outputFormat),
]);

preliminaryInputStream?.getTracks().forEach(track => track.stop());
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/utils/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isIosDevice } from "./compatibility";

export type InputConfig = {
preferHeadphonesForIosDevices?: boolean;
deviceId?: string;
};

const LIBSAMPLERATE_JS =
Expand All @@ -14,6 +15,7 @@ export class Input {
sampleRate,
format,
preferHeadphonesForIosDevices,
deviceId,
}: FormatConfig & InputConfig): Promise<Input> {
let context: AudioContext | null = null;
let inputStream: MediaStream | null = null;
Expand All @@ -25,7 +27,9 @@ export class Input {
noiseSuppression: { ideal: true },
};

if (isIosDevice() && preferHeadphonesForIosDevices) {
if (deviceId) {
options.deviceId = deviceId;
} else if (isIosDevice() && preferHeadphonesForIosDevices) {
const availableDevices =
await window.navigator.mediaDevices.enumerateDevices();
const idealDevice = availableDevices.find(
Expand Down
13 changes: 12 additions & 1 deletion packages/client/src/utils/output.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { loadAudioConcatProcessor } from "./audioConcatProcessor";
import { FormatConfig } from "./connection";

export type OutputConfig = {
outputDeviceId?: string;
};

export class Output {
public static async create({
sampleRate,
format,
}: FormatConfig): Promise<Output> {
outputDeviceId,
}: FormatConfig & OutputConfig): Promise<Output> {
let context: AudioContext | null = null;
try {
context = new AudioContext({ sampleRate });
const analyser = context.createAnalyser();
const gain = context.createGain();
gain.connect(analyser);

// Try to set the output device if specified and supported
if (outputDeviceId && "setSinkId" in AudioContext.prototype) {
await (context as any).setSinkId(outputDeviceId);
}

analyser.connect(context.destination);
await loadAudioConcatProcessor(context.audioWorklet);
const worklet = new AudioWorkletNode(context, "audio-concat-processor");
Expand Down