Repository navigation
Fix SCO over USB: align packets to the alternate setting, select it per SCO link - #1011
Merged
Merged
Conversation
Each HCI SCO packet sent over the USB isochronous endpoint spans 3 USB frames, with the HCI header at the start of the first one (Core Spec Vol 4, Part B, 2.1.1), e.g. 48 data bytes per packet with alternate setting 2. The USB sink sent each HCI SCO packet as-is, split into max-packet-size chunks, and only one isochronous transfer at a time, shared with ACL data and commands: - packets that don't fill exactly 3 USB frames are dropped by controllers such as Intel BE200, so nothing was heard on the remote side, - one transfer at a time leaves gaps between transfers, so SCO data was sent at ~91% of the link rate, which distorts the audio. Send SCO data on its own path instead: re-packetize the data of each connection into HCI SCO packets that exactly fill 3 isochronous packets (keeping the remainder for the next packet), and keep several isochronous OUT transfers in flight. Also make find_endpoints() return the isochronous endpoints of the selected alternate setting rather than those of the last setting it iterated over.
There was a problem hiding this comment.
Code Review
This pull request refactors SCO data transmission over USB isochronous endpoints in bumble/transport/usb.py by implementing a multi-transfer queue to keep transfers in flight and introducing a re-packetization mechanism for SCO data. It also adds comprehensive unit tests in tests/transport/usb_test.py. The review feedback identifies a potential infinite loop in on_sco_packet if the calculated packet data size is zero, and suggests reducing the log level of cancelled transfers during shutdown from warning to debug to prevent noisy logs.
zxzxwu
force-pushed
the
fix-usb-sco-out
branch
from
October 16, 2026 19:41
f9ad700 to
a8c427b
Compare
…shed With sco=0, the alternate setting with the largest max packet size was selected when the transport was opened (alternate setting 6 on Intel BE200), before any SCO link existed. BE200 then neither sends nor receives any SCO data. Like Linux btusb, keep alternate setting 0 while there is no SCO link, and select the alternate setting when SCO links are established or closed, before the host gets the event (Core Spec Vol 4, Part B, Table 2.1): - transparent air mode (mSBC, LC3 SWB): alternate setting 6, or 1 if not available, - otherwise, 16-bit samples on HCI: 2, 4 or 5 for 1, 2 or 3 links; 8-bit samples: the number of links. The sample size is tracked from HCI_Write_Voice_Setting and HCI_Enhanced_Setup/Accept_Synchronous_Connection. With alternate setting 6, each HCI SCO packet (3 + 60 bytes, one 7.5 ms codec frame) is sent in a single USB frame, back to back, so codec frames are aligned to HCI SCO packets in both directions. Empty SCO packets received around alternate setting changes are dropped. An explicit alternate setting (sco=N) is still used as-is.
zxzxwu
force-pushed
the
fix-usb-sco-out
branch
from
October 16, 2026 19:42
a8c427b to
f2f1d41
Compare
- Don't set up SCO OUT transfers when the max packet size leaves no room for SCO data (or more than 255 bytes), which made on_sco_packet() loop forever. - Log cancelled SCO OUT transfers (transport closed, alternate setting changed) at debug level, and don't reuse them: only transfers of the current alternate setting are returned to the free queue.
- Add ScoEndpoints (frozen dataclass) for the isochronous endpoints of a SCO alternate setting, and a _find_isochronous_endpoints() helper shared by find_endpoints() and find_sco_settings(), using max()/comprehensions instead of nested loops. - Select the alternate setting with next(), and find the configuration with next() in open_usb_transport(). - Parse the HCI events and commands used for the dynamic alternate setting with HCI_Event/HCI_Command.from_bytes() and match statements, instead of hard-coded offsets. - Factor out transfer cancellation and queue draining, and reuse start_sco() in UsbPacketSource.start(). - Annotate types (usb1 classes, queues, tasks) of the code added for SCO.
zxzxwu
force-pushed
the
fix-usb-sco-out
branch
from
October 19, 2026 07:04
fc24e8a to
54d015d
Compare
barbibulle
approved these changes
Oct 22, 2026
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.
Fix SCO over USB: align packets to the alternate setting, select it per SCO link
Problems
UsbPacketSinksent each HCI SCO packet from the host as-is (e.g. 120 bytes of CVSD, or a 60-byte mSBC/LC3 frame), split into max-packet-size chunks. Intel BE200 drops such packets, so the remote side received silence.sco=0picked the alternate setting at open time. The setting with the largest max packet size was selected before any SCO link existed (alternate setting 6 on BE200). BE200 then neither sends nor receives any SCO data, in either direction.Fix
Send SCO data on its own path, separate from ACL data and commands, with several (
NUMBER_OF_SCO_OUT_TRANSFERS = 4) isochronous OUT transfers in flight.Re-packetize the data of each connection handle into HCI SCO packets that exactly fill 3 isochronous packets (
3 * max_packet_size - 3data bytes), keeping the remainder for the next packet.With alternate setting 6 (Core Spec Vol 4, Part B, Table 2.1), each HCI SCO packet (3 + 60 bytes, i.e. one 7.5 ms mSBC / LC3 SWB frame) is sent in a single USB frame, back to back (BE200 does not accept empty frames in between, like
BTUSB_ALT6_CONTINUOUS_TXin Linux). Codec frames are then aligned to HCI SCO packets in both directions, so the host doesn't need to reassemble them.With
sco=0, select the alternate setting like Linux btusb: alternate setting 0 while there is no SCO link, then, when SCO links are established or closed (before the host gets the event):HCI_Write_Voice_SettingandHCI_Enhanced_Setup/Accept_Synchronous_Connection.An explicit alternate setting (
sco=N) is still used as-is.Drop empty SCO packets received around alternate setting changes.
find_endpoints()now returns the isochronous endpoints of the selected alternate setting, instead of those of the last setting it iterated over.Testing
Unit tests (18 passed in
tests/transport/usb_test.py, 1046 passed for the whole suite): re-packetization for alternate settings 1, 2 and 6 with multiple transfers in flight; alternate setting selection for transparent (with / without alternate setting 6) and CVSD (1–2 links, 16- and 8-bit samples); empty SCO packets dropped;find_endpoints()returning the selected setting's endpoints. The new tests fail onmain(9 failures when the test file is run against the unfixed tree), so they do exercise the new behaviour.Intel BE200 as HFP Hands-Free (
usb:<path>+sco=0), Pixel phone as Audio Gateway, 1 kHz tone in both directions, unmodified test:Before this change, with
sco=0: no SCO data in either direction. With an explicitsco=1/sco=2, the phone received silence.Realtek (2x ASUS BT600,
0b05:1d70,usb:<path>+sco=0), the two dongles connected over the USB transport, legacySetup Synchronous Connectiononly (this controller does not implement the enhanced commands). Every isochronous transfer bumble submitted was recorded through libusb, so the framing below is what actually went on the wire; the payload was a 1 kHz 16-bit PCM tone, and a self-identifying payload pattern for the byte-exactness check:The transparent path fixes the same failure as on BE200: with
sco=0,mainselects setting 6 before any SCO link exists and delivers no SCO data at all on this dongle, while with this change the setting is chosen once the link is established and the audio arrives intact. Disconnecting and re-establishing a link works as well (the setting goes back to 0, then to 6 for the new link).Notes from that run:
sco=Non this dongle, no SCO data is received in either direction, onmainand with this change: the setting has to be chosen after the link is established, which is now whatsco=0does.mainor here: the negotiated link has a 60-byte air payload, while alternate setting 2 can only carry 48 bytes of HCI data per packet (and selecting 6 before the link delivers nothing either).maindelivers no SCO data at all on this dongle, so this is not a regression, but CVSD stays unverified on Realtek.sco_packet_data_size0 andon_sco_packet()loop forever, becauseconfigure_sco()only checksgetMaxPacketSize() > 0(unreachable on the controllers tested, whose settings are 9–63 bytes). The guard suggested in review (max_packet_size * USB_FRAMES_PER_SCO_PACKET > HCI_SCO_HEADER_SIZE) closes that hole.