USB iso OUT: pace packets by audio rate, not wMaxPacketSize#94
Merged
Conversation
`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>
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.
Summary
libusb_set_iso_packet_lengthswas being called with the endpoint'swMaxPacketSize(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:
That last number matches the observed
nativeWritereturn 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-standardAudioTrackpath was unaffected because the kernel UAC driver computes the per-frame byte count from the negotiated sample rate.Fix
Use the three
outputSampleRate/outputChannels/outputBytesPerSampleparameters (which were always being passed from Java but ignored bynativeWrite) to compute the correct per-frame byte count for USB FS:Apply it to both buffer allocation and
libusb_set_iso_packet_lengths. Fall back tomaxPacketSize(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
nativeWritefor 12.64 s of audio used to return at +12.14 s; now returns at +12.64 s (verified indebug.logtiming).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