Skip to content

Add selectable audio inputs - #31

Merged
sri-rang merged 12 commits into
mainfrom
feature/issue-30-audio-input
Aug 7, 2026
Merged

Add selectable audio inputs#31
sri-rang merged 12 commits into
mainfrom
feature/issue-30-audio-input

Conversation

@sri-rang

@sri-rang sri-rang commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add persistent selection of a specific microphone, the system default, or no audio input
  • keep device availability, dictation actions, notifications, and live input-level monitoring synchronized
  • guard capture callbacks against stale device signals during monitoring restarts
  • add deterministic controller and QML coverage for selection, hot-plug recovery, monitoring failures, accessibility, and stable layout

Closes #30

Validation

  • make lint && make test — passed (7/7 tests)
  • env QT_MEDIA_BACKEND=gstreamer ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 make test BUILD_DIR=build-pr-sanitizers BUILD_TYPE=Debug CMAKE_ARGS="-DKASTWORD_ENABLE_SANITIZERS=ON -DKASTWORD_FETCH_DEFAULT_MODEL=OFF" — passed (7/7 tests)
  • make coverage BUILD_DIR=build-pr-coverage BUILD_TYPE=Debug CMAKE_ARGS="-DKASTWORD_FETCH_DEFAULT_MODEL=OFF" — passed (81.8% lines, 70.0% branches)
  • make install-smoke — passed
  • make validate — passed (a preceding run hit an intermittent existing ModelManager test failure; the isolated test and final complete rerun passed)

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add selectable, persistent audio input selection with live monitoring

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add persistent microphone selection: None, system default, or a specific device.
• Keep dictation availability, status text, tray actions, and notifications in sync.
• Add audio input UI with live level monitoring, hot-plug handling, and retryable errors.
Diagram

graph TD
QML["QML: Audio Input page"] --> CTRL["AppController"] --> CAP["AudioCapture"] --> MEDIA["Qt Multimedia devices"]
CTRL --> CFG[("KConfig settings")]
CTRL --> TRAY["System tray UI"]
MEDIA -->|"hot-plug"| CAP
CAP -->|"level/inputs/errors"| CTRL
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist “best-effort” selection (fallback to default on disconnect)
  • ➕ Fewer blocked states; dictation keeps working when a specific mic disappears
  • ➕ Less UI prompting/notifications during hot-unplug scenarios
  • ➖ Violates the PR’s explicit privacy/intent guarantee (never silently switch devices)
  • ➖ Can surprise users by recording from an unintended microphone
2. Store a richer device key (backend name + id + description) and attempt rebind
  • ➕ More robust across backend changes where raw device ids may not be stable
  • ➕ Allows matching the same physical device after re-enumeration
  • ➖ More complexity and edge cases (conflicts, partial matches, localization)
  • ➖ Still cannot guarantee stable identity across all platforms/backends

Recommendation: Current approach (explicit None/default/specific selection with no silent fallback) is the safest UX for privacy and predictability, and it aligns with the feature intent. The base64-encoded backend id is a reasonable persistence mechanism; if stability issues appear across backends, consider augmenting the persisted key with optional metadata for best-effort rebind while still requiring user confirmation before switching devices.

Files changed (14) +1366 / -250

Enhancement (8) +490 / -46
AppController.cppAdd persistent audio input selection and synchronize dictation availability +107/-5

Add persistent audio input selection and synchronize dictation availability

• Reads/writes the selected AudioInputId setting, exposes audio input list/status, and gates dictation start on both model and audio readiness. Adds handling for hot-plug updates, monitoring error propagation, and user-facing notifications when audio input is disabled/unavailable.

src/AppController.cpp

AppController.hExpose audio input properties/actions to QML and tray integration +28/-0

Expose audio input properties/actions to QML and tray integration

• Adds QML-facing properties for audioInputs, selected audioInputId, readiness/status, monitoring error, selection enablement, and dictationActionEnabled. Adds invokables to toggle monitoring and retry monitoring, plus new signals for availability and setup requests.

src/AppController.h

AudioCapture.cppImplement selectable device resolution, hot-plug awareness, and monitoring mode +144/-11

Implement selectable device resolution, hot-plug awareness, and monitoring mode

• Introduces device id encoding, device enumeration, and selected device resolution (None/default/specific). Adds a monitoring mode that emits level readings without buffering audio, restarts on hot-plug, pauses during recording, and surfaces monitoring errors separately from capture failures.

src/AudioCapture.cpp

AudioCapture.hExtend AudioCapture API for device selection and monitoring +33/-1

Extend AudioCapture API for device selection and monitoring

• Adds AudioInputDevice struct, selection APIs (selectedDeviceId/noDeviceId/availability), monitoring enablement and error reporting, and new signals for inputs/monitoring changes.

src/AudioCapture.h

PlatformIntegration.cppDrive tray presentation from overall dictation action availability +4/-4

Drive tray presentation from overall dictation action availability

• Renames the trayPresentation parameter from modelReady to actionEnabled so the tray action reflects combined availability (model + audio input + state).

src/PlatformIntegration.cpp

PlatformIntegration.hUpdate trayPresentation signature to accept actionEnabled +1/-1

Update trayPresentation signature to accept actionEnabled

• Adjusts the helper API to match the new semantics used by main/tray wiring.

src/PlatformIntegration.h

main.cppSynchronize tray action enablement with dictationActionEnabled and open audio setup +19/-17

Synchronize tray action enablement with dictationActionEnabled and open audio setup

• Centralizes tray presentation updates and listens to dictationAvailabilityChanged. Triggers window activation for audioInputSetupRequested and adjusts initial “ready” tray message to use dictationActionEnabled.

src/main.cpp

Main.qmlAdd Audio Input page with selectable list, monitoring UI, and stable navigation +154/-7

Add Audio Input page with selectable list, monitoring UI, and stable navigation

• Adds a dedicated Audio input page/tab listing inputs (None/default/devices), status messages, retryable monitoring errors, and a live level meter with privacy note. Enables/pauses monitoring based on window visibility/current view and updates navigation sizing and view indices.

src/qml/Main.qml

Tests (3) +559 / -7
AppControllerTest.cppAdd unit coverage for audio input selection, persistence, hot-plug, and monitoring +298/-5

Add unit coverage for audio input selection, persistence, hot-plug, and monitoring

• Extends the fake audio capture to model device lists/defaults/availability and monitoring. Adds tests for selection persistence, system-default changes, no-silent-fallback recovery, monitoring toggles, and monitoring pause/retry behavior, plus tray-presentation semantics changes.

tests/AppControllerTest.cpp

QmlTestSetup.cppExtend QML test controller stub with audio input + monitoring behavior +99/-0

Extend QML test controller stub with audio input + monitoring behavior

• Adds QML-exposed properties and invokables for audio inputs, readiness/status, monitoring enabled/error, retry counting, and setup requests so UI tests can validate the new page behavior.

tests/QmlTestSetup.cpp

tst_Main.qmlAdd QML tests for audio input page, monitoring lifecycle, accessibility, and layout stability +162/-2

Add QML tests for audio input page, monitoring lifecycle, accessibility, and layout stability

• Adds coverage for opening audio setup, selecting devices including None, handling unavailable selections, enabling/disabling monitoring on view/visibility changes, retrying monitoring failures, locking selection while busy, and ensuring dictation stop remains enabled during device loss.

tests/qml/tst_Main.qml

Documentation (2) +315 / -197
README.mdDocument selectable audio input behavior and update roadmap +7/-4

Document selectable audio input behavior and update roadmap

• Updates the feature list and usage documentation to describe audio input selection (None/default/specific) and the no-silent-fallback behavior. Adjusts roadmap wording to focus on broader audio-device/hot-plug testing.

README.md

kastword.poRefresh translation catalog for new audio input strings +308/-193

Refresh translation catalog for new audio input strings

• Updates POT timestamp, message locations, and adds new UI/status strings for audio input selection, availability, and monitoring failures.

po/x-test/kastword.po

Other (1) +2 / -0
ci.ymlInstall GStreamer multimedia backend and force it for sanitizer tests +2/-0

Install GStreamer multimedia backend and force it for sanitizer tests

• Adds the qt6-multimedia-gstreamer package to the Arch CI image and sets QT_MEDIA_BACKEND=gstreamer for ASAN/UBSAN runs to stabilize Qt Multimedia behavior under sanitizers.

.github/workflows/ci.yml

@qodo-code-review

qodo-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

Action required

1. Stale QIODevice readyRead pointer ✓ Resolved 🐞 Bug ☼ Reliability
Description
AudioCapture's QIODevice::readyRead handler dereferences the mutable member m_device, but
stopSource() sets m_device=nullptr while the readyRead connection may still fire (especially with
the new monitoring + hot-plug restarts calling stopSource()). This can crash (nullptr deref) or
process data from the wrong device if a stale readyRead arrives after monitoring restarts with a new
m_device.
Code

src/AudioCapture.cpp[R265-268]

  m_source->stop();
  m_device = nullptr;
  m_source.reset();
+  m_monitoring = false;
Evidence
Hot-plug recovery now calls stopSource() while monitoring is active, and stopSource() nulls
m_device. The readyRead handler uses m_device without a null check or sender validation, so a
delayed/queued readyRead can dereference nullptr or operate on the wrong device after a restart.

src/AudioCapture.cpp[51-58]
src/AudioCapture.cpp[233-249]
src/AudioCapture.cpp[262-270]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AudioCapture` connects `QIODevice::readyRead` with a lambda that reads via the mutable member `m_device`. New monitoring/hot-plug logic calls `stopSource()` (which sets `m_device = nullptr` and tears down the backend), creating a window where a late/queued/re-entrant `readyRead` can run and dereference `m_device` (nullptr) or read from a newly-assigned device.

### Issue Context
This PR adds monitoring and hot-plug recovery. Monitoring makes `readyRead` fire frequently, and hot-plug changes now call `stopSource()` from `QMediaDevices::audioInputsChanged`, increasing the likelihood of stale `readyRead` deliveries during teardown/restart.

### Fix Focus Areas
- src/AudioCapture.cpp[51-58]
- src/AudioCapture.cpp[233-249]
- src/AudioCapture.cpp[262-270]

### What to change
- In the `readyRead` lambda, do not dereference `m_device` directly.
 - Prefer reading from the sender device (e.g., `auto *device = qobject_cast<QIODevice*>(sender());`) or capture a stable `QPointer<QIODevice>` at connect time.
 - Add a guard to ignore stale signals (e.g., if `device != m_device` return) so an old device can’t drain/affect a new monitoring/recording session.
- Optionally, store the `QMetaObject::Connection` for the device and explicitly disconnect it in `stopSource()` before nulling `m_device`.

### Acceptance criteria
- No crash is possible if `stopSource()` runs while `readyRead` is pending.
- A `readyRead` from an old device cannot read/drain data from a newly started device session.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Context used
✅ Compliance rules (platform): 20 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Comment thread src/AudioCapture.cpp
@qodo-code-review

Copy link
Copy Markdown

PR approved by Qodo

All merge criteria satisfied — approved by default policy

@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (1)

Grey Divider

🔗 Fix PR: #32

This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR.

Prompt for coding agent

This is an automated fix prepared on a separate branch (#32). It is NOT applied to this PR.
To use it: review Fix PR #32 (https://github.com/Shape-Machine/Kastword/pull/32), evaluate each change critically against your local context, and cherry-pick the changes that are correct into this branch. Do not accept them blindly.
Process — 1 fixed
  • ☑ Fixed: Stale QIODevice readyRead pointer

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit dc3a631

@sri-rang
sri-rang merged commit 320ca35 into main Aug 7, 2026
2 checks passed
@sri-rang
sri-rang deleted the feature/issue-30-audio-input branch August 7, 2026 19:09
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.

Let users choose and recover microphone input

1 participant