multiple packets per transfer

This commit is contained in:
Gilles Boccon-Gibod
2026-05-29 08:47:28 +02:00
parent 9b2e345a1e
commit 5ee2d80ce4
4 changed files with 80 additions and 49 deletions
+15 -13
View File
@@ -1721,6 +1721,15 @@ class CodecID(SpecableEnum):
VENDOR_SPECIFIC = 0xFF
# From Bluetooth Assigned Numbers, 2.10 PCM_Data_Format
class PcmDataFormat(SpecableEnum):
NA = 0x00
ONES_COMPLEMENT = 0x01
TWOS_COMPLEMENT = 0x02
SIGN_MAGNITUDE = 0x03
UNSIGNED = 0x04
@dataclasses.dataclass(frozen=True)
class CodingFormat:
codec_id: CodecID
@@ -1729,7 +1738,7 @@ class CodingFormat:
@classmethod
def parse_from_bytes(cls, data: bytes, offset: int) -> tuple[int, CodingFormat]:
(codec_id, company_id, vendor_specific_codec_id) = struct.unpack_from(
codec_id, company_id, vendor_specific_codec_id = struct.unpack_from(
'<BHH', data, offset
)
return offset + 5, cls(
@@ -2063,7 +2072,7 @@ class HCI_Object:
)
continue
(field_name, field_type) = object_field
field_name, field_type = object_field
result += HCI_Object.serialize_field(hci_object[field_name], field_type)
return bytes(result)
@@ -3106,8 +3115,8 @@ class HCI_Enhanced_Setup_Synchronous_Connection_Command(HCI_AsyncCommand):
output_coding_format: int = field(metadata=metadata(CodingFormat.parse_from_bytes))
input_coded_data_size: int = field(metadata=metadata(2))
output_coded_data_size: int = field(metadata=metadata(2))
input_pcm_data_format: int = field(metadata=metadata(1))
output_pcm_data_format: int = field(metadata=metadata(1))
input_pcm_data_format: int = field(metadata=PcmDataFormat.type_metadata(1))
output_pcm_data_format: int = field(metadata=PcmDataFormat.type_metadata(1))
input_pcm_sample_payload_msb_position: int = field(metadata=metadata(1))
output_pcm_sample_payload_msb_position: int = field(metadata=metadata(1))
input_data_path: int = field(metadata=metadata(1))
@@ -3118,13 +3127,6 @@ class HCI_Enhanced_Setup_Synchronous_Connection_Command(HCI_AsyncCommand):
packet_type: int = field(metadata=metadata(2))
retransmission_effort: int = field(metadata=metadata(1))
class PcmDataFormat(SpecableEnum):
NA = 0x00
ONES_COMPLEMENT = 0x01
TWOS_COMPLEMENT = 0x02
SIGN_MAGNITUDE = 0x03
UNSIGNED = 0x04
class DataPath(SpecableEnum):
HCI = 0x00
PCM = 0x01
@@ -3171,8 +3173,8 @@ class HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(HCI_AsyncComman
output_coding_format: int = field(metadata=metadata(CodingFormat.parse_from_bytes))
input_coded_data_size: int = field(metadata=metadata(2))
output_coded_data_size: int = field(metadata=metadata(2))
input_pcm_data_format: int = field(metadata=metadata(1))
output_pcm_data_format: int = field(metadata=metadata(1))
input_pcm_data_format: int = field(metadata=PcmDataFormat.type_metadata(1))
output_pcm_data_format: int = field(metadata=PcmDataFormat.type_metadata(1))
input_pcm_sample_payload_msb_position: int = field(metadata=metadata(1))
output_pcm_sample_payload_msb_position: int = field(metadata=metadata(1))
input_data_path: int = field(metadata=metadata(1))
+3 -6
View File
@@ -44,6 +44,7 @@ from bumble.hci import (
CodecID,
CodingFormat,
HCI_Enhanced_Setup_Synchronous_Connection_Command,
PcmDataFormat,
)
# -----------------------------------------------------------------------------
@@ -1954,12 +1955,8 @@ class EscoParameters:
output_coding_format: CodingFormat = CodingFormat(CodecID.LINEAR_PCM)
input_coded_data_size: int = 16
output_coded_data_size: int = 16
input_pcm_data_format: (
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat
) = HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
output_pcm_data_format: (
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat
) = HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
input_pcm_data_format: PcmDataFormat = PcmDataFormat.TWOS_COMPLEMENT
output_pcm_data_format: PcmDataFormat = PcmDataFormat.TWOS_COMPLEMENT
input_pcm_sample_payload_msb_position: int = 0
output_pcm_sample_payload_msb_position: int = 0
input_data_path: HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath = (
+35 -3
View File
@@ -58,6 +58,8 @@ USB_BT_HCI_CLASS_TUPLE = (
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER,
)
MAX_SCO_PACKET_SIZE = 1024
# -----------------------------------------------------------------------------
def load_libusb():
@@ -227,7 +229,17 @@ class UsbPacketSink:
self.bulk_out = bulk_out
self.isochronous_out = isochronous_out
self.bulk_or_control_out_transfer = device.getTransfer()
self.isochronous_out_transfer = device.getTransfer(iso_packets=1)
self.isochronous_out_transfer = (
device.getTransfer(
iso_packets=(
MAX_SCO_PACKET_SIZE // isochronous_out.getMaxPacketSize()
if isochronous_out.getMaxPacketSize()
else 1
)
)
if isochronous_out is not None
else None
)
self.out_transfer_ready = asyncio.Semaphore(1)
self.packets: asyncio.Queue[bytes] = (
asyncio.Queue()
@@ -298,17 +310,29 @@ class UsbPacketSink:
self.bulk_or_control_out_transfer.submit()
submitted = True
elif packet_type == hci.HCI_SYNCHRONOUS_DATA_PACKET:
if self.isochronous_out is None:
if self.isochronous_out_transfer is None:
logger.warning(
color('isochronous packets not supported', 'red')
)
self.out_transfer_ready.release()
continue
# Setup a list of packet lengths, each up to the max packet size
iso_max_packet_size = self.isochronous_out.getMaxPacketSize()
iso_packet_count = (
len(packet_payload) + iso_max_packet_size - 1
) // iso_max_packet_size
iso_packet_lengths = [iso_max_packet_size] * (iso_packet_count - 1)
iso_packet_lengths.append(
len(packet_payload) - sum(iso_packet_lengths)
)
# Set up and submit the isochronous transfer
self.isochronous_out_transfer.setIsochronous(
self.isochronous_out.getAddress(),
packet_payload,
callback=self.transfer_callback,
iso_transfer_length_list=iso_packet_lengths,
)
self.isochronous_out_transfer.submit()
submitted = True
@@ -340,6 +364,9 @@ class UsbPacketSink:
self.bulk_or_control_out_transfer,
self.isochronous_out_transfer,
):
if transfer is None:
continue
if transfer.isSubmitted():
# Try to cancel the transfer, but that may fail because it may have
# already completed
@@ -352,6 +379,11 @@ class UsbPacketSink:
except usb1.USBError as error:
logger.debug(f'OUT transfer likely already completed ({error})')
try:
transfer.close()
except usb1.USBError as error:
logger.warning(f'failed to close transfer ({error})')
READ_SIZE = 4096
@@ -585,7 +617,7 @@ class UsbTransport(Transport):
sink.start()
# Create a thread to process events
self.event_thread = threading.Thread(target=self.run)
self.event_thread = threading.Thread(target=self.run, daemon=True)
self.event_thread.start()
def run(self):
+27 -27
View File
@@ -41,28 +41,27 @@ output_wav: wave.Wave_write | None = None
def on_audio_packet(packet: hci.HCI_SynchronousDataPacket) -> None:
if (
packet.packet_status
== hci.HCI_SynchronousDataPacket.Status.CORRECTLY_RECEIVED_DATA
!= hci.HCI_SynchronousDataPacket.Status.CORRECTLY_RECEIVED_DATA
):
if output_wav:
# Save the PCM audio to the output
output_wav.writeframes(packet.data)
else:
print('!!! discarding packet with status ', packet.packet_status.name)
return
frame_count = len(packet.data) // 2
print(f">>> received {frame_count} PCM samples")
if output_wav:
# Save the PCM audio to the output
output_wav.writeframes(packet.data)
if input_wav and hf_protocol:
# Send PCM audio from the input
frame_count = len(packet.data) // 2
while frame_count:
# NOTE: we use a fixed number of frames here, this should likely be adjusted
# based on the transport parameters (like the USB max packet size)
chunk_size = min(frame_count, 16)
if not (pcm_data := input_wav.readframes(chunk_size)):
return
frame_count -= chunk_size
hf_protocol.dlc.multiplexer.l2cap_channel.connection.device.host.send_sco_sdu(
connection_handle=packet.connection_handle,
sdu=pcm_data,
)
# Send PCM audio from the input, same amount as what was received
while not (pcm_data := input_wav.readframes(frame_count)):
input_wav.setpos(0) # Loop
print(f">>> sending {frame_count} PCM samples")
hf_protocol.dlc.multiplexer.l2cap_channel.connection.device.host.send_sco_sdu(
connection_handle=packet.connection_handle,
sdu=pcm_data,
)
# -----------------------------------------------------------------------------
@@ -123,6 +122,16 @@ def on_sco_request(
print('!!! no supported command for SCO connection request')
return
global output_wav
if output_wav:
output_wav.setnchannels(1)
output_wav.setsampwidth(2)
match protocol.active_codec:
case hfp.AudioCodec.CVSD:
output_wav.setframerate(8000)
case hfp.AudioCodec.MSBC:
output_wav.setframerate(16000)
connection.on('sco_connection', on_sco_connection)
@@ -159,15 +168,6 @@ def on_ag_indicator(indicator):
# -----------------------------------------------------------------------------
def on_codec_negotiation(codec: hfp.AudioCodec):
print(f'### Negotiated codec: {codec.name}')
global output_wav
if output_wav:
output_wav.setnchannels(1)
output_wav.setsampwidth(2)
match codec:
case hfp.AudioCodec.CVSD:
output_wav.setframerate(8000)
case hfp.AudioCodec.MSBC:
output_wav.setframerate(16000)
# -----------------------------------------------------------------------------