Add adjustable audio ducking during recording (closes #683) - #684
Open
dhryniewicz wants to merge 5 commits into
Open
Add adjustable audio ducking during recording (closes #683)#684dhryniewicz wants to merge 5 commits into
dhryniewicz wants to merge 5 commits into
Conversation
Contributor
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="VoiceInk/MediaController.swift">
<violation number="1" location="VoiceInk/MediaController.swift:51">
P2: `activeDuckingMode` can stay stale across a mute cycle, so a later stop may restore an old saved volume even when the current recording never ducked audio.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Contributor
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="VoiceInk/MediaController.swift">
<violation number="1" location="VoiceInk/MediaController.swift:44">
P1: Clearing `activeDuckingMode` before a new ducking action succeeds can discard the only restore state after canceling the previous pending unmute, leaving system audio stuck in the prior muted/ducked state.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Contributor
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="VoiceInk/MediaController.swift">
<violation number="1" location="VoiceInk/MediaController.swift:109">
P2: Clears ducking ownership/state even if the immediate restore fails, so a cancelled prior cycle can leave audio modified with no saved baseline for the next cycle.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Contributor
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="VoiceInk/MediaController.swift">
<violation number="1" location="VoiceInk/MediaController.swift:102">
P1: Overwriting an unresolved `.mute` restore with `.volume` loses the fact that the device still needs an unmute, which can leave system audio muted after recording stops.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
dhryniewicz
force-pushed
the
feature/audio-ducking
branch
from
May 14, 2026 08:30
e3afee3 to
c9630a6
Compare
Replace the binary mute toggle with a configurable ducking percentage (0-100%, default 100% to preserve existing behavior). At 100% the system audio is fully muted via kAudioDevicePropertyMute as before; below 100% the current output volume is reduced via kAudioDevicePropertyVolumeScalar and restored when recording ends. Closes Beingpax#683 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If a pending unmute task was cancelled by a new recording, the previous mode (e.g. .volume(savedVolume)) lingered. If the new cycle then took a no-op path (ducking percent 0, or applyVolumeDucking failure), the stop would incorrectly restore an old saved volume from the previous recording. Reset the mode up front so only a successful path sets it. Identified by cubic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix cleared activeDuckingMode at the start of muteSystemAudio, but if the prior cycle's unmute task had been cancelled mid-delay the audio was still in a ducked/muted state — and clearing the mode threw away the only handle on the saved volume. A no-op new cycle (0% or failure) would then leave audio stuck. Restore synchronously instead: when starting a new cycle, drive the prior cycle's restore to completion immediately (unmute or set volume back to savedVolume), then clear the mode. This fixes both the original stale-mode case and the new stuck-audio regression, and as a side benefit the new cycle's applyVolumeDucking now reads the user's true volume rather than a still-ducked value. Identified by cubic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If setSystemMuted/setSystemVolume returned false from the synchronous restore path, the code still cleared activeDuckingMode and didMuteAudio. That left audio in a modified state (still ducked or still muted) with no record of how to undo it: a subsequent stop would skip the restore, and the next ducking cycle would anchor its baseline on the already- ducked volume, eventually drifting the user's true volume to zero. Bail early on failure so the prior cycle's mode survives. Then teach applyVolumeDucking to reuse savedVolume from a still-active prior mode as its baseline rather than reading the current (still-ducked) volume. Identified by cubic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If applyPendingRestoreImmediately failed to undo a prior .mute (or .volume), the new cycle could pick the other type and overwrite the mode, losing the prior restore information. Concretely: an unresolved .mute overwritten with .volume(savedVolume) would unmute-skip on stop and leave audio silent; the symmetric .volume → .mute path would unmute on stop but trap the device at the ducked level instead of restoring the user's saved volume. Bail out of applyFullMute when an unresolved .volume is active, and out of applyVolumeDucking when an unresolved .mute is active. The recording proceeds without applying ducking for that cycle; the prior mode survives and unmuteSystemAudio retries the original restore on stop. Identified by cubic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dhryniewicz
force-pushed
the
feature/audio-ducking
branch
from
May 28, 2026 11:11
c9630a6 to
92e6b18
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the feature requested in #683: a slider for adjustable volume reduction (ducking) during recording, instead of only binary mute/unmute. Users who listen to background music or video while dictating can now keep audio audible at a reduced level rather than silencing it completely.
Behavior
kAudioDevicePropertyMutepath, so the macOS volume slider keeps its visual position (just shows the muted icon), and existing user-mute detection still applies.kAudioDevicePropertyVolumeScalartocurrentVolume × (100 − percent) / 100, and restores the original volume on stop (respecting the existingaudioResumptionDelay).Implementation
MediaController.swift: addedaudioDuckingPercent(UserDefaults-backed), aDuckingModeenum to track which path was used (mute vs. volume scalar) for correct restore, andgetSystemVolume()/setSystemVolume()helpers mirroring the existing CoreAudio mute helpers.SettingsView.swift: added aSlider+ percentage label +InfoTipinside the existing Mute row.AppDefaults.swift: registeredaudioDuckingPercent: 100so existing users continue to get full mute.The
unmuteSystemAudio()cancellation/generation logic is preserved; volume restore is gated by the samemuteGenerationcheck so quickly-retriggered recordings don't race.Test plan
make local— builds cleanly with no new warningsaudioResumptionDelay > 0, volume restore is correctly delayed