Skip to content

Proposal + prototype: application ducking (attenuate other apps while one speaks) #394

Description

@possibilities

Hi! I built application ducking for FineTune on a fork and wanted to share it in case the idea or any of the implementation is useful. This is not a PR and there's no expectation that it gets merged — I'd mostly like to know whether this is a direction you'd want in FineTune at all before anyone spends more time on it.

The feature: one app (a voice assistant, a meeting app) signals that it's about to speak, FineTune attenuates the other apps with a proper attack/hold/release envelope, then fades them back. The guide/url-schemes.md "Meeting mode" example is essentially the manual version of this, which is what suggested it.

Up front: this was written with AI assistance

The implementation and tests were produced by an AI coding agent (Claude) working against the codebase, with a human directing the work and reviewing the output. I'd rather say that plainly than have you find out from the commit style.

Practically, that means the code follows the patterns it found in the repo and the tests pass, but it hasn't had the kind of skeptical human read that audio code deserves. If there's interest, I'm happy to put it through deeper human and independent review, and to reshape it to whatever you'd want upstream — different architecture, smaller scope, or just take the ideas.

What's implemented

  • RT-safe duck stage in ProcessTapController — a one-pole follower with asymmetric attack/release, folded into the existing per-frame gain loop as currentVol * duckCurrent * crossfadeMultiplier * outputGateMultiplier. No allocation, locks, logging, or ObjC in the callback, per the guidelines in CONTRIBUTING.md.
  • DuckingCoordinator + DuckLeaseRegistry — lease bookkeeping and target resolution, both clock-injected and free of CoreAudio so they unit-test without hardware.
  • A finetune://duck-* URL interfaceduck-begin / duck-heartbeat / duck-end / duck-release-all, extending the existing URLHandler pattern.
  • Settings → Audio → Ducking — depth, target mode, timing presets with advanced attack/hold/release, live status and a panic release.
  • Docs in guide/ducking.md, plus entries in guide/url-schemes.md.

Ducking settings

Design choices that affect integration

A few decisions that seemed important for this not to be invasive:

Off by default, and off after upgrade — the settings decode additively, so an existing settings.json gets a disabled default and nothing is attenuated until someone turns it on.

Duck gain is deliberately not folded into effectiveVolume. It lives in its own nonisolated(unsafe) word with its own envelope. Two reasons: it needs asymmetric attack/release, which the shared 30 ms rampCoefficient can't express; and keeping it out of _volume means the existing tap.volume = effectiveVolume(...) call sites (slider drag, device-volume change, boost, settings reset) can't clobber a live duck, and performDestructiveDeviceSwitch's save-and-ramp-back of _volume can't swallow a release landing inside its window.

Decoupled from any particular caller. FineTune has no knowledge of who's asking — no bundled integration, no hardcoded bundle IDs. Anything that can open a URL can drive it. Leases are per-utterance with a TTL, and no live duck is ever persisted, so a crashed caller or a FineTune restart returns audio to normal on its own rather than leaving it stuck quiet.

The URL surface is unauthenticated, which I've documented rather than papered over: macOS gives no trustworthy caller identity for a custom-URL open, so requester/source are self-asserted labels. It's bounded by a master switch, a max-depth clamp, TTL and absolute duration caps, concurrency and rate limits, and a panic release. guide/ducking.md says all of this explicitly, including that a one-way URL isn't a security mechanism.

Verification — and what I have not done

Green on GitHub Actions (macos-26, Xcode 26.1.1): 912 tests pass, 0 fail, of which 155 are new ducking tests covering the envelope, lease semantics, URL parsing, settings migration, the buffer-processing path, and the coordinator.

I also ran it for real on macOS 26.5.2 with a CI-built binary: enabling ducking, firing duck-begin/duck-end from the shell, and hearing other audio drop and come back correctly.

That's one machine, one output device, and a handful of apps. I have not done the checks CONTRIBUTING.md asks for before submitting:

  • 2+ output devices
  • Device hot-plug during playback — specifically during an active duck, where the tap is rebuilt under the envelope
  • 5+ apps playing simultaneously
  • Bluetooth A2DP↔SCO transitions mid-duck
  • Multi-buffer/stacked aggregate output on real hardware (unit-tested, not heard)

So: correct under test and working in one real setup, not validated across the device matrix. I didn't want to present it as more than that.

There's also a known gap that's a product limitation rather than a bug — apps filtered by AudioProcessMonitor.systemDaemonPrefixes (Siri, notification and alert sounds) have no tap and therefore can't be ducked. Notification chimes cut through. It's documented.

If you're interested

Happy to reshape this as a focused PR, split it into smaller pieces, hand over the design and let someone else implement it, or drop it entirely if it's not a direction you want. No hard feelings either way — the fork works for our use, and I mainly didn't want to sit on it silently if it's useful to others.

Licensing is unchanged: GPLv3, same as the project.


Environment: macOS 26.5.2 (25F84), Apple Silicon. Test build produced by the fork's CI (ad-hoc signed, not notarized). Audio apps in the manual test: Chrome and Safari.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions