Skip to content

fix(audio): remove Independent Volume instead of writing system volume - #867

Open
postoso wants to merge 1 commit into
altic-dev:mainfrom
postoso:fix/522-app-local-cue-gain
Open

fix(audio): remove Independent Volume instead of writing system volume#867
postoso wants to merge 1 commit into
altic-dev:mainfrom
postoso:fix/522-app-local-cue-gain

Conversation

@postoso

@postoso postoso commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Description

You asked whether the option should just be removed rather than kept with app-side compensation. It should, so this PR now does that instead.

Independent Volume was the only code path that wrote the macOS output volume. It read the current level, set the output to the selected cue level for the length of the cue, then restored it on a timer. Writing the output volume is globally audible, and that is what #522 reports.

Compensating app-side, which is what this PR did before, does not preserve what the option promised. The CoreAudio volume scalar and AVAudioPlayer.volume are different gain domains, so a player gain derived from their ratio does not land on the selected level, and it caps at full gain once the selected level is above the current output. An option that only half keeps its promise is worse than no option.

What is left is the single cue-volume slider. The cue plays at that level through AVAudioPlayer, so it scales with system output like any other sound, and nothing in the app writes the system volume.

Four latent bugs go with the removal, all of them in the save and restore path. getSystemVolume returned 1.0 on both failure paths, so a failed read was saved as the baseline and the restore drove the output to maximum. The restore re-resolved the default output device, so switching outputs mid-cue wrote one device's baseline onto another. The delayed restore had no termination handler, so quitting mid-cue left the device at the cue level. Overlapping cues shared one savedSystemVolume slot.

The Independent Volume setting copy is removed with the toggle. It promised a level that stays constant regardless of system volume and warned that playback temporarily changes system volume, and both were wrong.

The tradeoff, stated plainly: if you kept system audio low and relied on an independently louder cue, the cue is now quieter. That capability cannot exist without moving everyone else's volume, which was the bug.

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Closes #522.

Testing

  • Tested on Apple Silicon Mac
  • Tested on macOS version:
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml
  • Ran tests locally: xcodebuild test -project Fluid.xcodeproj -scheme Fluid -destination 'platform=macOS,arch=arm64'
  • Tested on Intel Mac
  • Ran formatter locally: swiftformat --config .swiftformat Sources

swiftlint --strict over the whole repository reports 7 violations, all legacy_swiftui_aspect_ratio, all in files this PR does not touch. A clean checkout of main reports the identical 7 on the same SwiftLint build, so they are not from this change.

Locally I ran xcodebuild build-for-testing rather than the full suite, so the test target compiles. CI ran the suite and it is green, which is also the first green Build on this branch: the DirectAudioReliabilityTests failure I flagged earlier is not present on this head.

This PR removes the eight gain-policy tests it had added earlier, because the policy they covered no longer exists, and DictationE2ETests.swift is back to its state on main. There is no pure seam left to test here and I would rather not add a test that asserts nothing.

Rebased on current main.

Screenshots / Video

  • No UI/visual changes; screenshots/video are not applicable.

Settings sound section on current main; this PR removes the Independent Volume row.

FluidVoice Settings sound section on current main, showing the Independent Volume row and its two lines of copy

@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown

Greptile Summary

The PR removes Independent Volume and the CoreAudio system-volume mutation path, leaving transcription cues controlled solely through AVAudioPlayer.volume.

  • Removes the persisted Independent Volume preference and backup field.
  • Removes the corresponding Settings control.
  • Removes system-volume reads, writes, and delayed restoration from cue playback.

Confidence Score: 5/5

The PR appears safe to merge because no eligible blocking failure or outstanding prior finding was established.

No blocking failure remains.

Reviews (2): Last reviewed commit: "fix(audio): remove Independent Volume in..." | Re-trigger Greptile

@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: 40dd7c5e56

ℹ️ 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 +49 to +51
// The cap is what keeps the audible result at or below the current
// output: `output * min(1, desired / output)` is `min(output, desired)`.
return .play(playerVolume: min(1, desired / output))

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 Convert device volume through its dB curve

On output devices whose CoreAudio scalar-to-decibel mapping is nonlinear, dividing desired by the hardware volume scalar does not produce the gain needed to reach the desired audible level: the hardware scalar and AVAudioPlayer.volume are different gain domains. For example, an output scalar of 0.8 and desired scalar of 0.4 need not correspond to a player gain of 0.5, so Independent Volume can play substantially above or below the selected level. Convert the device scalars to decibels using its CoreAudio conversion property, then derive the player gain from the dB difference.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The scalar ratio is not valid across those gain domains. I'm removing the Independent
Volume path instead, so this compensation code and the exact-level claim go away.

Comment on lines +16 to +18
/// Independent Volume compensates the player toward the selected level instead
/// of moving the Mac output volume, so the current output stays the ceiling and
/// other audio is never touched.

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 Update the Independent Volume setting copy

When Independent Volume is enabled and system output is below the selected cue level, this new ceiling deliberately makes the cue quieter, but SettingsView.swift:375-377 still promises that the sound stays constant regardless of system volume and warns that playback temporarily changes system volume. Users therefore see a description that is now false in both directions; update that copy alongside this behavioral change.

