Repository navigation
Fix RX: MediaTek USB audio HAL capture + reinit lifecycle bugs (split from #925) - #949
Open
laniol8926 wants to merge 6 commits into
Open
laniol8926 wants to merge 6 commits into
laniol8926 wants to merge 6 commits into
Conversation
Picking a new input device in Settings persisted GeneralVariables.audioInputDeviceId and audioInputVendorId/ProductId, but nothing told the already-running MicRecorder to reopen with it -- MicRecorder only reads those fields at construction time (openAudioRecord()'s setPreferredDevice() / openUsbAudioInput()). So the new selection silently had no effect on the live capture until the next USB attach/detach or app restart happened to rebuild it. Confirmed on real hardware (Edge 2022 + QMX): logcat showed writeConfig persisting the new device id with no MicRecorder reinit or preferredDevice line following it, and the running capture stayed on audioInputDeviceId=0 (the phone's built-in mic) the whole session -- exactly the silent-no-op the user suspected. Mirrors the RX-channel-change (onRxAudioChannelChanged) and Bluetooth-SCO-connect (refreshBluetoothHeadsetMode) paths, which already call reinitializeAudioInput() for the same reason. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
reinitialize() decided whether to restart capture by reading isRunning -- but a transient AudioRecord creation failure (AudioFlinger createRecord_l returning -1, observed in the field right after a replug/power-cycle, before the audio HAL had caught up) sets isRunning false as a side effect from the capture thread's own failure-detection path, with no owner-driven stopRecord() ever happening. Every reinitialize() after that read the now-stale isRunning as false and silently skipped start() -- permanently, even once the operator picked a completely valid device afterward (confirmed on real hardware: setPreferredDevice() kept reporting ok=true/deviceFound=true, and AudioFlinger genuinely created the record track, but start() was simply never called again). Fix: track the owner's actual intent in a new field, shouldBeRunning, touched only by the two owner-facing entry points (start()/ stopRecord()) and left alone by transient internal failures. reinitialize() now keys its end-of-method restart decision off that instead of isRunning, and does its own internal stop/rebuild teardown inline rather than through stopRecord() (which would otherwise clear the very flag it needs to survive). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Confirmed on real hardware (QMX over USB Audio Class, MediaTek audio HAL) that requesting ENCODING_PCM_FLOAT from a USB input silently produces unusable samples on this HAL -- AudioALSAStreamIn's own log line, "wrong format(0x5), default use wordSize = 2", shows it falling back to a 16-bit word size internally while still being asked for 4-byte floats. Reproduced twice in two single app processes with everything else held constant (same CAT mode, same device, same session): the HAL occasionally renegotiates to 16-bit PCM across a USB re-enumeration, and decoding started working immediately every time that happened, then stopped immediately on the next reinit back to float. This is what the whole night's RX investigation was actually chasing -- not mode, not audio-device selection, not I/Q mode (the radio decoded fine in Digi/MD6 with fmt=1 negotiated and no Q90; ever sent, which also rules that fix out as the deciding factor here, even though it's independently correct for the FT8TW#78 scenario). MicRecorder now requests ENCODING_PCM_16BIT (which is what the commented-out line already showed as the original format before an earlier change switched to float) and converts to float in the reader loop via the new int16ToFloat(), matching float2Short()'s existing inverse scale in the TX path. Covered by a new unit test file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Trims the audioFormat field comment to the why in a few lines (per Copilot review feedback on PR patrickrb#925, where this fix originated, carried over here since it's the same underlying change). Extracts reinitialize()'s restart check into MicRecorder.shouldRestartAfterReinitialize(boolean), matching this file's existing convention for pinning a load-bearing boolean decision with its own test (captureLoopShouldContinue, shouldJoinCaptureThread), and adds MicRecorderRestartDecisionTest covering both scenarios: restart after a transient failure left isRunning false without stopRecord(), and no restart after stopRecord() itself cleared shouldBeRunning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Author
|
Couldn't self-apply the |
5 of 6 tasks
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #949 +/- ##
============================================
+ Coverage 41.70% 41.92% +0.22%
Complexity 252 252
============================================
Files 280 280
Lines 34002 34105 +103
Branches 4076 4088 +12
============================================
+ Hits 14180 14299 +119
+ Misses 19531 19494 -37
- Partials 291 312 +21
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
persisted AudioDeviceInfo id goes stale Confirmed on real hardware: power-cycling a QMX alone re-enumerated its USB audio interface with a new AudioDeviceInfo id, reverting the Settings input-device picker (and the actual MicRecorder/FT8TransmitSignal routing) to "Default" even though the same physical device was still attached -- AudioDeviceInfo's numeric id is not guaranteed stable across a USB reconnect, but its product name is, and the app already persists the device's VID:PID whenever it's picked (just never used it as a fallback). Added UsbAudioDevice.resolveDeviceIndex: exact id match first, else match by the product name of the live USB device whose VID:PID equals what was persisted. Wired into MicRecorder.findAudioDeviceById (input), FT8TransmitSignal.findAudioDeviceById (output), and AudioDeviceSpinnerAdapter.getPositionByDeviceId (the Settings picker's own re-selection on screen open) -- all three previously only matched by exact id, so a power-cycle silently fell back to Default in all of them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
persisted for framework picks, and exact name match fails on the HAL's
decorated name
Blocker 1: picking a framework AudioManager USB device (e.g. "USB-Audio -
QMX Transceiver") always persisted VID:PID 0:0 -- getUsbAudioDeviceInfo()
only returns non-null for the separate USB-direct list, so the picker's
`else if (deviceId != -1)` branch unconditionally zeroed it, leaving
resolveDeviceIndex's fallback with nothing to fall back to. Fixed by
looking up the raw UsbDevice by product-name match
(AudioDeviceSpinnerAdapter.getUsbFrameworkDeviceProductName +
UsbAudioDevice.matchByProductName) and persisting ITS VID:PID instead, in
both RadioAudioSettings.kt (Compose) and ConfigFragment.java (legacy).
Blocker 2: even with VID:PID persisted correctly, UsbDevice.getProductName()
("QMX Transceiver", confirmed via dumpsys usb) never equals the
AudioDeviceInfo list's name for the same card ("USB-Audio - QMX
Transceiver", confirmed via dumpsys audio) -- Android's generic USB-Audio
HAL decorates the raw descriptor name. resolveDeviceIndex's match (and the
new matchByProductName) now use case-insensitive containment instead of
equality.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
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.
Debug APK
Fork PR — CI builds the debug APK on
ft8af/**changes but can't post a comment here (read-only token). See theDebug APK for manual testingjob's summary page on this PR's checks tab once it finishes.Device test
needs-device-test): changesMicRecorder's capture format (float → PCM16) and its reinitialize/restart lifecycle, which affects USB-audio and standard-mic RX on every rig, not just the one it was found on.Manual test checklist
AudioRecordcreation failure right after a replug could permanently stop capture for the rest of the app session).What changed
Split out of PR #925 (QRP Labs QMX via hamlib) per review feedback: that PR found and fixed three
MicRecorderbugs during an all-night real-hardware RX investigation (full narrative in that PR's comment thread), but none of the three are QMX-specific — they affect every rig's RX audio capture — so they're reviewed/tested here independently of the QMX catalog change.onSelectpersisted the new device id but never told the already-runningMicRecorderto rebuild with it — it only reads that field at construction time. Fixed by calling the existingreinitializeAudioInput()from the picker, mirroring the RX-channel-change and Bluetooth-SCO-connect paths, which already did this for the same reason.AudioRecordcreation failure permanently stopped capture for the rest of the app session.reinitialize()decided whether to restart by readingisRunning, but a transient failure (observed right after a USB replug/power-cycle, before the audio HAL caught up) setsisRunning = falseas a side effect with no owner action — so every laterreinitialize()(even the operator picking a perfectly valid device afterward) sawisRunningalready false and silently skipped restarting. Fixed with a separateshouldBeRunningintent flag, touched only bystart()/stopRecord(), with the restart decision keyed off that instead (shouldRestartAfterReinitialize, now unit-tested).ENCODING_PCM_FLOATsilently breaks capture on at least one real MediaTek USB audio HAL. Found via a precise real-hardware A/B (same running app process, same CAT mode, same device — only the HAL's negotiated format differed across a USB re-enumeration): every float-format open decoded nothing, every PCM16-format open decoded normally. The HAL's own log explains why: it falls back to a 16-bit word size internally while still being asked to hand back 4-byte floats, producing unusable samples with no error surfaced anywhere. Fixed by requestingENCODING_PCM_16BITand converting to float in software in the reader loop (int16ToFloat).AudioDeviceInfoid is not stable across a reconnect — confirmed on real hardware when a QMX power-cycle alone reverted the Settings input picker to "Default".MicRecorder.findAudioDeviceById,FT8TransmitSignal.findAudioDeviceById, andAudioDeviceSpinnerAdapter.getPositionByDeviceIdall matched only by exact numeric id, so a stale id after any USB re-enumeration silently fell back to the default device/display, even though the app already persists the device's VID:PID on every pick. Fixed withUsbAudioDevice.resolveDeviceIndex: exact id match first, else fall back to the product name of the live USB device whose VID:PID matches what was persisted (product name doesn't change across reconnects). Applied symmetrically to both input and output.dumpsysdata while testing Fix 26 bugs: crashes, resource leaks, encoding errors, RTL support #4: (a) picking a framework AudioManager USB entry (as opposed to the separate USB-direct list) always persisted VID:PID0:0— nothing populated it for that path — so the fallback above had nothing to fall back to; fixed by looking up the matching rawUsbDeviceby product name at pick time (AudioDeviceSpinnerAdapter.getUsbFrameworkDeviceProductName+UsbAudioDevice.matchByProductName) and persisting its VID:PID, in both the Compose picker and the legacyConfigFragment. (b) Even with VID:PID persisted correctly,UsbDevice.getProductName()("QMX Transceiver") never equals theAudioDeviceInfolist's name for the same card ("USB-Audio - QMX Transceiver") — the generic USB-Audio HAL decorates the raw name — so the match needed to be case-insensitive containment, not equality.Risks
Medium. This changes the capture audio format and the restart/reinit lifecycle for every rig's RX path, not just the one it was diagnosed on (a Motorola Edge 2022, Android 15, QMX over USB Audio Class via MediaTek's audio HAL). PCM16 is the format Android's
AudioRecordguarantees support for, and 24-bit-capable USB codecs losing precision to 16-bit is not meaningful for FT8/FT4 decoding — but this hasn't been smoke-tested against other USB audio interfaces (IC-7300/705, CM108/Digirig) or Bluetooth SCO mics, which is exactly what this PR's own manual test checklist (item 4) and QA pass should confirm before merge.Tested automatically
MicRecorderInt16ToFloatTest(new): the PCM16→float conversion applied to every capture buffer — full-scale positive/negative, silence, and partial-length reads.MicRecorderRestartDecisionTest(new):shouldRestartAfterReinitialize— restarts when a transient failure leftisRunningfalse withoutstopRecord(); does not restart afterstopRecord()itself clearedshouldBeRunning.UsbAudioDeviceResolveIndexTest(new):resolveDeviceIndex— exact id match, product-name fallback when the id isn't found (incl. the real decorated-name mismatch), no match when there's nothing to fall back to, null product-name entries don't throw.UsbAudioDeviceNameMatchTest(new):unambiguousNameMatchIndex(the pure decision behindmatchByProductName) — single containment match, no match, ambiguous match (two candidates) refuses to guess, blank/null input, null entries in the candidate list.Code review
QA evidence
(to be filled in by QA)
🤖 Generated with Claude Code
https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf