Skip to content

USB iso OUT: pace packets by audio rate, not wMaxPacketSize#94

Merged
patrickrb merged 1 commit into
mainfrom
fix/usb-iso-out-packet-size
Jun 3, 2026
Merged

USB iso OUT: pace packets by audio rate, not wMaxPacketSize#94
patrickrb merged 1 commit into
mainfrom
fix/usb-iso-out-packet-size

Conversation

@patrickrb
Copy link
Copy Markdown
Owner

Summary

libusb_set_iso_packet_lengths was being called with the endpoint's wMaxPacketSize (200 bytes for a C-Media CM108 at 48 kHz stereo 16-bit) but USB Audio Class devices play back exactly however many bytes per frame the host hands them. Sending 200 bytes per 1 ms frame at a negotiated 48 kHz rate makes the device clock samples out 200/192 = 4.17 % faster than negotiated — every audio tone gets shifted up by that ratio.

For FT8 specifically:

  • 1000 Hz audio offset → 1042 Hz on the air
  • WSJT-X tone spacing 6.25 Hz → ~6.51 Hz (off the decoder's tone grid)
  • 12.64 s message duration → 12.14 s real time

That last number matches the observed nativeWrite return timing exactly, and the off-grid tones are why receivers couldn't decode our signal even though the rig was happily putting out clean ALC-correct 70 W of properly modulated SSB. The Android-standard AudioTrack path was unaffected because the kernel UAC driver computes the per-frame byte count from the negotiated sample rate.

Fix

Use the three outputSampleRate/outputChannels/outputBytesPerSample parameters (which were always being passed from Java but ignored by nativeWrite) to compute the correct per-frame byte count for USB FS:

int bytesPerFrame = (outputSampleRate * outputChannels * outputBytesPerSample) / 1000;

Apply it to both buffer allocation and libusb_set_iso_packet_lengths. Fall back to maxPacketSize (with a loud log) if the format args are bad, so unrecognized devices degrade to wrong-pitch audio rather than a silent abort.

Test plan

  • Pixel 8 + FT-891 + C-Media CM108 (0D8C:0012): nativeWrite for 12.64 s of audio used to return at +12.14 s; now returns at +12.64 s (verified in debug.log timing).
  • FT8 tones now land on the WSJT-X grid → spots appear on PSKReporter when transmitting CQ on 14.074 MHz.
  • Sanity check: car-dash USB CODEC path (the original libusb iso target from PR USB Audio: libusb iso OUT for TX (companion to #83) #84) still works.

Notes

This is independent of #93 (Costas chop) but both are required for direct-USB-audio TX to actually decode. The author tested the local build with both PRs applied; this PR text only covers the iso fix.

🤖 Generated with Claude Code

`libusb_set_iso_packet_lengths` was being called with the endpoint's
wMaxPacketSize (200 bytes for C-Media CM108 at 48kHz stereo 16-bit) but
USB Audio Class devices play back exactly however many bytes per frame
the host hands them. Sending 200 bytes per 1ms frame at a negotiated
48kHz rate makes the device clock samples out 200/192 = 4.17% faster
than negotiated, time-stretching every FT8 tone:

  - 1000 Hz audio offset → 1042 Hz on the air
  - WSJT-X tone spacing 6.25 Hz → ~6.51 Hz
  - 12.64s message duration → 12.14s real time

That last number matches the observed nativeWrite return timing exactly,
and the off-grid tones are why receivers couldn't decode our signal even
though the rig was happily putting out clean ALC-correct 70W of properly
modulated SSB. The Android-standard AudioTrack path was unaffected
because the kernel UAC driver computes the per-frame byte count from
the negotiated sample rate.

Use the three unused outputSampleRate/outputChannels/outputBytesPerSample
parameters (they were always being passed from the Java side, just
ignored by the native fn) to compute the correct per-frame size for
USB FS (sampleRate * channels * bytes / 1000) and apply it to both
buffer allocation and `libusb_set_iso_packet_lengths`. Fall back to
maxPktSize with a loud error if the format args are bad, so an
unrecognized device degrades to wrong-pitch audio rather than a silent
abort.

Verified locally on Pixel 8 + FT-891 + C-Media CM108:
  - Before: nativeWrite returns at +12.14s for 12.64s of audio
  - After:  nativeWrite returns at +12.64s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@patrickrb patrickrb merged commit cdbbcf7 into main Jun 3, 2026
2 of 3 checks passed
@patrickrb patrickrb deleted the fix/usb-iso-out-packet-size branch June 3, 2026 16:19
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