Skip to content

Hold exclusive audio focus during AudioTrack TX and Tune#597

Open
patrickrb wants to merge 1 commit into
devfrom
feat/tx-audio-focus
Open

Hold exclusive audio focus during AudioTrack TX and Tune#597
patrickrb wants to merge 1 commit into
devfrom
feat/tx-audio-focus

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

When the radio's USB sound card is used as an Android output device (the AudioTrack TX path), the transmit waveform goes through Android's shared mixer. Any sound another app makes during the 12.6 s transmission — touch/keyboard clicks, notification dings, media — is mixed into the same stream and transmitted on air. Field symptom: using other apps while FT8AF transmits makes the radio "click" with phone activity, and those sounds corrupt the FT8 waveform.

Fix

New TxAudioFocus helper requests AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE for the duration of playback and abandons it on teardown:

  • FT8 TX (playFT8Signal AudioTrack branch): acquired just before the track is created, released in afterPlayAudio(). The CAT and direct-USB (libusb) branches never acquire it, and release is a safe no-op there.
  • Tune carrier (playTuneTone): its own instance, acquired in the worker and released in the existing finally teardown, so tune and TX can never release each other's focus.

While held, the system suppresses notification sounds and asks other apps to pause; between cycles the phone behaves normally. Focus is advisory — a denied request is logged to debug.log and TX proceeds regardless. Grant/deny outcome is also logged per cycle for field diagnosis.

Known limits (documented in the class javadoc): focus cannot block system touch sounds or apps that ignore focus; the direct-USB output mode remains the bulletproof option since it bypasses the mixer entirely.

Testing

  • New TxAudioFocusTest (9 Robolectric tests): grant/deny/null-context acquire, transient-exclusive gain type, idempotent acquire, release/abandon semantics, no-op release, re-acquire after release.
  • Full testDebugUnitTest suite passes.
  • Not yet field-verified on the rig (phone's single USB-C port was not available); the AudioTrack code path itself is unchanged apart from the focus acquire/release.

🤖 Generated with Claude Code

The AudioTrack TX path plays through Android's shared mixer, so sounds
from other apps (touch clicks, notifications, media) routed to the same
output device were mixed into the audio feeding the rig and transmitted
on air, corrupting the FT8 waveform.

Add TxAudioFocus, which requests AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE for
the duration of each transmission (and the Tune carrier) and abandons it
on teardown. The system suppresses notification sounds and asks other
apps to pause while it is held. Focus is advisory, so a denied request
is log-only and TX always proceeds. The CAT and direct-USB (libusb)
paths never acquire focus; release in afterPlayAudio() is a no-op there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 29.41%. Comparing base (647b12e) to head (d3a8e18).
⚠️ Report is 3 commits behind head on dev.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #597      +/-   ##
============================================
+ Coverage     29.36%   29.41%   +0.04%     
  Complexity      197      197              
============================================
  Files           179      179              
  Lines         24068    24097      +29     
  Branches       3263     3268       +5     
============================================
+ Hits           7067     7087      +20     
- Misses        16780    16787       +7     
- Partials        221      223       +2     
Flag Coverage Δ
android 14.62% <ø> (+0.10%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

Pull request overview

This PR adds transient-exclusive audio focus management to the Android AudioTrack transmit paths so that other apps’ sounds (notifications/media/etc.) are less likely to be mixed into the over-the-air TX audio during FT8 transmissions and Tune carrier playback.

Changes:

  • Introduces a new TxAudioFocus helper to acquire/release AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE around TX playback windows.
  • Integrates focus acquisition/release into FT8TransmitSignal for the AudioTrack FT8 TX branch and the Tune worker.
  • Adds Robolectric unit tests covering focus acquisition/denial/idempotency and release semantics.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/TxAudioFocus.java New helper to request/abandon transient-exclusive audio focus during TX.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java Acquires audio focus for AudioTrack FT8 TX and Tune, and releases on teardown.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/TxAudioFocusTest.java New Robolectric tests validating focus request parameters and lifecycle behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +57 to +64
request = new AudioFocusRequest.Builder(
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setOnAudioFocusChangeListener(focusChangeListener)
.build();
Comment on lines +39 to +45
@Test
public void acquire_requestsTransientExclusiveGain() {
focus.acquire(context);

assertThat(shadowAudioManager.getLastAudioFocusRequest().durationHint)
.isEqualTo(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE);
}
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.

2 participants