From cca49d1be0ddffd97f17c2eca5b2d79e61ecc94f Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Wed, 13 Dec 2023 19:11:04 +0800 Subject: [PATCH] Conversational --- .vscode/settings.json | 1 + bumble/profiles/bap.py | 24 ++++++- examples/run_unicast_server.py | 127 ++++++++++++++++++++++----------- 3 files changed, 110 insertions(+), 42 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b564a38b1..d168bf21d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "ansired", "ansiyellow", "appendleft", + "ascs", "ASHA", "asyncio", "ATRAC", diff --git a/bumble/profiles/bap.py b/bumble/profiles/bap.py index 0386906d1..6cb0d29c9 100644 --- a/bumble/profiles/bap.py +++ b/bumble/profiles/bap.py @@ -217,6 +217,13 @@ class FrameDuration(enum.IntEnum): DURATION_7500_US = 0x00 DURATION_10000_US = 0x01 + @property + def us(self) -> int: + return { + FrameDuration.DURATION_7500_US: 7500, + FrameDuration.DURATION_10000_US: 10000, + }[self] + class SupportedFrameDuration(enum.IntFlag): '''Bluetooth Assigned Numbers, Section 6.12.4.2 - Frame Duration''' @@ -833,14 +840,23 @@ def on_cis_request( cig_id: int, cis_id: int, ) -> None: - if cis_id == self.cis_id and self.state == self.State.ENABLING: + if ( + cig_id == self.cig_id + and cis_id == self.cis_id + and self.state == self.State.ENABLING + ): acl_connection.abort_on( 'flush', self.service.device.accept_cis_request(cis_handle) ) def on_cis_establishment(self, cis_link: device.CisLink) -> None: - if cis_link.cis_id == self.cis_id and self.state == self.State.ENABLING: + if ( + cis_link.cig_id == self.cig_id + and cis_link.cis_id == self.cis_id + and self.state == self.State.ENABLING + ): self.cis_link = cis_link + self.cis_link.on('disconnection', self.on_cis_disconnection) async def post_cis_established(): await self.service.device.send_command( @@ -859,6 +875,9 @@ async def post_cis_established(): cis_link.acl_connection.abort_on('flush', post_cis_established()) + def on_cis_disconnection(self, _reason) -> None: + self.cis_link = None + def on_config_codec( self, target_latency: int, @@ -1016,6 +1035,7 @@ def state(self) -> State: def state(self, new_state: State) -> None: logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}') self._state = new_state + self.emit('state_change', new_state) @property def value(self): diff --git a/examples/run_unicast_server.py b/examples/run_unicast_server.py index e71cbeff9..70637dea4 100644 --- a/examples/run_unicast_server.py +++ b/examples/run_unicast_server.py @@ -20,8 +20,9 @@ import sys import os import struct +import functools from bumble.core import AdvertisingData -from bumble.device import Device, CisLink +from bumble.device import Device from bumble.hci import ( CodecID, CodingFormat, @@ -38,6 +39,8 @@ PacRecord, PublishedAudioCapabilitiesService, AudioStreamControlService, + AudioRole, + AseStateMachine, ) from bumble.transport import open_transport_or_link @@ -62,10 +65,10 @@ async def main() -> None: device.add_service( PublishedAudioCapabilitiesService( - supported_source_context=ContextType.PROHIBITED, - available_source_context=ContextType.PROHIBITED, - supported_sink_context=ContextType.MEDIA, - available_sink_context=ContextType.MEDIA, + supported_source_context=ContextType.CONVERSATIONAL, + available_source_context=ContextType.CONVERSATIONAL, + supported_sink_context=ContextType.MEDIA | ContextType.CONVERSATIONAL, + available_sink_context=ContextType.MEDIA | ContextType.CONVERSATIONAL, sink_audio_locations=( AudioLocation.FRONT_LEFT | AudioLocation.FRONT_RIGHT ), @@ -103,10 +106,34 @@ async def main() -> None: ), ), ], + source_audio_locations=(AudioLocation.FRONT_LEFT), + source_pac=[ + # Codec Capability Setting 16_2 + PacRecord( + coding_format=CodingFormat(CodecID.LC3), + codec_specific_capabilities=CodecSpecificCapabilities( + supported_sampling_frequencies=( + SupportedSamplingFrequency.FREQ_16000 + ), + supported_frame_durations=( + SupportedFrameDuration.DURATION_10000_US_SUPPORTED + ), + supported_audio_channel_counts=[1], + min_octets_per_codec_frame=40, + max_octets_per_codec_frame=40, + supported_max_codec_frames_per_sdu=1, + ), + ), + ], ) ) - device.add_service(AudioStreamControlService(device, sink_ase_id=[1, 2])) + ascs = AudioStreamControlService( + device, + sink_ase_id=[1, 2], + source_ase_id=[3], + ) + device.add_service(ascs) advertising_data = bytes( AdvertisingData( @@ -132,41 +159,61 @@ async def main() -> None: ] ) ) - subprocess = await asyncio.create_subprocess_shell( - f'dlc3 | ffplay pipe:0', - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - stdin = subprocess.stdin - assert stdin - - # Write a fake LC3 header to dlc3. - stdin.write( - bytes([0x1C, 0xCC]) # Header. - + struct.pack( - '