- 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.
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""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()
|