Skip to content

Add "Reduce volume" to Audio Behavior while Recording - #230

Open
lflanagan wants to merge 1 commit into
kitlangton:mainfrom
lflanagan:reduce-volume-audio-behavior
Open

lflanagan wants to merge 1 commit into
kitlangton:mainfrom
lflanagan:reduce-volume-audio-behavior

Conversation

@lflanagan

@lflanagan lflanagan commented May 21, 2026

Copy link
Copy Markdown

Summary

  • Add a Reduce Volume audio behavior for recording.
  • Add Settings controls for target volume, fade out, and fade in.
  • Restore the original output volume after recording, while respecting manual volume changes during recording.

Screenshot

Reduce Volume settings

Testing

  • cd HexCore && swift test
  • xcodebuild -scheme Hex -configuration Debug -skipMacroValidation -skipPackagePluginValidation CODE_SIGNING_ALLOWED=NO build

Summary by CodeRabbit

  • New Features

    • Added "Reduce Volume" audio-behavior option with configurable playback level and fade-out/fade-in durations during recording.
    • Improved volume restoration to respect overlapping recordings and manual volume changes.
  • Bug Fixes

    • Hotkey now deterministically cancels then starts when already transcribing.
  • Tests

    • Added tests for recording volume defaults and clamping of fade durations.
  • Localization

    • Added strings for the new audio-behavior controls and fade labels.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa61dd6a-e50a-48c1-bc2e-19ae19bb1d6d

📥 Commits

Reviewing files that changed from the base of the PR and between 28603f6 and 829a7b7.

📒 Files selected for processing (8)
  • .changeset/e8998ed7.md
  • Hex/Clients/RecordingClient.swift
  • Hex/Features/Settings/GeneralSectionView.swift
  • Hex/Features/Settings/SettingsFeature.swift
  • Hex/Features/Transcription/TranscriptionFeature.swift
  • HexCore/Sources/HexCore/Settings/HexSettings.swift
  • HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift
  • Localizable.xcstrings
✅ Files skipped from review due to trivial changes (2)
  • .changeset/e8998ed7.md
  • Localizable.xcstrings

📝 Walkthrough

Walkthrough

Adds a configurable "reduce volume" recording mode with playback-level and fade-in/out settings, implements session-aware ramping and manual-adjust detection in the recording client, updates UI and settings schema, adjusts transcription hotkey flow, and adds tests, localization, and a changeset.

Changes

Recording Volume Ducking with Fade Controls

Layer / File(s) Summary
Settings schema: volume properties and defaults
HexCore/Sources/HexCore/Settings/HexSettings.swift
RecordingAudioBehavior gains reduceVolume; adds persisted recordingReducedVolume, recordingVolumeFadeOutDuration, recordingVolumeFadeInDuration with defaults, clamping helpers, and normalization on init/decode.
Settings feature actions for volume control
Hex/Features/Settings/SettingsFeature.swift
Adds setRecordingReducedVolume(Double), setRecordingVolumeFadeOutDuration(Double), and setRecordingVolumeFadeInDuration(Double) actions and reducer handling that writes clamped values into hexSettings.
Volume ducking core system in RecordingClient
Hex/Clients/RecordingClient.swift
Implements session-aware volume ducking with muteSystemVolume, reduceSystemVolume, restoreSystemVolume, generation guards, fade ramping, a manual-adjust monitor, and tracking state (volumeFadeTask, volumeMonitorTask, volumeControlGeneration, previousVolume, lastAppliedRecordingVolume, volumeAdjustmentThreshold).
Recording lifecycle integration: start, resume, cleanup
Hex/Clients/RecordingClient.swift
Wires new volume functions into startRecording(), adds abortRecordingStart() for start failures, updates resumeMediaIfNeeded() to restore with fade and skip on manual change, expands cleanup() to cancel/restore volume state.
Settings UI: volume controls and formatting
Hex/Features/Settings/GeneralSectionView.swift
Adds Reduce Volume option to the picker and conditionally renders playback-level and fade-duration sliders with formatting helpers when selected.
Transcription: sequential cancel-then-start
Hex/Features/Transcription/TranscriptionFeature.swift
Hot-key handling now runs cancel then start sequentially when already transcribing.
Tests, localization, and release metadata
HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift, Localizable.xcstrings, .changeset/e8998ed7.md
Adds migration and clamping tests for new settings, new localization strings for UI labels, and a changeset marking a minor release.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • kitlangton

