Skip to content

Comments

expose audio output device selection in preferences#464

Merged
nschimme merged 1 commit intoMUME:masterfrom
nschimme:audio
Feb 24, 2026
Merged

expose audio output device selection in preferences#464
nschimme merged 1 commit intoMUME:masterfrom
nschimme:audio

Conversation

@nschimme
Copy link
Contributor

@nschimme nschimme commented Feb 24, 2026

Summary by Sourcery

Add configurable audio output device selection and propagate it through configuration, UI, and audio managers.

New Features:

  • Expose audio output device selection in the audio preferences UI, including persistence of the chosen device.
  • Apply the configured audio output device to both music and sound effects playback, with automatic fallback to the system default device and handling of missing devices.

Enhancements:

  • Refresh the list of available audio output devices when system audio outputs change and initialize it on startup.
  • Guard audio-device-related logic behind existing no-audio compile-time flags and prevent interaction with device selection when audio is disabled.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 24, 2026

Reviewer's Guide

Implements configurable audio output device selection: the preferences UI now exposes an output device combo box whose selection is persisted in configuration and used to set the QAudioOutput device for both music and sound effects, with automatic updates when configuration or available devices change.

Sequence diagram for updating audio output device from preferences to audio managers

sequenceDiagram
    actor User
    participant AudioPage
    participant Configuration
    participant AudioSettings
    participant ChangeMonitor
    participant AudioManager
    participant MusicManager
    participant SfxManager
    participant QMediaDevices

    User->>AudioPage: Change outputDeviceComboBox selection
    AudioPage->>AudioPage: slot_outputDeviceChanged(index)
    AudioPage->>AudioSettings: setOutputDeviceId(selectedDeviceId)
    AudioSettings->>ChangeMonitor: notifyAll()

    ChangeMonitor->>AudioManager: onConfigurationChanged()
    AudioManager->>AudioManager: updateOutputDevices()

    alt DeviceId_is_empty
        AudioManager->>QMediaDevices: defaultAudioOutput()
        QMediaDevices-->>AudioManager: targetDevice
    else Specific_device_selected
        AudioManager->>QMediaDevices: audioOutputs()
        QMediaDevices-->>AudioManager: devices
        AudioManager->>AudioManager: Find_device_matching_deviceId
        AudioManager->>AudioManager: targetDevice = matching_or_default
    end

    AudioManager->>MusicManager: updateOutputDevice(targetDevice)
    AudioManager->>SfxManager: updateOutputDevice(targetDevice)

    MusicManager->>MusicManager: Set_device_on_all_channels
    SfxManager->>SfxManager: Set_device_on_output

    Note over AudioManager,QMediaDevices: QMediaDevices.audioOutputsChanged also triggers updateOutputDevices()
Loading

Class diagram for configurable audio output device selection

classDiagram
    class Configuration {
    }

    class AudioSettings {
        -ChangeMonitor m_changeMonitor
        -int m_musicVolume
        -int m_soundVolume
        -QByteArray m_outputDeviceId
        -bool m_unlocked
        +void read(QSettings &conf)
        +void write(QSettings &conf) const
        +int getMusicVolume() const
        +void setMusicVolume(int value)
        +int getSoundVolume() const
        +void setSoundVolume(int value)
        +const QByteArray &getOutputDeviceId() const
        +void setOutputDeviceId(const QByteArray &id)
        +bool isUnlocked() const
        +void setUnlocked()
    }

    class AudioManager {
        -MusicManager *m_music
        -SfxManager *m_sfx
        +AudioManager(MediaLibrary &library, GameObserver &observer, QObject *parent)
        +~AudioManager()
        +void updateVolumes()
        +void updateOutputDevices()
    }

    class MusicManager {
        +MusicManager(MediaLibrary &library, QObject *parent)
        +void playMusic(const QString &musicFile)
        +void stopMusic()
        +void updateVolumes()
        +void slot_onMediaChanged()
        +void updateOutputDevice(const QAudioDevice &device)
    }

    class SfxManager {
        +SfxManager(MediaLibrary &library, QObject *parent)
        +void playSound(const QString &soundName)
        +void updateVolume()
        +void updateOutputDevice(const QAudioDevice &device)
    }

    class AudioPage {
        +AudioPage(QWidget *parent)
        +~AudioPage()
        +void slot_loadConfig()
        +void slot_musicVolumeChanged(int value)
        +void slot_soundsVolumeChanged(int value)
        +void slot_outputDeviceChanged(int index)
        +void slot_updateDevices()
    }

    class QMediaDevices {
        +static QList~QAudioDevice~ audioOutputs()
        +static QAudioDevice defaultAudioOutput()
        +signal audioOutputsChanged()
    }

    class QAudioDevice {
        +QByteArray id() const
        +QString description() const
    }

    class QAudioOutput {
        +void setDevice(const QAudioDevice &device)
        +QAudioDevice device() const
    }

    Configuration *-- AudioSettings : contains
    AudioManager --> Configuration : reads_writes_audio_settings
    AudioManager *-- MusicManager : owns
    AudioManager *-- SfxManager : owns
    AudioPage --> Configuration : loads_and_saves_AudioSettings
    AudioManager --> QMediaDevices : queries_outputs
    MusicManager --> QAudioOutput : uses
    SfxManager --> QAudioOutput : uses
    QAudioOutput --> QAudioDevice : current_device
    AudioPage --> QMediaDevices : populates_device_list
    AudioSettings --> QAudioDevice : stores_id_of
Loading

File-Level Changes

Change Details Files
Expose audio output device selection in the audio preferences UI and keep it in sync with configuration and device availability.
  • Initialize the audio output device combo box on page construction and update it when audio output devices change using QMediaDevices::audioOutputsChanged.
  • Populate the combo box with a System Default entry plus available QAudioDevice outputs, preserving the current selection and adding a placeholder if a previously selected device is missing.
  • Load and save the selected device id to configuration via SignalBlocker-wrapped UI updates to avoid spurious signal handling, including a disabled state when audio is compiled out.
src/preferences/audiopage.cpp
src/preferences/audiopage.h
src/preferences/audiopage.ui
src/global/SignalBlocker.h
Persist the selected audio output device id as part of audio configuration.
  • Add an m_outputDeviceId field to AudioSettings with getter/setter that notifies the change monitor.
  • Introduce a KEY_AUDIO_OUTPUT_DEVICE setting key and read/write the device id in QSettings alongside existing volume settings.
src/configuration/configuration.h
src/configuration/configuration.cpp
Apply the selected audio output device to both music and sound effects managers and keep it updated when configuration or devices change.
  • Add AudioManager::updateOutputDevices, invoked on construction and whenever audio configuration or QMediaDevices::audioOutputsChanged fires, to resolve the target QAudioDevice (either system default or the configured id) and propagate it.
  • Extend MusicManager and SfxManager with updateOutputDevice methods that set QAudioOutput::setDevice when the device differs, covering both music channels and the shared SFX output.
  • Ensure audio managers include QAudioDevice/QMediaDevices headers behind MMAPPER_NO_AUDIO guards and slightly adjust constructor initialization order by calling updateOutputDevices and updateVolumes.
src/media/AudioManager.cpp
src/media/AudioManager.h
src/media/MusicManager.cpp
src/media/MusicManager.h
src/media/SfxManager.cpp
src/media/SfxManager.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • You’re storing QAudioDevice::id() (a QByteArray) in the combo box and AudioSettings as a QString, then comparing it back to device.id(); consider keeping this consistently as QByteArray (e.g., store/compare QByteArray or use QVariant with QByteArray) to avoid subtle issues with implicit conversions or non-text IDs.
  • There are two different audio-disabling macros in use (NO_AUDIO and MMAPPER_NO_AUDIO); it would be safer to standardize on one of these in the new code paths so that the UI enablement and the compile-time audio guards cannot diverge.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- You’re storing `QAudioDevice::id()` (a `QByteArray`) in the combo box and `AudioSettings` as a `QString`, then comparing it back to `device.id()`; consider keeping this consistently as `QByteArray` (e.g., store/compare `QByteArray` or use `QVariant` with `QByteArray`) to avoid subtle issues with implicit conversions or non-text IDs.
- There are two different audio-disabling macros in use (`NO_AUDIO` and `MMAPPER_NO_AUDIO`); it would be safer to standardize on one of these in the new code paths so that the UI enablement and the compile-time audio guards cannot diverge.

## Individual Comments

### Comment 1
<location path="src/preferences/audiopage.cpp" line_range="92-101" />
<code_context>
+
+void AudioPage::slot_updateDevices()
+{
+    ui->outputDeviceComboBox->blockSignals(true);
+    QString currentId = ui->outputDeviceComboBox->currentData().toString();
+    if (currentId.isEmpty()) {
+        currentId = getConfig().audio.getOutputDeviceId();
+    }
+
+    ui->outputDeviceComboBox->clear();
+    ui->outputDeviceComboBox->addItem(tr("System Default"), QString());
+
+#ifndef MMAPPER_NO_AUDIO
+    const QList<QAudioDevice> devices = QMediaDevices::audioOutputs();
+    for (const QAudioDevice &device : devices) {
+        ui->outputDeviceComboBox->addItem(device.description(), device.id());
+    }
+#endif
+
+    int index = ui->outputDeviceComboBox->findData(currentId);
+    if (index != -1) {
+        ui->outputDeviceComboBox->setCurrentIndex(index);
+    } else {
+        ui->outputDeviceComboBox->setCurrentIndex(0);
+    }
+    ui->outputDeviceComboBox->blockSignals(false);
+}
</code_context>
<issue_to_address>
**suggestion:** Using RAII for blocking signals would make this more robust against early returns.

This is safe as written, but fragile if future edits add early returns between the two calls. Prefer `QSignalBlocker blocker(ui->outputDeviceComboBox);` to make this robust and exception-safe while keeping intent clear.

Suggested implementation:

```cpp
void AudioPage::slot_updateDevices()
{
    QSignalBlocker blocker(ui->outputDeviceComboBox);

    const auto &settings = getConfig().audio;
    ui->musicVolumeSlider->setValue(settings.getMusicVolume());
    ui->soundsVolumeSlider->setValue(settings.getSoundVolume());

    QString currentId = ui->outputDeviceComboBox->currentData().toString();
    if (currentId.isEmpty()) {
        currentId = settings.getOutputDeviceId();
    }

    ui->outputDeviceComboBox->clear();
    ui->outputDeviceComboBox->addItem(tr("System Default"), QString());

#ifndef MMAPPER_NO_AUDIO
    const QList<QAudioDevice> devices = QMediaDevices::audioOutputs();
    for (const QAudioDevice &device : devices) {
        ui->outputDeviceComboBox->addItem(device.description(), device.id());
    }
#endif

    int index = ui->outputDeviceComboBox->findData(currentId);
    if (index != -1) {
        ui->outputDeviceComboBox->setCurrentIndex(index);
    } else {
        ui->outputDeviceComboBox->setCurrentIndex(0);
    }
}

```

To compile successfully, ensure the following headers are included at the top of `src/preferences/audiopage.cpp` (or in the corresponding header if that's where other Qt includes live):

1. `#include <QSignalBlocker>`
2. If not already present for the device enumeration:
   - `#include <QMediaDevices>`
   - `#include <QAudioDevice>`

Place these alongside the existing Qt includes to match the project's include style.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 1.81818% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.58%. Comparing base (26d1a9d) to head (46d27ca).
⚠️ Report is 205 commits behind head on master.

Files with missing lines Patch % Lines
src/preferences/audiopage.cpp 0.00% 28 Missing ⚠️
src/media/AudioManager.cpp 0.00% 14 Missing ⚠️
src/configuration/configuration.h 0.00% 4 Missing ⚠️
src/media/MusicManager.cpp 0.00% 4 Missing ⚠️
src/media/SfxManager.cpp 0.00% 3 Missing ⚠️
src/configuration/configuration.cpp 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #464       +/-   ##
===========================================
- Coverage   66.48%   25.58%   -40.90%     
===========================================
  Files          85      500      +415     
  Lines        4186    41178    +36992     
  Branches      255     4479     +4224     
===========================================
+ Hits         2783    10537     +7754     
- Misses       1403    30641    +29238     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nschimme
Copy link
Contributor Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/media/MusicManager.cpp" line_range="213-216" />
<code_context>
 }

 #ifndef MMAPPER_NO_AUDIO
+void MusicManager::updateOutputDevice(const QAudioDevice &device)
+{
+    for (int i = 0; i < 2; ++i) {
+        if (m_channels[i].audioOutput->device() != device) {
+            m_channels[i].audioOutput->setDevice(device);
+        }
</code_context>
<issue_to_address>
**suggestion:** updateOutputDevice assumes exactly two channels via a magic number

The loop is hard-coded to `for (int i = 0; i < 2; ++i)`, which couples this function to the current channel count. If `m_channels` ever changes size, this will silently misbehave. Please iterate over `std::size(m_channels)` or a named constant representing the channel count instead of using the literal `2`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +213 to +216
void MusicManager::updateOutputDevice(const QAudioDevice &device)
{
for (int i = 0; i < 2; ++i) {
if (m_channels[i].audioOutput->device() != device) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: updateOutputDevice assumes exactly two channels via a magic number

The loop is hard-coded to for (int i = 0; i < 2; ++i), which couples this function to the current channel count. If m_channels ever changes size, this will silently misbehave. Please iterate over std::size(m_channels) or a named constant representing the channel count instead of using the literal 2.

@nschimme nschimme merged commit 31c6591 into MUME:master Feb 24, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant