Skip to content

feat(audio): keep app volume during calls by holding a ducking exemption (fixes #241) - #482

Open
CharlieBaker2002 wants to merge 1 commit into
ronitsingh10:mainfrom
CharlieBaker2002:feat/keep-volume-during-calls
Open

CharlieBaker2002 wants to merge 1 commit into
ronitsingh10:mainfrom
CharlieBaker2002:feat/keep-volume-during-calls

Conversation

@CharlieBaker2002

@CharlieBaker2002 CharlieBaker2002 commented Oct 31, 2026 •

Copy link
Copy Markdown

Fixes #241. Should also resolve the "call audio is very quiet while FineTune runs" reports in #113, #156, #262, #360, #404 and #426 — see How it relates to other PRs below.

Problem

While a call app drives Apple's voice-processing IO (FaceTime's avconferenced, WhatsApp, …), coreaudiod ducks every other HAL client on the output device to a combined volume scalar of 0.178 (-15 dB). FineTune renders every tapped app through its own HAL client, so the whole mixer drops to ~18% for the length of the call. When FineTune taps the call app too, the call itself goes through that same client and gets ducked as well — that is the faint call audio in the issues above.

This is a long-standing macOS complaint with no setting to turn it off — e.g. the r/mac thread Possible to turn off audio ducking for FaceTime? (now archived) asks exactly that, and #241 asks FineTune for a toggle.

From coreaudiod's log on macOS 26.6.2 during a FaceTime call (FineTune 1.9.0 is pid 40318, avconferenced is pid 787):

HALS_Device::_GetCombinedVolumeScalar: BuiltInSpeakerDevice client 2458 (pid 40318) is not present, and is ducked with a combined volume scalar of 0.177828
HALS_Device::_GetCombinedVolumeScalar: BuiltInSpeakerDevice client 212 (pid 787) is present, and is ducked with a combined volume scalar of 1.000000

(@vfmatzkin's analysis in #113 found the same 0.177828 scalar.)

Fix

coreaudiod never ducks a client that holds a duck request of its own. Its log calls those clients "duckers" and gives them a scalar of 1.0 whatever anyone else requests. So FineTune now holds a request of 0.999 (-0.01 dB, inaudible) on every output device via AudioDeviceDuck:

AudioDeviceDuck(deviceID, 0.999, nil, 0)   // hold — FineTune becomes a "ducker"
AudioDeviceDuck(deviceID, 1.0,   nil, 0)   // release — exactly 1.0 withdraws the request

Everything FineTune replays stays at full volume; the call's ducking of audio that bypasses FineTune (ignored apps, system sounds) is untouched, and the call app's own audio is unaffected either way.

Three properties of the request make the implementation small. All three were checked against _UpdateClientVolumeScalarDucking in coreaudiod's log during a live FaceTime call, using a probe that renders through a private aggregate device exactly the way ProcessTapController does:

Scenario Result
Request made on the real output device, IO running through a private aggregate (tap + output sub-device) ducker, scalar 1.000
Request made before any IO proc or aggregate exists still a ducker once IO starts
Aggregate torn down and recreated after the request still a ducker

So the request is bound to the process ↔ device pair, not to an IO proc, and one request per output device from AudioEngine is enough — nothing in the real-time tap code changes.

What's in the diff

File Change
Audio/Engine/CallDuckingGuard.swift New. Resolves AudioDeviceDuck with dlsym, holds/releases requests per device, keeps a held set so repeated calls are no-ops. The duck function is injectable for tests.
Audio/Engine/AudioEngine.swift Owns the guard. Holds on every output device at start, on device (re)connect, and as a cheap no-op safety net in the existing 2 s health-monitor loop (covers coreaudiod restarts). Releases on stop() and when the setting is turned off.
Settings/SettingsManager.swift AppSettings.keepVolumeDuringCalls, default on; missing key decodes as on.
Views/Settings/Tabs/AudioTab.swift Settings → Audio → Keep Volume During Calls toggle.
FineTuneTests/CallDuckingGuardTests.swift, AppSettingsKeepVolumeDuringCallsTests.swift 13 new test cases (bookkeeping, failure retry, release-on-disable, decoding default).
README.md, README.zh-CN.md, guide/troubleshooting.md Feature bullet and a troubleshooting entry.

About AudioDeviceDuck

It is exported by CoreAudio but not declared in the public headers; WebKit and Chromium call it with the same signature (AudioDeviceID, Float32 level, const AudioTimeStamp*, Float32 ramp), and Mozilla's bindings note it has existed since macOS 10.13. It is resolved at runtime, so if a future macOS drops it the guard is a silent no-op and calls duck FineTune exactly as they do today. FineTune already relies on two SPIs of the same kind (TCCAccessRequest, responsibility_get_pid_responsible_for_pid). The technique is the one ShutTheDuckOff (GPL-3.0, same as FineTune) is built on; nothing was copied from it.

How it relates to other PRs

Testing

  • xcodebuild test -skip-testing:FineTuneUITests (the CI invocation): 978 passed, 0 failed on macOS 26.6.2 / Xcode 27.0 / Apple Silicon.
  • Live FaceTime call with Spotify and Chrome playing, build installed in place of 1.9.0. coreaudiod's log for the new process (pid 47926) right after launch:
HALS_Device::_DuckClientVolumeScalar: client 2800 (pid 47926) ducking device <private> (107) to 0.999000 over (0.000000, 1)
HALS_Device::_UpdateClientVolumeScalarDucking: client 2800 (pid 47926) is a ducker so its ducked volume scalar on device <private> (107) is 1.000000
HALS_Device::_GetCombinedVolumeScalar: BuiltInSpeakerDevice client 2800 (pid 47926) is present, and is ducked with a combined volume scalar of 1.000000

Music stayed at full volume for the rest of the call and avconferenced kept its own 1.000 scalar. Relaunching during the same call with keepVolumeDuringCalls set to false in settings.json: no request is made and FineTune's client is ducked to 0.177828, i.e. exactly 1.9.0's behaviour; relaunching with it back on: ducker at 1.000 on all three output devices. The drop and recovery were audible. The runtime release path (setEnabled(false) → AudioDeviceDuck(…, 1.0)) is covered by the unit tests, and the probe confirmed that a 1.0 request turns the client back into a duckee (_DuckClientVolumeScalar: … unducking device … is a duckee … 0.177828).

  • Nothing in the audio callback changed, so the RT-safety surface is untouched.

🤖 Generated with Claude Code

macOS ducks every other HAL client on an output device to 0.178 (-15 dB)
while a voice-processing client (FaceTime's avconferenced, WhatsApp, …) is
on a call. FineTune renders every tapped app through its own HAL client,
so the whole mixer — and a tapped call app itself — dropped to ~18% for
the length of the call (ronitsingh10#67, ronitsingh10#113, ronitsingh10#156, ronitsingh10#241, ronitsingh10#262, ronitsingh10#360, ronitsingh10#404, ronitsingh10#426).

coreaudiod never ducks a client that holds a duck request of its own. The
new CallDuckingGuard holds AudioDeviceDuck(device, 0.999) on every output
device, which exempts FineTune's replay while leaving the call's ducking
of audio that bypasses FineTune (ignored apps, system sounds) untouched.
The request is bound to the process/device pair: it works before any IO
starts and survives aggregate teardown, so one request per device from
AudioEngine is enough (checked on macOS 26.6 against
_UpdateClientVolumeScalarDucking in coreaudiod's log). AudioDeviceDuck is
exported but undocumented; it is resolved with dlsym and the feature
degrades to today's behaviour if a future macOS drops it.

- Settings → Audio → "Keep Volume During Calls" (keepVolumeDuringCalls,
  default on); turning it off releases the requests immediately.
- Unit tests for the guard's bookkeeping and the setting's default and
  decoding; README and troubleshooting updated.

Fixes ronitsingh10#241

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

[Feature Request] Enable / Disable Audio Ducking in calls

1 participant