Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions agent-notes/improve-audio-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Goal

Correct the existing audio settings panel without changing product call sites.

# State

Isolated branch based on improve/integration-contract (PR #1). Output and input
population share one small private method. Tests cover configuration defaults,
unavailable equipment, selection fallback, saving and preservation.

# Decisions and why

Use the framework's existing configuration accessor. Disabled empty controls
cannot save a nonexistent choice. Failed input initialization (sound_input=None)
does not prevent output settings from working. Register this module's existing
labels in the app_elements translation domain. Device initialization and applying
settings remain owned by app_framework and products.

# Surprises

sound_lib output enumeration can return no items; input initialization can fail
while the manifest recording flag remains true. Both occur in real source paths.

# Next step

Publish the focused PR and read its CI result. Tests double native/service
boundaries; no live audio, native wx or screen-reader result claimed. No merge.
44 changes: 26 additions & 18 deletions app_elements/audio.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import app_framework.helpers
from app_framework.configuration import get_configuration_value
import i18n_core
import sys
from gui_builder import fields
from wx_utils import forms as wx_forms

import app_elements

i18n_core.install_module_translation("app_elements", module=sys.modules[__name__])
application = app_elements.find_application_module()


Expand All @@ -14,28 +18,32 @@ class DeviceSelectionPanel(wx_forms.AutoSizedPanel):

def render(self, *args, **kwargs):
super(DeviceSelectionPanel, self).render(*args, **kwargs)
sound_devices = application.sound_output.get_device_names()
self.output_device.set_items(sound_devices)
current_device = application.config["audio"]["device"]
if current_device in sound_devices:
self.output_device.set_index_to_item(current_device)
elif "Default" in sound_devices:
self.output_device.set_index_to_item("Default")
else:
self.output_device.set_index(0)
if not app_framework.helpers.has_sound_recording(application):
self._render_device(self.output_device, getattr(application, "sound_output", None), "device")
if app_framework.helpers.has_sound_recording(application):
self._render_device(self.input_device, getattr(application, "sound_input", None), "input_device")

def _render_device(self, field, device, config_key):
sound_devices = device.get_device_names() if device is not None else []
field.set_items(sound_devices)
field.set_enabled(bool(sound_devices))
if not sound_devices:
return
sound_devices = application.sound_input.get_device_names()
self.input_device.set_items(sound_devices)
current_device = application.config["audio"]["input_device"]
current_device = get_configuration_value(application.config, ["audio", config_key], "Default")
if current_device in sound_devices:
self.input_device.set_index_to_item(current_device)
field.set_index_to_item(current_device)
elif "Default" in sound_devices:
self.input_device.set_index_to_item("Default")
field.set_index_to_item("Default")
else:
self.input_device.set_index(0)
field.set_index(0)

def set_config_values(self):
application.config["audio"]["device"] = self.output_device.get_choice()
fields_to_save = [(self.output_device, "device")]
if app_framework.helpers.has_sound_recording(application):
application.config["audio"]["input_device"] = self.input_device.get_choice()
fields_to_save.append((self.input_device, "input_device"))
for field, config_key in fields_to_save:
choice = field.get_choice()
if choice is None or choice not in field.get_items():
continue
if "audio" not in application.config:
application.config["audio"] = {}
application.config["audio"][config_key] = choice
77 changes: 77 additions & 0 deletions tests/test_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import unittest
from unittest.mock import Mock

from support import load_component


class AudioSelectionTests(unittest.TestCase):
def test_missing_section_uses_framework_default_and_saves_valid_choice(self):
output = Mock(get_device_names=Mock(return_value=["Speakers", "Default"]))
with load_component("app_elements.audio", sound_output=output) as (component, app, modules):
panel = component.DeviceSelectionPanel()
panel.render()
modules["app_framework.configuration"].get_configuration_value.assert_called_once_with(
app.config, ["audio", "device"], "Default"
)
panel.output_device.set_index_to_item.assert_called_once_with("Default")
panel.output_device.get_choice.return_value = "Speakers"
panel.output_device.get_items.return_value = ["Speakers", "Default"]
panel.set_config_values()
self.assertEqual(app.config, {"audio": {"device": "Speakers"}})

def test_configured_device_is_selected_before_default(self):
output = Mock(get_device_names=Mock(return_value=["Default", "USB Headset"]))
with load_component("app_elements.audio", sound_output=output) as (component, _, modules):
modules["app_framework.configuration"].get_configuration_value.return_value = "USB Headset"
panel = component.DeviceSelectionPanel()
panel.render()
panel.output_device.set_index_to_item.assert_called_once_with("USB Headset")

def test_missing_device_falls_back_to_first_when_no_default_is_enumerated(self):
output = Mock(get_device_names=Mock(return_value=["Speakers"]))
with load_component("app_elements.audio", sound_output=output) as (component, _, _):
panel = component.DeviceSelectionPanel()
panel.render()
panel.output_device.set_index.assert_called_once_with(0)

def test_no_outputs_or_failed_input_initialization_preserves_configuration(self):
config = {"audio": {"device": "Disconnected", "input_device": "Microphone"}}
output = Mock(get_device_names=Mock(return_value=[]))
with load_component("app_elements.audio", sound_output=output, sound_input=None,
record_sound=True, config=config) as (component, app, _):
panel = component.DeviceSelectionPanel()
panel.render()
for field in (panel.output_device, panel.input_device):
field.set_items.assert_called_once_with([])
field.set_enabled.assert_called_once_with(False)
field.set_index.assert_not_called()
field.get_choice.return_value = None
field.get_items.return_value = []
panel.set_config_values()
self.assertEqual(app.config, config)
self.assertEqual(app.config["audio"]["device"], "Disconnected")
self.assertEqual(app.config["audio"]["input_device"], "Microphone")

def test_recording_choice_saved_without_overwriting_other_audio_settings(self):
output = Mock(get_device_names=Mock(return_value=["Default"]))
input_device = Mock(get_device_names=Mock(return_value=["Default", "Mic"]))
with load_component("app_elements.audio", sound_output=output, sound_input=input_device,
record_sound=True, config={"audio": {"global_volume": 40}}) as (component, app, _):
panel = component.DeviceSelectionPanel()
panel.render()
panel.output_device.get_choice.return_value = "Default"
panel.output_device.get_items.return_value = ["Default"]
panel.input_device.get_choice.return_value = "Mic"
panel.input_device.get_items.return_value = ["Default", "Mic"]
panel.set_config_values()
self.assertEqual(app.config["audio"], {
"global_volume": 40, "device": "Default", "input_device": "Mic"
})

def test_invalid_selection_cannot_replace_an_existing_device(self):
with load_component("app_elements.audio", config={"audio": {"device": "Saved"}}) as (component, app, _):
panel = component.DeviceSelectionPanel()
panel.output_device.get_choice.return_value = "Stale"
panel.output_device.get_items.return_value = []
panel.set_config_values()
self.assertEqual(app.config["audio"]["device"], "Saved")
Loading