Skip to content

Show mute warning when recording with system audio muted#229

Open
ojhurst wants to merge 1 commit into
zachlatta:mainfrom
ojhurst:feat/mute-warning-on-record-start
Open

Show mute warning when recording with system audio muted#229
ojhurst wants to merge 1 commit into
zachlatta:mainfrom
ojhurst:feat/mute-warning-on-record-start

Conversation

@ojhurst

@ojhurst ojhurst commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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.

Red pill overlay showing Unmute to hear sound with exclamation mark icon

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 existing SystemAudioStatus.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 to DispatchQueue.main.async, and the error toast must run after that block completes to avoid being overwritten. A 0.3-second asyncAfter guarantees correct ordering.

Implementation

  • 1 file changed, 7 insertions — no new views, state properties, or methods
  • Uses existing SystemAudioStatus.isDefaultOutputMuted() (already used by the audio interruption feature)
  • Uses existing showError(_:dismissAfter:) (already used for other transient error toasts)
  • Only fires when alertSoundsEnabled is true — no point warning about muted audio when sounds are turned off in settings

Notes / Considered

  • Why showError instead of a custom view? A dedicated amber warning view was prototyped but showError is simpler (zero new code surface) and the red styling is more attention-grabbing for something the user should act on.
  • Why 0.3 seconds? The recording overlay methods dispatch to 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.
  • Why not fire immediately on key press? Placing the check in beginRecording (before the recording overlay renders) was tried first, but the 200ms initializing timer and the onRecordingReady callback both override the overlay phase, stomping the mute warning before it can render. Moving it to onRecordingReady with a short delay after the overlay is established was the only reliable placement.
  • Volume at zero vs mute buttonisDefaultOutputMuted() 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.
  • Non-notched Macs — the pill sizing and positioning are handled by the existing showError layout, which already accounts for notched and non-notched displays.
  • Stale session guard — the 0.3-second delayed closure checks self.isRecording before 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 the isRecording check 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):

Scenario Result
Mute via keyboard button, tap shortcut to record Red "Unmute to hear sound" pill appears for 3 seconds, then returns to waveform. Recording continues normally.
Unmute, tap shortcut to record No warning. Normal start chime plays, waveform appears immediately.
Multiple muted recordings in a row Warning fires reliably each time.
Unmute mid-recording after warning Warning auto-dismisses on its 3-second timer regardless of mute state change.

Instrumentation used during development:

  • File-based debug logging (~/freeflow-mute-debug.log) confirmed SystemAudioStatus.isDefaultOutputMuted() correctly returns true when the keyboard mute button is pressed and false when unmuted. Volume level is preserved (0.5) in both states — the mute property is independent of the volume slider.
  • Log trace confirmed the full sequence: onRecordingReady → mute check → scheduling showError in 0.3sfiring showError now.

Dispatch ordering verified: Earlier attempts placed the mute check in beginRecording (immediate on key press) and in onRecordingReady without a delay. Both were stomped by transitionToRecording()'s async dispatch overriding the overlay phase. The 0.3-second asyncAfter was the minimum reliable delay.

Test plan for reviewer

  • Mute system audio, start a recording — red "Unmute to hear sound" pill appears for 3 seconds, then returns to waveform
  • Unmute system audio, start a recording — no warning appears, normal flow
  • Disable alert sounds in settings, mute system audio, start a recording — no warning (sounds are off anyway)
  • Verify recording audio capture is not affected by the mute warning overlay

Summary by CodeRabbit

  • New Features
    • App now displays a notification when alert sounds are enabled but device audio output is muted, helping you understand why sounds aren't playing.

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).
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Muted Audio Output Detection

Layer / File(s) Summary
Muted output detection and error overlay
Sources/AppState.swift
When recording starts, the app detects muted system audio and schedules an error overlay prompting the user to unmute (auto-dismissing) alongside the "Tink" alert sound.

Possibly related PRs

  • zachlatta/freeflow#114: Both PRs implement "unmute to hear sound" UX by detecting the system default output mute state (main PR when alert audio begins; related PR when Preview is clicked).

Poem

🎵 A freeflow recorder starts to sing,
But hush—the speaker's muted by a silent thing!
A gentle overlay appears with care,
"Unmute to hear," it whispers through the air. 🔊

🎯 1 (Trivial) | ⏱️ ~3 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a mute warning notification when recording starts with system audio muted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1100c72 and f9342d4.

📒 Files selected for processing (1)
  • Sources/AppState.swift

Comment thread Sources/AppState.swift
Comment on lines +2146 to +2153
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)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for showError method signatures in RecordingOverlayManager/RecordingOverlay files
rg -n -A3 'func showError' --type=swift

Repository: 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 50

Repository: 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 50

Repository: zachlatta/freeflow

Length of output: 594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '160,240p' Sources/RecordingOverlay.swift | nl -ba

Repository: 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' Sources

Repository: 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.

@marcbodea

Copy link
Copy Markdown
Collaborator

Can we add a toggle in the settings for this? I use the audio alerts but often have my laptop on mute

@ojhurst

ojhurst commented Jun 12, 2026 via email

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants