Repository navigation
Microphone device selector (#137) - #415
Merged
Merged
Conversation
null (default) means the OS default input, unchanged behaviour. Any other value is an opaque device id from the renderer's enumerateDevices() - nothing to validate it against outside a renderer with mic permission, so the store only checks it's null or a non-empty string. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
The mic and audio graph are resident for the app's lifetime (see the file's own header comment), so picking a different device mid-session means tearing the graph down and rebuilding it against the new one - there's no "just change a parameter" path in the Web Audio API for this. A device switch is queued rather than applied immediately when a recording is in progress - rebuilding the graph mid-flight would corrupt the chunks already collected. It applies right after the recording finishes or is cancelled. Startup now waits for main to push the configured device once, rather than opening the system default unconditionally at load - the two triggers racing (an unconditional self-start plus main's push arriving mid-flight) would have let a device switch requested during the first second of app launch silently no-op against an in-flight, already -wrong-device capturePromise. If the selected device is gone (unplugged, a Bluetooth headset turned off) the open falls back to the system default rather than leaving every dictation broken until the user notices and reopens Settings - logged, not silent. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
pushMicrophoneDevice sends the configured device to the capture window right after it loads (the deterministic start signal capture.js now waits for - see the previous commit) and again on every settingsStore change, same subscribe-on-create pattern as updateTrayMenu. The capture window just no-ops when the pushed id matches what it's already using. IPC + bridge: settings:set-microphone-device, validated by the store. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
Same select-based pattern as the dictation-language and overlay -position sections. Devices come from enumerateDevices() - labels need an active or previously-granted mic permission on this window first, so it opens a throwaway getUserMedia stream just long enough to unlock them, then stops it immediately; the capture window's own resident stream is untouched. A saved device that's no longer in the list (unplugged since it was picked) shows as "Not connected" rather than a blank selection - capture.js's own fallback to the system default runs regardless of what this dropdown shows. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
# Conflicts: # electron/main.js # electron/preload.js # electron/settingsStore.js # electron/settingsStore.test.js # src/openstreamBridge.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #137. Audio capture always used the OS default input device; this adds a Settings picker for a specific one.
What's built
settingsStore.js:microphoneDeviceId- null (default, unchanged behaviour) or a device id from the renderer'senumerateDevices().capture.js: the mic and audio graph are resident for the app's lifetime (per the file's own header comment), so picking a different device means tearing the graph down and rebuilding it, not just changing a parameter. A switch requested mid-recording is queued and applied right after the recording finishes, rather than corrupting the chunks already collected. If the selected device is gone (unplugged, a Bluetooth headset turned off), it falls back to the system default and logs it - a broken selection would otherwise silently break every future dictation until the user reopens Settings.pushMicrophoneDevice, same pattern asupdateTrayMenu).Judgement calls to veto
capture.jscalledprepareCapture()unconditionally at load. I changed it to wait for main's firstset-devicemessage instead - a race between an unconditional self-start and main's push arriving mid-flight could let an early device switch silently no-op against an already-in-flight, wrong-devicecapturePromise. This does mean capture now has a hard dependency on that first IPC message arriving; if it somehow didn't, capture would never start. I judged the deterministic single trigger safer than the alternative race, but flagging it since it changes a startup invariant.What I could not verify
No real second microphone in this session's sandbox, and no way to run the live capture pipeline here (same class of gap as #253's Swift piece). Everything is compile/typecheck/unit-verified; the actual device-switching behaviour - a real device list, a live mid-recording switch, a real unplug-and-fallback - is unexercised.
docs/testing/microphone-selector-137.mdhas the manual checklist. Please run it (ideally with a second input device on hand) before trusting this - I'd rather you catch a live-audio-graph bug than merge on faith.Tests
settingsStore.test.js(+1 formicrophoneDeviceId). Full suite (369 electron + existing vitest) + typecheck + build green.capture.jshas no unit tests, same as before this PR - it's browser-API-driven (getUserMedia,AudioContext) and was never covered.🤖 Generated with Claude Code