Poem

🐇 In hush of start the speakers bow,
A gentle fade, a softened vow,
The rabbit taps, the volume slides,
Then whispers back where silence hides. 🎧✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% 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 title accurately captures the main feature addition across the changeset: introducing a new 'Reduce Volume' audio behavior option with associated controls and implementation.
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.

@lflanagan
lflanagan marked this pull request as ready for review May 21, 2026 07:42
@lflanagan
lflanagan force-pushed the reduce-volume-audio-behavior branch from e2868a1 to 28603f6 Compare May 21, 2026 07:45

@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: 2

🤖 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 `@Hex/Clients/RecordingClient.swift`:
- Around line 1003-1013: The guard in adoptCurrentVolumeForRecording currently
requires volumeFadeTask != nil which prevents re-arming monitoring when a new
session inherits an already-ducked volume; change the logic to only require
recordingSessionID == sessionID and previousVolume != nil (remove the
volumeFadeTask != nil check), and then if volumeFadeTask exists cancel it and
nil it, call advanceVolumeControlGeneration(), set lastAppliedRecordingVolume =
getSystemVolume(), and call startRecordingVolumeMonitor(sessionID:generation:)
so the volumeMonitorTask is restarted; reference adoptCurrentVolumeForRecording,
volumeFadeTask, previousVolume, advanceVolumeControlGeneration,
lastAppliedRecordingVolume, getSystemVolume, and startRecordingVolumeMonitor.
- Around line 380-383: previousVolume is being restored against
getDefaultOutputDevice() instead of the actual device that was ducked, so if the
user switches outputs mid-recording the wrong device gets the restored level;
add a stored device identifier (e.g., previousDuckedDeviceID or reuse a clearly
named property alongside previousVolume and lastAppliedRecordingVolume) when you
duck the volume in the code path that calls getDefaultOutputDevice(), then use
that stored deviceID in all restore/save/manual-adjustment paths (the restore
method, any saveVolume/restoreVolume helpers, and manual-adjust handlers) to
target only the exact AudioDeviceID you changed rather than the current default
device. Ensure the new property is set when applying the duck, cleared after a
successful restore, and used wherever getDefaultOutputDevice() is currently used
to determine which device to adjust.
🪄 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

Run ID: 0dfde6c5-a62a-4241-8178-10bd9f196da6

📥 Commits

Reviewing files that changed from the base of the PR and between f988cb7 and e2868a1.

📒 Files selected for processing (8)
  • .changeset/e8998ed7.md
  • Hex/Clients/RecordingClient.swift
  • Hex/Features/Settings/GeneralSectionView.swift
  • Hex/Features/Settings/SettingsFeature.swift
  • Hex/Features/Transcription/TranscriptionFeature.swift
  • HexCore/Sources/HexCore/Settings/HexSettings.swift
  • HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift
  • Localizable.xcstrings

Comment on lines +380 to +383
/// Tracks previous system volume when volume is changed for recording
private var previousVolume: Float?
/// Tracks the last output volume Hex intentionally applied during recording volume control.
private var lastAppliedRecordingVolume: Float?

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 | 🟠 Major | ⚡ Quick win

Track the ducked output device with previousVolume.

previousVolume is restored through getDefaultOutputDevice() later, not the device that was actually ducked. If the user switches outputs mid-recording, Hex will restore the old level onto the new default device and never restore the original device's volume.

💡 Suggested direction
-  private var previousVolume: Float?
+  private var previousVolume: (deviceID: AudioDeviceID, volume: Float)?

Use that stored deviceID in the save/restore/manual-adjustment paths so restore only targets the same output that Hex changed.

🤖 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 `@Hex/Clients/RecordingClient.swift` around lines 380 - 383, previousVolume is
being restored against getDefaultOutputDevice() instead of the actual device
that was ducked, so if the user switches outputs mid-recording the wrong device
gets the restored level; add a stored device identifier (e.g.,
previousDuckedDeviceID or reuse a clearly named property alongside
previousVolume and lastAppliedRecordingVolume) when you duck the volume in the
code path that calls getDefaultOutputDevice(), then use that stored deviceID in
all restore/save/manual-adjustment paths (the restore method, any
saveVolume/restoreVolume helpers, and manual-adjust handlers) to target only the
exact AudioDeviceID you changed rather than the current default device. Ensure
the new property is set when applying the duck, cleared after a successful
restore, and used wherever getDefaultOutputDevice() is currently used to
determine which device to adjust.

Comment thread Hex/Clients/RecordingClient.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2868a11d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +545 to +549
if wasRecording {
// Stop the recording to release microphone access
let url = await recording.stopRecording()
guard !Task.isCancelled else { return }
try? FileManager.default.removeItem(at: url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop recorder even when canceling from transcribing state

In handleCancel, gating recording.stopRecording() on wasRecording can leave the recorder running if cancel arrives just after .stopRecording flips state to transcribing but before that effect actually calls recording.stopRecording(). In that window, wasRecording is false, this path skips the explicit stop, and .cancel(id: CancelID.transcription) can cancel the in-flight stop effect before it reaches the recorder, so mic capture/resources (and related media/volume cleanup) can remain active unexpectedly.

Useful? React with 👍 / 👎.

Comment on lines +1027 to +1029
if rampGeneration != nil {
recordingLogger.notice("Restoring system volume to \(String(format: "%.2f", volume))")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale restore volume when restore ramp cannot start

If startVolumeRamp fails during restore (for example, when output changes to a device without software volume control), this branch leaves previousVolume intact and only logs on success. Since clearMediaState() no longer resets previousVolume, later sessions can incorrectly treat that stale value as an active restore target and unexpectedly change system volume even for behaviors like .doNothing.

Useful? React with 👍 / 👎.

@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: 2

🧹 Nitpick comments (1)
HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift (1)

44-46: ⚡ Quick win

Use the shared default constant in the default-value assertion.

The assertion on Line 45 should reference HexSettings.defaultRecordingReducedVolume instead of 0.2 to avoid brittle tests when defaults change.

Proposed change
 func testNewSettingsDefaultRecordingReducedVolume() {
-	XCTAssertEqual(HexSettings().recordingReducedVolume, 0.2)
+	XCTAssertEqual(HexSettings().recordingReducedVolume, HexSettings.defaultRecordingReducedVolume)
 }
🤖 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 `@HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift` around lines 44 -
46, Replace the hard-coded literal 0.2 in the
testNewSettingsDefaultRecordingReducedVolume() assertion with the shared default
constant by asserting HexSettings().recordingReducedVolume equals
HexSettings.defaultRecordingReducedVolume; update the XCTAssertEqual call to
reference the HexSettings.defaultRecordingReducedVolume constant instead of the
magic number to keep the test resilient to default changes.
🤖 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 `@Hex/Features/Settings/GeneralSectionView.swift`:
- Around line 90-104: The Picker controls created with empty titles (Picker("",
selection: ...)) are not reliably labeled for accessibility; update the Picker
in GeneralSectionView (the Picker bound to
store.hexSettings.recordingAudioBehavior) to include an explicit accessibility
label like .accessibilityLabel("Audio Behavior while Recording"), and likewise
update the Picker in HistorySectionView (the Picker for maximum history entries)
to include .accessibilityLabel("Maximum History Entries") so screen readers get
the proper label.

In `@Localizable.xcstrings`:
- Around line 87-89: The new recording-volume UI strings (e.g., the key "After
recording ends" and the other recording-volume keys added in the same diff
ranges) lack German translations; add corresponding "de" entries for each new
key in Localizable.xcstrings so German users see fully localized text. Locate
the keys including "After recording ends" and the other recording-volume strings
referenced in the comment ranges (around 314-322, 506-508, 560-564) and provide
appropriate German translations under the "de" locale for each key, preserving
the existing key names and file structure.

---

Nitpick comments:
In `@HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift`:
- Around line 44-46: Replace the hard-coded literal 0.2 in the
testNewSettingsDefaultRecordingReducedVolume() assertion with the shared default
constant by asserting HexSettings().recordingReducedVolume equals
HexSettings.defaultRecordingReducedVolume; update the XCTAssertEqual call to
reference the HexSettings.defaultRecordingReducedVolume constant instead of the
magic number to keep the test resilient to default changes.
🪄 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

Run ID: 528df683-befc-4dd1-8010-ccfaba2214f5

📥 Commits

Reviewing files that changed from the base of the PR and between e2868a1 and 28603f6.

📒 Files selected for processing (8)
  • .changeset/e8998ed7.md
  • Hex/Clients/RecordingClient.swift
  • Hex/Features/Settings/GeneralSectionView.swift
  • Hex/Features/Settings/SettingsFeature.swift
  • Hex/Features/Transcription/TranscriptionFeature.swift
  • HexCore/Sources/HexCore/Settings/HexSettings.swift
  • HexCore/Tests/HexCoreTests/HexSettingsMigrationTests.swift
  • Localizable.xcstrings
✅ Files skipped from review due to trivial changes (1)
  • .changeset/e8998ed7.md

Comment on lines +90 to +104
Picker("", selection: Binding(
get: { store.hexSettings.recordingAudioBehavior },
set: { store.send(.setRecordingAudioBehavior($0)) }
)) {
Label("Pause Media", systemImage: "pause")
.tag(RecordingAudioBehavior.pauseMedia)
Label("Mute Volume", systemImage: "speaker.slash")
.tag(RecordingAudioBehavior.mute)
Label("Reduce Volume", systemImage: "speaker.wave.1")
.tag(RecordingAudioBehavior.reduceVolume)
Label("Do Nothing", systemImage: "hand.raised.slash")
.tag(RecordingAudioBehavior.doNothing)
}
.pickerStyle(.menu)
}

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify empty-title pickers in SwiftUI views.
rg -nP 'Picker\(\s*""\s*,\s*selection:' --type swift -C2

Repository: kitlangton/Hex

Length of output: 949


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show picker context in both files
sed -n '70,120p' Hex/Features/Settings/GeneralSectionView.swift
echo "----"
sed -n '1,80p' Hex/Features/Settings/HistorySectionView.swift

# Check for nearby accessibility modifiers in these files
rg -n "accessibilityLabel|labelsHidden" Hex/Features/Settings/GeneralSectionView.swift Hex/Features/Settings/HistorySectionView.swift

Repository: kitlangton/Hex

Length of output: 3989


Add explicit accessibility labels to empty-title Picker controls

  • Hex/Features/Settings/GeneralSectionView.swift (Picker("", selection: ...)): nearby Text("Audio Behavior while Recording") doesn’t reliably label the picker; add an explicit accessibility label.
  • Hex/Features/Settings/HistorySectionView.swift (Picker("", selection: ...)): same pattern for Text("Maximum History Entries").
Proposed fix
-						Picker("", selection: Binding(
+						Picker("Audio Behavior while Recording", selection: Binding(
 							get: { store.hexSettings.recordingAudioBehavior },
 							set: { store.send(.setRecordingAudioBehavior($0)) }
 						)) {
 							...
 						}
 						.pickerStyle(.menu)
+						.labelsHidden()
+						.accessibilityLabel("Audio Behavior while Recording")
🤖 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 `@Hex/Features/Settings/GeneralSectionView.swift` around lines 90 - 104, The
Picker controls created with empty titles (Picker("", selection: ...)) are not
reliably labeled for accessibility; update the Picker in GeneralSectionView (the
Picker bound to store.hexSettings.recordingAudioBehavior) to include an explicit
accessibility label like .accessibilityLabel("Audio Behavior while Recording"),
and likewise update the Picker in HistorySectionView (the Picker for maximum
history entries) to include .accessibilityLabel("Maximum History Entries") so
screen readers get the proper label.

Comment thread Localizable.xcstrings
Comment on lines +87 to 89
"After recording ends" : {

},

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 | 🟡 Minor | ⚡ Quick win

Add German localizations for the new recording-volume strings.

These newly added user-facing keys currently have no de entries, so German users will see mixed-language settings UI.

Also applies to: 314-322, 506-508, 560-564

🤖 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 `@Localizable.xcstrings` around lines 87 - 89, The new recording-volume UI
strings (e.g., the key "After recording ends" and the other recording-volume
keys added in the same diff ranges) lack German translations; add corresponding
"de" entries for each new key in Localizable.xcstrings so German users see fully
localized text. Locate the keys including "After recording ends" and the other
recording-volume strings referenced in the comment ranges (around 314-322,
506-508, 560-564) and provide appropriate German translations under the "de"
locale for each key, preserving the existing key names and file structure.

@lflanagan
lflanagan force-pushed the reduce-volume-audio-behavior branch from 28603f6 to 829a7b7 Compare May 22, 2026 01:12
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