Add microphone picker and device management features

- Implement `list_input_devices` to retrieve and filter input-capable audio devices.
- Create `MicPickerDialog` for selecting and saving microphone settings.
- Add `set_env_value` function to update .env file with selected microphone index.
- Update UI components to integrate microphone selection functionality.
- Enhance tests for microphone device handling and environment variable management.
This commit is contained in:
2026-09-24 23:31:18 -06:00
parent ad9925da02
commit 54030a7b31
8 changed files with 283 additions and 245 deletions
+98
View File
@@ -0,0 +1,98 @@
"""Microphone picker — MIC_DEVICE in .env is a device index most people have
no way to look up short of running a script. This dialog lists whatever
sounddevice can actually see, lets you click one, and writes its index back
to .env so it survives a restart.
The index, not the name, is what gets saved: the same device is commonly
listed once per host API (MME, DirectSound, WASAPI, WDM-KS, ...) under the
same or a near-identical name, which makes a bare name an ambiguous pick that
sounddevice will flatly refuse to resolve (see audio/mic.list_input_devices).
The index is exact.
The mic is only ever opened once, at startup (see controller.run()), so a
pick made here still needs a restart before Bolt actually uses it — this
dialog just makes finding and saving the right device painless, and offers
to do the restart for you.
"""
from __future__ import annotations
from typing import Callable, Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QListWidget,
QListWidgetItem, QPushButton, QVBoxLayout,
)
from ..audio.mic import list_input_devices
_DEFAULT_LABEL = "System default"
class MicPickerDialog(QDialog):
def __init__(
self,
get_current: Callable[[], Optional[int]],
on_save: Callable[[Optional[int]], None],
):
super().__init__()
self._get_current = get_current
self._on_save = on_save
self.setWindowTitle("Bolt — select microphone")
self.resize(440, 320)
self._list = QListWidget()
refresh_button = QPushButton("Refresh")
refresh_button.clicked.connect(self.refresh)
top = QHBoxLayout()
top.addWidget(QLabel("Pick the microphone Bolt should listen on:"))
top.addStretch(1)
top.addWidget(refresh_button)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._save)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(top)
layout.addWidget(self._list)
layout.addWidget(buttons)
def refresh(self) -> None:
self._list.clear()
default_item = QListWidgetItem(_DEFAULT_LABEL)
default_item.setData(Qt.UserRole, None)
self._list.addItem(default_item)
current = self._get_current()
selected = default_item
try:
devices = list_input_devices()
except Exception as exc:
self._list.addItem(f"Could not list devices: {exc}")
devices = []
for device in devices:
item = QListWidgetItem(
f"{device['name']} [{device['hostapi']}] (#{device['index']})"
)
item.setData(Qt.UserRole, device["index"])
self._list.addItem(item)
if current == device["index"]:
selected = item
self._list.setCurrentItem(selected)
def _save(self) -> None:
item = self._list.currentItem()
value = item.data(Qt.UserRole) if item is not None else None
self._on_save(value)
self.accept()
def show_refreshed(self) -> None:
self.refresh()
self.show()
self.raise_()
self.activateWindow()