Skip to content

USB Audio: libusb iso OUT for TX (companion to #83)#84

Merged
patrickrb merged 1 commit into
mainfrom
feat/libusb-tx-output
Jun 1, 2026
Merged

USB Audio: libusb iso OUT for TX (companion to #83)#84
patrickrb merged 1 commit into
mainfrom
feat/libusb-tx-output

Conversation

@patrickrb
Copy link
Copy Markdown
Owner

Summary

Follow-up to #83. That PR fixed the input path on the car-dash; this one fixes the output path the same way. The first end-to-end QSO test on the car-dash caught the symmetric bug — (USB direct) selected for Audio Output causes TX to abort after ~286ms because UsbAudioDevice.writeAudio() calls UsbRequest.initialize() on the OUT endpoint, the car-dash kernel returns false (same iso-not-supported behavior as input), writeAudio returns immediately, FT8TransmitSignal.playViaUsbAudio short-circuits to afterPlayAudio(), and the PTT releases before any audio is sent.

What this gives you

With both #83 (RX) and this PR (TX) shipped, (USB direct) selected for both Audio Input and Audio Output, the FT8 audio pipeline becomes completely independent of Android's audio framework:

  • TX audio goes straight to the radio's USB iso OUT endpoint via libusb / USBFS.
  • No AudioTrack session is opened, so Android's audio policy never tries to route TX audio anywhere.
  • Spotify / YouTube / Maps / Android Auto can keep playing through the car speakers uninterrupted while FT8 transmits — no ducking, no fight for the speaker, no rerouting.
  • Same for the input side via USB Audio: libusb-backed iso capture (fixes car-dash) #83: media output and FT8 capture are two independent pipelines.

What's in it

  • usb_audio_capture.cpp: new OutputSession + onOutputComplete callback. Same N=4 transfers × 8 packets pattern as the capture side. The caller blocks on libusb_handle_events_timeout_completed until the in-flight counter drops to zero. Errors propagate back as libusb error codes.
  • UsbAudioNative.nativeWrite(): synchronous JNI entry point. Takes the fd + endpoint params + pre-formatted PCM byte buffer (Java still does float→int16 + channel duplication, same as before) and returns 0 on success or a negative LIBUSB_ERROR_* on failure.
  • UsbAudioDevice.writeAudio(): try the libusb path first, fall back to the original UsbRequest loop on non-zero return. Both paths log to debug.log so the next car-dash run will tell us exactly which one took the TX.

Test plan

  • assembleDebug compiles cleanly across all four ABIs. JNI symbol Java_com_bg7yoz_ft8cn_wave_UsbAudioNative_nativeWrite is present in libft8af_usb.so.
  • Pixel 8 smoke test before merge (not yet run — Pixel was unplugged when I tried installDebug after pushing). Need to confirm the existing UsbRequest output path doesn't regress on devices where it was already working, AND that the libusb path can drive an iso OUT endpoint on the Pixel.
  • Car-dash via Play internal track after merge: set Audio Output to (USB direct), run a full TX cycle. Expected lines in debug.log:
    serial.send[4]: MX1;
    UsbAudioDevice: trying libusb native write fd=... ep=0x... bytes=...
    nativeWrite: 4 transfers submitted, pcmLen=... ep=0x... maxPkt=...
    UsbAudioDevice: libusb native write OK
    serial.send[4]: MX0;       ← ~12.6s after MX1, not 286ms
    
  • A/B test on car-dash: with (USB direct) selected for output, start Spotify or another music app. Confirm music keeps playing through car speakers while FT8 TX uses the USB radio. This is the user-visible benefit, not just absence of bugs.

🤖 Generated with Claude Code

Mirrors the input-side libusb capture work. The car-dash log from
the first end-to-end QSO test caught the symmetric bug on the output
side: with Audio Output set to (USB direct), UsbAudioDevice.writeAudio()
calls UsbRequest.initialize() on the OUT endpoint, the car-dash kernel
returns false (same iso-not-supported behavior as the input path),
writeAudio returns immediately, FT8TransmitSignal.playViaUsbAudio
short-circuits to afterPlayAudio(), and the PTT releases ~286ms after
keying. TX appears to "abort" twice in a row.

This commit adds a synchronous nativeWrite() that mirrors the input
path's design:

- usb_audio_capture.cpp: new OutputSession + onOutputComplete callback,
  same N=4 transfers × 8 packets pattern as the capture side. Caller
  blocks on libusb_handle_events_timeout_completed until in-flight
  drops to zero. Errors propagate back as libusb error codes.
- UsbAudioNative.nativeWrite(): synchronous JNI entry point. Takes the
  fd + endpoint params + pre-formatted PCM byte buffer (Java still
  does float->int16 + channel duplication, same as before) and
  returns 0 on success or a negative LIBUSB_ERROR_* on failure.
- UsbAudioDevice.writeAudio(): try the libusb path first, fall back
  to the original UsbRequest loop on non-zero return. Both paths log
  to debug.log so the next car-dash run will tell us exactly which
  one took the TX.

Build verified: assembleDebug compiles cleanly across all four ABIs,
the JNI symbol Java_com_bg7yoz_ft8cn_wave_UsbAudioNative_nativeWrite
is present in libft8af_usb.so. NOT YET smoke-tested on the Pixel —
the device was unplugged when I tried to install. Logically symmetric
with the input path that's already proven working in the field, so
the risk profile is the same as the input PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@patrickrb patrickrb merged commit 31c5f2e into main Jun 1, 2026
2 checks passed
@patrickrb patrickrb deleted the feat/libusb-tx-output branch June 1, 2026 20:51
patrickrb added a commit that referenced this pull request Jun 1, 2026
jonstacks pushed a commit to jonstacks/FT8AF that referenced this pull request Jun 3, 2026
The car-dash TX-abort symptom (PTT keys → 250-300ms → PTT releases,
no audio sent) is still happening despite patrickrb#84 shipping the libusb
nativeWrite. But the full TX output path is logged only to logcat, so
from debug.log there's no way to tell:

  1. Which output branch ran (USB direct vs. AudioTrack).
  2. If USB direct ran, which step failed before reaching writeAudio
     (device lookup, USB permission, open, hasOutput, activateOutput).

Same instrumentation pattern as the input side that surfaced the real
bugs in patrickrb#82 / patrickrb#83.

Changes:
- playFT8Signal: log the branch decision with audioOutputDeviceId and
  the saved USB output VID:PID. Log which branch was taken.
- playViaUsbAudio: fileLog at each failure point and at the success
  pivots (device opened + output activated, before writeAudio,
  writeAudio return value). Every failure path has a distinct
  message so the failure point is obvious from one log line.

After the next car-dash repro we'll know whether the abort is:
- audioOutputDeviceId not being -1 at TX time (picker reset?),
- device lookup failing (UsbManager state),
- permission lost on re-attach,
- activateOutput failing on the car-dash kernel,
- or actually reaching writeAudio and libusb dying inside.

Co-Authored-By: Claude Opus 4.7 (1M context) <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.

1 participant