Show mute warning when recording with system audio muted#229
Conversation
When a user starts a dictation recording while their system audio output is muted, the start and stop chimes play silently with no indication why. This adds a brief "Unmute to hear sound" error toast that appears for 3 seconds after the recording overlay establishes, then auto-dismisses. Uses existing SystemAudioStatus.isDefaultOutputMuted() and showError() — no new views, state, or methods. The 0.3-second asyncAfter delay ensures the recording overlay has fully rendered before the mute notice overrides it. Only fires when alert sounds are enabled and the user is still recording (guarded by isRecording check in the delayed closure).
📝 WalkthroughWalkthroughThe PR adds detection for muted system audio output when recording begins. When the recording engine receives its first audio sample, the app now checks whether the system default output is muted and displays an auto-dismissing error overlay prompting the user to unmute, while still triggering the "Tink" alert sound. ChangesMuted Audio Output Detection
Possibly related PRs
Poem
🎯 1 (Trivial) | ⏱️ ~3 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/AppState.swift`:
- Around line 2146-2153: The call in AppState.swift uses
overlayManager.showError("Unmute to hear sound", dismissAfter: 3.0) but
RecordingOverlay only defines func showError(_ message: String); add an overload
func showError(_ message: String, dismissAfter: TimeInterval) to the
RecordingOverlay (and ensure overlayManager exposes that method) that mirrors
the existing showError behavior but uses the provided dismissAfter duration, or
alternatively change the AppState call to use the existing showError("Unmute to
hear sound") if you prefer the default 6.0s auto-dismiss.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1ef1f919-3778-40e2-a016-11e122968e7b
📒 Files selected for processing (1)
Sources/AppState.swift
| let isMuted = self.alertSoundsEnabled && SystemAudioStatus.isDefaultOutputMuted() | ||
| self.playAlertSound(named: "Tink") | ||
| if isMuted { | ||
| DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in | ||
| guard let self, self.isRecording else { return } | ||
| self.overlayManager.showError("Unmute to hear sound", dismissAfter: 3.0) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for showError method signatures in RecordingOverlayManager/RecordingOverlay files
rg -n -A3 'func showError' --type=swiftRepository: zachlatta/freeflow
Length of output: 759
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) All showError declarations"
rg -n --type=swift 'func\s+showError' Sources
echo "2) Any showError( calls with dismissAfter:"
rg -n --type=swift 'showError\s*\(.*dismissAfter' Sources
echo "3) Any showError call with two args (heuristic):"
rg -n --type=swift 'showError\s*\([^)]*,[^)]*\)' Sources | head -n 50Repository: zachlatta/freeflow
Length of output: 594
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) All showError declarations"
rg -n --type=swift 'func\s+showError' Sources
echo "2) Any showError( calls with dismissAfter:"
rg -n --type=swift 'showError\s*\(.*dismissAfter' Sources
echo "3) Any showError call with two args (heuristic):"
rg -n --type=swift 'showError\s*\([^)]*,[^)]*\)' Sources | head -n 50Repository: zachlatta/freeflow
Length of output: 594
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '160,240p' Sources/RecordingOverlay.swift | nl -baRepository: zachlatta/freeflow
Length of output: 105
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n Sources/RecordingOverlay.swift | sed -n '160,240p'Repository: zachlatta/freeflow
Length of output: 3540
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "A) Any dismissAfter label usage in Sources"
rg -n --type=swift 'dismissAfter\s*:' Sources
echo "B) Any showError declarations (broader pattern)"
rg -n --type=swift 'showError\s*\(' Sources
echo "C) Any protocol requirements mentioning showError"
rg -n --type=swift 'protocol\s+\w*.*\{[\s\S]*showError|showError\s*\{|\bshowError\b' SourcesRepository: zachlatta/freeflow
Length of output: 728
Fix missing showError(_:dismissAfter:) overload (current call likely won’t compile)
Sources/AppState.swift calls overlayManager.showError("Unmute to hear sound", dismissAfter: 3.0), but Sources/RecordingOverlay.swift only defines func showError(_ message: String) (auto-dismisses after 6.0). Add a showError(_:dismissAfter:) overload to RecordingOverlay/overlayManager, or change the call to showError("Unmute to hear sound").
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/AppState.swift` around lines 2146 - 2153, The call in AppState.swift
uses overlayManager.showError("Unmute to hear sound", dismissAfter: 3.0) but
RecordingOverlay only defines func showError(_ message: String); add an overload
func showError(_ message: String, dismissAfter: TimeInterval) to the
RecordingOverlay (and ensure overlayManager exposes that method) that mirrors
the existing showError behavior but uses the provided dismissAfter duration, or
alternatively change the AppState call to use the existing showError("Unmute to
hear sound") if you prefer the default 6.0s auto-dismiss.
|
Can we add a toggle in the settings for this? I use the audio alerts but often have my laptop on mute |
|
I don’t mind…you got the change or want me to?On Jun 12, 2026, at 3:32 AM, marcbodea ***@***.***> wrote:marcbodea left a comment (zachlatta/freeflow#229)
Can we add a toggle in the settings for this? I use the audio alerts but often have my laptop on mute
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: ***@***.***>
|
Summary
When a dictation recording starts while the system audio output is muted, the start and stop chimes play silently — there is no indication that sounds are being missed. This can be disorienting, especially for users who rely on the audio cues to know when recording has started or finished.
This adds a brief red error toast — "Unmute to hear sound" — that appears for 3 seconds after the recording overlay establishes, then auto-dismisses back to normal operation. Recording is not interrupted.
How it works
In
onRecordingReady, after the recording overlay has rendered and the start sound has played, the system mute state is checked via the existingSystemAudioStatus.isDefaultOutputMuted(). If muted and alert sounds are enabled,showError("Unmute to hear sound", dismissAfter: 3.0)fires after a 0.3-second delay.The delay is intentional —
transitionToRecording()dispatches toDispatchQueue.main.async, and the error toast must run after that block completes to avoid being overwritten. A 0.3-secondasyncAfterguarantees correct ordering.Implementation
SystemAudioStatus.isDefaultOutputMuted()(already used by the audio interruption feature)showError(_:dismissAfter:)(already used for other transient error toasts)alertSoundsEnabledis true — no point warning about muted audio when sounds are turned off in settingsNotes / Considered
showErrorinstead of a custom view? A dedicated amber warning view was prototyped butshowErroris simpler (zero new code surface) and the red styling is more attention-grabbing for something the user should act on.DispatchQueue.main.async. Without the delay, the mute toast and the recording overlay race — the recording overlay can stomp the toast before it renders. 0.3 seconds is long enough for the overlay to fully establish but short enough to feel immediate.beginRecording(before the recording overlay renders) was tried first, but the 200ms initializing timer and theonRecordingReadycallback both override the overlay phase, stomping the mute warning before it can render. Moving it toonRecordingReadywith a short delay after the overlay is established was the only reliable placement.isDefaultOutputMuted()checks the CoreAudio mute property specifically. A volume slider dragged to zero without pressing the mute button will not trigger this warning. This is intentional — the mute button is a deliberate action; a low volume setting is a preference.showErrorlayout, which already accounts for notched and non-notched displays.self.isRecordingbefore showing the toast. If a user stops recording within that 0.3-second window, the guard bails and no toast appears. A token-based invalidation pattern was considered but theisRecordingcheck is sufficient — the worst case (a brief toast after a very fast stop) auto-dismisses in 3 seconds.Testing performed
Tested across 5 builds on a MacBook Pro (macOS, built-in speakers as default output):
Instrumentation used during development:
~/freeflow-mute-debug.log) confirmedSystemAudioStatus.isDefaultOutputMuted()correctly returnstruewhen the keyboard mute button is pressed andfalsewhen unmuted. Volume level is preserved (0.5) in both states — the mute property is independent of the volume slider.onRecordingReady→ mute check →scheduling showError in 0.3s→firing showError now.Dispatch ordering verified: Earlier attempts placed the mute check in
beginRecording(immediate on key press) and inonRecordingReadywithout a delay. Both were stomped bytransitionToRecording()'s async dispatch overriding the overlay phase. The 0.3-secondasyncAfterwas the minimum reliable delay.Test plan for reviewer
Summary by CodeRabbit