Skip to content

Commit

Permalink
Conversational
Browse files Browse the repository at this point in the history
  • Loading branch information
zxzxwu committed Dec 13, 2023
1 parent 829d4fd commit 70958f3
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 40 deletions.
12 changes: 12 additions & 0 deletions bumble/profiles/bap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'''
Expand Down Expand Up @@ -841,6 +848,7 @@ def on_cis_request(
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
if 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(
Expand All @@ -859,6 +867,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,
Expand Down Expand Up @@ -1016,6 +1027,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):
Expand Down
127 changes: 87 additions & 40 deletions examples/run_unicast_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,6 +39,8 @@
PacRecord,
PublishedAudioCapabilitiesService,
AudioStreamControlService,
AudioRole,
AseStateMachine,
)

from bumble.transport import open_transport_or_link
Expand All @@ -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
),
Expand Down Expand Up @@ -103,10 +106,34 @@ async def main() -> None:
),
),
],
source_audio_locations=(AudioLocation.FRONT_CENTER),
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(
Expand All @@ -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(
'<HHHHHHI',
18, # Header length.
24000 // 100, # Sampling Rate(/100Hz).
0, # Bitrate(unused).
1, # Channels.
10000 // 10, # Frame duration(/10us).
0, # RFU.
0x0FFFFFFF, # Frame counts.
)
)

def on_pdu(pdu: HCI_IsoDataPacket):
# LC3 format: |frame_length(2)| + |frame(length)|.
if pdu.iso_sdu_length:
stdin.write(struct.pack('<H', pdu.iso_sdu_length))
stdin.write(pdu.iso_sdu_fragment)

def on_cis(cis_link: CisLink):
cis_link.on('pdu', on_pdu)

device.once('cis_establishment', on_cis)
def on_streaming(ase: AseStateMachine):
print('on_streaming')

async def on_cis_async():
print('on_cis_async')
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(
'<HHHHHHI',
18, # Header length.
(
ase.codec_specific_configuration.sampling_frequency.hz
// 100
), # Sampling Rate(/100Hz).
0, # Bitrate(unused).
ase.codec_specific_configuration.audio_channel_allocation, # Channels.
(
ase.codec_specific_configuration.frame_duration.us // 10
), # Frame duration(/10us).
0, # RFU.
0x0FFFFFFF, # Frame counts.
)
)

def on_pdu(pdu: HCI_IsoDataPacket):
# LC3 format: |frame_length(2)| + |frame(length)|.
if pdu.iso_sdu_length:
stdin.write(struct.pack('<H', pdu.iso_sdu_length))
stdin.write(pdu.iso_sdu_fragment)

ase.cis_link.on('pdu', on_pdu)

device.abort_on('flush', on_cis_async())

for ase in ascs.ase_state_machines.values():
if ase.role == AudioRole.SINK:

def on_state_change(*_, ase: AseStateMachine):
print(ase)
if ase.state == AseStateMachine.State.STREAMING:
on_streaming(ase)

ase.on('state_change', functools.partial(on_state_change, ase=ase))

await device.start_extended_advertising(
advertising_properties=(
Expand Down

0 comments on commit 70958f3

Please sign in to comment.