Skip to content

Commit 7823971

Browse files
authored
feat: add preferred microphone selection and settings management (#36)
1 parent df63633 commit 7823971

File tree

4 files changed

+167
-31
lines changed

4 files changed

+167
-31
lines changed

apps/desktop/src/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export interface AppSettingsData {
104104
autoStopSilence: boolean;
105105
silenceThreshold: number;
106106
maxRecordingDuration: number;
107+
preferredMicrophoneName?: string;
107108
};
108109
shortcuts?: {
109110
pushToTalk?: string;

apps/desktop/src/hooks/useAudioCapture.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,44 @@ export const useAudioCapture = ({
4242
},
4343
});
4444

45+
// Get user's preferred microphone from settings
46+
const { data: settings } = api.settings.getSettings.useQuery();
47+
const preferredMicrophoneName = settings?.recording?.preferredMicrophoneName;
48+
4549
const startCapture = useCallback(async () => {
4650
await mutexRef.current.runExclusive(async () => {
4751
try {
4852
console.log("AudioCapture: Starting audio capture");
4953

54+
// Build audio constraints
55+
const audioConstraints: MediaTrackConstraints = {
56+
channelCount: 1,
57+
sampleRate: SAMPLE_RATE,
58+
echoCancellation: true,
59+
noiseSuppression: true,
60+
autoGainControl: true,
61+
};
62+
63+
// Add deviceId if user has a preference
64+
if (preferredMicrophoneName) {
65+
const devices = await navigator.mediaDevices.enumerateDevices();
66+
const preferredDevice = devices.find(
67+
(device) =>
68+
device.kind === "audioinput" &&
69+
device.label === preferredMicrophoneName,
70+
);
71+
if (preferredDevice) {
72+
audioConstraints.deviceId = { exact: preferredDevice.deviceId };
73+
console.log(
74+
"AudioCapture: Using preferred microphone:",
75+
preferredMicrophoneName,
76+
);
77+
}
78+
}
79+
5080
// Get microphone stream
5181
streamRef.current = await navigator.mediaDevices.getUserMedia({
52-
audio: {
53-
channelCount: 1,
54-
sampleRate: SAMPLE_RATE,
55-
echoCancellation: true,
56-
noiseSuppression: true,
57-
autoGainControl: true,
58-
},
82+
audio: audioConstraints,
5983
});
6084

6185
// Create audio context
@@ -104,7 +128,7 @@ export const useAudioCapture = ({
104128
throw error;
105129
}
106130
});
107-
}, [onAudioChunk]);
131+
}, [onAudioChunk, preferredMicrophoneName]);
108132

109133
const stopCapture = useCallback(async () => {
110134
await mutexRef.current.runExclusive(async () => {

apps/desktop/src/renderer/main/pages/settings/components/MicrophoneSettings.tsx

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,59 @@ import {
88
} from "@/components/ui/card";
99
import { Label } from "@/components/ui/label";
1010
import { Switch } from "@/components/ui/switch";
11+
import {
12+
Select,
13+
SelectContent,
14+
SelectItem,
15+
SelectTrigger,
16+
SelectValue,
17+
} from "@/components/ui/select";
18+
import { api } from "@/trpc/react";
19+
import { useAudioDevices } from "@/hooks/useAudioDevices";
20+
import { toast } from "sonner";
21+
import { Mic, MicOff } from "lucide-react";
1122

1223
export function MicrophoneSettings() {
24+
const { data: settings, refetch: refetchSettings } =
25+
api.settings.getSettings.useQuery();
26+
const setPreferredMicrophone =
27+
api.settings.setPreferredMicrophone.useMutation();
28+
const { devices: audioDevices } = useAudioDevices();
29+
30+
const currentMicrophoneName = settings?.recording?.preferredMicrophoneName;
31+
32+
const handleMicrophoneChange = async (deviceName: string) => {
33+
try {
34+
// If "System Default" is selected, store null to follow system default
35+
const actualDeviceName = deviceName.startsWith("System Default")
36+
? null
37+
: deviceName;
38+
39+
await setPreferredMicrophone.mutateAsync({
40+
deviceName: actualDeviceName,
41+
});
42+
43+
// Refetch settings to update UI
44+
await refetchSettings();
45+
46+
toast.success(
47+
actualDeviceName
48+
? `Microphone changed to ${deviceName}`
49+
: "Using system default microphone",
50+
);
51+
} catch (error) {
52+
console.error("Failed to set preferred microphone:", error);
53+
toast.error("Failed to change microphone");
54+
}
55+
};
56+
57+
// Find the current selection value
58+
const currentSelectionValue =
59+
currentMicrophoneName &&
60+
audioDevices.some((device) => device.label === currentMicrophoneName)
61+
? currentMicrophoneName
62+
: audioDevices.find((d) => d.isDefault)?.label || "";
63+
1364
return (
1465
<Card>
1566
<CardHeader>
@@ -19,30 +70,44 @@ export function MicrophoneSettings() {
1970
<CardContent className="space-y-4">
2071
<div className="space-y-2">
2172
<Label htmlFor="microphone-select">Microphone</Label>
22-
<select
23-
id="microphone-select"
24-
className="w-full border rounded px-3 py-2"
73+
<Select
74+
value={currentSelectionValue}
75+
onValueChange={handleMicrophoneChange}
2576
>
26-
<option>System Default</option>
27-
<option>Built-in Microphone</option>
28-
</select>
29-
</div>
30-
31-
<div className="space-y-2">
32-
<Label htmlFor="input-volume">Input Volume</Label>
33-
<input
34-
type="range"
35-
id="input-volume"
36-
className="w-full"
37-
min="0"
38-
max="100"
39-
defaultValue="75"
40-
/>
41-
</div>
42-
43-
<div className="flex items-center space-x-2">
44-
<Switch id="noise-reduction" />
45-
<Label htmlFor="noise-reduction">Enable noise reduction</Label>
77+
<SelectTrigger id="microphone-select" className="w-full">
78+
<SelectValue placeholder="Select a microphone">
79+
<div className="flex items-center gap-2">
80+
{audioDevices.length > 0 ? (
81+
<Mic className="h-4 w-4" />
82+
) : (
83+
<MicOff className="h-4 w-4" />
84+
)}
85+
<span>{currentSelectionValue || "Select a microphone"}</span>
86+
</div>
87+
</SelectValue>
88+
</SelectTrigger>
89+
<SelectContent>
90+
{audioDevices.length === 0 ? (
91+
<SelectItem value="no-devices" disabled>
92+
No microphones available
93+
</SelectItem>
94+
) : (
95+
audioDevices.map((device) => (
96+
<SelectItem key={device.deviceId} value={device.label}>
97+
<div className="flex items-center gap-2">
98+
<Mic className="h-4 w-4" />
99+
<span>{device.label}</span>
100+
</div>
101+
</SelectItem>
102+
))
103+
)}
104+
</SelectContent>
105+
</Select>
106+
{audioDevices.length === 0 && (
107+
<p className="text-sm text-muted-foreground">
108+
No microphones detected. Please check your audio devices.
109+
</p>
110+
)}
46111
</div>
47112
</CardContent>
48113
</Card>

apps/desktop/src/trpc/routers/settings.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,50 @@ export const settingsRouter = createRouter({
256256
};
257257
});
258258
}),
259+
260+
// Set preferred microphone
261+
setPreferredMicrophone: procedure
262+
.input(
263+
z.object({
264+
deviceName: z.string().nullable(),
265+
}),
266+
)
267+
.mutation(async ({ input, ctx }) => {
268+
try {
269+
const settingsService =
270+
ctx.serviceManager.getService("settingsService");
271+
if (!settingsService) {
272+
throw new Error("SettingsService not available");
273+
}
274+
275+
// Get current recording settings
276+
const currentSettings = await settingsService.getRecordingSettings();
277+
278+
// Merge with new microphone preference
279+
const updatedSettings = {
280+
defaultFormat: "wav" as const,
281+
sampleRate: 16000 as const,
282+
autoStopSilence: false,
283+
silenceThreshold: 0.1,
284+
maxRecordingDuration: 300,
285+
...currentSettings,
286+
preferredMicrophoneName: input.deviceName || undefined,
287+
};
288+
289+
await settingsService.setRecordingSettings(updatedSettings);
290+
291+
const logger = ctx.serviceManager.getLogger();
292+
if (logger) {
293+
logger.main.info("Preferred microphone updated:", input.deviceName);
294+
}
295+
296+
return true;
297+
} catch (error) {
298+
const logger = ctx.serviceManager.getLogger();
299+
if (logger) {
300+
logger.main.error("Error setting preferred microphone:", error);
301+
}
302+
throw error;
303+
}
304+
}),
259305
});

0 commit comments

Comments
 (0)