mirror of
https://github.com/google/bumble.git
synced 2026-06-03 07:57:03 +00:00
multiple packets per transfer
This commit is contained in:
+15
-13
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user