From 7f2b4196b0366e8f9e5c18017049834b554bb1a9 Mon Sep 17 00:00:00 2001 From: thisisharsh7 <9u.harsh@gmail.com> Date: Wed, 14 Jan 2026 23:26:27 +0530 Subject: [PATCH 1/2] feat(whispering): add system default microphone with auto device detection --- .../src-tauri/src/recorder/recorder.rs | 5 ++++- .../selectors/ManualDeviceSelector.svelte | 18 +++++++++++++++++- .../selectors/VadDeviceSelector.svelte | 19 ++++++++++++++++++- .../src/lib/query/isomorphic/recorder.ts | 15 ++++++++++++++- apps/whispering/src/lib/settings/settings.ts | 6 +++--- 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/apps/whispering/src-tauri/src/recorder/recorder.rs b/apps/whispering/src-tauri/src/recorder/recorder.rs index 91fbbb6efd..9fdaaec98d 100644 --- a/apps/whispering/src-tauri/src/recorder/recorder.rs +++ b/apps/whispering/src-tauri/src/recorder/recorder.rs @@ -57,12 +57,15 @@ impl RecorderState { /// List available recording devices by name pub fn enumerate_devices(&self) -> Result> { let host = cpal::default_host(); - let devices = host + let mut devices: Vec = host .input_devices() .map_err(|e| format!("Failed to get input devices: {}", e))? .filter_map(|device| device.name().ok()) .collect(); + // Prepend "default" as the first option (uses OS system default) + devices.insert(0, "default".to_string()); + Ok(devices) } diff --git a/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte b/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte index 599b1ff146..3596d4e2a7 100644 --- a/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte +++ b/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte @@ -12,6 +12,7 @@ import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'; import { Spinner } from '@epicenter/ui/spinner'; import { Badge } from '@epicenter/ui/badge'; + import { createDeviceChangeListener } from '$lib/services/isomorphic/device-change.svelte'; const combobox = useCombobox(); @@ -51,6 +52,16 @@ enabled: combobox.open, })); + const deviceChangeListener = createDeviceChangeListener(); + + // Auto-refresh device list when devices change (web only - desktop uses polling) + $effect(() => { + if (combobox.open) { + deviceChangeListener.subscribe(); + getDevicesQuery.refetch(); + } + }); + $effect(() => { if (getDevicesQuery.isError) { rpc.notify.warning(getDevicesQuery.error); @@ -159,7 +170,12 @@ : 'opacity-0', )} /> - {device.label} + + {device.label} + {#if device.id === 'default'} + Auto + {/if} + {/each} {/if} diff --git a/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte b/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte index b0b7154d32..5a553ad276 100644 --- a/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte +++ b/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte @@ -12,6 +12,8 @@ import MicIcon from '@lucide/svelte/icons/mic'; import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'; import { Spinner } from '@epicenter/ui/spinner'; + import { Badge } from '@epicenter/ui/badge'; + import { createDeviceChangeListener } from '$lib/services/isomorphic/device-change.svelte'; const combobox = useCombobox(); @@ -27,6 +29,16 @@ enabled: combobox.open, })); + const deviceChangeListener = createDeviceChangeListener(); + + // Auto-refresh device list when devices change (web only - desktop uses polling) + $effect(() => { + if (combobox.open) { + deviceChangeListener.subscribe(); + getDevicesQuery.refetch(); + } + }); + $effect(() => { if (getDevicesQuery.isError) { rpc.notify.warning(getDevicesQuery.error); @@ -91,7 +103,12 @@ selectedDeviceId === device.id ? 'opacity-100' : 'opacity-0', )} /> - {device.label} + + {device.label} + {#if device.id === 'default'} + Auto + {/if} + {/each} {/if} diff --git a/apps/whispering/src/lib/query/isomorphic/recorder.ts b/apps/whispering/src/lib/query/isomorphic/recorder.ts index cef756b97c..f7c2ed07f4 100644 --- a/apps/whispering/src/lib/query/isomorphic/recorder.ts +++ b/apps/whispering/src/lib/query/isomorphic/recorder.ts @@ -37,8 +37,21 @@ export const recorder = { serviceError: error, }); } - return Ok(data); + // Transform "default" device ID to user-friendly "System Default" label + const devicesWithLabels = data.map((device) => { + if (device.id === 'default') { + return { + ...device, + label: 'System Default', + }; + } + return device; + }); + return Ok(devicesWithLabels); }, + // Poll for device changes on desktop (no native devicechange event in Tauri) + // On web, we use the devicechange event listener in components + refetchInterval: window.__TAURI_INTERNALS__ ? 3000 : false, }), // Query that returns the recorder state (IDLE or RECORDING) diff --git a/apps/whispering/src/lib/settings/settings.ts b/apps/whispering/src/lib/settings/settings.ts index 8e5dee1db1..2efe0cddfe 100644 --- a/apps/whispering/src/lib/settings/settings.ts +++ b/apps/whispering/src/lib/settings/settings.ts @@ -136,13 +136,13 @@ export const Settings = type({ */ 'recording.cpal.deviceId': type('string | null') .pipe(deviceIdTransform) - .default(null), + .default('default'), 'recording.navigator.deviceId': type('string | null') .pipe(deviceIdTransform) - .default(null), + .default('default'), 'recording.ffmpeg.deviceId': type('string | null') .pipe(deviceIdTransform) - .default(null), + .default('default'), // Browser recording settings (used when browser method is selected) 'recording.navigator.bitrateKbps': type From 2e9bbd89e6c2561c9d97981c06b21cc1ae943c7e Mon Sep 17 00:00:00 2001 From: thisisharsh7 <9u.harsh@gmail.com> Date: Wed, 28 Jan 2026 10:49:25 +0530 Subject: [PATCH 2/2] feat(whispering): add system default microphone with auto device detection --- .../selectors/ManualDeviceSelector.svelte | 5 ++- .../selectors/VadDeviceSelector.svelte | 5 ++- .../isomorphic/device-change.svelte.ts | 37 +++++++++++++++++++ .../lib/services/isomorphic/device-stream.ts | 24 +++++++++--- 4 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 apps/whispering/src/lib/services/isomorphic/device-change.svelte.ts diff --git a/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte b/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte index 3596d4e2a7..e7a70bdbd7 100644 --- a/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte +++ b/apps/whispering/src/lib/components/settings/selectors/ManualDeviceSelector.svelte @@ -55,11 +55,14 @@ const deviceChangeListener = createDeviceChangeListener(); // Auto-refresh device list when devices change (web only - desktop uses polling) + // Note: TanStack Query already fetches when enabled becomes true, so no manual refetch needed $effect(() => { if (combobox.open) { deviceChangeListener.subscribe(); - getDevicesQuery.refetch(); } + return () => { + deviceChangeListener.unsubscribe(); + }; }); $effect(() => { diff --git a/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte b/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte index 5a553ad276..6cb0ff9730 100644 --- a/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte +++ b/apps/whispering/src/lib/components/settings/selectors/VadDeviceSelector.svelte @@ -32,11 +32,14 @@ const deviceChangeListener = createDeviceChangeListener(); // Auto-refresh device list when devices change (web only - desktop uses polling) + // Note: TanStack Query already fetches when enabled becomes true, so no manual refetch needed $effect(() => { if (combobox.open) { deviceChangeListener.subscribe(); - getDevicesQuery.refetch(); } + return () => { + deviceChangeListener.unsubscribe(); + }; }); $effect(() => { diff --git a/apps/whispering/src/lib/services/isomorphic/device-change.svelte.ts b/apps/whispering/src/lib/services/isomorphic/device-change.svelte.ts new file mode 100644 index 0000000000..8f1d6983ff --- /dev/null +++ b/apps/whispering/src/lib/services/isomorphic/device-change.svelte.ts @@ -0,0 +1,37 @@ +/** + * Creates a device change listener for monitoring audio device changes + * Works in both web (MediaDevices) and desktop (Tauri) environments + */ +export function createDeviceChangeListener() { + let isSubscribed = false; + let handler: (() => void) | null = null; + + function subscribe() { + if (isSubscribed || !navigator.mediaDevices) { + return; + } + + // Subscribe to device changes (web only - desktop uses polling) + if ('ondevicechange' in navigator.mediaDevices) { + handler = () => { + // Device change detected - queries will auto-refresh via enabled flag + console.log('[DeviceChangeListener] Device change detected'); + }; + navigator.mediaDevices.addEventListener('devicechange', handler); + isSubscribed = true; + } + } + + function unsubscribe() { + if (handler && navigator.mediaDevices) { + navigator.mediaDevices.removeEventListener('devicechange', handler); + } + isSubscribed = false; + handler = null; + } + + return { + subscribe, + unsubscribe, + }; +} diff --git a/apps/whispering/src/lib/services/isomorphic/device-stream.ts b/apps/whispering/src/lib/services/isomorphic/device-stream.ts index be39138c7e..c512b99eac 100644 --- a/apps/whispering/src/lib/services/isomorphic/device-stream.ts +++ b/apps/whispering/src/lib/services/isomorphic/device-stream.ts @@ -61,10 +61,18 @@ export async function enumerateDevices(): Promise< (device) => device.kind === 'audioinput', ); // On Web: Return Device objects with both ID and label - return audioInputDevices.map((device) => ({ + const deviceList = audioInputDevices.map((device) => ({ id: asDeviceIdentifier(device.deviceId), label: device.label, })); + + // Prepend "System Default" as the first option + deviceList.unshift({ + id: asDeviceIdentifier('default'), + label: 'System Default', + }); + + return deviceList; }, catch: (error) => DeviceStreamServiceErr({ @@ -89,11 +97,17 @@ async function getStreamForDeviceIdentifier( return tryAsync({ try: async () => { // On Web: deviceIdentifier IS the deviceId, use it directly + // Special case: if deviceIdentifier is "default", omit deviceId to use system default + const audioConstraints = + deviceIdentifier === 'default' + ? WHISPER_RECOMMENDED_MEDIA_TRACK_CONSTRAINTS + : { + ...WHISPER_RECOMMENDED_MEDIA_TRACK_CONSTRAINTS, + deviceId: { exact: deviceIdentifier }, + }; + const stream = await navigator.mediaDevices.getUserMedia({ - audio: { - ...WHISPER_RECOMMENDED_MEDIA_TRACK_CONSTRAINTS, - deviceId: { exact: deviceIdentifier }, - }, + audio: audioConstraints, }); return stream; },