Useful? React with 👍 / 👎.

@postoso

postoso commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the Build FluidVoice failure is DirectAudioReliabilityTests.testReadinessGateRearmingCancelsExistingWaiter (264 executed, 1 failure). This PR does not touch the readiness gate or waiter machinery - the diff is confined to TranscriptionSoundPlayer.swift and its tests, and the full set including the 8 focused sound-player tests passes locally. Looks like an unrelated timing flake; happy to rebase or push a retrigger if you'd like a clean run.

@postoso

postoso commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up with a concrete main-branch comparison: the same failure predates this PR and appears unrelated to its changes. DirectAudioReliabilityTests.testReadinessGateRearmingCancelsExistingWaiter failed with the same ("staleSession") is not equal to ("cancelled") assertion in run 31668084019, a push to main at 2203c6bf on Aug 13, three days before this PR opened. That run had 238 tests and one failure; this run had 264 and one failure.

I could not find an open issue under the test name or DirectAudioReliabilityTests. Happy to file one if it is not already known.

@github-actions

Copy link
Copy Markdown

This pull request has been marked stale because it has had no activity for 5 days. It will be closed in 2 days if there is no further activity. Add the keep-open or pinned label to prevent automatic closure.

@github-actions github-actions Bot added the stale label Aug 25, 2026
@altic-dev

Copy link
Copy Markdown
Owner

Thanks for the PR @postoso . I still wonder if this is worth doing actually. Should we just remove the option by itself?

having the sound cue louder than the audio itself seems a little off :/ No?

@github-actions github-actions Bot removed the stale label Aug 27, 2026
@postoso

postoso commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

I think removing it is the better call. Once the system-volume write is gone, Independent Volume
only gives partial app-side compensation: it raises the cue's player gain as system volume drops,
capped at full gain. It no longer keeps the cue independent, and the result is device-dependent
because the two controls are different gain domains.

Removing it still fixes #522, since the system-volume write only ran on that path. I'll revise this
PR to drop the toggle and the save/set/restore code, keep one cue-volume slider, and fix the
settings copy, which describes the old behaviour either way.

Independent Volume was the only code path that wrote the macOS output
volume. It saved the current level, set the output to the selected cue
level for the length of the cue, then restored it, which is what let a
transcription cue change other applications' audio (altic-dev#522).

Compensating app-side instead does not preserve what the option
promised. The CoreAudio volume scalar and AVAudioPlayer.volume are
different gain domains, so a player gain derived from their ratio does
not land on the selected level, and it caps at full gain once the
selected level is above the current output.

Removing the option leaves one cue-volume slider, and nothing writes the
system volume any more. It also removes four failure modes that lived in
the save/restore path: a failed CoreAudio read was saved as 1.0 and
restored as maximum, the restore re-resolved the default output so
switching outputs mid-cue wrote one device's baseline onto another, the
delayed restore had no termination handler, and overlapping cues shared
one saved-volume slot.

The Independent Volume settings copy is removed with the toggle. It
described the old behaviour ("stays constant regardless of system
volume", "temporarily changes system volume during playback") and was
wrong in both directions either way.
@postoso
postoso force-pushed the fix/522-app-local-cue-gain branch from 40dd7c5 to e19f498 Compare September 1, 2026 23:38
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T23:40:37.539858Z e19f498 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

ℹ️ 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".

let transcriptionStartSound: SettingsStore.TranscriptionStartSound
let transcriptionSoundVolume: Float
let transcriptionSoundIndependentVolume: Bool
let autoUpdateCheckEnabled: Bool

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 Keep the deprecated field in same-schema backups

When a user exports a backup from this build and then restores it with the immediately preceding app version (for example, after downgrading), decoding fails because that version's synthesized SettingsBackupPayload decoder still requires transcriptionSoundIndependentVolume. The backup schema remains 1.0, so these files appear compatible but are reported as invalid JSON; retain the field in the encoded payload with a harmless fixed value, or introduce explicit schema migration/version handling.

Useful? React with 👍 / 👎.

@postoso postoso changed the title fix(audio): play transcription cues with app-local gain fix(audio): remove Independent Volume instead of writing system volume Sep 2, 2026
@postoso

postoso commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Pushed the revision, and updated the title and body to match. The Independent Volume toggle and the save/set/restore code are gone, so nothing in the app writes the Mac output volume any more, which is the part #522 was about. What is left is the single cue-volume slider: the cue plays at that level through AVAudioPlayer, so it scales with system output like any other sound.

The setting copy went with the toggle. It promised a level that stays constant regardless of system volume and warned that playback temporarily changes system volume, and both were wrong once the write is gone. I dropped the gain compensation and its tests rather than keep a reduced version of the option, since the compensation could not hold that promise anyway: the CoreAudio volume scalar and AVAudioPlayer.volume are different gain domains, so a player gain derived from their ratio does not land on the selected level.

I rebased on current main while I was in there. The diff is four files, +6/-132, and CI is green including the test run, which also clears the DirectAudioReliabilityTests failure that was on the old head.

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.

Independent volume selection causes temporary system volume spike

2 participants