Skip to content

feat: add preferred microphone selection and settings management #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2025
Merged
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
1 change: 1 addition & 0 deletions apps/desktop/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface AppSettingsData {
autoStopSilence: boolean;
silenceThreshold: number;
maxRecordingDuration: number;
preferredMicrophoneName?: string;
};
shortcuts?: {
pushToTalk?: string;
Expand Down
40 changes: 32 additions & 8 deletions apps/desktop/src/hooks/useAudioCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,44 @@ export const useAudioCapture = ({
},
});

// Get user's preferred microphone from settings
const { data: settings } = api.settings.getSettings.useQuery();
const preferredMicrophoneName = settings?.recording?.preferredMicrophoneName;

const startCapture = useCallback(async () => {
await mutexRef.current.runExclusive(async () => {
try {
console.log("AudioCapture: Starting audio capture");

// Build audio constraints
const audioConstraints: MediaTrackConstraints = {
channelCount: 1,
sampleRate: SAMPLE_RATE,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
};

// Add deviceId if user has a preference
if (preferredMicrophoneName) {
const devices = await navigator.mediaDevices.enumerateDevices();
const preferredDevice = devices.find(
(device) =>
device.kind === "audioinput" &&
device.label === preferredMicrophoneName,
);
if (preferredDevice) {
audioConstraints.deviceId = { exact: preferredDevice.deviceId };
console.log(
"AudioCapture: Using preferred microphone:",
preferredMicrophoneName,
);
}
}

// Get microphone stream
streamRef.current = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
sampleRate: SAMPLE_RATE,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
audio: audioConstraints,
});

// Create audio context
Expand Down Expand Up @@ -104,7 +128,7 @@ export const useAudioCapture = ({
throw error;
}
});
}, [onAudioChunk]);
}, [onAudioChunk, preferredMicrophoneName]);

const stopCapture = useCallback(async () => {
await mutexRef.current.runExclusive(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,59 @@ import {
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/trpc/react";
import { useAudioDevices } from "@/hooks/useAudioDevices";
import { toast } from "sonner";
import { Mic, MicOff } from "lucide-react";

export function MicrophoneSettings() {
const { data: settings, refetch: refetchSettings } =
api.settings.getSettings.useQuery();
const setPreferredMicrophone =
api.settings.setPreferredMicrophone.useMutation();
const { devices: audioDevices } = useAudioDevices();

const currentMicrophoneName = settings?.recording?.preferredMicrophoneName;

const handleMicrophoneChange = async (deviceName: string) => {
try {
// If "System Default" is selected, store null to follow system default
const actualDeviceName = deviceName.startsWith("System Default")
? null
: deviceName;

await setPreferredMicrophone.mutateAsync({
deviceName: actualDeviceName,
});

// Refetch settings to update UI
await refetchSettings();

toast.success(
actualDeviceName
? `Microphone changed to ${deviceName}`
: "Using system default microphone",
);
} catch (error) {
console.error("Failed to set preferred microphone:", error);
toast.error("Failed to change microphone");
}
};

// Find the current selection value
const currentSelectionValue =
currentMicrophoneName &&
audioDevices.some((device) => device.label === currentMicrophoneName)
? currentMicrophoneName
: audioDevices.find((d) => d.isDefault)?.label || "";

return (
<Card>
<CardHeader>
Expand All @@ -19,30 +70,44 @@ export function MicrophoneSettings() {
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="microphone-select">Microphone</Label>
<select
id="microphone-select"
className="w-full border rounded px-3 py-2"
<Select
value={currentSelectionValue}
onValueChange={handleMicrophoneChange}
>
<option>System Default</option>
<option>Built-in Microphone</option>
</select>
</div>

<div className="space-y-2">
<Label htmlFor="input-volume">Input Volume</Label>
<input
type="range"
id="input-volume"
className="w-full"
min="0"
max="100"
defaultValue="75"
/>
</div>

<div className="flex items-center space-x-2">
<Switch id="noise-reduction" />
<Label htmlFor="noise-reduction">Enable noise reduction</Label>
<SelectTrigger id="microphone-select" className="w-full">
<SelectValue placeholder="Select a microphone">
<div className="flex items-center gap-2">
{audioDevices.length > 0 ? (
<Mic className="h-4 w-4" />
) : (
<MicOff className="h-4 w-4" />
)}
<span>{currentSelectionValue || "Select a microphone"}</span>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
{audioDevices.length === 0 ? (
<SelectItem value="no-devices" disabled>
No microphones available
</SelectItem>
) : (
audioDevices.map((device) => (
<SelectItem key={device.deviceId} value={device.label}>
<div className="flex items-center gap-2">
<Mic className="h-4 w-4" />
<span>{device.label}</span>
</div>
</SelectItem>
))
)}
</SelectContent>
</Select>
{audioDevices.length === 0 && (
<p className="text-sm text-muted-foreground">
No microphones detected. Please check your audio devices.
</p>
)}
</div>
</CardContent>
</Card>
Expand Down
46 changes: 46 additions & 0 deletions apps/desktop/src/trpc/routers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,50 @@ export const settingsRouter = createRouter({
};
});
}),

// Set preferred microphone
setPreferredMicrophone: procedure
.input(
z.object({
deviceName: z.string().nullable(),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const settingsService =
ctx.serviceManager.getService("settingsService");
if (!settingsService) {
throw new Error("SettingsService not available");
}

// Get current recording settings
const currentSettings = await settingsService.getRecordingSettings();

// Merge with new microphone preference
const updatedSettings = {
defaultFormat: "wav" as const,
sampleRate: 16000 as const,
autoStopSilence: false,
silenceThreshold: 0.1,
maxRecordingDuration: 300,
...currentSettings,
preferredMicrophoneName: input.deviceName || undefined,
};

await settingsService.setRecordingSettings(updatedSettings);

const logger = ctx.serviceManager.getLogger();
if (logger) {
logger.main.info("Preferred microphone updated:", input.deviceName);
}

return true;
} catch (error) {
const logger = ctx.serviceManager.getLogger();
if (logger) {
logger.main.error("Error setting preferred microphone:", error);
}
throw error;
}
}),
});
Loading